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) {