1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This file is part of the mo4-coding-standard (phpcs standard) |
5
|
|
|
* |
6
|
|
|
* PHP version 5 |
7
|
|
|
* |
8
|
|
|
* @category PHP |
9
|
|
|
* @package PHP_CodeSniffer-MO4 |
10
|
|
|
* @author Xaver Loppenstedt <[email protected]> |
11
|
|
|
* @license http://spdx.org/licenses/MIT MIT License |
12
|
|
|
* @version GIT: master |
13
|
|
|
* @link https://github.com/Mayflower/mo4-coding-standard |
14
|
|
|
*/ |
15
|
|
|
|
16
|
|
|
namespace MO4\Sniffs\Formatting; |
17
|
|
|
|
18
|
|
|
use PHP_CodeSniffer\Exceptions\RuntimeException; |
19
|
|
|
use PHP_CodeSniffer\Files\File; |
20
|
|
|
use PHP_CodeSniffer\Standards\PSR2\Sniffs\Namespaces\UseDeclarationSniff; |
21
|
|
|
use PHP_CodeSniffer\Util\Common; |
22
|
|
|
use PHP_CodeSniffer\Util\Tokens as PHP_CodeSniffer_Tokens; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* Alphabetical Use Statements sniff. |
26
|
|
|
* |
27
|
|
|
* Use statements must be in alphabetical order, grouped by empty lines. |
28
|
|
|
* |
29
|
|
|
* @category PHP |
30
|
|
|
* @package PHP_CodeSniffer-MO4 |
31
|
|
|
* @author Xaver Loppenstedt <[email protected]> |
32
|
|
|
* @author Steffen Ritter <[email protected]> |
33
|
|
|
* @author Christian Albrecht <[email protected]> |
34
|
|
|
* @copyright 2013-2017 Xaver Loppenstedt, some rights reserved. |
35
|
|
|
* @license http://spdx.org/licenses/MIT MIT License |
36
|
|
|
* @link https://github.com/Mayflower/mo4-coding-standard |
37
|
|
|
*/ |
38
|
|
|
class AlphabeticalUseStatementsSniff extends UseDeclarationSniff |
39
|
|
|
{ |
40
|
|
|
|
41
|
|
|
const NAMESPACE_SEPARATOR_STRING = '\\'; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Sorting order, can be one of: |
45
|
|
|
* 'dictionary', 'string', 'string-locale' or 'string-case-insensitive' |
46
|
|
|
* |
47
|
|
|
* Unknown types will be mapped to 'string'. |
48
|
|
|
* |
49
|
|
|
* @var string |
50
|
|
|
*/ |
51
|
|
|
public $order = 'dictionary'; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Supported ordering methods |
55
|
|
|
* |
56
|
|
|
* @var array |
57
|
|
|
*/ |
58
|
|
|
private $supportedOrderingMethods = [ |
59
|
|
|
'dictionary', |
60
|
|
|
'string', |
61
|
|
|
'string', |
62
|
|
|
'string-locale', |
63
|
|
|
'string-case-insensitive', |
64
|
|
|
]; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Last import seen in group |
68
|
|
|
* |
69
|
|
|
* @var string |
70
|
|
|
*/ |
71
|
|
|
private $lastImport = ''; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Line number of the last seen use statement |
75
|
|
|
* |
76
|
|
|
* @var integer |
77
|
|
|
*/ |
78
|
|
|
private $lastLine = -1; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Current file |
82
|
|
|
* |
83
|
|
|
* @var string |
84
|
|
|
*/ |
85
|
|
|
private $currentFile; |
86
|
|
|
|
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* Processes this test, when one of its tokens is encountered. |
90
|
|
|
* |
91
|
|
|
* @param File $phpcsFile The file being scanned. |
92
|
|
|
* @param int $stackPtr The position of the current token in |
93
|
|
|
* the stack passed in $tokens. |
94
|
|
|
* |
95
|
|
|
* @return void |
96
|
|
|
*/ |
97
|
|
|
public function process(File $phpcsFile, $stackPtr) |
98
|
|
|
{ |
99
|
|
|
if (in_array($this->order, $this->supportedOrderingMethods, true) === false) { |
100
|
|
|
$error = sprintf( |
101
|
|
|
"'%s' is not a valid order function for %s! Pick one of: %s", |
102
|
|
|
$this->order, |
103
|
|
|
Common::getSniffCode(__CLASS__), |
104
|
|
|
implode(', ', $this->supportedOrderingMethods) |
105
|
|
|
); |
106
|
|
|
|
107
|
|
|
$phpcsFile->addError($error, $stackPtr, 'InvalidOrder'); |
108
|
|
|
|
109
|
|
|
return; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
parent::process($phpcsFile, $stackPtr); |
113
|
|
|
|
114
|
|
|
if ($this->currentFile !== $phpcsFile->getFilename()) { |
115
|
|
|
$this->lastLine = -1; |
116
|
|
|
$this->lastImport = ''; |
117
|
|
|
$this->currentFile = $phpcsFile->getFilename(); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$tokens = $phpcsFile->getTokens(); |
121
|
|
|
$line = $tokens[$stackPtr]['line']; |
122
|
|
|
|
123
|
|
|
// Ignore function () use () {...}. |
124
|
|
|
$isNonImportUse = $this->checkIsNonImportUse($phpcsFile, $stackPtr); |
125
|
|
|
if (true === $isNonImportUse) { |
126
|
|
|
return; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
$currentImportArr = $this->getUseImport($phpcsFile, $stackPtr); |
130
|
|
|
if ($currentImportArr === false) { |
131
|
|
|
return; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
$currentPtr = $currentImportArr['startPtr']; |
135
|
|
|
$currentImport = $currentImportArr['content']; |
136
|
|
|
|
137
|
|
|
if (($this->lastLine + 1) < $line) { |
138
|
|
|
$this->lastLine = $line; |
139
|
|
|
$this->lastImport = $currentImport; |
140
|
|
|
|
141
|
|
|
return; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
$fixable = false; |
145
|
|
|
if ($this->lastImport !== '' |
146
|
|
|
&& $this->compareString($this->lastImport, $currentImport) > 0 |
147
|
|
|
) { |
148
|
|
|
$msg = 'USE statements must be sorted alphabetically, order %s'; |
149
|
|
|
$code = 'MustBeSortedAlphabetically'; |
150
|
|
|
$fixable = $phpcsFile->addFixableError($msg, $currentPtr, $code, [$this->order]); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
if (true === $fixable) { |
154
|
|
|
// Find the correct position in current use block. |
155
|
|
|
$newDestinationPtr |
156
|
|
|
= $this->findNewDestination($phpcsFile, $stackPtr, $currentImport); |
157
|
|
|
|
158
|
|
|
$currentUseStr = $this->getUseStatementAsString($phpcsFile, $stackPtr); |
159
|
|
|
|
160
|
|
|
$phpcsFile->fixer->beginChangeset(); |
161
|
|
|
$phpcsFile->fixer->addContentBefore($newDestinationPtr, $currentUseStr); |
162
|
|
|
$this->fixerClearLine($phpcsFile, $stackPtr); |
163
|
|
|
$phpcsFile->fixer->endChangeset(); |
164
|
|
|
}//end if |
165
|
|
|
|
166
|
|
|
$this->lastImport = $currentImport; |
167
|
|
|
$this->lastLine = $line; |
168
|
|
|
|
169
|
|
|
}//end process() |
170
|
|
|
|
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Get the import class name for use statement pointed by $stackPtr. |
174
|
|
|
* |
175
|
|
|
* @param File $phpcsFile PHP CS File |
176
|
|
|
* @param int $stackPtr pointer |
177
|
|
|
* |
178
|
|
|
* @return array|false |
179
|
|
|
*/ |
180
|
|
|
private function getUseImport(File $phpcsFile, $stackPtr) |
181
|
|
|
{ |
182
|
|
|
$importTokens = [ |
183
|
|
|
T_NS_SEPARATOR, |
184
|
|
|
T_STRING, |
185
|
|
|
]; |
186
|
|
|
|
187
|
|
|
$start = $phpcsFile->findNext( |
188
|
|
|
PHP_CodeSniffer_Tokens::$emptyTokens, |
189
|
|
|
($stackPtr + 1), |
190
|
|
|
null, |
191
|
|
|
true |
192
|
|
|
); |
193
|
|
|
// $start is false when "use" is the last token in file... |
194
|
|
|
if ($start === false) { |
195
|
|
|
return false; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
$start = (int) $start; |
199
|
|
|
$end = $phpcsFile->findNext($importTokens, $start, null, true); |
200
|
|
|
$import = $phpcsFile->getTokensAsString($start, ($end - $start)); |
201
|
|
|
|
202
|
|
|
return [ |
203
|
|
|
'startPtr' => $start, |
204
|
|
|
'content' => $import, |
205
|
|
|
]; |
206
|
|
|
|
207
|
|
|
}//end getUseImport() |
208
|
|
|
|
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Get the full use statement as string, including trailing white space. |
212
|
|
|
* |
213
|
|
|
* @param File $phpcsFile PHP CS File |
214
|
|
|
* @param int $stackPtr pointer |
215
|
|
|
* |
216
|
|
|
* @return string |
217
|
|
|
*/ |
218
|
|
|
private function getUseStatementAsString( |
219
|
|
|
File $phpcsFile, |
220
|
|
|
$stackPtr |
221
|
|
|
) { |
222
|
|
|
$tokens = $phpcsFile->getTokens(); |
223
|
|
|
|
224
|
|
|
$useEndPtr = $phpcsFile->findNext([T_SEMICOLON], ($stackPtr + 2)); |
225
|
|
|
$useLength = ($useEndPtr - $stackPtr + 1); |
226
|
|
|
if ($tokens[($useEndPtr + 1)]['code'] === T_WHITESPACE) { |
227
|
|
|
$useLength++; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
return $phpcsFile->getTokensAsString($stackPtr, $useLength); |
231
|
|
|
|
232
|
|
|
}//end getUseStatementAsString() |
233
|
|
|
|
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Check if "use" token is not used for import. |
237
|
|
|
* E.g. function () use () {...}. |
238
|
|
|
* |
239
|
|
|
* @param File $phpcsFile PHP CS File |
240
|
|
|
* @param int $stackPtr pointer |
241
|
|
|
* |
242
|
|
|
* @return bool |
243
|
|
|
*/ |
244
|
|
|
private function checkIsNonImportUse(File $phpcsFile, $stackPtr) |
245
|
|
|
{ |
246
|
|
|
$tokens = $phpcsFile->getTokens(); |
247
|
|
|
|
248
|
|
|
$prev = $phpcsFile->findPrevious( |
249
|
|
|
PHP_CodeSniffer_Tokens::$emptyTokens, |
250
|
|
|
($stackPtr - 1), |
251
|
|
|
null, |
252
|
|
|
true, |
253
|
|
|
null, |
254
|
|
|
true |
255
|
|
|
); |
256
|
|
|
|
257
|
|
|
if (false !== $prev) { |
258
|
|
|
$prevToken = $tokens[$prev]; |
259
|
|
|
|
260
|
|
|
if ($prevToken['code'] === T_CLOSE_PARENTHESIS) { |
261
|
|
|
return true; |
262
|
|
|
} |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
return false; |
266
|
|
|
|
267
|
|
|
}//end checkIsNonImportUse() |
268
|
|
|
|
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* Replace all the token in same line as the element pointed to by $stackPtr |
272
|
|
|
* the by the empty string. |
273
|
|
|
* This will delete the line. |
274
|
|
|
* |
275
|
|
|
* @param File $phpcsFile PHP CS file |
276
|
|
|
* @param int $stackPtr pointer |
277
|
|
|
* |
278
|
|
|
* @return void |
279
|
|
|
*/ |
280
|
|
|
private function fixerClearLine(File $phpcsFile, $stackPtr) |
281
|
|
|
{ |
282
|
|
|
$tokens = $phpcsFile->getTokens(); |
283
|
|
|
$line = $tokens[$stackPtr]['line']; |
284
|
|
|
|
285
|
|
View Code Duplication |
for ($i = ($stackPtr - 1); $tokens[$i]['line'] === $line; $i--) { |
|
|
|
|
286
|
|
|
$phpcsFile->fixer->replaceToken($i, ''); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
View Code Duplication |
for ($i = $stackPtr; $tokens[$i]['line'] === $line; $i++) { |
|
|
|
|
290
|
|
|
$phpcsFile->fixer->replaceToken($i, ''); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
}//end fixerClearLine() |
294
|
|
|
|
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Find a new destination pointer for the given import string in current |
298
|
|
|
* use block. |
299
|
|
|
* |
300
|
|
|
* @param File $phpcsFile PHP CS File |
301
|
|
|
* @param int $stackPtr pointer |
302
|
|
|
* @param string $import import string requiring new position |
303
|
|
|
* |
304
|
|
|
* @return int |
305
|
|
|
*/ |
306
|
|
|
private function findNewDestination( |
307
|
|
|
File $phpcsFile, |
308
|
|
|
$stackPtr, |
309
|
|
|
$import |
310
|
|
|
) { |
311
|
|
|
$tokens = $phpcsFile->getTokens(); |
312
|
|
|
|
313
|
|
|
$line = $tokens[$stackPtr]['line']; |
314
|
|
|
$prevLine = false; |
315
|
|
|
$prevPtr = $stackPtr; |
316
|
|
|
do { |
317
|
|
|
$ptr = $prevPtr; |
318
|
|
|
// Use $line for the first iteration. |
319
|
|
|
if ($prevLine !== false) { |
320
|
|
|
$line = $prevLine; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
$prevPtr = $phpcsFile->findPrevious(T_USE, ($ptr - 1)); |
324
|
|
|
if ($prevPtr === false) { |
325
|
|
|
break; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
$prevLine = $tokens[$prevPtr]['line']; |
329
|
|
|
$prevImportArr = $this->getUseImport($phpcsFile, (int) $prevPtr); |
330
|
|
|
} while ($prevLine === ($line - 1) |
331
|
|
|
&& ($this->compareString($prevImportArr['content'], $import) > 0) |
332
|
|
|
); |
333
|
|
|
|
334
|
|
|
return $ptr; |
335
|
|
|
|
336
|
|
|
}//end findNewDestination() |
337
|
|
|
|
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Compare namespace strings according defined order function. |
341
|
|
|
* |
342
|
|
|
* @param string $a first namespace string |
343
|
|
|
* @param string $b second namespace string |
344
|
|
|
* |
345
|
|
|
* @return int |
346
|
|
|
*/ |
347
|
|
|
private function compareString($a, $b) |
348
|
|
|
{ |
349
|
|
|
switch ($this->order) { |
350
|
|
|
case 'string': |
351
|
|
|
return strcmp($a, $b); |
352
|
|
|
case 'string-locale': |
353
|
|
|
return strcoll($a, $b); |
354
|
|
|
case 'string-case-insensitive': |
355
|
|
|
return strcasecmp($a, $b); |
356
|
|
|
default: |
357
|
|
|
// Default is 'dictionary'. |
358
|
|
|
return $this->dictionaryCompare($a, $b); |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
}//end compareString() |
362
|
|
|
|
363
|
|
|
|
364
|
|
|
/** |
365
|
|
|
* Lexicographical namespace string compare. |
366
|
|
|
* |
367
|
|
|
* Example: |
368
|
|
|
* |
369
|
|
|
* use Doctrine\ORM\Query; |
370
|
|
|
* use Doctrine\ORM\Query\Expr; |
371
|
|
|
* use Doctrine\ORM\QueryBuilder; |
372
|
|
|
* |
373
|
|
|
* @param string $a first namespace string |
374
|
|
|
* @param string $b second namespace string |
375
|
|
|
* |
376
|
|
|
* @return int |
377
|
|
|
*/ |
378
|
|
|
private function dictionaryCompare($a, $b) |
379
|
|
|
{ |
380
|
|
|
$min = min(strlen($a), strlen($b)); |
381
|
|
|
|
382
|
|
|
for ($i = 0; $i < $min; $i++) { |
383
|
|
|
if ($a[$i] === $b[$i]) { |
384
|
|
|
continue; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
if ($a[$i] === self::NAMESPACE_SEPARATOR_STRING) { |
388
|
|
|
return -1; |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
if ($b[$i] === self::NAMESPACE_SEPARATOR_STRING) { |
392
|
|
|
return 1; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
if ($a[$i] < $b[$i]) { |
396
|
|
|
return -1; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
if ($a[$i] > $b[$i]) { |
400
|
|
|
return 1; |
401
|
|
|
} |
402
|
|
|
}//end for |
403
|
|
|
|
404
|
|
|
return strcmp(substr($a, $min), substr($b, $min)); |
|
|
|
|
405
|
|
|
|
406
|
|
|
}//end dictionaryCompare() |
407
|
|
|
|
408
|
|
|
|
409
|
|
|
}//end class |
410
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.