NewConstantScalarExpressionsSniff::isStaticValue()   F
last analyzed

Complexity

Conditions 36
Paths 82

Size

Total Lines 128

Duplication

Lines 12
Ratio 9.38 %

Importance

Changes 0
Metric Value
dl 12
loc 128
rs 3.3333
c 0
b 0
f 0
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, 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\InitialValue;
12
13
use PHPCompatibility\Sniff;
14
use PHPCompatibility\PHPCSHelper;
15
use PHP_CodeSniffer_File as File;
16
use PHP_CodeSniffer_Tokens as Tokens;
17
18
/**
19
 * Detect constant scalar expressions being used to set an initial value.
20
 *
21
 * Since PHP 5.6, it is now possible to provide a scalar expression involving
22
 * numeric and string literals and/or constants in contexts where PHP previously
23
 * expected a static value, such as constant and property declarations and
24
 * default values for function parameters.
25
 *
26
 * PHP version 5.6
27
 *
28
 * @link https://www.php.net/manual/en/migration56.new-features.php#migration56.new-features.const-scalar-exprs
29
 * @link https://wiki.php.net/rfc/const_scalar_exprs
30
 *
31
 * @since 8.2.0
32
 */
33
class NewConstantScalarExpressionsSniff extends Sniff
34
{
35
36
    /**
37
     * Error message.
38
     *
39
     * @since 8.2.0
40
     *
41
     * @var string
42
     */
43
    const ERROR_PHRASE = 'Constant scalar expressions are not allowed %s in PHP 5.5 or earlier.';
44
45
    /**
46
     * Partial error phrases to be used in combination with the error message constant.
47
     *
48
     * @since 8.2.0
49
     *
50
     * @var array
51
     */
52
    protected $errorPhrases = array(
53
        'const'     => 'when defining constants using the const keyword',
54
        'property'  => 'in property declarations',
55
        'staticvar' => 'in static variable declarations',
56
        'default'   => 'in default function arguments',
57
    );
58
59
    /**
60
     * Tokens which were allowed to be used in these declarations prior to PHP 5.6.
61
     *
62
     * This list will be enriched in the setProperties() method.
63
     *
64
     * @since 8.2.0
65
     *
66
     * @var array
67
     */
68
    protected $safeOperands = array(
69
        \T_LNUMBER                  => \T_LNUMBER,
70
        \T_DNUMBER                  => \T_DNUMBER,
71
        \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING,
72
        \T_TRUE                     => \T_TRUE,
73
        \T_FALSE                    => \T_FALSE,
74
        \T_NULL                     => \T_NULL,
75
76
        \T_LINE                     => \T_LINE,
77
        \T_FILE                     => \T_FILE,
78
        \T_DIR                      => \T_DIR,
79
        \T_FUNC_C                   => \T_FUNC_C,
80
        \T_CLASS_C                  => \T_CLASS_C,
81
        \T_TRAIT_C                  => \T_TRAIT_C,
82
        \T_METHOD_C                 => \T_METHOD_C,
83
        \T_NS_C                     => \T_NS_C,
84
85
        // Special cases:
86
        \T_NS_SEPARATOR             => \T_NS_SEPARATOR,
87
        /*
88
         * This can be neigh anything, but for any usage except constants,
89
         * the T_STRING will be combined with non-allowed tokens, so we should be good.
90
         */
91
        \T_STRING                   => \T_STRING,
92
    );
93
94
95
    /**
96
     * Returns an array of tokens this test wants to listen for.
97
     *
98
     * @since 8.2.0
99
     *
100
     * @return array
101
     */
102
    public function register()
103
    {
104
        // Set the properties up only once.
105
        $this->setProperties();
106
107
        return array(
108
            \T_CONST,
109
            \T_VARIABLE,
110
            \T_FUNCTION,
111
            \T_CLOSURE,
112
            \T_STATIC,
113
        );
114
    }
115
116
117
    /**
118
     * Make some adjustments to the $safeOperands property.
119
     *
120
     * @since 8.2.0
121
     *
122
     * @return void
123
     */
124
    public function setProperties()
125
    {
126
        $this->safeOperands += Tokens::$heredocTokens;
127
        $this->safeOperands += Tokens::$emptyTokens;
128
    }
129
130
131
    /**
132
     * Do a version check to determine if this sniff needs to run at all.
133
     *
134
     * @since 8.2.0
135
     *
136
     * @return bool
137
     */
138
    protected function bowOutEarly()
139
    {
140
        return ($this->supportsBelow('5.5') !== true);
141
    }
142
143
144
    /**
145
     * Processes this test, when one of its tokens is encountered.
146
     *
147
     * @since 8.2.0
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(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'])) {
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...
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(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true);
244
                    if ($next === false || $tokens[$next]['code'] !== \T_VARIABLE) {
245
                        // Late static binding.
246
                        return;
247
                    }
248
249
                    if ($this->isClassProperty($phpcsFile, $next) === true) {
250
                        // Class properties are examined based on the T_VARIABLE token.
251
                        return;
252
                    }
253
                    unset($next);
254
255
                    $type = 'staticvar';
256
                }
257
258
                $endOfStatement = $phpcsFile->findNext(array(\T_SEMICOLON, \T_CLOSE_TAG), ($stackPtr + 1));
259
                if ($endOfStatement === false) {
260
                    // No semi-colon - live coding.
261
                    return;
262
                }
263
264
                $targetNestingLevel = 0;
265 View Code Duplication
                if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
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...
266
                    $targetNestingLevel = \count($tokens[$stackPtr]['nested_parenthesis']);
267
                }
268
269
                // Examine each variable/constant in multi-declarations.
270
                $start = $stackPtr;
271
                $end   = $stackPtr;
272
                while (($end = $phpcsFile->findNext(array(\T_COMMA, \T_SEMICOLON, \T_OPEN_SHORT_ARRAY, \T_CLOSE_TAG), ($end + 1), ($endOfStatement + 1))) !== false) {
273
274
                    $maybeSkipTo = $this->isRealEndOfDeclaration($tokens, $end, $targetNestingLevel);
275
                    if ($maybeSkipTo !== true) {
276
                        $end = $maybeSkipTo;
277
                        continue;
278
                    }
279
280
                    $start = $phpcsFile->findNext(Tokens::$emptyTokens, ($start + 1), $end, true);
281
                    if ($start === false
282
                        || ($tokens[$stackPtr]['code'] === \T_CONST && $tokens[$start]['code'] !== \T_STRING)
283
                        || ($tokens[$stackPtr]['code'] !== \T_CONST && $tokens[$start]['code'] !== \T_VARIABLE)
284
                    ) {
285
                        // Shouldn't be possible.
286
                        continue;
287
                    }
288
289
                    if ($this->isValidAssignment($phpcsFile, $start, $end) === false) {
290
                        // Create the "found" snippet.
291
                        $content    = '';
292
                        $tokenCount = ($end - $start);
293
                        if ($tokenCount < 20) {
294
                            // Prevent large arrays from being added to the error message.
295
                            $content = $phpcsFile->getTokensAsString($start, ($tokenCount + 1));
296
                        }
297
298
                        $this->throwError($phpcsFile, $start, $type, $content);
299
                    }
300
301
                    $start = $end;
302
                }
303
304
                // Skip to the end of the statement to prevent duplicate messages for multi-declarations.
305
                return $endOfStatement;
306
        }
307
    }
308
309
310
    /**
311
     * Is a value declared and is the value declared valid pre-PHP 5.6 ?
312
     *
313
     * @since 8.2.0
314
     *
315
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
316
     * @param int                   $stackPtr  The position of the current token in the
317
     *                                         stack passed in $tokens.
318
     * @param int                   $end       The end of the value definition.
319
     *                                         This will normally be a comma or semi-colon.
320
     *
321
     * @return bool
322
     */
323
    protected function isValidAssignment(File $phpcsFile, $stackPtr, $end)
324
    {
325
        $tokens = $phpcsFile->getTokens();
326
        $next   = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), $end, true);
