1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace BestIt\Sniffs\Formatting; |
6
|
|
|
|
7
|
|
|
use BestIt\CodeSniffer\Helper\ClassHelper; |
8
|
|
|
use BestIt\CodeSniffer\Helper\TokenHelper; |
9
|
|
|
use BestIt\Sniffs\AbstractSniff; |
10
|
|
|
use BestIt\Sniffs\ClassRegistrationTrait; |
11
|
|
|
use function substr_count; |
12
|
|
|
use const T_CLOSE_CURLY_BRACKET; |
13
|
|
|
use const T_OPEN_CURLY_BRACKET; |
14
|
|
|
use const T_SEMICOLON; |
15
|
|
|
use const T_WHITESPACE; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Checks the newlines between the trait uses. |
19
|
|
|
* |
20
|
|
|
* This is a refactores copy of the slevomat code sniff. |
21
|
|
|
* |
22
|
|
|
* @author blange <[email protected]> |
23
|
|
|
* @package BestIt\Sniffs\Formatting |
24
|
|
|
*/ |
25
|
|
|
class TraitUseSpacingSniff extends AbstractSniff |
26
|
|
|
{ |
27
|
|
|
use ClassRegistrationTrait; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* You MUST not provide additional lines after your last rait usage. |
31
|
|
|
*/ |
32
|
|
|
public const CODE_INCORRECT_LINES_COUNT_AFTER_LAST_USE = 'IncorrectLinesCountAfterLastUse'; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* You MUST not provide additional new lines before your first trait use. |
36
|
|
|
*/ |
37
|
|
|
public const CODE_INCORRECT_LINES_COUNT_BEFORE_FIRST_USE = 'IncorrectLinesCountBeforeFirstUse'; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* You MUST not provide additional new lines between trait usages. |
41
|
|
|
*/ |
42
|
|
|
public const CODE_INCORRECT_LINES_COUNT_BETWEEN_USES = 'IncorrectLinesCountBetweenUses'; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* How many lines after the last use. |
46
|
|
|
*/ |
47
|
|
|
private const LINES_AFTER_LAST_USE = 1; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* How many lines after the last use. |
51
|
|
|
*/ |
52
|
|
|
private const LINES_AFTER_LAST_USE_WHEN_LAST_IN_CLASS = 0; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* How many use before the first one. |
56
|
|
|
*/ |
57
|
|
|
private const LINES_BEFORE_FIRST_USE = 0; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* How many lines between the uses. |
61
|
|
|
*/ |
62
|
|
|
private const LINES_BETWEEN_USES = 0; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* The message to the user for the error before usages. |
66
|
|
|
*/ |
67
|
|
|
private const MESSAGE_INCORRECT_LINES_COUNT_BEFORE_FIRST_USE = |
68
|
|
|
'Expected %d lines before first use statement, found %d.'; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* The message to the user for the error after the last usage. |
72
|
|
|
*/ |
73
|
|
|
private const MESSAGE_INCORRECT_LINES_COUNT_AFTER_LAST_USE = |
74
|
|
|
'Expected %d lines after last use statement, found %d.'; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* The message to the user for the error between uses. |
78
|
|
|
*/ |
79
|
|
|
private const MESSAGE_INCORRECT_LINES_COUNT_BETWEEN_USES = |
80
|
|
|
'Expected %d lines between same types of use statement, found %d.'; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* The use declarations positions of this "class". |
84
|
|
|
* |
85
|
|
|
* @var array |
86
|
|
|
*/ |
87
|
|
|
private $uses; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Returns false if there are no uses. |
91
|
|
|
* |
92
|
|
|
* @return bool |
93
|
|
|
*/ |
94
|
|
|
protected function areRequirementsMet(): bool |
95
|
|
|
{ |
96
|
|
|
return (bool) $this->uses = ClassHelper::getTraitUsePointers($this->getFile(), $this->getStackPos()); |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Checks the line after the last use and registers an error if needed. |
101
|
|
|
* |
102
|
|
|
* @param int $lastUsePos |
103
|
|
|
* |
104
|
|
|
* @return void |
105
|
|
|
*/ |
106
|
|
|
private function checkLinesAfterLastUse(int $lastUsePos): void |
107
|
|
|
{ |
108
|
|
|
$lastUseEndPos = $this->getLastUseEndPos($lastUsePos); |
109
|
|
|
|
110
|
|
|
list($realLinesAfterUse, $whitespaceEnd) = $this->getRealLinesAfterLastUse($lastUseEndPos); |
111
|
|
|
|
112
|
|
|
$requiredLinesAfter = $this->isEndOfClass($lastUseEndPos) |
113
|
|
|
? self::LINES_AFTER_LAST_USE_WHEN_LAST_IN_CLASS |
114
|
|
|
: self::LINES_AFTER_LAST_USE; |
115
|
|
|
|
116
|
|
|
if ($realLinesAfterUse !== $requiredLinesAfter) { |
117
|
|
|
$fix = $this->getFile()->addFixableError( |
118
|
|
|
self::MESSAGE_INCORRECT_LINES_COUNT_AFTER_LAST_USE, |
119
|
|
|
$lastUsePos, |
120
|
|
|
self::CODE_INCORRECT_LINES_COUNT_AFTER_LAST_USE, |
121
|
|
|
[ |
122
|
|
|
$requiredLinesAfter, |
123
|
|
|
$realLinesAfterUse |
124
|
|
|
] |
125
|
|
|
); |
126
|
|
|
|
127
|
|
|
if ($fix) { |
128
|
|
|
$this->fixLineAfterLastUse($lastUseEndPos, $whitespaceEnd, $requiredLinesAfter); |
129
|
|
|
} |
130
|
|
|
} |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Checks the lines before the first usage and registers an error if needed. |
135
|
|
|
* |
136
|
|
|
* @param int $firstUsePos |
137
|
|
|
* |
138
|
|
|
* @return void |
139
|
|
|
*/ |
140
|
|
|
private function checkLinesBeforeFirstUse(int $firstUsePos): void |
141
|
|
|
{ |
142
|
|
|
$posBeforeFirstUse = TokenHelper::findPreviousExcluding($this->getFile(), T_WHITESPACE, $firstUsePos - 1); |
143
|
|
|
$realLinesBeforeUse = $this->getRealLinesBeforeFirstUse($firstUsePos, $posBeforeFirstUse); |
144
|
|
|
|
145
|
|
|
if ($realLinesBeforeUse !== self::LINES_BEFORE_FIRST_USE) { |
146
|
|
|
$fix = $this->getFile()->addFixableError( |
147
|
|
|
self::MESSAGE_INCORRECT_LINES_COUNT_BEFORE_FIRST_USE, |
148
|
|
|
$firstUsePos, |
149
|
|
|
self::CODE_INCORRECT_LINES_COUNT_BEFORE_FIRST_USE, |
150
|
|
|
[ |
151
|
|
|
self::LINES_BEFORE_FIRST_USE, |
152
|
|
|
$realLinesBeforeUse |
153
|
|
|
] |
154
|
|
|
); |
155
|
|
|
|
156
|
|
|
if ($fix) { |
157
|
|
|
$this->fixLinesBeforeFirstUse($firstUsePos, $posBeforeFirstUse, self::LINES_BEFORE_FIRST_USE); |
|
|
|
|
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Checks the lines between uses and registers an erro rif needed. |
164
|
|
|
* |
165
|
|
|
* @return void |
166
|
|
|
*/ |
167
|
|
|
private function checkLinesBetweenUses(): void |
168
|
|
|
{ |
169
|
|
|
$file = $this->getFile(); |
170
|
|
|
$previousUsePos = null; |
171
|
|
|
|
172
|
|
|
foreach ($this->uses as $usePos) { |
173
|
|
|
if ($previousUsePos === null) { |
174
|
|
|
$previousUsePos = $usePos; |
175
|
|
|
continue; |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
$posBeforeUse = TokenHelper::findPreviousEffective($file, $usePos - 1); |
179
|
|
|
$previousUseEndPos = TokenHelper::findNextLocal( |
180
|
|
|
$file, |
181
|
|
|
[T_SEMICOLON, T_OPEN_CURLY_BRACKET], |
182
|
|
|
$previousUsePos + 1 |
183
|
|
|
); |
184
|
|
|
|
185
|
|
|
$realLinesBetweenUse = $this->getRealLinesBetweenUses( |
186
|
|
|
$previousUseEndPos, |
187
|
|
|
$usePos |
188
|
|
|
); |
189
|
|
|
|
190
|
|
|
$previousUsePos = $usePos; |
191
|
|
|
|
192
|
|
|
if ($realLinesBetweenUse !== self::LINES_BETWEEN_USES) { |
193
|
|
|
$errorParameters = [ |
194
|
|
|
self::MESSAGE_INCORRECT_LINES_COUNT_BETWEEN_USES, |
195
|
|
|
$usePos, |
196
|
|
|
self::CODE_INCORRECT_LINES_COUNT_BETWEEN_USES, |
197
|
|
|
[ |
198
|
|
|
self::LINES_BETWEEN_USES, |
199
|
|
|
$realLinesBetweenUse |
200
|
|
|
] |
201
|
|
|
]; |
202
|
|
|
|
203
|
|
|
if ($previousUseEndPos !== $posBeforeUse) { |
204
|
|
|
$file->addError(...$errorParameters); |
205
|
|
|
} else { |
206
|
|
|
$fix = $file->addFixableError(...$errorParameters); |
207
|
|
|
|
208
|
|
|
if ($fix) { |
209
|
|
|
$this->fixLinesBetweenUses($usePos, $previousUseEndPos, self::LINES_BETWEEN_USES); |
|
|
|
|
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
} |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* Fixes the lines which are allowed after the last use. |
218
|
|
|
* |
219
|
|
|
* @param int $lastUseEndPos |
220
|
|
|
* @param int $whitespaceEnd |
221
|
|
|
* @param int $requiredLinesAfter |
222
|
|
|
* |
223
|
|
|
* @return void |
224
|
|
|
*/ |
225
|
|
|
private function fixLineAfterLastUse( |
226
|
|
|
int $lastUseEndPos, |
227
|
|
|
int $whitespaceEnd, |
228
|
|
|
int $requiredLinesAfter |
229
|
|
|
): void { |
230
|
|
|
$file = $this->getFile(); |
231
|
|
|
|
232
|
|
|
$file->fixer->beginChangeset(); |
233
|
|
|
|
234
|
|
|
for ($i = $lastUseEndPos + 1; $i <= $whitespaceEnd; $i++) { |
235
|
|
|
$file->fixer->replaceToken($i, ''); |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
for ($i = 0; $i <= $requiredLinesAfter; $i++) { |
239
|
|
|
$file->fixer->addNewline($lastUseEndPos); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
$file->fixer->endChangeset(); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Fixes the lines before the first use. |
247
|
|
|
* |
248
|
|
|
* @param int $firstUsePos |
249
|
|
|
* @param int $posBeforeFirstUse |
250
|
|
|
* |
251
|
|
|
* @return void |
252
|
|
|
*/ |
253
|
|
|
private function fixLinesBeforeFirstUse( |
254
|
|
|
int $firstUsePos, |
255
|
|
|
int $posBeforeFirstUse |
256
|
|
|
): void { |
257
|
|
|
$file = $this->getFile(); |
258
|
|
|
$file->fixer->beginChangeset(); |
259
|
|
|
|
260
|
|
|
$posBeforeIndentation = TokenHelper::findPreviousContent( |
261
|
|
|
$file, |
262
|
|
|
T_WHITESPACE, |
263
|
|
|
$file->eolChar, |
264
|
|
|
$firstUsePos, |
265
|
|
|
$posBeforeFirstUse |
266
|
|
|
); |
267
|
|
|
|
268
|
|
|
if ($posBeforeIndentation !== null) { |
269
|
|
|
for ($i = $posBeforeFirstUse + 1; $i <= $posBeforeIndentation; $i++) { |
270
|
|
|
$file->fixer->replaceToken($i, ''); |
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
for ($i = 0; $i <= self::LINES_BEFORE_FIRST_USE; $i++) { |
274
|
|
|
$file->fixer->addNewline($posBeforeFirstUse); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
$file->fixer->endChangeset(); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* Fixes the lines between the uses. |
282
|
|
|
* |
283
|
|
|
* @param int $usePos |
284
|
|
|
* @param int $previousUseEndPos |
285
|
|
|
* |
286
|
|
|
* @return void |
287
|
|
|
*/ |
288
|
|
|
private function fixLinesBetweenUses(int $usePos, int $previousUseEndPos): void |
289
|
|
|
{ |
290
|
|
|
$file = $this->getFile(); |
291
|
|
|
|
292
|
|
|
$posBeforeIndentation = TokenHelper::findPreviousContent( |
293
|
|
|
$file, |
294
|
|
|
T_WHITESPACE, |
295
|
|
|
$file->eolChar, |
296
|
|
|
$usePos, |
297
|
|
|
$previousUseEndPos |
298
|
|
|
); |
299
|
|
|
|
300
|
|
|
$file->fixer->beginChangeset(); |
301
|
|
|
if ($posBeforeIndentation !== null) { |
302
|
|
|
for ($i = $previousUseEndPos + 1; $i <= $posBeforeIndentation; $i++) { |
303
|
|
|
$file->fixer->replaceToken($i, ''); |
304
|
|
|
} |
305
|
|
|
} |
306
|
|
|
for ($i = 0; $i <= self::LINES_BETWEEN_USES; $i++) { |
307
|
|
|
$file->fixer->addNewline($previousUseEndPos); |
308
|
|
|
} |
309
|
|
|
$file->fixer->endChangeset(); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Gets the position on which the last use ends. |
314
|
|
|
* |
315
|
|
|
* @param int $lastUsePos |
316
|
|
|
* |
317
|
|
|
* @return int |
318
|
|
|
*/ |
319
|
|
|
private function getLastUseEndPos(int $lastUsePos): int |
320
|
|
|
{ |
321
|
|
|
$file = $this->getFile(); |
322
|
|
|
$tokens = $file->getTokens(); |
323
|
|
|
|
324
|
|
|
$lastUseEndPos = TokenHelper::findNextLocal( |
325
|
|
|
$file, |
326
|
|
|
[T_SEMICOLON, T_OPEN_CURLY_BRACKET], |
327
|
|
|
$lastUsePos + 1 |
328
|
|
|
); |
329
|
|
|
|
330
|
|
|
if ($tokens[$lastUseEndPos]['code'] === T_OPEN_CURLY_BRACKET) { |
331
|
|
|
$lastUseEndPos = $tokens[$lastUseEndPos]['bracket_closer']; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
return $lastUseEndPos; |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
/** |
338
|
|
|
* Gets the real lines after the last use. |
339
|
|
|
* |
340
|
|
|
* @param int $lastUseEndPos |
341
|
|
|
* |
342
|
|
|
* @return array The first element is the line count, and the second element is when the whitespace ends. |
343
|
|
|
*/ |
344
|
|
|
private function getRealLinesAfterLastUse(int $lastUseEndPos): array |
345
|
|
|
{ |
346
|
|
|
$file = $this->getFile(); |
347
|
|
|
$tokens = $file->getTokens(); |
348
|
|
|
$whitespaceEnd = TokenHelper::findNextExcluding($file, T_WHITESPACE, $lastUseEndPos + 1) - 1; |
349
|
|
|
|
350
|
|
|
if ($lastUseEndPos !== $whitespaceEnd && $tokens[$whitespaceEnd]['content'] !== $file->eolChar) { |
351
|
|
|
$lastEolPos = TokenHelper::findPreviousContent( |
352
|
|
|
$file, |
353
|
|
|
T_WHITESPACE, |
354
|
|
|
$file->eolChar, |
355
|
|
|
$whitespaceEnd - 1, |
356
|
|
|
$lastUseEndPos |
357
|
|
|
); |
358
|
|
|
$whitespaceEnd = $lastEolPos ?? $lastUseEndPos; |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
$whitespaceAfterLastUse = TokenHelper::getContent($file, $lastUseEndPos + 1, $whitespaceEnd); |
362
|
|
|
|
363
|
|
|
$realLinesAfterUse = substr_count($whitespaceAfterLastUse, $file->eolChar) - 1; |
364
|
|
|
|
365
|
|
|
return [$realLinesAfterUse, $whitespaceEnd]; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* Returns the real lines before the first use. |
370
|
|
|
* |
371
|
|
|
* @param int $firstUsePos |
372
|
|
|
* @param int $posBeforeFirstUse |
373
|
|
|
* |
374
|
|
|
* @return int |
375
|
|
|
*/ |
376
|
|
|
private function getRealLinesBeforeFirstUse(int $firstUsePos, int $posBeforeFirstUse): int |
377
|
|
|
{ |
378
|
|
|
$file = $this->getFile(); |
379
|
|
|
$whitespaceBeforeFirstUse = ''; |
380
|
|
|
|
381
|
|
|
if ($posBeforeFirstUse + 1 !== $firstUsePos) { |
382
|
|
|
$whitespaceBeforeFirstUse .= TokenHelper::getContent( |
383
|
|
|
$file, |
384
|
|
|
$posBeforeFirstUse + 1, |
385
|
|
|
$firstUsePos - 1 |
386
|
|
|
); |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
return substr_count($whitespaceBeforeFirstUse, $file->eolChar) - 1; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* Returns the real lines between the uses. |
394
|
|
|
* |
395
|
|
|
* @param int $previousUseEndPos |
396
|
|
|
* @param int $usePos |
397
|
|
|
* |
398
|
|
|
* @return int |
399
|
|
|
*/ |
400
|
|
|
private function getRealLinesBetweenUses(int &$previousUseEndPos, int $usePos): int |
401
|
|
|
{ |
402
|
|
|
$tokens = $this->getFile()->getTokens(); |
403
|
|
|
|
404
|
|
|
if ($tokens[$previousUseEndPos]['code'] === T_OPEN_CURLY_BRACKET) { |
405
|
|
|
$previousUseEndPos = $tokens[$previousUseEndPos]['bracket_closer']; |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
return $tokens[$usePos]['line'] - $tokens[$previousUseEndPos]['line'] - 1; |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
/** |
412
|
|
|
* Is the given Position the end of the class. |
413
|
|
|
* |
414
|
|
|
* @param int $lastUseEndPos |
415
|
|
|
* |
416
|
|
|
* @return bool |
417
|
|
|
*/ |
418
|
|
|
private function isEndOfClass(int $lastUseEndPos): bool |
419
|
|
|
{ |
420
|
|
|
$file = $this->getFile(); |
421
|
|
|
$tokens = $file->getTokens(); |
422
|
|
|
|
423
|
|
|
$posAfterLastUse = TokenHelper::findNextEffective($file, $lastUseEndPos + 1); |
424
|
|
|
|
425
|
|
|
return $tokens[$posAfterLastUse]['code'] === T_CLOSE_CURLY_BRACKET; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
/** |
429
|
|
|
* Processes the token. |
430
|
|
|
* |
431
|
|
|
* @return void |
432
|
|
|
*/ |
433
|
|
|
protected function processToken(): void |
434
|
|
|
{ |
435
|
|
|
$this->checkLinesBeforeFirstUse($this->uses[0]); |
436
|
|
|
$this->checkLinesAfterLastUse($this->uses[count($this->uses) - 1]); |
437
|
|
|
|
438
|
|
|
if (count($this->uses) > 1) { |
439
|
|
|
$this->checkLinesBetweenUses(); |
440
|
|
|
} |
441
|
|
|
} |
442
|
|
|
} |
443
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.