• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHPCSUtils / 7142774330

08 Dec 2023 02:50PM UTC coverage: 99.172% (-0.2%) from 99.343%
7142774330

push

github

web-flow
Merge pull request #527 from PHPCSStandards/develop

Release 1.0.9

21 of 21 new or added lines in 4 files covered. (100.0%)

6 existing lines in 2 files now uncovered.

2873 of 2897 relevant lines covered (99.17%)

147.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.63
/PHPCSUtils/Internal/IsShortArrayOrList.php
1
<?php
2
/**
3
 * PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
4
 *
5
 * @package   PHPCSUtils
6
 * @copyright 2019-2020 PHPCSUtils Contributors
7
 * @license   https://opensource.org/licenses/LGPL-3.0 LGPL3
8
 * @link      https://github.com/PHPCSStandards/PHPCSUtils
9
 */
10

11
namespace PHPCSUtils\Internal;
12

13
use PHP_CodeSniffer\Exceptions\RuntimeException;
14
use PHP_CodeSniffer\Files\File;
15
use PHP_CodeSniffer\Util\Tokens;
16
use PHPCSUtils\BackCompat\Helper;
17
use PHPCSUtils\Internal\Cache;
18
use PHPCSUtils\Internal\IsShortArrayOrListWithCache;
19
use PHPCSUtils\Internal\StableCollections;
20
use PHPCSUtils\Tokens\Collections;
21
use PHPCSUtils\Utils\Arrays;
22
use PHPCSUtils\Utils\Context;
23
use PHPCSUtils\Utils\Parentheses;
24
use PHPCSUtils\Utils\PassedParameters;
25

26
/**
27
 * Determination of short array vs short list vs square brackets.
28
 *
29
 * Short list versus short array determination is done based on the following:
30
 * - For "outer" arrays/lists the determination is straight forward, the surrounding
31
 *   tokens give enough clues. This includes "outer" arrays/lists in `foreach()` conditions
32
 *   or in attributes.
33
 * - For nested short arrays/lists, it's a whole different matter.
34
 *   - Both arrays as well as lists can be used when setting _keys_ in arrays and lists
35
 *     (with array access or as a parameter in a function call etc).
36
 *     For arrays, a nested array used as a key will always need array access.
37
 *     For lists, a plain array can be used as the key. (seriously ^@!%&#?)
38
 *     When used as a key though, the nesting is irrelevant and if there is nesting, the "outer"
39
 *     set of brackets will also be part of the key.
40
 *   - Both arrays as well as lists can be used as _values_ in arrays.
41
 *   - Only nested lists (or variables) can be used as _values_ in lists.
42
 *
43
 * ---------------------------------------------------------------------------------------------
44
 * This class is only intended for internal use by PHPCSUtils and is not part of the public API.
45
 * This also means that it has no promise of backward compatibility.
46
 *
47
 * End-users should use the {@see \PHPCSUtils\Utils\Arrays::isShortArray()}
48
 * or the {@see \PHPCSUtils\Utils\Lists::isShortList()} method instead.
49
 * ---------------------------------------------------------------------------------------------
50
 *
51
 * @internal
52
 *
53
 * @since 1.0.0
54
 */
