Skip to content

Commit 4654c16

Browse files
committed
Check local type aliases above traits
1 parent 60021c2 commit 4654c16

8 files changed

+390
-166
lines changed

Diff for: conf/config.level0.neon

+2-7
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ rules:
5050
- PHPStan\Rules\Classes\InstantiationRule
5151
- PHPStan\Rules\Classes\InstantiationCallableRule
5252
- PHPStan\Rules\Classes\InvalidPromotedPropertiesRule
53+
- PHPStan\Rules\Classes\LocalTypeAliasesRule
54+
- PHPStan\Rules\Classes\LocalTypeTraitAliasesRule
5355
- PHPStan\Rules\Classes\NewStaticRule
5456
- PHPStan\Rules\Classes\NonClassAttributeClassRule
5557
- PHPStan\Rules\Classes\TraitAttributeClassRule
@@ -258,13 +260,6 @@ services:
258260
tags:
259261
- phpstan.rules.rule
260262

261-
-
262-
class: PHPStan\Rules\Classes\LocalTypeAliasesRule
263-
arguments:
264-
globalTypeAliases: %typeAliases%
265-
tags:
266-
- phpstan.rules.rule
267-
268263
-
269264
class: PHPStan\Reflection\ConstructorsHelper
270265
arguments:

Diff for: conf/config.neon

+5
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,11 @@ services:
953953
arguments:
954954
checkInternalClassCaseSensitivity: %checkInternalClassCaseSensitivity%
955955

956+
-
957+
class: PHPStan\Rules\Classes\LocalTypeAliasesCheck
958+
arguments:
959+
globalTypeAliases: %typeAliases%
960+
956961
-
957962
class: PHPStan\Rules\Comparison\ConstantConditionRuleHelper
958963
arguments:

Diff for: src/Rules/Classes/LocalTypeAliasesCheck.php

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
use PHPStan\Analyser\NameScope;
6+
use PHPStan\PhpDoc\TypeNodeResolver;
7+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
8+
use PHPStan\Reflection\ClassReflection;
9+
use PHPStan\Reflection\ReflectionProvider;
10+
use PHPStan\Rules\RuleError;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use PHPStan\Type\CircularTypeAliasErrorType;
13+
use PHPStan\Type\ErrorType;
14+
use PHPStan\Type\Generic\TemplateType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeTraverser;
17+
use function array_key_exists;
18+
use function in_array;
19+
use function sprintf;
20+
21+
class LocalTypeAliasesCheck
22+
{
23+
24+
/**
25+
* @param array<string, string> $globalTypeAliases
26+
*/
27+
public function __construct(
28+
private array $globalTypeAliases,
29+
private ReflectionProvider $reflectionProvider,
30+
private TypeNodeResolver $typeNodeResolver,
31+
)
32+
{
33+
}
34+
35+
/**
36+
* @return RuleError[]
37+
*/
38+
public function check(ClassReflection $reflection): array
39+
{
40+
$phpDoc = $reflection->getResolvedPhpDoc();
41+
if ($phpDoc === null) {
42+
return [];
43+
}
44+
45+
$nameScope = $phpDoc->getNullableNameScope();
46+
$resolveName = static function (string $name) use ($nameScope): string {
47+
if ($nameScope === null) {
48+
return $name;
49+
}
50+
51+
return $nameScope->resolveStringName($name);
52+
};
53+
54+
$errors = [];
55+
$className = $reflection->getName();
56+
57+
$importedAliases = [];
58+
59+
foreach ($phpDoc->getTypeAliasImportTags() as $typeAliasImportTag) {
60+
$aliasName = $typeAliasImportTag->getImportedAs() ?? $typeAliasImportTag->getImportedAlias();
61+
$importedAlias = $typeAliasImportTag->getImportedAlias();
62+
$importedFromClassName = $typeAliasImportTag->getImportedFrom();
63+
64+
if (!$this->reflectionProvider->hasClass($importedFromClassName)) {
65+
$errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: class %s does not exist.', $importedAlias, $importedFromClassName))->build();
66+
continue;
67+
}
68+
69+
$importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName);
70+
$typeAliases = $importedFromReflection->getTypeAliases();
71+
72+
if (!array_key_exists($importedAlias, $typeAliases)) {
73+
$errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: type alias does not exist in %s.', $importedAlias, $importedFromClassName))->build();
74+
continue;
75+
}
76+
77+
$resolvedName = $resolveName($aliasName);
78+
if ($this->reflectionProvider->hasClass($resolveName($aliasName))) {
79+
$classReflection = $this->reflectionProvider->getClass($resolvedName);
80+
$classLikeDescription = 'a class';
81+
if ($classReflection->isInterface()) {
82+
$classLikeDescription = 'an interface';
83+
} elseif ($classReflection->isTrait()) {
84+
$classLikeDescription = 'a trait';
85+
} elseif ($classReflection->isEnum()) {
86+
$classLikeDescription = 'an enum';
87+
}
88+
$errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className))->build();
89+
continue;
90+
}
91+
92+
if (array_key_exists($aliasName, $this->globalTypeAliases)) {
93+
$errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build();
94+
continue;
95+
}
96+
97+
$importedAs = $typeAliasImportTag->getImportedAs();
98+
if ($importedAs !== null && !$this->isAliasNameValid($importedAs, $nameScope)) {
99+
$errors[] = RuleErrorBuilder::message(sprintf('Imported type alias %s has an invalid name: %s.', $importedAlias, $importedAs))->build();
100+
continue;
101+
}
102+
103+
$importedAliases[] = $aliasName;
104+
}
105+
106+
foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) {
107+
$aliasName = $typeAliasTag->getAliasName();
108+
109+
if (in_array($aliasName, $importedAliases, true)) {
110+
$errors[] = RuleErrorBuilder::message(sprintf('Type alias %s overwrites an imported type alias of the same name.', $aliasName))->build();
111+
continue;
112+
}
113+
114+
$resolvedName = $resolveName($aliasName);
115+
if ($this->reflectionProvider->hasClass($resolvedName)) {
116+
$classReflection = $this->reflectionProvider->getClass($resolvedName);
117+
$classLikeDescription = 'a class';
118+
if ($classReflection->isInterface()) {
119+
$classLikeDescription = 'an interface';
120+
} elseif ($classReflection->isTrait()) {
121+
$classLikeDescription = 'a trait';
122+
} elseif ($classReflection->isEnum()) {
123+
$classLikeDescription = 'an enum';
124+
}
125+
$errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className))->build();
126+
continue;
127+
}
128+
129+
if (array_key_exists($aliasName, $this->globalTypeAliases)) {
130+
$errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build();
131+
continue;
132+
}
133+
134+
if (!$this->isAliasNameValid($aliasName, $nameScope)) {
135+
$errors[] = RuleErrorBuilder::message(sprintf('Type alias has an invalid name: %s.', $aliasName))->build();
136+
continue;
137+
}
138+
139+
$resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver);
140+
$foundError = false;
141+
TypeTraverser::map($resolvedType, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type {
142+
if ($foundError) {
143+
return $type;
144+
}
145+
146+
if ($type instanceof CircularTypeAliasErrorType) {
147+
$errors[] = RuleErrorBuilder::message(sprintf('Circular definition detected in type alias %s.', $aliasName))->build();
148+
$foundError = true;
149+
return $type;
150+
}
151+
152+
if ($type instanceof ErrorType) {
153+
$errors[] = RuleErrorBuilder::message(sprintf('Invalid type definition detected in type alias %s.', $aliasName))->build();
154+
$foundError = true;
155+
return $type;
156+
}
157+
158+
return $traverse($type);
159+
});
160+
}
161+
162+
return $errors;
163+
}
164+
165+
private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): bool
166+
{
167+
if ($nameScope === null) {
168+
return true;
169+
}
170+
171+
$aliasNameResolvedType = $this->typeNodeResolver->resolve(new IdentifierTypeNode($aliasName), $nameScope->bypassTypeAliases());
172+
return ($aliasNameResolvedType->isObject()->yes() && !in_array($aliasName, ['self', 'parent'], true))
173+
|| $aliasNameResolvedType instanceof TemplateType; // aliases take precedence over type parameters, this is reported by other rules using TemplateTypeCheck
174+
}
175+
176+
}

