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

PHPCompatibility / PHPCompatibility / 19664189793

25 Nov 2025 09:11AM UTC coverage: 98.424%. Remained the same
19664189793

push

github

web-flow
Merge pull request #1995 from PHPCompatibility/feature/ghactions-update-for-php-8.5-release

GH Actions: update for the release of PHP 8.5

9368 of 9518 relevant lines covered (98.42%)

37.41 hits per line

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

95.95
/PHPCompatibility/Sniffs/Keywords/ForbiddenNamesSniff.php
1
<?php
2
/**
3
 * PHPCompatibility, an external standard for PHP_CodeSniffer.
4
 *
5
 * @package   PHPCompatibility
6
 * @copyright 2012-2020 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\Keywords;
12

13
use PHPCompatibility\Helpers\ScannedCode;
14
use PHPCompatibility\Sniff;
15
use PHP_CodeSniffer\Files\File;
16
use PHP_CodeSniffer\Util\Tokens;
17
use PHPCSUtils\Tokens\Collections;
18
use PHPCSUtils\Utils\Conditions;
19
use PHPCSUtils\Utils\Constants;
20
use PHPCSUtils\Utils\FunctionDeclarations;
21
use PHPCSUtils\Utils\MessageHelper;
22
use PHPCSUtils\Utils\Namespaces;
23
use PHPCSUtils\Utils\ObjectDeclarations;
24
use PHPCSUtils\Utils\PassedParameters;
25
use PHPCSUtils\Utils\Scopes;
26
use PHPCSUtils\Utils\TextStrings;
27
use PHPCSUtils\Utils\UseStatements;
28

29
/**
30
 * Detects the use of reserved keywords as class, function, namespace or constant names.
31
 *
32
 * PHP version All
33
 *
34
 * @link https://www.php.net/manual/en/reserved.keywords.php
35
 *
36
 * @since 5.5
37
 * @since 10.0.0 - Strictly checks declarations and aliases only.
38
 *               - This class is now `final`.
39
 */