55
final class IsShortArrayOrList
56
{
57

58
    /**
59
     * Type annotation for short arrays.
60
     *
61
     * @since 1.0.0
62
     *
63
     * @var string
64
     */
65
    const SHORT_ARRAY = 'short array';
66

67
    /**
68
     * Type annotation for short lists.
69
     *
70
     * @since 1.0.0
71
     *
72
     * @var string
73
     */
74
    const SHORT_LIST = 'short list';
75

76
    /**
77
     * Type annotation for square brackets.
78
     *
79
     * @since 1.0.0
80
     *
81
     * @var string
82
     */
83
    const SQUARE_BRACKETS = 'square brackets';
84

85
    /**
86
     * Limit for the amount of items to retrieve from inside a nested array/list.
87
     *
88
     * @since 1.0.0
89
     *
90
     * @var int
91
     */
92
    const ITEM_LIMIT = 5;
93

94
    /**
95
     * Limit for recursing over inner nested arrays/lists.
96
     *
97
     * @since 1.0.0
98
     *
99
     * @var int
100
     */
101
    const RECURSION_LIMIT = 3;
102

103
    /**
104
     * The PHPCS file in which the current stackPtr was found.
105
     *
106
     * @since 1.0.0
107
     *
108
     * @var \PHP_CodeSniffer\Files\File
109
     */
110
    private $phpcsFile;
111

112
    /**
113
     * The token stack from the current file.
114
     *
115
     * @since 1.0.0
116
     *
117
     * @var array<int, array<string, mixed>>
118
     */
119
    private $tokens;
120

121
    /**
122
     * Stack pointer to the open bracket.
123
     *
124
     * @since 1.0.0
125
     *
126
     * @var int
127
     */
128
    private $opener;
129

130
    /**
131
     * Stack pointer to the close bracket.
132
     *
133
     * @since 1.0.0
134
     *
135
     * @var int
136
     */
137
    private $closer;
138

139
    /**
140
     * Stack pointer to the first non-empty token before the open bracket.
141
     *
142
     * @since 1.0.0
143
     *
144
     * @var int
145
     */
146
    private $beforeOpener;
147

148
    /**
149
     * Stack pointer to the first non-empty token after the close bracket.
150
     *
151
     * @since 1.0.0
152
     *
153
     * @var int|false Will be `false` if the close bracket is the last token in the file.
154
     */
155
    private $afterCloser;
156

157
    /**
158
     * Current PHPCS version being used.
159
     *
160
     * @since 1.0.0
161
     *
162
     * @var string
163
     */
164
    private $phpcsVersion;
165

166
    /**
167
     * Tokens which can open a short array or short list (PHPCS cross-version compatible).
168
     *
169
     * @since 1.0.0
170
     *
171
     * @var array<int|string, int|string>
172
     */
173
    private $openBrackets;
174

175
    /**
176
     * Constructor.
177
     *
178
     * @since 1.0.0
179
     *
180
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
181
     * @param int                         $stackPtr  The position of the short array opener token.
182
     *
183
     * @return void
184
     *
185
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the token passed is not one of the
186
     *                                                      accepted types or doesn't exist.
187
     */
188
    public function __construct(File $phpcsFile, $stackPtr)
36✔
189
    {
190
        $tokens       = $phpcsFile->getTokens();
36✔
191
        $openBrackets = StableCollections::$shortArrayListOpenTokensBC;
36✔
192

193
        if (isset($tokens[$stackPtr]) === false
36✔
194
            || isset($openBrackets[$tokens[$stackPtr]['code']]) === false
36✔
195
        ) {
18✔
196
            throw new RuntimeException(
24✔
197
                'The IsShortArrayOrList class expects to be passed a T_OPEN_SHORT_ARRAY or T_OPEN_SQUARE_BRACKET token.'
12✔
198
            );
24✔
199
        }
200

201
        $this->phpcsFile = $phpcsFile;
12✔
202
        $this->tokens    = $tokens;
12✔
203
        $this->opener    = $stackPtr;
12✔
204

205
        $this->closer = $stackPtr;
12✔
206
        if (isset($this->tokens[$stackPtr]['bracket_closer'])) {
12✔
207
            $this->closer = $this->tokens[$stackPtr]['bracket_closer'];
12✔
208
        }
6✔
209

210
        $this->beforeOpener = $this->phpcsFile->findPrevious(Tokens::$emptyTokens, ($this->opener - 1), null, true);
12✔
211
        $this->afterCloser  = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($this->closer + 1), null, true);
12✔
212

213
        $this->phpcsVersion = Helper::getVersion();
12✔
214
        $this->openBrackets = $openBrackets;
12✔
215
    }
6✔
216

