Skip to content

Commit

Permalink
Nested generic types
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Feb 28, 2021
1 parent d0e6683 commit e671cc0
Show file tree
Hide file tree
Showing 33 changed files with 985 additions and 42 deletions.
16 changes: 16 additions & 0 deletions src/Analyser/NameScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@ public function withTemplateTypeMap(TemplateTypeMap $map): self
);
}

public function unsetTemplateType(string $name): self
{
$map = $this->templateTypeMap;
if (!$map->hasType($name)) {
return $this;
}

return new self(
$this->namespace,
$this->uses,
$this->className,
$this->functionName,
$this->templateTypeMap->unsetType($name)
);
}

/**
* @param mixed[] $properties
* @return self
Expand Down
2 changes: 1 addition & 1 deletion src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope

$resolved[$valueNode->name] = new TemplateTag(
$valueNode->name,
$valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScope) : new MixedType(),
$valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScope->unsetTemplateType($valueNode->name)) : new MixedType(),
$variance
);
$resolvedPrefix[$valueNode->name] = $prefix;
Expand Down
9 changes: 4 additions & 5 deletions src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -843,11 +843,10 @@ public function getTemplateTypeMap(): TemplateTypeMap
return $this->templateTypeMap;
}

$templateTypeMap = new TemplateTypeMap(array_map(function (TemplateTag $tag): Type {
return TemplateTypeFactory::fromTemplateTag(
TemplateTypeScope::createWithClass($this->getName()),
$tag
);
$templateTypeScope = TemplateTypeScope::createWithClass($this->getName());

$templateTypeMap = new TemplateTypeMap(array_map(static function (TemplateTag $tag) use ($templateTypeScope): Type {
return TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag);
}, $this->getTemplateTags()));

$this->templateTypeMap = $templateTypeMap;
Expand Down
3 changes: 0 additions & 3 deletions src/Rules/Generics/ClassTemplateTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPStan\Type\Generic\TemplateTypeScope;

/**
* @implements \PHPStan\Rules\Rule<InClassNode>
Expand Down Expand Up @@ -34,7 +33,6 @@ public function processNode(Node $node, Scope $scope): array
return [];
}
$classReflection = $scope->getClassReflection();
$className = $classReflection->getName();
if ($classReflection->isAnonymous()) {
$displayName = 'anonymous class';
} else {
Expand All @@ -43,7 +41,6 @@ public function processNode(Node $node, Scope $scope): array

return $this->templateTypeCheck->check(
$node,
TemplateTypeScope::createWithClass($className),
$classReflection->getTemplateTags(),
sprintf('PHPDoc tag @template for %s cannot have existing class %%s as its name.', $displayName),
sprintf('PHPDoc tag @template for %s cannot have existing type alias %%s as its name.', $displayName),
Expand Down
2 changes: 0 additions & 2 deletions src/Rules/Generics/FunctionTemplateTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Generic\TemplateTypeScope;

/**
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Function_>
Expand Down Expand Up @@ -54,7 +53,6 @@ public function processNode(Node $node, Scope $scope): array

return $this->templateTypeCheck->check(
$node,
TemplateTypeScope::createWithFunction($functionName),
$resolvedPhpDoc->getTemplateTags(),
sprintf('PHPDoc tag @template for function %s() cannot have existing class %%s as its name.', $functionName),
sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $functionName),
Expand Down
2 changes: 0 additions & 2 deletions src/Rules/Generics/InterfaceTemplateTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Generic\TemplateTypeScope;

/**
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Interface_>
Expand Down Expand Up @@ -54,7 +53,6 @@ public function processNode(Node $node, Scope $scope): array

return $this->templateTypeCheck->check(
$node,
TemplateTypeScope::createWithClass($interfaceName),
$resolvedPhpDoc->getTemplateTags(),
sprintf('PHPDoc tag @template for interface %s cannot have existing class %%s as its name.', $interfaceName),
sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $interfaceName),
Expand Down
2 changes: 0 additions & 2 deletions src/Rules/Generics/MethodTemplateTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\VerbosityLevel;

/**
Expand Down Expand Up @@ -59,7 +58,6 @@ public function processNode(Node $node, Scope $scope): array
$methodTemplateTags = $resolvedPhpDoc->getTemplateTags();
$messages = $this->templateTypeCheck->check(
$node,
TemplateTypeScope::createWithMethod($className, $methodName),
$methodTemplateTags,
sprintf('PHPDoc tag @template for method %s::%s() cannot have existing class %%s as its name.', $className, $methodName),
sprintf('PHPDoc tag @template for method %s::%s() cannot have existing type alias %%s as its name.', $className, $methodName),
Expand Down
23 changes: 20 additions & 3 deletions src/Rules/Generics/TemplateTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
use PHPStan\Rules\ClassCaseSensitivityCheck;
use PHPStan\Rules\ClassNameNodePair;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
Expand All @@ -27,6 +28,8 @@ class TemplateTypeCheck

private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck;

private GenericObjectTypeCheck $genericObjectTypeCheck;

/** @var array<string, string> */
private array $typeAliases;

