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

PHPCompatibility / PHPCompatibility / 18854982515

27 Oct 2025 08:28PM UTC coverage: 98.401%. Remained the same
18854982515

push

github

web-flow
Merge pull request #1947 from PHPCompatibility/php-8.5/newfunctionparam-grapheme-locale

PHP 8.5 | NewFunctionParameters: detect use of grapheme_*() $locale (RFC)

9231 of 9381 relevant lines covered (98.4%)

37.55 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
            } else {
492
                if ($this->isKeywordReservedPriorToPHP8($tokens[$i]['content']) === true) {
28✔
493
                    $this->checkName($phpcsFile, $i, $tokens[$i]['content']);
28✔
494
                    $this->checkOtherName($phpcsFile, $i, $tokens[$i]['content'], 'namespace declaration');
28✔
495
                }
8✔
496
            }
497
        }
8✔
498
    }
16✔
499

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

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

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

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

536
        return true;
32✔
537
    }
538

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

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

564
        $this->checkName($phpcsFile, $stackPtr, $name);
208✔
565

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

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

588
        $nameLC = \strtolower($name);
108✔
589

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

776
                $checkOtherLocal = true;
40✔
777

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

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

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

792
                continue;
40✔
793
            }
794

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

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

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

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

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

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

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

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

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

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

866
                $nextNonEmpty = $maybeUseNext;
16✔
867
            }
4✔
868

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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