217
    /**
218
     * Determine whether the bracket is a short array, short list or real square bracket.
219
     *
220
     * @since 1.0.0
221
     *
222
     * @return string Either 'short array', 'short list' or 'square brackets'.
223
     */
224
    public function solve()
52✔
225
    {
226
        if ($this->isSquareBracket() === true) {
52✔
227
            return self::SQUARE_BRACKETS;
4✔
228
        }
229

230
        if ($this->afterCloser === false) {
48✔
231
            // Live coding. Short array until told differently.
232
            return self::SHORT_ARRAY;
4✔
233
        }
234

235
        // If the bracket closer is followed by an equals sign, it's always a short list.
236
        if ($this->tokens[$this->afterCloser]['code'] === \T_EQUAL) {
44✔
237
            return self::SHORT_LIST;
12✔
238
        }
239

240
        // Attributes can only contain constant expressions, i.e. lists not allowed.
241
        if (Context::inAttribute($this->phpcsFile, $this->opener) === true) {
40✔
242
            return self::SHORT_ARRAY;
4✔
243
        }
244

245
        $type = $this->isInForeach();
36✔
246
        if ($type !== false) {
36✔
247
            return $type;
8✔
248
        }
249

250
        /*
251
         * Check if this can be a nested set of brackets used as a value.
252
         * That's the only "confusing" syntax left. In all other cases, it will be a short array.
253
         */
254
        $hasRiskyTokenBeforeOpener = false;
28✔
255
        if (isset($this->openBrackets[$this->tokens[$this->beforeOpener]['code']]) === true
28✔
256
            || $this->tokens[$this->beforeOpener]['code'] === \T_COMMA
28✔
257
            || $this->tokens[$this->beforeOpener]['code'] === \T_DOUBLE_ARROW
28✔
258
        ) {
14✔
259
            $hasRiskyTokenBeforeOpener = true;
24✔
260
        }
12✔
261

262
        $hasRiskyTokenAfterCloser = false;
28✔
263
        if ($this->tokens[$this->afterCloser]['code'] === \T_COMMA
28✔
264
            || $this->tokens[$this->afterCloser]['code'] === \T_CLOSE_SHORT_ARRAY
20✔
265
            || $this->tokens[$this->afterCloser]['code'] === \T_CLOSE_SQUARE_BRACKET
20✔
266
        ) {
14✔
267
            $hasRiskyTokenAfterCloser = true;
24✔
268
        }
12✔
269

270
        if ($hasRiskyTokenBeforeOpener === false || $hasRiskyTokenAfterCloser === false) {
28✔
271
            return self::SHORT_ARRAY;
8✔
272
        }
273

274
        /*
275
         * Check if this is the first/last item in a "parent" set of brackets.
276
         * If so, skip straight to the parent and determine the type of that, the type
277
         * of the inner set of brackets will be the same (as all other options have
278
         * already been eliminated).
279
         */
280
        if (isset($this->openBrackets[$this->tokens[$this->beforeOpener]['code']]) === true) {
24✔
281
            return IsShortArrayOrListWithCache::getType($this->phpcsFile, $this->beforeOpener);
4✔
282
        }
283

284
        $nextEffectiveAfterCloser = $this->afterCloser;
20✔
285
        if ($this->tokens[$this->afterCloser]['code'] === \T_COMMA) {
20✔
286
            // Skip over potential trailing commas.
287
            $nextEffectiveAfterCloser = $this->phpcsFile->findNext(
16✔
288
                Tokens::$emptyTokens,
16✔
289
                ($this->afterCloser + 1),
16✔
290
                null,
16✔
291
                true
8✔
292
            );
16✔
293
        }
8✔
294

295
        if ($this->tokens[$nextEffectiveAfterCloser]['code'] === \T_CLOSE_SHORT_ARRAY
20✔
296
            || $this->tokens[$nextEffectiveAfterCloser]['code'] === \T_CLOSE_SQUARE_BRACKET
20✔
297
        ) {
10✔
298
            return IsShortArrayOrListWithCache::getType($this->phpcsFile, $nextEffectiveAfterCloser);
8✔
299
        }
300

301
        /*
302
         * Okay, so as of here, we know this set of brackets is preceded by a comma or double arrow
303
         * and followed by a comma. This is the only ambiguous syntax left.
304
         */
305

306
        /*
307
         * Check if this could be a (nested) short list at all.
308
         * A list must have at least one variable inside and can not be empty.
309
         * An array, however, cannot contain empty items.
310
         */
311
        $type = $this->walkInside($this->opener);
12✔
312
        if ($type !== false) {
12✔
313
            return $type;
8✔
314
        }
315

316
        // Last resort: walk up in the file to see if we can find a set of parent brackets...
317
        $type = $this->walkOutside();
4✔
318
        if ($type !== false) {
4✔
319
            return $type;
4✔
320
        }
321

322
        // If everything failed, this will be a short array (shouldn't be possible).
323
        return self::SHORT_ARRAY; // @codeCoverageIgnore
324
    }
