diff --git a/src/Reflection/ClassConstantReflection.php b/src/Reflection/ClassConstantReflection.php index 097082b7618..3f626da01e3 100644 --- a/src/Reflection/ClassConstantReflection.php +++ b/src/Reflection/ClassConstantReflection.php @@ -40,4 +40,8 @@ public function getNativeType(): ?Type; public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock; + public function getInitializerExprType(): Type; + + public function getTypeByStaticAccess(bool $isFinalClass): Type; + } diff --git a/src/Reflection/Dummy/DummyClassConstantReflection.php b/src/Reflection/Dummy/DummyClassConstantReflection.php index 768c5bdf275..b14a44c21d4 100644 --- a/src/Reflection/Dummy/DummyClassConstantReflection.php +++ b/src/Reflection/Dummy/DummyClassConstantReflection.php @@ -122,4 +122,14 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock return null; } + public function getInitializerExprType(): Type + { + return new MixedType(); + } + + public function getTypeByStaticAccess(bool $isFinalClass): Type + { + return new MixedType(); + } + } diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 88e19731792..98c9e3632dd 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -2566,20 +2566,10 @@ function (Type $type, callable $traverse): Type { } $constantReflection = $constantClassReflection->getConstant($constantName); - if ( - !$constantClassReflection->isFinal() - && !$constantReflection->isFinal() - && !$constantReflection->hasPhpDocType() - && !$constantReflection->hasNativeType() - ) { + $constantType = $constantReflection->getTypeByStaticAccess($constantClassReflection->isFinal()); + if ($constantType instanceof MixedType) { unset($this->currentlyResolvingClassConstant[$resolvingName]); - return new MixedType(); - } - - if (!$constantClassReflection->isFinal()) { - $constantType = $constantReflection->getValueType(); - } else { - $constantType = $this->getType($constantReflection->getValueExpr(), InitializerExprContext::fromClassReflection($constantReflection->getDeclaringClass())); + return $constantType; } $nativeType = $constantReflection->getNativeType(); diff --git a/src/Reflection/RealClassClassConstantReflection.php b/src/Reflection/RealClassClassConstantReflection.php index d0b69f5eedc..25095881fc8 100644 --- a/src/Reflection/RealClassClassConstantReflection.php +++ b/src/Reflection/RealClassClassConstantReflection.php @@ -7,6 +7,7 @@ use PHPStan\Internal\DeprecatedAttributeHelper; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; @@ -91,6 +92,24 @@ public function getValueType(): Type return $this->valueType; } + public function getInitializerExprType(): Type + { + return $this->initializerExprTypeResolver->getType($this->getValueExpr(), InitializerExprContext::fromClassReflection($this->declaringClass)); + } + + public function getTypeByStaticAccess(bool $isFinalClass): Type + { + if ($isFinalClass || $this->isFinal()) { + return $this->getInitializerExprType(); + } + + if (!$this->hasPhpDocType() && !$this->hasNativeType()) { + return new MixedType(); + } + + return $this->getValueType(); + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php index a92a6172737..8f47e07c977 100644 --- a/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php @@ -119,4 +119,14 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock return $this->constantReflection->getResolvedPhpDoc(); } + public function getInitializerExprType(): Type + { + return $this->constantReflection->getInitializerExprType(); + } + + public function getTypeByStaticAccess(bool $isFinalClass): Type + { + return $this->constantReflection->getTypeByStaticAccess($isFinalClass); + } + } diff --git a/src/Type/ClassConstantAccessType.php b/src/Type/ClassConstantAccessType.php index e3be822f447..e284fa65143 100644 --- a/src/Type/ClassConstantAccessType.php +++ b/src/Type/ClassConstantAccessType.php @@ -8,6 +8,7 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\LateResolvableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use function count; final class ClassConstantAccessType implements CompoundType, LateResolvableType { @@ -49,13 +50,44 @@ public function isResolvable(): bool return !TypeUtils::containsTemplateType($this->type); } - protected function getResult(): Type + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($this->type->hasConstant($this->constantName)->yes()) { - return $this->type->getConstant($this->constantName)->getValueType(); + $valueType = $this->type->getConstant($this->constantName)->getValueType(); + return $otherType->isSuperTypeOf($valueType); + } + + return $otherType->isSuperTypeOf($this->resolve()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + if ($this->type->hasConstant($this->constantName)->yes()) { + $valueType = $this->type->getConstant($this->constantName)->getValueType(); + return $acceptingType->accepts($valueType, $strictTypes); + } + + $result = $this->resolve(); + + if ($result instanceof CompoundType) { + return $result->isAcceptedBy($acceptingType, $strictTypes); } - return new ErrorType(); + return $acceptingType->accepts($result, $strictTypes); + } + + protected function getResult(): Type + { + if (!$this->type->hasConstant($this->constantName)->yes()) { + return new ErrorType(); + } + + $constantReflection = $this->type->getConstant($this->constantName); + + $classReflections = $this->type->getObjectClassReflections(); + $isFinalClass = count($classReflections) === 1 && $classReflections[0]->isFinal(); + + return $constantReflection->getTypeByStaticAccess($isFinalClass); } /** @@ -89,7 +121,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type public function toPhpDocNode(): TypeNode { - return new ConstTypeNode(new ConstFetchNode('static', $this->constantName)); + return new ConstTypeNode(new ConstFetchNode((string) $this->type->toPhpDocNode(), $this->constantName)); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-13828.php b/tests/PHPStan/Analyser/nsrt/bug-13828.php index 551b694536b..71dd98fc982 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13828.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13828.php @@ -22,8 +22,8 @@ class BarBaz extends FooBar function test(FooBar $foo, BarBaz $bar): void { - assertType("'foo'", $foo->test()); - assertType("'bar'", $bar->test()); + assertType('mixed', $foo->test()); + assertType('mixed', $bar->test()); } final class FinalFoo @@ -146,7 +146,7 @@ public function test(): string function testUntypedConstant(WithUntypedConstant $foo): void { - assertType("'foo'", $foo->test()); + assertType('mixed', $foo->test()); } final class FinalChild extends FooBar @@ -173,5 +173,88 @@ public function test(): string function testFinalTypedConstant(WithFinalTypedConstant $foo): void { - assertType('non-empty-string', $foo->test()); + assertType("'foo'", $foo->test()); +} + +final class FinalClassWithNativeType +{ + const string FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalClassWithNativeType(FinalClassWithNativeType $foo): void +{ + assertType("'foo'", $foo->test()); +} + +final class FinalClassWithPhpDocType +{ + /** @var non-empty-string */ + const FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalClassWithPhpDocType(FinalClassWithPhpDocType $foo): void +{ + assertType("'foo'", $foo->test()); +} + +final class FinalClassWithBothTypes +{ + /** @var non-empty-string */ + const string FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalClassWithBothTypes(FinalClassWithBothTypes $foo): void +{ + assertType("'foo'", $foo->test()); +} + +class WithFinalPhpDocConstant +{ + /** @var non-empty-string */ + final const FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalPhpDocConstant(WithFinalPhpDocConstant $foo): void +{ + assertType("'foo'", $foo->test()); +} + +class WithFinalNativeConstant +{ + final const string FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalNativeConstant(WithFinalNativeConstant $foo): void +{ + assertType("'foo'", $foo->test()); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14556.php b/tests/PHPStan/Analyser/nsrt/bug-14556.php new file mode 100644 index 00000000000..472b7bb447a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14556.php @@ -0,0 +1,99 @@ +test()); + assertType('mixed', $bar->test()); + assertType("'bar'", $baz->test()); +} + +class WithNativeType +{ + const string FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testNativeType(WithNativeType $foo): void +{ + assertType('string', $foo->test()); +} + +class WithPhpDocType +{ + /** @var non-empty-string */ + const FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testPhpDocType(WithPhpDocType $foo): void +{ + assertType('non-empty-string', $foo->test()); +} + +class WithFinalConstant +{ + final const FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalConstant(WithFinalConstant $foo): void +{ + assertType("'foo'", $foo->test()); +} + +class WithFinalTypedConstant +{ + /** @var non-empty-string */ + final const string FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalTypedConstant(WithFinalTypedConstant $foo): void +{ + assertType("'foo'", $foo->test()); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6989.php b/tests/PHPStan/Analyser/nsrt/bug-6989.php index 3ea5dbe4b33..9ce61ee636d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6989.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6989.php @@ -17,7 +17,7 @@ class MyClass */ public function myMethod(array $items1, array $items2, array $items3): array { - assertType('array{key: string}', $items1); + assertType('non-empty-array', $items1); assertType('array{key: string}', $items2); assertType('array{key: string}', $items3); @@ -40,7 +40,7 @@ class ParentClass extends MyClass */ public function myMethod2(array $items1, array $items2, array $items3, array $items4, array $items5): array { - assertType('array{different_key: string}', $items1); + assertType('non-empty-array', $items1); assertType('array{different_key: string}', $items2); assertType('array{key: string}', $items3); assertType('array{different_key: string}', $items4); diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index 3cf9e5f3fa6..df83d849578 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -522,6 +522,18 @@ public static function dataToPhpDocNodeWithoutCheckingEquals(): iterable new ConstantFloatType(-0.0), '-0.0', ]; + + $reflectionProvider = self::createReflectionProvider(); + + yield [ + new ClassConstantAccessType(new StaticType($reflectionProvider->getClass(stdClass::class)), 'FOO'), + 'static::FOO', + ]; + + yield [ + new ClassConstantAccessType(new ObjectType('stdClass'), 'FOO'), + 'stdClass::FOO', + ]; } #[DataProvider('dataToPhpDocNodeWithoutCheckingEquals')]