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

keradus / PHP-CS-Fixer / 17377459942

01 Sep 2025 12:19PM UTC coverage: 94.684% (-0.009%) from 94.693%
17377459942

push

github

web-flow
chore: `Tokens::offsetSet` - explicit validation of input (#9004)

1 of 5 new or added lines in 1 file covered. (20.0%)

306 existing lines in 60 files now uncovered.

28390 of 29984 relevant lines covered (94.68%)

45.5 hits per line

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

99.28
/src/Fixer/ConstantNotation/NativeConstantInvocationFixer.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <fabien@symfony.com>
9
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14

15
namespace PhpCsFixer\Fixer\ConstantNotation;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
20
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
22
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
23
use PhpCsFixer\FixerDefinition\CodeSample;
24
use PhpCsFixer\FixerDefinition\FixerDefinition;
25
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
26
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
27
use PhpCsFixer\Tokenizer\Token;
28
use PhpCsFixer\Tokenizer\Tokens;
29
use PhpCsFixer\Tokenizer\TokensAnalyzer;
30
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
31

32
/**
33
 * @phpstan-type _AutogeneratedInputConfiguration array{
34
 *  exclude?: list<string>,
35
 *  fix_built_in?: bool,
36
 *  include?: list<string>,
37
 *  scope?: 'all'|'namespaced',
38
 *  strict?: bool,
39
 * }
40
 * @phpstan-type _AutogeneratedComputedConfiguration array{
41
 *  exclude: list<string>,
42
 *  fix_built_in: bool,
43
 *  include: list<string>,
44
 *  scope: 'all'|'namespaced',
45
 *  strict: bool,
46
 * }
47
 *
48
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
49
 *
50
 * @author Filippo Tessarotto <zoeslam@gmail.com>
51
 *
52
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
53
 */
54
final class NativeConstantInvocationFixer extends AbstractFixer implements ConfigurableFixerInterface
55
{
56
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
57
    use ConfigurableFixerTrait;
58

59
    /**
60
     * @var array<string, true>
61
     */
62
    private array $constantsToEscape = [];
63

64
    /**
65
     * @var array<string, true>
66
     */
67
    private array $caseInsensitiveConstantsToEscape = [];
68

69
    public function getDefinition(): FixerDefinitionInterface
70
    {
71
        return new FixerDefinition(
3✔
72
            'Add leading `\` before constant invocation of internal constant to speed up resolving. Constant name match is case-sensitive, except for `null`, `false` and `true`.',
3✔
73
            [
3✔
74
                new CodeSample("<?php var_dump(PHP_VERSION, M_PI, MY_CUSTOM_PI);\n"),
3✔
75
                new CodeSample(
3✔
76
                    <<<'PHP'
3✔
77
                        <?php
78
                        namespace space1 {
79
                            echo PHP_VERSION;
80
                        }
81
                        namespace {
82
                            echo M_PI;
83
                        }
84

85
                        PHP,
3✔
86
                    ['scope' => 'namespaced']
3✔
87
                ),
3✔
88
                new CodeSample(
3✔
89
                    "<?php var_dump(PHP_VERSION, M_PI, MY_CUSTOM_PI);\n",
3✔
90
                    [
3✔
91
                        'include' => [
3✔
92
                            'MY_CUSTOM_PI',
3✔
93
                        ],
3✔
94
                    ]
3✔
95
                ),
3✔
96
                new CodeSample(
3✔
97
                    "<?php var_dump(PHP_VERSION, M_PI, MY_CUSTOM_PI);\n",
3✔
98
                    [
3✔
99
                        'fix_built_in' => false,
3✔
100
                        'include' => [
3✔
101
                            'MY_CUSTOM_PI',
3✔
102
                        ],
3✔
103
                    ]
3✔
104
                ),
3✔
105
                new CodeSample(
3✔
106
                    "<?php var_dump(PHP_VERSION, M_PI, MY_CUSTOM_PI);\n",
3✔
107
                    [
3✔
108
                        'exclude' => [
3✔
109
                            'M_PI',
3✔
110
                        ],
3✔
111
                    ]
3✔
112
                ),
3✔
113
            ],
3✔
114
            null,
3✔
115
            'Risky when any of the constants are namespaced or overridden.'
3✔
116
        );
3✔
117
    }
118

119
    /**
120
     * {@inheritdoc}
121
     *
122
     * Must run before GlobalNamespaceImportFixer.
123
     * Must run after FunctionToConstantFixer.
124
     */
125
    public function getPriority(): int
126
    {
127
        return 1;
1✔
128
    }
129

130
    public function isCandidate(Tokens $tokens): bool
131
    {
132
        return $tokens->isTokenKindFound(\T_STRING);
51✔
133
    }
134

135
    public function isRisky(): bool
136
    {
137
        return true;
1✔
138
    }
139

140
    protected function configurePostNormalisation(): void
141
    {
142
        $uniqueConfiguredExclude = array_unique($this->configuration['exclude']);
69✔
143

144
        // Case-sensitive constants handling
145
        $constantsToEscape = array_values($this->configuration['include']);
69✔
146

147
        if (true === $this->configuration['fix_built_in']) {
69✔
148
            $getDefinedConstants = get_defined_constants(true);
69✔
149
            unset($getDefinedConstants['user']);
69✔
150
            foreach ($getDefinedConstants as $constants) {
69✔
151
                $constantsToEscape = [...$constantsToEscape, ...array_keys($constants)];
69✔
152
            }
153
        }
154

155
        $constantsToEscape = array_diff(
69✔
156
            array_unique($constantsToEscape),
69✔
157
            $uniqueConfiguredExclude
69✔
158
        );
69✔
159

160
        // Case-insensitive constants handling
161
        $caseInsensitiveConstantsToEscape = [];
69✔
162

163
        foreach ($constantsToEscape as $constantIndex => $constant) {
69✔
164
            $loweredConstant = strtolower($constant);
69✔
165
            if (\in_array($loweredConstant, ['null', 'false', 'true'], true)) {
69✔
166
                $caseInsensitiveConstantsToEscape[] = $loweredConstant;
69✔
167
                unset($constantsToEscape[$constantIndex]);
69✔
168
            }
169
        }
170

171
        $caseInsensitiveConstantsToEscape = array_diff(
69✔
172
            array_unique($caseInsensitiveConstantsToEscape),
69✔
173
            array_map(
69✔
174
                static fn (string $function): string => strtolower($function),
69✔
175
                $uniqueConfiguredExclude,
69✔
176
            ),
69✔
177
        );
69✔
178

179
        // Store the cache
180
        $this->constantsToEscape = array_fill_keys($constantsToEscape, true);
69✔
181
        ksort($this->constantsToEscape);
69✔
182

183
        $this->caseInsensitiveConstantsToEscape = array_fill_keys($caseInsensitiveConstantsToEscape, true);
69✔
184
        ksort($this->caseInsensitiveConstantsToEscape);
69✔
185
    }
186

187
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
188
    {
189
        if ('all' === $this->configuration['scope']) {
51✔
190
            $this->fixConstantInvocations($tokens, 0, \count($tokens) - 1);
49✔
191

192
            return;
49✔
193
        }
194

195
        $namespaces = $tokens->getNamespaceDeclarations();
3✔
196

197
        // 'scope' is 'namespaced' here
198
        foreach (array_reverse($namespaces) as $namespace) {
3✔
199
            if ($namespace->isGlobalNamespace()) {
3✔
200
                continue;
3✔
201
            }
202

203
            $this->fixConstantInvocations($tokens, $namespace->getScopeStartIndex(), $namespace->getScopeEndIndex());
2✔
204
        }
205
    }
206

207
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
208
    {
209
        $constantChecker = static function (array $value): bool {
69✔
210
            foreach ($value as $constantName) {
69✔
211
                if (trim($constantName) !== $constantName) {
69✔
212
                    throw new InvalidOptionsException(\sprintf(
1✔
213
                        'Each element must be a non-empty, trimmed string, got "%s" instead.',
1✔
214
                        get_debug_type($constantName)
1✔
215
                    ));
1✔
216
                }
217
            }
218

219
            return true;
69✔
220
        };
69✔
221

222
        return new FixerConfigurationResolver([
69✔
223
            (new FixerOptionBuilder('fix_built_in', 'Whether to fix constants returned by `get_defined_constants`. User constants are not accounted in this list and must be specified in the include one.'))
69✔
224
                ->setAllowedTypes(['bool'])
69✔
225
                ->setDefault(true)
69✔
226
                ->getOption(),
69✔
227
            (new FixerOptionBuilder('include', 'List of additional constants to fix.'))
69✔
228
                ->setAllowedTypes(['string[]'])
69✔
229
                ->setAllowedValues([$constantChecker])
69✔
230
                ->setDefault([])
69✔
231
                ->getOption(),
69✔
232
            (new FixerOptionBuilder('exclude', 'List of constants to ignore.'))
69✔
233
                ->setAllowedTypes(['string[]'])
69✔
234
                ->setAllowedValues([$constantChecker])
69✔
235
                ->setDefault(['null', 'false', 'true'])
69✔
236
                ->getOption(),
69✔
237
            (new FixerOptionBuilder('scope', 'Only fix constant invocations that are made within a namespace or fix all.'))
69✔
238
                ->setAllowedValues(['all', 'namespaced'])
69✔
239
                ->setDefault('all')
69✔
240
                ->getOption(),
69✔
241
            (new FixerOptionBuilder('strict', 'Whether leading `\` of constant invocation not meant to have it should be removed.'))
69✔
242
                ->setAllowedTypes(['bool'])
69✔
243
                ->setDefault(true)
69✔
244
                ->getOption(),
69✔
245
        ]);
69✔
246
    }
247

248
    private function fixConstantInvocations(Tokens $tokens, int $startIndex, int $endIndex): void
249
    {
250
        $useDeclarations = (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens);
50✔
251
        $useConstantDeclarations = [];
50✔
252

253
        foreach ($useDeclarations as $use) {
50✔
254
            if ($use->isConstant()) {
4✔
255
                $useConstantDeclarations[$use->getShortName()] = true;
1✔
256
            }
257
        }
258

259
        $tokenAnalyzer = new TokensAnalyzer($tokens);
50✔
260

261
        for ($index = $endIndex; $index > $startIndex; --$index) {
50✔
262
            $token = $tokens[$index];
50✔
263

264
            // test if we are at a constant call
265
            if (!$token->isGivenKind(\T_STRING)) {
50✔
266
                continue;
50✔
267
            }
268

269
            if (!$tokenAnalyzer->isConstantInvocation($index)) {
50✔
270
                continue;
44✔
271
            }
272

273
            $tokenContent = $token->getContent();
22✔
274
            $prevIndex = $tokens->getPrevMeaningfulToken($index);
22✔
275

276
            if (!isset($this->constantsToEscape[$tokenContent]) && !isset($this->caseInsensitiveConstantsToEscape[strtolower($tokenContent)])) {
22✔
277
                if (false === $this->configuration['strict']) {
15✔
UNCOV
278
                    continue;
×
279
                }
280

281
                if (!$tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
15✔
282
                    continue;
14✔
283
                }
284

285
                $prevPrevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
4✔
286

287
                if ($tokens[$prevPrevIndex]->isGivenKind(\T_STRING)) {
4✔
288
                    continue;
2✔
289
                }
290

291
                $tokens->clearTokenAndMergeSurroundingWhitespace($prevIndex);
3✔
292

293
                continue;
3✔
294
            }
295

296
            if (isset($useConstantDeclarations[$tokenContent])) {
18✔
297
                continue;
1✔
298
            }
299

300
            if ($tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
18✔
301
                continue;
17✔
302
            }
303

304
            $tokens->insertAt($index, new Token([\T_NS_SEPARATOR, '\\']));
18✔
305
        }
306
    }
307
}
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