325

326
    /**
327
     * Check if the brackets are in actual fact real square brackets.
328
     *
329
     * @since 1.0.0
330
     *
331
     * @return bool TRUE if these are real square brackets; FALSE otherwise.
332
     */
333
    private function isSquareBracket()
68✔
334
    {
335
        if ($this->opener === $this->closer) {
68✔
336
            // Parse error (unclosed bracket) or live coding. Bow out.
337
            return true;
4✔
338
        }
339

340
        // Check if this is a bracket we need to examine or a mistokenization.
341
        return ($this->isShortArrayBracket() === false);
64✔
342
    }
343

344
    /**
345
     * Verify that the current set of brackets is not affected by known PHPCS cross-version tokenizer issues.
346
     *
347
     * List of current tokenizer issues which affect the short array/short list tokenization:
348
     * - {@link https://github.com/squizlabs/PHP_CodeSniffer/pull/3632 PHPCS#3632} (PHPCS < 3.7.2)
349
     *
350
     * List of previous tokenizer issues which affected the short array/short list tokenization for reference:
351
     * - {@link https://github.com/squizlabs/PHP_CodeSniffer/issues/1284 PHPCS#1284} (PHPCS < 2.8.1)
352
     * - {@link https://github.com/squizlabs/PHP_CodeSniffer/issues/1381 PHPCS#1381} (PHPCS < 2.9.0)
353
     * - {@link https://github.com/squizlabs/PHP_CodeSniffer/issues/1971 PHPCS#1971} (PHPCS 2.8.0 - 3.2.3)
354
     * - {@link https://github.com/squizlabs/PHP_CodeSniffer/pull/3013 PHPCS#3013} (PHPCS < 3.5.6)
355
     * - {@link https://github.com/squizlabs/PHP_CodeSniffer/pull/3172 PHPCS#3172} (PHPCS < 3.6.0)
356
     *
357
     * @since 1.0.0
358
     *
359
     * @return bool TRUE if this is actually a short array bracket which needs to be examined,
360
     *              FALSE if it is an (incorrectly tokenized) square bracket.
361
     */
362
    private function isShortArrayBracket()
