ForbiddenNamesSniff::processNonString()   F
last analyzed

Complexity

Conditions 39
Paths 39

Size

Total Lines 143

Duplication

Lines 7
Ratio 4.9 %

Importance

Changes 0
Metric Value
dl 7
loc 143
rs 3.3333
c 0
b 0
f 0
cc 39
nc 39
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
 * Detects the use of reserved keywords as class, function, namespace or constant names.
19
 *
20
 * PHP version All
21
 *
22
 * @link https://www.php.net/manual/en/reserved.keywords.php
23
 *
24
 * @since 5.5
25
 */
26
class ForbiddenNamesSniff extends Sniff
27
{
28
29
    /**
30
     * A list of keywords that can not be used as function, class and namespace name or constant name.
31
     * Mentions since which version it's not allowed.
32
     *
33
     * @since 5.5
34
     *
35
     * @var array(string => string)
36
     */
37
    protected $invalidNames = array(
38
        'abstract'      => '5.0',
39
        'and'           => 'all',
40
        'array'         => 'all',
41
        'as'            => 'all',
42
        'break'         => 'all',
43
        'callable'      => '5.4',
44
        'case'          => 'all',
45
        'catch'         => '5.0',
46
        'class'         => 'all',
47
        'clone'         => '5.0',
48
        'const'         => 'all',
49
        'continue'      => 'all',
50
        'declare'       => 'all',
51
        'default'       => 'all',
52
        'die'           => 'all',
53
        'do'            => 'all',
54
        'echo'          => 'all',
55
        'else'          => 'all',
56
        'elseif'        => 'all',
57
        'empty'         => 'all',
58
        'enddeclare'    => 'all',
59
        'endfor'        => 'all',
60
        'endforeach'    => 'all',
61
        'endif'         => 'all',
62
        'endswitch'     => 'all',
63
        'endwhile'      => 'all',
64
        'eval'          => 'all',
65
        'exit'          => 'all',
66
        'extends'       => 'all',
67
        'final'         => '5.0',
68
        'finally'       => '5.5',
69
        'for'           => 'all',
70
        'foreach'       => 'all',
71
        'function'      => 'all',
72
        'global'        => 'all',
73
        'goto'          => '5.3',
74
        'if'            => 'all',
75
        'implements'    => '5.0',
76
        'include'       => 'all',
77
        'include_once'  => 'all',
78
        'instanceof'    => '5.0',
79
        'insteadof'     => '5.4',
80
        'interface'     => '5.0',
81
        'isset'         => 'all',
82
        'list'          => 'all',
83
        'namespace'     => '5.3',
84
        'new'           => 'all',
85
        'or'            => 'all',
86
        'print'         => 'all',
87
        'private'       => '5.0',
88
        'protected'     => '5.0',
89
        'public'        => '5.0',
90
        'require'       => 'all',
91
        'require_once'  => 'all',
92
        'return'        => 'all',
93
        'static'        => 'all',
94
        'switch'        => 'all',
95
        'throw'         => '5.0',
96
        'trait'         => '5.4',
97
        'try'           => '5.0',
98
        'unset'         => 'all',
99
        'use'           => 'all',
100
        'var'           => 'all',
101
        'while'         => 'all',
102
        'xor'           => 'all',
103
        'yield'         => '5.5',
104
        '__class__'     => 'all',
105
        '__dir__'       => '5.3',
106
        '__file__'      => 'all',
107
        '__function__'  => 'all',
108
        '__method__'    => 'all',
109
        '__namespace__' => '5.3',
110
    );
111
112
    /**
113
     * A list of keywords that can follow use statements.
114
     *
115
     * @since 7.0.1
116
     *
117
     * @var array(string => string)
118
     */
119
    protected $validUseNames = array(
120
        'const'    => true,
121
        'function' => true,
122
    );
123
124
    /**
125
     * Scope modifiers and other keywords allowed in trait use statements.
126
     *
127
     * @since 7.1.4
128
     *
129
     * @var array
130
     */
131
    private $allowedModifiers = array();
132
133
    /**
134
     * Targeted tokens.
135
     *
136
     * @since 5.5
137
     *
138
     * @var array
139
     */
140
    protected $targetedTokens = array(
141
        \T_CLASS,
142
        \T_FUNCTION,
143
        \T_NAMESPACE,
144
        \T_STRING,
145
        \T_CONST,
146
        \T_USE,
147
        \T_AS,
148
        \T_EXTENDS,
149
        \T_INTERFACE,
150
        \T_TRAIT,
151
    );
152
153
    /**
154
     * Returns an array of tokens this test wants to listen for.
155
     *
156
     * @since 5.5
157
     *
158
     * @return array
159
     */
160
    public function register()
161
    {
162
        $this->allowedModifiers           = Tokens::$scopeModifiers;
163
        $this->allowedModifiers[\T_FINAL] = \T_FINAL;
164
165
        $tokens = $this->targetedTokens;
166
167
        if (\defined('T_ANON_CLASS')) {
168
            $tokens[] = \T_ANON_CLASS;
169
        }
170
171
        return $tokens;
172
    }
173
174
    /**
175
     * Processes this test, when one of its tokens is encountered.
176
     *
177
     * @since 5.5
178
     *
179
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
180
     * @param int                   $stackPtr  The position of the current token in the
181
     *                                         stack passed in $tokens.
182
     *
183
     * @return void
184
     */
185
    public function process(File $phpcsFile, $stackPtr)
186
    {
187
        $tokens = $phpcsFile->getTokens();
188
189
        /*
190
         * We distinguish between the class, function and namespace names vs the define statements.
191
         */
192
        if ($tokens[$stackPtr]['type'] === 'T_STRING') {
193
            $this->processString($phpcsFile, $stackPtr, $tokens);
194
        } else {
195
            $this->processNonString($phpcsFile, $stackPtr, $tokens);
196
        }
197
    }
198
199
    /**
200
     * Processes this test, when one of its tokens is encountered.
201
     *
202
     * @since 5.5
203
     *
204
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
205
     * @param int                   $stackPtr  The position of the current token in the
206
     *                                         stack passed in $tokens.
207
     * @param array                 $tokens    The stack of tokens that make up
208
     *                                         the file.
209
     *
210
     * @return void
211
     */
212
    public function processNonString(File $phpcsFile, $stackPtr, $tokens)
213
    {
214
        $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
215
        if ($nextNonEmpty === false) {
216
            return;
217
        }
218
219
        /*
220
         * Deal with anonymous classes - `class` before a reserved keyword is sometimes
221
         * misidentified as `T_ANON_CLASS`.
222
         * In PHPCS < 2.3.4 these were tokenized as T_CLASS no matter what.
223
         */
224
        if ($tokens[$stackPtr]['type'] === 'T_ANON_CLASS' || $tokens[$stackPtr]['type'] === 'T_CLASS') {
225
            $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
226
            if ($prevNonEmpty !== false && $tokens[$prevNonEmpty]['type'] === 'T_NEW') {
227
                return;
228
            }
229
        }
230
231
        /*
232
         * PHP 5.6 allows for use const and use function, but only if followed by the function/constant name.
233
         * - `use function HelloWorld` => move to the next token (HelloWorld) to verify.
234
         * - `use const HelloWorld` => move to the next token (HelloWorld) to verify.
235
         */
236
        elseif ($tokens[$stackPtr]['type'] === 'T_USE'
237
            && isset($this->validUseNames[strtolower($tokens[$nextNonEmpty]['content'])]) === true
238
        ) {
239
            $maybeUseNext = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true, null, true);
240
            if ($maybeUseNext !== false && $this->isEndOfUseStatement($tokens[$maybeUseNext]) === false) {
241
                $nextNonEmpty = $maybeUseNext;
242
            }
243
        }
244
245
        /*
246
         * Deal with visibility modifiers.
247
         * - `use HelloWorld { sayHello as protected; }` => valid, bow out.
248
         * - `use HelloWorld { sayHello as private myPrivateHello; }` => move to the next token to verify.
249
         */
250
        elseif ($tokens[$stackPtr]['type'] === 'T_AS'
251
            && isset($this->allowedModifiers[$tokens[$nextNonEmpty]['code']]) === true
252
            && $phpcsFile->hasCondition($stackPtr, \T_USE) === true
253
        ) {
254
            $maybeUseNext = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true, null, true);
255
            if ($maybeUseNext === false || $this->isEndOfUseStatement($tokens[$maybeUseNext]) === true) {
256
                return;
257
            }
258
259
            $nextNonEmpty = $maybeUseNext;
260
        }