327 View Code Duplication
        if ($next === false || $tokens[$next]['code'] !== \T_EQUAL) {
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...
328
            // No value assigned.
329
            return true;
330
        }
331
332
        return $this->isStaticValue($phpcsFile, $tokens, ($next + 1), ($end - 1));
333
    }
334
335
336
    /**
337
     * Is a value declared and is the value declared constant as accepted in PHP 5.5 and lower ?
338
     *
339
     * @since 8.2.0
340
     *
341
     * @param \PHP_CodeSniffer_File $phpcsFile    The file being scanned.
342
     * @param array                 $tokens       The token stack of the current file.
343
     * @param int                   $start        The stackPtr from which to start examining.
344
     * @param int                   $end          The end of the value definition (inclusive),
345
     *                                            i.e. this token will be examined as part of
346
     *                                            the snippet.
347
     * @param int                   $nestedArrays Optional. Array nesting level when examining
348
     *                                            the content of an array.
349
     *
350
     * @return bool
351
     */
352
    protected function isStaticValue(File $phpcsFile, $tokens, $start, $end, $nestedArrays = 0)
353
    {
354
        $nextNonSimple = $phpcsFile->findNext($this->safeOperands, $start, ($end + 1), true);
355
        if ($nextNonSimple === false) {
356
            return true;
357
        }
358
359
        /*
360
         * OK, so we have at least one token which needs extra examination.
361
         */
362
        switch ($tokens[$nextNonSimple]['code']) {
363
            case \T_MINUS:
364
            case \T_PLUS:
365
                if ($this->isNumber($phpcsFile, $start, $end, true) !== false) {
366
                    // Int or float with sign.
367
                    return true;
368
                }
369
370
                return false;
371
372
            case \T_NAMESPACE:
373
            case \T_PARENT:
374
            case \T_SELF:
375
            case \T_DOUBLE_COLON:
376
                $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonSimple + 1), ($end + 1), true);
