NewConstantScalarExpressionsSniff::isStaticValue()   F
last analyzed

Complexity

Conditions 36
Paths 82

Size

Total Lines 138

Duplication

Lines 12
Ratio 8.7 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 12
loc 138
rs 3.3333
cc 36
nc 82
nop 5

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\Sniffs\PHP\NewConstantScalarExpressionsSniff.
4
 *
5
 * PHP version 5.6
6
 *
7
 * @category PHP
8
 * @package  PHPCompatibility
9
 * @author   Juliette Reinders Folmer <[email protected]>
10
 */
11
12
namespace PHPCompatibility\Sniffs\PHP;
13
14
use PHPCompatibility\Sniff;
15
use PHPCompatibility\PHPCSHelper;
16
17
/**
18
 * \PHPCompatibility\Sniffs\PHP\NewConstantScalarExpressionsSniff.
19
 *
20
 * Since PHP 5.6, it is now possible to provide a scalar expression involving
21
 * numeric and string literals and/or constants in contexts where PHP previously
22
 * expected a static value, such as constant and property declarations and
23
 * default function arguments.
24
 *
25
 * PHP version 5.6
26
 *
27
 * @category PHP
28
 * @package  PHPCompatibility
29
 * @author   Juliette Reinders Folmer <[email protected]>
30
 */