40
final class ForbiddenNamesSniff extends Sniff
41
{
42

43
    /**
44
     * A list of keywords that can not be used as function, class and namespace name or constant name.
45
     * Mentions since which version it's not allowed.
46
     *
47
     * @since 5.5
48
     *
49
     * @var array<string, string>
50
     */
51
    protected $invalidNames = [
52
        'abstract'      => '5.0',
53
        'and'           => 'all',
54
        'array'         => 'all',
55
        'as'            => 'all',
56
        'break'         => 'all',
57
        'callable'      => '5.4',
58
        'case'          => 'all',
59
        'catch'         => '5.0',
60
        'class'         => 'all',
61
        'clone'         => '5.0',
62
        'const'         => 'all',
63
        'continue'      => 'all',
64
        'declare'       => 'all',
65
        'default'       => 'all',
66
        'die'           => 'all',
67
        'do'            => 'all',
68
        'echo'          => 'all',
69
        'else'          => 'all',
70
        'elseif'        => 'all',
71
        'empty'         => 'all',
72
        'enddeclare'    => 'all',
73
        'endfor'        => 'all',
74
        'endforeach'    => 'all',
75
        'endif'         => 'all',
76
        'endswitch'     => 'all',
77
        'endwhile'      => 'all',
78
        'eval'          => 'all',
79
        'exit'          => 'all',
80
        'extends'       => 'all',
81
        'final'         => '5.0',
82
        'finally'       => '5.5',
83
        'fn'            => '7.4',
84
        'for'           => 'all',
85
        'foreach'       => 'all',
86
        'function'      => 'all',
87
        'global'        => 'all',
88
        'goto'          => '5.3',
89
        'if'            => 'all',
90
        'implements'    => '5.0',
91
        'include'       => 'all',
92
        'include_once'  => 'all',
93
        'instanceof'    => '5.0',
94
        'insteadof'     => '5.4',
95
        'interface'     => '5.0',
96
        'isset'         => 'all',
97
        'list'          => 'all',
98
        'match'         => '8.0',
99
        'namespace'     => '5.3',
100
        'new'           => 'all',
101
        'or'            => 'all',
102
        'print'         => 'all',
103
        'private'       => '5.0',
104
        'protected'     => '5.0',
105
        'public'        => '5.0',
106
        'readonly'      => '8.1',
107
        'require'       => 'all',
108
        'require_once'  => 'all',
109
        'return'        => 'all',
110
        'static'        => 'all',
111
        'switch'        => 'all',
112
        'throw'         => '5.0',
113
        'trait'         => '5.4',
114
        'try'           => '5.0',
115
        'unset'         => 'all',
116
        'use'           => 'all',
117
        'var'           => 'all',
118
        'while'         => 'all',
119
        'xor'           => 'all',
120
        'yield'         => '5.5',
121
        '__class__'     => 'all',
122
        '__dir__'       => '5.3',
123
        '__file__'      => 'all',
124
        '__function__'  => 'all',
125
        '__line__'      => 'all',
126
        '__method__'    => 'all',
127
        '__namespace__' => '5.3',
128
        '__trait__'     => '5.4',
129
    ];
130

131
    /**
132
     * Other keywords to recognize as forbidden names.
133
     *
134
     * These keywords cannot be used to name a class, interface or trait.
135
     * Prior to PHP 8.0, they were also prohibited from being used in namespaces.
136
     *
137
     * @since 7.0.8
138
     * @since 10.0.0 Moved from the ForbiddenNamesAsDeclared sniff to this sniff.
139
     *
140
     * @var array<string, string>
141
     */
142
    protected $otherForbiddenNames = [
143
        'parent'   => '5.0',
144
        'self'     => '5.0',
145
        'null'     => '7.0',
146
        'true'     => '7.0',
147
        'false'    => '7.0',
148
        'bool'     => '7.0',
149
        'int'      => '7.0',
150
        'float'    => '7.0',
151
        'string'   => '7.0',
152
        'iterable' => '7.1',
153
        'void'     => '7.1',
154
        'object'   => '7.2',
155
        'mixed'    => '8.0',
156
        'never'    => '8.1',
157
    ];
158

159
    /**
160
     * Keywords to recognize as soft reserved names.
161
     *
162
     * Using any of these keywords to name a class, interface, trait or namespace
163
     * is highly discouraged since they may be used in future versions of PHP.
164
     *
165
     * @since 7.0.8
166
     * @since 10.0.0 Moved from the ForbiddenNamesAsDeclared sniff to this sniff.
167
     *
168
     * @var array<string, string>
169
     */
170
    protected $softReservedNames = [
171
        'resource' => '7.0',
172
        'object'   => '7.0',
173
        'mixed'    => '7.0',
174
        'numeric'  => '7.0',
175
        'enum'     => '8.1',
176
    ];
177

178
    /**
179
     * Combined list of the two lists above.
180
     *
181
     * Used for quick check whether or not something is a reserved
182
     * word.
183
     * Set from the `register()` method.
184
     *
185
     * @since 7.0.8
186
     * @since 10.0.0 Moved from the ForbiddenNamesAsDeclared sniff to this sniff.
187
     *
188
     * @var array<string, string>
189
     */
190
    private $allOtherForbiddenNames = [];
191

192
    /**
193
     * A list of keywords that can follow use statements.
194
     *
195
     * @since 7.0.1
196
     *
197
     * @var array<string, true>
198
     */
199
    protected $validUseNames = [
200
        'const'    => true,
201
        'function' => true,
202
    ];
203

204
    /**
205
     * Scope modifiers and other keywords allowed in trait use statements.
206
     *
207
     * @since 7.1.4
208
     *
209
     * @var array<int|string, int|string>
210
     */
211
    private $allowedModifiers = [];
212

213
    /**
214
     * Targeted tokens.
215
     *
216
     * @since 5.5
217
     *
218
     * @var array<int|string>
219
     */
220
    protected $targetedTokens = [
221
        \T_NAMESPACE,
222
        \T_CLASS,
223
        \T_INTERFACE,
224
        \T_TRAIT,
225
        \T_ENUM,
226
        \T_FUNCTION,
227
        \T_CONST,
228
        \T_STRING, // Function calls to `define()`.
229
        \T_NAME_FULLY_QUALIFIED, // FQN function calls to `define()`.
230
        \T_USE,
231
        \T_ANON_CLASS, // Only for a specific tokenizer issue.
232
    ];
233

234
    /**
235
     * Returns an array of tokens this test wants to listen for.
236
     *
237
     * @since 5.5
238
     *
239
     * @return array<int|string>
240
     */
241
    public function register()
216✔
242
    {
243
        $this->allowedModifiers           = Tokens::$scopeModifiers;
216✔
244
        $this->allowedModifiers[\T_FINAL] = \T_FINAL;
216✔
245

246
        // Do the "other reserved keywords" list merge only once.
247
        $this->allOtherForbiddenNames = \array_merge($this->otherForbiddenNames, $this->softReservedNames);
216✔
248

249
        return $this->targetedTokens;
216✔
250
    }
251

252
    /**
253
     * Processes this test, when one of its tokens is encountered.
254
     *
255
     * @since 5.5
256
     *
257
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
258
     * @param int                         $stackPtr  The position of the current token in the
259
     *                                               stack passed in $tokens.
260
     *
261
     * @return void
262
     */
263
    public function process(File $phpcsFile, $stackPtr)
264✔
264
    {
265
        $tokens = $phpcsFile->getTokens();
264✔
266

267
        switch ($tokens[$stackPtr]['code']) {
264✔
268
            case \T_NAMESPACE:
66✔
269
                $this->processNamespaceDeclaration($phpcsFile, $stackPtr);
152✔
270
                return;
152✔
271

272
            case \T_CLASS:
66✔
273
            case \T_INTERFACE:
66✔
274
            case \T_TRAIT:
66✔
275
            case \T_ENUM:
66✔
276
                $this->processOODeclaration($phpcsFile, $stackPtr);
208✔
277
                return;
208✔
278

279
            case \T_FUNCTION:
128✔
280
                $this->processFunctionDeclaration($phpcsFile, $stackPtr);
112✔
281
                return;
112✔
282

283
            case \T_CONST:
128✔
284
                $this->processConstDeclaration($phpcsFile, $stackPtr);
104✔
285
                return;
104✔
286

287
            case \T_STRING:
128✔
288
                /*
289
                 * Handle a very specific edge case `enum extends/implements`, where PHP itself does not
290
                 * correctly tokenize the keyword in PHP 8.1+.
291
                 * Additionally, handle that the PHPCS tokenizer does not backfill `enum` to `T_ENUM`
292
                 * when followed by a reserved keyword which can not be a valid name on PHP < 8.1.
293
                 * As these are edge cases/parse errors anyway, we cannot reasonably expect PHPCS to
294
                 * handle this.
295
                 */
296
                if (\strtolower($tokens[$stackPtr]['content']) === 'enum') {
256✔
297
                    $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
96✔
298
                    if ($tokens[$prevNonEmpty]['code'] === \T_DOUBLE_COLON
96✔
299
                        || $tokens[$prevNonEmpty]['code'] === \T_OBJECT_OPERATOR
96✔
300
                        || $tokens[$prevNonEmpty]['code'] === \T_NULLSAFE_OBJECT_OPERATOR
96✔
301
                        || $tokens[$prevNonEmpty]['code'] === \T_CLASS
96✔
302
                        || $tokens[$prevNonEmpty]['code'] === \T_INTERFACE
78✔
303
                        || $tokens[$prevNonEmpty]['code'] === \T_TRAIT
66✔
304
                        || $tokens[$prevNonEmpty]['code'] === \T_EXTENDS
58✔
305
                        || $tokens[$prevNonEmpty]['code'] === \T_IMPLEMENTS
56✔
306
                        || $tokens[$prevNonEmpty]['code'] === \T_NS_SEPARATOR
86✔
307
                    ) {
24✔
308
                        // Use of a construct named `enum`, not an enum declaration.
309
                        return;
56✔
310
                    }
311

312
                    $lastCondition = Conditions::getLastCondition($phpcsFile, $stackPtr);
56✔
313
                    if ($tokens[$lastCondition]['code'] === \T_USE) {
56✔
314
                        // Trait use conflict resolution. Ignore.
315
                        return;
4✔
316
                    }
317

318
                    $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
56✔
319
                    if ($nextNonEmpty === false) {
56✔
320
                        return;
×
321
                    }
322

323
                    $nextNonEmptyLC = \strtolower($tokens[$nextNonEmpty]['content']);
56✔
324
                    if (isset($this->invalidNames[$nextNonEmptyLC])) {
56✔
325
                        $this->checkName($phpcsFile, $stackPtr, $tokens[$nextNonEmpty]['content']);
16✔
326

327
                        $this->checkOtherName(
16✔
328
                            $phpcsFile,
16✔
329
                            $stackPtr,
16✔
330
                            $tokens[$nextNonEmpty]['content'],
16✔
331
                            $tokens[$stackPtr]['content'] . ' declaration'
16✔
332
                        );
12✔
333
                    }
4✔
334
                    return;
56✔
335
                }
336

337
                $this->processString($phpcsFile, $stackPtr);
256✔
338
                return;
256✔
339

340
            case \T_NAME_FULLY_QUALIFIED:
80✔
341
                $this->processString($phpcsFile, $stackPtr);
12✔
342
                return;
12✔
343

344
            case \T_USE:
80✔
345
                $type = UseStatements::getType($phpcsFile, $stackPtr);
128✔
346

347
                if ($type === 'closure') {
128✔
348
                    // Not interested in closure use.
349
                    return;
8✔
350
                }
351

352
                if ($type === 'import') {
128✔
353
                    $this->processUseImportStatement($phpcsFile, $stackPtr);
120✔
354
                    return;
120✔
355
                }
356

357
                if ($type === 'trait') {
32✔
358
                    $this->processUseTraitStatement($phpcsFile, $stackPtr);
32✔
359
                    return;
32✔
360
                }
361

362
                /*
363
                 * When keywords are used in trait import statements, it sometimes confuses the PHPCS tokenizer
364
                 * and the 'conditions' aren't always correctly set, so we need to do an additional check for
365
                 * the last condition potentially being a previous trait T_USE.
366
                 */
367
                $traitScopes = Tokens::$ooScopeTokens;
16✔
368
                unset($traitScopes[\T_INTERFACE]);
16✔
369

370
                if (Conditions::hasCondition($phpcsFile, $stackPtr, $traitScopes) === false) {
16✔
371
                    return;
×
372
                }
373

374
                $current = $stackPtr;
16✔
375
                do {
376
                    $current = Conditions::getLastCondition($phpcsFile, $current);
16✔
377
                    if ($current === false) {
16✔
378
                        return;
×
379
                    }
380
                } while ($tokens[$current]['code'] === \T_USE);
16✔
381

382
                if (isset($traitScopes[$tokens[$current]['code']]) === true) {
16✔
383
                    $this->processUseTraitStatement($phpcsFile, $stackPtr);
16✔
384
                }
4✔
385

386
                return;
16✔
387

388
            case \T_ANON_CLASS:
24✔
389
                /*
390
                 * Deal with anonymous classes - `class` before a reserved keyword is sometimes
391
                 * misidentified as `T_ANON_CLASS`.
392
                 */
393
                $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
44✔
394
                if ($prevNonEmpty !== false && $tokens[$prevNonEmpty]['code'] === \T_NEW) {
44✔
395
                    return;
16✔
396
                }
397

398
                // Ok, so this isn't really an anonymous class.
399
                $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
28✔
400
                if ($nextNonEmpty === false) {
28✔
401
                    return;
×
402
                }
403

404
                $this->checkName($phpcsFile, $stackPtr, $tokens[$nextNonEmpty]['content']);
28✔
405
                return;
28✔
406
        }
407
    }
408

409
    /**
410
     * Processes namespace declarations.
411
     *
412
     * @since 10.0.0
413
     *
414
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
415
     * @param int                         $stackPtr  The position of the current token in the
416
     *                                               stack passed in $tokens.
417
     *
418
     * @return void
419
     */
420
    protected function processNamespaceDeclaration(File $phpcsFile, $stackPtr)
152✔
421
    {
422
        /*
423
         * Note: explicitly only excluding use of the keyword as an operator, not the "undetermined"
424
         * type, as the "undetermined" cases are often exactly the type of errors this sniff is trying to detect.
425
         *
426
         * Also note: that is also the reason to determine the namespace name within this method and
427
         * not to use the `Namespaces::getDeclaredName()` method.
428
         */
429
        $type = Namespaces::getType($phpcsFile, $stackPtr);
152✔
430
        if ($type === 'operator') {
152✔
431
            return;
12✔
432
        }
433

434
        $endOfStatement = $phpcsFile->findNext(Collections::namespaceDeclarationClosers(), ($stackPtr + 1));
152✔
435
        if ($endOfStatement === false) {
152✔
436
            // Live coding or parse error.
437
            return;
×
438
        }
439

440
        $tokens = $phpcsFile->getTokens();
152✔
441
        $next   = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), ($endOfStatement + 1), true);
