diff --git a/composer.json b/composer.json index 4a923688602d..241cd9fe6f1b 100644 --- a/composer.json +++ b/composer.json @@ -18,9 +18,9 @@ "jean85/pretty-package-versions": "^1.2", "nette/robot-loader": "^3.2", "nette/utils": "^3.1", - "nikic/php-parser": "4.6", + "nikic/php-parser": "^4.7", "ondram/ci-detector": "^3.4", - "phpstan/phpdoc-parser": "^0.4.7", + "phpstan/phpdoc-parser": "^0.4.8", "phpstan/phpstan-phpunit": "^0.12.10", "psr/simple-cache": "^1.0", "sebastian/diff": "^3.0|^4.0", @@ -37,7 +37,7 @@ "symplify/set-config-resolver": "^8.1.15", "symplify/smart-file-system": "^8.1.15", "tracy/tracy": "^2.7", - "phpstan/phpstan": "^0.12.33" + "phpstan/phpstan": "^0.12.34" }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.16", @@ -55,6 +55,9 @@ "symplify/phpstan-extensions": "^8.1.15", "thecodingmachine/phpstan-strict-rules": "^0.12" }, + "replace": { + "rector/rector-prefixed": "self.version" + }, "autoload": { "psr-4": { "Rector\\Architecture\\": "rules/architecture/src", diff --git a/config/set/php80.php b/config/set/php80.php index 07ab70e8371e..65c32947baf5 100644 --- a/config/set/php80.php +++ b/config/set/php80.php @@ -12,6 +12,7 @@ use Rector\Php80\Rector\Identical\StrEndsWithRector; use Rector\Php80\Rector\Identical\StrStartsWithRector; use Rector\Php80\Rector\NotIdentical\StrContainsRector; +use Rector\Php80\Rector\Switch_\ChangeSwitchToMatchRector; use Rector\Php80\Rector\Ternary\GetDebugTypeRector; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -39,4 +40,6 @@ $services->set(RemoveUnusedVariableInCatchRector::class); $services->set(ClassPropertyAssignToConstructorPromotionRector::class); + + $services->set(ChangeSwitchToMatchRector::class); }; diff --git a/docs/nodes_overview.md b/docs/nodes_overview.md index b7bc39bad165..591c1704b121 100644 --- a/docs/nodes_overview.md +++ b/docs/nodes_overview.md @@ -376,6 +376,25 @@ list($someVariable) * `$items` - `/** @var (ArrayItem|null)[] List of items to assign to */`
+### `PhpParser\Node\Expr\Match_` + + * requires arguments on construct + + +#### Example PHP Code + +```php +match ($variable) { + 1 => 'yes', +} +``` + +#### Public Properties + + * `$cond` - `/** @var Node\Expr */` + * `$arms` - `/** @var MatchArm[] */` +
+ ### `PhpParser\Node\Expr\MethodCall` * requires arguments on construct @@ -2509,6 +2528,23 @@ identifier * `$specialClassNames` - ``
+### `PhpParser\Node\MatchArm` + + * requires arguments on construct + + +#### Example PHP Code + +```php +1 => 'yes' +``` + +#### Public Properties + + * `$conds` - `/** @var null|Node\Expr[] */` + * `$body` - `/** @var Node\Expr */` +
+ ### `PhpParser\Node\NullableType` * requires arguments on construct diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index 27b429e755ea..04622d30a940 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# All 541 Rectors Overview +# All 542 Rectors Overview - [Projects](#projects) --- @@ -54,7 +54,7 @@ - [Php72](#php72) (11) - [Php73](#php73) (10) - [Php74](#php74) (15) -- [Php80](#php80) (11) +- [Php80](#php80) (12) - [PhpDeglobalize](#phpdeglobalize) (1) - [PhpSpecToPHPUnit](#phpspectophpunit) (7) - [Polyfill](#polyfill) (2) @@ -10559,6 +10559,47 @@ Change annotation to attribute

+### `ChangeSwitchToMatchRector` + +- class: [`Rector\Php80\Rector\Switch_\ChangeSwitchToMatchRector`](/../master/rules/php80/src/Rector/Switch_/ChangeSwitchToMatchRector.php) +- [test fixtures](/../master/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture) + +Change `switch()` to `match()` + +```diff + class SomeClass + { + public function run() + { +- $statement = switch ($this->lexer->lookahead['type']) { +- case Lexer::T_SELECT: +- $statement = $this->SelectStatement(); +- break; +- +- case Lexer::T_UPDATE: +- $statement = $this->UpdateStatement(); +- break; +- +- case Lexer::T_DELETE: +- $statement = $this->DeleteStatement(); +- break; +- +- default: +- $this->syntaxError('SELECT, UPDATE or DELETE'); +- break; +- } ++ $statement = match ($this->lexer->lookahead['type']) { ++ Lexer::T_SELECT => $this->SelectStatement(), ++ Lexer::T_UPDATE => $this->UpdateStatement(), ++ Lexer::T_DELETE => $this->DeleteStatement(), ++ default => $this->syntaxError('SELECT, UPDATE or DELETE'), ++ }; + } + } +``` + +

