diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 0c0af430e8..9fd81c9e90 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -5599,6 +5599,10 @@ private function exactInstantiation(New_ $node, string $className): ?Type } $classReflection = $this->reflectionProvider->getClass($resolvedClassName); + $nonFinalClassReflection = $classReflection; + if (!$isStatic) { + $classReflection = $classReflection->asFinal(); + } if ($classReflection->hasConstructor()) { $constructorMethod = $classReflection->getConstructor(); } else { @@ -5648,7 +5652,7 @@ private function exactInstantiation(New_ $node, string $className): ?Type return $methodResult; } - $objectType = $isStatic ? new StaticType($classReflection) : new ObjectType($resolvedClassName); + $objectType = $isStatic ? new StaticType($classReflection) : new ObjectType($resolvedClassName, null, $classReflection); if (!$classReflection->isGeneric()) { return $objectType; } @@ -5674,7 +5678,8 @@ private function exactInstantiation(New_ $node, string $className): ?Type if (count($classTemplateTypes) === count($originalClassTemplateTypes)) { $propertyType = TypeCombinator::removeNull($this->getType($assignedToProperty)); - if ($objectType->isSuperTypeOf($propertyType)->yes()) { + $nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, null, $nonFinalClassReflection); + if ($nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) { return $propertyType; } } @@ -5689,9 +5694,13 @@ private function exactInstantiation(New_ $node, string $className): ?Type [], ); } + + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); return new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $types, + null, + $classReflection->withTypes($types)->asFinal(), ); } @@ -5706,9 +5715,12 @@ private function exactInstantiation(New_ $node, string $className): ?Type ); } + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); return new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $types, + null, + $classReflection->withTypes($types)->asFinal(), ); } $newType = new GenericObjectType($resolvedClassName, $classReflection->typeMapToList($classReflection->getTemplateTypeMap())); @@ -5723,9 +5735,12 @@ private function exactInstantiation(New_ $node, string $className): ?Type ); } + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); return new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $types, + null, + $classReflection->withTypes($types)->asFinal(), ); } $ancestorClassReflections = $ancestorType->getObjectClassReflections(); @@ -5739,9 +5754,12 @@ private function exactInstantiation(New_ $node, string $className): ?Type ); } + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); return new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $types, + null, + $classReflection->withTypes($types)->asFinal(), ); } @@ -5758,9 +5776,12 @@ private function exactInstantiation(New_ $node, string $className): ?Type ); } + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); return new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $types, + null, + $classReflection->withTypes($types)->asFinal(), ); } $newParentTypeClassReflection = $newParentTypeClassReflections[0]; @@ -5803,9 +5824,12 @@ private function exactInstantiation(New_ $node, string $className): ?Type ); } + $types = $classReflection->typeMapToList(new TemplateTypeMap($resolvedTypeMap)); return new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList(new TemplateTypeMap($resolvedTypeMap)), + $types, + null, + $classReflection->withTypes($types)->asFinal(), ); } @@ -5817,14 +5841,17 @@ private function exactInstantiation(New_ $node, string $className): ?Type ); $resolvedTemplateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap(); + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()); $newGenericType = new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()), + $types, + null, + $classReflection->withTypes($types)->asFinal(), ); if ($isStatic) { $newGenericType = new GenericStaticType( $classReflection, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()), + $types, null, [], ); diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 0c2bbd532c..6ec2c329d4 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -175,6 +175,7 @@ public function __construct( private array $universalObjectCratesClasses, private ?string $extraCacheKey = null, private ?TemplateTypeVarianceMap $resolvedCallSiteVarianceMap = null, + private ?bool $finalByKeywordOverride = null, ) { } @@ -306,6 +307,10 @@ public function getCacheKey(): string $cacheKey .= '<' . implode(',', $templateTypes) . '>'; } + if ($this->hasFinalByKeywordOverride()) { + $cacheKey .= '-f=' . ($this->isFinalByKeyword() ? 't' : 'f'); + } + if ($this->extraCacheKey !== null) { $cacheKey .= '-' . $this->extraCacheKey; } @@ -1276,12 +1281,21 @@ public function acceptsNamedArguments(): bool return $this->acceptsNamedArguments; } + public function hasFinalByKeywordOverride(): bool + { + return $this->isClass() && $this->finalByKeywordOverride !== null; + } + public function isFinalByKeyword(): bool { if ($this->isAnonymous()) { return true; } + if ($this->isClass() && $this->finalByKeywordOverride !== null) { + return $this->finalByKeywordOverride; + } + return $this->reflection->isFinal(); } @@ -1543,6 +1557,7 @@ public function withTypes(array $types): self $this->universalObjectCratesClasses, null, $this->resolvedCallSiteVarianceMap, + $this->finalByKeywordOverride, ); } @@ -1573,6 +1588,42 @@ public function withVariances(array $variances): self $this->universalObjectCratesClasses, null, $this->varianceMapFromList($variances), + $this->finalByKeywordOverride, + ); + } + + public function asFinal(): self + { + if ($this->getNativeReflection()->isFinal()) { + return $this; + } + if ($this->finalByKeywordOverride === true) { + return $this; + } + + return new self( + $this->reflectionProvider, + $this->initializerExprTypeResolver, + $this->fileTypeMapper, + $this->stubPhpDocProvider, + $this->phpDocInheritanceResolver, + $this->phpVersion, + $this->signatureMapProvider, + $this->attributeReflectionFactory, + $this->propertiesClassReflectionExtensions, + $this->methodsClassReflectionExtensions, + $this->allowedSubTypesClassReflectionExtensions, + $this->requireExtendsPropertiesClassReflectionExtension, + $this->requireExtendsMethodsClassReflectionExtension, + $this->displayName, + $this->reflection, + $this->anonymousFilename, + $this->resolvedTemplateTypeMap, + $this->stubPhpDocBlock, + $this->universalObjectCratesClasses, + null, + $this->resolvedCallSiteVarianceMap, + true, ); } diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index db7e3f2a32..e4c34e609f 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -223,7 +223,7 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createYes(); } - if (!$classRef->getNativeReflection()->isFinal()) { + if (!$classRef->isFinalByKeyword()) { return TrinaryLogic::createMaybe(); } @@ -265,7 +265,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) return FunctionCallableVariant::createFromVariants($method, $method->getVariants()); } - if (!$classReflection->getNativeReflection()->isFinal()) { + if (!$classReflection->isFinalByKeyword()) { return [new TrivialParametersAcceptor()]; } } diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 5a05575059..19764b31ff 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -377,21 +377,37 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult throw new ShouldNotHappenException(); } - if ($thatClassNames[0] === $thisClassName) { - return $transformResult(IsSuperTypeOfResult::createYes()); - } - - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); $thisClassReflection = $this->getClassReflection(); + $thatClassReflections = $type->getObjectClassReflections(); + if (count($thatClassReflections) === 1) { + $thatClassReflection = $thatClassReflections[0]; + } else { + $thatClassReflection = null; + } - if ($thisClassReflection === null || !$reflectionProvider->hasClass($thatClassNames[0])) { + if ($thisClassReflection === null || $thatClassReflection === null) { + if ($thatClassNames[0] === $thisClassName) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); + } return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - $thatClassReflection = $reflectionProvider->getClass($thatClassNames[0]); + if ($thatClassNames[0] === $thisClassName) { + if ($thisClassReflection->getNativeReflection()->isFinal()) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); + } + + if ($thisClassReflection->hasFinalByKeywordOverride()) { + if (!$thatClassReflection->hasFinalByKeywordOverride()) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createMaybe()); + } + } + + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); + } if ($thisClassReflection->isTrait() || $thatClassReflection->isTrait()) { - return IsSuperTypeOfResult::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); } if ($thisClassReflection->getName() === $thatClassReflection->getName()) { @@ -406,11 +422,11 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - if ($thisClassReflection->isInterface() && !$thatClassReflection->getNativeReflection()->isFinal()) { + if ($thisClassReflection->isInterface() && !$thatClassReflection->isFinalByKeyword()) { return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - if ($thatClassReflection->isInterface() && !$thisClassReflection->getNativeReflection()->isFinal()) { + if ($thatClassReflection->isInterface() && !$thisClassReflection->isFinalByKeyword()) { return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } @@ -550,6 +566,10 @@ private function describeCache(): string $description .= '-'; $description .= (string) $reflection->getNativeReflection()->getStartLine(); $description .= '-'; + + if ($reflection->hasFinalByKeywordOverride()) { + $description .= 'f=' . ($reflection->isFinalByKeyword() ? 't' : 'f'); + } } return $this->cachedDescription = $description; @@ -1331,7 +1351,7 @@ private function findCallableParametersAcceptors(): ?array ); } - if (!$classReflection->getNativeReflection()->isFinal()) { + if (!$classReflection->isFinalByKeyword()) { return [new TrivialParametersAcceptor()]; } diff --git a/tests/PHPStan/Analyser/nsrt/get-debug-type.php b/tests/PHPStan/Analyser/nsrt/get-debug-type.php index bc3823a68b..2408a6acde 100644 --- a/tests/PHPStan/Analyser/nsrt/get-debug-type.php +++ b/tests/PHPStan/Analyser/nsrt/get-debug-type.php @@ -33,7 +33,7 @@ function doFoo(bool $b, int $i, float $f, $d, $r, string $s, array $a, $intOrStr assertType("'float'", get_debug_type($d)); assertType("'string'", get_debug_type($s)); assertType("'array'", get_debug_type($a)); - assertType("string", get_debug_type($o)); + assertType("'stdClass'", get_debug_type($o)); assertType("string", get_debug_type($std)); assertType("'GetDebugType\\\\A'", get_debug_type($A)); assertType("string", get_debug_type($r)); diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index 19f40e9421..2ead0c53c5 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -504,4 +504,15 @@ public function testBug3632(): void ]); } + public function testNewIsAlwaysFinalClass(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/impossible-instanceof-new-is-always-final.php'], [ + [ + 'Instanceof between ImpossibleInstanceofNewIsAlwaysFinal\Bar and ImpossibleInstanceofNewIsAlwaysFinal\Foo will always evaluate to false.', + 17, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/impossible-instanceof-new-is-always-final.php b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-new-is-always-final.php new file mode 100644 index 0000000000..1141e6c3dd --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-new-is-always-final.php @@ -0,0 +1,26 @@ +analyse([__DIR__ . '/data/bug-4852.php'], [ + [ + 'Dead catch - Exception is never thrown in the try block.', + 63, + ], [ 'Dead catch - Exception is never thrown in the try block.', 78, diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index cde66da1c8..b61120eb27 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -103,7 +103,7 @@ public function testRule(): void 106, ], [ - 'Trying to invoke CallCallables\Baz but it might not be a callable.', + 'Trying to invoke CallCallables\Baz but it\'s not a callable.', 113, ], [ diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index bedf530cdc..95a55073ed 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3052,7 +3052,7 @@ public function testObjectShapes(): void [ 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, Exception given.', 14, - 'Exception might not have property $foo.', + PHP_VERSION_ID >= 80200 ? 'Exception does not have property $foo.' : 'Exception might not have property $foo.', ], [ 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, Exception given.', diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 57d68802f9..f1e6d16f50 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -633,8 +633,18 @@ public function dataDynamicProperties(): array $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $errors = [ [ - 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 29, + 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', + 14, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', + 15, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', + 16, $tipText, ], ]; @@ -655,21 +665,15 @@ public function dataDynamicProperties(): array 11, $tipText, ], - [ - 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', - 14, - $tipText, - ], - [ - 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', - 15, - $tipText, - ], - [ - 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', - 16, - $tipText, - ], + ], $errors); + + $errors[] = [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 29, + $tipText, + ]; + + $errorsWithMore = array_merge($errorsWithMore, [ [ 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', 20, @@ -685,7 +689,12 @@ public function dataDynamicProperties(): array 22, $tipText, ], - ], $errors); + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 29, + $tipText, + ], + ]); $errorsWithMore = array_merge($errorsWithMore, [ [ @@ -808,12 +817,12 @@ public function testPhp82AndDynamicProperties(bool $b): void 34, $tipText, ]; + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', + 71, + $tipText, + ]; if ($b) { - $errors[] = [ - 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', - 71, - $tipText, - ]; $errors[] = [ 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', 78, @@ -997,4 +1006,22 @@ public function testAsymmetricVisibility(): void $this->analyse([__DIR__ . '/data/read-asymmetric-visibility.php'], []); } + public function testNewIsAlwaysFinalClass(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2.'); + } + + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/null-coalesce-new-is-always-final.php'], [ + [ + 'Access to an undefined property NullCoalesceIsAlwaysFinal\Foo::$bar.', + 12, + 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/null-coalesce-new-is-always-final.php b/tests/PHPStan/Rules/Properties/data/null-coalesce-new-is-always-final.php new file mode 100644 index 0000000000..3a02557bd5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/null-coalesce-new-is-always-final.php @@ -0,0 +1,13 @@ +bar ?? 'no'; +}; diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index b615d9deea..7dd88c8e69 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -57,6 +57,7 @@ use Traversable; use function array_map; use function array_reverse; +use function get_class; use function implode; use function sprintf; use const PHP_VERSION_ID; @@ -2740,6 +2741,18 @@ public function dataUnion(): iterable ObjectType::class, $c->getName(), ]; + + $nonFinalClass = $reflectionProvider->getClass(\NullCoalesceIsAlwaysFinal\Foo::class); + $finalClass = $nonFinalClass->asFinal(); + + yield [ + [ + new ObjectType($finalClass->getName(), null, $finalClass), + new ObjectType($nonFinalClass->getName(), null, $nonFinalClass), + ], + ObjectType::class, + $nonFinalClass->getDisplayName(), + ]; } /** @@ -2762,6 +2775,16 @@ public function testUnion( $actualTypeDescription .= '=implicit'; } } + if (get_class($actualType) === ObjectType::class) { + $actualClassReflection = $actualType->getClassReflection(); + if ( + $actualClassReflection !== null + && $actualClassReflection->hasFinalByKeywordOverride() + && $actualClassReflection->isFinal() + ) { + $actualTypeDescription .= '=final'; + } + } $this->assertSame( $expectedTypeDescription, @@ -2809,6 +2832,16 @@ public function testUnionInversed( $actualTypeDescription .= '=implicit'; } } + if (get_class($actualType) === ObjectType::class) { + $actualClassReflection = $actualType->getClassReflection(); + if ( + $actualClassReflection !== null + && $actualClassReflection->hasFinalByKeywordOverride() + && $actualClassReflection->isFinal() + ) { + $actualTypeDescription .= '=final'; + } + } $this->assertSame( $expectedTypeDescription, $actualTypeDescription, @@ -4618,6 +4651,18 @@ public function dataIntersect(): iterable GenericStaticType::class, 'static(PHPStan\Generics\FunctionsAssertType\C)', ]; + + $nonFinalClass = $reflectionProvider->getClass(\NullCoalesceIsAlwaysFinal\Foo::class); + $finalClass = $nonFinalClass->asFinal(); + + yield [ + [ + new ObjectType($finalClass->getName(), null, $finalClass), + new ObjectType($nonFinalClass->getName(), null, $nonFinalClass), + ], + ObjectType::class, + $nonFinalClass->getDisplayName() . '=final', + ]; } /** @@ -4647,6 +4692,18 @@ public function testIntersect( $actualTypeDescription .= '=implicit'; } } + + if (get_class($actualType) === ObjectType::class && $actualType->isEnum()->no()) { + $actualClassReflection = $actualType->getClassReflection(); + if ( + $actualClassReflection !== null + && $actualClassReflection->hasFinalByKeywordOverride() + && $actualClassReflection->isFinal() + ) { + $actualTypeDescription .= '=final'; + } + } + $this->assertSame($expectedTypeDescription, $actualTypeDescription); $this->assertInstanceOf($expectedTypeClass, $actualType); } @@ -4678,6 +4735,17 @@ public function testIntersectInversed( $actualTypeDescription .= '=implicit'; } } + + if (get_class($actualType) === ObjectType::class && $actualType->isEnum()->no()) { + $actualClassReflection = $actualType->getClassReflection(); + if ( + $actualClassReflection !== null + && $actualClassReflection->hasFinalByKeywordOverride() + && $actualClassReflection->isFinal() + ) { + $actualTypeDescription .= '=final'; + } + } $this->assertSame($expectedTypeDescription, $actualTypeDescription); $this->assertInstanceOf($expectedTypeClass, $actualType); }