152✔
442
        if ($next === $endOfStatement || $tokens[$next]['code'] === \T_NS_SEPARATOR) {
152✔
443
            // Declaration of global namespace. I.e.: namespace {} or use as non-scoped operator.
444
            return;
60✔
445
        }
446

447
        /*
448
         * Deal with PHP 8 relaxing the rules.
449
         * "The namespace declaration will accept any name, including isolated reserved keywords.
450
         *  The only restriction is that the namespace name cannot start with a `namespace` segment"
451
         *
452
         * Note: keywords which didn't become reserved prior to PHP 8.0 should never be flagged
453
         * when used in namespace names, as they are not problematic in PHP < 8.0.
454
         */
455
        $nextContentLC = \strtolower($tokens[$next]['content']);
100✔
456
        if (ScannedCode::shouldRunOnOrBelow('7.4') === false
100✔
457
            && $nextContentLC !== 'namespace'
100✔
458
            && \strpos($nextContentLC, 'namespace\\') !== 0 // PHPCS 4.x.
100✔
459
        ) {
26✔
460
            return;
68✔
461
        }
462

463
        for ($i = $next; $i < $endOfStatement; $i++) {
32✔
464
            if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true
32✔
465
                || $tokens[$i]['code'] === \T_NS_SEPARATOR
32✔
466
            ) {
8✔
467
                continue;
20✔
468
            }
