-
-
Notifications
You must be signed in to change notification settings - Fork 394
/
CollectionGenericStaticMethodDynamicMethodReturnTypeExtension.php
137 lines (111 loc) · 5.14 KB
/
CollectionGenericStaticMethodDynamicMethodReturnTypeExtension.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
<?php
namespace NunoMaduro\Larastan\ReturnTypes;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnionType;
class CollectionGenericStaticMethodDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return Collection::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
if ($methodReflection->getDeclaringClass()->getName() === EloquentCollection::class) {
return in_array($methodReflection->getName(), ['find']);
}
return in_array($methodReflection->getName(), [
'chunk', 'chunkWhile', 'collapse', 'combine',
'countBy', 'crossJoin', 'flatMap', 'flip',
'groupBy', 'keyBy', 'keys',
'make', 'map', 'mapInto',
'mapToDictionary', 'mapToGroups',
'mapWithKeys', 'mergeRecursive',
'pad', 'partition', 'pluck',
'pop', 'random', 'shift', 'sliding', 'split',
'splitIn', 'values', 'wrap', 'zip',
], true);
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
$returnType = ParametersAcceptorSelector::selectFromArgs(
$scope,
$methodCall->getArgs(),
$methodReflection->getVariants()
)->getReturnType();
if (! $returnType instanceof ObjectType && ! $returnType instanceof UnionType) {
return $returnType;
}
$calledOnType = $scope->getType($methodCall->var);
if (! $calledOnType instanceof TypeWithClassName) {
return $returnType;
}
$classReflection = $calledOnType->getClassReflection();
if ($classReflection === null) {
return $returnType;
}
// If it's called on Support collection, just return.
if ($classReflection->getName() === Collection::class) {
return $returnType;
}
// Special cases for methods returning single models
if ($classReflection->getName() === EloquentCollection::class && (new ObjectType(Model::class))->isSuperTypeOf($returnType)->yes()) {
return $returnType;
}
// If it's a UnionType, traverse the types and try to find a collection object type
if ($returnType instanceof UnionType) {
return $returnType->traverse(function (Type $type) use ($classReflection) {
if ($type instanceof GenericObjectType && (($innerReflection = $type->getClassReflection())) !== null) {
return $this->handleGenericObjectType($classReflection, $innerReflection);
}
return $type;
});
}
$returnTypeClassReflection = $returnType->getClassReflection();
if ($returnTypeClassReflection === null) {
return $returnType;
}
return $this->handleGenericObjectType($classReflection, $returnTypeClassReflection);
}
private function handleGenericObjectType(ClassReflection $classReflection, ClassReflection $returnTypeClassReflection): ObjectType
{
if ($classReflection->getActiveTemplateTypeMap()->count() !== $returnTypeClassReflection->getActiveTemplateTypeMap()->count()) {
return new ObjectType($classReflection->getName());
}
$genericTypes = $returnTypeClassReflection->typeMapToList($returnTypeClassReflection->getActiveTemplateTypeMap());
// If the key type is gonna be a model, we change it to string
if ((new ObjectType(Model::class))->isSuperTypeOf($genericTypes[0])->yes()) {
$genericTypes[0] = new StringType();
}
$genericTypes = array_map(static function (Type $type) use ($classReflection) {
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($classReflection): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}
if ($type instanceof GenericObjectType && (($innerTypeReflection = $type->getClassReflection()) !== null)) {
$genericTypes = $innerTypeReflection->typeMapToList($innerTypeReflection->getActiveTemplateTypeMap());
return new GenericObjectType($classReflection->getName(), $genericTypes);
}
return $traverse($type);
});
}, $genericTypes);
return new GenericObjectType($classReflection->getName(), $genericTypes);
}
}