Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
final class TurboExtensionEnabler
{

public const EXPECTED_EXTENSION_VERSION = '28c29b8';
public const EXPECTED_EXTENSION_VERSION = '075bda9';

private static bool $active = false;

Expand Down
19 changes: 19 additions & 0 deletions src/Type/FileTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ final class FileTypeMapper
/** @var array<string, NameScope> */
private array $inProcessNameScopes = [];

/** @var array<string, int> */
private array $classNameScopesBeingCreated = [];

/** @var array<string, ResolvedPhpDocBlock> */
private array $resolvedPhpDocBlockCache = [];

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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]);
}
}
}

Expand Down
12 changes: 7 additions & 5 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bug10049Recursive\SimpleEntity> in PHPDoc tag @template Bug10049Recursive\SimpleEntity is not subtype of template type SELF of Bug10049Recursive\SimpleEntity<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<Bug10049Recursive\TestEntity> in PHPDoc tag @extends is not subtype of template type SELF of Bug10049Recursive\SimpleEntity<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<Bug10049Recursive\AnotherEntity> in PHPDoc tag @extends is not subtype of template type SELF of Bug10049Recursive\SimpleEntity<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<Bug10049Recursive\TestEntity> in PHPDoc tag @extends is not subtype of template type SELF of Bug10049Recursive\SimpleEntity<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<Bug10049Recursive\AnotherEntity> in PHPDoc tag @extends is not subtype of template type SELF of Bug10049Recursive\SimpleEntity<Bug10049Recursive\SimpleEntity> of class Bug10049Recursive\SimpleEntity.', $errors[4]->getMessage());
$this->assertSame(59, $errors[4]->getLine());
}

#[RequiresPhp('>= 8.0.0')]
Expand Down
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'], []);
}

}
15 changes: 15 additions & 0 deletions tests/PHPStan/Rules/PhpDoc/data/bug-15448-dependency-usage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Bug15448Dependency;

function create(): ErrorIterator
{
return new ErrorIterator([]);
}

function errors(ErrorIterator $errors): void
{
/** @var Error $error */
foreach ($errors as $error) {
}
}
45 changes: 45 additions & 0 deletions tests/PHPStan/Rules/PhpDoc/data/bug-15448-dependency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php // lint >= 8.0

namespace Bug15448Dependency;

class Error
{
}

/**
* @template T of Error|ErrorIterator
* @implements \Iterator<int, T>
*/
class ErrorIterator implements \Iterator
{

/** @param list<T> $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
{
}

}
26 changes: 26 additions & 0 deletions tests/PHPStan/Rules/PhpDoc/data/bug-15448.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php // lint >= 8.0

namespace Bug15448;

class Error
{
}

/**
* @template T of Error|ErrorIterator
* @implements \Iterator<int, T>
*/
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) {
}
}
23 changes: 22 additions & 1 deletion turbo-ext/src/ClassReflection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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();
Expand Down
Loading