469

470
            if (isset(Collections::nameTokens()[$tokens[$i]['code']]) === true
32✔
471
                && $tokens[$i]['code'] !== \T_STRING
32✔
472
            ) {
8✔
473
                /*
474
                 * This is a PHP 8.0 "namespaced name as single token" token (PHPCS 4.x).
475
                 * This also means that there can be no whitespace or comments in the name.
476
                 */
477
                $parts = \explode('\\', $tokens[$i]['content']);
12✔
478
                $parts = \array_filter($parts); // Remove empties.
12✔
479

480
                if (empty($parts)) {
12✔
481
                    // Shouldn't be possible, but just in case.
482
                    continue;
×
483
                }
484

485
                foreach ($parts as $part) {
12✔
486
                    if ($this->isKeywordReservedPriorToPHP8($part) === true) {
12✔
487
                        $this->checkName($phpcsFile, $i, $part);
12✔
488
                        $this->checkOtherName($phpcsFile, $i, $part, 'namespace declaration');
12✔
489
                    }
490
                }
491
            } elseif ($this->isKeywordReservedPriorToPHP8($tokens[$i]['content']) === true) {
28✔
492
                $this->checkName($phpcsFile, $i, $tokens[$i]['content']);
28✔
493
                $this->checkOtherName($phpcsFile, $i, $tokens[$i]['content'], 'namespace declaration');
28✔
494
            }
8✔
495
        }
8✔
496
    }
16✔
497

498
    /**
499
     * Check if a keyword was marked as reserved prior to PHP 8.0.
500
     *
501
     * Helper method for the `processNamespaceDeclaration()` method.
502
     *
503
     * Keywords which didn't become reserved prior to PHP 8.0 should never be flagged
504
     * when used in namespace names, as they are not problematic in PHP < 8.0.
505
     *
506
     * @param string $name The name to check.
507
     *
508
     * @return bool
509
     */