Diff for: src/Rules/Classes/LocalTypeAliasesRule.php

+2-156
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,17 @@
33
namespace PHPStan\Rules\Classes;
44

55
use PhpParser\Node;
6-
use PHPStan\Analyser\NameScope;
76
use PHPStan\Analyser\Scope;
87
use PHPStan\Node\InClassNode;
9-
use PHPStan\PhpDoc\TypeNodeResolver;
10-
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
11-
use PHPStan\Reflection\ReflectionProvider;
128
use PHPStan\Rules\Rule;
13-
use PHPStan\Rules\RuleErrorBuilder;
14-
use PHPStan\Type\CircularTypeAliasErrorType;
15-
use PHPStan\Type\ErrorType;
16-
use PHPStan\Type\Generic\TemplateType;
17-
use PHPStan\Type\Type;
18-
use PHPStan\Type\TypeTraverser;
19-
use function array_key_exists;
20-
use function in_array;
21-
use function sprintf;
229

2310
/**
2411
* @implements Rule<InClassNode>
2512
*/
2613
class LocalTypeAliasesRule implements Rule
2714
{
2815

29-
/**
30-
* @param array<string, string> $globalTypeAliases
31-
*/
32-
public function __construct(
33-
private array $globalTypeAliases,
34-
private ReflectionProvider $reflectionProvider,
35-
private TypeNodeResolver $typeNodeResolver,
36-
)
16+
public function __construct(private LocalTypeAliasesCheck $check)
3717
{
3818
}
3919

@@ -44,141 +24,7 @@ public function getNodeType(): string
4424

4525
public function processNode(Node $node, Scope $scope): array
4626
{
47-
$reflection = $node->getClassReflection();
48-
$phpDoc = $reflection->getResolvedPhpDoc();
49-
if ($phpDoc === null) {
50-
return [];
51-
}
52-
53-
$nameScope = $phpDoc->getNullableNameScope();
54-
$resolveName = static function (string $name) use ($nameScope): string {
55-
if ($nameScope === null) {
56-
return $name;
57-
}
58-
59-
return $nameScope->resolveStringName($name);
60-
};
61-
62-
$errors = [];
63-
$className = $reflection->getName();
64-
65-
$importedAliases = [];
66-
67-
foreach ($phpDoc->getTypeAliasImportTags() as $typeAliasImportTag) {
68-
$aliasName = $typeAliasImportTag->getImportedAs() ?? $typeAliasImportTag->getImportedAlias();
69-
$importedAlias = $typeAliasImportTag->getImportedAlias();
70-
$importedFromClassName = $typeAliasImportTag->getImportedFrom();
71-
72-
if (!$this->reflectionProvider->hasClass($importedFromClassName)) {
73-
$errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: class %s does not exist.', $importedAlias, $importedFromClassName))->build();
74-
continue;
75-
}
76-
77-
$importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName);
78-
$typeAliases = $importedFromReflection->getTypeAliases();
79-
80-
if (!array_key_exists($importedAlias, $typeAliases)) {
81-
$errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: type alias does not exist in %s.', $importedAlias, $importedFromClassName))->build();
82-
continue;
83-
}
84-
85-
$resolvedName = $resolveName($aliasName);
86-
if ($this->reflectionProvider->hasClass($resolveName($aliasName))) {
87-
$classReflection = $this->reflectionProvider->getClass($resolvedName);
88-
$classLikeDescription = 'a class';
89-
if ($classReflection->isInterface()) {
90-
$classLikeDescription = 'an interface';
91-
} elseif ($classReflection->isTrait()) {
92-
$classLikeDescription = 'a trait';
93-
} elseif ($classReflection->isEnum()) {
94-
$classLikeDescription = 'an enum';
95-
}
96-
$errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className))->build();
97-
continue;
98-
}
99-
100-
if (array_key_exists($aliasName, $this->globalTypeAliases)) {
101-
$errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build();
102-
continue;
103-
}
104-
105-
$importedAs = $typeAliasImportTag->getImportedAs();
106-
if ($importedAs !== null && !$this->isAliasNameValid($importedAs, $nameScope)) {
107-
$errors[] = RuleErrorBuilder::message(sprintf('Imported type alias %s has an invalid name: %s.', $importedAlias, $importedAs))->build();
108-
continue;
109-
}
110-
111-
$importedAliases[] = $aliasName;
112-
}
113-
114-
foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) {
115-
$aliasName = $typeAliasTag->getAliasName();
116-
117-
if (in_array($aliasName, $importedAliases, true)) {
118-
$errors[] = RuleErrorBuilder::message(sprintf('Type alias %s overwrites an imported type alias of the same name.', $aliasName))->build();
119-
continue;
120-
}
121-
122-
$resolvedName = $resolveName($aliasName);
123-
if ($this->reflectionProvider->hasClass($resolvedName)) {
124-
$classReflection = $this->reflectionProvider->getClass($resolvedName);
125-
$classLikeDescription = 'a class';
126-
if ($classReflection->isInterface()) {
127-
$classLikeDescription = 'an interface';
128-
} elseif ($classReflection->isTrait()) {
129-
$classLikeDescription = 'a trait';
130-
} elseif ($classReflection->isEnum()) {
131-
$classLikeDescription = 'an enum';
132-
}
133-
$errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className))->build();
134-
continue;
135-
}
136-
137-
if (array_key_exists($aliasName, $this->globalTypeAliases)) {
138-
$errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build();
139-
continue;
140-
}
141-
142-
if (!$this->isAliasNameValid($aliasName, $nameScope)) {
143-
$errors[] = RuleErrorBuilder::message(sprintf('Type alias has an invalid name: %s.', $aliasName))->build();
144-
continue;
145-
}
146-
147-
$resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver);
148-
$foundError = false;
149-
TypeTraverser::map($resolvedType, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type {
150-
if ($foundError) {
151-
return $type;
152-
}
153-
154-
if ($type instanceof CircularTypeAliasErrorType) {
155-
$errors[] = RuleErrorBuilder::message(sprintf('Circular definition detected in type alias %s.', $aliasName))->build();
156-
$foundError = true;
157-
return $type;
158-
}
159-
160-
if ($type instanceof ErrorType) {
161-
$errors[] = RuleErrorBuilder::message(sprintf('Invalid type definition detected in type alias %s.', $aliasName))->build();
162-
$foundError = true;
163-
return $type;
164-
}
165-
166-
return $traverse($type);
167-
});
168-
}
169-
170-
return $errors;
171-
}
172-
173-
private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): bool
174-
{
175-
if ($nameScope === null) {
176-
return true;
177-
}
178-
179-
$aliasNameResolvedType = $this->typeNodeResolver->resolve(new IdentifierTypeNode($aliasName), $nameScope->bypassTypeAliases());
180-
return ($aliasNameResolvedType->isObject()->yes() && !in_array($aliasName, ['self', 'parent'], true))
181-
|| $aliasNameResolvedType instanceof TemplateType; // aliases take precedence over type parameters, this is reported by other rules using TemplateTypeCheck
27+
return $this->check->check($node->getClassReflection());
18228
}
18329

18430
}

0 commit comments

Comments
 (0)