377
378
                if ($tokens[$nextNonSimple]['code'] === \T_NAMESPACE) {
379
                    // Allow only `namespace\...`.
380
                    if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_NS_SEPARATOR) {
381
                        return false;
382
                    }
383
                } elseif ($tokens[$nextNonSimple]['code'] === \T_PARENT
384
                    || $tokens[$nextNonSimple]['code'] === \T_SELF
385
                ) {
386
                    // Allow only `parent::` and `self::`.
387 View Code Duplication
                    if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_DOUBLE_COLON) {
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...
388
                        return false;
389
                    }
390
                } elseif ($tokens[$nextNonSimple]['code'] === \T_DOUBLE_COLON) {
391
                    // Allow only `T_STRING::T_STRING`.
392
                    if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_STRING) {
393
                        return false;
394
                    }
395
396
                    $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($nextNonSimple - 1), null, true);
397
                    // No need to worry about parent/self, that's handled above and
398
                    // the double colon is skipped over in that case.
399
                    if ($prevNonEmpty === false || $tokens[$prevNonEmpty]['code'] !== \T_STRING) {
400
                        return false;
401
                    }
402
                }
403
404
                // Examine what comes after the namespace/parent/self/double colon, if anything.
405
                return $this->isStaticValue($phpcsFile, $tokens, ($nextNonEmpty + 1), $end, $nestedArrays);
406
407
            case \T_ARRAY:
408
            case \T_OPEN_SHORT_ARRAY:
409
                ++$nestedArrays;
410
411
                $arrayItems = $this->getFunctionCallParameters($phpcsFile, $nextNonSimple);
412
                if (empty($arrayItems) === false) {
413
                    foreach ($arrayItems as $item) {
414
                        // Check for a double arrow, but only if it's for this array item, not for a nested array.
415
                        $doubleArrow = false;
416
417
                        $maybeDoubleArrow = $phpcsFile->findNext(
418
                            array(\T_DOUBLE_ARROW, \T_ARRAY, \T_OPEN_SHORT_ARRAY),
419
                            $item['start'],
420
                            ($item['end'] + 1)
421
                        );
422
                        if ($maybeDoubleArrow !== false && $tokens[$maybeDoubleArrow]['code'] === \T_DOUBLE_ARROW) {
423
                            // Double arrow is for this nesting level.
424
                            $doubleArrow = $maybeDoubleArrow;
425
                        }
426
427
                        if ($doubleArrow === false) {
428 View Code Duplication
                            if ($this->isStaticValue($phpcsFile, $tokens, $item['start'], $item['end'], $nestedArrays) === false) {
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...
429
                                return false;
430
                            }
431
432
                        } else {
433
                            // Examine array key.
434 View Code Duplication
                            if ($this->isStaticValue($phpcsFile, $tokens, $item['start'], ($doubleArrow - 1), $nestedArrays) === false) {
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...
435
                                return false;
436
                            }
437
438
                            // Examine array value.
439 View Code Duplication
                            if ($this->isStaticValue($phpcsFile, $tokens, ($doubleArrow + 1), $item['end'], $nestedArrays) === false) {
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...
440
                                return false;
441
                            }
442
                        }
443
                    }
444
                }
445
446
                --$nestedArrays;
447
448
                /*
449
                 * Find the end of the array.
450
                 * We already know we will have a valid closer as otherwise we wouldn't have been
451
                 * able to get the array items.
452
                 */
453
                $closer = ($nextNonSimple + 1);
454
                if ($tokens[$nextNonSimple]['code'] === \T_OPEN_SHORT_ARRAY
455
                    && isset($tokens[$nextNonSimple]['bracket_closer']) === true
456
                ) {
457
                    $closer = $tokens[$nextNonSimple]['bracket_closer'];
458
                } else {
459
                    $maybeOpener = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonSimple + 1), ($end + 1), true);
460
                    if ($tokens[$maybeOpener]['code'] === \T_OPEN_PARENTHESIS) {
461
                        $opener = $maybeOpener;
462
                        if (isset($tokens[$opener]['parenthesis_closer']) === true) {
463
                            $closer = $tokens[$opener]['parenthesis_closer'];
464
                        }
465
                    }
466
                }
467
468
                if ($closer === $end) {
469
                    return true;
470
                }
471
472
                // Examine what comes after the array, if anything.
473
                return $this->isStaticValue($phpcsFile, $tokens, ($closer + 1), $end, $nestedArrays);
474
475
        }
476
477
        // Ok, so this unsafe token was not one of the exceptions, i.e. this is a PHP 5.6+ syntax.
478
        return false;
479
    }
480
481
482
    /**
483
     * Throw an error if a scalar expression is found.
484
     *
485
     * @since 8.2.0
486
     *
487
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
488
     * @param int                   $stackPtr  The position of the token to link the error to.
489
     * @param string                $type      Type of usage found.
490
     * @param string                $content   Optional. The value for the declaration as found.
491
     *
492
     * @return void
493
     */
494
    protected function throwError(File $phpcsFile, $stackPtr, $type, $content = '')
495
    {
496
        $error     = static::ERROR_PHRASE;
497
        $phrase    = '';
498
        $errorCode = 'Found';
499
500
        if (isset($this->errorPhrases[$type]) === true) {
501
            $errorCode = $this->stringToErrorCode($type) . 'Found';
502
            $phrase    = $this->errorPhrases[$type];
503
        }
504
505
        $data = array($phrase);
506
507
        if (empty($content) === false) {
508
            $error .= ' Found: %s';
509
            $data[] = $content;
510
        }
511
512
        $phpcsFile->addError($error, $stackPtr, $errorCode, $data);
513
    }
514
515
516
    /**
517
     * Helper function to find the end of multi variable/constant declarations.
518
     *
519
     * Checks whether a certain part of a declaration needs to be skipped over or
520
     * if it is the real end of the declaration.
521
     *
522
     * @since 8.2.0
523
     *
524
     * @param array $tokens      Token stack of the current file.
525
     * @param int   $endPtr      The token to examine as a candidate end pointer.
526
     * @param int   $targetLevel Target nesting level.
527
     *
528
     * @return bool|int True if this is the real end. Int stackPtr to skip to if not.
529
     */
530
    private function isRealEndOfDeclaration($tokens, $endPtr, $targetLevel)
531
    {
532
        // Ignore anything within short array definition brackets for now.
533
        if ($tokens[$endPtr]['code'] === \T_OPEN_SHORT_ARRAY
534
            && (isset($tokens[$endPtr]['bracket_opener'])
535
                && $tokens[$endPtr]['bracket_opener'] === $endPtr)
536
            && isset($tokens[$endPtr]['bracket_closer'])
537
        ) {
538
            // Skip forward to the end of the short array definition.
539
            return $tokens[$endPtr]['bracket_closer'];
540
        }
541
542
        // Skip past comma's at a lower nesting level.
543
        if ($tokens[$endPtr]['code'] === \T_COMMA) {
544
            // Check if a comma is at the nesting level we're targetting.
545
            $nestingLevel = 0;
546 View Code Duplication
            if (isset($tokens[$endPtr]['nested_parenthesis']) === true) {
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...
547
                $nestingLevel = \count($tokens[$endPtr]['nested_parenthesis']);
548
            }
549
            if ($nestingLevel > $targetLevel) {
550
                return $endPtr;
551
            }
552
        }
553
554
        return true;
555
    }
556
}
557