510
    private function isKeywordReservedPriorToPHP8($name)
32✔
511
    {
512
        $nameLC = \strtolower($name);
32✔
513

514
        if (isset($this->invalidNames[$nameLC]) === true
32✔
515
            && $this->invalidNames[$nameLC] !== 'all'
32✔
516
            && \version_compare($this->invalidNames[$nameLC], '8.0', '>=')
32✔
517
        ) {
8✔
518
            return false;
8✔
519
        }
520

521
        if (isset($this->softReservedNames[$nameLC]) === true
32✔
522
            && \version_compare($this->softReservedNames[$nameLC], '8.0', '>=')
32✔
523
        ) {
8✔
524
            return false;
8✔
525
        }
526

527
        if (isset($this->otherForbiddenNames[$nameLC]) === true
32✔
528
            && isset($this->softReservedNames[$nameLC]) === false
32✔
529
            && \version_compare($this->otherForbiddenNames[$nameLC], '8.0', '>=')
32✔
530
        ) {
8✔
531
            return false;
8✔
532
        }
533

534
        return true;
32✔
535
    }
536

537
    /**
538
     * Processes class/trait/interface declarations.
539
     *
540
     * @since 10.0.0
541
     *
542
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
543
     * @param int                         $stackPtr  The position of the current token in the
544
     *                                               stack passed in $tokens.
545
     *
546
     * @return void
547
     */
548
    protected function processOODeclaration(File $phpcsFile, $stackPtr)
208✔
549
    {
550
        $tokens        = $phpcsFile->getTokens();
208✔
551
        $lastCondition = Conditions::getLastCondition($phpcsFile, $stackPtr);
208✔
552
        if ($tokens[$lastCondition]['code'] === \T_USE) {
208✔
553
            // Trait use conflict resolution. Ignore.
554
            return;
24✔
555
        }
556

557
        $name = ObjectDeclarations::getName($phpcsFile, $stackPtr);
208✔
558
        if (isset($name) === false || $name === '') {
208✔
559
            return;
4✔
560
        }
561

562
        $this->checkName($phpcsFile, $stackPtr, $name);
208✔
563

564
        $tokens = $phpcsFile->getTokens();
208✔
565
        $this->checkOtherName($phpcsFile, $stackPtr, $name, $tokens[$stackPtr]['content'] . ' declaration');
208✔
566
    }
104✔
567

568
    /**
569
     * Processes function and method declarations.
570
     *
571
     * @since 10.0.0
572
     *
573
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
574
     * @param int                         $stackPtr  The position of the current token in the
575
     *                                               stack passed in $tokens.
576
     *
577
     * @return void
578
     */
579
    protected function processFunctionDeclaration(File $phpcsFile, $stackPtr)
112✔
580
    {
581
        $name = FunctionDeclarations::getName($phpcsFile, $stackPtr);
112✔
582
        if (empty($name)) {
112✔
583
            return;
4✔
584
        }
585

586
        $nameLC = \strtolower($name);
108✔
587

588
        /*
589
         * Deal with `readonly` being a reserved keyword, but still being allowed
590
         * as a function name.
591
         *
592
         * @link https://github.com/php/php-src/pull/7468 (PHP 8.1)
593
         * @link https://github.com/php/php-src/pull/9512 (PHP 8.2 follow-up)
594
         */
595
        if ($nameLC === 'readonly') {
108✔
596
            return;
8✔
597
        }
598

599
        if (isset($this->invalidNames[$nameLC]) === false) {
108✔
600
            return;
108✔
601
        }
602

603
        /*
604
         * Deal with PHP 7 relaxing the rules.
605
         * "As of PHP 7.0.0 these keywords are allowed as property, constant, and method names
606
         * of classes, interfaces and traits."
607
         *
608
         * Note: keywords which didn't become reserved prior to PHP 7.0 should never be flagged
609
         * when used as method names, as they are not problematic in PHP < 7.0.
610
         */
611
        if (Scopes::isOOMethod($phpcsFile, $stackPtr) === true
40✔
612
            && (ScannedCode::shouldRunOnOrBelow('5.6') === false
36✔
613
                || ($this->invalidNames[$nameLC] !== 'all'
30✔
614
                && \version_compare($this->invalidNames[$nameLC], '7.0', '>=')))
34✔
615
        ) {
10✔
616
            return;
16✔
617
        }
618

619
        $this->checkName($phpcsFile, $stackPtr, $name);
24✔
620
    }
12✔
621

622
    /**
623
     * Processes global/class constant declarations using the `const` keyword.
624
     *
625
     * @since 10.0.0
626
     *
627
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
628
     * @param int                         $stackPtr  The position of the current token in the
629
     *                                               stack passed in $tokens.
630
     *
631
     * @return void
632
     */
633
    protected function processConstDeclaration(File $phpcsFile, $stackPtr)
