From 88736b798c3b0c5634dd5dd2e2518e2edf6f3452 Mon Sep 17 00:00:00 2001 From: Alfred Bez <1001186+alfredbez@users.noreply.github.com> Date: Sun, 18 Oct 2026 06:42:31 +0000 Subject: [PATCH] perf: reject non-callable arrays before scope lookup --- .../ArrayToFirstClassCallableRectorTest.php | 39 +++++++++++++++++++ .../ArrayToFirstClassCallableRector.php | 27 ++++++++----- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/ArrayToFirstClassCallableRectorTest.php b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/ArrayToFirstClassCallableRectorTest.php index a8e18eece44..d6cc3ec159d 100644 --- a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/ArrayToFirstClassCallableRectorTest.php +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/ArrayToFirstClassCallableRectorTest.php @@ -5,7 +5,13 @@ namespace Rector\Tests\Php81\Rector\Array_\ArrayToFirstClassCallableRector; use Iterator; +use PhpParser\Node\ArrayItem; +use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Scalar\String_; use PHPUnit\Framework\Attributes\DataProvider; +use Rector\NodeTypeResolver\Node\AttributeKey; +use Rector\Php81\Rector\Array_\ArrayToFirstClassCallableRector; use Rector\Testing\PHPUnit\AbstractRectorTestCase; final class ArrayToFirstClassCallableRectorTest extends AbstractRectorTestCase @@ -21,6 +27,39 @@ public static function provideData(): Iterator return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); } + #[DataProvider('provideSkippedArrays')] + public function testSkipWithoutScope(Array_ $array): void + { + $rector = $this->make(ArrayToFirstClassCallableRector::class); + + $this->assertNull($rector->refactor($array)); + } + + /** + * @return Iterator + */ + public static function provideSkippedArrays(): Iterator + { + $caller = new ArrayItem(new Variable('this')); + $method = new ArrayItem(new String_('run')); + + yield 'empty array' => [new Array_()]; + yield 'one item' => [new Array_([$caller])]; + yield 'three items' => [new Array_([$caller, $method, $method])]; + + foreach ([ + AttributeKey::IS_CLASS_CONST_VALUE, + AttributeKey::IS_DEFAULT_PROPERTY_VALUE, + AttributeKey::IS_PARAM_DEFAULT, + AttributeKey::IS_UNPACKED_ARG_VALUE, + ] as $attribute) { + yield $attribute => [ + new Array_([$caller, $method], [ + $attribute => true, + ])]; + } + } + public function provideConfigFilePath(): string { return __DIR__ . '/config/configured_rule.php'; diff --git a/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php b/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php index 15e77d6c1d3..0d1e6fc19a6 100644 --- a/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php +++ b/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php @@ -5,6 +5,7 @@ namespace Rector\Php81\Rector\Array_; use PhpParser\Node; +use PhpParser\Node\ArrayItem; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\MethodCall; @@ -90,27 +91,23 @@ public function getNodeTypes(): array */ public function refactor(Node $node): StaticCall|MethodCall|null { - if ($node->getAttribute(AttributeKey::IS_ARG_NOT_ACCEPTING_CLOSURE)) { + if (count($node->items) !== 2) { return null; } - if ($node->getAttribute(AttributeKey::IS_ARRAY_AS_STRING_CALLABLE)) { + if (! $node->items[0] instanceof ArrayItem || ! $node->items[1] instanceof ArrayItem) { return null; } - if ($node->getAttribute(AttributeKey::IS_ARRAY_IN_ATTRIBUTE) === true) { + if ($node->getAttribute(AttributeKey::IS_ARG_NOT_ACCEPTING_CLOSURE)) { return null; } - $scope = ScopeFetcher::fetch($node); - - $arrayCallable = $this->arrayCallableMethodMatcher->match($node, $scope); - if (! $arrayCallable instanceof ArrayCallable) { + if ($node->getAttribute(AttributeKey::IS_ARRAY_AS_STRING_CALLABLE)) { return null; } - $callerExpr = $arrayCallable->getCallerExpr(); - if (! $callerExpr instanceof Variable && ! $callerExpr instanceof PropertyFetch && ! $callerExpr instanceof ClassConstFetch) { + if ($node->getAttribute(AttributeKey::IS_ARRAY_IN_ATTRIBUTE) === true) { return null; } @@ -130,6 +127,18 @@ public function refactor(Node $node): StaticCall|MethodCall|null return null; } + $scope = ScopeFetcher::fetch($node); + + $arrayCallable = $this->arrayCallableMethodMatcher->match($node, $scope); + if (! $arrayCallable instanceof ArrayCallable) { + return null; + } + + $callerExpr = $arrayCallable->getCallerExpr(); + if (! $callerExpr instanceof Variable && ! $callerExpr instanceof PropertyFetch && ! $callerExpr instanceof ClassConstFetch) { + return null; + } + $args = [new VariadicPlaceholder()]; if ($callerExpr instanceof ClassConstFetch) { $type = $this->getType($callerExpr->class);