• 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

99.03
/PHPCompatibility/Sniffs/Attributes/NewAttributesSniff.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\Attributes;
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

19
/**
20
 * Attributes as a form of structured, syntactic metadata to declarations of classes, properties,
21
 * functions, methods, parameters and constants is supported as of PHP 8.0.
22
 *
23
 * Attributes have been implemented in a manner which makes them largely cross-version
24
 * compatible (no parse error) with older PHP versions, however, there are a few exceptions.
25
 * Most notably, the following usages of attributes are not cross-version compatible:
26
 * - Multi-line attributes.
27
 * - Inline attributes, i.e. an attribute followed by other code on the same line.
28
 * - Attributes containing (something which looks like) a PHP close tag in a string argument
29
 *   passed to the attribute on the same line as the attribute opener.
30
 *
31
 * Aside from that, the functionality attached to the attribute will (obviously) not work
32
 * when the code is run on PHP < 8.0.
33
 *
34
 * {@internal This sniff does not check whether attributes are used correctly and in
35
 * combination with syntaxes for which attributes are valid.
36
 * If that's not the case, PHP 8.0 would throw a parse error anyway.}
37
 *
38
 * PHP version 8.0
39
 *
40
 * @link https://wiki.php.net/rfc/attributes_v2
41
 * @link https://wiki.php.net/rfc/attribute_amendments
42
 * @link https://wiki.php.net/rfc/shorter_attribute_syntax
43
 * @link https://wiki.php.net/rfc/shorter_attribute_syntax_change
44
 * @link https://www.php.net/manual/en/language.attributes.php
45
 *
46
 * @since 10.0.0
47
 */