104✔
634
    {
635
        $tokens       = $phpcsFile->getTokens();
104✔
636
        $isOOConstant = Scopes::isOOConstant($phpcsFile, $stackPtr);
104✔
637

638
        if ($isOOConstant === false) {
104✔
639
            // Non-class constant declared using the "const" keyword.
640
            $namePtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
88✔
641
            if ($namePtr === false) {
88✔
642
                // Live coding or parse error.
643
                return;
×
644
            }
645

646
            $name   = $tokens[$namePtr]['content'];
88✔
647
            $nameLc = \strtolower($name);
88✔
648
            if (isset($this->invalidNames[$nameLc]) === false) {
88✔
649
                return;
82✔
650
            }
651
        } else {
4✔
652
            // Class constants can be typed since PHP 8.3, so handle these separately.
653
            $properties = Constants::getProperties($phpcsFile, $stackPtr);
32✔
654
            $namePtr    = $properties['name_token'];
32✔
655
            $name       = $tokens[$namePtr]['content'];
32✔
656
            $nameLc     = \strtolower($name);
32✔
657
            if (isset($this->invalidNames[$nameLc]) === false) {
32✔
658
                return;
24✔
659
            }
660
        }
661

662
        /*
663
         * Deal with PHP 7 relaxing the rules.
664
         * "As of PHP 7.0.0 these keywords are allowed as property, constant, and method names
665
         * of classes, interfaces and traits, except that class may not be used as constant name."
666
         *
667
         * Note: keywords which didn't become reserved prior to PHP 7.0 should never be flagged
668
         * when used as OO constant names, as they are not problematic in PHP < 7.0.
669
         */
670
        if ($nameLc !== 'class'
30✔
671
            && $isOOConstant === true
40✔
672
            && (ScannedCode::shouldRunOnOrBelow('5.6') === false
38✔
673
                || ($this->invalidNames[$nameLc] !== 'all'
34✔
674
                && \version_compare($this->invalidNames[$nameLc], '7.0', '>=')))
36✔
675
        ) {
10✔
676
            return;
16✔
677
        }
678

679
        $this->checkName($phpcsFile, $namePtr, $name);
24✔
680
    }
12✔
681

682
    /**
683
     * Processes constant declarations via a function call to `define()`.
684
     *
685
     * @since 5.5
686
     * @since 10.0.0 - Removed the $tokens parameter.
687
     *               - Visibility changed from `public` to `protected`.
688
     *               - Now also handles T_NAME_FULLY_QUALIFIED tokens for PHPCS 4.x support.
689
     *
690
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
691
     * @param int                         $stackPtr  The position of the current token in the
692
     *                                               stack passed in $tokens.
693
     *
694
     * @return void
695
     */
696
    protected function processString(File $phpcsFile, $stackPtr)
256✔
697
    {
698
        $tokens = $phpcsFile->getTokens();
256✔
699

700
        // Look for function calls to `define()`.
701
        if (\strtolower(\ltrim($tokens[$stackPtr]['content'], '\\')) !== 'define') {
256✔
702
            return;
248✔
703
        }
704

705
        // Retrieve the define(d) constant name.
706
        $constantName = PassedParameters::getParameter($phpcsFile, $stackPtr, 1, 'constant_name');
24✔
707
        if ($constantName === false) {
24✔
708
            return;
×
709
        }
710

711
        $defineName = TextStrings::stripQuotes($constantName['clean']);
24✔
712
        $this->checkName($phpcsFile, $stackPtr, $defineName);
24✔
713
    }
12✔
714

715
    /**
716
     * Processes alias declarations in import use statements.
717
     *
718
     * @since 10.0.0
719
     *
720
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
721
     * @param int                         $stackPtr  The position of the current token in the
722
     *                                               stack passed in $tokens.
723
     *
724
     * @return void
725
     */
726
    protected function processUseImportStatement(File $phpcsFile, $stackPtr)
120✔
727
    {
728
        $tokens = $phpcsFile->getTokens();
120✔
729

730
        $endOfStatement = $phpcsFile->findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1));
120✔
731
        if ($endOfStatement === false) {
120✔
732
            // Live coding or parse error.
733
            return;
8✔
734
        }
735

736
        $checkOther   = true;
112✔
737
        $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), $endOfStatement, true);
112✔
738
        if (isset($this->validUseNames[$tokens[$nextNonEmpty]['content']]) === true) {
112✔
739
            $checkOther = false;
64✔
740
        }
16✔
741

742
        $checkOtherLocal = true;
112✔
743
        $nextPtr         = $stackPtr;
112✔
744
        $find            = [
56✔
745
            \T_AS             => \T_AS,
112✔
746
            \T_OPEN_USE_GROUP => \T_OPEN_USE_GROUP,
84✔
747
        ];
84✔
748

749
        //$current = ($stackPtr + 1);
