Skip to content

Commit 8766923

Browse files
committed
Support for generic traits and specifying template types with @use
1 parent ace100c commit 8766923

9 files changed

+623
-59
lines changed

conf/config.level2.neon

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ rules:
2121
- PHPStan\Rules\Generics\MethodTemplateTypeRule
2222
- PHPStan\Rules\Generics\MethodSignatureVarianceRule
2323
- PHPStan\Rules\Generics\TraitTemplateTypeRule
24+
- PHPStan\Rules\Generics\UsedTraitsRule
2425
- PHPStan\Rules\Methods\IncompatibleDefaultParameterTypeRule
2526
- PHPStan\Rules\Operators\InvalidBinaryOperationRule
2627
- PHPStan\Rules\Operators\InvalidUnaryOperationRule

src/Rules/Generics/GenericAncestorsCheck.php

+1-4
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,7 @@ public function check(
9090
$messages = array_merge($messages, $genericObjectTypeCheckMessages);
9191

9292
foreach ($ancestorType->getReferencedClasses() as $referencedClass) {
93-
if (
94-
$this->reflectionProvider->hasClass($referencedClass)
95-
&& !$this->reflectionProvider->getClass($referencedClass)->isTrait()
96-
) {
93+
if ($this->reflectionProvider->hasClass($referencedClass)) {
9794
continue;
9895
}
9996

src/Rules/Generics/UsedTraitsRule.php

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Generics;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\PhpDoc\Tag\UsesTag;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Type\FileTypeMapper;
10+
use PHPStan\Type\Type;
11+
12+
/**
13+
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\TraitUse>
14+
*/
15+
class UsedTraitsRule implements Rule
16+
{
17+
18+
private \PHPStan\Type\FileTypeMapper $fileTypeMapper;
19+
20+
private \PHPStan\Rules\Generics\GenericAncestorsCheck $genericAncestorsCheck;
21+
22+
public function __construct(
23+
FileTypeMapper $fileTypeMapper,
24+
GenericAncestorsCheck $genericAncestorsCheck
25+
)
26+
{
27+
$this->fileTypeMapper = $fileTypeMapper;
28+
$this->genericAncestorsCheck = $genericAncestorsCheck;
29+
}
30+
31+
public function getNodeType(): string
32+
{
33+
return Node\Stmt\TraitUse::class;
34+
}
35+
36+
public function processNode(Node $node, Scope $scope): array
37+
{
38+
if (!$scope->isInClass()) {
39+
throw new \PHPStan\ShouldNotHappenException();
40+
}
41+
42+
$className = $scope->getClassReflection()->getName();
43+
$traitName = null;
44+
if ($scope->isInTrait()) {
45+
$traitName = $scope->getTraitReflection()->getName();
46+
}
47+
$useTags = [];
48+
$docComment = $node->getDocComment();
49+
if ($docComment !== null) {
50+
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
51+
$scope->getFile(),
52+
$className,
53+
$traitName,
54+
null,
55+
$docComment->getText()
56+
);
57+
$useTags = $resolvedPhpDoc->getUsesTags();
58+
}
59+
60+
$description = sprintf('class %s', $className);
61+
$typeDescription = 'class';
62+
if ($traitName !== null) {
63+
$description = sprintf('trait %s', $traitName);
64+
$typeDescription = 'trait';
65+
}
66+
67+
return $this->genericAncestorsCheck->check(
68+
$node->traits,
69+
array_map(static function (UsesTag $tag): Type {
70+
return $tag->getType();
71+
}, $useTags),
72+
sprintf('%s @use tag contains incompatible type %%s.', ucfirst($description)),
73+
sprintf('%s has @use tag, but does not use any trait.', ucfirst($description)),
74+
sprintf('The @use tag of %s describes %%s but the %s uses %%s.', $description, $typeDescription),
75+
'PHPDoc tag @use contains generic type %s but trait %s is not generic.',
76+
'Generic type %s in PHPDoc tag @use does not specify all template types of trait %s: %s',
77+
'Generic type %s in PHPDoc tag @use specifies %d template types, but trait %s supports only %d: %s',
78+
'Type %s in generic type %s in PHPDoc tag @use is not subtype of template type %s of trait %s.',
79+
'PHPDoc tag @use has invalid type %s.',
80+
sprintf('%s uses generic trait %%s but does not specify its types: %%s', ucfirst($description)),
81+
sprintf('in used type %%s of %s', $description)
82+
);
83+
}
84+
85+
}

src/Type/FileTypeMapper.php

+111-55
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
1717
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
1818
use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider;
19+
use PHPStan\Type\Generic\GenericObjectType;
1920
use PHPStan\Type\Generic\TemplateType;
2021
use PHPStan\Type\Generic\TemplateTypeFactory;
22+
use PHPStan\Type\Generic\TemplateTypeHelper;
2123
use PHPStan\Type\Generic\TemplateTypeMap;
2224
use function array_key_exists;
2325
use function file_exists;
@@ -214,7 +216,7 @@ private function shouldPhpDocNodeBeCachedToDisk(PhpDocNode $phpDocNode): bool
214216
private function getResolvedPhpDocMap(string $fileName): array
215217
{
216218
if (!isset($this->memoryCache[$fileName])) {
217-
$cacheKey = sprintf('%s-phpdocstring-v6-generic-bound', $fileName);
219+
$cacheKey = sprintf('%s-phpdocstring-v7-generic-traits', $fileName);
218220
$variableCacheKey = implode(',', array_map(static function (array $file): string {
219221
return sprintf('%s-%d', $file['filename'], $file['modifiedTime']);
220222
}, $this->getCachedDependentFilesWithTimestamps($fileName)));
@@ -313,56 +315,7 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia
313315
$resolvableTemplateTypes = true;
314316
}
315317
} elseif ($node instanceof Node\Stmt\TraitUse) {
316-
$traitMethodAliases = [];
317-
foreach ($node->adaptations as $traitUseAdaptation) {
318-
if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
319-
continue;
320-
}
321-
322-
if ($traitUseAdaptation->trait === null) {
323-
continue;
324-
}
325-
326-
if ($traitUseAdaptation->newName === null) {
327-
continue;
328-
}
329-
330-
$traitMethodAliases[$traitUseAdaptation->trait->toString()][$traitUseAdaptation->method->toString()] = $traitUseAdaptation->newName->toString();
331-
}
332-
333-
foreach ($node->traits as $traitName) {
334-
/** @var class-string $traitName */
335-
$traitName = (string) $traitName;
336-
$reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider();
337-
if (!$reflectionProvider->hasClass($traitName)) {
338-
continue;
339-
}
340-
341-
$traitReflection = $reflectionProvider->getClass($traitName);
342-
if (!$traitReflection->isTrait()) {
343-
continue;
344-
}
345-
if ($traitReflection->getFileName() === false) {
346-
continue;
347-
}
348-
if (!file_exists($traitReflection->getFileName())) {
349-
continue;
350-
}
351-
352-
$className = $classStack[count($classStack) - 1] ?? null;
353-
if ($className === null) {
354-
throw new \PHPStan\ShouldNotHappenException();
355-
}
356-
357-
$traitPhpDocMap = $this->createFilePhpDocMap(
358-
$traitReflection->getFileName(),
359-
$traitName,
360-
$className,
361-
$traitMethodAliases[$traitName] ?? []
362-
);
363-
$phpDocMap = array_merge($phpDocMap, $traitPhpDocMap);
364-
}
365-
return null;
318+
$resolvableTemplateTypes = true;
366319
} elseif ($node instanceof Node\Stmt\ClassMethod) {
367320
$functionName = $node->name->name;
368321
if (array_key_exists($functionName, $traitMethodAliases)) {
@@ -431,10 +384,6 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia
431384
}
432385

433386
$typeMapStack[] = function () use ($fileName, $className, $lookForTrait, $functionName, $phpDocString, $typeMapCb): TemplateTypeMap {
434-
static $typeMap = null;
435-
if ($typeMap !== null) {
436-
return $typeMap;
437-
}
438387
$resolvedPhpDoc = $this->getResolvedPhpDoc(
439388
$fileName,
440389
$className,
@@ -466,6 +415,113 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia
466415

467416
$uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
468417
}
418+
} elseif ($node instanceof Node\Stmt\TraitUse) {
419+
$traitMethodAliases = [];
420+
foreach ($node->adaptations as $traitUseAdaptation) {
421+
if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
422+
continue;
423+
}
424+
425+
if ($traitUseAdaptation->trait === null) {
426+
continue;
427+
}
428+
429+
if ($traitUseAdaptation->newName === null) {
430+
continue;
431+
}
432+
433+
$traitMethodAliases[$traitUseAdaptation->trait->toString()][$traitUseAdaptation->method->toString()] = $traitUseAdaptation->newName->toString();
434+
}
435+
436+
$useDocComment = null;
437+
if ($node->getDocComment() !== null) {
438+
$useDocComment = $node->getDocComment()->getText();
439+
}
440+
441+
foreach ($node->traits as $traitName) {
442+
/** @var class-string $traitName */
443+
$traitName = (string) $traitName;
444+
$reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider();
445+
if (!$reflectionProvider->hasClass($traitName)) {
446+
continue;
447+
}
448+
449+
$traitReflection = $reflectionProvider->getClass($traitName);
450+
if (!$traitReflection->isTrait()) {
451+
continue;
452+
}
453+
if ($traitReflection->getFileName() === false) {
454+
continue;
455+
}
456+
if (!file_exists($traitReflection->getFileName())) {
457+
continue;
458+
}
459+
460+
$className = $classStack[count($classStack) - 1] ?? null;
461+
if ($className === null) {
462+
throw new \PHPStan\ShouldNotHappenException();
463+
}
464+
465+
$traitPhpDocMap = $this->createFilePhpDocMap(
466+
$traitReflection->getFileName(),
467+
$traitName,
468+
$className,
469+
$traitMethodAliases[$traitName] ?? []
470+
);
471+
$finalTraitPhpDocMap = [];
472+
foreach ($traitPhpDocMap as $phpDocKey => $callback) {
473+
$finalTraitPhpDocMap[$phpDocKey] = function () use ($callback, $traitReflection, $fileName, $className, $lookForTrait, $useDocComment): NameScopedPhpDocString {
474+
/** @var NameScopedPhpDocString $original */
475+
$original = $callback();
476+
if (!$traitReflection->isGeneric()) {
477+
return $original;
478+
}
479+
480+
$traitTemplateTypeMap = $traitReflection->getTemplateTypeMap();
481+
482+
$useType = null;
483+
if ($useDocComment !== null) {
484+
$useTags = $this->getResolvedPhpDoc(
485+
$fileName,
486+
$className,
487+
$lookForTrait,
488+
null,
489+
$useDocComment
490+
)->getUsesTags();
491+
foreach ($useTags as $useTag) {
492+
$useTagType = $useTag->getType();
493+
if (!$useTagType instanceof GenericObjectType) {
494+
continue;
495+
}
496+
497+
if ($useTagType->getClassName() !== $traitReflection->getName()) {
498+
continue;
499+
}
500+
501+
$useType = $useTagType;
502+
break;
503+
}
504+
}
505+
506+
if ($useType === null) {
507+
return new NameScopedPhpDocString(
508+
$original->getPhpDocString(),
509+
$original->getNameScope()->withTemplateTypeMap($traitTemplateTypeMap->resolveToBounds())
510+
);
511+
}
512+
513+
$transformedTraitTypeMap = $traitReflection->typeMapFromList($useType->getTypes());
514+
515+
return new NameScopedPhpDocString(
516+
$original->getPhpDocString(),
517+
$original->getNameScope()->withTemplateTypeMap($traitTemplateTypeMap->map(static function (string $name, Type $type) use ($transformedTraitTypeMap): Type {
518+
return TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap);
519+
}))
520+
);
521+
};
522+
}
523+
$phpDocMap = array_merge($phpDocMap, $finalTraitPhpDocMap);
524+
}
469525
}
470526

