From 3db50b800ca1ac4d8077cec406a94cdfa4de6178 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 30 Jun 2026 09:03:10 +0200 Subject: [PATCH] [Php81] Skip ArrayToFirstClassCallableRector on serialized callable array keys and Definition::setFactory() --- .../Fixture/skip_callback_keyed_array.php.inc | 17 +++++ .../Fixture/skip_factory_keyed_array.php.inc | 17 +++++ .../Fixture/skip_set_factory_argument.php.inc | 17 +++++ ...skip_validation_groups_keyed_array.php.inc | 17 +++++ .../ArrayToFirstClassCallableRector.php | 4 ++ src/NodeTypeResolver/Node/AttributeKey.php | 6 ++ .../NodeVisitor/ContextNodeVisitor.php | 63 +++++++++++++++++++ 7 files changed, 141 insertions(+) create mode 100644 rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_callback_keyed_array.php.inc create mode 100644 rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_factory_keyed_array.php.inc create mode 100644 rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_set_factory_argument.php.inc create mode 100644 rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_validation_groups_keyed_array.php.inc diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_callback_keyed_array.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_callback_keyed_array.php.inc new file mode 100644 index 00000000000..b90bec635b9 --- /dev/null +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_callback_keyed_array.php.inc @@ -0,0 +1,17 @@ + [$this, 'name'], + ]; + } + + public function name() + { + } +} diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_factory_keyed_array.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_factory_keyed_array.php.inc new file mode 100644 index 00000000000..aded529f7c7 --- /dev/null +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_factory_keyed_array.php.inc @@ -0,0 +1,17 @@ + [$this, 'name'], + ]; + } + + public function name() + { + } +} diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_set_factory_argument.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_set_factory_argument.php.inc new file mode 100644 index 00000000000..6c6a3300e76 --- /dev/null +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_set_factory_argument.php.inc @@ -0,0 +1,17 @@ +setFactory([$this, 'name']); + } + + public function name() + { + } +} diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_validation_groups_keyed_array.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_validation_groups_keyed_array.php.inc new file mode 100644 index 00000000000..0d39bf7f41c --- /dev/null +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_validation_groups_keyed_array.php.inc @@ -0,0 +1,17 @@ + [$this, 'name'], + ]; + } + + public function name() + { + } +} diff --git a/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php b/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php index c13e89d1aa4..30609679dce 100644 --- a/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php +++ b/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php @@ -94,6 +94,10 @@ public function refactor(Node $node): StaticCall|MethodCall|null return null; } + if ($node->getAttribute(AttributeKey::IS_ARRAY_AS_STRING_CALLABLE)) { + return null; + } + $scope = ScopeFetcher::fetch($node); $arrayCallable = $this->arrayCallableMethodMatcher->match($node, $scope); diff --git a/src/NodeTypeResolver/Node/AttributeKey.php b/src/NodeTypeResolver/Node/AttributeKey.php index 24a9ed109e0..33f40638ebe 100644 --- a/src/NodeTypeResolver/Node/AttributeKey.php +++ b/src/NodeTypeResolver/Node/AttributeKey.php @@ -157,6 +157,12 @@ final class AttributeKey public const string IS_INSIDE_SYMFONY_PHP_CLOSURE = 'is_inside_symfony_php_closure'; + /** + * Array callable kept as data, not converted to first class callable, + * e.g. 'callback'/'factory' keyed array item, or Definition::setFactory() argument + */ + public const string IS_ARRAY_AS_STRING_CALLABLE = 'is_array_as_string_callable'; + public const string IS_INSIDE_BYREF_FUNCTION_LIKE = 'is_inside_byref_function_like'; public const string CLASS_CONST_FETCH_NAME = 'class_const_fetch_name'; diff --git a/src/PhpParser/NodeVisitor/ContextNodeVisitor.php b/src/PhpParser/NodeVisitor/ContextNodeVisitor.php index 94c53d446ab..d3652bdff79 100644 --- a/src/PhpParser/NodeVisitor/ContextNodeVisitor.php +++ b/src/PhpParser/NodeVisitor/ContextNodeVisitor.php @@ -6,12 +6,14 @@ use PhpParser\Node; use PhpParser\Node\Arg; +use PhpParser\Node\ArrayItem; use PhpParser\Node\Attribute; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\BinaryOp\BooleanAnd; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\Isset_; +use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\PostDec; use PhpParser\Node\Expr\PostInc; use PhpParser\Node\Expr\PreDec; @@ -19,8 +21,10 @@ use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Identifier; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Param; +use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Do_; @@ -87,6 +91,16 @@ public function enterNode(Node $node): ?Node return null; } + if ($node instanceof Array_) { + $this->processArrayInSerializedCallableKey($node); + return null; + } + + if ($node instanceof MethodCall) { + $this->processSetFactoryArgument($node); + return null; + } + if ($node instanceof If_ || $node instanceof Else_ || $node instanceof ElseIf_) { $this->processContextInIf($node); return null; @@ -140,6 +154,55 @@ private function processContextInClass(Node $node): void } } + /** + * The array is data, not a callable to convert: 'callback'/'factory' keyed item holds + * a [$value, 'method'] pair likely to be serialized as a string. + */ + private function processArrayInSerializedCallableKey(Array_ $array): void + { + foreach ($array->items as $arrayItem) { + if (! $arrayItem instanceof ArrayItem) { + continue; + } + + if (! $arrayItem->key instanceof String_) { + continue; + } + + if (! in_array($arrayItem->key->value, ['callback', 'factory', 'validation_groups'], true)) { + continue; + } + + if ($arrayItem->value instanceof Array_) { + $arrayItem->value->setAttribute(AttributeKey::IS_ARRAY_AS_STRING_CALLABLE, true); + } + } + } + + /** + * Symfony's Definition::setFactory() does not accept a first class callable, + * keep the [$value, 'method'] array as is. + */ + private function processSetFactoryArgument(MethodCall $methodCall): void + { + if (! $methodCall->name instanceof Identifier) { + return; + } + + if ($methodCall->name->toString() !== 'setFactory') { + return; + } + + $firstArg = $methodCall->args[0] ?? null; + if (! $firstArg instanceof Arg) { + return; + } + + if ($firstArg->value instanceof Array_) { + $firstArg->value->setAttribute(AttributeKey::IS_ARRAY_AS_STRING_CALLABLE, true); + } + } + private function processContextInAttribute(Attribute $attribute): void { $this->simpleCallableNodeTraverser->traverseNodesWithCallable(