diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 064cec277ec..dc0f262cac8 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1909,7 +1909,12 @@ public function isGeneric(): bool return $this->isGeneric = false; } - $this->isGeneric = count($this->getTemplateTags()) > 0; + $isGeneric = count($this->getTemplateTags()) > 0; + if ($this->fileTypeMapper->isClassNameScopeBeingCreated($this->getName())) { + return $isGeneric; + } + + $this->isGeneric = $isGeneric; } return $this->isGeneric; @@ -2145,7 +2150,12 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock return $this->resolvedPhpDocBlock; } - return $this->resolvedPhpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $this->getName(), null, null, $this->reflectionDocComment); + $resolvedPhpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $this->getName(), null, null, $this->reflectionDocComment); + if ($this->fileTypeMapper->isClassNameScopeBeingCreated($this->getName())) { + return $resolvedPhpDocBlock; + } + + return $this->resolvedPhpDocBlock = $resolvedPhpDocBlock; } public function getTraitContextResolvedPhpDoc(self $implementingClass): ?ResolvedPhpDocBlock diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 88c83cac563..f636fa80fce 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '28c29b8'; + public const EXPECTED_EXTENSION_VERSION = '075bda9'; private static bool $active = false; diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 673a2bb89ba..39c8bbbf315 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -68,6 +68,9 @@ final class FileTypeMapper /** @var array */ private array $inProcessNameScopes = []; + /** @var array */ + private array $classNameScopesBeingCreated = []; + /** @var array */ private array $resolvedPhpDocBlockCache = []; @@ -151,6 +154,15 @@ public function getResolvedPhpDoc( ); } + /** + * While a class's name scope is being created, the PHPDoc resolved for that class + * does not have its template tags yet. + */ + public function isClassNameScopeBeingCreated(string $className): bool + { + return isset($this->classNameScopesBeingCreated[$className]); + } + private function createResolvedPhpDocBlock( PhpDocNode $phpDocNode, NameScope $nameScope, @@ -240,6 +252,10 @@ public function getNameScope( $intermediaryNameScope = $nameScopeMap[$nameScopeKey]; $this->inProcess[$nameScopeKey] = true; + $classNameScope = $className !== null && $traitName === null && $functionName === null ? $className : null; + if ($classNameScope !== null) { + $this->classNameScopesBeingCreated[$classNameScope] = ($this->classNameScopesBeingCreated[$classNameScope] ?? 0) + 1; + } try { $parents = [$intermediaryNameScope]; @@ -351,6 +367,9 @@ public function getNameScope( } finally { unset($this->inProcess[$nameScopeKey]); unset($this->inProcessNameScopes[$nameScopeKey]); + if ($classNameScope !== null && --$this->classNameScopesBeingCreated[$classNameScope] === 0) { + unset($this->classNameScopesBeingCreated[$classNameScope]); + } } } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index eb6149da32e..eb674dfe08a 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1392,15 +1392,17 @@ public function testBug10049(): void { // crash $errors = $this->runAnalyse(__DIR__ . '/data/bug-10049-recursive.php'); - $this->assertCount(4, $errors); + $this->assertCount(5, $errors); $this->assertSame('PHPDoc tag @template for class Bug10049Recursive\SimpleEntity cannot have existing class Bug10049Recursive\SimpleEntity as its name.', $errors[0]->getMessage()); $this->assertSame(8, $errors[0]->getLine()); $this->assertSame('Type Bug10049Recursive\SimpleEntity in generic type Bug10049Recursive\SimpleEntity in PHPDoc tag @template Bug10049Recursive\SimpleEntity is not subtype of template type SELF of Bug10049Recursive\SimpleEntity of class Bug10049Recursive\SimpleEntity.', $errors[1]->getMessage()); $this->assertSame(8, $errors[1]->getLine()); - $this->assertSame('Type Bug10049Recursive\TestEntity in generic type Bug10049Recursive\SimpleEntity in PHPDoc tag @extends is not subtype of template type SELF of Bug10049Recursive\SimpleEntity of class Bug10049Recursive\SimpleEntity.', $errors[2]->getMessage()); - $this->assertSame(46, $errors[2]->getLine()); - $this->assertSame('Type Bug10049Recursive\AnotherEntity in generic type Bug10049Recursive\SimpleEntity in PHPDoc tag @extends is not subtype of template type SELF of Bug10049Recursive\SimpleEntity of class Bug10049Recursive\SimpleEntity.', $errors[3]->getMessage()); - $this->assertSame(59, $errors[3]->getLine()); + $this->assertSame('Method Bug10049Recursive\SimpleEntity::__construct() has parameter $table with generic class Bug10049Recursive\SimpleEntity but does not specify its types: SELF', $errors[2]->getMessage()); + $this->assertSame(13, $errors[2]->getLine()); + $this->assertSame('Type Bug10049Recursive\TestEntity in generic type Bug10049Recursive\SimpleEntity in PHPDoc tag @extends is not subtype of template type SELF of Bug10049Recursive\SimpleEntity of class Bug10049Recursive\SimpleEntity.', $errors[3]->getMessage()); + $this->assertSame(46, $errors[3]->getLine()); + $this->assertSame('Type Bug10049Recursive\AnotherEntity in generic type Bug10049Recursive\SimpleEntity in PHPDoc tag @extends is not subtype of template type SELF of Bug10049Recursive\SimpleEntity of class Bug10049Recursive\SimpleEntity.', $errors[4]->getMessage()); + $this->assertSame(59, $errors[4]->getLine()); } #[RequiresPhp('>= 8.0.0')] diff --git a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php index ed6e20e2c2d..aa3c3843f0e 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -653,4 +653,16 @@ public function testUnconstrainedQueryResult(): void $this->analyse([__DIR__ . '/../Methods/data/unconstrained-query-result.php'], []); } + public function testBug15448(): void + { + $this->checkTypeAgainstPhpDocType = true; + $this->analyse([__DIR__ . '/data/bug-15448.php'], []); + } + + public function testBug15448ClassNotAnalysed(): void + { + $this->checkTypeAgainstPhpDocType = true; + $this->analyse([__DIR__ . '/data/bug-15448-dependency-usage.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-15448-dependency-usage.php b/tests/PHPStan/Rules/PhpDoc/data/bug-15448-dependency-usage.php new file mode 100644 index 00000000000..ae0388d0e5d --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-15448-dependency-usage.php @@ -0,0 +1,15 @@ += 8.0 + +namespace Bug15448Dependency; + +class Error +{ +} + +/** + * @template T of Error|ErrorIterator + * @implements \Iterator + */ +class ErrorIterator implements \Iterator +{ + + /** @param list $errors */ + public function __construct(private array $errors) + { + } + + /** @return T */ + public function current(): Error|self + { + return $this->errors[0]; + } + + public function next(): void + { + } + + public function key(): int + { + return 0; + } + + public function valid(): bool + { + return false; + } + + public function rewind(): void + { + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-15448.php b/tests/PHPStan/Rules/PhpDoc/data/bug-15448.php new file mode 100644 index 00000000000..c59dc4decfa --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-15448.php @@ -0,0 +1,26 @@ += 8.0 + +namespace Bug15448; + +class Error +{ +} + +/** + * @template T of Error|ErrorIterator + * @implements \Iterator + */ +abstract class ErrorIterator implements \Iterator +{ + + /** @return T */ + abstract public function current(): Error|self; + +} + +function errors(ErrorIterator $errors): void +{ + /** @var Error $error */ + foreach ($errors as $error) { + } +} diff --git a/turbo-ext/src/ClassReflection.cpp b/turbo-ext/src/ClassReflection.cpp index 34c5e3297d7..cfe53ba5343 100644 --- a/turbo-ext/src/ClassReflection.cpp +++ b/turbo-ext/src/ClassReflection.cpp @@ -3985,11 +3985,25 @@ class ClassReflection zv::Args args{fileName.raw(), name.raw(), zv::null, zv::null, slot(PT_CR_PROP_REFLECTION_DOC_COMMENT).raw()}; zv::Val resolved = callService(PT_CR_PROP_FILE_TYPE_MAPPER, "fileTypeMapper", PT_LC("getresolvedphpdoc"), 5, args); if (UNEXPECTED(resolved.isUndef())) return zv::Val(); + bool beingCreated; + if (UNEXPECTED(!isClassNameScopeBeingCreated(beingCreated))) return zv::Val(); + if (beingCreated) return resolved; writeSlot(PT_CR_PROP_RESOLVED_PHP_DOC_BLOCK, zv::Val::copyOf(resolved.ref())); return resolved; } + /* $this->fileTypeMapper->isClassNameScopeBeingCreated($this->getName()); false = pending exception */ + bool isClassNameScopeBeingCreated(bool &out) + { + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return false; + zv::Val result = callService(PT_CR_PROP_FILE_TYPE_MAPPER, "fileTypeMapper", PT_LC("isclassnamescopebeingcreated"), 1, name.raw()); + if (UNEXPECTED(result.isUndef())) return false; + out = result.ref().isTrue(); + return true; + } + /* ?ResolvedPhpDocBlock */ zv::Val getTraitContextResolvedPhpDoc(zv::Ref implementingClass) { @@ -4318,7 +4332,14 @@ class ClassReflection zv::Val templateTags = getTemplateTags(); if (UNEXPECTED(templateTags.isUndef())) return false; - writeSlot(PT_CR_PROP_IS_GENERIC, zv::Val::boolean(countOf(templateTags.ref()) > 0)); + bool isGeneric_ = countOf(templateTags.ref()) > 0; + bool beingCreated; + if (UNEXPECTED(!isClassNameScopeBeingCreated(beingCreated))) return false; + if (beingCreated) { + out = isGeneric_; + return true; + } + writeSlot(PT_CR_PROP_IS_GENERIC, zv::Val::boolean(isGeneric_)); } out = slot(PT_CR_PROP_IS_GENERIC).isTrue();