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

keradus / PHP-CS-Fixer / 16999983712

15 Aug 2025 09:42PM UTC coverage: 94.75% (-0.09%) from 94.839%
16999983712

push

github

keradus
ci: more self-fixing checks on lowest/highest PHP

28263 of 29829 relevant lines covered (94.75%)

45.88 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
final class NativeConstantInvocationFixer extends AbstractFixer implements ConfigurableFixerInterface
53
{
54
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
55
    use ConfigurableFixerTrait;
56

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

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

67
    public function getDefinition(): FixerDefinitionInterface
68
    {
69
        return new FixerDefinition(
3✔
70
            '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✔
71
            [
3✔
72
                new CodeSample("<?php var_dump(PHP_VERSION, M_PI, MY_CUSTOM_PI);\n"),
3✔
73
                new CodeSample(
3✔
74
                    '<?php
3✔
75
namespace space1 {
76
    echo PHP_VERSION;
77
}
78
namespace {
79
    echo M_PI;
80
}
81
',
3✔
82
                    ['scope' => 'namespaced']
3✔
83
                ),
3✔
84
                new CodeSample(
3✔
85
                    "<?php var_dump(PHP_VERSION, M_PI, MY_CUSTOM_PI);\n",
3✔
86
                    [
3✔
87
                        'include' => [
3✔
88
                            'MY_CUSTOM_PI',
3✔
89
                        ],
3✔
90
                    ]
3✔
91
                ),
3✔
92
                new CodeSample(
3✔
93
                    "<?php var_dump(PHP_VERSION, M_PI, MY_CUSTOM_PI);\n",
3✔
94
                    [
3✔
95
                        'fix_built_in' => false,
3✔
96
                        'include' => [
3✔
97
                            'MY_CUSTOM_PI',
3✔
98
                        ],
3✔
99
                    ]
3✔
100
                ),
3✔
101
                new CodeSample(
3✔
102
                    "<?php var_dump(PHP_VERSION, M_PI, MY_CUSTOM_PI);\n",
3✔
103
                    [
3✔
104
                        'exclude' => [
3✔
105
                            'M_PI',
3✔
106
                        ],
3✔
107
                    ]
3✔
108
                ),
3✔
109
            ],
3✔
110
            null,
3✔
111
            'Risky when any of the constants are namespaced or overridden.'
3✔
112
        );
3✔
113
    }
114

115
    /**
116
     * {@inheritdoc}
117
     *
118
     * Must run before GlobalNamespaceImportFixer.
119
     * Must run after FunctionToConstantFixer.
120
     */
121
    public function getPriority(): int
122
    {
123
        return 1;
1✔
124
    }
125

126
    public function isCandidate(Tokens $tokens): bool
127
    {
128
        return $tokens->isTokenKindFound(\T_STRING);
51✔
129
    }
130

131
    public function isRisky(): bool
132
    {
133
        return true;
1✔
134
    }
135

136
    protected function configurePostNormalisation(): void
137
    {
138
        $uniqueConfiguredExclude = array_unique($this->configuration['exclude']);
69✔
139

140
        // Case-sensitive constants handling
141
        $constantsToEscape = array_values($this->configuration['include']);
69✔
142

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

151
        $constantsToEscape = array_diff(
69✔
152
            array_unique($constantsToEscape),
69✔
153
            $uniqueConfiguredExclude
69✔
154
        );
69✔
155

156
        // Case-insensitive constants handling
157
        $caseInsensitiveConstantsToEscape = [];
69✔
158

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

167
        $caseInsensitiveConstantsToEscape = array_diff(
69✔
168
            array_unique($caseInsensitiveConstantsToEscape),
69✔
169
            array_map(
69✔
170
                static fn (string $function): string => strtolower($function),
69✔
171
                $uniqueConfiguredExclude,
69✔
172
            ),
69✔
173
        );
69✔
174

175
        // Store the cache
176
        $this->constantsToEscape = array_fill_keys($constantsToEscape, true);
69✔
177
        ksort($this->constantsToEscape);
69✔
178

179
        $this->caseInsensitiveConstantsToEscape = array_fill_keys($caseInsensitiveConstantsToEscape, true);
69✔
180
        ksort($this->caseInsensitiveConstantsToEscape);
69✔
181
    }
182

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

188
            return;
49✔
189
        }
