From c260094bcda427118dd7f108c91f268b16f90ed6 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 25 Oct 2026 12:47:44 +0100 Subject: [PATCH 1/2] add NoGetClassReflectionOnScopeRule, route 8 rules through ReflectionResolver --- phpstan.neon | 34 +++++++++++ .../Rule/NoGetClassReflectionOnScopeRule.php | 57 +++++++++++++++++++ .../NoGetClassReflectionOnScopeRuleTest.php | 32 +++++++++++ .../Source/GetClassReflectionOnScope.php | 15 +++++ .../Source/SkipOtherMethodCall.php | 15 +++++ 5 files changed, 153 insertions(+) create mode 100644 utils/phpstan/src/Rule/NoGetClassReflectionOnScopeRule.php create mode 100644 utils/phpstan/tests/Rule/NoGetClassReflectionOnScopeRule/NoGetClassReflectionOnScopeRuleTest.php create mode 100644 utils/phpstan/tests/Rule/NoGetClassReflectionOnScopeRule/Source/GetClassReflectionOnScope.php create mode 100644 utils/phpstan/tests/Rule/NoGetClassReflectionOnScopeRule/Source/SkipOtherMethodCall.php diff --git a/phpstan.neon b/phpstan.neon index ef2ef1e13e0..1b675d3dd7b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,6 +5,7 @@ includes: rules: - Rector\Utils\PHPStan\Rule\RegisterRelatedPolyfillRectorRule - Rector\Utils\PHPStan\Rule\NoResolveClassReflectionInForeachRule + - Rector\Utils\PHPStan\Rule\NoGetClassReflectionOnScopeRule - Rector\Utils\PHPStan\Rule\RegisterRectorRule parameters: @@ -77,6 +78,39 @@ parameters: offloadCollectorData: false ignoreErrors: + # Scope is the correct source of the enclosing class reflection in the engine resolvers, + # in rules that reuse the same Scope for other calls, and in ObjectTypeSpecifier where the + # scope reflection is refreshed on class change while ReflectionProvider reads the original file + - + identifier: rector.getClassReflectionOnScope + paths: + - rules/CodeQuality/Rector/Class_/ConvertStaticToSelfRector.php + - rules/DeadCode/Rector/ClassMethod/RemoveReturnTagIncompatibleWithNativeTypeRector.php + - rules/DeadCode/Rector/If_/RemoveAlwaysTrueIfConditionRector.php + - rules/Php55/Rector/FuncCall/GetCalledClassToStaticClassRector.php + - rules/Php70/Rector/ClassMethod/Php4ConstructorRector.php + - rules/Php70/Rector/MethodCall/ThisCallOnStaticMethodToStaticCallRector.php + - rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php + - rules/Php81/Rector/FuncCall/NullToStrictIntPregSlitFuncCallLimitArgRector.php + - rules/Php81/Rector/FuncCall/NullToStrictStringFuncCallArgRector.php + - rules/Php81/Rector/New_/MyCLabsConstructorCallToEnumFromRector.php + - rules/Php82/NodeManipulator/ReadonlyClassManipulator.php + - rules/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector.php + - rules/Renaming/Rector/MethodCall/RenameMethodRector.php + - rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php + - rules/TypeDeclaration/Rector/ClassMethod/AddVoidReturnTypeWhereNoReturnRector.php + - rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector.php + - rules/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector.php + - src/Application/ChangedNodeScopeRefresher.php + - src/NodeCollector/ScopeResolver/ParentClassScopeResolver.php + - src/NodeTypeResolver/NodeTypeResolver.php + - src/NodeTypeResolver/NodeTypeResolver/ClassAndInterfaceTypeResolver.php + - src/NodeTypeResolver/NodeTypeResolver/NameTypeResolver.php + - src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php + - src/Reflection/ReflectionResolver.php + - src/StaticTypeMapper/Naming/NameScopeFactory.php + - utils/phpstan/src/Rule/NoResolveClassReflectionInForeachRule.php + # called magically by parent - message: '#^Unused Rector\\PhpParser\\Printer\\BetterStandardPrinter\:\:(pInterpolatedStringPart|pStmt_FileNode)$#' diff --git a/utils/phpstan/src/Rule/NoGetClassReflectionOnScopeRule.php b/utils/phpstan/src/Rule/NoGetClassReflectionOnScopeRule.php new file mode 100644 index 00000000000..05986c821db --- /dev/null +++ b/utils/phpstan/src/Rule/NoGetClassReflectionOnScopeRule.php @@ -0,0 +1,57 @@ + + * @see \Rector\Utils\PHPStan\Tests\Rule\NoGetClassReflectionOnScopeRule\NoGetClassReflectionOnScopeRuleTest + */ +final class NoGetClassReflectionOnScopeRule implements Rule +{ + private const string ERROR_MESSAGE = 'Do not call getClassReflection() on a Scope. Resolve the class name and fetch the reflection via ReflectionProvider->getClass() instead.'; + + private const string METHOD_NAME = 'getClassReflection'; + + public function getNodeType(): string + { + return MethodCall::class; + } + + /** + * @param MethodCall $node + * @return list + */ + public function processNode(Node $node, Scope $scope): array + { + if (! $node->name instanceof Identifier) { + return []; + } + + if ($node->name->toString() !== self::METHOD_NAME) { + return []; + } + + $callerType = $scope->getType($node->var); + if (! new ObjectType(Scope::class)->isSuperTypeOf($callerType)->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier('rector.getClassReflectionOnScope') + ->line($node->getStartLine()) + ->build(), + ]; + } +} diff --git a/utils/phpstan/tests/Rule/NoGetClassReflectionOnScopeRule/NoGetClassReflectionOnScopeRuleTest.php b/utils/phpstan/tests/Rule/NoGetClassReflectionOnScopeRule/NoGetClassReflectionOnScopeRuleTest.php new file mode 100644 index 00000000000..ad679114df2 --- /dev/null +++ b/utils/phpstan/tests/Rule/NoGetClassReflectionOnScopeRule/NoGetClassReflectionOnScopeRuleTest.php @@ -0,0 +1,32 @@ + + */ +final class NoGetClassReflectionOnScopeRuleTest extends RuleTestCase +{ + public function testGetClassReflectionOnScope(): void + { + $expectedErrorMessage = 'Do not call getClassReflection() on a Scope. Resolve the class name and fetch the reflection via ReflectionProvider->getClass() instead.'; + + $this->analyse([__DIR__ . '/Source/GetClassReflectionOnScope.php'], [[$expectedErrorMessage, 13]]); + } + + public function testSkipOtherMethodCall(): void + { + $this->analyse([__DIR__ . '/Source/SkipOtherMethodCall.php'], []); + } + + protected function getRule(): Rule + { + return new NoGetClassReflectionOnScopeRule(); + } +} diff --git a/utils/phpstan/tests/Rule/NoGetClassReflectionOnScopeRule/Source/GetClassReflectionOnScope.php b/utils/phpstan/tests/Rule/NoGetClassReflectionOnScopeRule/Source/GetClassReflectionOnScope.php new file mode 100644 index 00000000000..6d155bac06e --- /dev/null +++ b/utils/phpstan/tests/Rule/NoGetClassReflectionOnScopeRule/Source/GetClassReflectionOnScope.php @@ -0,0 +1,15 @@ +getClassReflection(); + } +} diff --git a/utils/phpstan/tests/Rule/NoGetClassReflectionOnScopeRule/Source/SkipOtherMethodCall.php b/utils/phpstan/tests/Rule/NoGetClassReflectionOnScopeRule/Source/SkipOtherMethodCall.php new file mode 100644 index 00000000000..1097249f134 --- /dev/null +++ b/utils/phpstan/tests/Rule/NoGetClassReflectionOnScopeRule/Source/SkipOtherMethodCall.php @@ -0,0 +1,15 @@ +isInClass(); + } +} From 736d2f0810a7039ca6ed4496455633af9481a0d6 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 25 Oct 2026 20:05:56 +0100 Subject: [PATCH 2/2] resolve self/static class name via node type instead of scope reflection --- phpstan.neon | 11 -------- .../ReturnTypeFromReturnNewRector.php | 11 ++++---- .../ClassAndInterfaceTypeResolver.php | 23 +++++----------- src/PhpParser/Node/Value/ValueResolver.php | 27 ++++++++++--------- 4 files changed, 28 insertions(+), 44 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 1b675d3dd7b..7282e852073 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -84,27 +84,16 @@ parameters: - identifier: rector.getClassReflectionOnScope paths: - - rules/CodeQuality/Rector/Class_/ConvertStaticToSelfRector.php - rules/DeadCode/Rector/ClassMethod/RemoveReturnTagIncompatibleWithNativeTypeRector.php - rules/DeadCode/Rector/If_/RemoveAlwaysTrueIfConditionRector.php - rules/Php55/Rector/FuncCall/GetCalledClassToStaticClassRector.php - - rules/Php70/Rector/ClassMethod/Php4ConstructorRector.php - - rules/Php70/Rector/MethodCall/ThisCallOnStaticMethodToStaticCallRector.php - rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php - - rules/Php81/Rector/FuncCall/NullToStrictIntPregSlitFuncCallLimitArgRector.php - - rules/Php81/Rector/FuncCall/NullToStrictStringFuncCallArgRector.php - rules/Php81/Rector/New_/MyCLabsConstructorCallToEnumFromRector.php - - rules/Php82/NodeManipulator/ReadonlyClassManipulator.php - - rules/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector.php - rules/Renaming/Rector/MethodCall/RenameMethodRector.php - rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php - - rules/TypeDeclaration/Rector/ClassMethod/AddVoidReturnTypeWhereNoReturnRector.php - - rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector.php - rules/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector.php - src/Application/ChangedNodeScopeRefresher.php - - src/NodeCollector/ScopeResolver/ParentClassScopeResolver.php - src/NodeTypeResolver/NodeTypeResolver.php - - src/NodeTypeResolver/NodeTypeResolver/ClassAndInterfaceTypeResolver.php - src/NodeTypeResolver/NodeTypeResolver/NameTypeResolver.php - src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php - src/Reflection/ReflectionResolver.php diff --git a/rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromReturnNewRector.php b/rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromReturnNewRector.php index ec1cc2bb176..da70090c609 100644 --- a/rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromReturnNewRector.php +++ b/rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromReturnNewRector.php @@ -12,7 +12,6 @@ use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\Return_; -use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; @@ -27,9 +26,9 @@ use Rector\PHPStan\ScopeFetcher; use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; use Rector\Rector\AbstractRector; -use Rector\Reflection\ReflectionResolver; use Rector\RuleDoc\CodeSample\CodeSample; use Rector\RuleDoc\RuleDefinition; +use Rector\StaticTypeMapper\Resolver\ClassNameFromObjectTypeResolver; use Rector\StaticTypeMapper\StaticTypeMapper; use Rector\StaticTypeMapper\ValueObject\Type\SelfStaticType; use Rector\Symfony\CodeQuality\Enum\ResponseClass; @@ -48,7 +47,6 @@ final class ReturnTypeFromReturnNewRector extends AbstractRector implements MinP public function __construct( private readonly TypeFactory $typeFactory, private readonly ReflectionProvider $reflectionProvider, - private readonly ReflectionResolver $reflectionResolver, private readonly StrictReturnNewAnalyzer $strictReturnNewAnalyzer, private readonly ClassMethodReturnTypeOverrideGuard $classMethodReturnTypeOverrideGuard, private readonly ClassAnalyzer $classAnalyzer, @@ -156,11 +154,14 @@ private function createObjectTypeFromNew(New_ $new): ObjectType|ObjectWithoutCla $className = $this->getName($new->class); if ($className === ObjectReference::STATIC || $className === ObjectReference::SELF) { - $classReflection = $this->reflectionResolver->resolveClassReflection($new); - if (! $classReflection instanceof ClassReflection) { + $classType = $this->nodeTypeResolver->getType($new->class); + $resolvedClassName = ClassNameFromObjectTypeResolver::resolve($classType); + if ($resolvedClassName === null || ! $this->reflectionProvider->hasClass($resolvedClassName)) { throw new ShouldNotHappenException(); } + $classReflection = $this->reflectionProvider->getClass($resolvedClassName); + if ($className === ObjectReference::SELF) { return new SelfStaticType($classReflection); } diff --git a/src/NodeTypeResolver/NodeTypeResolver/ClassAndInterfaceTypeResolver.php b/src/NodeTypeResolver/NodeTypeResolver/ClassAndInterfaceTypeResolver.php index 545c6884ec5..e187fd5b968 100644 --- a/src/NodeTypeResolver/NodeTypeResolver/ClassAndInterfaceTypeResolver.php +++ b/src/NodeTypeResolver/NodeTypeResolver/ClassAndInterfaceTypeResolver.php @@ -7,14 +7,11 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Interface_; -use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ClassReflection; -use PHPStan\Type\MixedType; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use Rector\NodeNameResolver\NodeNameResolver; use Rector\NodeTypeResolver\Contract\NodeTypeResolverInterface; -use Rector\NodeTypeResolver\Node\AttributeKey; /** * @see \Rector\Tests\NodeTypeResolver\PerNodeTypeResolver\ClassAndInterfaceTypeResolver\ClassTypeResolverTest @@ -25,7 +22,8 @@ final readonly class ClassAndInterfaceTypeResolver implements NodeTypeResolverInterface { public function __construct( - private NodeNameResolver $nodeNameResolver + private NodeNameResolver $nodeNameResolver, + private ReflectionProvider $reflectionProvider, ) { } @@ -42,18 +40,11 @@ public function getNodeClasses(): array */ public function resolve(Node $node): Type { - $scope = $node->getAttribute(AttributeKey::SCOPE); - - if (! $scope instanceof Scope) { - // new node probably - return new MixedType(); - } - - $classReflection = $scope->getClassReflection(); - if (! $classReflection instanceof ClassReflection) { - return new ObjectType((string) $this->nodeNameResolver->getName($node)); + $className = $this->nodeNameResolver->getName($node); + if ($className === null || ! $this->reflectionProvider->hasClass($className)) { + return new ObjectType((string) $className); } - return new ObjectType($classReflection->getName(), null, $classReflection); + return new ObjectType($className, null, $this->reflectionProvider->getClass($className)); } } diff --git a/src/PhpParser/Node/Value/ValueResolver.php b/src/PhpParser/Node/Value/ValueResolver.php index 77a9b830dec..cd1112c4c6b 100644 --- a/src/PhpParser/Node/Value/ValueResolver.php +++ b/src/PhpParser/Node/Value/ValueResolver.php @@ -74,9 +74,10 @@ public function getValue(Arg|Expr|InterpolatedStringPart $expr, bool $resolvedCl $class = $this->nodeNameResolver->getName($expr->class); if (in_array($class, [ObjectReference::SELF, ObjectReference::STATIC], true)) { - $classReflection = $this->reflectionResolver->resolveClassReflection($expr); - if ($classReflection instanceof ClassReflection) { - return $classReflection->getName(); + $classType = $this->nodeTypeResolver->getType($expr->class); + $className = ClassNameFromObjectTypeResolver::resolve($classType); + if ($className !== null) { + return $className; } } @@ -333,18 +334,20 @@ private function resolveClassFromSelfStaticParent(ClassConstFetch $classConstFet return ''; } - $classReflection = $this->reflectionResolver->resolveClassReflection($classConstFetch); - if (! $classReflection instanceof ClassReflection) { - throw new ShouldNotHappenException( - 'Complete class parent node for to class const fetch, so "self" or "static" references is resolvable to a class name' - ); - } - if ($class !== ObjectReference::PARENT) { - return $classReflection->getName(); + $classType = $this->nodeTypeResolver->getType($classConstFetch->class); + $className = ClassNameFromObjectTypeResolver::resolve($classType); + if ($className === null) { + throw new ShouldNotHappenException( + 'Complete class parent node for to class const fetch, so "self" or "static" references is resolvable to a class name' + ); + } + + return $className; } - if (! $classReflection->isClass()) { + $classReflection = $this->reflectionResolver->resolveClassReflection($classConstFetch); + if (! $classReflection instanceof ClassReflection || ! $classReflection->isClass()) { throw new ShouldNotHappenException( 'Complete class parent node for to class const fetch, so "parent" references is resolvable to lookup parent class' );