Completed
Push — feature/remove-php52-work-arou... ( 4600c9 )
by Juliette
05:06 queued 03:21
created

ForbiddenNamesSniff::processString()   C

Complexity

Conditions 8
Paths 5

Size

Total Lines 37
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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