Skip to content

Commit b02ee14

Browse files
committed
Support for @mixin
1 parent 28d1c34 commit b02ee14

15 files changed

+657
-0
lines changed

conf/config.level2.neon

+6
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ rules:
3333
- PHPStan\Rules\PhpDoc\WrongVariableNameInVarTagRule
3434

3535
services:
36+
-
37+
class: PHPStan\Rules\Classes\MixinRule
38+
arguments:
39+
checkClassCaseSensitivity: %checkClassCaseSensitivity%
40+
tags:
41+
- phpstan.rules.rule
3642
-
3743
class: PHPStan\Rules\Functions\CallCallablesRule
3844
arguments:

conf/config.neon

+10
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,16 @@ services:
476476
-
477477
class: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository
478478

479+
-
480+
class: PHPStan\Reflection\Mixin\MixinMethodsClassReflectionExtension
481+
tags:
482+
- phpstan.broker.methodsClassReflectionExtension
483+
484+
-
485+
class: PHPStan\Reflection\Mixin\MixinPropertiesClassReflectionExtension
486+
tags:
487+
- phpstan.broker.propertiesClassReflectionExtension
488+
479489
-
480490
class: PHPStan\Reflection\Php\PhpClassReflectionExtension
481491
arguments:

src/PhpDoc/PhpDocNodeResolver.php

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PHPStan\PhpDoc\Tag\ImplementsTag;
99
use PHPStan\PhpDoc\Tag\MethodTag;
1010
use PHPStan\PhpDoc\Tag\MethodTagParameter;
11+
use PHPStan\PhpDoc\Tag\MixinTag;
1112
use PHPStan\PhpDoc\Tag\ParamTag;
1213
use PHPStan\PhpDoc\Tag\PropertyTag;
1314
use PHPStan\PhpDoc\Tag\ReturnTag;
@@ -16,6 +17,7 @@
1617
use PHPStan\PhpDoc\Tag\UsesTag;
1718
use PHPStan\PhpDoc\Tag\VarTag;
1819
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode;
20+
use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
1921
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
2022
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
2123
use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
@@ -339,6 +341,20 @@ public function resolveThrowsTags(PhpDocNode $phpDocNode, NameScope $nameScope):
339341
return new ThrowsTag(TypeCombinator::union(...$types));
340342
}
341343