120✔
363
    {
364
        if ($this->tokens[$this->opener]['code'] === \T_OPEN_SQUARE_BRACKET) {
120✔
365
            if (\version_compare($this->phpcsVersion, '3.7.2', '>=') === true) {
56✔
366
                // These will just be properly tokenized, plain square brackets. No need for further checks.
367
                return false;
56✔
368
            }
369

370
            /*
371
             * BC: Work around a bug in the tokenizer of PHPCS < 3.7.2, where a `[` would be
372
             * tokenized as T_OPEN_SQUARE_BRACKET instead of T_OPEN_SHORT_ARRAY if it was
373
             * preceded by the close parenthesis of a non-braced control structure.
374
             *
375
             * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3632
376
             */
UNCOV
377
            if ($this->tokens[$this->beforeOpener]['code'] === \T_CLOSE_PARENTHESIS
×
UNCOV
378
                && isset($this->tokens[$this->beforeOpener]['parenthesis_owner']) === true
×
379
                // phpcs:ignore Generic.Files.LineLength.TooLong
UNCOV
380
                && isset(Tokens::$scopeOpeners[$this->tokens[$this->tokens[$this->beforeOpener]['parenthesis_owner']]['code']]) === true
×
381
            ) {
UNCOV
382
                return true;
×
383
            }
384

385
            // These are really just plain square brackets.
UNCOV
386
            return false;
×
387
        }
388

389
        return true;
64✔
390
    }
391

392
    /**
393
     * Check is this set of brackets is used within a foreach expression.
394
     *
395
     * @since 1.0.0
396
     *
397
     * @return string|false The determined type or FALSE if undetermined.
398
     */
399
    private function isInForeach()
60✔
400
    {
401
        $inForeach = Context::inForeachCondition($this->phpcsFile, $this->opener);
60✔
402
        if ($inForeach === false) {
60✔
403
            return false;
6✔
404
        }
405

406
        switch ($inForeach) {
407
            case 'beforeAs':
54✔
408
                if ($this->tokens[$this->afterCloser]['code'] === \T_AS) {
22✔
409
                    return self::SHORT_ARRAY;
20✔
410
                }
411

412
                break;
10✔
413

414
            case 'afterAs':
32✔
415
                if ($this->tokens[$this->afterCloser]['code'] === \T_CLOSE_PARENTHESIS) {
32✔
416
                    $owner = Parentheses::getOwner($this->phpcsFile, $this->afterCloser);
24✔
417
                    if ($owner !== false && $this->tokens[$owner]['code'] === \T_FOREACH) {
24✔
418
                        return self::SHORT_LIST;
22✔
419
                    }
420
                }
421

422
                break;
16✔
423
        }
424

425
        /*
426
         * Everything else will be a nested set of brackets (provided we're talking valid PHP),
427
         * so disregard as it can not be determined yet.
428
         */
429
        return false;
26✔
430
    }
431

432
    /**
433
     * Walk the first part of the contents between the brackets to see if we can determine if this
434
     * is a short array or short list based on its contents.
435
     *
436
     * Short lists can only have another (nested) list or variable assignments, including property assignments
437
     * and array index assignment, as the value inside the brackets.
438
     *
439
     * This won't walk the complete contents as that could be a huge performance drain. Just the first x items.
440
     *
441
     * @since 1.0.0
442
     *
443
     * @param int $opener     The position of the short array open bracket token.
444
     * @param int $recursions Optional. Keep track of how often we've recursed into this methd.
445
     *                        Prevent infinite loops for extremely deeply nested arrays.
446
     *                        Defaults to 0.
447
     *
448
     * @return string|false The determined type or FALSE if undetermined.
449
     */
450
    private function walkInside($opener, $recursions = 0)