471527
return null;

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+12
Original file line numberDiff line numberDiff line change
@@ -5636,6 +5636,16 @@ public function dataPseudoTypeGlobal(): array
56365636
return $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-global.php');
56375637
}
56385638

5639+
public function dataGenericTraits(): array
5640+
{
5641+
return $this->gatherAssertTypes(__DIR__ . '/data/generic-traits.php');
5642+
}
5643+
5644+
public function dataBug4423(): array
5645+
{
5646+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4423.php');
5647+
}
5648+
56395649
/**
56405650
* @dataProvider dataArrayFunctions
56415651
* @param string $description
@@ -11246,6 +11256,8 @@ private function gatherAssertTypes(string $file): array
1124611256
* @dataProvider dataPseudoTypeGlobal
1124711257
* @dataProvider dataPseudoTypeNamespace
1124811258
* @dataProvider dataPseudoTypeOverrides
11259+
* @dataProvider dataGenericTraits
11260+
* @dataProvider dataBug4423
1124911261
* @param string $assertType
1125011262
* @param string $file
1125111263
* @param mixed ...$args
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug4423;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
/**
8+
* @template T
9+
*/
10+
class Bar {}
11+
12+
/**
13+
* @template K
14+
* @property-read Bar<K> $bar
15+
* @method Bar<K> doBar()
16+
*/
17+
trait Foo {
18+
19+
/** @var Bar<K> */
20+
public $baz;
21+
22+
/** @param K $k */
23+
public function doFoo($k)
24+
{
25+
assertType('T (class Bug4423\Child, argument)', $k);
26+
assertType('Bug4423\Bar<T (class Bug4423\Child, argument)>', $this->bar);
27+
assertType('Bug4423\Bar<T (class Bug4423\Child, argument)>', $this->baz);
28+
assertType('Bug4423\Bar<T (class Bug4423\Child, argument)>', $this->doBar());
29+
assertType('Bug4423\Bar<T (class Bug4423\Child, argument)>', $this->doBaz());
30+
}
31+
32+
/** @return Bar<K> */
33+
public function doBaz()
34+
{
35+
36+
}
37+
38+
}
39+
40+
/**
41+
* @template T
42+
* @template K
43+
*/
44+
class Base {
45+
46+
}
47+
48+
/**
49+
* @template T
50+
* @extends Base<int, T>
51+
*/
52+
class Child extends Base {
53+
/** @phpstan-use Foo<T> */
54+
use Foo;
55+
}
56+
57+
function (Child $child): void {
58+
/** @var Child<int> $child */
59+
assertType('Bug4423\Child<int>', $child);
60+
assertType('Bug4423\Bar<int>', $child->bar);
61+
assertType('Bug4423\Bar<int>', $child->baz);
62+
assertType('Bug4423\Bar<int>', $child->doBar());
63+
assertType('Bug4423\Bar<int>', $child->doBaz());
64+
};

0 commit comments

Comments
 (0)