31
class NewConstantScalarExpressionsSniff extends Sniff
32
{
33
34
    /**
35
     * Error message.
36
     *
37
     * @var string
38
     */
39
    const ERROR_PHRASE = 'Constant scalar expressions are not allowed %s in PHP 5.5 or earlier.';
40
41
    /**
42
     * Partial error phrases to be used in combination with the error message constant.
43
     *
44
     * @var array
45
     */
46
    protected $errorPhrases = array(
47
        'const'     => 'when defining constants using the const keyword',
48
        'property'  => 'in property declarations',
49
        'staticvar' => 'in static variable declarations',
50
        'default'   => 'in default function arguments',
51
    );
52
53
    /**
54
     * Tokens which were allowed to be used in these declarations prior to PHP 5.6.
55
     *
56
     * This list will be enriched in the setProperties() method.
57
     *
58
     * @var array
59
     */
60
    protected $safeOperands = array(
61
        T_LNUMBER                  => T_LNUMBER,
62
        T_DNUMBER                  => T_DNUMBER,
63
        T_CONSTANT_ENCAPSED_STRING => T_CONSTANT_ENCAPSED_STRING,
64
        T_TRUE                     => T_TRUE,
65
        T_FALSE                    => T_FALSE,
66
        T_NULL                     => T_NULL,
67
68
        T_LINE                     => T_LINE,
69
        T_FILE                     => T_FILE,
70
        T_DIR                      => T_DIR,
71
        T_FUNC_C                   => T_FUNC_C,
72
        T_CLASS_C                  => T_CLASS_C,
73
        T_METHOD_C                 => T_METHOD_C,
74
        T_NS_C                     => T_NS_C,
75
76
        // Special cases:
77
        T_NS_SEPARATOR             => T_NS_SEPARATOR,
78
        /*
79
         * This can be neigh anything, but for any usage except constants,
80
         * the T_STRING will be combined with non-allowed tokens, so we should be good.
81
         */
82
        T_STRING                   => T_STRING,
83
    );
84
85
86
    /**
87
     * Returns an array of tokens this test wants to listen for.
88
     *
89
     * @return array
90
     */
91
    public function register()
92
    {
93
        // Set the properties up only once.
94
        $this->setProperties();
95
96
        return array(
97
            T_CONST,
98
            T_VARIABLE,
99
            T_FUNCTION,
100
            T_CLOSURE,
101
            T_STATIC,
102
        );
103
    }
104
105
106
    /**
107
     * Make some adjustments to the $safeOperands property.
108
     *
109
     * @return void
110
     */
111
    public function setProperties()
112
    {
113
        /*
114
         * Not available on PHPCS 1.x icw PHP 5.3.
115
         * Not a problem for recognition as when the token is not available
116
         * __TRAIT__ will be tokenized as T_STRING which will pass.
117
         */
118
        if (defined('T_TRAIT_C') === true) {
119
            // phpcs:ignore PHPCompatibility.PHP.NewConstants.t_trait_cFound
120
            $this->safeOperands[T_TRAIT_C] = T_TRAIT_C;
121
        }
122
123
        $emptyTokens   = \PHP_CodeSniffer_Tokens::$emptyTokens;
124
        $heredocTokens = \PHP_CodeSniffer_Tokens::$heredocTokens;
125
        if (version_compare(PHPCSHelper::getVersion(), '2.0', '<')) {
126
            // PHPCS 1.x compat.
127
            $emptyTokens   = array_combine($emptyTokens, $emptyTokens);
128
            $heredocTokens = array_combine($heredocTokens, $heredocTokens);
129
        }
130
131
        $this->safeOperands = $this->safeOperands + $heredocTokens + $emptyTokens;
132
    }
133
134
135
    /**
136
     * Do a version check to determine if this sniff needs to run at all.
137
     *
138
     * @return bool
139
     */
140
    protected function bowOutEarly()
141
    {
142
        return ($this->supportsBelow('5.5') !== true);
143
    }
144
145
146
    /**
147
     * Processes this test, when one of its tokens is encountered.
148
     *
149
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
150
     * @param int                   $stackPtr  The position of the current token in the
151
     *                                         stack passed in $tokens.
152
     *
153
     * @return void|int Null or integer stack pointer to skip forward.
154
     */
155
    public function process(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
156
    {
157
        if ($this->bowOutEarly() === true) {
158
            return;
159
        }
160
161
        $tokens = $phpcsFile->getTokens();
162
163
        switch ($tokens[$stackPtr]['type']) {
164
            case 'T_FUNCTION':
165
            case 'T_CLOSURE':
166
                $params = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
167
                if (empty($params)) {
168
                    // No parameters.
169
                    return;
170
                }
171
172
                $funcToken = $tokens[$stackPtr];
173
174
                if (isset($funcToken['parenthesis_owner'], $funcToken['parenthesis_opener'], $funcToken['parenthesis_closer']) === false
175
                    || $funcToken['parenthesis_owner'] !== $stackPtr
176
                    || isset($tokens[$funcToken['parenthesis_opener']], $tokens[$funcToken['parenthesis_closer']]) === false
177
                ) {
178
                    // Hmm.. something is going wrong as these should all be available & valid.
179
                    return;
180
                }
181
182
                $opener = $funcToken['parenthesis_opener'];
183
                $closer = $funcToken['parenthesis_closer'];
184
185
                // Which nesting level is the one we are interested in ?
186
                $nestedParenthesisCount = 1;
187 View Code Duplication
                if (isset($tokens[$opener]['nested_parenthesis'])) {
188
                    $nestedParenthesisCount += count($tokens[$opener]['nested_parenthesis']);
189
                }
190
191
                foreach ($params as $param) {
192
                    if (isset($param['default']) === false) {
193
                        continue;
194
                    }
195
196
                    $end = $param['token'];
197
                    while (($end = $phpcsFile->findNext(array(T_COMMA, T_CLOSE_PARENTHESIS), ($end + 1), ($closer + 1))) !== false) {
198
                        $maybeSkipTo = $this->isRealEndOfDeclaration($tokens, $end, $nestedParenthesisCount);
199
                        if ($maybeSkipTo !== true) {
200
                            $end = $maybeSkipTo;
201
                            continue;
202
                        }
203
204
                        // Ignore closing parenthesis/bracket if not 'ours'.
205
                        if ($tokens[$end]['code'] === T_CLOSE_PARENTHESIS && $end !== $closer) {
206
                            continue;
207
                        }
208
209
                        // Ok, we've found the end of the param default value declaration.
210
                        break;
211
                    }
212
213
                    if ($this->isValidAssignment($phpcsFile, $param['token'], $end) === false) {
214
                        $this->throwError($phpcsFile, $param['token'], 'default', $param['content']);
215
                    }
216
                }
217
218
                /*
219
                 * No need for the sniff to be triggered by the T_VARIABLEs in the function
220
                 * definition as we've already examined them above, so let's skip over them.
221
                 */
222
                return $closer;
223
224
            case 'T_VARIABLE':
225
            case 'T_STATIC':
226
            case 'T_CONST':
227
                $type = 'const';
228
229
                // Filter out non-property declarations.
230
                if ($tokens[$stackPtr]['code'] === T_VARIABLE) {
231
                    if ($this->isClassProperty($phpcsFile, $stackPtr) === false) {
232
                        return;
233
                    }
234
235
                    $type = 'property';
236
237
                    // Move back one token to have the same starting point as the others.
238
                    $stackPtr = ($stackPtr - 1);
239
                }
240
241
                // Filter out late static binding and class properties.
242
                if ($tokens[$stackPtr]['code'] === T_STATIC) {
243
                    $next = $phpcsFile->findNext(
244
                        \PHP_CodeSniffer_Tokens::$emptyTokens,
245
                        ($stackPtr + 1),
246
                        null,
247
                        true,
248
                        null,
249
                        true
250
                    );
251
                    if ($next === false || $tokens[$next]['code'] !== T_VARIABLE) {
252
                        // Late static binding.
253
                        return;
254
                    }
255
256
                    if ($this->isClassProperty($phpcsFile, $next) === true) {
257
                        // Class properties are examined based on the T_VARIABLE token.
258
                        return;
259
                    }
260
                    unset($next);
261
262
                    $type = 'staticvar';
263
                }
264
265
                $endOfStatement = $phpcsFile->findNext(array(T_SEMICOLON, T_CLOSE_TAG), ($stackPtr + 1));
266
                if ($endOfStatement === false) {
267
                    // No semi-colon - live coding.
268
                    return;
269
                }
270
271
                $targetNestingLevel = 0;
272
                if (isset($tokens[$stackPtr]['nested_parenthesis']) == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
273
                    $targetNestingLevel = count($tokens[$stackPtr]['nested_parenthesis']);
274
                }
275
276
                // Examine each variable/constant in multi-declarations.
277
                $start = $stackPtr;
278
                $end   = $stackPtr;
279
                while (($end = $phpcsFile->findNext(array(T_COMMA, T_SEMICOLON, T_OPEN_SHORT_ARRAY, T_CLOSE_TAG), ($end + 1), ($endOfStatement + 1))) !== false) {
280
281
                    $maybeSkipTo = $this->isRealEndOfDeclaration($tokens, $end, $targetNestingLevel);
282
                    if ($maybeSkipTo !== true) {
283
                        $end = $maybeSkipTo;
284
                        continue;
285
                    }
286
287
                    $start = $phpcsFile->findNext(\PHP_CodeSniffer_Tokens::$emptyTokens, ($start + 1), $end, true);
288
                    if ($start === false
289
                        || ($tokens[$stackPtr]['code'] === T_CONST && $tokens[$start]['code'] !== T_STRING)
290
                        || ($tokens[$stackPtr]['code'] !== T_CONST && $tokens[$start]['code'] !== T_VARIABLE)
291
                    ) {
292
                        // Shouldn't be possible.
293
                        continue;
294
                    }
295
296
                    if ($this->isValidAssignment($phpcsFile, $start, $end) === false) {
297
                        // Create the "found" snippet.
298
                        $content    = '';
299
                        $tokenCount = ($end - $start);
300
                        if ($tokenCount < 20) {
301
                            // Prevent large arrays from being added to the error message.
302
                            $content = $phpcsFile->getTokensAsString($start, ($tokenCount + 1));
303
                        }
304
305
                        $this->throwError($phpcsFile, $start, $type, $content);
306
                    }
307
308
                    $start = $end;
309
                }
310
311
                // Skip to the end of the statement to prevent duplicate messages for multi-declarations.
312
                return $endOfStatement;
313
        }
314
    }
315
316
317
    /**
318
     * Is a value declared and is the value declared valid pre-PHP 5.6 ?
319
     *
320
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
321
     * @param int                   $stackPtr  The position of the current token in the
322
     *                                         stack passed in $tokens.
323
     * @param int                   $end       The end of the value definition.
324
     *                                         This will normally be a comma or semi-colon.
325
     *
326
     * @return bool
327
     */
328
    protected function isValidAssignment(\PHP_CodeSniffer_File $phpcsFile, $stackPtr, $end)
329
    {
330
        $tokens = $phpcsFile->getTokens();
331
        $next   = $phpcsFile->findNext(\PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr + 1), $end, true);
332 View Code Duplication
        if ($next === false || $tokens[$next]['code'] !== T_EQUAL) {
333
            // No value assigned.
334
            return true;
335
        }
336
337
        return $this->isStaticValue($phpcsFile, $tokens, ($next + 1), ($end - 1));
338
    }
339
340
341
    /**
342
     * Is a value declared and is the value declared constant as accepted in PHP 5.5 and lower ?
343
     *
344
     * @param \PHP_CodeSniffer_File $phpcsFile    The file being scanned.
345
     * @param array                 $tokens       The token stack of the current file.
346
     * @param int                   $start        The stackPtr from which to start examining.
347
     * @param int                   $end          The end of the value definition (inclusive),
348
     *                                            i.e. this token will be examined as part of
349
     *                                            the snippet.
350
     * @param bool                  $nestedArrays Optional. Array nesting level when examining
351
     *                                            the content of an array.
352
     *
353
     * @return bool
354
     */
355
    protected function isStaticValue(\PHP_CodeSniffer_File $phpcsFile, $tokens, $start, $end, $nestedArrays = 0)
356
    {
357
        $nextNonSimple = $phpcsFile->findNext($this->safeOperands, $start, ($end + 1), true);
358
        if ($nextNonSimple === false) {
359
            return true;
360
        }
361
362
        /*
363
         * OK, so we have at least one token which needs extra examination.
364
         */
365
        switch ($tokens[$nextNonSimple]['code']) {
366
            case T_MINUS:
367
            case T_PLUS:
368
                if ($this->isNumber($phpcsFile, $start, $end, true) !== false) {
369
                    // Int or float with sign.
370
                    return true;
371
                }
372
373
                return false;
374
375
            case T_NAMESPACE:
376
            case T_PARENT:
377
            case T_SELF:
378
            case T_DOUBLE_COLON:
379
                $nextNonEmpty = $phpcsFile->findNext(
380
                    \PHP_CodeSniffer_Tokens::$emptyTokens,
381
                    ($nextNonSimple + 1),
382
                    ($end + 1),
383
                    true
384
                );
385
386
                if ($tokens[$nextNonSimple]['code'] === T_NAMESPACE) {
387
                    // Allow only `namespace\...`.
388
                    if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== T_NS_SEPARATOR) {
389
                        return false;
390
                    }
391
                } elseif ($tokens[$nextNonSimple]['code'] === T_PARENT
392
                    || $tokens[$nextNonSimple]['code'] === T_SELF
393
                ) {
394
                    // Allow only `parent::` and `self::`.
395 View Code Duplication
                    if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== T_DOUBLE_COLON) {
396
                        return false;
397
                    }
398
                } elseif ($tokens[$nextNonSimple]['code'] === T_DOUBLE_COLON) {
399
                    // Allow only `T_STRING::T_STRING`.
400
                    if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== T_STRING) {
401
                        return false;
402
                    }
403
404
                    $prevNonEmpty = $phpcsFile->findPrevious(\PHP_CodeSniffer_Tokens::$emptyTokens, ($nextNonSimple - 1), null, true);
405
                    // No need to worry about parent/self, that's handled above and
406
                    // the double colon is skipped over in that case.
407
                    if ($prevNonEmpty === false || $tokens[$prevNonEmpty]['code'] !== T_STRING) {
408
                        return false;
409
                    }
410
                }
