1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
namespace TheCodingMachine\PHPStan\Rules\TypeHints; |
5
|
|
|
|
6
|
|
|
use PHPStan\Reflection\Php\PhpFunctionReflection; |
7
|
|
|
use PHPStan\Reflection\Php\PhpMethodReflection; |
8
|
|
|
use PHPStan\Type\ArrayType; |
9
|
|
|
use PHPStan\Type\BooleanType; |
10
|
|
|
use PHPStan\Type\CallableType; |
11
|
|
|
use PHPStan\Type\FloatType; |
12
|
|
|
use PHPStan\Type\IntegerType; |
13
|
|
|
use PHPStan\Type\IterableType; |
14
|
|
|
use PHPStan\Type\MixedType; |
15
|
|
|
use PHPStan\Type\NullType; |
16
|
|
|
use PHPStan\Type\ObjectType; |
17
|
|
|
use PHPStan\Type\ObjectWithoutClassType; |
18
|
|
|
use PHPStan\Type\StringType; |
19
|
|
|
use PHPStan\Type\Type; |
20
|
|
|
use PhpParser\Node; |
21
|
|
|
use PHPStan\Analyser\Scope; |
22
|
|
|
use PHPStan\Broker\Broker; |
23
|
|
|
use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; |
24
|
|
|
use PHPStan\Reflection\Php\PhpParameterReflection; |
25
|
|
|
use PHPStan\Rules\Rule; |
26
|
|
|
use PHPStan\Type\UnionType; |
27
|
|
|
use PHPStan\Type\VerbosityLevel; |
28
|
|
|
|
29
|
|
|
abstract class AbstractMissingTypeHintRule implements Rule |
30
|
|
|
{ |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @var Broker |
34
|
|
|
*/ |
35
|
|
|
private $broker; |
36
|
|
|
|
37
|
|
|
public function __construct(Broker $broker) |
38
|
|
|
{ |
39
|
|
|
$this->broker = $broker; |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
abstract public function getNodeType(): string; |
43
|
|
|
|
44
|
|
|
abstract public function isReturnIgnored(Node $node): bool; |
45
|
|
|
|
46
|
|
|
abstract protected function getReflection(Node\FunctionLike $function, Scope $scope, Broker $broker) : ParametersAcceptorWithPhpDocs; |
47
|
|
|
|
48
|
|
|
abstract protected function shouldSkip(Node\FunctionLike $function, Scope $scope): bool; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @param \PhpParser\Node\Stmt\Function_|\PhpParser\Node\Stmt\ClassMethod $node |
52
|
|
|
* @param \PHPStan\Analyser\Scope $scope |
53
|
|
|
* @return string[] |
54
|
|
|
*/ |
55
|
|
|
public function processNode(Node $node, Scope $scope): array |
56
|
|
|
{ |
57
|
|
|
/*if ($node->getLine() < 0) { |
58
|
|
|
// Fixes some problems with methods in anonymous class (the line number is poorly reported). |
59
|
|
|
return []; |
60
|
|
|
}*/ |
61
|
|
|
|
62
|
|
|
if ($this->shouldSkip($node, $scope)) { |
63
|
|
|
return []; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
$parametersAcceptor = $this->getReflection($node, $scope, $this->broker); |
67
|
|
|
|
68
|
|
|
$errors = []; |
69
|
|
|
|
70
|
|
|
foreach ($parametersAcceptor->getParameters() as $parameter) { |
71
|
|
|
$debugContext = new ParameterDebugContext($scope, $node, $parameter); |
72
|
|
|
$result = $this->analyzeParameter($debugContext, $parameter); |
73
|
|
|
|
74
|
|
|
if ($result !== null) { |
75
|
|
|
$errors[] = $result; |
76
|
|
|
} |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
if (!$this->isReturnIgnored($node)) { |
80
|
|
|
$debugContext = new FunctionDebugContext($scope, $node); |
81
|
|
|
$returnTypeError = $this->analyzeReturnType($debugContext, $parametersAcceptor); |
82
|
|
|
if ($returnTypeError !== null) { |
83
|
|
|
$errors[] = $returnTypeError; |
84
|
|
|
} |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
return $errors; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Analyzes a parameter and returns the error string if something goes wrong or null if everything is ok. |
92
|
|
|
* |
93
|
|
|
* @param PhpParameterReflection $parameter |
94
|
|
|
* @return null|string |
95
|
|
|
*/ |
96
|
|
|
private function analyzeParameter(DebugContextInterface $context, PhpParameterReflection $parameter): ?string |
97
|
|
|
{ |
98
|
|
|
//$typeResolver = new \phpDocumentor\Reflection\TypeResolver(); |
|
|
|
|
99
|
|
|
|
100
|
|
|
$phpTypeHint = $parameter->getNativeType(); |
101
|
|
|
//try { |
102
|
|
|
$docBlockTypeHints = $parameter->getPhpDocType(); |
103
|
|
|
/*} catch (\InvalidArgumentException $e) { |
|
|
|
|
104
|
|
|
return sprintf('%s, for parameter $%s, invalid docblock @param encountered. %s', |
105
|
|
|
$this->getContext($parameter), |
106
|
|
|
$parameter->getName(), |
107
|
|
|
$e->getMessage() |
108
|
|
|
); |
109
|
|
|
}*/ |
110
|
|
|
|
111
|
|
|
if ($phpTypeHint instanceof MixedType && $phpTypeHint->isExplicitMixed() === false) { |
|
|
|
|
112
|
|
|
return $this->analyzeWithoutTypehint($context, $docBlockTypeHints); |
113
|
|
|
} else { |
114
|
|
|
// If there is a type-hint, we have nothing to say unless it is an array. |
115
|
|
|
if ($parameter->isVariadic()) { |
116
|
|
|
// Hack: wrap the native type in an array is variadic |
117
|
|
|
$phpTypeHint = new ArrayType(new IntegerType(), $phpTypeHint); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
return $this->analyzeWithTypehint($context, $phpTypeHint, $docBlockTypeHints); |
121
|
|
|
} |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* @return null|string |
126
|
|
|
*/ |
127
|
|
|
private function analyzeReturnType(DebugContextInterface $debugContext, ParametersAcceptorWithPhpDocs $function): ?string |
128
|
|
|
{ |
129
|
|
|
$phpTypeHint = $function->getNativeReturnType(); |
130
|
|
|
$docBlockTypeHints = $function->getPhpDocReturnType(); |
131
|
|
|
|
132
|
|
|
// If there is a type-hint, we have nothing to say unless it is an array. |
133
|
|
|
if ($phpTypeHint instanceof MixedType && $phpTypeHint->isExplicitMixed() === false) { |
|
|
|
|
134
|
|
|
return $this->analyzeWithoutTypehint($debugContext, $docBlockTypeHints); |
135
|
|
|
} else { |
136
|
|
|
return $this->analyzeWithTypehint($debugContext, $phpTypeHint, $docBlockTypeHints); |
137
|
|
|
} |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* @param DebugContextInterface $debugContext |
142
|
|
|
* @param Type $phpTypeHint |
143
|
|
|
* @param Type $docBlockTypeHints |
144
|
|
|
* @return null|string |
145
|
|
|
*/ |
146
|
|
|
private function analyzeWithTypehint(DebugContextInterface $debugContext, Type $phpTypeHint, Type $docBlockTypeHints): ?string |
147
|
|
|
{ |
148
|
|
|
$docblockWithoutNullable = $this->typesWithoutNullable($docBlockTypeHints); |
149
|
|
|
|
150
|
|
|
if (!$this->isTypeIterable($phpTypeHint)) { |
151
|
|
|
// FIXME: this should be handled with the "accepts" method of types (and actually, this is already triggered by PHPStan 0.10) |
152
|
|
|
|
153
|
|
|
if ($docBlockTypeHints instanceof MixedType && $docBlockTypeHints->isExplicitMixed() === false) { |
|
|
|
|
154
|
|
|
// No docblock. |
155
|
|
|
return null; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
// Let's detect mismatches between docblock and PHP typehint |
159
|
|
|
if ($docblockWithoutNullable instanceof UnionType) { |
|
|
|
|
160
|
|
|
$docblocks = $docblockWithoutNullable->getTypes(); |
161
|
|
|
} else { |
162
|
|
|
$docblocks = [$docblockWithoutNullable]; |
163
|
|
|
} |
164
|
|
|
$phpTypeHintWithoutNullable = $this->typesWithoutNullable($phpTypeHint); |
165
|
|
|
foreach ($docblocks as $docblockTypehint) { |
166
|
|
|
if (get_class($docblockTypehint) !== get_class($phpTypeHintWithoutNullable)) { |
167
|
|
|
if ($debugContext instanceof ParameterDebugContext) { |
168
|
|
|
return sprintf('%s type is type-hinted to "%s" but the @param annotation says it is a "%s". Please fix the @param annotation.', (string) $debugContext, $phpTypeHint->describe(VerbosityLevel::typeOnly()), $docblockTypehint->describe(VerbosityLevel::typeOnly())); |
169
|
|
|
} elseif (!$docblockTypehint instanceof MixedType || $docblockTypehint->isExplicitMixed()) { |
|
|
|
|
170
|
|
|
return sprintf('%s return type is type-hinted to "%s" but the @return annotation says it is a "%s". Please fix the @return annotation.', (string) $debugContext, $phpTypeHint->describe(VerbosityLevel::typeOnly()), $docblockTypehint->describe(VerbosityLevel::typeOnly())); |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
return null; |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
if ($phpTypeHint instanceof ArrayType) { |
|
|
|
|
179
|
|
|
if ($docblockWithoutNullable instanceof MixedType && !$docblockWithoutNullable->isExplicitMixed()) { |
|
|
|
|
180
|
|
|
if ($debugContext instanceof ParameterDebugContext) { |
181
|
|
|
return sprintf('%s type is "array". Please provide a @param annotation to further specify the type of the array. For instance: @param int[] $%s', (string) $debugContext, $debugContext->getName()); |
182
|
|
|
} else { |
183
|
|
|
return sprintf('%s return type is "array". Please provide a @param annotation to further specify the type of the array. For instance: @return int[]', (string) $debugContext); |
184
|
|
|
} |
185
|
|
|
} else { |
186
|
|
|
if ($docblockWithoutNullable instanceof UnionType) { |
|
|
|
|
187
|
|
|
$docblocks = $docblockWithoutNullable->getTypes(); |
188
|
|
|
} else { |
189
|
|
|
$docblocks = [$docblockWithoutNullable]; |
190
|
|
|
} |
191
|
|
|
foreach ($docblocks as $docblockTypehint) { |
192
|
|
|
if (!$this->isTypeIterable($docblockTypehint)) { |
193
|
|
|
if ($debugContext instanceof ParameterDebugContext) { |
194
|
|
|
return sprintf('%s mismatching type-hints for parameter %s. PHP type hint is "array" and docblock type hint is %s.', (string) $debugContext, $debugContext->getName(), $docblockTypehint->describe(VerbosityLevel::typeOnly())); |
195
|
|
|
} else { |
196
|
|
|
return sprintf('%s mismatching type-hints for return type. PHP type hint is "array" and docblock declared return type is %s.', (string) $debugContext, $docblockTypehint->describe(VerbosityLevel::typeOnly())); |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
if ($docblockTypehint instanceof ArrayType && $docblockTypehint->getKeyType() instanceof MixedType && $docblockTypehint->getItemType() instanceof MixedType && $docblockTypehint->getKeyType()->isExplicitMixed() && $docblockTypehint->getItemType()->isExplicitMixed()) { |
|
|
|
|
201
|
|
|
if ($debugContext instanceof ParameterDebugContext) { |
202
|
|
|
return sprintf('%s type is "array". Please provide a more specific @param annotation in the docblock. For instance: @param int[] $%s. Use @param mixed[] $%s if this is really an array of mixed values.', (string) $debugContext, $debugContext->getName(), $debugContext->getName()); |
203
|
|
|
} else { |
204
|
|
|
return sprintf('%s return type is "array". Please provide a more specific @return annotation. For instance: @return int[]. Use @return mixed[] if this is really an array of mixed values.', (string) $debugContext); |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
return null; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
private function isTypeIterable(Type $phpTypeHint) : bool |
215
|
|
|
{ |
216
|
|
|
return /*$phpTypeHint->isIterable()->maybe() ||*/ $phpTypeHint->isIterable()->yes(); |
|
|
|
|
217
|
|
|
/*if ($phpTypeHint instanceof Array_ || $phpTypeHint instanceof Iterable_) { |
218
|
|
|
return true; |
219
|
|
|
} |
220
|
|
|
if ($phpTypeHint instanceof Object_) { |
221
|
|
|
// TODO: cache BetterReflection for better performance! |
222
|
|
|
try { |
223
|
|
|
$class = (new BetterReflection())->classReflector()->reflect((string) $phpTypeHint); |
224
|
|
|
} catch (IdentifierNotFound $e) { |
225
|
|
|
// Class not found? Let's not throw an error. It will be caught by other rules anyway. |
226
|
|
|
return false; |
227
|
|
|
} |
228
|
|
|
if ($class->implementsInterface('Traversable')) { |
229
|
|
|
return true; |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
return false;*/ |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* @param DebugContextInterface $debugContext |
238
|
|
|
* @param Type $docBlockTypeHints |
239
|
|
|
* @return null|string |
240
|
|
|
*/ |
241
|
|
|
private function analyzeWithoutTypehint(DebugContextInterface $debugContext, Type $docBlockTypeHints): ?string |
242
|
|
|
{ |
243
|
|
|
if ($docBlockTypeHints instanceof MixedType && $docBlockTypeHints->isExplicitMixed() === false) { |
|
|
|
|
244
|
|
|
if ($debugContext instanceof ParameterDebugContext) { |
245
|
|
|
return sprintf('%s has no type-hint and no @param annotation.', (string) $debugContext); |
246
|
|
|
} else { |
247
|
|
|
return sprintf('%s there is no return type and no @return annotation.', (string) $debugContext); |
248
|
|
|
} |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
$nativeTypehint = $this->isNativelyTypehintable($docBlockTypeHints); |
252
|
|
|
|
253
|
|
|
if ($nativeTypehint !== null) { |
254
|
|
|
if ($debugContext instanceof ParameterDebugContext) { |
255
|
|
|
return sprintf('%s can be type-hinted to "%s".', (string) $debugContext, $nativeTypehint); |
256
|
|
|
} else { |
257
|
|
|
return sprintf('%s a "%s" return type can be added.', (string) $debugContext, $nativeTypehint); |
258
|
|
|
} |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
return null; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* @param Type $docBlockTypeHints |
266
|
|
|
* @return string|null |
267
|
|
|
*/ |
268
|
|
|
private function isNativelyTypehintable(Type $docBlockTypeHints): ?string |
269
|
|
|
{ |
270
|
|
|
if ($docBlockTypeHints instanceof UnionType) { |
|
|
|
|
271
|
|
|
$count = count($docBlockTypeHints->getTypes()); |
272
|
|
|
} else { |
273
|
|
|
$count = 1; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
if ($count > 2) { |
277
|
|
|
return null; |
278
|
|
|
} |
279
|
|
|
$isNullable = $this->isNullable($docBlockTypeHints); |
280
|
|
|
if ($count === 2 && !$isNullable) { |
281
|
|
|
return null; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
$type = $this->typesWithoutNullable($docBlockTypeHints); |
285
|
|
|
// At this point, there is at most one element here |
286
|
|
|
/*if (empty($type)) { |
|
|
|
|
287
|
|
|
return null; |
288
|
|
|
}*/ |
289
|
|
|
|
290
|
|
|
//$type = $types[0]; |
|
|
|
|
291
|
|
|
|
292
|
|
|
// "object" type-hint is not available in PHP 7.1 |
293
|
|
|
if ($type instanceof ObjectWithoutClassType) { |
|
|
|
|
294
|
|
|
// In PHP 7.2, this is true but not in PHP 7.1 |
295
|
|
|
return null; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
if ($type instanceof ObjectType) { |
|
|
|
|
299
|
|
|
return ($isNullable?'?':'').'\\'.$type->describe(VerbosityLevel::typeOnly()); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
if ($type instanceof ArrayType) { |
|
|
|
|
303
|
|
|
return ($isNullable?'?':'').'array'; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
if ($this->isNativeType($type)) { |
307
|
|
|
return ($isNullable?'?':'').$type->describe(VerbosityLevel::typeOnly()); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
// TODO: more definitions to add here |
311
|
|
|
// Manage interface/classes |
312
|
|
|
// Manage array of things => (cast to array) |
313
|
|
|
|
314
|
|
|
|
315
|
|
|
return null; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
private function isNativeType(Type $type): bool |
319
|
|
|
{ |
320
|
|
|
if ($type instanceof StringType |
|
|
|
|
321
|
|
|
|| $type instanceof IntegerType |
|
|
|
|
322
|
|
|
|| $type instanceof BooleanType |
|
|
|
|
323
|
|
|
|| $type instanceof FloatType |
|
|
|
|
324
|
|
|
|| $type instanceof CallableType |
|
|
|
|
325
|
|
|
|| $type instanceof IterableType |
|
|
|
|
326
|
|
|
) { |
327
|
|
|
return true; |
328
|
|
|
} |
329
|
|
|
return false; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* @param Type $docBlockTypeHints |
334
|
|
|
* @return bool |
335
|
|
|
*/ |
336
|
|
|
private function isNullable(Type $docBlockTypeHints): bool |
337
|
|
|
{ |
338
|
|
|
if ($docBlockTypeHints instanceof UnionType) { |
|
|
|
|
339
|
|
|
foreach ($docBlockTypeHints->getTypes() as $docBlockTypeHint) { |
340
|
|
|
if ($docBlockTypeHint instanceof NullType) { |
|
|
|
|
341
|
|
|
return true; |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
} |
345
|
|
|
return false; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* Removes "null" from the list of types. |
350
|
|
|
* |
351
|
|
|
* @param Type $docBlockTypeHints |
352
|
|
|
* @return Type |
353
|
|
|
*/ |
354
|
|
|
private function typesWithoutNullable(Type $docBlockTypeHints): Type |
355
|
|
|
{ |
356
|
|
|
if ($docBlockTypeHints instanceof UnionType) { |
|
|
|
|
357
|
|
|
$filteredTypes = array_values(array_filter($docBlockTypeHints->getTypes(), function (Type $item) { |
358
|
|
|
return !$item instanceof NullType; |
|
|
|
|
359
|
|
|
})); |
360
|
|
|
if (\count($filteredTypes) === 1) { |
361
|
|
|
return $filteredTypes[0]; |
362
|
|
|
} |
363
|
|
|
return new UnionType($filteredTypes); |
364
|
|
|
} |
365
|
|
|
return $docBlockTypeHints; |
366
|
|
|
} |
367
|
|
|
} |
368
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.