1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
namespace TheCodingMachine\PHPStan\Rules\TypeHints; |
5
|
|
|
|
6
|
|
|
|
7
|
|
|
use phpDocumentor\Reflection\Type; |
8
|
|
|
use phpDocumentor\Reflection\Types\Array_; |
9
|
|
|
use phpDocumentor\Reflection\Types\Boolean; |
10
|
|
|
use phpDocumentor\Reflection\Types\Callable_; |
11
|
|
|
use phpDocumentor\Reflection\Types\Float_; |
12
|
|
|
use phpDocumentor\Reflection\Types\Integer; |
13
|
|
|
use phpDocumentor\Reflection\Types\Mixed; |
14
|
|
|
use phpDocumentor\Reflection\Types\Null_; |
15
|
|
|
use phpDocumentor\Reflection\Types\Object_; |
16
|
|
|
use phpDocumentor\Reflection\Types\Scalar; |
17
|
|
|
use phpDocumentor\Reflection\Types\String_; |
18
|
|
|
use PhpParser\Node; |
19
|
|
|
use PHPStan\Analyser\Scope; |
20
|
|
|
use PHPStan\Broker\Broker; |
21
|
|
|
use PHPStan\Rules\Rule; |
22
|
|
|
use Roave\BetterReflection\Reflection\ReflectionClass; |
23
|
|
|
use Roave\BetterReflection\Reflection\ReflectionFunction; |
24
|
|
|
use Roave\BetterReflection\Reflection\ReflectionMethod; |
25
|
|
|
use Roave\BetterReflection\Reflection\ReflectionParameter; |
26
|
|
|
use Roave\BetterReflection\Util\FindReflectionOnLine; |
27
|
|
|
|
28
|
|
|
abstract class AbstractMissingTypeHintRule implements Rule |
29
|
|
|
{ |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var Broker |
33
|
|
|
*/ |
34
|
|
|
private $broker; |
35
|
|
|
|
36
|
|
|
public function __construct(Broker $broker) |
37
|
|
|
{ |
38
|
|
|
$this->broker = $broker; |
39
|
|
|
} |
40
|
|
|
|
41
|
|
|
abstract public function getNodeType(): string; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @param ReflectionMethod|ReflectionFunction $reflection |
45
|
|
|
* @return string |
46
|
|
|
*/ |
47
|
|
|
abstract public function getContext($reflection): string; |
48
|
|
|
|
49
|
|
|
abstract public function isReturnIgnored(Node $node): bool; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @param \PhpParser\Node\Stmt\Function_|\PhpParser\Node\Stmt\ClassMethod $node |
53
|
|
|
* @param \PHPStan\Analyser\Scope $scope |
54
|
|
|
* @return string[] |
55
|
|
|
*/ |
56
|
|
|
public function processNode(Node $node, Scope $scope): array |
57
|
|
|
{ |
58
|
|
|
// TODO: improve performance by caching better reflection results. |
59
|
|
|
$finder = FindReflectionOnLine::buildDefaultFinder(); |
60
|
|
|
|
61
|
|
|
if ($node->getLine() < 0) { |
62
|
|
|
// Fixes some problems with methods in anonymous class (the line number is poorly reported). |
63
|
|
|
return []; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
$reflection = $finder($scope->getFile(), $node->getLine()); |
67
|
|
|
|
68
|
|
|
// If the method implements/extends another method, we have no choice on the signature so let's bypass this check. |
69
|
|
|
if ($reflection instanceof ReflectionMethod && $this->isInherited($reflection)) { |
|
|
|
|
70
|
|
|
return []; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
$errors = []; |
74
|
|
|
|
75
|
|
|
if ($reflection === null) { |
76
|
|
|
throw new \RuntimeException('Could not find item at '.$scope->getFile().':'.$node->getLine()); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
foreach ($reflection->getParameters() as $parameter) { |
80
|
|
|
$result = $this->analyzeParameter($parameter); |
81
|
|
|
|
82
|
|
|
if ($result !== null) { |
83
|
|
|
$errors[] = $result; |
84
|
|
|
} |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
if (!$this->isReturnIgnored($node)) { |
88
|
|
|
$returnTypeError = $this->analyzeReturnType($reflection); |
89
|
|
|
if ($returnTypeError !== null) { |
90
|
|
|
$errors[] = $returnTypeError; |
91
|
|
|
} |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
return $errors; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Analyzes a parameter and returns the error string if xomething goes wrong or null if everything is ok. |
99
|
|
|
* |
100
|
|
|
* @param ReflectionParameter $parameter |
101
|
|
|
* @return null|string |
102
|
|
|
*/ |
103
|
|
|
private function analyzeParameter(ReflectionParameter $parameter): ?string |
104
|
|
|
{ |
105
|
|
|
$phpTypeHint = $parameter->getTypeHint(); |
106
|
|
|
$docBlockTypeHints = $parameter->getDocBlockTypes(); |
107
|
|
|
|
108
|
|
|
// If there is a type-hint, we have nothing to say unless it is an array. |
109
|
|
|
if ($phpTypeHint !== null) { |
110
|
|
|
return $this->analyzeWithTypehint($parameter, $phpTypeHint, $docBlockTypeHints); |
111
|
|
|
} else { |
112
|
|
|
return $this->analyzeWithoutTypehint($parameter, $docBlockTypeHints); |
113
|
|
|
} |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* @param ReflectionFunction|ReflectionMethod $function |
118
|
|
|
* @return null|string |
119
|
|
|
*/ |
120
|
|
|
private function analyzeReturnType($function): ?string |
121
|
|
|
{ |
122
|
|
|
$reflectionPhpTypeHint = $function->getReturnType(); |
123
|
|
|
$phpTypeHint = null; |
124
|
|
|
if ($reflectionPhpTypeHint !== null) { |
125
|
|
|
$phpTypeHint = $reflectionPhpTypeHint->getTypeObject(); |
126
|
|
|
} |
127
|
|
|
$docBlockTypeHints = $function->getDocBlockReturnTypes(); |
128
|
|
|
|
129
|
|
|
// If there is a type-hint, we have nothing to say unless it is an array. |
130
|
|
|
if ($phpTypeHint !== null) { |
131
|
|
|
return $this->analyzeWithTypehint($function, $phpTypeHint, $docBlockTypeHints); |
132
|
|
|
} else { |
133
|
|
|
return $this->analyzeWithoutTypehint($function, $docBlockTypeHints); |
134
|
|
|
} |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* @param ReflectionParameter|ReflectionMethod|ReflectionFunction $context |
139
|
|
|
* @param Type $phpTypeHint |
140
|
|
|
* @param Type[] $docBlockTypeHints |
141
|
|
|
* @return null|string |
142
|
|
|
*/ |
143
|
|
|
private function analyzeWithTypehint($context, Type $phpTypeHint, array $docBlockTypeHints): ?string |
144
|
|
|
{ |
145
|
|
|
$docblockWithoutNullable = $this->typesWithoutNullable($docBlockTypeHints); |
146
|
|
|
|
147
|
|
|
if (!$phpTypeHint instanceof Array_) { |
|
|
|
|
148
|
|
|
// Let's detect mismatches between docblock and PHP typehint |
149
|
|
|
foreach ($docblockWithoutNullable as $docblockTypehint) { |
150
|
|
|
if (get_class($docblockTypehint) !== get_class($phpTypeHint)) { |
151
|
|
|
if ($context instanceof ReflectionParameter) { |
|
|
|
|
152
|
|
|
return sprintf('%s, parameter $%s type is type-hinted to "%s" but the @param annotation says it is a "%s". Please fix the @param annotation.', $this->getContext($context), $context->getName(), (string) $phpTypeHint, (string) $docblockTypehint); |
153
|
|
|
} else { |
154
|
|
|
return sprintf('%s, return type is type-hinted to "%s" but the @return annotation says it is a "%s". Please fix the @return annotation.', $this->getContext($context), (string) $phpTypeHint, (string) $docblockTypehint); |
155
|
|
|
} |
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
return null; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
if (empty($docblockWithoutNullable)) { |
163
|
|
|
if ($context instanceof ReflectionParameter) { |
|
|
|
|
164
|
|
|
return sprintf('%s, parameter $%s type is "array". Please provide a @param annotation to further specify the type of the array. For instance: @param int[] $%s', $this->getContext($context), $context->getName(), $context->getName()); |
165
|
|
|
} else { |
166
|
|
|
return sprintf('%s, return type is "array". Please provide a @param annotation to further specify the type of the array. For instance: @return int[]', $this->getContext($context)); |
167
|
|
|
} |
168
|
|
|
} else { |
169
|
|
|
foreach ($docblockWithoutNullable as $docblockTypehint) { |
170
|
|
|
if (!$docblockTypehint instanceof Array_) { |
|
|
|
|
171
|
|
|
if ($context instanceof ReflectionParameter) { |
|
|
|
|
172
|
|
|
return sprintf('%s, mismatching type-hints for parameter %s. PHP type hint is "array" and docblock type hint is %s.', $this->getContext($context), $context->getName(), (string)$docblockTypehint); |
173
|
|
|
} else { |
174
|
|
|
return sprintf('%s, mismatching type-hints for return type. PHP type hint is "array" and docblock declared return type is %s.', $this->getContext($context), (string)$docblockTypehint); |
175
|
|
|
} |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
if ($docblockTypehint->getValueType() instanceof Mixed) { |
|
|
|
|
179
|
|
|
if ($context instanceof ReflectionParameter) { |
|
|
|
|
180
|
|
|
return sprintf('%s, parameter $%s type is "array". Please provide a more specific @param annotation. For instance: @param int[] $%s', $this->getContext($context), $context->getName(), $context->getName()); |
181
|
|
|
} else { |
182
|
|
|
return sprintf('%s, return type is "array". Please provide a more specific @return annotation. For instance: @return int[]', $this->getContext($context)); |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
return null; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* @param ReflectionParameter|ReflectionMethod|ReflectionFunction $context |
193
|
|
|
* @param Type[] $docBlockTypeHints |
194
|
|
|
* @return null|string |
195
|
|
|
*/ |
196
|
|
|
private function analyzeWithoutTypehint($context, array $docBlockTypeHints): ?string |
197
|
|
|
{ |
198
|
|
|
if (empty($docBlockTypeHints)) { |
199
|
|
|
if ($context instanceof ReflectionParameter) { |
|
|
|
|
200
|
|
|
return sprintf('%s, parameter $%s has no type-hint and no @param annotation.', $this->getContext($context), $context->getName()); |
201
|
|
|
} else { |
202
|
|
|
return sprintf('%s, there is no return type and no @return annotation.', $this->getContext($context)); |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
$nativeTypehint = $this->isNativelyTypehintable($docBlockTypeHints); |
207
|
|
|
|
208
|
|
|
if ($nativeTypehint !== null) { |
209
|
|
|
if ($context instanceof ReflectionParameter) { |
|
|
|
|
210
|
|
|
return sprintf('%s, parameter $%s can be type-hinted to "%s".', $this->getContext($context), $context->getName(), $nativeTypehint); |
211
|
|
|
} else { |
212
|
|
|
return sprintf('%s, a "%s" return type can be added.', $this->getContext($context), $nativeTypehint); |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
return null; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* @param Type[] $docBlockTypeHints |
221
|
|
|
* @return string|null |
222
|
|
|
*/ |
223
|
|
|
private function isNativelyTypehintable(array $docBlockTypeHints): ?string |
224
|
|
|
{ |
225
|
|
|
if (count($docBlockTypeHints) > 2) { |
226
|
|
|
return null; |
227
|
|
|
} |
228
|
|
|
$isNullable = $this->isNullable($docBlockTypeHints); |
229
|
|
|
if (count($docBlockTypeHints) === 2 && !$isNullable) { |
230
|
|
|
return null; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
$types = $this->typesWithoutNullable($docBlockTypeHints); |
234
|
|
|
// At this point, there is at most one element here |
235
|
|
|
if (empty($types)) { |
236
|
|
|
return null; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
$type = $types[0]; |
240
|
|
|
|
241
|
|
|
if ($this->isNativeType($type)) { |
242
|
|
|
return ($isNullable?'?':'').((string)$type); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
if ($type instanceof Array_) { |
|
|
|
|
246
|
|
|
return ($isNullable?'?':'').'array'; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
// TODO: more definitions to add here |
250
|
|
|
// Manage interface/classes |
251
|
|
|
// Manage array of things => (cast to array) |
252
|
|
|
|
253
|
|
|
if ($type instanceof Object_) { |
|
|
|
|
254
|
|
|
return ($isNullable?'?':'').((string)$type); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
return null; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
private function isNativeType(Type $type): bool |
261
|
|
|
{ |
262
|
|
|
if ($type instanceof String_ |
|
|
|
|
263
|
|
|
|| $type instanceof Integer |
|
|
|
|
264
|
|
|
|| $type instanceof Boolean |
|
|
|
|
265
|
|
|
|| $type instanceof Float_ |
|
|
|
|
266
|
|
|
|| $type instanceof Scalar |
|
|
|
|
267
|
|
|
|| $type instanceof Callable_ |
|
|
|
|
268
|
|
|
|| ((string) $type) === 'iterable' |
269
|
|
|
) { |
270
|
|
|
return true; |
271
|
|
|
} |
272
|
|
|
return false; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* @param Type[] $docBlockTypeHints |
277
|
|
|
* @return bool |
278
|
|
|
*/ |
279
|
|
|
private function isNullable(array $docBlockTypeHints): bool |
280
|
|
|
{ |
281
|
|
|
foreach ($docBlockTypeHints as $docBlockTypeHint) { |
282
|
|
|
if ($docBlockTypeHint instanceof Null_) { |
|
|
|
|
283
|
|
|
return true; |
284
|
|
|
} |
285
|
|
|
} |
286
|
|
|
return false; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* Removes "null" from the list of types. |
291
|
|
|
* |
292
|
|
|
* @param Type[] $docBlockTypeHints |
293
|
|
|
* @return Type[] |
294
|
|
|
*/ |
295
|
|
|
private function typesWithoutNullable(array $docBlockTypeHints): array |
296
|
|
|
{ |
297
|
|
|
return array_filter($docBlockTypeHints, function($item) { |
298
|
|
|
return !$item instanceof Null_; |
|
|
|
|
299
|
|
|
}); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
private function isInherited(ReflectionMethod $method, ReflectionClass $class = null): bool |
303
|
|
|
{ |
304
|
|
|
if ($class === null) { |
305
|
|
|
$class = $method->getDeclaringClass(); |
306
|
|
|
} |
307
|
|
|
$interfaces = $class->getInterfaces(); |
308
|
|
|
foreach ($interfaces as $interface) { |
309
|
|
|
if ($interface->hasMethod($method->getName())) { |
310
|
|
|
return true; |
311
|
|
|
} |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
$parentClass = $class->getParentClass(); |
315
|
|
|
if ($parentClass !== null) { |
316
|
|
|
return $this->isInherited($method, $parentClass); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
return false; |
320
|
|
|
} |
321
|
|
|
} |
322
|
|
|
|
This error could be the result of:
1. Missing dependencies
PHP Analyzer uses your
composer.json
file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects thecomposer.json
to be in the root folder of your repository.Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the
require
orrequire-dev
section?2. Missing use statement
PHP does not complain about undefined classes in
ìnstanceof
checks. For example, the following PHP code will work perfectly fine:If you have not tested against this specific condition, such errors might go unnoticed.