156✔
451
    {
452
        // Get the first 5 "parameters" and ignore the "is short array" check.
453
        $items = PassedParameters::getParameters($this->phpcsFile, $opener, self::ITEM_LIMIT, true);
156✔
454

455
        if ($items === []) {
156✔
456
            /*
457
             * A list can not be empty, so this must be an array, however as this is a nested
458
             * set of brackets, let the outside brackets be the decider as it may be
459
             * a coding error which a sniff needs to flag.
460
             */
461
            return false;
8✔
462
        }
463

464
        // Make sure vars assigned by reference are handled correctly.
465
        $skip   = Tokens::$emptyTokens;
148✔
466
        $skip[] = \T_BITWISE_AND;
148✔
467

468
        $skipNames = Collections::namespacedNameTokens() + Collections::ooHierarchyKeywords();
148✔
469

470
        foreach ($items as $item) {
148✔
471
            /*
472
             * If we encounter a completely empty item, this must be a short list as arrays cannot contain
473
             * empty items.
474
             */
475
            if ($item['clean'] === '') {
148✔
476
                return self::SHORT_LIST;
16✔
477
            }
478

479
            /*
480
             * If the "value" part of the entry doesn't start with a variable, a (nested) short list/array,
481
             * or a static property assignment, we know for sure that the outside brackets will be an array.
482
             */
483
            $arrow = Arrays::getDoubleArrowPtr($this->phpcsFile, $item['start'], $item['end']);
144✔
484
            if ($arrow === false) {
144✔
485
                $firstNonEmptyInValue = $this->phpcsFile->findNext($skip, $item['start'], ($item['end'] + 1), true);
100✔
486
            } else {
50✔
487
                $firstNonEmptyInValue = $this->phpcsFile->findNext($skip, ($arrow + 1), ($item['end'] + 1), true);
44✔
488
            }
489

490
            if ($this->tokens[$firstNonEmptyInValue]['code'] !== \T_VARIABLE
144✔
491
                && isset(Collections::namespacedNameTokens()[$this->tokens[$firstNonEmptyInValue]['code']]) === false
144✔
492
                && isset(Collections::ooHierarchyKeywords()[$this->tokens[$firstNonEmptyInValue]['code']]) === false
144✔
493
                && isset($this->openBrackets[$this->tokens[$firstNonEmptyInValue]['code']]) === false
144✔
494
            ) {
72✔
495
                return self::SHORT_ARRAY;
44✔
496
            }
497

498
            /*
499
             * Check if this is a potential list assignment to a static variable.
500
             * If not, again, we can be sure it will be a short array.
501
             */
502
            if (isset(Collections::namespacedNameTokens()[$this->tokens[$firstNonEmptyInValue]['code']]) === true
116✔
503
                || isset(Collections::ooHierarchyKeywords()[$this->tokens[$firstNonEmptyInValue]['code']]) === true
116✔
504
            ) {
58✔
505
                $nextAfter = $this->phpcsFile->findNext($skipNames, ($firstNonEmptyInValue + 1), null, true);
40✔
506

507
                if ($this->tokens[$nextAfter]['code'] !== \T_DOUBLE_COLON) {
40✔
508
                    return self::SHORT_ARRAY;
8✔
509
                } else {
510
                    /*
511
                     * Double colon, so make sure there is a variable after it.
512
                     * If not, it's constant or function call, i.e. a short array.
513
                     */
514
                    $nextNextAfter = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($nextAfter + 1), null, true);
32✔
515
                    if ($this->tokens[$nextNextAfter]['code'] !== \T_VARIABLE) {
32✔
516
                        return self::SHORT_ARRAY;
12✔
517
                    }
518
                }
519

520
                continue;
20✔
521
            }
522

523
            if (isset($this->openBrackets[$this->tokens[$firstNonEmptyInValue]['code']]) === true) {
116✔
524
                /*
525
                 * If the "value" part starts with an open bracket, but has other tokens after it, the current,
526
                 * outside set of brackets will always be an array (the brackets in the value can still be both,
527
                 * but that's not the concern of the current determination).
528
                 */
529
                $lastNonEmptyInValue = $this->phpcsFile->findPrevious(
44✔
530
                    Tokens::$emptyTokens,
44✔
531
                    $item['end'],
44✔
532
                    $item['start'],
44✔
533
                    true
22✔
534
                );
44✔
535
                if (isset($this->tokens[$firstNonEmptyInValue]['bracket_closer']) === true
44✔
536
                    && $this->tokens[$firstNonEmptyInValue]['bracket_closer'] !== $lastNonEmptyInValue
44✔
537
                ) {
22✔
538
                    return self::SHORT_ARRAY;
12✔
539
                }
540

541
                /*
542
                 * Recursively check the inner set of brackets for contents indicating this is not a short list.
543
                 */
544
                if ($recursions < self::RECURSION_LIMIT) {
32✔
545
                    $innerType = $this->walkInside($firstNonEmptyInValue, ($recursions + 1));
32✔
546
                    if ($innerType !== false) {
32✔
547
                        return $innerType;
12✔
548
                    }
549
                }
10✔
550
            }
10✔
551
        }
