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

PHPCSStandards / PHPCSUtils / 17477384437

25 Aug 2025 08:42PM UTC coverage: 99.753% (-0.03%) from 99.781%
17477384437

push

github

web-flow
Merge pull request #703 from PHPCSStandards/dependabot/github_actions/actions/upload-pages-artifact-4

GH Actions: Bump actions/upload-pages-artifact from 3 to 4

3637 of 3646 relevant lines covered (99.75%)

218.57 hits per line

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

99.11
/PHPCSUtils/AbstractSniffs/AbstractArrayDeclarationSniff.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\AbstractSniffs;
12

13
use PHP_CodeSniffer\Files\File;
14
use PHP_CodeSniffer\Sniffs\Sniff;
15
use PHP_CodeSniffer\Util\Tokens;
16
use PHPCSUtils\Exceptions\LogicException;
17
use PHPCSUtils\Exceptions\UnexpectedTokenType;
18
use PHPCSUtils\Tokens\Collections;
19
use PHPCSUtils\Utils\Arrays;
20
use PHPCSUtils\Utils\Numbers;
21
use PHPCSUtils\Utils\PassedParameters;
22
use PHPCSUtils\Utils\TextStrings;
23

24
/**
25
 * Abstract sniff to easily examine all parts of an array declaration.
26
 *
27
 * @since 1.0.0
28
 */
