diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index bb8a4347e6..7f31372b5a 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -28,7 +28,7 @@ class NameScope * @param array $constUses alias(string) => fullName(string) * @param array $typeAliasesMap */ - public function __construct(private ?string $namespace, private array $uses, private ?string $className = null, private ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, private array $typeAliasesMap = [], private bool $bypassTypeAliases = false, private array $constUses = []) + public function __construct(private ?string $namespace, private array $uses, private ?string $className = null, private ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, private array $typeAliasesMap = [], private bool $bypassTypeAliases = false, private array $constUses = [], private ?string $typeAliasClassName = null) { $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); } @@ -64,6 +64,11 @@ public function getClassName(): ?string return $this->className; } + public function getClassNameForTypeAlias(): ?string + { + return $this->typeAliasClassName ?? $this->className; + } + public function resolveStringName(string $name): string { if (strpos($name, '\\') === 0) { diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index a6988fb3e3..be622c5303 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -269,6 +269,8 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA } $traitFound = true; + $typeAliasStack[] = $this->getTypeAliasesMap($node->getDocComment()); + $functionStack[] = null; } else { if ($node->name === null) { if (!$node instanceof Node\Stmt\Class_) { @@ -304,12 +306,12 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { $phpDocString = GetLastDocComment::forNode($node); if ($phpDocString !== null) { - $typeMapStack[] = function () use ($namespace, $uses, $className, $functionName, $phpDocString, $typeMapStack, $typeAliasStack, $constUses): TemplateTypeMap { + $typeMapStack[] = function () use ($namespace, $uses, $className, $lookForTrait, $functionName, $phpDocString, $typeMapStack, $typeAliasStack, $constUses): TemplateTypeMap { $phpDocNode = $this->resolvePhpDocStringToDocNode($phpDocString); $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null; $currentTypeMap = $typeMapCb !== null ? $typeMapCb() : null; $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? []; - $nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap, $typeAliasesMap, false, $constUses); + $nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap, $typeAliasesMap, false, $constUses, $lookForTrait); $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); $templateTypeScope = $nameScope->getTemplateTypeScope(); if ($templateTypeScope === null) { @@ -355,6 +357,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $typeAliasesMap, false, $constUses, + $lookForTrait, ); } @@ -492,8 +495,8 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA return null; }, - static function (Node $node, $callbackResult) use ($lookForTrait, &$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack, &$constUses): void { - if ($node instanceof Node\Stmt\ClassLike && $lookForTrait === null) { + static function (Node $node, $callbackResult) use (&$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack, &$constUses): void { + if ($node instanceof Node\Stmt\ClassLike) { if (count($classStack) === 0) { throw new ShouldNotHappenException(); } diff --git a/src/Type/UsefulTypeAliasResolver.php b/src/Type/UsefulTypeAliasResolver.php index 6cfb76d373..191bad9794 100644 --- a/src/Type/UsefulTypeAliasResolver.php +++ b/src/Type/UsefulTypeAliasResolver.php @@ -69,7 +69,7 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): return null; } - $className = $nameScope->getClassName(); + $className = $nameScope->getClassNameForTypeAlias(); if ($className === null) { return null; } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 82d02315f7..63ff28dbb8 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1170,6 +1170,12 @@ public function testBug9008(): void $this->assertNoErrors($errors); } + public function testBug5091(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5091.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index cc0ae66602..d717ae12df 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1215,6 +1215,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8956.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8917.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/ds-copy.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/trait-type-alias.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-5091.php b/tests/PHPStan/Analyser/data/bug-5091.php new file mode 100644 index 0000000000..626843d9cf --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5091.php @@ -0,0 +1,175 @@ + '']; + } + } +} + +namespace Bug5091 { + + /** + * @phpstan-type MyType array{foobar: string} + */ + trait MyTrait + { + /** + * @return array + */ + public function MyMethod(): array + { + return [['foobar' => 'foo']]; + } + } + + class MyClass + { + use MyTrait; + } + + /** + * @phpstan-type TypeArrayAjaxResponse array{ + * message : string, + * status : int, + * success : bool, + * value : null|float|int|string, + * } + */ + trait MyTrait2 + { + /** @return TypeArrayAjaxResponse */ + protected function getAjaxResponse(): array + { + return [ + "message" => "test", + "status" => 200, + "success" => true, + "value" => 5, + ]; + } + } + + class MyController + { + use MyTrait2; + } + + + /** + * @phpstan-type X string + */ + class Types {} + + /** + * @phpstan-import-type X from Types + */ + trait t { + /** @return X */ + public function getX() { + return "123"; + } + } + + class aClass + { + use t; + } + + /** + * @phpstan-import-type X from Types + */ + class Z { + /** @return X */ + public function getX() { // works as expected + return "123"; + } + } + + /** + * @phpstan-type SomePhpstanType array{ + * property: mixed + * } + */ + trait TraitWithType + { + /** + * @phpstan-return SomePhpstanType + */ + protected function get(): array + { + return [ + 'property' => 'something', + ]; + } + } + + /** + * @phpstan-import-type SomePhpstanType from TraitWithType + */ + class ClassWithTraitWithType + { + use TraitWithType; + + /** + * @phpstan-return SomePhpstanType + */ + public function SomeMethod(): array + { + return $this->get(); + } + } + + /** + * @phpstan-type FooJson array{bar: string} + */ + trait Foo { + /** + * @phpstan-return FooJson + */ + public function sayHello(\DateTime $date): array + { + return [ + 'bar'=> 'baz' + ]; + } + } + + /** + * @phpstan-import-type FooJson from Foo + */ + class HelloWorld + { + use Foo; + } + +} diff --git a/tests/PHPStan/Analyser/data/trait-type-alias.php b/tests/PHPStan/Analyser/data/trait-type-alias.php new file mode 100644 index 0000000000..b83fe210fa --- /dev/null +++ b/tests/PHPStan/Analyser/data/trait-type-alias.php @@ -0,0 +1,54 @@ +