Skip to content

Commit

Permalink
Support for generic traits and specifying template types with @use
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Mar 9, 2021
1 parent ace100c commit 8766923
Show file tree
Hide file tree
Showing 9 changed files with 623 additions and 59 deletions.
1 change: 1 addition & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ rules:
- PHPStan\Rules\Generics\MethodTemplateTypeRule
- PHPStan\Rules\Generics\MethodSignatureVarianceRule
- PHPStan\Rules\Generics\TraitTemplateTypeRule
- PHPStan\Rules\Generics\UsedTraitsRule
- PHPStan\Rules\Methods\IncompatibleDefaultParameterTypeRule
- PHPStan\Rules\Operators\InvalidBinaryOperationRule
- PHPStan\Rules\Operators\InvalidUnaryOperationRule
Expand Down
5 changes: 1 addition & 4 deletions src/Rules/Generics/GenericAncestorsCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,7 @@ public function check(
$messages = array_merge($messages, $genericObjectTypeCheckMessages);

foreach ($ancestorType->getReferencedClasses() as $referencedClass) {
if (
$this->reflectionProvider->hasClass($referencedClass)
&& !$this->reflectionProvider->getClass($referencedClass)->isTrait()
) {
if ($this->reflectionProvider->hasClass($referencedClass)) {
continue;
}

Expand Down
85 changes: 85 additions & 0 deletions src/Rules/Generics/UsedTraitsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Generics;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\PhpDoc\Tag\UsesTag;
use PHPStan\Rules\Rule;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Type;

/**
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\TraitUse>
*/
class UsedTraitsRule implements Rule
{

private \PHPStan\Type\FileTypeMapper $fileTypeMapper;

private \PHPStan\Rules\Generics\GenericAncestorsCheck $genericAncestorsCheck;

public function __construct(
FileTypeMapper $fileTypeMapper,
GenericAncestorsCheck $genericAncestorsCheck
)
{
$this->fileTypeMapper = $fileTypeMapper;
$this->genericAncestorsCheck = $genericAncestorsCheck;
}

public function getNodeType(): string
{
return Node\Stmt\TraitUse::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$scope->isInClass()) {
throw new \PHPStan\ShouldNotHappenException();
}

$className = $scope->getClassReflection()->getName();
$traitName = null;
if ($scope->isInTrait()) {
$traitName = $scope->getTraitReflection()->getName();
}
$useTags = [];
$docComment = $node->getDocComment();
if ($docComment !== null) {
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$className,
$traitName,
null,
$docComment->getText()
);
$useTags = $resolvedPhpDoc->getUsesTags();
}

$description = sprintf('class %s', $className);
$typeDescription = 'class';
if ($traitName !== null) {
$description = sprintf('trait %s', $traitName);
$typeDescription = 'trait';
}

return $this->genericAncestorsCheck->check(
$node->traits,
array_map(static function (UsesTag $tag): Type {
return $tag->getType();
}, $useTags),
sprintf('%s @use tag contains incompatible type %%s.', ucfirst($description)),
sprintf('%s has @use tag, but does not use any trait.', ucfirst($description)),
sprintf('The @use tag of %s describes %%s but the %s uses %%s.', $description, $typeDescription),
'PHPDoc tag @use contains generic type %s but trait %s is not generic.',
'Generic type %s in PHPDoc tag @use does not specify all template types of trait %s: %s',
'Generic type %s in PHPDoc tag @use specifies %d template types, but trait %s supports only %d: %s',
'Type %s in generic type %s in PHPDoc tag @use is not subtype of template type %s of trait %s.',
'PHPDoc tag @use has invalid type %s.',
sprintf('%s uses generic trait %%s but does not specify its types: %%s', ucfirst($description)),
sprintf('in used type %%s of %s', $description)
);
}

}
166 changes: 111 additions & 55 deletions src/Type/FileTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeFactory;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeMap;
use function array_key_exists;
use function file_exists;
Expand Down Expand Up @@ -214,7 +216,7 @@ private function shouldPhpDocNodeBeCachedToDisk(PhpDocNode $phpDocNode): bool
private function getResolvedPhpDocMap(string $fileName): array
{
if (!isset($this->memoryCache[$fileName])) {
$cacheKey = sprintf('%s-phpdocstring-v6-generic-bound', $fileName);
$cacheKey = sprintf('%s-phpdocstring-v7-generic-traits', $fileName);
$variableCacheKey = implode(',', array_map(static function (array $file): string {
return sprintf('%s-%d', $file['filename'], $file['modifiedTime']);
}, $this->getCachedDependentFilesWithTimestamps($fileName)));
Expand Down Expand Up @@ -313,56 +315,7 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia
$resolvableTemplateTypes = true;
}
} elseif ($node instanceof Node\Stmt\TraitUse) {
$traitMethodAliases = [];
foreach ($node->adaptations as $traitUseAdaptation) {
if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
continue;
}

if ($traitUseAdaptation->trait === null) {
continue;
}

if ($traitUseAdaptation->newName === null) {
continue;
}

$traitMethodAliases[$traitUseAdaptation->trait->toString()][$traitUseAdaptation->method->toString()] = $traitUseAdaptation->newName->toString();
}

foreach ($node->traits as $traitName) {
/** @var class-string $traitName */
$traitName = (string) $traitName;
$reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider();
if (!$reflectionProvider->hasClass($traitName)) {
continue;
}

$traitReflection = $reflectionProvider->getClass($traitName);
if (!$traitReflection->isTrait()) {
continue;
}
if ($traitReflection->getFileName() === false) {
continue;
}
if (!file_exists($traitReflection->getFileName())) {
continue;
}

$className = $classStack[count($classStack) - 1] ?? null;
if ($className === null) {
throw new \PHPStan\ShouldNotHappenException();
}

$traitPhpDocMap = $this->createFilePhpDocMap(
$traitReflection->getFileName(),
$traitName,
$className,
$traitMethodAliases[$traitName] ?? []
);
$phpDocMap = array_merge($phpDocMap, $traitPhpDocMap);
}
return null;
$resolvableTemplateTypes = true;
} elseif ($node instanceof Node\Stmt\ClassMethod) {
$functionName = $node->name->name;
if (array_key_exists($functionName, $traitMethodAliases)) {
Expand Down Expand Up @@ -431,10 +384,6 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia
}

$typeMapStack[] = function () use ($fileName, $className, $lookForTrait, $functionName, $phpDocString, $typeMapCb): TemplateTypeMap {
static $typeMap = null;
if ($typeMap !== null) {
return $typeMap;
}
$resolvedPhpDoc = $this->getResolvedPhpDoc(
$fileName,
$className,
Expand Down Expand Up @@ -466,6 +415,113 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia

$uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
}
} elseif ($node instanceof Node\Stmt\TraitUse) {
$traitMethodAliases = [];
foreach ($node->adaptations as $traitUseAdaptation) {
if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
continue;
}

if ($traitUseAdaptation->trait === null) {
continue;
}

if ($traitUseAdaptation->newName === null) {
continue;
}

$traitMethodAliases[$traitUseAdaptation->trait->toString()][$traitUseAdaptation->method->toString()] = $traitUseAdaptation->newName->toString();
}

$useDocComment = null;
if ($node->getDocComment() !== null) {
$useDocComment = $node->getDocComment()->getText();
}

foreach ($node->traits as $traitName) {
/** @var class-string $traitName */
$traitName = (string) $traitName;
$reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider();
if (!$reflectionProvider->hasClass($traitName)) {
continue;
}

$traitReflection = $reflectionProvider->getClass($traitName);
if (!$traitReflection->isTrait()) {
continue;
}
if ($traitReflection->getFileName() === false) {
continue;
}
if (!file_exists($traitReflection->getFileName())) {
continue;
}

$className = $classStack[count($classStack) - 1] ?? null;
if ($className === null) {
throw new \PHPStan\ShouldNotHappenException();
}

$traitPhpDocMap = $this->createFilePhpDocMap(
$traitReflection->getFileName(),
$traitName,
$className,
$traitMethodAliases[$traitName] ?? []
);
$finalTraitPhpDocMap = [];
foreach ($traitPhpDocMap as $phpDocKey => $callback) {
$finalTraitPhpDocMap[$phpDocKey] = function () use ($callback, $traitReflection, $fileName, $className, $lookForTrait, $useDocComment): NameScopedPhpDocString {
/** @var NameScopedPhpDocString $original */
$original = $callback();
if (!$traitReflection->isGeneric()) {
return $original;
}

$traitTemplateTypeMap = $traitReflection->getTemplateTypeMap();

$useType = null;
if ($useDocComment !== null) {
$useTags = $this->getResolvedPhpDoc(
$fileName,
$className,
$lookForTrait,
null,
$useDocComment
)->getUsesTags();
foreach ($useTags as $useTag) {
$useTagType = $useTag->getType();
if (!$useTagType instanceof GenericObjectType) {
continue;
}

if ($useTagType->getClassName() !== $traitReflection->getName()) {
continue;
}

$useType = $useTagType;
break;
}
}

if ($useType === null) {
return new NameScopedPhpDocString(
$original->getPhpDocString(),
$original->getNameScope()->withTemplateTypeMap($traitTemplateTypeMap->resolveToBounds())
);
}

$transformedTraitTypeMap = $traitReflection->typeMapFromList($useType->getTypes());

return new NameScopedPhpDocString(
$original->getPhpDocString(),
$original->getNameScope()->withTemplateTypeMap($traitTemplateTypeMap->map(static function (string $name, Type $type) use ($transformedTraitTypeMap): Type {
return TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap);
}))
);
};
}
$phpDocMap = array_merge($phpDocMap, $finalTraitPhpDocMap);
}
}