411
412
                // Examine what comes after the namespace/parent/self/double colon, if anything.
413
                return $this->isStaticValue($phpcsFile, $tokens, ($nextNonEmpty + 1), $end, $nestedArrays);
414
415
            case T_ARRAY:
416
            case T_OPEN_SHORT_ARRAY:
417
                ++$nestedArrays;
418
419
                $arrayItems = $this->getFunctionCallParameters($phpcsFile, $nextNonSimple);
420
                if (empty($arrayItems) === false) {
421
                    foreach ($arrayItems as $item) {
422
                        // Check for a double arrow, but only if it's for this array item, not for a nested array.
423
                        $doubleArrow = false;
424
425
                        $maybeDoubleArrow = $phpcsFile->findNext(
426
                            array(T_DOUBLE_ARROW, T_ARRAY, T_OPEN_SHORT_ARRAY),
427
                            $item['start'],
428
                            ($item['end'] + 1)
429
                        );
430
                        if ($maybeDoubleArrow !== false && $tokens[$maybeDoubleArrow]['code'] === T_DOUBLE_ARROW) {
431
                            // Double arrow is for this nesting level.
432
                            $doubleArrow = $maybeDoubleArrow;
433
                        }
434
435
                        if ($doubleArrow === false) {
436 View Code Duplication
                            if ($this->isStaticValue($phpcsFile, $tokens, $item['start'], $item['end'], $nestedArrays) === false) {
437
                                return false;
438
                            }
439
440
                        } else {
441
                            // Examine array key.
442 View Code Duplication
                            if ($this->isStaticValue($phpcsFile, $tokens, $item['start'], ($doubleArrow - 1), $nestedArrays) === false) {
443
                                return false;
444
                            }
445
446
                            // Examine array value.
447 View Code Duplication
                            if ($this->isStaticValue($phpcsFile, $tokens, ($doubleArrow + 1), $item['end'], $nestedArrays) === false) {
448
                                return false;
449
                            }
450
                        }
451
                    }
452
                }
