From dd7c4bfa28375725913f68bd52d5fd4491114e1e Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 1 Dec 2025 23:15:23 +0100 Subject: [PATCH 1/2] [cleanup] Make FirstClassCallableRector use attributes over node traverser hacks --- .../Array_/FirstClassCallableRector.php | 21 +++++----- .../LazyContainerFactory.php | 2 + src/NodeTypeResolver/Node/AttributeKey.php | 4 ++ ...PropertyOrClassConstDefaultNodeVisitor.php | 38 +++++++++++++++++++ 4 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/PropertyOrClassConstDefaultNodeVisitor.php diff --git a/rules/Php81/Rector/Array_/FirstClassCallableRector.php b/rules/Php81/Rector/Array_/FirstClassCallableRector.php index a870664c6c4..5fbe5dfa85c 100644 --- a/rules/Php81/Rector/Array_/FirstClassCallableRector.php +++ b/rules/Php81/Rector/Array_/FirstClassCallableRector.php @@ -12,8 +12,6 @@ use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Stmt\ClassConst; -use PhpParser\Node\Stmt\Property; use PhpParser\Node\VariadicPlaceholder; use PhpParser\NodeVisitor; use PHPStan\Analyser\Scope; @@ -21,6 +19,7 @@ use PHPStan\Reflection\ReflectionProvider; use Rector\NodeCollector\NodeAnalyzer\ArrayCallableMethodMatcher; use Rector\NodeCollector\ValueObject\ArrayCallable; +use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\PHPStan\ScopeFetcher; use Rector\Rector\AbstractRector; use Rector\Reflection\ReflectionResolver; @@ -86,14 +85,14 @@ public function name() */ public function getNodeTypes(): array { - return [Property::class, ClassConst::class, Array_::class, Closure::class]; + return [Array_::class, Closure::class]; } /** - * @param Property|ClassConst|Array_|Closure $node + * @param Array_|Closure $node * @return StaticCall|MethodCall|null|NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN */ - public function refactor(Node $node): int|null|StaticCall|MethodCall + public function refactor(Node $node): StaticCall|MethodCall|null|int { if ($node instanceof Closure) { if ($this->symfonyPhpClosureDetector->detect($node)) { @@ -103,10 +102,6 @@ public function refactor(Node $node): int|null|StaticCall|MethodCall return null; } - if ($node instanceof Property || $node instanceof ClassConst) { - return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; - } - $scope = ScopeFetcher::fetch($node); $arrayCallable = $this->arrayCallableMethodMatcher->match($node, $scope); @@ -119,6 +114,14 @@ public function refactor(Node $node): int|null|StaticCall|MethodCall return null; } + if ($node->getAttribute(AttributeKey::IS_DEFAULT_CLASS_CONST_VALUE)) { + return null; + } + + if ($node->getAttribute(AttributeKey::IS_DEFAULT_PROPERTY_VALUE)) { + return null; + } + $args = [new VariadicPlaceholder()]; if ($callerExpr instanceof ClassConstFetch) { $type = $this->getType($callerExpr->class); diff --git a/src/DependencyInjection/LazyContainerFactory.php b/src/DependencyInjection/LazyContainerFactory.php index 5279e7394d3..d23a85c1610 100644 --- a/src/DependencyInjection/LazyContainerFactory.php +++ b/src/DependencyInjection/LazyContainerFactory.php @@ -102,6 +102,7 @@ use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\ContextNodeVisitor; use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\GlobalVariableNodeVisitor; use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\NameNodeVisitor; +use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\PropertyOrClassConstDefaultNodeVisitor; use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\StaticVariableNodeVisitor; use Rector\NodeTypeResolver\PHPStan\Scope\PHPStanNodeScopeResolver; use Rector\NodeTypeResolver\Reflection\BetterReflection\SourceLocatorProvider\DynamicSourceLocatorProvider; @@ -243,6 +244,7 @@ final class LazyContainerFactory GlobalVariableNodeVisitor::class, NameNodeVisitor::class, StaticVariableNodeVisitor::class, + PropertyOrClassConstDefaultNodeVisitor::class, ]; /** diff --git a/src/NodeTypeResolver/Node/AttributeKey.php b/src/NodeTypeResolver/Node/AttributeKey.php index a685d2bbc9c..29db12effe9 100644 --- a/src/NodeTypeResolver/Node/AttributeKey.php +++ b/src/NodeTypeResolver/Node/AttributeKey.php @@ -286,4 +286,8 @@ final class AttributeKey * @var string */ public const HAS_MERGED_COMMENTS = 'has_merged_comments'; + + public const IS_DEFAULT_PROPERTY_VALUE = 'is_default_property_value'; + + public const IS_DEFAULT_CLASS_CONST_VALUE = 'is_default_class_const_value'; } diff --git a/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/PropertyOrClassConstDefaultNodeVisitor.php b/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/PropertyOrClassConstDefaultNodeVisitor.php new file mode 100644 index 00000000000..1016ae5d4ea --- /dev/null +++ b/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/PropertyOrClassConstDefaultNodeVisitor.php @@ -0,0 +1,38 @@ +props as $propertyItem) { + $default = $propertyItem->default; + if (! $default instanceof Expr) { + continue; + } + + $default->setAttribute(AttributeKey::IS_DEFAULT_PROPERTY_VALUE, true); + } + } + + if ($node instanceof ClassConst) { + foreach ($node->consts as $const) { + $const->value->setAttribute(AttributeKey::IS_DEFAULT_CLASS_CONST_VALUE, true); + } + } + + return null; + } +} From 281f25edde37578bfd99436dd1593e498b5fd8c8 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 1 Dec 2025 23:54:07 +0100 Subject: [PATCH 2/2] handle nested arrays as well --- .../Fixture/skip_on_class_const.php.inc | 4 ++ .../Fixture/skip_on_property.php.inc | 4 ++ .../Array_/FirstClassCallableRector.php | 4 +- src/NodeTypeResolver/Node/AttributeKey.php | 2 +- ...PropertyOrClassConstDefaultNodeVisitor.php | 5 ++- .../NodeTraverser/SimpleTraverser.php | 38 +++++++++++++++++++ 6 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 src/PhpParser/NodeTraverser/SimpleTraverser.php diff --git a/rules-tests/Php81/Rector/Array_/FirstClassCallableRector/Fixture/skip_on_class_const.php.inc b/rules-tests/Php81/Rector/Array_/FirstClassCallableRector/Fixture/skip_on_class_const.php.inc index deae97c19b1..42ecfe37351 100644 --- a/rules-tests/Php81/Rector/Array_/FirstClassCallableRector/Fixture/skip_on_class_const.php.inc +++ b/rules-tests/Php81/Rector/Array_/FirstClassCallableRector/Fixture/skip_on_class_const.php.inc @@ -6,6 +6,10 @@ final class SkipOnClassConst { private const CLASS_CONST = [SkipOnClassConst::class, 'size']; + private const CLASS_CONST_NESTED = [ + 'hey' => [SkipOnClassConst::class, 'size'] + ]; + public function size() { } diff --git a/rules-tests/Php81/Rector/Array_/FirstClassCallableRector/Fixture/skip_on_property.php.inc b/rules-tests/Php81/Rector/Array_/FirstClassCallableRector/Fixture/skip_on_property.php.inc index c3af789fb95..a3e2eb68b09 100644 --- a/rules-tests/Php81/Rector/Array_/FirstClassCallableRector/Fixture/skip_on_property.php.inc +++ b/rules-tests/Php81/Rector/Array_/FirstClassCallableRector/Fixture/skip_on_property.php.inc @@ -6,6 +6,10 @@ final class SkipOnProperty { private $prop = [SkipOnProperty::class, 'size']; + private $nestedProperty = [ + 'some_callback' => [SkipOnProperty::class, 'size'] + ]; + public function size() { } diff --git a/rules/Php81/Rector/Array_/FirstClassCallableRector.php b/rules/Php81/Rector/Array_/FirstClassCallableRector.php index 5fbe5dfa85c..5f054846570 100644 --- a/rules/Php81/Rector/Array_/FirstClassCallableRector.php +++ b/rules/Php81/Rector/Array_/FirstClassCallableRector.php @@ -31,6 +31,7 @@ use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; /** + * @see RFC https://wiki.php.net/rfc/first_class_callable_syntax * @see \Rector\Tests\Php81\Rector\Array_\FirstClassCallableRector\FirstClassCallableRectorTest */ final class FirstClassCallableRector extends AbstractRector implements MinPhpVersionInterface @@ -45,7 +46,6 @@ public function __construct( public function getRuleDefinition(): RuleDefinition { - // see RFC https://wiki.php.net/rfc/first_class_callable_syntax return new RuleDefinition('Upgrade array callable to first class callable', [ new CodeSample( <<<'CODE_SAMPLE' @@ -114,7 +114,7 @@ public function refactor(Node $node): StaticCall|MethodCall|null|int return null; } - if ($node->getAttribute(AttributeKey::IS_DEFAULT_CLASS_CONST_VALUE)) { + if ($node->getAttribute(AttributeKey::IS_CLASS_CONST_VALUE)) { return null; } diff --git a/src/NodeTypeResolver/Node/AttributeKey.php b/src/NodeTypeResolver/Node/AttributeKey.php index 29db12effe9..7178abf2109 100644 --- a/src/NodeTypeResolver/Node/AttributeKey.php +++ b/src/NodeTypeResolver/Node/AttributeKey.php @@ -289,5 +289,5 @@ final class AttributeKey public const IS_DEFAULT_PROPERTY_VALUE = 'is_default_property_value'; - public const IS_DEFAULT_CLASS_CONST_VALUE = 'is_default_class_const_value'; + public const IS_CLASS_CONST_VALUE = 'is_default_class_const_value'; } diff --git a/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/PropertyOrClassConstDefaultNodeVisitor.php b/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/PropertyOrClassConstDefaultNodeVisitor.php index 1016ae5d4ea..85e953d57b2 100644 --- a/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/PropertyOrClassConstDefaultNodeVisitor.php +++ b/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/PropertyOrClassConstDefaultNodeVisitor.php @@ -11,6 +11,7 @@ use PhpParser\NodeVisitorAbstract; use Rector\Contract\PhpParser\DecoratingNodeVisitorInterface; use Rector\NodeTypeResolver\Node\AttributeKey; +use Rector\PhpParser\NodeTraverser\SimpleTraverser; final class PropertyOrClassConstDefaultNodeVisitor extends NodeVisitorAbstract implements DecoratingNodeVisitorInterface { @@ -23,13 +24,13 @@ public function enterNode(Node $node): ?Node continue; } - $default->setAttribute(AttributeKey::IS_DEFAULT_PROPERTY_VALUE, true); + SimpleTraverser::decorateWithTrueAttribute($default, AttributeKey::IS_DEFAULT_PROPERTY_VALUE); } } if ($node instanceof ClassConst) { foreach ($node->consts as $const) { - $const->value->setAttribute(AttributeKey::IS_DEFAULT_CLASS_CONST_VALUE, true); + SimpleTraverser::decorateWithTrueAttribute($const->value, AttributeKey::IS_CLASS_CONST_VALUE); } } diff --git a/src/PhpParser/NodeTraverser/SimpleTraverser.php b/src/PhpParser/NodeTraverser/SimpleTraverser.php new file mode 100644 index 00000000000..4456a79d554 --- /dev/null +++ b/src/PhpParser/NodeTraverser/SimpleTraverser.php @@ -0,0 +1,38 @@ +setAttribute($this->attributeKey, true); + return null; + } + }; + + $nodeTraverser = new NodeTraverser($callableNodeVisitor); + + $nodes = $nodesOrNode instanceof Node ? [$nodesOrNode] : $nodesOrNode; + $nodeTraverser->traverse($nodes); + } +}