1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Gskema\TypeSniff\Sniffs\CodeElement; |
4
|
|
|
|
5
|
|
|
use Gskema\TypeSniff\Core\Type\TypeComparator; |
6
|
|
|
use PHP_CodeSniffer\Files\File; |
7
|
|
|
use Gskema\TypeSniff\Core\CodeElement\Element\AbstractFqcnMethodElement; |
8
|
|
|
use Gskema\TypeSniff\Core\CodeElement\Element\ClassMethodElement; |
9
|
|
|
use Gskema\TypeSniff\Core\CodeElement\Element\CodeElementInterface; |
10
|
|
|
use Gskema\TypeSniff\Core\CodeElement\Element\InterfaceMethodElement; |
11
|
|
|
use Gskema\TypeSniff\Core\DocBlock\DocBlock; |
12
|
|
|
use Gskema\TypeSniff\Core\DocBlock\UndefinedDocBlock; |
13
|
|
|
use Gskema\TypeSniff\Core\Type\Common\ArrayType; |
14
|
|
|
use Gskema\TypeSniff\Core\Type\Common\UndefinedType; |
15
|
|
|
use Gskema\TypeSniff\Core\Type\Common\VoidType; |
16
|
|
|
use Gskema\TypeSniff\Core\Type\Declaration\NullableType; |
17
|
|
|
use Gskema\TypeSniff\Core\Type\DocBlock\CompoundType; |
18
|
|
|
use Gskema\TypeSniff\Core\Type\DocBlock\NullType; |
19
|
|
|
use Gskema\TypeSniff\Core\Type\DocBlock\TypedArrayType; |
20
|
|
|
use Gskema\TypeSniff\Core\Type\TypeConverter; |
21
|
|
|
use Gskema\TypeSniff\Core\Type\TypeInterface; |
22
|
|
|
|
23
|
|
|
class FqcnMethodSniff implements CodeElementSniffInterface |
24
|
|
|
{ |
25
|
|
|
/** @var string[] */ |
26
|
|
|
protected $baseUsefulTags = [ |
27
|
|
|
'@deprecated', |
28
|
|
|
'@throws', |
29
|
|
|
'@dataProvider', |
30
|
|
|
'@see', |
31
|
|
|
'@todo', |
32
|
|
|
'@inheritDoc' |
33
|
|
|
]; |
34
|
|
|
|
35
|
|
|
/** @var string[] */ |
36
|
|
|
protected $usefulTags = []; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @inheritDoc |
40
|
|
|
*/ |
41
|
4 |
|
public function configure(array $config): void |
42
|
|
|
{ |
43
|
4 |
|
$rawTags = array_merge($this->baseUsefulTags, $config['usefulTags'] ?? []); |
44
|
|
|
|
45
|
4 |
|
$usefulTags = []; |
46
|
4 |
|
foreach ($rawTags as $rawTag) { |
47
|
4 |
|
$usefulTags[] = strtolower(ltrim($rawTag, '@')); |
48
|
|
|
} |
49
|
4 |
|
$usefulTags = array_unique($usefulTags); |
50
|
|
|
|
51
|
4 |
|
$this->usefulTags = $usefulTags; |
52
|
4 |
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @inheritDoc |
56
|
|
|
*/ |
57
|
4 |
|
public function register(): array |
58
|
|
|
{ |
59
|
|
|
return [ |
60
|
4 |
|
ClassMethodElement::class, |
61
|
|
|
// TraitMethodElement::class, // can be used to implement interface, not possible to know if it is extended |
62
|
|
|
InterfaceMethodElement::class, |
63
|
|
|
]; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @inheritDoc |
68
|
|
|
* @param AbstractFqcnMethodElement $method |
69
|
|
|
*/ |
70
|
3 |
|
public function process(File $file, CodeElementInterface $method): void |
71
|
|
|
{ |
72
|
3 |
|
$warningCountBefore = $file->getWarningCount(); |
73
|
|
|
|
74
|
|
|
// @TODO Assert description |
75
|
3 |
|
$this->processMethod($file, $method); |
76
|
|
|
|
77
|
3 |
|
$hasNewWarnings = $file->getWarningCount() > $warningCountBefore; |
78
|
3 |
|
if (!$hasNewWarnings && $this->hasUselessDocBlock($method)) { |
79
|
1 |
|
$file->addWarningOnLine('Useless PHPDoc', $method->getLine(), 'FqcnMethodSniff'); |
80
|
|
|
} |
81
|
3 |
|
} |
82
|
|
|
|
83
|
3 |
|
protected function processMethod(File $file, AbstractFqcnMethodElement $method): void |
84
|
|
|
{ |
85
|
3 |
|
$fnSig = $method->getSignature(); |
86
|
3 |
|
$docBlock = $method->getDocBlock(); |
87
|
3 |
|
$isMagicMethod = '__' === substr($fnSig->getName(), 0, 2); |
88
|
3 |
|
$isConstructMethod = '__construct' === $fnSig->getName(); |
89
|
3 |
|
$hasInheritDocTag = $docBlock->hasTag('inheritdoc'); |
90
|
|
|
|
91
|
|
|
// @inheritDoc |
92
|
|
|
// __construct can be detected as extended and magic, but we want to inspect it anyway |
93
|
3 |
|
if (!$isConstructMethod) { |
94
|
3 |
|
if ($hasInheritDocTag || $isMagicMethod) { |
95
|
1 |
|
return; |
96
|
3 |
|
} elseif ($method->isExtended()) { |
97
|
1 |
|
$file->addWarningOnLine('Missing @inheritDoc tag', $method->getLine(), 'FqcnMethodSniff'); |
98
|
1 |
|
return; |
99
|
|
|
} |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
// @param |
103
|
3 |
|
foreach ($fnSig->getParams() as $fnParam) { |
104
|
3 |
|
$paramName = $fnParam->getName(); |
105
|
3 |
|
$tag = $docBlock->getParamTag($paramName); |
106
|
|
|
|
107
|
3 |
|
$subject = sprintf('parameter $%s', $paramName); |
108
|
|
|
|
109
|
3 |
|
$fnType = $fnParam->getType(); |
110
|
3 |
|
$fnTypeLine = $fnParam->getLine(); |
111
|
3 |
|
$docType = $tag ? $tag->getType() : null; |
112
|
3 |
|
$docTypeLine = $tag ? $tag->getLine() : $fnTypeLine; |
113
|
|
|
|
114
|
3 |
|
$this->processSigType($file, $docBlock, $subject, $fnType, $fnTypeLine, $docType, $docTypeLine); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
// @return |
118
|
3 |
|
if (!$isConstructMethod) { |
119
|
3 |
|
$docType = $docBlock->getReturnTag(); |
120
|
3 |
|
$this->processSigType( |
121
|
3 |
|
$file, |
122
|
3 |
|
$docBlock, |
123
|
3 |
|
'return value', |
124
|
3 |
|
$fnSig->getReturnType(), |
125
|
3 |
|
$fnSig->getReturnLine(), |
126
|
3 |
|
$docType ? $docType->getType() : null, |
127
|
3 |
|
$docType ? $docType->getLine() : $fnSig->getLine() |
128
|
|
|
); |
129
|
|
|
} |
130
|
3 |
|
} |
131
|
|
|
|
132
|
3 |
|
protected function processSigType( |
133
|
|
|
File $file, |
134
|
|
|
DocBlock $docBlock, |
135
|
|
|
string $subject, |
136
|
|
|
TypeInterface $fnType, |
137
|
|
|
int $fnTypeLine, |
138
|
|
|
?TypeInterface $docType, |
139
|
|
|
int $docTypeLine |
140
|
|
|
): void { |
141
|
|
|
// @TODO Required mixed[][] instead of array[] |
142
|
|
|
|
143
|
3 |
|
$isReturnType = 'return value' === $subject; |
144
|
|
|
// $isParamType = !$isReturnType; |
145
|
|
|
|
146
|
|
|
/** @var string[][] $warnings */ |
147
|
3 |
|
$warnings = []; |
148
|
3 |
|
if ($docBlock instanceof UndefinedDocBlock) { |
149
|
|
|
// Require docType for undefined type or array type |
150
|
2 |
|
if ($fnType instanceof UndefinedType) { |
151
|
2 |
|
$warnings[$fnTypeLine][] = 'Add type declaration for :subject: or create PHPDoc with type hint'; |
152
|
1 |
|
} elseif ($this->containsType($fnType, ArrayType::class)) { |
153
|
2 |
|
$warnings[$fnTypeLine][] = 'Create PHPDoc with typed array type hint for :subject:, .e.g.: "string[]" or "SomeClass[]"'; |
154
|
|
|
} |
155
|
3 |
|
} elseif (null === $docType) { |
156
|
|
|
// Require docTag unless void return type |
157
|
2 |
|
if ($isReturnType) { |
158
|
2 |
|
if (!($fnType instanceof VoidType)) { |
159
|
2 |
|
$warnings[$fnTypeLine][] = 'Missing PHPDoc tag or void type declaration for :subject:'; |
160
|
|
|
} |
161
|
|
|
} else { |
162
|
2 |
|
$warnings[$fnTypeLine][] = 'Missing PHPDoc tag for :subject:'; |
163
|
|
|
} |
164
|
|
|
} else { |
165
|
3 |
|
$docTypeDefined = !($docType instanceof UndefinedType); |
166
|
3 |
|
$fnTypeDefined = !($fnType instanceof UndefinedType); |
167
|
|
|
|
168
|
3 |
|
if ($docTypeDefined) { |
169
|
|
|
// Require typed array type |
170
|
|
|
// Require composite with null instead of null |
171
|
|
|
// @TODO true/void/false/$this/ cannot be param tags |
172
|
|
|
|
173
|
3 |
|
$docHasTypedArray = $this->containsType($docType, TypedArrayType::class); |
174
|
3 |
|
$docHasArray = $this->containsType($docType, ArrayType::class); |
175
|
|
|
|
176
|
3 |
|
if ($docHasTypedArray && $docHasArray) { |
177
|
1 |
|
$warnings[$docTypeLine][] = 'Remove array type, typed array type is present in PHPDoc for :subject:.'; |
178
|
3 |
|
} elseif (!$docHasTypedArray && $docHasArray) { |
179
|
2 |
|
$warnings[$docTypeLine][] = 'Replace array type with typed array type in PHPDoc for :subject:. Use mixed[] for generic arrays.'; |
180
|
|
|
} |
181
|
|
|
|
182
|
3 |
|
if ($docType instanceof NullType) { |
183
|
1 |
|
if ($isReturnType) { |
184
|
|
|
$warnings[$docTypeLine][] = 'Use void :subject :type declaration or change type to compound, e.g. SomeClass|null'; |
185
|
|
|
} else { |
186
|
3 |
|
$warnings[$docTypeLine][] = 'Change type hint for :subject: to compound, e.g. SomeClass|null'; |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
} else { |
190
|
|
|
// Require docType (example from fnType) |
191
|
1 |
|
$exampleDocType = TypeConverter::toExampleDocType($fnType); |
192
|
1 |
|
if (null !== $exampleDocType) { |
193
|
1 |
|
$warnings[$docTypeLine][] = sprintf('Add type hint in PHPDoc tag for :subject:, e.g. "%s"', $exampleDocType->toString()); |
194
|
|
|
} else { |
195
|
1 |
|
$warnings[$docTypeLine][] = 'Add type hint in PHPDoc tag for :subject:'; |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
3 |
|
if (!$fnTypeDefined) { |
200
|
|
|
// Require fnType if possible (check, suggest from docType) |
201
|
1 |
|
if ($suggestedFnType = TypeConverter::toFunctionType($docType)) { |
202
|
1 |
|
$warnings[$fnTypeLine][] = sprintf('Add type declaration for :subject:, e.g.: "%s"', $suggestedFnType->toString()); |
203
|
|
|
} |
204
|
|
|
// @TODO Warning for null? |
205
|
|
|
} |
206
|
|
|
|
207
|
3 |
|
if ($docTypeDefined && $fnTypeDefined) { |
208
|
|
|
// Require to add missing types to docType, |
209
|
|
|
// @TODO Remove void return docType, |
210
|
|
|
|
211
|
|
|
/** @var TypeInterface[] $wrongDocTypes */ |
212
|
|
|
/** @var TypeInterface[] $missingDocTypes */ |
213
|
3 |
|
[$wrongDocTypes, $missingDocTypes] = TypeComparator::compare($docType, $fnType); |
214
|
|
|
|
215
|
3 |
|
if ($wrongDocTypes) { |
216
|
3 |
|
$warnings[$docTypeLine][] = sprintf( |
217
|
3 |
|
'Type %s "%s" %s not compatible with :subject: type declaration', |
218
|
3 |
|
isset($wrongDocTypes[1]) ? 'hints' : 'hint', |
219
|
3 |
|
$this->implodeTypes($wrongDocTypes), |
220
|
3 |
|
isset($wrongDocTypes[1]) ? 'are' : 'is' |
221
|
|
|
); |
222
|
|
|
} |
223
|
|
|
|
224
|
3 |
|
if ($missingDocTypes) { |
|
|
|
|
225
|
3 |
|
$warnings[$docTypeLine][] = sprintf( |
226
|
3 |
|
'Missing "%s" %s in :subject: type hint', |
227
|
3 |
|
$this->implodeTypes($missingDocTypes), |
228
|
3 |
|
isset($missingDocTypes[1]) ? 'types' : 'type' |
229
|
|
|
); |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
} |
233
|
|
|
|
234
|
3 |
|
foreach ($warnings as $line => $lineWarnings) { |
235
|
3 |
|
foreach ($lineWarnings as $warningTpl) { |
236
|
3 |
|
$warning = str_replace(':subject:', $subject, $warningTpl); |
237
|
3 |
|
$file->addWarningOnLine($warning, $line, 'FqcnMethodSniff'); |
238
|
|
|
} |
239
|
|
|
} |
240
|
3 |
|
} |
241
|
|
|
|
242
|
3 |
|
protected function containsType(TypeInterface $type, string $typeClassName): bool |
243
|
|
|
{ |
244
|
3 |
|
return is_a($type, $typeClassName) |
245
|
3 |
|
|| ($type instanceof CompoundType && $type->containsType($typeClassName)) |
246
|
3 |
|
|| ($type instanceof NullableType && $type->containsType($typeClassName)); |
247
|
|
|
} |
248
|
|
|
|
249
|
3 |
|
protected function hasUselessDocBlock(AbstractFqcnMethodElement $method): bool |
250
|
|
|
{ |
251
|
3 |
|
$fnSig = $method->getSignature(); |
252
|
3 |
|
$docBlock = $method->getDocBlock(); |
253
|
|
|
|
254
|
3 |
|
$usefulTagNames = array_diff($this->usefulTags, ['param', 'return']); |
255
|
|
|
|
256
|
3 |
|
$docReturnTag = $docBlock->getReturnTag(); |
257
|
|
|
|
258
|
3 |
|
$hasUsefulDescription = $docBlock->hasDescription() |
259
|
3 |
|
&& !preg_match('#^\w+\s+constructor\.?$#', $docBlock->getDescription()); |
260
|
|
|
|
261
|
3 |
|
if ($docBlock instanceof UndefinedDocBlock |
262
|
3 |
|
|| $hasUsefulDescription |
263
|
3 |
|
|| $docBlock->hasOneOfTags($usefulTagNames) |
264
|
3 |
|
|| ($docReturnTag && $docReturnTag->hasDescription()) |
265
|
|
|
) { |
266
|
2 |
|
return false; |
267
|
|
|
} |
268
|
|
|
|
269
|
3 |
|
foreach ($fnSig->getParams() as $fnParam) { |
270
|
2 |
|
$paramTag = $docBlock->getParamTag($fnParam->getName()); |
271
|
2 |
|
if (null === $paramTag) { |
272
|
|
|
continue; |
273
|
|
|
} |
274
|
|
|
|
275
|
2 |
|
if ($paramTag->hasDescription()) { |
276
|
1 |
|
return false; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
// @TODO Cleaner way? |
280
|
1 |
|
$fnType = $fnParam->getType(); |
281
|
1 |
|
$rawFnType = $fnType instanceof NullableType |
282
|
1 |
|
? $fnType->toDocString() |
283
|
1 |
|
: $fnType->toString(); |
284
|
1 |
|
if ($paramTag->getType()->toString() !== $rawFnType) { |
285
|
1 |
|
return false; |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
|
289
|
2 |
|
$returnTag = $docBlock->getReturnTag(); |
290
|
2 |
|
$returnType = $fnSig->getReturnType(); |
291
|
|
|
|
292
|
|
|
// @TODO ?? |
293
|
2 |
|
if ($returnTag && $returnType) { |
294
|
1 |
|
$rawReturnType = $returnType instanceof NullableType |
295
|
|
|
? $returnType->toDocString() |
296
|
1 |
|
: $returnType->toString(); |
297
|
1 |
|
if ($returnTag->getType()->toString() !== $rawReturnType) { |
298
|
1 |
|
return false; |
299
|
|
|
} |
300
|
|
|
} |
301
|
|
|
|
302
|
1 |
|
return true; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* @param TypeInterface[] $types |
307
|
|
|
* @return string|null |
308
|
|
|
*/ |
309
|
3 |
|
protected function implodeTypes(array $types): ?string |
310
|
|
|
{ |
311
|
3 |
|
$rawTypes = []; |
312
|
3 |
|
foreach ($types as $type) { |
313
|
3 |
|
$rawTypes[] = $type->toString(); |
314
|
|
|
} |
315
|
|
|
|
316
|
3 |
|
return $rawTypes ? implode(', ', $rawTypes) : null; |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.