261
262
        /*
263
         * Deal with foreach ( ... as list() ).
264
         */
265
        elseif ($tokens[$stackPtr]['type'] === 'T_AS'
266
            && isset($tokens[$stackPtr]['nested_parenthesis']) === true
267
            && $tokens[$nextNonEmpty]['code'] === \T_LIST
268
        ) {
269
            $parentheses = array_reverse($tokens[$stackPtr]['nested_parenthesis'], true);
270 View Code Duplication
            foreach ($parentheses as $open => $close) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
271
                if (isset($tokens[$open]['parenthesis_owner'])
272
                    && $tokens[$tokens[$open]['parenthesis_owner']]['code'] === \T_FOREACH
273
                ) {
274
                    return;
275
                }
276
            }
277
        }
278
279
        /*
280
         * Deal with functions declared to return by reference.
281
         */
282
        elseif ($tokens[$stackPtr]['type'] === 'T_FUNCTION'
283
            && $tokens[$nextNonEmpty]['type'] === 'T_BITWISE_AND'
284
        ) {
285
            $maybeUseNext = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true, null, true);
286
            if ($maybeUseNext === false) {
287
                // Live coding.
288
                return;
289
            }
290
291
            $nextNonEmpty = $maybeUseNext;
292
        }
293
294
        /*
295
         * Deal with nested namespaces.
296
         */
