diff --git a/build.xml b/build.xml
index cd530ab15d..91632acb53 100644
--- a/build.xml
+++ b/build.xml
@@ -221,7 +221,7 @@
checkreturn="true"
>
-
+
diff --git a/build/baseline-7.4.neon b/build/baseline-7.4.neon
index 9217c32c19..2db001615e 100644
--- a/build/baseline-7.4.neon
+++ b/build/baseline-7.4.neon
@@ -93,6 +93,6 @@ parameters:
count: 1
path: ../src/Reflection/ReflectionProvider/SetterReflectionProviderProvider.php
-
- message: "#^Class class@anonymous/src/Testing/TestCase\\.php\\:267 has an uninitialized property \\$reflectionProvider\\. Give it default value or assign it in the constructor\\.$#"
+ message: "#^Class class@anonymous/src/Testing/TestCase\\.php\\:270 has an uninitialized property \\$reflectionProvider\\. Give it default value or assign it in the constructor\\.$#"
count: 1
path: ../src/Testing/TestCase.php
diff --git a/conf/config.level0.neon b/conf/config.level0.neon
index a615d9401f..728ddc8ec3 100644
--- a/conf/config.level0.neon
+++ b/conf/config.level0.neon
@@ -200,3 +200,10 @@ services:
-
class: PHPStan\Rules\Whitespace\FileWhitespaceRule
+
+ -
+ class: PHPStan\Rules\Classes\LocalTypeAliasesRule
+ arguments:
+ globalTypeAliases: %typeAliases%
+ tags:
+ - phpstan.rules.rule
diff --git a/conf/config.neon b/conf/config.neon
index fa8ff89021..0464df88d3 100644
--- a/conf/config.neon
+++ b/conf/config.neon
@@ -366,13 +366,6 @@ services:
-
class: PHPStan\PhpDoc\ConstExprNodeResolver
- -
- class: PHPStan\PhpDoc\TypeAlias\TypeAliasesTypeNodeResolverExtension
- arguments:
- aliases: %typeAliases%
- tags:
- - phpstan.phpDoc.typeNodeResolverExtension
-
-
class: PHPStan\PhpDoc\TypeNodeResolver
@@ -747,7 +740,6 @@ services:
class: PHPStan\Rules\Generics\TemplateTypeCheck
arguments:
checkClassCaseSensitivity: %checkClassCaseSensitivity%
- typeAliases: %typeAliases%
-
class: PHPStan\Rules\Generics\VarianceCheck
@@ -802,6 +794,11 @@ services:
-
class: PHPStan\Type\FileTypeMapper
+ -
+ class: PHPStan\Type\TypeAliasResolver
+ arguments:
+ globalTypeAliases: %typeAliases%
+
-
class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension
tags:
diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php
index fe18eea6fc..44f2430847 100644
--- a/src/Analyser/NameScope.php
+++ b/src/Analyser/NameScope.php
@@ -20,18 +20,21 @@ class NameScope
private TemplateTypeMap $templateTypeMap;
+ private bool $bypassTypeAliases;
+
/**
* @param string|null $namespace
* @param array $uses alias(string) => fullName(string)
* @param string|null $className
*/
- public function __construct(?string $namespace, array $uses, ?string $className = null, ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null)
+ public function __construct(?string $namespace, array $uses, ?string $className = null, ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, bool $bypassTypeAliases = false)
{
$this->namespace = $namespace;
$this->uses = $uses;
$this->className = $className;
$this->functionName = $functionName;
$this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty();
+ $this->bypassTypeAliases = $bypassTypeAliases;
}
public function getNamespace(): ?string
@@ -137,6 +140,16 @@ public function unsetTemplateType(string $name): self
);
}
+ public function bypassTypeAliases(): self
+ {
+ return new self($this->namespace, $this->uses, $this->className, $this->functionName, $this->templateTypeMap, true);
+ }
+
+ public function shouldBypassTypeAliases(): bool
+ {
+ return $this->bypassTypeAliases;
+ }
+
/**
* @param mixed[] $properties
* @return self
diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php
index ecc757874e..aa188fe918 100644
--- a/src/PhpDoc/PhpDocNodeResolver.php
+++ b/src/PhpDoc/PhpDocNodeResolver.php
@@ -14,6 +14,8 @@
use PHPStan\PhpDoc\Tag\ReturnTag;
use PHPStan\PhpDoc\Tag\TemplateTag;
use PHPStan\PhpDoc\Tag\ThrowsTag;
+use PHPStan\PhpDoc\Tag\TypeAliasImportTag;
+use PHPStan\PhpDoc\Tag\TypeAliasTag;
use PHPStan\PhpDoc\Tag\UsesTag;
use PHPStan\PhpDoc\Tag\VarTag;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode;
@@ -365,6 +367,43 @@ public function resolveMixinTags(PhpDocNode $phpDocNode, NameScope $nameScope):
}, $phpDocNode->getMixinTagValues());
}
+ /**
+ * @return array
+ */
+ public function resolveTypeAliasTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
+ {
+ $resolved = [];
+
+ foreach (['@psalm-type', '@phpstan-type'] as $tagName) {
+ foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) {
+ $alias = $typeAliasTagValue->alias;
+ $typeNode = $typeAliasTagValue->type;
+ $resolved[$alias] = new TypeAliasTag($alias, $typeNode, $nameScope);
+ }
+ }
+
+ return $resolved;
+ }
+
+ /**
+ * @return array
+ */
+ public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
+ {
+ $resolved = [];
+
+ foreach (['@psalm-import-type', '@phpstan-import-type'] as $tagName) {
+ foreach ($phpDocNode->getTypeAliasImportTagValues($tagName) as $typeAliasImportTagValue) {
+ $importedAlias = $typeAliasImportTagValue->importedAlias;
+ $importedFrom = $nameScope->resolveStringName($typeAliasImportTagValue->importedFrom->name);
+ $importedAs = $typeAliasImportTagValue->importedAs;
+ $resolved[$importedAs ?? $importedAlias] = new TypeAliasImportTag($importedAlias, $importedFrom, $importedAs);
+ }
+ }
+
+ return $resolved;
+ }
+
public function resolveDeprecatedTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?\PHPStan\PhpDoc\Tag\DeprecatedTag
{
foreach ($phpDocNode->getDeprecatedTagValues() as $deprecatedTagValue) {
diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php
index ab9e29173a..d40ce8fa7d 100644
--- a/src/PhpDoc/ResolvedPhpDocBlock.php
+++ b/src/PhpDoc/ResolvedPhpDocBlock.php
@@ -7,6 +7,8 @@
use PHPStan\PhpDoc\Tag\ParamTag;
use PHPStan\PhpDoc\Tag\ReturnTag;
use PHPStan\PhpDoc\Tag\ThrowsTag;
+use PHPStan\PhpDoc\Tag\TypeAliasImportTag;
+use PHPStan\PhpDoc\Tag\TypeAliasTag;
use PHPStan\PhpDoc\Tag\TypedTag;
use PHPStan\PhpDoc\Tag\VarTag;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
@@ -64,6 +66,12 @@ class ResolvedPhpDocBlock
/** @var array|false */
private $mixinTags = false;
+ /** @var array|false */
+ private $typeAliasTags = false;
+
+ /** @var array|false */
+ private $typeAliasImportTags = false;
+
/** @var \PHPStan\PhpDoc\Tag\DeprecatedTag|false|null */
private $deprecatedTag = false;
@@ -133,6 +141,8 @@ public static function createEmpty(): self
$self->returnTag = null;
$self->throwsTag = null;
$self->mixinTags = [];
+ $self->typeAliasTags = [];
+ $self->typeAliasImportTags = [];
$self->deprecatedTag = null;
$self->isDeprecated = false;
$self->isInternal = false;
@@ -176,6 +186,8 @@ public function merge(array $parents, array $parentPhpDocBlocks): self
$result->returnTag = self::mergeReturnTags($this->getReturnTag(), $parents, $parentPhpDocBlocks);
$result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parents);
$result->mixinTags = $this->getMixinTags();
+ $result->typeAliasTags = $this->getTypeAliasTags();
+ $result->typeAliasImportTags = $this->getTypeAliasImportTags();
$result->deprecatedTag = $this->getDeprecatedTag();
$result->isDeprecated = $result->deprecatedTag !== null;
$result->isInternal = $this->isInternal();
@@ -219,6 +231,9 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self
$self->paramTags = $newParamTags;
$self->returnTag = $this->returnTag;
$self->throwsTag = $this->throwsTag;
+ $self->mixinTags = $this->mixinTags;
+ $self->typeAliasTags = $this->typeAliasTags;
+ $self->typeAliasImportTags = $this->typeAliasImportTags;
$self->deprecatedTag = $this->deprecatedTag;
$self->isDeprecated = $this->isDeprecated;
$self->isInternal = $this->isInternal;
@@ -399,6 +414,36 @@ public function getMixinTags(): array
return $this->mixinTags;
}
+ /**
+ * @return array
+ */
+ public function getTypeAliasTags(): array
+ {
+ if ($this->typeAliasTags === false) {
+ $this->typeAliasTags = $this->phpDocNodeResolver->resolveTypeAliasTags(
+ $this->phpDocNode,
+ $this->getNameScope()
+ );
+ }
+
+ return $this->typeAliasTags;
+ }
+
+ /**
+ * @return array
+ */
+ public function getTypeAliasImportTags(): array
+ {
+ if ($this->typeAliasImportTags === false) {
+ $this->typeAliasImportTags = $this->phpDocNodeResolver->resolveTypeAliasImportTags(
+ $this->phpDocNode,
+ $this->getNameScope()
+ );
+ }
+
+ return $this->typeAliasImportTags;
+ }
+
public function getDeprecatedTag(): ?\PHPStan\PhpDoc\Tag\DeprecatedTag
{
if ($this->deprecatedTag === false) {
diff --git a/src/PhpDoc/Tag/TypeAliasImportTag.php b/src/PhpDoc/Tag/TypeAliasImportTag.php
new file mode 100644
index 0000000000..62fb7cdb77
--- /dev/null
+++ b/src/PhpDoc/Tag/TypeAliasImportTag.php
@@ -0,0 +1,36 @@
+importedAlias = $importedAlias;
+ $this->importedFrom = $importedFrom;
+ $this->importedAs = $importedAs;
+ }
+
+ public function getImportedAlias(): string
+ {
+ return $this->importedAlias;
+ }
+
+ public function getImportedFrom(): string
+ {
+ return $this->importedFrom;
+ }
+
+ public function getImportedAs(): ?string
+ {
+ return $this->importedAs;
+ }
+
+}
diff --git a/src/PhpDoc/Tag/TypeAliasTag.php b/src/PhpDoc/Tag/TypeAliasTag.php
new file mode 100644
index 0000000000..d5ed9646cf
--- /dev/null
+++ b/src/PhpDoc/Tag/TypeAliasTag.php
@@ -0,0 +1,41 @@
+aliasName = $aliasName;
+ $this->typeNode = $typeNode;
+ $this->nameScope = $nameScope;
+ }
+
+ public function getAliasName(): string
+ {
+ return $this->aliasName;
+ }
+
+ public function getTypeAlias(): \PHPStan\Type\TypeAlias
+ {
+ return new \PHPStan\Type\TypeAlias(
+ $this->typeNode,
+ $this->nameScope
+ );
+ }
+
+}
diff --git a/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php b/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php
deleted file mode 100644
index cb940f021d..0000000000
--- a/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php
+++ /dev/null
@@ -1,78 +0,0 @@
- */
- private array $aliases;
-
- /** @var array */
- private array $resolvedTypes = [];
-
- /** @var array */
- private array $inProcess = [];
-
- /**
- * @param TypeStringResolver $typeStringResolver
- * @param ReflectionProvider $reflectionProvider
- * @param array $aliases
- */
- public function __construct(
- TypeStringResolver $typeStringResolver,
- ReflectionProvider $reflectionProvider,
- array $aliases
- )
- {
- $this->typeStringResolver = $typeStringResolver;
- $this->reflectionProvider = $reflectionProvider;
- $this->aliases = $aliases;
- }
-
- public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
- {
- if ($typeNode instanceof IdentifierTypeNode) {
- $aliasName = $typeNode->name;
- if (array_key_exists($aliasName, $this->resolvedTypes)) {
- return $this->resolvedTypes[$aliasName];
- }
- if (!array_key_exists($aliasName, $this->aliases)) {
- return null;
- }
-
- if ($this->reflectionProvider->hasClass($aliasName)) {
- throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s already exists as a class.', $aliasName));
- }
-
- if (array_key_exists($aliasName, $this->inProcess)) {
- throw new \PHPStan\ShouldNotHappenException(sprintf('Circular definition for type alias %s.', $aliasName));
- }
-
- $this->inProcess[$aliasName] = true;
-
- $aliasTypeString = $this->aliases[$aliasName];
- $aliasType = $this->typeStringResolver->resolve($aliasTypeString);
- $this->resolvedTypes[$aliasName] = $aliasType;
-
- unset($this->inProcess[$aliasName]);
-
- return $aliasType;
- }
- return null;
- }
-
-}
diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php
index 96b9d9dc53..2552f50c41 100644
--- a/src/PhpDoc/TypeNodeResolver.php
+++ b/src/PhpDoc/TypeNodeResolver.php
@@ -60,6 +60,7 @@
use PHPStan\Type\StringType;
use PHPStan\Type\ThisType;
use PHPStan\Type\Type;
+use PHPStan\Type\TypeAliasResolver;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnionType;
@@ -293,6 +294,14 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
}
}
+ if (!$nameScope->shouldBypassTypeAliases()) {
+ $aliasName = $typeNode->name;
+ $typeAlias = $this->getTypeAliasResolver()->resolveTypeAlias($aliasName, $nameScope);
+ if ($typeAlias !== null) {
+ return $typeAlias;
+ }
+ }
+
$templateType = $nameScope->resolveTemplateTypeName($typeNode->name);
if ($templateType !== null) {
return $templateType;
@@ -704,4 +713,9 @@ private function getReflectionProvider(): ReflectionProvider
return $this->container->getByType(ReflectionProvider::class);
}
+ private function getTypeAliasResolver(): TypeAliasResolver
+ {
+ return $this->container->getByType(TypeAliasResolver::class);
+ }
+
}
diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php
index 4952bb1938..a549a55879 100644
--- a/src/Reflection/ClassReflection.php
+++ b/src/Reflection/ClassReflection.php
@@ -12,6 +12,8 @@
use PHPStan\PhpDoc\Tag\MixinTag;
use PHPStan\PhpDoc\Tag\PropertyTag;
use PHPStan\PhpDoc\Tag\TemplateTag;
+use PHPStan\PhpDoc\Tag\TypeAliasImportTag;
+use PHPStan\PhpDoc\Tag\TypeAliasTag;
use PHPStan\Reflection\Php\PhpClassReflectionExtension;
use PHPStan\Reflection\Php\PhpPropertyReflection;
use PHPStan\Type\ErrorType;
@@ -22,6 +24,7 @@
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\Type;
+use PHPStan\Type\TypeAlias;
use PHPStan\Type\VerbosityLevel;
use ReflectionMethod;
@@ -98,6 +101,12 @@ class ClassReflection implements ReflectionWithFilename
/** @var \PHPStan\Reflection\ClassReflection|false|null */
private $cachedParentClass = null;
+ /** @var array|null */
+ private ?array $typeAliases = null;
+
+ /** @var array */
+ private static array $resolvingTypeAliasImports = [];
+
/**
* @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider
* @param \PHPStan\Type\FileTypeMapper $fileTypeMapper
@@ -752,6 +761,67 @@ private function getTraitNames(): array
return $traitNames;
}
+ /**
+ * @return array
+ */
+ public function getTypeAliases(): array
+ {
+ if ($this->typeAliases === null) {
+ $resolvedPhpDoc = $this->getResolvedPhpDoc();
+ if ($resolvedPhpDoc === null) {
+ return $this->typeAliases = [];
+ }
+
+ $typeAliasImportTags = $resolvedPhpDoc->getTypeAliasImportTags();
+ $typeAliasTags = $resolvedPhpDoc->getTypeAliasTags();
+
+ // prevent circular imports
+ if (array_key_exists($this->getName(), self::$resolvingTypeAliasImports)) {
+ throw new \PHPStan\Type\CircularTypeAliasDefinitionException();
+ }
+
+ self::$resolvingTypeAliasImports[$this->getName()] = true;
+
+ $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag): ?TypeAlias {
+ $importedAlias = $typeAliasImportTag->getImportedAlias();
+ $importedFromClassName = $typeAliasImportTag->getImportedFrom();
+
+ if (!$this->reflectionProvider->hasClass($importedFromClassName)) {
+ return null;
+ }
+
+ $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName);
+
+ try {
+ $typeAliases = $importedFromReflection->getTypeAliases();
+ } catch (\PHPStan\Type\CircularTypeAliasDefinitionException $e) {
+ return TypeAlias::invalid();
+ }
+
+ if (!array_key_exists($importedAlias, $typeAliases)) {
+ return null;
+ }
+
+ return $typeAliases[$importedAlias];
+ }, $typeAliasImportTags);
+
+ unset(self::$resolvingTypeAliasImports[$this->getName()]);
+
+ $localAliases = array_map(static function (TypeAliasTag $typeAliasTag): TypeAlias {
+ return $typeAliasTag->getTypeAlias();
+ }, $typeAliasTags);
+
+ $this->typeAliases = array_filter(
+ array_merge($importedAliases, $localAliases),
+ static function (?TypeAlias $typeAlias): bool {
+ return $typeAlias !== null;
+ }
+ );
+ }
+
+ return $this->typeAliases;
+ }
+
public function getDeprecatedDescription(): ?string
{
if ($this->deprecatedDescription === null && $this->isDeprecated()) {
diff --git a/src/Reflection/SignatureMap/SignatureMapParser.php b/src/Reflection/SignatureMap/SignatureMapParser.php
index 2a4e203d85..66432fda22 100644
--- a/src/Reflection/SignatureMap/SignatureMapParser.php
+++ b/src/Reflection/SignatureMap/SignatureMapParser.php
@@ -49,7 +49,7 @@ private function getTypeFromString(string $typeString, ?string $className): Type
return new MixedType(true);
}
- return $this->typeStringResolver->resolve($typeString, new NameScope(null, [], $className));
+ return $this->typeStringResolver->resolve($typeString, (new NameScope(null, [], $className))->bypassTypeAliases());
}
/**
diff --git a/src/Rules/Classes/LocalTypeAliasesRule.php b/src/Rules/Classes/LocalTypeAliasesRule.php
new file mode 100644
index 0000000000..d87fd12b46
--- /dev/null
+++ b/src/Rules/Classes/LocalTypeAliasesRule.php
@@ -0,0 +1,138 @@
+
+ */
+class LocalTypeAliasesRule implements Rule
+{
+
+ /** @var array */
+ private array $globalTypeAliases;
+
+ private ReflectionProvider $reflectionProvider;
+
+ private TypeNodeResolver $typeNodeResolver;
+
+ /**
+ * @param array $globalTypeAliases
+ */
+ public function __construct(
+ array $globalTypeAliases,
+ ReflectionProvider $reflectionProvider,
+ TypeNodeResolver $typeNodeResolver
+ )
+ {
+ $this->globalTypeAliases = $globalTypeAliases;
+ $this->reflectionProvider = $reflectionProvider;
+ $this->typeNodeResolver = $typeNodeResolver;
+ }
+
+ public function getNodeType(): string
+ {
+ return InClassNode::class;
+ }
+
+ public function processNode(Node $node, Scope $scope): array
+ {
+ $reflection = $node->getClassReflection();
+ $phpDoc = $reflection->getResolvedPhpDoc();
+ if ($phpDoc === null) {
+ return [];
+ }
+
+ $nameScope = $phpDoc->getNullableNameScope();
+ $resolveName = static function (string $name) use ($nameScope): string {
+ if ($nameScope === null) {
+ return $name;
+ }
+
+ return $nameScope->resolveStringName($name);
+ };
+
+ $errors = [];
+ $className = $reflection->getName();
+
+ $importedAliases = [];
+
+ foreach ($phpDoc->getTypeAliasImportTags() as $typeAliasImportTag) {
+ $aliasName = $typeAliasImportTag->getImportedAs() ?? $typeAliasImportTag->getImportedAlias();
+ $importedAlias = $typeAliasImportTag->getImportedAlias();
+ $importedFromClassName = $typeAliasImportTag->getImportedFrom();
+
+ if (!$this->reflectionProvider->hasClass($importedFromClassName)) {
+ $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: class %s does not exist.', $importedAlias, $importedFromClassName))->build();
+ continue;
+ }
+
+ $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName);
+ $typeAliases = $importedFromReflection->getTypeAliases();
+
+ if (!array_key_exists($importedAlias, $typeAliases)) {
+ $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: type alias does not exist in %s.', $importedAlias, $importedFromClassName))->build();
+ continue;
+ }
+
+ if ($this->reflectionProvider->hasClass($resolveName($aliasName))) {
+ $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a class in scope of %s.', $aliasName, $className))->build();
+ continue;
+ }
+
+ if (array_key_exists($aliasName, $this->globalTypeAliases)) {
+ $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build();
+ continue;
+ }
+
+ $importedAliases[] = $aliasName;
+ }
+
+ foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) {
+ $aliasName = $typeAliasTag->getAliasName();
+
+ if (in_array($aliasName, $importedAliases, true)) {
+ $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s overwrites an imported type alias of the same name.', $aliasName))->build();
+ continue;
+ }
+
+ if ($this->reflectionProvider->hasClass($resolveName($aliasName))) {
+ $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a class in scope of %s.', $aliasName, $className))->build();
+ continue;
+ }
+
+ if (array_key_exists($aliasName, $this->globalTypeAliases)) {
+ $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build();
+ continue;
+ }
+
+ $resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver);
+ $foundError = false;
+ TypeTraverser::map($resolvedType, static function (\PHPStan\Type\Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): \PHPStan\Type\Type {
+ if ($foundError) {
+ return $type;
+ }
+
+ if ($type instanceof ErrorType) {
+ $errors[] = RuleErrorBuilder::message(sprintf('Circular definition detected in type alias %s.', $aliasName))->build();
+ $foundError = true;
+ return $type;
+ }
+
+ return $traverse($type);
+ });
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/Rules/Generics/ClassTemplateTypeRule.php b/src/Rules/Generics/ClassTemplateTypeRule.php
index e4b92bff73..ec726ce894 100644
--- a/src/Rules/Generics/ClassTemplateTypeRule.php
+++ b/src/Rules/Generics/ClassTemplateTypeRule.php
@@ -6,6 +6,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
+use PHPStan\Type\Generic\TemplateTypeScope;
/**
* @implements \PHPStan\Rules\Rule
@@ -33,6 +34,7 @@ public function processNode(Node $node, Scope $scope): array
return [];
}
$classReflection = $scope->getClassReflection();
+ $className = $classReflection->getName();
if ($classReflection->isAnonymous()) {
$displayName = 'anonymous class';
} else {
@@ -41,6 +43,7 @@ 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),
diff --git a/src/Rules/Generics/FunctionTemplateTypeRule.php b/src/Rules/Generics/FunctionTemplateTypeRule.php
index 1d7d7bf831..2a6d2bc921 100644
--- a/src/Rules/Generics/FunctionTemplateTypeRule.php
+++ b/src/Rules/Generics/FunctionTemplateTypeRule.php
@@ -6,6 +6,7 @@
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_>
@@ -53,6 +54,7 @@ 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),
diff --git a/src/Rules/Generics/InterfaceTemplateTypeRule.php b/src/Rules/Generics/InterfaceTemplateTypeRule.php
index 046de07a51..87f8b8cded 100644
--- a/src/Rules/Generics/InterfaceTemplateTypeRule.php
+++ b/src/Rules/Generics/InterfaceTemplateTypeRule.php
@@ -6,6 +6,7 @@
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_>
@@ -53,6 +54,7 @@ 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),
diff --git a/src/Rules/Generics/MethodTemplateTypeRule.php b/src/Rules/Generics/MethodTemplateTypeRule.php
index f9e0ba4255..57ac147a2c 100644
--- a/src/Rules/Generics/MethodTemplateTypeRule.php
+++ b/src/Rules/Generics/MethodTemplateTypeRule.php
@@ -7,6 +7,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\FileTypeMapper;
+use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\VerbosityLevel;
/**
@@ -58,6 +59,7 @@ 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),
diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php
index 45bc1ee86e..057623f0da 100644
--- a/src/Rules/Generics/TemplateTypeCheck.php
+++ b/src/Rules/Generics/TemplateTypeCheck.php
@@ -9,16 +9,17 @@
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateType;
+use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
+use PHPStan\Type\TypeAliasResolver;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
-use function array_key_exists;
use function array_map;
class TemplateTypeCheck
@@ -30,40 +31,34 @@ class TemplateTypeCheck
private GenericObjectTypeCheck $genericObjectTypeCheck;
- /** @var array */
- private array $typeAliases;
+ private TypeAliasResolver $typeAliasResolver;
private bool $checkClassCaseSensitivity;
- /**
- * @param ReflectionProvider $reflectionProvider
- * @param ClassCaseSensitivityCheck $classCaseSensitivityCheck
- * @param GenericObjectTypeCheck $genericObjectTypeCheck
- * @param array $typeAliases
- * @param bool $checkClassCaseSensitivity
- */
public function __construct(
ReflectionProvider $reflectionProvider,
ClassCaseSensitivityCheck $classCaseSensitivityCheck,
GenericObjectTypeCheck $genericObjectTypeCheck,
- array $typeAliases,
+ TypeAliasResolver $typeAliasResolver,
bool $checkClassCaseSensitivity
)
{
$this->reflectionProvider = $reflectionProvider;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
$this->genericObjectTypeCheck = $genericObjectTypeCheck;
- $this->typeAliases = $typeAliases;
+ $this->typeAliasResolver = $typeAliasResolver;
$this->checkClassCaseSensitivity = $checkClassCaseSensitivity;
}
/**
* @param \PhpParser\Node $node
+ * @param TemplateTypeScope $templateTypeScope
* @param array $templateTags
* @return \PHPStan\Rules\RuleError[]
*/
public function check(
Node $node,
+ TemplateTypeScope $templateTypeScope,
array $templateTags,
string $sameTemplateTypeNameAsClassMessage,
string $sameTemplateTypeNameAsTypeMessage,
@@ -80,7 +75,7 @@ public function check(
$templateTagName
))->build();
}
- if (array_key_exists($templateTagName, $this->typeAliases)) {
+ if ($this->typeAliasResolver->hasTypeAlias($templateTagName, $templateTypeScope->getClassName())) {
$messages[] = RuleErrorBuilder::message(sprintf(
$sameTemplateTypeNameAsTypeMessage,
$templateTagName
diff --git a/src/Rules/Generics/TraitTemplateTypeRule.php b/src/Rules/Generics/TraitTemplateTypeRule.php
index d06ffc9f4e..21b89339cd 100644
--- a/src/Rules/Generics/TraitTemplateTypeRule.php
+++ b/src/Rules/Generics/TraitTemplateTypeRule.php
@@ -6,6 +6,7 @@
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_>
@@ -53,6 +54,7 @@ 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),
diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php
index 2d9d1a2fdb..f17d2df84e 100644
--- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php
+++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php
@@ -31,6 +31,8 @@ class InvalidPHPStanDocTagRule implements \PHPStan\Rules\Rule
'@phpstan-method',
'@phpstan-pure',
'@phpstan-impure',
+ '@phpstan-type',
+ '@phpstan-import-type',
];
private Lexer $phpDocLexer;
diff --git a/src/Testing/TestCase.php b/src/Testing/TestCase.php
index fc73b7aed0..1eafaa2b05 100644
--- a/src/Testing/TestCase.php
+++ b/src/Testing/TestCase.php
@@ -41,6 +41,8 @@
use PHPStan\PhpDoc\PhpDocNodeResolver;
use PHPStan\PhpDoc\PhpDocStringResolver;
use PHPStan\PhpDoc\StubPhpDocProvider;
+use PHPStan\PhpDoc\TypeNodeResolver;
+use PHPStan\PhpDoc\TypeStringResolver;
use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension;
use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension;
use PHPStan\Reflection\BetterReflection\BetterReflectionProvider;
@@ -70,6 +72,7 @@
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension;
use PHPStan\Type\Type;
+use PHPStan\Type\TypeAliasResolver;
abstract class TestCase extends \PHPUnit\Framework\TestCase
{
@@ -570,6 +573,21 @@ public function createScopeFactory(Broker $broker, TypeSpecifier $typeSpecifier)
);
}
+ /**
+ * @param array $globalTypeAliases
+ */
+ public function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver
+ {
+ $container = self::getContainer();
+
+ return new TypeAliasResolver(
+ $globalTypeAliases,
+ $container->getByType(TypeStringResolver::class),
+ $container->getByType(TypeNodeResolver::class),
+ $reflectionProvider
+ );
+ }
+
protected function shouldTreatPhpDocTypesAsCertain(): bool
{
return true;
diff --git a/src/Type/CircularTypeAliasDefinitionException.php b/src/Type/CircularTypeAliasDefinitionException.php
new file mode 100644
index 0000000000..62a946a020
--- /dev/null
+++ b/src/Type/CircularTypeAliasDefinitionException.php
@@ -0,0 +1,8 @@
+typeNode = $typeNode;
+ $this->nameScope = $nameScope;
+ }
+
+ public static function invalid(): self
+ {
+ $self = new self(new IdentifierTypeNode('*ERROR*'), new NameScope(null, []));
+ $self->resolvedType = new ErrorType();
+ return $self;
+ }
+
+ public function resolve(TypeNodeResolver $typeNodeResolver): Type
+ {
+ if ($this->resolvedType === null) {
+ $this->resolvedType = $typeNodeResolver->resolve(
+ $this->typeNode,
+ $this->nameScope
+ );
+ }
+
+ return $this->resolvedType;
+ }
+
+}
diff --git a/src/Type/TypeAliasResolver.php b/src/Type/TypeAliasResolver.php
new file mode 100644
index 0000000000..a87dbc6f9b
--- /dev/null
+++ b/src/Type/TypeAliasResolver.php
@@ -0,0 +1,164 @@
+ */
+ private array $globalTypeAliases;
+
+ private TypeStringResolver $typeStringResolver;
+
+ private TypeNodeResolver $typeNodeResolver;
+
+ private ReflectionProvider $reflectionProvider;
+
+ /** @var array */
+ private array $resolvedGlobalTypeAliases = [];
+
+ /** @var array */
+ private array $resolvedLocalTypeAliases = [];
+
+ /** @var array */
+ private array $resolvingClassTypeAliases = [];
+
+ /** @var array */
+ private array $inProcess = [];
+
+ /**
+ * @param array $globalTypeAliases
+ */
+ public function __construct(
+ array $globalTypeAliases,
+ TypeStringResolver $typeStringResolver,
+ TypeNodeResolver $typeNodeResolver,
+ ReflectionProvider $reflectionProvider
+ )
+ {
+ $this->globalTypeAliases = $globalTypeAliases;
+ $this->typeStringResolver = $typeStringResolver;
+ $this->typeNodeResolver = $typeNodeResolver;
+ $this->reflectionProvider = $reflectionProvider;
+ }
+
+ public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool
+ {
+ $hasGlobalTypeAlias = array_key_exists($aliasName, $this->globalTypeAliases);
+ if ($hasGlobalTypeAlias) {
+ return true;
+ }
+
+ if ($classNameScope === null || !$this->reflectionProvider->hasClass($classNameScope)) {
+ return false;
+ }
+
+ $classReflection = $this->reflectionProvider->getClass($classNameScope);
+ $localTypeAliases = $classReflection->getTypeAliases();
+ return array_key_exists($aliasName, $localTypeAliases);
+ }
+
+ public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type
+ {
+ return $this->resolveLocalTypeAlias($aliasName, $nameScope)
+ ?? $this->resolveGlobalTypeAlias($aliasName, $nameScope);
+ }
+
+ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): ?Type
+ {
+ $className = $nameScope->getClassName();
+ if ($className === null) {
+ return null;
+ }
+
+ $aliasNameInClassScope = $className . '::' . $aliasName;
+
+ if (array_key_exists($aliasNameInClassScope, $this->resolvedLocalTypeAliases)) {
+ return $this->resolvedLocalTypeAliases[$aliasNameInClassScope];
+ }
+
+ // prevent infinite recursion
+ if (array_key_exists($className, $this->resolvingClassTypeAliases)) {
+ return null;
+ }
+
+ $this->resolvingClassTypeAliases[$className] = true;
+
+ if (!$this->reflectionProvider->hasClass($className)) {
+ unset($this->resolvingClassTypeAliases[$className]);
+ return null;
+ }
+
+ $classReflection = $this->reflectionProvider->getClass($className);
+ $localTypeAliases = $classReflection->getTypeAliases();
+
+ unset($this->resolvingClassTypeAliases[$className]);
+
+ if (!array_key_exists($aliasName, $localTypeAliases)) {
+ return null;
+ }
+
+ if ($this->reflectionProvider->hasClass($nameScope->resolveStringName($aliasName))) {
+ return null;
+ }
+
+ if (array_key_exists($aliasName, $this->globalTypeAliases)) {
+ return null;
+ }
+
+ if (array_key_exists($aliasNameInClassScope, $this->inProcess)) {
+ // resolve circular reference as ErrorType to make it easier to detect
+ throw new \PHPStan\Type\CircularTypeAliasDefinitionException();
+ }
+
+ $this->inProcess[$aliasNameInClassScope] = true;
+
+ try {
+ $unresolvedAlias = $localTypeAliases[$aliasName];
+ $resolvedAliasType = $unresolvedAlias->resolve($this->typeNodeResolver);
+ } catch (\PHPStan\Type\CircularTypeAliasDefinitionException $e) {
+ $resolvedAliasType = new ErrorType();
+ }
+
+ $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $resolvedAliasType;
+ unset($this->inProcess[$aliasNameInClassScope]);
+
+ return $resolvedAliasType;
+ }
+
+ private function resolveGlobalTypeAlias(string $aliasName, NameScope $nameScope): ?Type
+ {
+ if (!array_key_exists($aliasName, $this->globalTypeAliases)) {
+ return null;
+ }
+
+ if (array_key_exists($aliasName, $this->resolvedGlobalTypeAliases)) {
+ return $this->resolvedGlobalTypeAliases[$aliasName];
+ }
+
+ if ($this->reflectionProvider->hasClass($nameScope->resolveStringName($aliasName))) {
+ throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s already exists as a class.', $aliasName));
+ }
+
+ if (array_key_exists($aliasName, $this->inProcess)) {
+ throw new \PHPStan\ShouldNotHappenException(sprintf('Circular definition for type alias %s.', $aliasName));
+ }
+
+ $this->inProcess[$aliasName] = true;
+
+ $aliasTypeString = $this->globalTypeAliases[$aliasName];
+ $aliasType = $this->typeStringResolver->resolve($aliasTypeString);
+ $this->resolvedGlobalTypeAliases[$aliasName] = $aliasType;
+
+ unset($this->inProcess[$aliasName]);
+
+ return $aliasType;
+ }
+
+}
diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php
index f7c0295663..0b0021d99c 100644
--- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php
+++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php
@@ -10341,6 +10341,10 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/multi-assign.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-reduce-types-first.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4803.php');
+
+ require_once __DIR__ . '/data/type-aliases.php';
+
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/type-aliases.php');
}
/**
@@ -10995,6 +10999,11 @@ private function processFile(
);
}
+ public static function getAdditionalConfigFiles(): array
+ {
+ return [__DIR__ . '/typeAliases.neon'];
+ }
+
public function dataDeclareStrictTypes(): array
{
return [
diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/data/type-aliases.php
new file mode 100644
index 0000000000..188a8ea6c1
--- /dev/null
+++ b/tests/PHPStan/Analyser/data/type-aliases.php
@@ -0,0 +1,161 @@
+baz);
+ assertType('*ERROR*', $this->qux);
+ }
+
+ }
+
+ /**
+ * @phpstan-import-type CircularTypeAliasImport1 from Baz
+ * @phpstan-type CircularTypeAliasImport2 CircularTypeAliasImport1
+ */
+ class Qux
+ {
+ }
+
+ /**
+ * @phpstan-type LocalTypeAlias callable(string $value): (string|false)
+ * @phpstan-type NestedLocalTypeAlias LocalTypeAlias[]
+ * @phpstan-import-type ExportedTypeAlias from Bar as ImportedTypeAlias
+ * @phpstan-import-type ReexportedTypeAlias from Baz
+ * @phpstan-type NestedImportedTypeAlias iterable
+ * @phpstan-import-type ScopedAlias from SubScope\Bar
+ * @phpstan-import-type ImportedAliasFromNonClass from int
+ * @phpstan-import-type ImportedAliasFromUnknownClass from UnknownClass
+ * @phpstan-import-type ImportedUknownAlias from SubScope\Bar
+ * @phpstan-type Baz never
+ * @phpstan-type GlobalTypeAlias never
+ * @phpstan-type RecursiveTypeAlias RecursiveTypeAlias[]
+ * @phpstan-type CircularTypeAlias1 CircularTypeAlias2
+ * @phpstan-type CircularTypeAlias2 CircularTypeAlias1
+ * @phpstan-type int ShouldNotHappen
+ * @property GlobalTypeAlias $globalAliasProperty
+ * @property LocalTypeAlias $localAliasProperty
+ * @property ImportedTypeAlias $importedAliasProperty
+ * @property ReexportedTypeAlias $reexportedAliasProperty
+ * @property ScopedAlias $scopedAliasProperty
+ * @property RecursiveTypeAlias $recursiveAliasProperty
+ * @property CircularTypeAlias1 $circularAliasProperty
+ */
+ class Foo
+ {
+
+ /**
+ * @param GlobalTypeAlias $parameter
+ */
+ public function globalAlias($parameter)
+ {
+ assertType('int|string', $parameter);
+ }
+
+ /**
+ * @param LocalTypeAlias $parameter
+ */
+ public function localAlias($parameter)
+ {
+ assertType('callable(string): string|false', $parameter);
+ }
+
+ /**
+ * @param NestedLocalTypeAlias $parameter
+ */
+ public function nestedLocalAlias($parameter)
+ {
+ assertType('array', $parameter);
+ }
+
+ /**
+ * @param ImportedTypeAlias $parameter
+ */
+ public function importedAlias($parameter)
+ {
+ assertType('Countable&Traversable', $parameter);
+ }
+
+ /**
+ * @param NestedImportedTypeAlias $parameter
+ */
+ public function nestedImportedAlias($parameter)
+ {
+ assertType('iterable', $parameter);
+ }
+
+ /**
+ * @param ImportedAliasFromNonClass $parameter1
+ * @param ImportedAliasFromUnknownClass $parameter2
+ * @param ImportedUknownAlias $parameter3
+ */
+ public function invalidImports($parameter1, $parameter2, $parameter3)
+ {
+ assertType('TypeAliasesDataset\ImportedAliasFromNonClass', $parameter1);
+ assertType('TypeAliasesDataset\ImportedAliasFromUnknownClass', $parameter2);
+ assertType('TypeAliasesDataset\ImportedUknownAlias', $parameter3);
+ }
+
+ /**
+ * @param Baz $parameter
+ */
+ public function conflictingAlias($parameter)
+ {
+ assertType('TypeAliasesDataset\Baz', $parameter);
+ }
+
+ public function __get(string $name)
+ {
+ return null;
+ }
+
+ /** @param int $int */
+ public function testIntAlias($int)
+ {
+ assertType('int', $int);
+ }
+
+ }
+
+ assertType('int|string', (new Foo)->globalAliasProperty);
+ assertType('callable(string): string|false', (new Foo)->localAliasProperty);
+ assertType('Countable&Traversable', (new Foo)->importedAliasProperty);
+ assertType('Countable&Traversable', (new Foo)->reexportedAliasProperty);
+ assertType('TypeAliasesDataset\SubScope\Foo', (new Foo)->scopedAliasProperty);
+ assertType('*ERROR*', (new Foo)->recursiveAliasProperty);
+ assertType('*ERROR*', (new Foo)->circularAliasProperty);
+
+}
diff --git a/tests/PHPStan/Analyser/typeAliases.neon b/tests/PHPStan/Analyser/typeAliases.neon
new file mode 100644
index 0000000000..9f2daa753a
--- /dev/null
+++ b/tests/PHPStan/Analyser/typeAliases.neon
@@ -0,0 +1,3 @@
+parameters:
+ typeAliases:
+ GlobalTypeAlias: int|string
diff --git a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php
new file mode 100644
index 0000000000..76213ff049
--- /dev/null
+++ b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php
@@ -0,0 +1,82 @@
+
+ */
+class LocalTypeAliasesRuleTest extends RuleTestCase
+{
+
+ protected function getRule(): Rule
+ {
+ return new LocalTypeAliasesRule(
+ ['GlobalTypeAlias' => 'int|string'],
+ $this->createReflectionProvider(),
+ self::getContainer()->getByType(TypeNodeResolver::class)
+ );
+ }
+
+ public function testRule(): void
+ {
+ $this->analyse([__DIR__ . '/data/local-type-aliases.php'], [
+ [
+ 'Type alias ExistingClassAlias already exists as a class in scope of LocalTypeAliases\Bar.',
+ 22,
+ ],
+ [
+ 'Type alias GlobalTypeAlias already exists as a global type alias.',
+ 22,
+ ],
+ [
+ 'Circular definition detected in type alias RecursiveTypeAlias.',
+ 22,
+ ],
+ [
+ 'Circular definition detected in type alias CircularTypeAlias1.',
+ 22,
+ ],
+ [
+ 'Circular definition detected in type alias CircularTypeAlias2.',
+ 22,
+ ],
+ [
+ 'Cannot import type alias ImportedAliasFromNonClass: class LocalTypeAliases\int does not exist.',
+ 37,
+ ],
+ [
+ 'Cannot import type alias ImportedAliasFromUnknownClass: class LocalTypeAliases\UnknownClass does not exist.',
+ 37,
+ ],
+ [
+ 'Cannot import type alias ImportedUnknownAlias: type alias does not exist in LocalTypeAliases\Foo.',
+ 37,
+ ],
+ [
+ 'Type alias ExistingClassAlias already exists as a class in scope of LocalTypeAliases\Baz.',
+ 37,
+ ],
+ [
+ 'Type alias GlobalTypeAlias already exists as a global type alias.',
+ 37,
+ ],
+ [
+ 'Type alias OverwrittenTypeAlias overwrites an imported type alias of the same name.',
+ 37,
+ ],
+ [
+ 'Circular definition detected in type alias CircularTypeAliasImport2.',
+ 37,
+ ],
+ [
+ 'Circular definition detected in type alias CircularTypeAliasImport1.',
+ 45,
+ ],
+ ]);
+ }
+
+}
diff --git a/tests/PHPStan/Rules/Classes/data/local-type-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-aliases.php
new file mode 100644
index 0000000000..3d6c367f14
--- /dev/null
+++ b/tests/PHPStan/Rules/Classes/data/local-type-aliases.php
@@ -0,0 +1,47 @@
+createReflectionProvider();
+ $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker);
return new ClassTemplateTypeRule(
new TemplateTypeCheck(
$broker,
new ClassCaseSensitivityCheck($broker),
new GenericObjectTypeCheck(),
- ['TypeAlias' => 'int'],
+ $typeAliasResolver,
true
)
);
@@ -29,6 +30,8 @@ protected function getRule(): Rule
public function testRule(): void
{
+ require_once __DIR__ . '/data/class-template.php';
+
$this->analyse([__DIR__ . '/data/class-template.php'], [
[
'PHPDoc tag @template for class ClassTemplateType\Foo cannot have existing class stdClass as its name.',
@@ -48,27 +51,35 @@ public function testRule(): void
],
[
'PHPDoc tag @template for class ClassTemplateType\Ipsum cannot have existing type alias TypeAlias as its name.',
- 40,
+ 41,
+ ],
+ [
+ 'PHPDoc tag @template for class ClassTemplateType\Dolor cannot have existing type alias LocalAlias as its name.',
+ 53,
+ ],
+ [
+ 'PHPDoc tag @template for class ClassTemplateType\Dolor cannot have existing type alias ImportedAlias as its name.',
+ 53,
],
[
'PHPDoc tag @template for anonymous class cannot have existing class stdClass as its name.',
- 45,
+ 58,
],
[
'PHPDoc tag @template T for anonymous class has invalid bound type ClassTemplateType\Zazzzu.',
- 50,
+ 63,
],
[
'PHPDoc tag @template T for anonymous class with bound type float is not supported.',
- 55,
+ 68,
],
[
'Class ClassTemplateType\Baz referenced with incorrect case: ClassTemplateType\baz.',
- 60,
+ 73,
],
[
'PHPDoc tag @template for anonymous class cannot have existing type alias TypeAlias as its name.',
- 65,
+ 78,
],
]);
}
diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php
index fa5420ebc7..7f3975cf63 100644
--- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php
+++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php
@@ -16,9 +16,11 @@ class FunctionTemplateTypeRuleTest extends RuleTestCase
protected function getRule(): Rule
{
$broker = $this->createReflectionProvider();
+ $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker);
+
return new FunctionTemplateTypeRule(
self::getContainer()->getByType(FileTypeMapper::class),
- new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), ['TypeAlias' => 'int'], true)
+ new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true)
);
}
diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php
index 06cc10f96f..231e2eb0c1 100644
--- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php
+++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php
@@ -16,14 +16,18 @@ class InterfaceTemplateTypeRuleTest extends RuleTestCase
protected function getRule(): Rule
{
$broker = $this->createReflectionProvider();
+ $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker);
+
return new InterfaceTemplateTypeRule(
self::getContainer()->getByType(FileTypeMapper::class),
- new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), ['TypeAlias' => 'int'], true)
+ new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true)
);
}
public function testRule(): void
{
+ require_once __DIR__ . '/data/interface-template.php';
+
$this->analyse([__DIR__ . '/data/interface-template.php'], [
[
'PHPDoc tag @template for interface InterfaceTemplateType\Foo cannot have existing class stdClass as its name.',
@@ -39,7 +43,15 @@ public function testRule(): void
],
[
'PHPDoc tag @template for interface InterfaceTemplateType\Lorem cannot have existing type alias TypeAlias as its name.',
- 32,
+ 33,
+ ],
+ [
+ 'PHPDoc tag @template for interface InterfaceTemplateType\Ipsum cannot have existing type alias LocalAlias as its name.',
+ 45,
+ ],
+ [
+ 'PHPDoc tag @template for interface InterfaceTemplateType\Ipsum cannot have existing type alias ImportedAlias as its name.',
+ 45,
],
]);
}
diff --git a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php
index 585bec3697..d8f2c90422 100644
--- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php
+++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php
@@ -16,14 +16,18 @@ class MethodTemplateTypeRuleTest extends RuleTestCase
protected function getRule(): Rule
{
$broker = $this->createReflectionProvider();
+ $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker);
+
return new MethodTemplateTypeRule(
self::getContainer()->getByType(FileTypeMapper::class),
- new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), ['TypeAlias' => 'int'], true)
+ new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true)
);
}
public function testRule(): void
{
+ require_once __DIR__ . '/data/method-template.php';
+
$this->analyse([__DIR__ . '/data/method-template.php'], [
[
'PHPDoc tag @template for method MethodTemplateType\Foo::doFoo() cannot have existing class stdClass as its name.',
@@ -43,7 +47,15 @@ public function testRule(): void
],
[
'PHPDoc tag @template for method MethodTemplateType\Lorem::doFoo() cannot have existing type alias TypeAlias as its name.',
- 63,
+ 66,
+ ],
+ [
+ 'PHPDoc tag @template for method MethodTemplateType\Ipsum::doFoo() cannot have existing type alias LocalAlias as its name.',
+ 85,
+ ],
+ [
+ 'PHPDoc tag @template for method MethodTemplateType\Ipsum::doFoo() cannot have existing type alias ImportedAlias as its name.',
+ 85,
],
]);
}
diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php
index 19a533f784..5ae1a3b1aa 100644
--- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php
+++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php
@@ -16,14 +16,18 @@ class TraitTemplateTypeRuleTest extends RuleTestCase
protected function getRule(): Rule
{
$broker = $this->createReflectionProvider();
+ $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker);
+
return new TraitTemplateTypeRule(
self::getContainer()->getByType(FileTypeMapper::class),
- new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), ['TypeAlias' => 'int'], true)
+ new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true)
);
}
public function testRule(): void
{
+ require_once __DIR__ . '/data/trait-template.php';
+
$this->analyse([__DIR__ . '/data/trait-template.php'], [
[
'PHPDoc tag @template for trait TraitTemplateType\Foo cannot have existing class stdClass as its name.',
@@ -39,7 +43,15 @@ public function testRule(): void
],
[
'PHPDoc tag @template for trait TraitTemplateType\Lorem cannot have existing type alias TypeAlias as its name.',
- 32,
+ 33,
+ ],
+ [
+ 'PHPDoc tag @template for trait TraitTemplateType\Ipsum cannot have existing type alias LocalAlias as its name.',
+ 45,
+ ],
+ [
+ 'PHPDoc tag @template for trait TraitTemplateType\Ipsum cannot have existing type alias ImportedAlias as its name.',
+ 45,
],
]);
}
diff --git a/tests/PHPStan/Rules/Generics/data/class-template.php b/tests/PHPStan/Rules/Generics/data/class-template.php
index 771edc5c00..752983b26a 100644
--- a/tests/PHPStan/Rules/Generics/data/class-template.php
+++ b/tests/PHPStan/Rules/Generics/data/class-template.php
@@ -35,6 +35,7 @@ class Lorem
}
/**
+ * @phpstan-type ExportedAlias string
* @template TypeAlias
*/
class Ipsum
@@ -42,6 +43,18 @@ class Ipsum
}
+/**
+ * @phpstan-type LocalAlias string
+ * @phpstan-import-type ExportedAlias from Ipsum as ImportedAlias
+ * @template LocalAlias
+ * @template ExportedAlias
+ * @template ImportedAlias
+ */
+class Dolor
+{
+
+}
+
new /** @template stdClass */ class
{
diff --git a/tests/PHPStan/Rules/Generics/data/interface-template.php b/tests/PHPStan/Rules/Generics/data/interface-template.php
index a29f5dc702..ed7f3ef667 100644
--- a/tests/PHPStan/Rules/Generics/data/interface-template.php
+++ b/tests/PHPStan/Rules/Generics/data/interface-template.php
@@ -27,6 +27,7 @@ interface Baz
}
/**
+ * @phpstan-type ExportedAlias string
* @template TypeAlias
*/
interface Lorem
@@ -34,6 +35,18 @@ interface Lorem
}
+/**
+ * @phpstan-type LocalAlias string
+ * @phpstan-import-type ExportedAlias from Lorem as ImportedAlias
+ * @template LocalAlias
+ * @template ExportedAlias
+ * @template ImportedAlias
+ */
+interface Ipsum
+{
+
+}
+
/** @template T */
interface NormalT
{
diff --git a/tests/PHPStan/Rules/Generics/data/method-template.php b/tests/PHPStan/Rules/Generics/data/method-template.php
index a5fb337100..fc6c4c87e2 100644
--- a/tests/PHPStan/Rules/Generics/data/method-template.php
+++ b/tests/PHPStan/Rules/Generics/data/method-template.php
@@ -54,6 +54,9 @@ public function doFoo()
}
+/**
+ * @phpstan-type ExportedAlias string
+ */
class Lorem
{
@@ -66,3 +69,22 @@ public function doFoo()
}
}
+
+/**
+ * @phpstan-type LocalAlias string
+ * @phpstan-import-type ExportedAlias from Lorem as ImportedAlias
+ */
+class Ipsum
+{
+
+ /**
+ * @template LocalAlias
+ * @template ExportedAlias
+ * @template ImportedAlias
+ */
+ public function doFoo()
+ {
+
+ }
+
+}
diff --git a/tests/PHPStan/Rules/Generics/data/trait-template.php b/tests/PHPStan/Rules/Generics/data/trait-template.php
index c5f3526edd..8870b4dd98 100644
--- a/tests/PHPStan/Rules/Generics/data/trait-template.php
+++ b/tests/PHPStan/Rules/Generics/data/trait-template.php
@@ -27,9 +27,22 @@ trait Baz
}
/**
+ * @phpstan-type ExportedAlias string
* @template TypeAlias
*/
trait Lorem
{
}
+
+/**
+ * @phpstan-type LocalAlias string
+ * @phpstan-import-type ExportedAlias from Lorem as ImportedAlias
+ * @template LocalAlias
+ * @template ExportedAlias
+ * @template ImportedAlias
+ */
+trait Ipsum
+{
+
+}