Completed
Push — master ( 999934...1f3765 )
by Juliette
12s queued 10s
created

ForbiddenNamesSniff::addError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 4
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) {
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...
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