453
454
                --$nestedArrays;
455
456
                /*
457
                 * Find the end of the array.
458
                 * We already know we will have a valid closer as otherwise we wouldn't have been
459
                 * able to get the array items.
460
                 */
461
                $closer = ($nextNonSimple + 1);
462
                if ($tokens[$nextNonSimple]['code'] === T_OPEN_SHORT_ARRAY
463
                    && isset($tokens[$nextNonSimple]['bracket_closer']) === true
464
                ) {
465
                    $closer = $tokens[$nextNonSimple]['bracket_closer'];
466
                } else {
467
                    $maybeOpener = $phpcsFile->findNext(
468
                        \PHP_CodeSniffer_Tokens::$emptyTokens,
469
                        ($nextNonSimple + 1),
470
                        ($end + 1),
471
                        true
472
                    );
473
                    if ($tokens[$maybeOpener]['code'] === T_OPEN_PARENTHESIS) {
474
                        $opener = $maybeOpener;
475
                        if (isset($tokens[$opener]['parenthesis_closer']) === true) {
476
                            $closer = $tokens[$opener]['parenthesis_closer'];
477
                        }
478
                    }
479
                }
480
481
                if ($closer === $end) {
482
                    return true;
483
                }
484
485
                // Examine what comes after the array, if anything.