344+
/**
345+
* @param PhpDocNode $phpDocNode
346+
* @param NameScope $nameScope
347+
* @return array<MixinTag>
348+
*/
349+
public function resolveMixinTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
350+
{
351+
return array_map(function (MixinTagValueNode $mixinTagValueNode) use ($nameScope): MixinTag {
352+
return new MixinTag(
353+
$this->typeNodeResolver->resolve($mixinTagValueNode->type, $nameScope)
354+
);
355+
}, $phpDocNode->getMixinTagValues());
356+
}
357+
342358
public function resolveDeprecatedTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?\PHPStan\PhpDoc\Tag\DeprecatedTag
343359
{
344360
foreach ($phpDocNode->getDeprecatedTagValues() as $deprecatedTagValue) {

src/PhpDoc/ResolvedPhpDocBlock.php

+19
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\PhpDoc;
44

55
use PHPStan\Analyser\NameScope;
6+
use PHPStan\PhpDoc\Tag\MixinTag;
67
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
78
use PHPStan\Type\Generic\TemplateTypeMap;
89

@@ -57,6 +58,9 @@ class ResolvedPhpDocBlock
5758
/** @var \PHPStan\PhpDoc\Tag\ThrowsTag|false|null */
5859
private $throwsTag = false;
5960

61+
/** @var array<MixinTag>|false */
62+
private $mixinTags = false;
63+
6064
/** @var \PHPStan\PhpDoc\Tag\DeprecatedTag|false|null */
6165
private $deprecatedTag = false;
6266

@@ -314,6 +318,21 @@ public function getThrowsTag(): ?\PHPStan\PhpDoc\Tag\ThrowsTag
314318
return $this->throwsTag;
315319
}
316320

321+
/**
322+
* @return array<MixinTag>
323+
*/
324+
public function getMixinTags(): array
325+
{
326+
if ($this->mixinTags === false) {
327+
$this->mixinTags = $this->phpDocNodeResolver->resolveMixinTags(
328+
$this->phpDocNode,
329+
$this->nameScope
330+
);
331+
}
332+
333+
return $this->mixinTags;
334+
}
335+
317336
public function getDeprecatedTag(): ?\PHPStan\PhpDoc\Tag\DeprecatedTag
318337
{
319338
if ($this->deprecatedTag === false) {

src/PhpDoc/Tag/MixinTag.php

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDoc\Tag;
4+
5+
use PHPStan\Type\Type;
6+
7+
class MixinTag
8+
{
9+
10+
/** @var \PHPStan\Type\Type */
11+
private $type;
12+
13+
public function __construct(Type $type)
14+
{
15+
$this->type = $type;
16+
}
17+
18+
public function getType(): Type
19+
{
20+
return $this->type;
21+
}
22+
23+
/**
24+
* @param mixed[] $properties
25+
* @return self
26+
*/
27+
public static function __set_state(array $properties): self
28+
{
29+
return new self(
30+
$properties['type']
31+
);
32+
}
33+
34+
}

src/Reflection/ClassReflection.php

+35
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
66
use PHPStan\PhpDoc\Tag\ExtendsTag;
77
use PHPStan\PhpDoc\Tag\ImplementsTag;
8+
use PHPStan\PhpDoc\Tag\MixinTag;
89
use PHPStan\PhpDoc\Tag\TemplateTag;
910
use PHPStan\Reflection\Php\PhpClassReflectionExtension;
1011
use PHPStan\Reflection\Php\PhpPropertyReflection;
@@ -941,4 +942,38 @@ private function isValidAncestorType(Type $type, array $ancestorClasses): bool
941942
return in_array($reflection->getName(), $ancestorClasses, true);
942943
}
943944

945+
/**
946+
* @return array<MixinTag>
947+
*/
948+
public function getMixinTags(): array
949+
{
950+
$resolvedPhpDoc = $this->getResolvedPhpDoc();
951+
if ($resolvedPhpDoc === null) {
952+
return [];
953+
}
954+
955+
return $resolvedPhpDoc->getMixinTags();
956+
}
957+
958+
/**
959+
* @return array<Type>
960+
*/
961+
public function getResolvedMixinTypes(): array
962+
{
963+
$types = [];
964+
foreach ($this->getMixinTags() as $mixinTag) {
965+
if (!$this->isGeneric()) {
966+
$types[] = $mixinTag->getType();
967+
continue;
968+
}
969+
970+
$types[] = TemplateTypeHelper::resolveTemplateTypes(
971+
$mixinTag->getType(),
972+
$this->getActiveTemplateTypeMap()
973+
);
974+
}
975+
976+
return $types;
977+
}
978+
944979
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Reflection\Mixin;
4+
5+
use PHPStan\Analyser\OutOfClassScope;
6+
use PHPStan\Reflection\ClassReflection;
7+
use PHPStan\Reflection\MethodReflection;
8+
use PHPStan\Reflection\MethodsClassReflectionExtension;
9+
10+
class MixinMethodsClassReflectionExtension implements MethodsClassReflectionExtension
11+
{
12+
13+
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
14+
{
15+
return $this->findMethod($classReflection, $methodName) !== null;
16+
}
17+
18+
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
19+
{
20+
$method = $this->findMethod($classReflection, $methodName);
21+
if ($method === null) {
22+
throw new \PHPStan\ShouldNotHappenException();
23+
}
24+
25+
return $method;
26+
}
27+
28+
private function findMethod(ClassReflection $classReflection, string $methodName): ?MethodReflection
29+
{
30+
$mixinTypes = $classReflection->getResolvedMixinTypes();
31+
foreach ($mixinTypes as $type) {
32+
if (!$type->hasMethod($methodName)->yes()) {
33+
continue;
34+
}
35+
36+
return $type->getMethod($methodName, new OutOfClassScope());
37+
}
38+
39+
foreach ($classReflection->getParents() as $parentClass) {
40+
$method = $this->findMethod($parentClass, $methodName);
41+
if ($method === null) {
42+
continue;
43+
}
44+
45+
return $method;
46+
}
47+
48+
return null;
49+
}
50+
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Reflection\Mixin;
4+
5+
use PHPStan\Analyser\OutOfClassScope;
6+
use PHPStan\Reflection\ClassReflection;
7+
use PHPStan\Reflection\PropertiesClassReflectionExtension;
8+
use PHPStan\Reflection\PropertyReflection;
9+
10+
class MixinPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension
11+
{
12+
13+
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
14+
{
15+
return $this->findProperty($classReflection, $propertyName) !== null;
16+
}
17+
18+
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
19+
{
20+
$property = $this->findProperty($classReflection, $propertyName);
21+
if ($property === null) {
22+
throw new \PHPStan\ShouldNotHappenException();
23+
}
24+
25+
return $property;
26+
}
27+
28+
private function findProperty(ClassReflection $classReflection, string $propertyName): ?PropertyReflection
29+
{
30+
$mixinTypes = $classReflection->getResolvedMixinTypes();
31+
foreach ($mixinTypes as $type) {
32+
if (!$type->hasProperty($propertyName)->yes()) {
33+
continue;
34+
}
35+
36+
return $type->getProperty($propertyName, new OutOfClassScope());
37+
}
38+
39+
foreach ($classReflection->getParents() as $parentClass) {
40+
$property = $this->findProperty($parentClass, $propertyName);
41+
if ($property === null) {
42+
continue;
43+
}
44+
45+
return $property;
46+
}
47+
48+
return null;
49+
}
50+
51+
}

0 commit comments

Comments
 (0)