297
        elseif ($tokens[$stackPtr]['type'] === 'T_NAMESPACE') {
298
            if ($tokens[$stackPtr + 1]['code'] === \T_NS_SEPARATOR) {
299
                // Not a namespace declaration, but use of, i.e. `namespace\someFunction();`.
300
                return;
301
            }
302
303
            $endToken      = $phpcsFile->findNext(array(\T_SEMICOLON, \T_OPEN_CURLY_BRACKET), ($stackPtr + 1), null, false, null, true);
304
            $namespaceName = trim($phpcsFile->getTokensAsString(($stackPtr + 1), ($endToken - $stackPtr - 1)));
305
            if (empty($namespaceName) === true) {
306
                return;
307
            }
308
309
            $namespaceParts = explode('\\', $namespaceName);
310
            foreach ($namespaceParts as $namespacePart) {
311
                $partLc = strtolower($namespacePart);
312
                if (isset($this->invalidNames[$partLc]) === false) {
313
                    continue;
314
                }
315
316
                // Find the token position of the part which matched.
317
                for ($i = ($stackPtr + 1); $i < $endToken; $i++) {
318
                    if ($tokens[$i]['content'] === $namespacePart) {
319
                        $nextNonEmpty = $i;
320
                        break;
321
                    }
322
                }
323
            }
324
            unset($i, $namespacePart, $partLc);
325
        }
326
327
        $nextContentLc = strtolower($tokens[$nextNonEmpty]['content']);
328
        if (isset($this->invalidNames[$nextContentLc]) === false) {
329
            return;
330
        }
331
332
        /*
333
         * Deal with PHP 7 relaxing the rules.
334
         * "As of PHP 7.0.0 these keywords are allowed as property, constant, and method names
335
         * of classes, interfaces and traits, except that class may not be used as constant name."
336
         */