+ ### `ClassOnObjectRector` - class: [`Rector\Php80\Rector\FuncCall\ClassOnObjectRector`](/../master/rules/php80/src/Rector/FuncCall/ClassOnObjectRector.php) diff --git a/phpstan.neon b/phpstan.neon index e4e672f79463..e800ec3b97e5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -318,7 +318,7 @@ parameters: - '#Method (.*?) specified in iterable type Symfony\\Component\\Process\\Process#' - '#Cannot cast PhpParser\\Node\\Expr\\Error\|PhpParser\\Node\\Identifier to string#' - - '#Class cognitive complexity for "DumpNodesCommand" is 103, keep it under 50#' + - '#Class cognitive complexity for "DumpNodesCommand" is \d+, keep it under 50#' - '#Cognitive complexity for "Rector\\Utils\\DocumentationGenerator\\Command\\DumpNodesCommand\:\:execute\(\)" is \d+, keep it under 9#' - '#Method Rector\\Utils\\DocumentationGenerator\\Node\\NodeClassProvider\:\:getNodeClasses\(\) should return array but returns array#' @@ -379,3 +379,4 @@ parameters: - '#Static property Symplify\\PackageBuilder\\Tests\\AbstractKernelTestCase\:\:\$container \(Psr\\Container\\ContainerInterface\) does not accept Rector\\Naming\\Tests\\Rector\\Class_\\RenamePropertyToMatchTypeRector\\Source\\ContainerInterface\|Symfony\\Component\\DependencyInjection\\Container#' - '#Static property Rector\\Core\\Testing\\PHPUnit\\AbstractGenericRectorTestCase\:\:\$allRectorContainer \(Rector\\Naming\\Tests\\Rector\\Class_\\RenamePropertyToMatchTypeRector\\Source\\ContainerInterface\|Symfony\\Component\\DependencyInjection\\Container\|null\) does not accept Psr\\Container\\ContainerInterface#' - '#Separate function "Symfony\\Component\\DependencyInjection\\Loader\\Configurator\\ref\(\)" in method call to standalone row to improve readability#' + - '#Class with base "(.*?)" name is already used in "_HumbugBox(.*?)#' diff --git a/rules/php80/src/Rector/Switch_/ChangeSwitchToMatchRector.php b/rules/php80/src/Rector/Switch_/ChangeSwitchToMatchRector.php new file mode 100644 index 000000000000..5c5a28141858 --- /dev/null +++ b/rules/php80/src/Rector/Switch_/ChangeSwitchToMatchRector.php @@ -0,0 +1,195 @@ +lexer->lookahead['type']) { + case Lexer::T_SELECT: + $statement = $this->SelectStatement(); + break; + + case Lexer::T_UPDATE: + $statement = $this->UpdateStatement(); + break; + + case Lexer::T_DELETE: + $statement = $this->DeleteStatement(); + break; + + default: + $this->syntaxError('SELECT, UPDATE or DELETE'); + break; + } + } +} +PHP +, + <<<'PHP' +class SomeClass +{ + public function run() + { + $statement = match ($this->lexer->lookahead['type']) { + Lexer::T_SELECT => $this->SelectStatement(), + Lexer::T_UPDATE => $this->UpdateStatement(), + Lexer::T_DELETE => $this->DeleteStatement(), + default => $this->syntaxError('SELECT, UPDATE or DELETE'), + }; + } +} +PHP + + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [Switch_::class]; + } + + /** + * @param Switch_ $node + */ + public function refactor(Node $node): ?Node + { + $this->assignExpr = null; + + if (! $this->hasEachCaseBreak($node)) { + return null; + } + + if (! $this->hasSingleStmtCases($node)) { + return null; + } + + if (! $this->hasSingleAssignVariableInStmtCase($node)) { + return null; + } + + $matchArms = $this->createMatchArmsFromCases($node->cases); + $match = new Match_($node->cond, $matchArms); + if ($this->assignExpr) { + return new Assign($this->assignExpr, $match); + } + + return $match; + } + + private function hasEachCaseBreak(Switch_ $switch): bool + { + foreach ($switch->cases as $case) { + foreach ($case->stmts as $caseStmt) { + if (! $caseStmt instanceof Break_) { + continue; + } + + return true; + } + } + + return false; + } + + /** + * @param Case_[] $cases + * @return MatchArm[] + */ + private function createMatchArmsFromCases(array $cases): array + { + $matchArms = []; + foreach ($cases as $case) { + $stmt = $case->stmts[0]; + if (! $stmt instanceof Expression) { + throw new ShouldNotHappenException(); + } + + $expr = $stmt->expr; + + if ($expr instanceof Assign) { + $this->assignExpr = $expr->var; + $expr = $expr->expr; + } + + $condList = $case->cond === null ? null : [$case->cond]; + $matchArms[] = new MatchArm($condList, $expr); + } + + return $matchArms; + } + + private function hasSingleStmtCases(Switch_ $switch): bool + { + foreach ($switch->cases as $case) { + $stmtsWithoutBreak = array_filter($case->stmts, function (Node $node) { + return ! $node instanceof Break_; + }); + + if (count($stmtsWithoutBreak) !== 1) { + return false; + } + } + + return true; + } + + private function hasSingleAssignVariableInStmtCase(Switch_ $switch): bool + { + $assignVariableNames = []; + + foreach ($switch->cases as $case) { + /** @var Expression $onlyStmt */ + $onlyStmt = $case->stmts[0]; + $expr = $onlyStmt->expr; + + if (! $expr instanceof Assign) { + continue; + } + + $assignVariableNames[] = $this->getName($expr->var); + } + + $assignVariableNames = array_unique($assignVariableNames); + + return count($assignVariableNames) <= 1; + } +} diff --git a/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/ChangeSwitchToMatchRectorTest.php b/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/ChangeSwitchToMatchRectorTest.php new file mode 100644 index 000000000000..f9ea41dee5b8 --- /dev/null +++ b/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/ChangeSwitchToMatchRectorTest.php @@ -0,0 +1,31 @@ +doTestFileInfo($fileInfo); + } + + public function provideData(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return ChangeSwitchToMatchRector::class; + } +} diff --git a/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture/fixture.php.inc b/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture/fixture.php.inc new file mode 100644 index 000000000000..38de7e1d0b4b --- /dev/null +++ b/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture/fixture.php.inc @@ -0,0 +1,38 @@ +lexer->lookahead['type']) { + case Lexer::T_DELETE: + $statement = $this->DeleteStatement(); + break; + + default: + $this->syntaxError('SELECT, UPDATE or DELETE'); + break; + } + } +} + +?> +----- +lexer->lookahead['type']) { + Lexer::T_DELETE => $this->DeleteStatement(), + default => $this->syntaxError('SELECT, UPDATE or DELETE'), + }; + } +} + +?> diff --git a/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture/skip_2_and_more_stmts.php.inc b/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture/skip_2_and_more_stmts.php.inc new file mode 100644 index 000000000000..8e9e69872a37 --- /dev/null +++ b/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture/skip_2_and_more_stmts.php.inc @@ -0,0 +1,16 @@ +lexer->lookahead['type']) { + case Lexer::T_DELETE: + $statement = $this->DeleteStatement(); + $statement = $this->DeleteStatement(); + break; + } + } +} diff --git a/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture/skip_different_variable_assign.php.inc b/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture/skip_different_variable_assign.php.inc new file mode 100644 index 000000000000..815d93c63fe0 --- /dev/null +++ b/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture/skip_different_variable_assign.php.inc @@ -0,0 +1,17 @@ +lexer->lookahead['type']) { + case 'a': + $statementA = $this->DeleteStatement(); + case 'b': + $statementB = $this->DeleteStatement(); + break; + } + } +} diff --git a/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture/skip_missing_break.php.inc b/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture/skip_missing_break.php.inc new file mode 100644 index 000000000000..67958dbd4ed6 --- /dev/null +++ b/rules/php80/tests/Rector/Switch_/ChangeSwitchToMatchRector/Fixture/skip_missing_break.php.inc @@ -0,0 +1,14 @@ +lexer->lookahead['type']) { + case Lexer::T_DELETE: + $statement = $this->DeleteStatement(); + } + } +} diff --git a/utils/documentation-generator/src/Command/DumpNodesCommand.php b/utils/documentation-generator/src/Command/DumpNodesCommand.php index c9d09e384c19..04b3fd9d55c3 100644 --- a/utils/documentation-generator/src/Command/DumpNodesCommand.php +++ b/utils/documentation-generator/src/Command/DumpNodesCommand.php @@ -31,6 +31,7 @@ use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Expr\Isset_; use PhpParser\Node\Expr\List_; +use PhpParser\Node\Expr\Match_; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\PostDec; @@ -48,6 +49,7 @@ use PhpParser\Node\Expr\Variable; use PhpParser\Node\Expr\YieldFrom; use PhpParser\Node\Identifier; +use PhpParser\Node\MatchArm; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\NullableType; @@ -244,6 +246,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $useUseNode = new UseUse(new Name('UsedNamespace')); $someVariableNode = new Variable(self::SOME_VARIABLE); + $matchArm = new MatchArm([new LNumber(1)], new String_('yes')); + if ($nodeClass === NullableType::class) { $node = new NullableType('SomeType'); } elseif ($nodeClass === Const_::class) { @@ -270,6 +274,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $node = new TraitUse([new Name('trait')]); } elseif ($nodeClass === Switch_::class) { $node = new Switch_(new Variable(self::VARIABLE), [new Case_(new LNumber(1))]); + } elseif ($nodeClass === Match_::class) { + $node = new Match_(new Variable(self::VARIABLE), [$matchArm]); + } elseif ($nodeClass === MatchArm::class) { + $node = $matchArm; } elseif ($nodeClass === Echo_::class) { $node = new Echo_([new String_('hello')]); } elseif ($nodeClass === StaticVar::class) {