190

191
        $namespaces = $tokens->getNamespaceDeclarations();
3✔
192

193
        // 'scope' is 'namespaced' here
194
        foreach (array_reverse($namespaces) as $namespace) {
3✔
195
            if ($namespace->isGlobalNamespace()) {
3✔
196
                continue;
3✔
197
            }
198

199
            $this->fixConstantInvocations($tokens, $namespace->getScopeStartIndex(), $namespace->getScopeEndIndex());
2✔
200
        }
201
    }
202

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

215
            return true;
69✔
216
        };
69✔
217

218
        return new FixerConfigurationResolver([
69✔
219
            (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✔
220
                ->setAllowedTypes(['bool'])
69✔
221
                ->setDefault(true)
69✔
222
                ->getOption(),
69✔
223
            (new FixerOptionBuilder('include', 'List of additional constants to fix.'))
69✔
224
                ->setAllowedTypes(['string[]'])
69✔
225
                ->setAllowedValues([$constantChecker])
69✔
226
                ->setDefault([])
69✔
227
                ->getOption(),
69✔
228
            (new FixerOptionBuilder('exclude', 'List of constants to ignore.'))
69✔
229
                ->setAllowedTypes(['string[]'])
69✔
230
                ->setAllowedValues([$constantChecker])
69✔
231
                ->setDefault(['null', 'false', 'true'])
69✔
232
                ->getOption(),
69✔
233
            (new FixerOptionBuilder('scope', 'Only fix constant invocations that are made within a namespace or fix all.'))
69✔
234
                ->setAllowedValues(['all', 'namespaced'])
69✔
235
                ->setDefault('all')
69✔
236
                ->getOption(),
69✔
237
            (new FixerOptionBuilder('strict', 'Whether leading `\` of constant invocation not meant to have it should be removed.'))
69✔
238
                ->setAllowedTypes(['bool'])
69✔
239
                ->setDefault(true)
69✔
240
                ->getOption(),
69✔
241
        ]);
69✔
242
    }
243

244
    private function fixConstantInvocations(Tokens $tokens, int $startIndex, int $endIndex): void
245
    {
246
        $useDeclarations = (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens);
50✔
247
        $useConstantDeclarations = [];
50✔
248

249
        foreach ($useDeclarations as $use) {
50✔
250
            if ($use->isConstant()) {
4✔
251
                $useConstantDeclarations[$use->getShortName()] = true;
1✔
252
            }
253
        }
254

255
        $tokenAnalyzer = new TokensAnalyzer($tokens);
50✔
256

257
        for ($index = $endIndex; $index > $startIndex; --$index) {
50✔
258
            $token = $tokens[$index];
50✔
259

260
            // test if we are at a constant call
261
            if (!$token->isGivenKind(\T_STRING)) {
50✔
262
                continue;
50✔
263
            }
264

265
            if (!$tokenAnalyzer->isConstantInvocation($index)) {
50✔
266
                continue;
44✔
267
            }
268

269
            $tokenContent = $token->getContent();
22✔
270
            $prevIndex = $tokens->getPrevMeaningfulToken($index);
22✔
271

272
            if (!isset($this->constantsToEscape[$tokenContent]) && !isset($this->caseInsensitiveConstantsToEscape[strtolower($tokenContent)])) {
22✔
273
                if (false === $this->configuration['strict']) {
15✔
274
                    continue;
×
275
                }
276

277
                if (!$tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
15✔
278
                    continue;
14✔
279
                }
280

281
                $prevPrevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
4✔
282

283
                if ($tokens[$prevPrevIndex]->isGivenKind(\T_STRING)) {
4✔
284
                    continue;
2✔
285
                }
286

287
                $tokens->clearTokenAndMergeSurroundingWhitespace($prevIndex);
3✔
288

289
                continue;
3✔
290
            }
291

292
            if (isset($useConstantDeclarations[$tokenContent])) {
18✔
293
                continue;
1✔
294
            }
295

296
            if ($tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
18✔
297
                continue;
17✔
298
            }
299

300
            $tokens->insertAt($index, new Token([\T_NS_SEPARATOR, '\\']));
18✔
301
        }
302
    }
303
}
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