diff --git a/phpstan.neon b/phpstan.neon index ef2ef1e13e0..7282e852073 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,28 @@ 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/DeadCode/Rector/ClassMethod/RemoveReturnTagIncompatibleWithNativeTypeRector.php + - rules/DeadCode/Rector/If_/RemoveAlwaysTrueIfConditionRector.php + - rules/Php55/Rector/FuncCall/GetCalledClassToStaticClassRector.php + - rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php + - rules/Php81/Rector/New_/MyCLabsConstructorCallToEnumFromRector.php + - rules/Renaming/Rector/MethodCall/RenameMethodRector.php + - rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php + - rules/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector.php + - src/Application/ChangedNodeScopeRefresher.php + - src/NodeTypeResolver/NodeTypeResolver.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/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' ); 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(); + } +}