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

keradus / PHP-CS-Fixer / 16303177127

15 Jul 2025 06:22PM UTC coverage: 94.758% (-0.05%) from 94.806%
16303177127

push

github

keradus
bumped version

28199 of 29759 relevant lines covered (94.76%)

45.91 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\Analysis\NamespaceAnalysis;
27
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
28
use PhpCsFixer\Tokenizer\Token;
29
use PhpCsFixer\Tokenizer\Tokens;
30
use PhpCsFixer\Tokenizer\TokensAnalyzer;
31
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
32

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

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

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

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

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

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

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

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

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

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

152
        /** @var list<string> */
153
        $constantsToEscape = array_diff(
69✔
154
            array_unique($constantsToEscape),
69✔
155
            $uniqueConfiguredExclude
69✔
156
        );
69✔
157

158
        // Case-insensitive constants handling
159
        $caseInsensitiveConstantsToEscape = [];
69✔
160

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

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

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

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

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

190
            return;
49✔
191
        }
192

193
        $namespaces = $tokens->getNamespaceDeclarations();
3✔
194

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

292
                continue;
3✔
293
            }
294

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

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

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