Expand All @@ -35,31 +38,32 @@ class TemplateTypeCheck
/**
* @param ReflectionProvider $reflectionProvider
* @param ClassCaseSensitivityCheck $classCaseSensitivityCheck
* @param GenericObjectTypeCheck $genericObjectTypeCheck
* @param array<string, string> $typeAliases
* @param bool $checkClassCaseSensitivity
*/
public function __construct(
ReflectionProvider $reflectionProvider,
ClassCaseSensitivityCheck $classCaseSensitivityCheck,
GenericObjectTypeCheck $genericObjectTypeCheck,
array $typeAliases,
bool $checkClassCaseSensitivity
)
{
$this->reflectionProvider = $reflectionProvider;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
$this->genericObjectTypeCheck = $genericObjectTypeCheck;
$this->typeAliases = $typeAliases;
$this->checkClassCaseSensitivity = $checkClassCaseSensitivity;
}

/**
* @param \PhpParser\Node $node
* @param \PHPStan\Type\Generic\TemplateTypeScope $templateTypeScope
* @param array<string, \PHPStan\PhpDoc\Tag\TemplateTag> $templateTags
* @return \PHPStan\Rules\RuleError[]
*/
public function check(
Node $node,
TemplateTypeScope $templateTypeScope,
array $templateTags,
string $sameTemplateTypeNameAsClassMessage,
string $sameTemplateTypeNameAsTypeMessage,
Expand Down Expand Up @@ -113,7 +117,9 @@ public function check(
|| $boundClass === IntegerType::class
|| $boundClass === ObjectWithoutClassType::class
|| $boundClass === ObjectType::class
|| $boundClass === GenericObjectType::class
|| $type instanceof UnionType
|| $type instanceof TemplateType
) {
return $traverse($type);
}
Expand All @@ -122,6 +128,17 @@ public function check(

return $type;
});

$genericObjectErrors = $this->genericObjectTypeCheck->check(
$boundType,
sprintf('PHPDoc tag @template %s bound contains generic type %%s but class %%s is not generic.', $templateTagName),
sprintf('PHPDoc tag @template %s bound has type %%s which does not specify all template types of class %%s: %%s', $templateTagName),
sprintf('PHPDoc tag @template %s bound has type %%s which specifies %%d template types, but class %%s supports only %%d: %%s', $templateTagName),
sprintf('Type %%s in generic type %%s in PHPDoc tag @template %s is not subtype of template type %%s of class %%s.', $templateTagName),
);
foreach ($genericObjectErrors as $genericObjectError) {
$messages[] = $genericObjectError;
}
}

return $messages;
Expand Down
2 changes: 0 additions & 2 deletions src/Rules/Generics/TraitTemplateTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Generic\TemplateTypeScope;