750
        while (($nextPtr + 1) < $endOfStatement) {
112✔
751
            $nextPtr = $phpcsFile->findNext($find, ($nextPtr + 1), $endOfStatement);
112✔
752
            if ($nextPtr === false) {
112✔
753
                break;
64✔
754
            }
755

756
            /*
757
             * Group use statements can contain substatements for function/const imports.
758
             * These _do_ have to be checked for the fully reserved names, but the reservation on "other" names
759
             * does not apply.
760
             *
761
             * To allow for this, check the first non-empty token after a group use open bracket and after a
762
             * comma to see if it is the `function` or `const` keyword.
763
             *
764
             * Note: the T_COMMA token is only added to `$find` if we've seen a group use open bracket.
765
             */
766
            if ($tokens[$nextPtr]['code'] === \T_OPEN_USE_GROUP
104✔
767
                || $tokens[$nextPtr]['code'] === \T_COMMA
104✔
768
            ) {
26✔
769
                if ($checkOther === false) {
56✔
770
                    // Function/const applies to the whole group use statement.
771
                    continue;
16✔
772
                }
773

774
                $checkOtherLocal = true;
40✔
775

776
                $nextPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextPtr + 1), $endOfStatement, true);
40✔
777
                if ($nextPtr === false) {
40✔
778
                    // Group use with trailing comma.
779
                    break;
×
780
                }
781

782
                if (isset($this->validUseNames[$tokens[$nextPtr]['content']]) === true) {
40✔
783
                    $checkOtherLocal = false;
24✔
784
                }
6✔
785

786
                if ($tokens[$nextPtr]['code'] === \T_OPEN_USE_GROUP) {
40✔
787
                    $find[\T_COMMA] = \T_COMMA;
×
788
                }
789

790
                continue;
40✔
791
            }
792

793
            // Ok, so this must be an T_AS token.
794
            $nextPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextPtr + 1), $endOfStatement, true);
104✔
795
            if ($nextPtr === false) {
104✔
796
                break;
×
797
            }
798

799
            $this->checkName($phpcsFile, $nextPtr, $tokens[$nextPtr]['content']);
104✔
800

801
            if ($checkOther === false || $checkOtherLocal === false) {
104✔
802
                continue;
72✔
803
            }
804

805
            $this->checkOtherName($phpcsFile, $nextPtr, $tokens[$nextPtr]['content'], 'import use alias');
48✔
806
        }
12✔
807
    }
56✔
808

809
    /**
810
     * Processes alias declarations in trait use statements.
811
     *
812
     * @since 10.0.0
813
     *
814
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
815
     * @param int                         $stackPtr  The position of the current token in the
816
     *                                               stack passed in $tokens.
817
     *
818
     * @return void
819
     */
820
    protected function processUseTraitStatement(File $phpcsFile, $stackPtr)
32✔
821
    {
822
        $tokens    = $phpcsFile->getTokens();
32✔
823
        $openCurly = $phpcsFile->findNext([\T_OPEN_CURLY_BRACKET, \T_SEMICOLON], ($stackPtr + 1));
32✔
824
        if ($openCurly === false || $tokens[$openCurly]['code'] === \T_SEMICOLON) {
32✔
825
            return;
16✔
826
        }
827

828
        // OK, so we have an open curly, do we have a closer too ?.
829
        if (isset($tokens[$openCurly]['bracket_closer']) === false) {
32✔
830
            return;
×
831
        }
832

833
        $current = $stackPtr;
32✔
834
        $closer  = $tokens[$openCurly]['bracket_closer'];
32✔
835
        do {
836
            $asPtr = $phpcsFile->findNext(\T_AS, ($current + 1), $closer);
32✔
837
            if ($asPtr === false) {
32✔
838
                break;
32✔
839
            }
840

841
            $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($asPtr + 1), $closer, true);
32✔
842
            if ($nextNonEmpty === false) {
32✔
843
                break;
×
844
            }
845

846
            /*
847
             * Deal with visibility modifiers.
848
             * - `use HelloWorld { sayHello as protected; }` => valid.
849
             * - `use HelloWorld { sayHello as private myPrivateHello; }` => move to the next token to verify.
850
             */
851
            if (isset($this->allowedModifiers[$tokens[$nextNonEmpty]['code']]) === true) {
32✔
852
                $maybeUseNext = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), $closer, true);
24✔
853
                if ($maybeUseNext === false) {
24✔
854
                    // Reached the end of the use statement.
855
                    break;
×
856
                }
857

858
                if ($tokens[$maybeUseNext]['code'] === \T_SEMICOLON) {
24✔
859
                    // Reached the end of a sub-statement.
860
                    $current = $maybeUseNext;
24✔
861
                    continue;
24✔
862
                }
863

864
                $nextNonEmpty = $maybeUseNext;
16✔
865
            }
4✔
866

867
            $this->checkName($phpcsFile, $nextNonEmpty, $tokens[$nextNonEmpty]['content']);
24✔
868

869
            $current = $nextNonEmpty;
24✔
870
        } while ($current !== false && $current < $closer);
32✔
871
    }
16✔
872

873
    /**
874
     * Check whether a particular name is a reserved keyword.
875
     *
876
     * @since 10.0.0
877
     *
878
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
879
     * @param int                         $stackPtr  The position of the current token in the
880
     *                                               stack passed in $tokens.
881
     * @param string                      $name      The declaration/alias name found.
882
     *
883
     * @return void
884
     */
885
    protected function checkName(File $phpcsFile, $stackPtr, $name)
