diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 022fa37b..af086c7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,13 +2,9 @@ name: CI on: push: - branches: - - 3.x - - 4.x - - 5.x + branches: [3.x, 4.x, 5.x] pull_request: - branches: - - '*' + branches: ['*'] workflow_dispatch: jobs: @@ -17,92 +13,80 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['8.1'] + php-version: ['8.1', '8.2', '8.3', '8.4'] prefer-lowest: [''] include: - php-version: '8.1' prefer-lowest: 'prefer-lowest' steps: - - uses: actions/checkout@v5 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - extensions: mbstring, intl - coverage: pcov - - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Get date part for cache key - id: key-date - run: echo "::set-output name=date::$(date +'%Y-%m')" - - - name: Cache composer dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} - - - name: Composer Install - run: | - if ${{ matrix.prefer-lowest == 'prefer-lowest' }}; then - make install-dev-lowest - elif ${{ matrix.php-version == '8.1' }}; then - make install-dev-ignore-reqs - else - make install-dev - fi - - - name: Setup problem matchers for PHPUnit - if: matrix.php-version == '8.1' - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - - name: Run PHPUnit - run: | - if [[ ${{ matrix.php-version }} == '8.1' ]]; then - export CODECOVERAGE=1 && vendor/bin/phpunit --display-incomplete --display-skipped --coverage-clover=coverage.xml - else - vendor/bin/phpunit - fi - - - name: Submit code coverage - if: matrix.php-version == '8.1' - uses: codecov/codecov-action@v5 + - uses: actions/checkout@v5 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, intl + coverage: pcov + + - name: Cache composer dependencies + id: composer-cache + uses: actions/cache@v4 + with: + path: ~/.composer/cache + key: ${{ runner.os }}-composer-${{ matrix.php-version }}-${{ matrix.prefer-lowest }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer-${{ matrix.php-version }}-${{ matrix.prefer-lowest }}- + + - name: Composer install + run: | + if [[ "${{ matrix.prefer-lowest }}" == "prefer-lowest" ]]; then + make install-dev-lowest + elif [[ "${{ matrix.php-version }}" == "8.1" ]]; then + make install-dev-ignore-reqs + else + make install-dev + fi + + - name: Setup problem matchers for PHPUnit + if: matrix.php-version == '8.1' + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Run PHPUnit + run: | + if [[ "${{ matrix.php-version }}" == "8.1" && "${{ matrix.prefer-lowest }}" != "prefer-lowest" ]]; then + export CODECOVERAGE=1 + vendor/bin/phpunit --display-incomplete --display-skipped --coverage-clover=coverage.xml + else + vendor/bin/phpunit + fi + + - name: Submit code coverage + if: matrix.php-version == '8.1' && matrix.prefer-lowest != 'prefer-lowest' + uses: codecov/codecov-action@v5 cs-stan: name: Coding Standard & Static Analysis runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v5 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.1' - extensions: mbstring, intl - coverage: none - - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Get date part for cache key - id: key-date - run: echo "::set-output name=date::$(date +'%Y-%m')" - - - name: Cache composer dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} - - - name: Composer install - run: make install-dev - - - name: Run PHP CodeSniffer - run: vendor/bin/phpcs --report=checkstyle + - uses: actions/checkout@v5 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + extensions: mbstring, intl + coverage: none + + - name: Cache composer dependencies + uses: actions/cache@v4 + with: + path: ~/.composer/cache + key: ${{ runner.os }}-composer-8.1-${{ hashFiles('**/composer.lock') }} + + - name: Composer install + run: make install-dev + + - name: Run PHP CodeSniffer + run: vendor/bin/phpcs --report=checkstyle diff --git a/config/rector/sets/cakephp60.php b/config/rector/sets/cakephp60.php index 6410f218..13ee95ed 100644 --- a/config/rector/sets/cakephp60.php +++ b/config/rector/sets/cakephp60.php @@ -2,6 +2,7 @@ declare(strict_types=1); use Cake\Upgrade\Rector\Rector\MethodCall\EventManagerOnRector; +use Cake\Upgrade\Rector\Rector\MethodCall\ReplaceCommandArgsIoWithPropertiesRector; use PHPStan\Type\ObjectType; use Rector\Config\RectorConfig; use Rector\Renaming\Rector\MethodCall\RenameMethodRector; @@ -20,6 +21,8 @@ // EventManager::on() signature change $rectorConfig->rule(EventManagerOnRector::class); + $rectorConfig->rule(ReplaceCommandArgsIoWithPropertiesRector::class); + // Changes related to the accessible => patchable rename $rectorConfig->ruleWithConfiguration(RenameMethodRector::class, [ new MethodCallRename('Cake\ORM\Entity', 'setAccess', 'setPatchable'), diff --git a/src/Rector/Rector/MethodCall/ReplaceCommandArgsIoWithPropertiesRector.php b/src/Rector/Rector/MethodCall/ReplaceCommandArgsIoWithPropertiesRector.php new file mode 100644 index 00000000..b447ac4e --- /dev/null +++ b/src/Rector/Rector/MethodCall/ReplaceCommandArgsIoWithPropertiesRector.php @@ -0,0 +1,152 @@ +args` and `$this->io`', + [ + new CodeSample( + <<<'CODE_SAMPLE' +class TestCommand extends Command +{ + public function execute(Arguments $args, ConsoleIo $io) + { + $io->out('Hello'); + $this->someMethod($args, $io); + } + + protected function someMethod(Arguments $args, ConsoleIo $io): void + { + $someArg = $args->getArgument('some'); + $io->warning('Warn'); + } +} +CODE_SAMPLE, + <<<'CODE_SAMPLE' +class TestCommand extends Command +{ + public function execute() + { + $this->io->out('Hello'); + $this->someMethod(); + } + + protected function someMethod(): void + { + $someArg = $this->args->getArgument('some'); + $this->io->warning('Warn'); + } +} +CODE_SAMPLE, + ), + ], + ); + } + + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + public function refactor(Node $node): ?Node + { + if (! $node instanceof ClassMethod) { + return null; + } + + // Make sure we are in a class + $scope = ScopeFetcher::fetch($node); + if (!$scope->isInClass()) { + return null; + } + $class = $scope->getClassReflection(); + + // Skip if class doesn't extend Command (you can expand to check parent name) + $baseCommandClass = $this->reflectionProvider->getClass(Command::class); + if ($class->getName() === Command::class || $class->isSubclassOfClass($baseCommandClass) === false) { + return null; + } + + // Find if params are $args and/or $io + $argsParam = $this->findParam($node, 'args'); + $ioParam = $this->findParam($node, 'io'); + + if (! $argsParam && ! $ioParam) { + return null; + } + + // Replace all `$args` and `$io` usages inside the method body + $this->traverseNodesWithCallable($node->stmts ?? [], function (Node $innerNode) use ($argsParam, $ioParam) { + // Replace `$args` and `$io` variables + if ($innerNode instanceof Variable) { + if ($argsParam && $innerNode->name === 'args') { + return new PropertyFetch(new Variable('this'), 'args'); + } + + if ($ioParam && $innerNode->name === 'io') { + return new PropertyFetch(new Variable('this'), 'io'); + } + } + + // Remove `$args` / `$io` from method calls on `$this` + if ( + $innerNode instanceof MethodCall + && $innerNode->var instanceof Variable + && $innerNode->var->name === 'this' + ) { + $innerNode->args = array_values(array_filter( + $innerNode->args, + fn(Node\Arg $arg) => !($arg->value instanceof Variable && + in_array($arg->value->name, ['args', 'io'], true)), + )); + + return $innerNode; + } + + return null; + }); + + // Remove the parameters themselves + $node->params = array_filter($node->params, function (Param $param) { + return !in_array($this->getName($param), ['args', 'io'], true); + }); + + return $node; + } + + private function findParam(ClassMethod $method, string $name): ?Param + { + foreach ($method->params as $param) { + if ($this->getName($param) === $name) { + return $param; + } + } + + return null; + } +} diff --git a/tests/TestCase/Rector/MethodCall/ReplaceCommandArgsIoWithPorpertiesRector/Fixture/fixture.php.inc b/tests/TestCase/Rector/MethodCall/ReplaceCommandArgsIoWithPorpertiesRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..40928ced --- /dev/null +++ b/tests/TestCase/Rector/MethodCall/ReplaceCommandArgsIoWithPorpertiesRector/Fixture/fixture.php.inc @@ -0,0 +1,49 @@ +out('Hello World'); + $this->someMethod($args, $io); + return static::CODE_SUCCESS; + } + + protected function someMethod(Arguments $args, ConsoleIo $io): void + { + $someArg = $args->getArgument('some'); + $io->warning('Warning'); + } +} +?> +----- +io->out('Hello World'); + $this->someMethod(); + return static::CODE_SUCCESS; + } + + protected function someMethod(): void + { + $someArg = $this->args->getArgument('some'); + $this->io->warning('Warning'); + } +} +?> diff --git a/tests/TestCase/Rector/MethodCall/ReplaceCommandArgsIoWithPorpertiesRector/ReplaceCommandArgsIoWithPropertiesRectorTest.php b/tests/TestCase/Rector/MethodCall/ReplaceCommandArgsIoWithPorpertiesRector/ReplaceCommandArgsIoWithPropertiesRectorTest.php new file mode 100644 index 00000000..24b1fe95 --- /dev/null +++ b/tests/TestCase/Rector/MethodCall/ReplaceCommandArgsIoWithPorpertiesRector/ReplaceCommandArgsIoWithPropertiesRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/TestCase/Rector/MethodCall/ReplaceCommandArgsIoWithPorpertiesRector/config/configured_rule.php b/tests/TestCase/Rector/MethodCall/ReplaceCommandArgsIoWithPorpertiesRector/config/configured_rule.php new file mode 100644 index 00000000..f492ea49 --- /dev/null +++ b/tests/TestCase/Rector/MethodCall/ReplaceCommandArgsIoWithPorpertiesRector/config/configured_rule.php @@ -0,0 +1,9 @@ +rule(ReplaceCommandArgsIoWithPropertiesRector::class); +}; diff --git a/tests/test_apps/original/RectorCommand-testApply60/src/Command/TestCommand.php b/tests/test_apps/original/RectorCommand-testApply60/src/Command/TestCommand.php index d307acac..d4708c52 100644 --- a/tests/test_apps/original/RectorCommand-testApply60/src/Command/TestCommand.php +++ b/tests/test_apps/original/RectorCommand-testApply60/src/Command/TestCommand.php @@ -12,7 +12,13 @@ class TestCommand extends Command public function execute(Arguments $args, ConsoleIo $io) { $io->out('Hello World'); - + $this->someMethod($args, $io); return static::CODE_SUCCESS; } + + protected function someMethod(Arguments $args, ConsoleIo $io): void + { + $someArg = $args->getArgument('some'); + $io->warning('Warning'); + } } diff --git a/tests/test_apps/upgraded/RectorCommand-testApply60/src/Command/TestCommand.php b/tests/test_apps/upgraded/RectorCommand-testApply60/src/Command/TestCommand.php index 164a76fa..d29ffb01 100644 --- a/tests/test_apps/upgraded/RectorCommand-testApply60/src/Command/TestCommand.php +++ b/tests/test_apps/upgraded/RectorCommand-testApply60/src/Command/TestCommand.php @@ -9,10 +9,16 @@ class TestCommand extends Command { - public function execute(Arguments $args, \Cake\Console\ConsoleIoInterface $io) + public function execute() { - $io->out('Hello World'); - + $this->io->out('Hello World'); + $this->someMethod(); return static::CODE_SUCCESS; } + + protected function someMethod(): void + { + $someArg = $this->args->getArgument('some'); + $this->io->warning('Warning'); + } }