1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* PHPCompatibility, an external standard for PHP_CodeSniffer. |
4
|
|
|
* |
5
|
|
|
* @package PHPCompatibility |
6
|
|
|
* @copyright 2012-2019 PHPCompatibility Contributors |
7
|
|
|
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3 |
8
|
|
|
* @link https://github.com/PHPCompatibility/PHPCompatibility |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace PHPCompatibility\Sniffs\Keywords; |
12
|
|
|
|
13
|
|
|
use PHPCompatibility\Sniff; |
14
|
|
|
use PHP_CodeSniffer_File as File; |
15
|
|
|
use PHP_CodeSniffer_Tokens as Tokens; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Prohibits the use of reserved keywords as class, function, namespace or constant names. |
19
|
|
|
*/ |
20
|
|
|
class ForbiddenNamesSniff extends Sniff |
21
|
|
|
{ |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* A list of keywords that can not be used as function, class and namespace name or constant name. |
25
|
|
|
* Mentions since which version it's not allowed. |
26
|
|
|
* |
27
|
|
|
* @var array(string => string) |
28
|
|
|
*/ |
29
|
|
|
protected $invalidNames = array( |
30
|
|
|
'abstract' => '5.0', |
31
|
|
|
'and' => 'all', |
32
|
|
|
'array' => 'all', |
33
|
|
|
'as' => 'all', |
34
|
|
|
'break' => 'all', |
35
|
|
|
'callable' => '5.4', |
36
|
|
|
'case' => 'all', |
37
|
|
|
'catch' => '5.0', |
38
|
|
|
'class' => 'all', |
39
|
|
|
'clone' => '5.0', |
40
|
|
|
'const' => 'all', |
41
|
|
|
'continue' => 'all', |
42
|
|
|
'declare' => 'all', |
43
|
|
|
'default' => 'all', |
44
|
|
|
'die' => 'all', |
45
|
|
|
'do' => 'all', |
46
|
|
|
'echo' => 'all', |
47
|
|
|
'else' => 'all', |
48
|
|
|
'elseif' => 'all', |
49
|
|
|
'empty' => 'all', |
50
|
|
|
'enddeclare' => 'all', |
51
|
|
|
'endfor' => 'all', |
52
|
|
|
'endforeach' => 'all', |
53
|
|
|
'endif' => 'all', |
54
|
|
|
'endswitch' => 'all', |
55
|
|
|
'endwhile' => 'all', |
56
|
|
|
'eval' => 'all', |
57
|
|
|
'exit' => 'all', |
58
|
|
|
'extends' => 'all', |
59
|
|
|
'final' => '5.0', |
60
|
|
|
'finally' => '5.5', |
61
|
|
|
'for' => 'all', |
62
|
|
|
'foreach' => 'all', |
63
|
|
|
'function' => 'all', |
64
|
|
|
'global' => 'all', |
65
|
|
|
'goto' => '5.3', |
66
|
|
|
'if' => 'all', |
67
|
|
|
'implements' => '5.0', |
68
|
|
|
'include' => 'all', |
69
|
|
|
'include_once' => 'all', |
70
|
|
|
'instanceof' => '5.0', |
71
|
|
|
'insteadof' => '5.4', |
72
|
|
|
'interface' => '5.0', |
73
|
|
|
'isset' => 'all', |
74
|
|
|
'list' => 'all', |
75
|
|
|
'namespace' => '5.3', |
76
|
|
|
'new' => 'all', |
77
|
|
|
'or' => 'all', |
78
|
|
|
'print' => 'all', |
79
|
|
|
'private' => '5.0', |
80
|
|
|
'protected' => '5.0', |
81
|
|
|
'public' => '5.0', |
82
|
|
|
'require' => 'all', |
83
|
|
|
'require_once' => 'all', |
84
|
|
|
'return' => 'all', |
85
|
|
|
'static' => 'all', |
86
|
|
|
'switch' => 'all', |
87
|
|
|
'throw' => '5.0', |
88
|
|
|
'trait' => '5.4', |
89
|
|
|
'try' => '5.0', |
90
|
|
|
'unset' => 'all', |
91
|
|
|
'use' => 'all', |
92
|
|
|
'var' => 'all', |
93
|
|
|
'while' => 'all', |
94
|
|
|
'xor' => 'all', |
95
|
|
|
'yield' => '5.5', |
96
|
|
|
'__class__' => 'all', |
97
|
|
|
'__dir__' => '5.3', |
98
|
|
|
'__file__' => 'all', |
99
|
|
|
'__function__' => 'all', |
100
|
|
|
'__method__' => 'all', |
101
|
|
|
'__namespace__' => '5.3', |
102
|
|
|
); |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* A list of keywords that can follow use statements. |
106
|
|
|
* |
107
|
|
|
* @var array(string => string) |
108
|
|
|
*/ |
109
|
|
|
protected $validUseNames = array( |
110
|
|
|
'const' => true, |
111
|
|
|
'function' => true, |
112
|
|
|
); |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Scope modifiers and other keywords allowed in trait use statements. |
116
|
|
|
* |
117
|
|
|
* @var array |
118
|
|
|
*/ |
119
|
|
|
private $allowedModifiers = array(); |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Targeted tokens. |
123
|
|
|
* |
124
|
|
|
* @var array |
125
|
|
|
*/ |
126
|
|
|
protected $targetedTokens = array( |
127
|
|
|
\T_CLASS, |
128
|
|
|
\T_FUNCTION, |
129
|
|
|
\T_NAMESPACE, |
130
|
|
|
\T_STRING, |
131
|
|
|
\T_CONST, |
132
|
|
|
\T_USE, |
133
|
|
|
\T_AS, |
134
|
|
|
\T_EXTENDS, |
135
|
|
|
\T_INTERFACE, |
136
|
|
|
\T_TRAIT, |
137
|
|
|
); |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Returns an array of tokens this test wants to listen for. |
141
|
|
|
* |
142
|
|
|
* @return array |
143
|
|
|
*/ |
144
|
|
|
public function register() |
145
|
|
|
{ |
146
|
|
|
$this->allowedModifiers = Tokens::$scopeModifiers; |
147
|
|
|
$this->allowedModifiers[\T_FINAL] = \T_FINAL; |
148
|
|
|
|
149
|
|
|
$tokens = $this->targetedTokens; |
150
|
|
|
|
151
|
|
|
if (\defined('T_ANON_CLASS')) { |
152
|
|
|
$tokens[] = \T_ANON_CLASS; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
return $tokens; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Processes this test, when one of its tokens is encountered. |
160
|
|
|
* |
161
|
|
|
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned. |
162
|
|
|
* @param int $stackPtr The position of the current token in the |
163
|
|
|
* stack passed in $tokens. |
164
|
|
|
* |
165
|
|
|
* @return void |
166
|
|
|
*/ |
167
|
|
|
public function process(File $phpcsFile, $stackPtr) |
168
|
|
|
{ |
169
|
|
|
$tokens = $phpcsFile->getTokens(); |
170
|
|
|
|
171
|
|
|
/* |
172
|
|
|
* We distinguish between the class, function and namespace names vs the define statements. |
173
|
|
|
*/ |
174
|
|
|
if ($tokens[$stackPtr]['type'] === 'T_STRING') { |
175
|
|
|
$this->processString($phpcsFile, $stackPtr, $tokens); |
176
|
|
|
} else { |
177
|
|
|
$this->processNonString($phpcsFile, $stackPtr, $tokens); |
178
|
|
|
} |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Processes this test, when one of its tokens is encountered. |
183
|
|
|
* |
184
|
|
|
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned. |
185
|
|
|
* @param int $stackPtr The position of the current token in the |
186
|
|
|
* stack passed in $tokens. |
187
|
|
|
* @param array $tokens The stack of tokens that make up |
188
|
|
|
* the file. |
189
|
|
|
* |
190
|
|
|
* @return void |
191
|
|
|
*/ |
192
|
|
|
public function processNonString(File $phpcsFile, $stackPtr, $tokens) |
193
|
|
|
{ |
194
|
|
|
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); |
195
|
|
|
if ($nextNonEmpty === false) { |
196
|
|
|
return; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/* |
200
|
|
|
* Deal with anonymous classes - `class` before a reserved keyword is sometimes |
201
|
|
|
* misidentified as `T_ANON_CLASS`. |
202
|
|
|
* In PHPCS < 2.3.4 these were tokenized as T_CLASS no matter what. |
203
|
|
|
*/ |
204
|
|
|
if ($tokens[$stackPtr]['type'] === 'T_ANON_CLASS' || $tokens[$stackPtr]['type'] === 'T_CLASS') { |
205
|
|
|
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); |
206
|
|
|
if ($prevNonEmpty !== false && $tokens[$prevNonEmpty]['type'] === 'T_NEW') { |
207
|
|
|
return; |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/* |
212
|
|
|
* PHP 5.6 allows for use const and use function, but only if followed by the function/constant name. |
213
|
|
|
* - `use function HelloWorld` => move to the next token (HelloWorld) to verify. |
214
|
|
|
* - `use const HelloWorld` => move to the next token (HelloWorld) to verify. |
215
|
|
|
*/ |
216
|
|
|
elseif ($tokens[$stackPtr]['type'] === 'T_USE' |
217
|
|
|
&& isset($this->validUseNames[strtolower($tokens[$nextNonEmpty]['content'])]) === true |
218
|
|
|
) { |
219
|
|
|
$maybeUseNext = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true, null, true); |
220
|
|
|
if ($maybeUseNext !== false && $this->isEndOfUseStatement($tokens[$maybeUseNext]) === false) { |
221
|
|
|
$nextNonEmpty = $maybeUseNext; |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/* |
226
|
|
|
* Deal with visibility modifiers. |
227
|
|
|
* - `use HelloWorld { sayHello as protected; }` => valid, bow out. |
228
|
|
|
* - `use HelloWorld { sayHello as private myPrivateHello; }` => move to the next token to verify. |
229
|
|
|
*/ |
230
|
|
|
elseif ($tokens[$stackPtr]['type'] === 'T_AS' |
231
|
|
|
&& isset($this->allowedModifiers[$tokens[$nextNonEmpty]['code']]) === true |
232
|
|
|
&& $phpcsFile->hasCondition($stackPtr, \T_USE) === true |
233
|
|
|
) { |
234
|
|
|
$maybeUseNext = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true, null, true); |
235
|
|
|
if ($maybeUseNext === false || $this->isEndOfUseStatement($tokens[$maybeUseNext]) === true) { |
236
|
|
|
return; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
$nextNonEmpty = $maybeUseNext; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/* |
243
|
|
|
* Deal with foreach ( ... as list() ). |
244
|
|
|
*/ |
245
|
|
|
elseif ($tokens[$stackPtr]['type'] === 'T_AS' |
246
|
|
|
&& isset($tokens[$stackPtr]['nested_parenthesis']) === true |
247
|
|
|
&& $tokens[$nextNonEmpty]['code'] === \T_LIST |
248
|
|
|
) { |
249
|
|
|
$parentheses = array_reverse($tokens[$stackPtr]['nested_parenthesis'], true); |
250
|
|
View Code Duplication |
foreach ($parentheses as $open => $close) { |
|
|
|
|
251
|
|
|
if (isset($tokens[$open]['parenthesis_owner']) |
252
|
|
|
&& $tokens[$tokens[$open]['parenthesis_owner']]['code'] === \T_FOREACH |
253
|
|
|
) { |
254
|
|
|
return; |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/* |
260
|
|
|
* Deal with functions declared to return by reference. |
261
|
|
|
*/ |
262
|
|
|
elseif ($tokens[$stackPtr]['type'] === 'T_FUNCTION' |
263
|
|
|
&& $tokens[$nextNonEmpty]['type'] === 'T_BITWISE_AND' |
264
|
|
|
) { |
265
|
|
|
$maybeUseNext = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true, null, true); |
266
|
|
|
if ($maybeUseNext === false) { |
267
|
|
|
// Live coding. |
268
|
|
|
return; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
$nextNonEmpty = $maybeUseNext; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/* |
275
|
|
|
* Deal with nested namespaces. |
276
|
|
|
*/ |
277
|
|
|
elseif ($tokens[$stackPtr]['type'] === 'T_NAMESPACE') { |
278
|
|
|
if ($tokens[$stackPtr + 1]['code'] === \T_NS_SEPARATOR) { |
279
|
|
|
// Not a namespace declaration, but use of, i.e. namespace\someFunction(); |
280
|
|
|
return; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
$endToken = $phpcsFile->findNext(array(\T_SEMICOLON, \T_OPEN_CURLY_BRACKET), ($stackPtr + 1), null, false, null, true); |
284
|
|
|
$namespaceName = trim($phpcsFile->getTokensAsString(($stackPtr + 1), ($endToken - $stackPtr - 1))); |
285
|
|
|
if (empty($namespaceName) === true) { |
286
|
|
|
return; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
$namespaceParts = explode('\\', $namespaceName); |
290
|
|
|
foreach ($namespaceParts as $namespacePart) { |
291
|
|
|
$partLc = strtolower($namespacePart); |
292
|
|
|
if (isset($this->invalidNames[$partLc]) === false) { |
293
|
|
|
continue; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
// Find the token position of the part which matched. |
297
|
|
|
for ($i = ($stackPtr + 1); $i < $endToken; $i++) { |
298
|
|
|
if ($tokens[$i]['content'] === $namespacePart) { |
299
|
|
|
$nextNonEmpty = $i; |
300
|
|
|
break; |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
} |
304
|
|
|
unset($i, $namespacePart, $partLc); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
$nextContentLc = strtolower($tokens[$nextNonEmpty]['content']); |
308
|
|
|
if (isset($this->invalidNames[$nextContentLc]) === false) { |
309
|
|
|
return; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/* |
313
|
|
|
* Deal with PHP 7 relaxing the rules. |
314
|
|
|
* "As of PHP 7.0.0 these keywords are allowed as property, constant, and method names |
315
|
|
|
* of classes, interfaces and traits, except that class may not be used as constant name." |
316
|
|
|
*/ |
317
|
|
|
if ((($tokens[$stackPtr]['type'] === 'T_FUNCTION' |
318
|
|
|
&& $this->inClassScope($phpcsFile, $stackPtr, false) === true) |
319
|
|
|
|| ($tokens[$stackPtr]['type'] === 'T_CONST' |
320
|
|
|
&& $this->isClassConstant($phpcsFile, $stackPtr) === true |
321
|
|
|
&& $nextContentLc !== 'class')) |
322
|
|
|
&& $this->supportsBelow('5.6') === false |
323
|
|
|
) { |
324
|
|
|
return; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
if ($this->supportsAbove($this->invalidNames[$nextContentLc])) { |
328
|
|
|
$data = array( |
329
|
|
|
$tokens[$nextNonEmpty]['content'], |
330
|
|
|
$this->invalidNames[$nextContentLc], |
331
|
|
|
); |
332
|
|
|
$this->addError($phpcsFile, $stackPtr, $tokens[$nextNonEmpty]['content'], $data); |
333
|
|
|
} |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* Processes this test, when one of its tokens is encountered. |
338
|
|
|
* |
339
|
|
|
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned. |
340
|
|
|
* @param int $stackPtr The position of the current token in the |
341
|
|
|
* stack passed in $tokens. |
342
|
|
|
* @param array $tokens The stack of tokens that make up |
343
|
|
|
* the file. |
344
|
|
|
* |
345
|
|
|
* @return void |
346
|
|
|
*/ |
347
|
|
|
public function processString(File $phpcsFile, $stackPtr, $tokens) |
348
|
|
|
{ |
349
|
|
|
$tokenContentLc = strtolower($tokens[$stackPtr]['content']); |
350
|
|
|
|
351
|
|
|
/* |
352
|
|
|
* Special case for PHP versions where the target is not yet identified as |
353
|
|
|
* its own token, but presents as T_STRING. |
354
|
|
|
* - trait keyword in PHP < 5.4 |
355
|
|
|
*/ |
356
|
|
|
if (version_compare(\PHP_VERSION_ID, '50400', '<') && $tokenContentLc === 'trait') { |
357
|
|
|
$this->processNonString($phpcsFile, $stackPtr, $tokens); |
358
|
|
|
return; |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
// Look for any define/defined tokens (both T_STRING ones, blame Tokenizer). |
362
|
|
|
if ($tokenContentLc !== 'define' && $tokenContentLc !== 'defined') { |
363
|
|
|
return; |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
// Retrieve the define(d) constant name. |
367
|
|
|
$firstParam = $this->getFunctionCallParameter($phpcsFile, $stackPtr, 1); |
368
|
|
|
if ($firstParam === false) { |
369
|
|
|
return; |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
$defineName = $this->stripQuotes($firstParam['raw']); |
373
|
|
|
$defineNameLc = strtolower($defineName); |
374
|
|
|
|
375
|
|
|
if (isset($this->invalidNames[$defineNameLc]) && $this->supportsAbove($this->invalidNames[$defineNameLc])) { |
376
|
|
|
$data = array( |
377
|
|
|
$defineName, |
378
|
|
|
$this->invalidNames[$defineNameLc], |
379
|
|
|
); |
380
|
|
|
$this->addError($phpcsFile, $stackPtr, $defineNameLc, $data); |
381
|
|
|
} |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* Add the error message. |
387
|
|
|
* |
388
|
|
|
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned. |
389
|
|
|
* @param int $stackPtr The position of the current token in the |
390
|
|
|
* stack passed in $tokens. |
391
|
|
|
* @param string $content The token content found. |
392
|
|
|
* @param array $data The data to pass into the error message. |
393
|
|
|
* |
394
|
|
|
* @return void |
395
|
|
|
*/ |
396
|
|
|
protected function addError(File $phpcsFile, $stackPtr, $content, $data) |
397
|
|
|
{ |
398
|
|
|
$error = "Function name, class name, namespace name or constant name can not be reserved keyword '%s' (since version %s)"; |
399
|
|
|
$errorCode = $this->stringToErrorCode($content) . 'Found'; |
400
|
|
|
$phpcsFile->addError($error, $stackPtr, $errorCode, $data); |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
|
404
|
|
|
/** |
405
|
|
|
* Check if the current token code is for a token which can be considered |
406
|
|
|
* the end of a (partial) use statement. |
407
|
|
|
* |
408
|
|
|
* @param int $token The current token information. |
409
|
|
|
* |
410
|
|
|
* @return bool |
411
|
|
|
*/ |
412
|
|
|
protected function isEndOfUseStatement($token) |
413
|
|
|
{ |
414
|
|
|
return \in_array($token['code'], array(\T_CLOSE_CURLY_BRACKET, \T_SEMICOLON, \T_COMMA), true); |
415
|
|
|
} |
416
|
|
|
} |
417
|
|
|
|
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.