486
                return $this->isStaticValue($phpcsFile, $tokens, ($closer + 1), $end, $nestedArrays);
487
488
        }
489
490
        // Ok, so this unsafe token was not one of the exceptions, i.e. this is a PHP 5.6+ syntax.
491
        return false;
492
    }
493
494
495
    /**
496
     * Throw an error if a scalar expression is found.
497
     *
498
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
499
     * @param int                   $stackPtr  The position of the token to link the error to.
500
     * @param string                $type      Type of usage found.
501
     * @param string                $content   Optional. The value for the declaration as found.
502
     *
503
     * @return void
504
     */
505
    protected function throwError(\PHP_CodeSniffer_File $phpcsFile, $stackPtr, $type, $content = '')
506
    {
507
        $error     = static::ERROR_PHRASE;
508
        $phrase    = '';
509
        $errorCode = 'Found';
510
511
        if (isset($this->errorPhrases[$type]) === true) {
512
            $errorCode = $this->stringToErrorCode($type) . 'Found';
513
            $phrase    = $this->errorPhrases[$type];
514
        }
515
516
        $data = array($phrase);
517
518
        if (empty($content) === false) {
519
            $error .= ' Found: %s';
520
            $data[] = $content;
521
        }
522
523
        $phpcsFile->addError($error, $stackPtr, $errorCode, $data);
524
    }
525
526
527
    /**
528
     * Helper function to find the end of multi variable/constant declarations.
529
     *
530
     * Checks whether a certain part of a declaration needs to be skipped over or
531
     * if it is the real end of the declaration.
532
     *
533
     * @param array $tokens      Token stack of the current file.
534
     * @param int   $endPtr      The token to examine as a candidate end pointer.
535
     * @param int   $targetLevel Target nesting level.
536
     *
537
     * @return bool|int True if this is the real end. Int stackPtr to skip to if not.
538
     */
539
    private function isRealEndOfDeclaration($tokens, $endPtr, $targetLevel)
540
    {
541
        // Ignore anything within short array definition brackets for now.
542 View Code Duplication
        if ($tokens[$endPtr]['code'] === T_OPEN_SHORT_ARRAY
543
            && (isset($tokens[$endPtr]['bracket_opener'])
544
                && $tokens[$endPtr]['bracket_opener'] === $endPtr)
545
            && isset($tokens[$endPtr]['bracket_closer'])
546
        ) {
547
            // Skip forward to the end of the short array definition.
548
            return $tokens[$endPtr]['bracket_closer'];
549
        }
550
551
        // Skip past comma's at a lower nesting level.
552
        if ($tokens[$endPtr]['code'] === T_COMMA) {
553
            // Check if a comma is at the nesting level we're targetting.
554
            $nestingLevel = 0;
555 View Code Duplication
            if (isset($tokens[$endPtr]['nested_parenthesis']) == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
556
                $nestingLevel = count($tokens[$endPtr]['nested_parenthesis']);
557
            }
558
            if ($nestingLevel > $targetLevel) {
559
                return $endPtr;
560
            }
561
        }
562
563
        return true;
564
    }
565
}
566