48
final class NewAttributesSniff extends Sniff
49
{
50

51
    /**
52
     * List of PHP native attributes which were introduced to allow for making code cross-version compatible.
53
     *
54
     * These attributes can be safely ignored.
55
     *
56
     * @since 10.0.0
57
     *
58
     * @var array<string, string> Key is the attribute name, value is the PHP version in which the attribute was introduced.
59
     */
60
    private $phpCrossVersionAttributes = [
61
        'AllowDynamicProperties' => '8.2',
62
        'ReturnTypeWillChange'   => '8.1',
63
    ];
64

65
    /**
66
     * List of other PHP native attributes.
67
     *
68
     * These attributes will be bundled into one error code to allow for easily selectively ignoring them.
69
     *
70
     * Source of the attributes list: https://www.php.net/manual/en/reserved.attributes.php
71
     *
72
     * @since 10.0.0
73
     *
74
     * @var array<string, string> Key is the attribute name, value is the PHP version in which the attribute was introduced.
75
     */
76
    private $otherPHPNativeAttributes = [
77
        'Attribute'          => '8.0',
78
        'Override'           => '8.3',
79
        'SensitiveParameter' => '8.2',
80
    ];
81

82
    /**
83
     * List of Doctrine ORM native attributes.
84
     *
85
     * These attributes will be bundled into one error code to allow for easily selectively ignoring them.
86
     *
87
     * {@internal Once the PHPCSUtils name resolution with namespace and import statements becomes available,
88
     * this list should no longer be necessary and all Doctrine attributes can be ignored based on their
89
     * fully qualified name starting with "Doctrine\ORM\Mapping".}
90
     *
91
     * Source of the attributes list: https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/attributes-reference.html
92
     *
93
     * This list is ordered as per the documentation for maintainability.
94
     *
95
     * @since 10.0.0
96
     *
97
     * @var array<string, bool> Key is the attribute name, value is irrelevant.
98
     */
99
    private $doctrineAttributes = [
100
        'AssociationOverride'   => true,
101
        'AttributeOverride'     => true,
102
        'Column'                => true,
103
        'Cache'                 => true,
104
        'ChangeTrackingPolicy'  => true,
105
        'CustomIdGenerator'     => true,
106
        'DiscriminatorColumn'   => true,
107
        'DiscriminatorMap'      => true,
108
        'Embeddable'            => true,
109
        'Embedded'              => true,
110
        'Entity'                => true,
111
        'GeneratedValue'        => true,
112
        'HasLifecycleCallbacks' => true,
113
        'Index'                 => true,
114
        'Id'                    => true,
115
        'InheritanceType'       => true,
116
        'InverseJoinColumn'     => true,
117
        'JoinColumn'            => true,
118
        'JoinTable'             => true,
119
        'ManyToOne'             => true,
120
        'ManyToMany'            => true,
121
        'MappedSuperclass'      => true,
122
        'OneToOne'              => true,
123
        'OneToMany'             => true,
124
        'OrderBy'               => true,
125
        'PostLoad'              => true,
126
        'PostPersist'           => true,
127
        'PostRemove'            => true,
128
        'PostUpdate'            => true,
129
        'PrePersist'            => true,
130
        'PreRemove'             => true,
131
        'PreUpdate'             => true,
132
        'SequenceGenerator'     => true,
133
        'Table'                 => true,
134
        'UniqueConstraint'      => true,
135
        'Version'               => true,
136
    ];
137

138
    /**
139
     * List of PHPStorm native attributes.
140
     *
141
     * These attributes will bundled into one error code to allow for easily selectively ignoring them.
142
     *
143
     * {@internal Once the PHPCSUtils name resolution with namespace and import statements becomes available,
144
     * this list should no longer be necessary and all PHPStorm attributes can be ignored based on their
145
     * fully qualified name starting with "JetBrains\PhpStorm".}
146
     *
147
     * Source of the attributes list: https://github.com/JetBrains/phpstorm-attributes
148
     *
149
     * This list is ordered as per the documentation for maintainability.
150
     *
151
     * @since 10.0.0
152
     *
153
     * @var array<string, bool> Key is the attribute name, value is irrelevant.
154
     */
155
    private $phpstormAttributes = [
156
        'ArrayShape'     => true,
157
        'Deprecated'     => true,
158
        'ExpectedValues' => true,
159
        'Immutable'      => true,
160
        'Language'       => true,
161
        'NoReturn'       => true,
162
        'ObjectShape'    => true,
163
        'Pure'           => true,
164
    ];
165

166
    /**
167
     * List of PHPUnit native attributes.
168
     *
169
     * These attributes will be bundled into one error code to allow for easily selectively ignoring them.
170
     *
171
     * {@internal Once the PHPCSUtils name resolution with namespace and import statements becomes available,
172
     * this list should no longer be necessary and all PHPUnit attributes can be ignored based on their
173
     * fully qualified name starting with "PHPUnit\Framework\Attributes".}
174
     *
175
     * Source of the attributes list: https://docs.phpunit.de/en/main/attributes.html
176
     *
177
     * This list is ordered as per the documentation for maintainability.
178
     *
179
     * @since 10.0.0
180
     *
181
     * @var array<string, bool> Key is the attribute name, value is irrelevant.
182
     */
183
    private $phpunitAttributes = [
184
        // Generic.
185
        'Test'                                       => true,
186
        'DisableReturnValueGenerationForTestDoubles' => true,
187
        'DoesNotPerformAssertions'                   => true,
188
        'IgnoreDeprecations'                         => true,
189
        'WithoutErrorHandler'                        => true,
190

191
        // Code Coverage.
192
        'CoversClass'                                => true,
193
        'CoversClassesThatImplementInterface'        => true,
194
        'CoversClassesThatExtendClass'               => true,
195
        'CoversTrait'                                => true,
196
        'CoversMethod'                               => true,
197
        'CoversFunction'                             => true,
198
        'CoversNothing'                              => true,
199
        'UsesClass'                                  => true,
200
        'UsesClassesThatImplementInterface'          => true,
201
        'UsesClassesThatExtendClass'                 => true,
202
        'UsesTrait'                                  => true,
203
        'UsesMethod'                                 => true,
204
        'UsesFunction'                               => true,
205

206
        // Data Provider.
207
        'DataProvider'                               => true,
208
        'DataProviderExternal'                       => true,
209
        'TestWith'                                   => true,
210
        'TestWithJson'                               => true,
211

212
        // Test Dependencies.
213
        'Depends'                                    => true,
214
        'DependsUsingDeepClone'                      => true,
215
        'DependsUsingShallowClone'                   => true,
216
        'DependsExternal'                            => true,
217
        'DependsExternalUsingDeepClone'              => true,
218
        'DependsExternalUsingShallowClone'           => true,
219
        'DependsOnClass'                             => true,
220
        'DependsOnClassUsingDeepClone'               => true,
221
        'DependsOnClassUsingShallowClone'            => true,
222

223
        // TestDox.
224
        'TestDox'                                    => true,
225
        'TestDoxFormatter'                           => true,
226
        'TestDoxFormatterExternal'                   => true,
227

228
        // Test Groups.
229
        'Group'                                      => true,
230
        'Small'                                      => true,
231
        'Medium'                                     => true,
232
        'Large'                                      => true,
233
        'Ticket'                                     => true,
234

235
        // Template Methods.
236
        'BeforeClass'                                => true,
237
        'Before'                                     => true,
238
        'PreCondition'                               => true,
239
        'PostCondition'                              => true,
240
        'After'                                      => true,
241
        'AfterClass'                                 => true,
242

243
        // Test Isolation.
244
        'BackupGlobals'                              => true,
245
        'ExcludeGlobalVariableFromBackup'            => true,
246
        'BackupStaticProperties'                     => true,
247
        'ExcludeStaticPropertyFromBackup'            => true,
248
        'RunInSeparateProcess'                       => true,
249
        'RunTestsInSeparateProcesses'                => true,
250
        'RunClassInSeparateProcess'                  => true,
251
        'PreserveGlobalState'                        => true,
252

253
        // Skipping Tests.
254
        'RequiresPhp'                                => true,
255
        'RequiresPhpExtension'                       => true,
256
        'RequiresSetting'                            => true,
257
        'RequiresPhpunit'                            => true,
258
        'RequiresPhpunitExtension'                   => true,
259
        'RequiresFunction'                           => true,
260
        'RequiresMethod'                             => true,
261
        'RequiresOperatingSystem'                    => true,
262
        'RequiresOperatingSystemFamily'              => true,
263
        'RequiresEnvironmentVariable'                => true,
264
    ];
265

266
    /**
267
     * Returns an array of tokens this test wants to listen for.
268
     *
269
     * @since 10.0.0
270
     *
271
     * @return array<int|string>
272
     */
273
    public function register()
16✔
274
    {
275
        // Handle case-insensitivity of attribute names.
276
        $this->phpCrossVersionAttributes = \array_change_key_case($this->phpCrossVersionAttributes, \CASE_LOWER);
16✔
277
        $this->otherPHPNativeAttributes  = \array_change_key_case($this->otherPHPNativeAttributes, \CASE_LOWER);
16✔
278
        $this->doctrineAttributes        = \array_change_key_case($this->doctrineAttributes, \CASE_LOWER);
16✔
279
        $this->phpstormAttributes        = \array_change_key_case($this->phpstormAttributes, \CASE_LOWER);
16✔
280
        $this->phpunitAttributes         = \array_change_key_case($this->phpunitAttributes, \CASE_LOWER);
16✔
281

282
        return [\T_ATTRIBUTE];
16✔
283
    }
284

285
    /**
286
     * Processes this test, when one of its tokens is encountered.
287
     *
288
     * @since 10.0.0
289
     *
290
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
291
     * @param int                         $stackPtr  The position of the current token in the
292
     *                                               stack passed in $tokens.
293
     *
294
     * @return void
295
     */
296
    public function process(File $phpcsFile, $stackPtr)
32✔
297
    {
298
        if (ScannedCode::shouldRunOnOrBelow('7.4') === false) {
32✔
299
            return;
16✔
300
        }
301

302
        $tokens = $phpcsFile->getTokens();
16✔
303

304
        if (isset($tokens[$stackPtr]['attribute_closer']) === false) {
16✔
305
            // Live coding or parse error. Shouldn't be possible as shouldn't have retokenized in that case.
306
            return; // @codeCoverageIgnore
307
        }
308

309
        $opener = $stackPtr;
16✔
310
        $closer = $tokens[$stackPtr]['attribute_closer'];
16✔
311

312
        /*
313
         * Check if the attribute is cross-version compatible with PHP < 8.0.
314
         */
315
        if ($tokens[$opener]['line'] !== $tokens[$closer]['line']) {
16✔
316
            $phpcsFile->addError(
16✔
317
                'Multi-line attributes will result in a parse error in PHP 7.4 and earlier.',
16✔
318
                $opener,
16✔
319
                'FoundMultiLine'
12✔
320
            );
12✔
321
        }
4✔
322

323
        $nextAfter = $phpcsFile->findNext(Tokens::$emptyTokens, ($closer + 1), null, true);
16✔
324
        if ($nextAfter !== false
12✔
325
            && $tokens[$nextAfter]['code'] !== \T_ATTRIBUTE
16✔
326
            && $tokens[$closer]['line'] === $tokens[$nextAfter]['line']
16✔
327
        ) {
4✔
328
            $phpcsFile->addError(
16✔
329
                'Code after an inline attribute on the same line will be ignored in PHP 7.4 and earlier and is likely to cause a parse error or functional error.',
16✔
330
                $closer,
16✔
331
                'FoundInline'
12✔
332
            );
12✔
333
        }
4✔
334

335
        $textPtr = $opener;
16✔
336
        while (($textPtr = $phpcsFile->findNext(\T_CONSTANT_ENCAPSED_STRING, ($textPtr + 1), $closer)) !== false) {
16✔
337
            if ($tokens[$textPtr]['line'] !== $tokens[$opener]['line']) {
16✔
338
                // We only need to examine text strings on the same line as the opener.
339
                break;
16✔
340
            }
341

342
            if (\strpos($tokens[$textPtr]['content'], '?>') !== false) {
16✔
343
                $phpcsFile->addError(
8✔
344
                    'Text string containing text which looks like a PHP close tag found on the same line as an attribute opener. This will cause PHP to switch to inline HTML in PHP 7.4 and earlier, which may lead to code exposure and will break functionality. Found: %s',
8✔
345
                    $textPtr,
8✔
346
                    'FoundCloseTag',
8✔
347
                    [$tokens[$textPtr]['content']]
8✔
348
                );
6✔
349
            }
2✔
350
        }
4✔
351

352
        /*
353
         * Collect the names of all attribute classes referenced.
354
         */
355
        $attributeNames = [];
16✔
356
        $currentName    = '';
16✔
357
        $startsAt       = null;
16✔
358

359
        for ($i = ($opener + 1); $i <= $closer; $i++) {
16✔
360
            if (isset(Tokens::$emptyTokens[$tokens[$i]['code']])) {
16✔
361
                continue;
16✔
362
            }
363

364
            if ($tokens[$i]['code'] === \T_OPEN_PARENTHESIS) {
16✔
365
                if (isset($tokens[$i]['parenthesis_closer']) === false) {
16✔
366
                    // Shouldn't be possible as in that case the attribute opener is not linked with the closer, but just in case.
367
                    break;
×
368
                }
369

370
                // Skip over whatever is passed to the Attribute constructor.
371
                $i = $tokens[$i]['parenthesis_closer'];
16✔
372
                continue;
16✔
373
            }
374

375
            if ($tokens[$i]['code'] === \T_COMMA
16✔
376
                || $i === $closer
16✔
377
            ) {
4✔
378
                // We've reached the end of the name.
379
                if ($currentName === '') {
16✔
380
                    // Parse error. Stop parsing this attribute.
381
                    break;
8✔
382
                }
383

384
                $attributeNames[$startsAt] = $currentName;
16✔
385
                $currentName               = '';
16✔
386
                $startsAt                  = null;
16✔
387
                continue;
16✔
388
            }
389

390
            if (isset(Collections::namespacedNameTokens()[$tokens[$i]['code']])) {
16✔
391
                $currentName .= $tokens[$i]['content'];
16✔
392

393
                if (isset($startsAt) === false) {
16✔
394
                    $startsAt = $i;
16✔
395
                }
4✔
396
            }
4✔
397
        }
4✔
398

399
        if (empty($attributeNames)) {
16✔
400
            // Parse error. Shouldn't be possible.
401
            return;
8✔
402
        }
403

404
        // Lowercase all found attributes to allow for case-insensitive name comparisons.
405
        $attributeNamesLC = \array_map('strtolower', $attributeNames);
16✔
406

407
        /*
408
         * Throw a warning for each attribute class encountered.
409
         */
410
        foreach ($attributeNamesLC as $startPtr => $attributeName) {
16✔
411
            $attributeName = \ltrim($attributeName, '\\');
16✔
412

413
            if (isset($this->phpCrossVersionAttributes[$attributeName])) {
16✔
414
                // Attribute referencing a PHP native cross-version compatibility feature. Ignore.
415
                continue;
8✔
416
            }
417

418
            if (\stripos($attributeName, 'PHPUnit\\Framework\\Attributes\\') === 0
16✔
419
                || isset($this->phpunitAttributes[$attributeName])
16✔
420
            ) {
4✔
421
                // Either a fully qualified PHPUnit attribute or an unqualified PHPUnit attribute.
422
                $errorCode = 'PHPUnitAttributeFound';
8✔
423
            } elseif (\stripos($attributeName, 'Doctrine\\ORM\\Mapping\\') === 0
16✔
424
                || isset($this->doctrineAttributes[$attributeName])
16✔
425
            ) {
4✔
426
                // Either a fully qualified Doctrine attribute or an unqualified Doctrine attribute.
427
                $errorCode = 'DoctrineORMAttributeFound';
8✔
428
            } elseif (\stripos($attributeName, 'JetBrains\\PhpStorm\\') === 0
16✔
429
                || isset($this->phpstormAttributes[$attributeName])
16✔
430
            ) {
4✔
431
                // Either a fully qualified PHPStorm attribute or an unqualified PHPStorm attribute.
432
                $errorCode = 'PHPStormAttributeFound';
8✔
433
            } elseif (isset($this->otherPHPNativeAttributes[$attributeName])) {
16✔
434
                // A PHP native attribute, not introduced for PHP cross-version compatibility.
435
                $errorCode = 'PHPNativeAttributeFound';
8✔
436
            } else {
2✔
437
                // All other attributes.
438
                $errorCode = 'Found';
16✔
439
            }
440

441
            $phpcsFile->addWarning(
16✔
442
                'Attributes are not supported in PHP 7.4 or earlier. They will be ignored and the application may not work as expected. Found: %s',
16✔
443
                $startPtr,
16✔
444
                $errorCode,
16✔
445
                ['#[' . $attributeNames[$startPtr] . '...]']
16✔
446
            );
12✔
447
        }
4✔
448
    }
8✔
449
}
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