return null;
Expand Down
12 changes: 12 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5636,6 +5636,16 @@ public function dataPseudoTypeGlobal(): array
return $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-global.php');
}

public function dataGenericTraits(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/generic-traits.php');
}

public function dataBug4423(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4423.php');
}

/**
* @dataProvider dataArrayFunctions
* @param string $description
Expand Down Expand Up @@ -11246,6 +11256,8 @@ private function gatherAssertTypes(string $file): array
* @dataProvider dataPseudoTypeGlobal
* @dataProvider dataPseudoTypeNamespace
* @dataProvider dataPseudoTypeOverrides
* @dataProvider dataGenericTraits
* @dataProvider dataBug4423
* @param string $assertType
* @param string $file
* @param mixed ...$args
Expand Down
64 changes: 64 additions & 0 deletions tests/PHPStan/Analyser/data/bug-4423.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php declare(strict_types = 1);

namespace Bug4423;

use function PHPStan\Analyser\assertType;

/**
* @template T
*/
class Bar {}

/**
* @template K
* @property-read Bar<K> $bar
* @method Bar<K> doBar()
*/
trait Foo {

/** @var Bar<K> */
public $baz;

/** @param K $k */
public function doFoo($k)
{
assertType('T (class Bug4423\Child, argument)', $k);
assertType('Bug4423\Bar<T (class Bug4423\Child, argument)>', $this->bar);
assertType('Bug4423\Bar<T (class Bug4423\Child, argument)>', $this->baz);
assertType('Bug4423\Bar<T (class Bug4423\Child, argument)>', $this->doBar());
assertType('Bug4423\Bar<T (class Bug4423\Child, argument)>', $this->doBaz());
}

/** @return Bar<K> */
public function doBaz()
{

}

}

/**
* @template T
* @template K
*/
class Base {

}

/**
* @template T
* @extends Base<int, T>
*/
class Child extends Base {
/** @phpstan-use Foo<T> */
use Foo;
}

function (Child $child): void {
/** @var Child<int> $child */
assertType('Bug4423\Child<int>', $child);
assertType('Bug4423\Bar<int>', $child->bar);
assertType('Bug4423\Bar<int>', $child->baz);
assertType('Bug4423\Bar<int>', $child->doBar());
assertType('Bug4423\Bar<int>', $child->doBaz());
};
Loading

0 comments on commit 8766923

Please sign in to comment.