248✔
886
    {
887
        $name = \strtolower($name);
248✔
888
        if (isset($this->invalidNames[$name]) === false) {
248✔
889
            return;
232✔
890
        }
891

892
        if ($this->invalidNames[$name] === 'all'
216✔
893
            || ScannedCode::shouldRunOnOrAbove($this->invalidNames[$name]) === true
216✔
894
        ) {
54✔
895
            $this->addError($phpcsFile, $stackPtr, $name);
216✔
896
        }
54✔
897
    }
108✔
898

899
    /**
900
     * Add the error message.
901
     *
902
     * @since 7.1.0
903
     * @since 10.0.0 Removed the $data parameter.
904
     *
905
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
906
     * @param int                         $stackPtr  The position of the current token in the
907
     *                                               stack passed in $tokens.
908
     * @param string                      $name      The declaration/alias name found in lowercase.
909
     *
910
     * @return void
911
     */
912
    protected function addError(File $phpcsFile, $stackPtr, $name)
216✔
913
    {
914
        $error     = "Function name, class name, namespace name or constant name can not be reserved keyword '%s' (since version %s)";
216✔
915
        $errorCode = MessageHelper::stringToErrorCode($name, true) . 'Found';
216✔
916

917
        // Display the magic constants in uppercase.
918
        $msgName = $name;
216✔
919
        if ($name[0] === '_' && $name[1] === '_') {
216✔
920
            $msgName = \strtoupper($name);
208✔
921
        }
52✔
922

923
        $data = [
108✔
924
            $msgName,
216✔
925
            $this->invalidNames[$name],
216✔
926
        ];
162✔
927

928
        $phpcsFile->addError($error, $stackPtr, $errorCode, $data);
216✔
929
    }
108✔
930

931
    /**
932
     * Check whether a particular name is one of the "other" reserved keywords.
933
     *
934
     * @since 10.0.0 Moved from the ForbiddenNamesAsDeclared sniff to this sniff.
935
     *
936
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
937
     * @param int                         $stackPtr  The position of the current token in the
938
     *                                               stack passed in $tokens.
939
     * @param string                      $name      The declaration/alias name found.
940
     * @param string                      $type      The type of statement in which the keyword was found.
941
     *
942
     * @return void
943
     */
944
    protected function checkOtherName(File $phpcsFile, $stackPtr, $name, $type)
224✔
945
    {
946
        $name = \strtolower($name);
224✔
947
        if (isset($this->allOtherForbiddenNames[$name]) === false) {
224✔
948
            return;
224✔
949
        }
950

951
        if (ScannedCode::shouldRunOnOrAbove('7.0') === false) {
112✔
952
            return;
16✔
953
        }
954

955
        $this->addOtherReservedError($phpcsFile, $stackPtr, $name, $type);
96✔
956
    }
48✔
957

958
    /**
959
     * Add the error message for when one of the "other" reserved keywords is detected.
960
     *
961
     * @since 7.0.8
962
     * @since 10.0.0 Moved from the ForbiddenNamesAsDeclared sniff to this sniff.
963
     *
964
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
965
     * @param int                         $stackPtr  The position of the current token in the
966
     *                                               stack passed in $tokens.
967
     * @param string                      $name      The declaration/alias name found in lowercase.
968
     * @param string                      $type      The type of statement in which the keyword was found.
969
     *
970
     * @return void
971
     */
972
    protected function addOtherReservedError(File $phpcsFile, $stackPtr, $name, $type)
96✔
973
    {
974
        // Build up the error message.
975
        $error     = "'%s' is a";
96✔
976
        $isError   = null;
96✔
977
        $errorCode = MessageHelper::stringToErrorCode($name, true) . 'Found';
96✔
978
        $data      = [
48✔
979
            $name,
96✔
980
        ];
72✔
981

982
        if (isset($this->softReservedNames[$name]) === true
96✔
983
            && ScannedCode::shouldRunOnOrAbove($this->softReservedNames[$name]) === true
96✔
984
        ) {
24✔
985
            $error  .= ' soft reserved keyword as of PHP version %s';
96✔
986
            $isError = false;
96✔
987
            $data[]  = $this->softReservedNames[$name];
96✔
988
        }
24✔
989

990
        if (isset($this->otherForbiddenNames[$name]) === true
96✔
991
            && ScannedCode::shouldRunOnOrAbove($this->otherForbiddenNames[$name]) === true
96✔
992
        ) {
24✔
993
            if (isset($isError) === true) {
88✔
994
                $error .= ' and a';
88✔
995
            }
22✔
996
            $error  .= ' reserved keyword as of PHP version %s';
88✔
997
            $isError = true;
88✔
998
            $data[]  = $this->otherForbiddenNames[$name];
88✔
999
        }
22✔
1000

1001
        if (isset($isError) === true) {
96✔
1002
            $error .= ' and should not be used to name a class, interface or trait or as part of a namespace (%s)';
96✔
1003
            $data[] = $type;
96✔
1004

1005
            MessageHelper::addMessage($phpcsFile, $error, $stackPtr, $isError, $errorCode, $data);
96✔
1006
        }
24✔
1007
    }
48✔
1008
}
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

© 2026 Coveralls, Inc