From 31e3a4c77b61dee239342ffbdd3fa0b2749dd8c4 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Thu, 18 Dec 2025 15:10:48 +0700 Subject: [PATCH 1/6] [Php81] Skip as Arg value on ArrayToFirstClassCallableRector --- .../Fixture/make_other_closure_pass.php.inc | 27 ------------------- ...value_with_caller_param_type_array.php.inc | 18 +++++++++++++ .../skip_as_service_factory_arg.php.inc | 11 ++++++++ .../ArrayCallableMethodMatcher.php | 4 +++ 4 files changed, 33 insertions(+), 27 deletions(-) delete mode 100644 rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/make_other_closure_pass.php.inc create mode 100644 rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_arg_value_with_caller_param_type_array.php.inc create mode 100644 rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_service_factory_arg.php.inc diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/make_other_closure_pass.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/make_other_closure_pass.php.inc deleted file mode 100644 index f125926e8bd..00000000000 --- a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/make_other_closure_pass.php.inc +++ /dev/null @@ -1,27 +0,0 @@ -services() - ->factory([SomeExternalObject::class, 'sleepStatic']); -}; - -?> ------ -services() - ->factory(SomeExternalObject::sleepStatic(...)); -}; - -?> diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_arg_value_with_caller_param_type_array.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_arg_value_with_caller_param_type_array.php.inc new file mode 100644 index 00000000000..857505b3513 --- /dev/null +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_arg_value_with_caller_param_type_array.php.inc @@ -0,0 +1,18 @@ +run([$this, 'some']); + } + + public function some() + {} +} \ No newline at end of file diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_service_factory_arg.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_service_factory_arg.php.inc new file mode 100644 index 00000000000..cd2c9ccf210 --- /dev/null +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_service_factory_arg.php.inc @@ -0,0 +1,11 @@ +services() + ->factory([SomeExternalObject::class, 'sleepStatic']); +}; diff --git a/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php b/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php index d655ca56113..0cf7995eabb 100644 --- a/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php +++ b/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php @@ -72,6 +72,10 @@ public function match( return null; } + if ($array->getAttribute(AttributeKey::IS_ARG_VALUE) === true) { + return null; + } + $values = $this->valueResolver->getValue($array); $className = $callerType->getClassName(); $secondItemValue = $items[1]->value; From 9a36e8a00eda116eb468f8b1e0dcb797043ca1c2 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 13 Feb 2026 07:23:05 +0700 Subject: [PATCH 2/6] fix --- .../ArrayCallableMethodMatcher.php | 4 +- src/NodeTypeResolver/Node/AttributeKey.php | 2 + .../NodeVisitor/ContextNodeVisitor.php | 54 ++++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php b/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php index 0cf7995eabb..6d0bc698972 100644 --- a/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php +++ b/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php @@ -72,7 +72,9 @@ public function match( return null; } - if ($array->getAttribute(AttributeKey::IS_ARG_VALUE) === true) { + if ($array->getAttribute(AttributeKey::IS_ARG_VALUE) === true && (bool) $array->getAttribute( + AttributeKey::IS_ARG_VALUE_CALLABLE + ) === false) { return null; } diff --git a/src/NodeTypeResolver/Node/AttributeKey.php b/src/NodeTypeResolver/Node/AttributeKey.php index e2256a4256f..5ab2f081e3e 100644 --- a/src/NodeTypeResolver/Node/AttributeKey.php +++ b/src/NodeTypeResolver/Node/AttributeKey.php @@ -166,4 +166,6 @@ final class AttributeKey public const string HAS_CLOSURE_WITH_VARIADIC_ARGS = 'has_closure_with_variadic_args'; public const string IS_IN_TRY_BLOCK = 'is_in_try_block'; + + public const string IS_ARG_VALUE_CALLABLE = 'is_arg_value_callable'; } diff --git a/src/PhpParser/NodeVisitor/ContextNodeVisitor.php b/src/PhpParser/NodeVisitor/ContextNodeVisitor.php index 4d147b6a9cb..0929091073a 100644 --- a/src/PhpParser/NodeVisitor/ContextNodeVisitor.php +++ b/src/PhpParser/NodeVisitor/ContextNodeVisitor.php @@ -10,6 +10,7 @@ use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\BinaryOp\BooleanAnd; +use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\Isset_; use PhpParser\Node\Expr\PostDec; @@ -19,6 +20,7 @@ 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\Stmt\Break_; @@ -36,15 +38,22 @@ use PhpParser\Node\Stmt\While_; use PhpParser\NodeVisitor; use PhpParser\NodeVisitorAbstract; +use PHPStan\Analyser\Scope; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\ArrayType; use Rector\Contract\PhpParser\DecoratingNodeVisitorInterface; use Rector\NodeTypeResolver\Node\AttributeKey; +use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper; use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser; use Rector\PhpParser\NodeTraverser\SimpleNodeTraverser; +use Rector\Reflection\ReflectionResolver; final class ContextNodeVisitor extends NodeVisitorAbstract implements DecoratingNodeVisitorInterface { public function __construct( - private readonly SimpleCallableNodeTraverser $simpleCallableNodeTraverser + private readonly SimpleCallableNodeTraverser $simpleCallableNodeTraverser, + private readonly ReflectionResolver $reflectionResolver ) { } @@ -92,6 +101,49 @@ public function enterNode(Node $node): ?Node return null; } + if ($node instanceof CallLike && ! $node->isFirstClassCallable()) { + $functionReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); + if (! $functionReflection instanceof FunctionReflection && ! $functionReflection instanceof MethodReflection) { + return null; + } + + $scope = $node->getAttribute(AttributeKey::SCOPE); + if (! $scope instanceof Scope) { + return null; + } + + $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select( + $functionReflection, + $node, + $scope + ); + + $args = $node->getArgs(); + foreach ($parametersAcceptor->getParameters() as $key => $parameterReflection) { + // also process maybe callable + if ($parameterReflection->getType()->isCallable()->no()) { + continue; + } + + if ($parameterReflection->getType() instanceof ArrayType) { + continue; + } + + // based on name + foreach ($args as $arg) { + if ($arg->name instanceof Identifier && $parameterReflection->getName() === $arg->name->toString()) { + $arg->value->setAttribute(AttributeKey::IS_ARG_VALUE_CALLABLE, true); + continue 2; + } + } + + // based on key + if (isset($args[$key])) { + $args[$key]->value->setAttribute(AttributeKey::IS_ARG_VALUE_CALLABLE, true); + } + } + } + if ($node instanceof Arg) { $node->value->setAttribute(AttributeKey::IS_ARG_VALUE, true); return null; From 112d316f7444ab6d0e5edff7c12d6a32742a94fc Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 13 Feb 2026 07:51:03 +0700 Subject: [PATCH 3/6] fix skip use on Symfony\Component\DependencyInjection\Loader\Configurator\ServiceConfigurator --- .../skip_as_service_factory_arg.php.inc | 11 --------- .../ArrayCallableMethodMatcher.php | 2 +- src/NodeTypeResolver/Node/AttributeKey.php | 2 +- .../NodeVisitor/ContextNodeVisitor.php | 23 +++++++++++-------- .../Configurator/ContainerConfigurator.php | 18 +++++++++++++++ .../Configurator/ServiceConfigurator.php | 18 +++++++++++++++ 6 files changed, 51 insertions(+), 23 deletions(-) delete mode 100644 rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_service_factory_arg.php.inc create mode 100644 stubs/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php create mode 100644 stubs/Symfony/Component/DependencyInjection/Loader/Configurator/ServiceConfigurator.php diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_service_factory_arg.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_service_factory_arg.php.inc deleted file mode 100644 index cd2c9ccf210..00000000000 --- a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_service_factory_arg.php.inc +++ /dev/null @@ -1,11 +0,0 @@ -services() - ->factory([SomeExternalObject::class, 'sleepStatic']); -}; diff --git a/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php b/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php index 6d0bc698972..ef76b7d47d3 100644 --- a/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php +++ b/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php @@ -73,7 +73,7 @@ public function match( } if ($array->getAttribute(AttributeKey::IS_ARG_VALUE) === true && (bool) $array->getAttribute( - AttributeKey::IS_ARG_VALUE_CALLABLE + AttributeKey::IS_ARG_VALUE_FACTORY_SERVICECONFIGURATOR ) === false) { return null; } diff --git a/src/NodeTypeResolver/Node/AttributeKey.php b/src/NodeTypeResolver/Node/AttributeKey.php index 5ab2f081e3e..080d8f776bd 100644 --- a/src/NodeTypeResolver/Node/AttributeKey.php +++ b/src/NodeTypeResolver/Node/AttributeKey.php @@ -167,5 +167,5 @@ final class AttributeKey public const string IS_IN_TRY_BLOCK = 'is_in_try_block'; - public const string IS_ARG_VALUE_CALLABLE = 'is_arg_value_callable'; + public const string IS_ARG_VALUE_FACTORY_SERVICECONFIGURATOR = 'is_arg_value_factory_serviceconfigurator'; } diff --git a/src/PhpParser/NodeVisitor/ContextNodeVisitor.php b/src/PhpParser/NodeVisitor/ContextNodeVisitor.php index 0929091073a..88d06d2d33c 100644 --- a/src/PhpParser/NodeVisitor/ContextNodeVisitor.php +++ b/src/PhpParser/NodeVisitor/ContextNodeVisitor.php @@ -103,7 +103,15 @@ public function enterNode(Node $node): ?Node if ($node instanceof CallLike && ! $node->isFirstClassCallable()) { $functionReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); - if (! $functionReflection instanceof FunctionReflection && ! $functionReflection instanceof MethodReflection) { + if (! $functionReflection instanceof MethodReflection) { + return null; + } + + if ($functionReflection->getDeclaringClass()->getName() !== 'Symfony\Component\DependencyInjection\Loader\Configurator\ServiceConfigurator') { + return null; + } + + if ($functionReflection->getName() !== 'factory') { return null; } @@ -120,26 +128,21 @@ public function enterNode(Node $node): ?Node $args = $node->getArgs(); foreach ($parametersAcceptor->getParameters() as $key => $parameterReflection) { - // also process maybe callable - if ($parameterReflection->getType()->isCallable()->no()) { - continue; - } - - if ($parameterReflection->getType() instanceof ArrayType) { + if ($parameterReflection->getName() !== 'factory') { continue; } // based on name foreach ($args as $arg) { - if ($arg->name instanceof Identifier && $parameterReflection->getName() === $arg->name->toString()) { - $arg->value->setAttribute(AttributeKey::IS_ARG_VALUE_CALLABLE, true); + if ($arg->name instanceof Identifier && 'factory' === $arg->name->toString()) { + $arg->value->setAttribute(AttributeKey::IS_ARG_VALUE_FACTORY_SERVICECONFIGURATOR, true); continue 2; } } // based on key if (isset($args[$key])) { - $args[$key]->value->setAttribute(AttributeKey::IS_ARG_VALUE_CALLABLE, true); + $args[$key]->value->setAttribute(AttributeKey::IS_ARG_VALUE_FACTORY_SERVICECONFIGURATOR, true); } } } diff --git a/stubs/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php b/stubs/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php new file mode 100644 index 00000000000..3c3fb6ad8a5 --- /dev/null +++ b/stubs/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php @@ -0,0 +1,18 @@ + Date: Fri, 13 Feb 2026 07:53:20 +0700 Subject: [PATCH 4/6] fix --- ..._value_with_caller_param_type_array.php.inc | 18 ------------------ .../ArrayCallableMethodMatcher.php | 4 +--- 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_arg_value_with_caller_param_type_array.php.inc diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_arg_value_with_caller_param_type_array.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_arg_value_with_caller_param_type_array.php.inc deleted file mode 100644 index 857505b3513..00000000000 --- a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_as_arg_value_with_caller_param_type_array.php.inc +++ /dev/null @@ -1,18 +0,0 @@ -run([$this, 'some']); - } - - public function some() - {} -} \ No newline at end of file diff --git a/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php b/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php index ef76b7d47d3..7f3a021e810 100644 --- a/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php +++ b/src/NodeCollector/NodeAnalyzer/ArrayCallableMethodMatcher.php @@ -72,9 +72,7 @@ public function match( return null; } - if ($array->getAttribute(AttributeKey::IS_ARG_VALUE) === true && (bool) $array->getAttribute( - AttributeKey::IS_ARG_VALUE_FACTORY_SERVICECONFIGURATOR - ) === false) { + if ($array->getAttribute(AttributeKey::IS_ARG_VALUE_FACTORY_SERVICECONFIGURATOR) === true) { return null; } From c796d4c192fc8b0071f0ecf7d7c53d0a3da83942 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 13 Feb 2026 00:55:18 +0000 Subject: [PATCH 5/6] [ci-review] Rector Rectify --- src/PhpParser/NodeVisitor/ContextNodeVisitor.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/PhpParser/NodeVisitor/ContextNodeVisitor.php b/src/PhpParser/NodeVisitor/ContextNodeVisitor.php index 88d06d2d33c..4c9d86becbd 100644 --- a/src/PhpParser/NodeVisitor/ContextNodeVisitor.php +++ b/src/PhpParser/NodeVisitor/ContextNodeVisitor.php @@ -39,9 +39,7 @@ use PhpParser\NodeVisitor; use PhpParser\NodeVisitorAbstract; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; -use PHPStan\Type\ArrayType; use Rector\Contract\PhpParser\DecoratingNodeVisitorInterface; use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper; From 11c85f9dbe45ab2cf8082d4372d9aae78bc5bcc1 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 13 Feb 2026 07:58:15 +0700 Subject: [PATCH 6/6] add back orig fixture --- .../Fixture/make_other_closure_pass.php.inc | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/make_other_closure_pass.php.inc diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/make_other_closure_pass.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/make_other_closure_pass.php.inc new file mode 100644 index 00000000000..f125926e8bd --- /dev/null +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/make_other_closure_pass.php.inc @@ -0,0 +1,27 @@ +services() + ->factory([SomeExternalObject::class, 'sleepStatic']); +}; + +?> +----- +services() + ->factory(SomeExternalObject::sleepStatic(...)); +}; + +?>