337
        if ((($tokens[$stackPtr]['type'] === 'T_FUNCTION'
338
                && $this->inClassScope($phpcsFile, $stackPtr, false) === true)
339
            || ($tokens[$stackPtr]['type'] === 'T_CONST'
340
                && $this->isClassConstant($phpcsFile, $stackPtr) === true
341
                && $nextContentLc !== 'class'))
342
            && $this->supportsBelow('5.6') === false
343
        ) {
344
            return;
345
        }
346
347
        if ($this->supportsAbove($this->invalidNames[$nextContentLc])) {
348
            $data = array(
349
                $tokens[$nextNonEmpty]['content'],
350
                $this->invalidNames[$nextContentLc],
351
            );
352
            $this->addError($phpcsFile, $stackPtr, $tokens[$nextNonEmpty]['content'], $data);
353
        }
354
    }
355
356
    /**
357
     * Processes this test, when one of its tokens is encountered.
358
     *
359
     * @since 5.5
360
     *
361
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
362
     * @param int                   $stackPtr  The position of the current token in the
363
     *                                         stack passed in $tokens.
364
     * @param array                 $tokens    The stack of tokens that make up
365
     *                                         the file.
366
     *
367
     * @return void
368
     */
369
    public function processString(File $phpcsFile, $stackPtr, $tokens)
370
    {
371
        $tokenContentLc = strtolower($tokens[$stackPtr]['content']);
372
373
        /*
374
         * Special case for PHP versions where the target is not yet identified as
375
         * its own token, but presents as T_STRING.
376
         * - trait keyword in PHP < 5.4
377
         */
378
        if (version_compare(\PHP_VERSION_ID, '50400', '<') && $tokenContentLc === 'trait') {
379
            $this->processNonString($phpcsFile, $stackPtr, $tokens);
380
            return;
381
        }
382
383
        // Look for any define/defined tokens (both T_STRING ones, blame Tokenizer).
384
        if ($tokenContentLc !== 'define' && $tokenContentLc !== 'defined') {
385
            return;
386
        }
387
388
        // Retrieve the define(d) constant name.
389
        $firstParam = $this->getFunctionCallParameter($phpcsFile, $stackPtr, 1);
390
        if ($firstParam === false) {
391
            return;
392
        }
393
394
        $defineName   = $this->stripQuotes($firstParam['raw']);
395
        $defineNameLc = strtolower($defineName);
396
397
        if (isset($this->invalidNames[$defineNameLc]) && $this->supportsAbove($this->invalidNames[$defineNameLc])) {
398
            $data = array(
399
                $defineName,
400
                $this->invalidNames[$defineNameLc],
401
            );
402
            $this->addError($phpcsFile, $stackPtr, $defineNameLc, $data);
403
        }
404
    }
405
406
407
    /**
408
     * Add the error message.
409
     *
410
     * @since 7.1.0
411
     *
412
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
413
     * @param int                   $stackPtr  The position of the current token in the
414
     *                                         stack passed in $tokens.
415
     * @param string                $content   The token content found.
416
     * @param array                 $data      The data to pass into the error message.
417
     *
418
     * @return void
419
     */
420
    protected function addError(File $phpcsFile, $stackPtr, $content, $data)
421
    {
422
        $error     = "Function name, class name, namespace name or constant name can not be reserved keyword '%s' (since version %s)";
423
        $errorCode = $this->stringToErrorCode($content) . 'Found';
424
        $phpcsFile->addError($error, $stackPtr, $errorCode, $data);
425
    }
426
427
428
    /**
429
     * Check if the current token code is for a token which can be considered
430
     * the end of a (partial) use statement.
431
     *
432
     * @since 7.0.8
433
     *
434
     * @param int $token The current token information.
435
     *
436
     * @return bool
437
     */
438
    protected function isEndOfUseStatement($token)
439
    {
440
        return \in_array($token['code'], array(\T_CLOSE_CURLY_BRACKET, \T_SEMICOLON, \T_COMMA), true);
441
    }
442
}
443