/**
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Trait_>
Expand Down Expand Up @@ -54,7 +53,6 @@ public function processNode(Node $node, Scope $scope): array

return $this->templateTypeCheck->check(
$node,
TemplateTypeScope::createWithClass($traitName),
$resolvedPhpDoc->getTemplateTags(),
sprintf('PHPDoc tag @template for trait %s cannot have existing class %%s as its name.', $traitName),
sprintf('PHPDoc tag @template for trait %s cannot have existing type alias %%s as its name.', $traitName),
Expand Down
10 changes: 10 additions & 0 deletions src/Type/FileTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ private function createResolvedPhpDocBlock(string $phpDocKey, NameScopedPhpDocSt
$templateTypeMap->getTypes()
))
);
$templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope);
$templateTypeMap = new TemplateTypeMap(array_map(static function (TemplateTag $tag) use ($templateTypeScope): Type {
return TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag);
}, $templateTags));
$nameScope = $nameScope->withTemplateTypeMap(
new TemplateTypeMap(array_merge(
$nameScope->getTemplateTypeMap()->getTypes(),
$templateTypeMap->getTypes()
))
);
} else {
$templateTypeMap = TemplateTypeMap::createEmpty();
}
Expand Down
23 changes: 17 additions & 6 deletions src/Type/Generic/GenericObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;

final class GenericObjectType extends ObjectType
class GenericObjectType extends ObjectType
{

/** @var array<int, Type> */
Expand Down Expand Up @@ -289,16 +289,27 @@ public function traverse(callable $cb): Type
}

if ($subtractedType !== $this->getSubtractedType() || $typesChanged) {
return new static(
$this->getClassName(),
$types,
$subtractedType
);
return $this->recreate($this->getClassName(), $types, $subtractedType);
}

return $this;
}

/**
* @param string $className
* @param Type[] $types
* @param Type|null $subtractedType
* @return self
*/
protected function recreate(string $className, array $types, ?Type $subtractedType): self
{
return new self(
$className,
$types,
$subtractedType
);
}

public function changeSubtractedType(?Type $subtractedType): Type
{
return new self($this->getClassName(), $this->types, $subtractedType);
Expand Down
75 changes: 75 additions & 0 deletions src/Type/Generic/TemplateGenericObjectType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Generic;

use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use PHPStan\Type\Type;

final class TemplateGenericObjectType extends GenericObjectType implements TemplateType
{

use UndecidedComparisonCompoundTypeTrait;
use TemplateTypeTrait;

/**
* @param Type[] $types
*/
public function __construct(
TemplateTypeScope $scope,
TemplateTypeStrategy $templateTypeStrategy,
TemplateTypeVariance $templateTypeVariance,
string $name,
string $mainType,
array $types
)
{
parent::__construct($mainType, $types);

$this->scope = $scope;
$this->strategy = $templateTypeStrategy;
$this->variance = $templateTypeVariance;
$this->name = $name;
$this->bound = new GenericObjectType($mainType, $types);
}

public function toArgument(): TemplateType
{
return new self(
$this->scope,
new TemplateTypeArgumentStrategy(),
$this->variance,
$this->name,
$this->getClassName(),
$this->getTypes()
);
}

protected function recreate(string $className, array $types, ?Type $subtractedType): GenericObjectType
{
return new self(
$this->scope,
$this->strategy,
$this->variance,
$this->name,
$className,
$types
);
}

/**
* @param mixed[] $properties
* @return Type
*/
public static function __set_state(array $properties): Type
{
return new self(
$properties['scope'],
$properties['strategy'],
$properties['variance'],
$properties['name'],
$properties['className'],
$properties['types']
);
}

}
5 changes: 5 additions & 0 deletions src/Type/Generic/TemplateTypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou
if ($bound instanceof TypeWithClassName && $boundClass === ObjectType::class) {
return new TemplateObjectType($scope, $strategy, $variance, $name, $bound->getClassName());
}

if ($bound instanceof GenericObjectType && $boundClass === GenericObjectType::class) {
return new TemplateGenericObjectType($scope, $strategy, $variance, $name, $bound->getClassName(), $bound->getTypes());
}

if ($boundClass === ObjectWithoutClassType::class) {
return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name);
}
Expand Down
9 changes: 6 additions & 3 deletions src/Type/Generic/TemplateTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ public static function resolveTemplateTypes(Type $type, TemplateTypeMap $standin
{
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($standins): Type {
if ($type instanceof TemplateType && !$type->isArgument()) {
$newType = $standins->getType($type->getName()) ?? $type;
$newType = $standins->getType($type->getName());
if ($newType === null) {
return $traverse($type);
}

if ($newType instanceof ErrorType) {
$newType = $type->getBound();
return $traverse($type->getBound());
}
if ($newType instanceof StaticType) {
$newType = $newType->getStaticObjectType();
return $traverse($newType->getStaticObjectType());
}

return $newType;
Expand Down
Loading

0 comments on commit e671cc0

Please sign in to comment.