29
abstract class AbstractArrayDeclarationSniff implements Sniff
30
{
31

32
    /**
33
     * The stack pointer to the array keyword or the short array open token.
34
     *
35
     * @since 1.0.0
36
     *
37
     * @var int
38
     */
39
    protected $stackPtr;
40

41
    /**
42
     * The token stack for the current file being examined.
43
     *
44
     * @since 1.0.0
45
     *
46
     * @var array<int, array<string, mixed>>
47
     */
48
    protected $tokens;
49

50
    /**
51
     * The stack pointer to the array opener.
52
     *
53
     * @since 1.0.0
54
     *
55
     * @var int
56
     */
57
    protected $arrayOpener;
58

59
    /**
60
     * The stack pointer to the array closer.
61
     *
62
     * @since 1.0.0
63
     *
64
     * @var int
65
     */
66
    protected $arrayCloser;
67

68
    /**
69
     * A multi-dimentional array with information on each array item.
70
     *
71
     * The array index is 1-based and contains the following information on each array item:
72
     * ```php
73
     * 1 => array(
74
     *   'start' => int,    // The stack pointer to the first token in the array item.
75
     *   'end'   => int,    // The stack pointer to the last token in the array item.
76
     *   'raw'   => string, // A string with the contents of all tokens between `start` and `end`.
77
     *   'clean' => string, // Same as `raw`, but all comment tokens have been stripped out.
78
     * )
79
     * ```
80
     *
81
     * @since 1.0.0
82
     *
83
     * @var array<int, array<string, int|string>>
84
     */
85
    protected $arrayItems;
86

87
    /**
88
     * How many items are in the array.
89
     *
90
     * @since 1.0.0
91
     *
92
     * @var int
93
     */
94
    protected $itemCount = 0;
95

96
    /**
97
     * Whether or not the array is single line.
98
     *
99
     * @since 1.0.0
100
     *
101
     * @var bool
102
     */
103
    protected $singleLine;
104

105
    /**
106
     * List of tokens which can safely be used with an eval() expression.
107
     *
108
     * This list gets enhanced with additional token groups in the constructor.
109
     *
110
     * @since 1.0.0
111
     *
112
     * @var array<int|string, int|string>
113
     */
114
    private $acceptedTokens = [
115
        \T_NULL                     => \T_NULL,
116
        \T_TRUE                     => \T_TRUE,
117
        \T_FALSE                    => \T_FALSE,
118
        \T_LNUMBER                  => \T_LNUMBER,
119
        \T_DNUMBER                  => \T_DNUMBER,
120
        \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING,
121
        \T_STRING_CONCAT            => \T_STRING_CONCAT,
122
        \T_BOOLEAN_NOT              => \T_BOOLEAN_NOT,
123
    ];
124

125
    /**
126
     * Set up this class.
127
     *
128
     * @since 1.0.0
129
     *
130
     * @codeCoverageIgnore
131
     *
132
     * @return void
133
     */
134
    final public function __construct()
135
    {
136
        // Enhance the list of accepted tokens.
137
        $this->acceptedTokens += Tokens::$assignmentTokens;
138
        $this->acceptedTokens += Tokens::$comparisonTokens;
139
        $this->acceptedTokens += Tokens::$arithmeticTokens;
140
        $this->acceptedTokens += Tokens::$operators;
141
        $this->acceptedTokens += Tokens::$booleanOperators;
142
        $this->acceptedTokens += Tokens::$castTokens;
143
        $this->acceptedTokens += Tokens::$bracketTokens;
144
        $this->acceptedTokens += Tokens::$heredocTokens;
145
        $this->acceptedTokens += Collections::ternaryOperators();
146
    }
147

148
    /**
149
     * Returns an array of tokens this test wants to listen for.
150
     *
151
     * @since 1.0.0
152
     *
153
     * @codeCoverageIgnore
154
     *
155
     * @return array<int|string>
156
     */
157
    public function register()
158
    {
159
        return Collections::arrayOpenTokensBC();
160
    }
161

162
    /**
163
     * Processes this test when one of its tokens is encountered.
164
     *
165
     * This method fills the properties with relevant information for examining the array
166
     * and then passes off to the {@see AbstractArrayDeclarationSniff::processArray()} method.
167
     *
168
     * @since 1.0.0
169
     *
170
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
171
     *                                               token was found.
172
     * @param int                         $stackPtr  The position in the PHP_CodeSniffer
173
     *                                               file's token stack where the token
174
     *                                               was found.
175
     *
176
     * @return void
177
     */
178
    final public function process(File $phpcsFile, $stackPtr)
90✔
179
    {
180
        try {
181
            $this->arrayItems = PassedParameters::getParameters($phpcsFile, $stackPtr);
90✔
182
        } catch (UnexpectedTokenType $e) {
42✔
183
            // Parse error, short list, real square open bracket or incorrectly tokenized short array token.
184
            return;
6✔
185
        }
186

187
        $openClose = Arrays::getOpenClose($phpcsFile, $stackPtr, true);
72✔
188
        if ($openClose === false) {
72✔
189
            // Parse error or live coding.
190
            return;
6✔
191
        }
192

193
        $this->stackPtr    = $stackPtr;
66✔
194
        $this->tokens      = $phpcsFile->getTokens();
66✔
195
        $this->arrayOpener = $openClose['opener'];
66✔
196
        $this->arrayCloser = $openClose['closer'];
66✔
197
        $this->itemCount   = \count($this->arrayItems);
66✔
198

199
        $this->singleLine = true;
66✔
200
        if ($this->tokens[$openClose['opener']]['line'] !== $this->tokens[$openClose['closer']]['line']) {
66✔
201
            $this->singleLine = false;
12✔
202
        }
4✔
203

204
        $this->processArray($phpcsFile);
66✔
205

206
        // Reset select properties between calls to this sniff to lower memory usage.
207
        $this->tokens     = [];
66✔
208
        $this->arrayItems = [];
66✔
209
    }
33✔
210

211
    /**
212
     * Process every part of the array declaration.
213
     *
214
     * Controller which calls the individual `process...()` methods for each part of the array.
215
     *
216
     * The method starts by calling the {@see AbstractArrayDeclarationSniff::processOpenClose()} method
217
     * and subsequently calls the following methods for each array item:
218
     *
219
     * Unkeyed arrays | Keyed arrays
220
     * -------------- | ------------
221
     * processNoKey() | processKey()
222
     * -              | processArrow()
223
     * processValue() | processValue()
224
     * processComma() | processComma()
225
     *
226
     * This is the default logic for the sniff, but can be overloaded in a concrete child class
227
     * if needed.
228
     *
229
     * @since 1.0.0
230
     *
231
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
232
     *                                               token was found.
233
     *
234
     * @return void
235
     */
236
    public function processArray(File $phpcsFile)
66✔
237
    {
238
        if ($this->processOpenClose($phpcsFile, $this->arrayOpener, $this->arrayCloser) === true) {
66✔
239
            return;
6✔
240
        }
241

242
        if ($this->itemCount === 0) {
60✔
243
            return;
6✔
244
        }
245

246
        foreach ($this->arrayItems as $itemNr => $arrayItem) {
54✔
247
            try {
248
                $arrowPtr = Arrays::getDoubleArrowPtr($phpcsFile, $arrayItem['start'], $arrayItem['end']);
54✔
249
            } catch (LogicException $e) {
22✔
250
                // Parse error: empty array item. Ignore.
251
                continue;
6✔
252
            }
253

254
            if ($arrowPtr !== false) {
54✔
255
                if ($this->processKey($phpcsFile, $arrayItem['start'], ($arrowPtr - 1), $itemNr) === true) {
30✔
256
                    return;
6✔
257
                }
258

259
                if ($this->processArrow($phpcsFile, $arrowPtr, $itemNr) === true) {
24✔
260
                    return;
6✔
261
                }
262

263
                if ($this->processValue($phpcsFile, ($arrowPtr + 1), $arrayItem['end'], $itemNr) === true) {
18✔
264
                    return;
8✔
265
                }
266
            } else {
6✔
267
                if ($this->processNoKey($phpcsFile, $arrayItem['start'], $itemNr) === true) {
48✔
268
                    return;
6✔
269
                }
270

271
                if ($this->processValue($phpcsFile, $arrayItem['start'], $arrayItem['end'], $itemNr) === true) {
42✔
272
                    return;
6✔
273
                }
274
            }
275

276
            $commaPtr = ($arrayItem['end'] + 1);
42✔
277
            if ($itemNr < $this->itemCount || $this->tokens[$commaPtr]['code'] === \T_COMMA) {
42✔
278
                if ($this->processComma($phpcsFile, $commaPtr, $itemNr) === true) {
42✔
279
                    return;
6✔
280
                }
281
            }
12✔
282
        }
12✔
283
    }
9✔
284

285
    /**
286
     * Process the array opener and closer.
287
     *
288
     * Optional method to be implemented in concrete child classes. By default, this method does nothing.
289
     *
290
     * @since 1.0.0
291
     *
292
     * @codeCoverageIgnore
293
     *
294
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
295
     *                                               token was found.
296
     * @param int                         $openPtr   The position of the array opener token in the token stack.
297
     * @param int                         $closePtr  The position of the array closer token in the token stack.
298
     *
299
     * @return true|void Returning `TRUE` will short-circuit the sniff and stop processing.
300
     *                   In effect, this means that the sniff will not examine the individual
301
     *                   array items if `TRUE` is returned.
302
     */
303
    public function processOpenClose(File $phpcsFile, $openPtr, $closePtr)
304
    {
305
    }
306

307
    /**
308
     * Process the tokens in an array key.
309
     *
310
     * Optional method to be implemented in concrete child classes. By default, this method does nothing.
311
     *
312
     * Note: The `$startPtr` and `$endPtr` do not discount whitespace or comments, but are all inclusive
313
     * to allow for examining all tokens in an array key.
314
     *
315
     * @since 1.0.0
316
     *
317
     * @codeCoverageIgnore
318
     *
319
     * @see \PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff::getActualArrayKey() Optional helper function.
320
     *
321
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
322
     *                                               token was found.
323
     * @param int                         $startPtr  The stack pointer to the first token in the "key" part of
324
     *                                               an array item.
325
     * @param int                         $endPtr    The stack pointer to the last token in the "key" part of
326
     *                                               an array item.
327
     * @param int                         $itemNr    Which item in the array is being handled.
328
     *                                               1-based, i.e. the first item is item 1, the second 2 etc.
329
     *
330
     * @return true|void Returning `TRUE` will short-circuit the array item loop and stop processing.
331
     *                   In effect, this means that the sniff will not examine the double arrow, the array
332
     *                   value or comma for this array item and will not process any array items after this one.
333
     */
334
    public function processKey(File $phpcsFile, $startPtr, $endPtr, $itemNr)
335
    {
336
    }
337

338
    /**
339
     * Process an array item without an array key.
340
     *
341
     * Optional method to be implemented in concrete child classes. By default, this method does nothing.
342
     *
343
     * Note: This method is _not_ intended for processing the array _value_. Use the
344
     * {@see AbstractArrayDeclarationSniff::processValue()} method to implement processing of the array value.
345
     *
346
     * @since 1.0.0
347
     *
348
     * @codeCoverageIgnore
349
     *
350
     * @see \PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff::processValue() Method to process the array value.
351
     *
352
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
353
     *                                               token was found.
354
     * @param int                         $startPtr  The stack pointer to the first token in the array item,
355
     *                                               which in this case will be the first token of the array
356
     *                                               value part of the array item.
357
     * @param int                         $itemNr    Which item in the array is being handled.
358
     *                                               1-based, i.e. the first item is item 1, the second 2 etc.
359
     *
360
     * @return true|void Returning `TRUE` will short-circuit the array item loop and stop processing.
361
     *                   In effect, this means that the sniff will not examine the array value or
362
     *                   comma for this array item and will not process any array items after this one.
363
     */
364
    public function processNoKey(File $phpcsFile, $startPtr, $itemNr)
365
    {
366
    }
367

368
    /**
369
     * Process the double arrow.
370
     *
371
     * Optional method to be implemented in concrete child classes. By default, this method does nothing.
372
     *
373
     * @since 1.0.0
374
     *
375
     * @codeCoverageIgnore
376
     *
377
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
378
     *                                               token was found.
379
     * @param int                         $arrowPtr  The stack pointer to the double arrow for the array item.
380
     * @param int                         $itemNr    Which item in the array is being handled.
381
     *                                               1-based, i.e. the first item is item 1, the second 2 etc.
382
     *
383
     * @return true|void Returning `TRUE` will short-circuit the array item loop and stop processing.
384
     *                   In effect, this means that the sniff will not examine the array value or
385
     *                   comma for this array item and will not process any array items after this one.
386
     */
387
    public function processArrow(File $phpcsFile, $arrowPtr, $itemNr)
388
    {
389
    }
390

391
    /**
392
     * Process the tokens in an array value.
393
     *
394
     * Optional method to be implemented in concrete child classes. By default, this method does nothing.
395
     *
396
     * Note: The `$startPtr` and `$endPtr` do not discount whitespace or comments, but are all inclusive
397
     * to allow for examining all tokens in an array value.
398
     *
399
     * @since 1.0.0
400
     *
401
     * @codeCoverageIgnore
402
     *
403
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
404
     *                                               token was found.
405
     * @param int                         $startPtr  The stack pointer to the first token in the "value" part of
406
     *                                               an array item.
407
     * @param int                         $endPtr    The stack pointer to the last token in the "value" part of
408
     *                                               an array item.
409
     * @param int                         $itemNr    Which item in the array is being handled.
410
     *                                               1-based, i.e. the first item is item 1, the second 2 etc.
411
     *
412
     * @return true|void Returning `TRUE` will short-circuit the array item loop and stop processing.
413
     *                   In effect, this means that the sniff will not examine the comma for this
414
     *                   array item and will not process any array items after this one.
415
     */
416
    public function processValue(File $phpcsFile, $startPtr, $endPtr, $itemNr)
417
    {
418
    }
419

420
    /**
421
     * Process the comma after an array item.
422
     *
423
     * Optional method to be implemented in concrete child classes. By default, this method does nothing.
424
     *
425
     * @since 1.0.0
426
     *
427
     * @codeCoverageIgnore
428
     *
429
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
430
     *                                               token was found.
431
     * @param int                         $commaPtr  The stack pointer to the comma.
432
     * @param int                         $itemNr    Which item in the array is being handled.
433
     *                                               1-based, i.e. the first item is item 1, the second 2 etc.
434
     *
435
     * @return true|void Returning `TRUE` will short-circuit the array item loop and stop processing.
436
     *                   In effect, this means that the sniff will not process any array items
437
     *                   after this one.
438
     */
439
    public function processComma(File $phpcsFile, $commaPtr, $itemNr)
440
    {
441
    }
442

443
    /**
444
     * Determine what the actual array key would be.
445
     *
446
     * Helper function for processsing array keys in the processKey() function.
447
     * Using this method is up to the sniff implementation in the child class.
448
     *
449
     * @since 1.0.0
450
     *
451
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
452
     *                                               token was found.
453
     * @param int                         $startPtr  The stack pointer to the first token in the "key" part of
454
     *                                               an array item.
455
     * @param int                         $endPtr    The stack pointer to the last token in the "key" part of
456
     *                                               an array item.
457
     *
458
     * @return string|int|void The string or integer array key or void if the array key could not
459
     *                         reliably be determined.
460
     */
461
    public function getActualArrayKey(File $phpcsFile, $startPtr, $endPtr)
54✔
462
    {
463
        /*
464
         * Determine the value of the key.
465
         */
466
        $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $startPtr, null, true);
54✔
467
        $lastNonEmpty  = $phpcsFile->findPrevious(Tokens::$emptyTokens, $endPtr, null, true);
54✔
468

469
        $content = '';
54✔
470

471
        for ($i = $firstNonEmpty; $i <= $lastNonEmpty; $i++) {
54✔
472
            if (isset(Tokens::$commentTokens[$this->tokens[$i]['code']]) === true) {
54✔
473
                continue;
6✔
474
            }
475

476
            if ($this->tokens[$i]['code'] === \T_WHITESPACE) {
54✔
477
                $content .= ' ';
30✔
478
                continue;
30✔
479
            }
480

481
            // Handle FQN true/false/null.
482
            if ($this->tokens[$i]['code'] === \T_NAME_FULLY_QUALIFIED) {
54✔
483
                $compareReadyKeyword = \strtolower($this->tokens[$i]['content']);
2✔
484
                if ($compareReadyKeyword === '\true'
2✔
485
                    || $compareReadyKeyword === '\false'
2✔
486
                    || $compareReadyKeyword === '\null'
2✔
487
                ) {
488
                    // FQN true/false/null on PHPCS 4.x. This can be handled.
489
                    $content .= $this->tokens[$i]['content'];
×
490
                    continue;
1✔
491
                }
492
            } elseif ($this->tokens[$i]['code'] === \T_NS_SEPARATOR) {
54✔
493
                // PHPCS 3.x.
494
                $nextNonEmpty   = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true);
16✔
495
                $nextNonEmptyLC = \strtolower($this->tokens[$nextNonEmpty]['content']);
16✔
496
                if ($nextNonEmpty !== false
8✔
497
                    // PHPCS 3.x with PHP < 8.0.
498
                    && ($this->tokens[$nextNonEmpty]['code'] === \T_TRUE
16✔
499
                    || $this->tokens[$nextNonEmpty]['code'] === \T_FALSE
16✔
500
                    || $this->tokens[$nextNonEmpty]['code'] === \T_NULL
15✔
501
                    // PHPCS 3.x with PHP >= 8.0 where the namespaced name tokenization has been undone.
8✔
502
                    || ($this->tokens[$nextNonEmpty]['code'] === \T_STRING
14✔
503
                        && ($nextNonEmptyLC === 'true' || $nextNonEmptyLC === 'false' || $nextNonEmptyLC === 'null')))
13✔
504
                ) {
8✔
505
                    // FQN true/false/null on PHPCS 3.x. This can be handled.
506
                    $content .= $this->tokens[$nextNonEmpty]['content'];
16✔
507
                    $i        = $nextNonEmpty;
16✔
508
                    continue;
16✔
509
                }
510
            }
2✔
511

512
            if (isset($this->acceptedTokens[$this->tokens[$i]['code']]) === false) {
54✔
513
                // This is not a key we can evaluate. Might be a variable or constant.
514
                return;
6✔
515
            }
516

517
            // Take PHP 7.4 numeric literal separators into account.
518
            if ($this->tokens[$i]['code'] === \T_LNUMBER || $this->tokens[$i]['code'] === \T_DNUMBER) {
54✔
519
                $number   = Numbers::getCompleteNumber($phpcsFile, $i);
18✔
520
                $content .= $number['content'];
18✔
521
                $i        = $number['last_token'];
18✔
522
                continue;
18✔
523
            }
524

525
            // Account for heredoc with vars.
526
            if ($this->tokens[$i]['code'] === \T_START_HEREDOC) {
54✔
527
                $text = TextStrings::getCompleteTextString($phpcsFile, $i);
18✔
528

529
                // Check if there's a variable in the heredoc.
530
                if ($text !== TextStrings::stripEmbeds($text)) {
18✔
531
                    return;
6✔
532
                }
533

534
                for ($j = $i; $j <= $this->tokens[$i]['scope_closer']; $j++) {
12✔
535
                    $content .= $this->tokens[$j]['content'];
12✔
536
                }
4✔
537

538
                $i = $this->tokens[$i]['scope_closer'];
12✔
539
                continue;
12✔
540
            }
541

542
            $content .= $this->tokens[$i]['content'];
42✔
543
        }
14✔
544

545
        // The PHP_EOL is to prevent getting parse errors when the key is a heredoc/nowdoc.
546
        $key = eval('return ' . $content . ';' . \PHP_EOL);
48✔
547

548
        /*
549
         * Ok, so now we know the base value of the key, let's determine whether it is
550
         * an acceptable index key for an array and if not, what it would turn into.
551
         */
552

553
        switch (\gettype($key)) {
48✔
554
            case 'NULL':
48✔
555
                // An array key of `null` will become an empty string.
556
                return '';
6✔
557

558
            case 'boolean':
48✔
559
                return ($key === true) ? 1 : 0;
12✔
560

561
            case 'integer':
48✔
562
                return $key;
18✔
563

564
            case 'double':
48✔
565
                return (int) $key; // Will automatically cut off the decimal part.
18✔
566

567
            case 'string':
48✔
568
                if (Numbers::isDecimalInt($key) === true) {
48✔
569
                    return (int) $key;
18✔
570
                }
571

572
                return $key;
30✔
573

574
            default:
575
                /*
576
                 * Shouldn't be possible. Either way, if it's not one of the above types,
577
                 * this is not a key we can handle.
578
                 */
579
                return; // @codeCoverageIgnore
580
        }
581
    }
582
}
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