52✔
552

553
        // Undetermined.
554
        return false;
60✔
555
    }
556

557
    /**
558
     * Walk up in the file to try and find an "outer" set of brackets for an ambiguous, potentially
559
     * nested set of brackets.
560
     *
561
     * This should really be the last resort, if all else fails to determine the type of the brackets.
562
     *
563
     * @since 1.0.0
564
     *
565
     * @return string|false The determined type or FALSE if undetermined.
566
     */
567
    private function walkOutside()
88✔
568
    {
569
        $stopPoints               = Collections::phpOpenTags();
88✔
570
        $stopPoints[\T_SEMICOLON] = \T_SEMICOLON;
88✔
571

572
        for ($i = ($this->opener - 1); $i >= 0; $i--) {
88✔
573
            // Skip over block comments (just in case).
574
            if ($this->tokens[$i]['code'] === \T_DOC_COMMENT_CLOSE_TAG) {
88✔
575
                $i = $this->tokens[$i]['comment_opener'];
8✔
576
                continue;
8✔
577
            }
578

579
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === true) {
88✔
580
                continue;
88✔
581
            }
582

583
            // Stop on an end of statement.
584
            if (isset($stopPoints[$this->tokens[$i]['code']]) === true) {
88✔
585
                // End of previous statement or start of document.
586
                return self::SHORT_ARRAY;
20✔
587
            }
588

589
            if (isset($this->tokens[$i]['scope_opener'], $this->tokens[$i]['scope_closer']) === true) {
88✔
590
                if ($i === $this->tokens[$i]['scope_opener']
16✔
591
                    && $this->tokens[$i]['scope_closer'] > $this->closer
16✔
592
                ) {
8✔
593
                    // Found a scope wrapping this set of brackets before finding a outer set of brackets.
594
                    // This will be a short array.
595
                    return self::SHORT_ARRAY;
8✔
596
                }
597

598
                if ($i === $this->tokens[$i]['scope_closer']
8✔
599
                    && isset($this->tokens[$i]['scope_condition']) === true
8✔
600
                ) {
4✔
601
                    $i = $this->tokens[$i]['scope_condition'];
8✔
602
                    continue;
8✔
603
                }
604

605
                // Scope opener without scope condition shouldn't be possible, but just in case.
606
                // @codeCoverageIgnoreStart
607
                $i = $this->tokens[$i]['scope_opener'];
608
                continue;
609
                // @codeCoverageIgnoreEnd
610
            }
611

612
            if (isset($this->tokens[$i]['parenthesis_opener'], $this->tokens[$i]['parenthesis_closer']) === true) {
88✔
613
                if ($i === $this->tokens[$i]['parenthesis_opener']
48✔
614
                    && $this->tokens[$i]['parenthesis_closer'] > $this->closer
48✔
615
                ) {
24✔
616
                    $beforeParensOpen = $this->phpcsFile->findPrevious(Tokens::$emptyTokens, ($i - 1), null, true);
40✔
617
                    if ($this->tokens[$beforeParensOpen]['code'] === \T_LIST) {
40✔
618
                        // Parse error, mixing long and short list, but that's not our concern.
619
                        return self::SHORT_LIST;
4✔
620
                    }
621

622
                    // Found parentheses wrapping this set of brackets before finding a outer set of brackets.
623
                    // This will be a short array.
624
                    return self::SHORT_ARRAY;
36✔
625
                }
626

627
                if ($i === $this->tokens[$i]['parenthesis_closer']) {
8✔
628
                    if (isset($this->tokens[$i]['parenthesis_owner']) === true) {
8✔
629
                        $i = $this->tokens[$i]['parenthesis_owner'];
4✔
630
                        continue;
4✔
631
                    }
632
                }
4✔
633

634
                // Parenthesis closer without owner (function call and such).
635
                $i = $this->tokens[$i]['parenthesis_opener'];
8✔
636
                continue;
8✔
637
            }
638

639
            /*
640
             * Skip over attributes.
641
             * No special handling needed, brackets within attributes won't reach this
642
             * method as they are already handled within the solve() method.
643
             */
644
            if (isset($this->tokens[$i]['attribute_opener'], $this->tokens[$i]['attribute_closer']) === true
88✔
645
                && $i === $this->tokens[$i]['attribute_closer']
88✔
646
            ) {
44✔
647
                $i = $this->tokens[$i]['attribute_opener'];
4✔
648
                continue;
4✔
649
            }
650

651
            /*
652
             * This is a close bracket, but it's not the outer wrapper.
653
             * As we skip over parentheses and curlies above, we *know* this will be a
654
             * set of brackets at the same bracket "nesting level" as the set we are examining.
655
             */
656
            if (isset($this->tokens[$i]['bracket_opener'], $this->tokens[$i]['bracket_closer']) === true
88✔
657
                && $i === $this->tokens[$i]['bracket_closer']
88✔
658
            ) {
44✔
659
                /*
660
                 * Now, if the set of brackets follows the same code pattern (comma or double arrow before,
661
                 * comma after), this will be an adjacent set of potentially nested brackets.
662
                 * If so, check if the type of the previous set of brackets has already been determined
663
                 * as adjacent sets of brackets will have the same type.
664
                 */
665
                $adjOpener    = $this->tokens[$i]['bracket_opener'];
16✔
666
                $prevNonEmpty = $this->phpcsFile->findPrevious(Tokens::$emptyTokens, ($adjOpener - 1), null, true);
16✔
667
                $nextNonEmpty = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true);
16✔
668

669
                if ($this->tokens[$prevNonEmpty]['code'] === $this->tokens[$this->beforeOpener]['code']
16✔
670
                    && $this->tokens[$nextNonEmpty]['code'] === $this->tokens[$this->afterCloser]['code']
16✔
671
                    && Cache::isCached($this->phpcsFile, IsShortArrayOrListWithCache::CACHE_KEY, $adjOpener) === true
16✔
672
                ) {
8✔
673
                    return Cache::get($this->phpcsFile, IsShortArrayOrListWithCache::CACHE_KEY, $adjOpener);
8✔
674
                }
675

676
                // If not, skip over it.
677
                $i = $this->tokens[$i]['bracket_opener'];
16✔
678
                continue;
16✔
679
            }
680

681
            // Open bracket.
682
            if (isset($this->openBrackets[$this->tokens[$i]['code']]) === true) {
88✔
683
                if (isset($this->tokens[$i]['bracket_closer']) === false
20✔
684
                    || $this->tokens[$i]['code'] === \T_OPEN_SQUARE_BRACKET
20✔
685
                ) {
10✔
686
                    /*
687
                     * If the type of the unclosed "outer" brackets cannot be determined or
688
                     * they are identified as plain square brackets, the inner brackets
689
                     * we are examining should be regarded as a short array.
690
                     */
691
                    return self::SHORT_ARRAY;
4✔
692
                }
693

694
                if ($this->tokens[$i]['bracket_closer'] > $this->closer) {
16✔
695
                    // This is one we have to examine further as an outer set of brackets.
696
                    // As all the other checks have already failed to get a result, we know that
697
                    // whatever the outer set is, the inner set will be the same.
698
                    return IsShortArrayOrListWithCache::getType($this->phpcsFile, $i);
16✔
699
                }
700
            }
701
        }
44✔
702

703
        // Reached the start of the file without finding an outer set of brackets.
704
        // Shouldn't be possible, but just in case.
705
        return false; // @codeCoverageIgnore
706
    }
707
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc