From f77bd0672f86ef1fe34d15129dd6dff33541e278 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 29 Aug 2026 19:19:49 +0200 Subject: [PATCH 1/3] Reuse RectorCheaperGuardsFirstRule from symplify/phpstan-rules The rule moved to symplify/phpstan-rules (rector set) in 14.13.0, where it is useful for anyone writing custom Rector rules. Drop the local copy and pull it in via the shared rector-rules.neon set. Claude-Session: https://claude.ai/code/session_01Na4eYvNqa2pdhZrBXbegrv --- composer.json | 2 +- phpstan.neon | 1 - .../src/Rule/CheaperGuardFirstRule.php | 274 ------------------ .../CheaperGuardFirstRuleTest.php | 37 --- .../Source/CheapGuardFirstRector.php | 38 --- .../Source/DependentGuardRector.php | 40 --- .../ExpensiveBeforeCheapGuardRector.php | 38 --- 7 files changed, 1 insertion(+), 429 deletions(-) delete mode 100644 utils/phpstan/src/Rule/CheaperGuardFirstRule.php delete mode 100644 utils/phpstan/tests/Rule/CheaperGuardFirstRule/CheaperGuardFirstRuleTest.php delete mode 100644 utils/phpstan/tests/Rule/CheaperGuardFirstRule/Source/CheapGuardFirstRector.php delete mode 100644 utils/phpstan/tests/Rule/CheaperGuardFirstRule/Source/DependentGuardRector.php delete mode 100644 utils/phpstan/tests/Rule/CheaperGuardFirstRule/Source/ExpensiveBeforeCheapGuardRector.php diff --git a/composer.json b/composer.json index c7451cac3b9..6ead49b6f70 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ "rector/swiss-knife": "^2.4.1", "shipmonk/composer-dependency-analyser": "^1.8", "symplify/easy-coding-standard": "^13.2.13", - "symplify/phpstan-rules": "^14.12.3", + "symplify/phpstan-rules": "^14.13", "tomasvotruba/class-leak": "^2.1", "tomasvotruba/fast-unit": "^0.1", "tomasvotruba/type-coverage": "^2.3.6", diff --git a/phpstan.neon b/phpstan.neon index f237e79a3ab..9d19c42b277 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,7 +3,6 @@ includes: - vendor/symplify/phpstan-rules/config/rector-rules.neon rules: - - Rector\Utils\PHPStan\Rule\CheaperGuardFirstRule - Rector\Utils\PHPStan\Rule\RegisterRelatedPolyfillRectorRule parameters: diff --git a/utils/phpstan/src/Rule/CheaperGuardFirstRule.php b/utils/phpstan/src/Rule/CheaperGuardFirstRule.php deleted file mode 100644 index 878ced98508..00000000000 --- a/utils/phpstan/src/Rule/CheaperGuardFirstRule.php +++ /dev/null @@ -1,274 +0,0 @@ - - * @see \Rector\Utils\PHPStan\Tests\Rule\CheaperGuardFirstRule\CheaperGuardFirstRuleTest - */ -final class CheaperGuardFirstRule implements Rule -{ - private const string ERROR_MESSAGE = 'Cheap guard on line %d can run before the expensive call on line %d; move the early return up to bail before the costly analysis.'; - - /** - * Calls that trigger heavy analysis (type resolution, docblock parsing, file re-parsing). - * - * @var string[] - */ - private const array EXPENSIVE_CALLS = [ - 'getType', - 'getNativeType', - 'isObjectType', - 'isObjectTypes', - 'createFromNode', - 'createFromNodeOrEmpty', - ]; - - /** - * Calls cheap enough to evaluate as a pre-filter. - * - * @var string[] - */ - private const array CHEAP_CALLS = ['isName', 'isNames', 'isFirstClassCallable', 'in_array', 'count']; - - private const string ABSTRACT_RECTOR_CLASS = 'Rector\Rector\AbstractRector'; - - public function getNodeType(): string - { - return ClassMethod::class; - } - - /** - * @param ClassMethod $node - * @return list - */ - public function processNode(Node $node, Scope $scope): array - { - if ($node->stmts === null) { - return []; - } - - $classReflection = $scope->getClassReflection(); - if (! $classReflection instanceof ClassReflection) { - return []; - } - - if (! in_array(self::ABSTRACT_RECTOR_CLASS, $classReflection->getParentClassesNames(), true)) { - return []; - } - - $stmts = $node->stmts; - $anchorIndex = $this->findExpensiveAnchorIndex($stmts); - if ($anchorIndex === null) { - return []; - } - - $assignedVariableNames = $this->resolveAssignedVariableNames($stmts[$anchorIndex]); - $counter = count($stmts); - - for ($index = $anchorIndex + 1; $index < $counter; ++$index) { - $stmt = $stmts[$index]; - - if ($this->isPureBailGuard($stmt)) { - /** @var If_ $stmt */ - if ($this->isCheapCondition($stmt->cond) && $this->isIndependent($stmt->cond, $assignedVariableNames)) { - return [ - RuleErrorBuilder::message( - sprintf(self::ERROR_MESSAGE, $stmt->getStartLine(), $stmts[$anchorIndex]->getStartLine()) - ) - ->identifier('rector.cheaperGuardFirst') - ->line($stmt->getStartLine()) - ->build(), - ]; - } - - // a dependent or non-cheap bail guard is legitimately here; keep scanning - continue; - } - - if ($stmt instanceof Expression && $stmt->expr instanceof Assign) { - $assignedVariableNames = [...$assignedVariableNames, ...$this->resolveAssignedVariableNames($stmt)]; - continue; - } - - // any other statement (value return, transformation, loop) makes hoisting unsafe - return []; - } - - return []; - } - - /** - * @param Stmt[] $stmts - */ - private function findExpensiveAnchorIndex(array $stmts): ?int - { - foreach ($stmts as $index => $stmt) { - if (! $stmt instanceof Expression && ! $stmt instanceof If_) { - continue; - } - - if ($this->containsCall($stmt, self::EXPENSIVE_CALLS)) { - return $index; - } - } - - return null; - } - - private function isPureBailGuard(Stmt $stmt): bool - { - if (! $stmt instanceof If_) { - return false; - } - - if ($stmt->elseifs !== [] || $stmt->else instanceof Else_) { - return false; - } - - if (count($stmt->stmts) !== 1) { - return false; - } - - $onlyStmt = $stmt->stmts[0]; - if ($onlyStmt instanceof Continue_) { - return true; - } - - if (! $onlyStmt instanceof Return_) { - return false; - } - - // bare "return;" or "return null;" - if (! $onlyStmt->expr instanceof Node) { - return true; - } - - return $onlyStmt->expr instanceof ConstFetch && $onlyStmt->expr->name->toLowerString() === 'null'; - } - - private function isCheapCondition(Expr $expr): bool - { - $nodeFinder = new NodeFinder(); - $callLikes = $nodeFinder->findInstanceOf($expr, CallLike::class); - - foreach ($callLikes as $callLike) { - $name = $this->resolveCallName($callLike); - if ($name === null) { - return false; - } - - if (! in_array($name, self::CHEAP_CALLS, true)) { - return false; - } - } - - return true; - } - - /** - * @param string[] $assignedVariableNames - */ - private function isIndependent(Expr $expr, array $assignedVariableNames): bool - { - if ($assignedVariableNames === []) { - return true; - } - - $nodeFinder = new NodeFinder(); - $variables = $nodeFinder->findInstanceOf($expr, Variable::class); - - foreach ($variables as $variable) { - if (! is_string($variable->name)) { - continue; - } - - if (in_array($variable->name, $assignedVariableNames, true)) { - return false; - } - } - - return true; - } - - /** - * @param string[] $callNames - */ - private function containsCall(Node $node, array $callNames): bool - { - $nodeFinder = new NodeFinder(); - $callLikes = $nodeFinder->findInstanceOf($node, CallLike::class); - - foreach ($callLikes as $callLike) { - $name = $this->resolveCallName($callLike); - if ($name !== null && in_array($name, $callNames, true)) { - return true; - } - } - - return false; - } - - private function resolveCallName(CallLike $callLike): ?string - { - if ($callLike instanceof MethodCall || $callLike instanceof NullsafeMethodCall || $callLike instanceof StaticCall) { - return $callLike->name instanceof Identifier ? $callLike->name->toString() : null; - } - - if ($callLike instanceof FuncCall) { - return $callLike->name instanceof Name ? $callLike->name->toString() : null; - } - - return null; - } - - /** - * @return string[] - */ - private function resolveAssignedVariableNames(Stmt $stmt): array - { - if (! $stmt instanceof Expression || ! $stmt->expr instanceof Assign) { - return []; - } - - $assign = $stmt->expr; - if ($assign->var instanceof Variable && is_string($assign->var->name)) { - return [$assign->var->name]; - } - - return []; - } -} diff --git a/utils/phpstan/tests/Rule/CheaperGuardFirstRule/CheaperGuardFirstRuleTest.php b/utils/phpstan/tests/Rule/CheaperGuardFirstRule/CheaperGuardFirstRuleTest.php deleted file mode 100644 index b369aef9321..00000000000 --- a/utils/phpstan/tests/Rule/CheaperGuardFirstRule/CheaperGuardFirstRuleTest.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ -final class CheaperGuardFirstRuleTest extends RuleTestCase -{ - public function testExpensiveBeforeCheapGuard(): void - { - $expectedErrorMessage = 'Cheap guard on line 32 can run before the expensive call on line 26; move the early return up to bail before the costly analysis.'; - - $this->analyse([__DIR__ . '/Source/ExpensiveBeforeCheapGuardRector.php'], [[$expectedErrorMessage, 32]]); - } - - public function testCheapGuardFirst(): void - { - $this->analyse([__DIR__ . '/Source/CheapGuardFirstRector.php'], []); - } - - public function testDependentGuard(): void - { - $this->analyse([__DIR__ . '/Source/DependentGuardRector.php'], []); - } - - protected function getRule(): Rule - { - return new CheaperGuardFirstRule(); - } -} diff --git a/utils/phpstan/tests/Rule/CheaperGuardFirstRule/Source/CheapGuardFirstRector.php b/utils/phpstan/tests/Rule/CheaperGuardFirstRule/Source/CheapGuardFirstRector.php deleted file mode 100644 index 92d62231b5f..00000000000 --- a/utils/phpstan/tests/Rule/CheaperGuardFirstRule/Source/CheapGuardFirstRector.php +++ /dev/null @@ -1,38 +0,0 @@ -isName($node, 'array')) { - return null; - } - - $type = $this->getType($node); - if ($type->isString()->yes()) { - return null; - } - - return $node; - } -} diff --git a/utils/phpstan/tests/Rule/CheaperGuardFirstRule/Source/DependentGuardRector.php b/utils/phpstan/tests/Rule/CheaperGuardFirstRule/Source/DependentGuardRector.php deleted file mode 100644 index 181e66cb5a3..00000000000 --- a/utils/phpstan/tests/Rule/CheaperGuardFirstRule/Source/DependentGuardRector.php +++ /dev/null @@ -1,40 +0,0 @@ -getType($node); - - // value-returning guard in between -> reordering is unsafe, must NOT report - if ($type->isString()->yes()) { - return $node; - } - - // depends on $type -> must NOT report - if ($type->isInteger()->yes()) { - return null; - } - - return $node; - } -} diff --git a/utils/phpstan/tests/Rule/CheaperGuardFirstRule/Source/ExpensiveBeforeCheapGuardRector.php b/utils/phpstan/tests/Rule/CheaperGuardFirstRule/Source/ExpensiveBeforeCheapGuardRector.php deleted file mode 100644 index ad9af5a48b8..00000000000 --- a/utils/phpstan/tests/Rule/CheaperGuardFirstRule/Source/ExpensiveBeforeCheapGuardRector.php +++ /dev/null @@ -1,38 +0,0 @@ -getType($node); - if ($type->isString()->yes()) { - return null; - } - - // cheap, independent of $type, but runs after the expensive getType() - if (! $this->isName($node, 'array')) { - return null; - } - - return $node; - } -} From 29d52323251f506a6cbcbf2d00ddfb250e062c46 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 29 Aug 2026 19:22:09 +0200 Subject: [PATCH 2/3] deprecate EARLY_RETURN set, as empty and already part of code-quality set --- src/Set/ValueObject/SetList.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Set/ValueObject/SetList.php b/src/Set/ValueObject/SetList.php index b8d8d69c852..209a06a8cc7 100644 --- a/src/Set/ValueObject/SetList.php +++ b/src/Set/ValueObject/SetList.php @@ -75,6 +75,9 @@ final class SetList public const string TYPE_DECLARATION_DOCBLOCKS = __DIR__ . '/../../../config/set/type-declaration-docblocks.php'; + /** + * @deprecated Use code-quality set instead, as all early return rules were moved there + */ public const string EARLY_RETURN = __DIR__ . '/../../../config/set/early-return.php'; public const string INSTANCEOF = __DIR__ . '/../../../config/set/instanceof.php'; From 6bf75cdfcc79e5542410bf76d0ae128bc09143d2 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 29 Aug 2026 19:37:18 +0200 Subject: [PATCH 3/3] Ignore deprecated EARLY_RETURN set constant resolved internally Claude-Session: https://claude.ai/code/session_01Na4eYvNqa2pdhZrBXbegrv --- phpstan.neon | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/phpstan.neon b/phpstan.neon index 9d19c42b277..a782535dc7b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -70,6 +70,12 @@ parameters: message: '#deprecated (class|interface) Rector\\(Set|Bridge)\\#' path: src/Configuration/RectorConfigBuilder.php + # EARLY_RETURN set is deprecated (empty, merged into code-quality), still mapped internally until removed + - + identifier: classConstant.deprecated + message: '#EARLY_RETURN of class Rector\\Set\\ValueObject\\SetList#' + path: src/Configuration/RectorConfigBuilder.php + # the deprecated github/gitlab output formatters are still tested until removed in next minor version - identifier: new.deprecatedClass