• 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

98.92
/src/Fixer/FunctionNotation/NativeFunctionInvocationFixer.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\FunctionNotation;
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\FunctionsAnalyzer;
27
use PhpCsFixer\Tokenizer\Token;
28
use PhpCsFixer\Tokenizer\Tokens;
29
use PhpCsFixer\Utils;
30
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
31

32
/**
33
 * @phpstan-type _AutogeneratedInputConfiguration array{
34
 *  exclude?: list<string>,
35
 *  include?: list<string>,
36
 *  scope?: 'all'|'namespaced',
37
 *  strict?: bool,
38
 * }
39
 * @phpstan-type _AutogeneratedComputedConfiguration array{
40
 *  exclude: list<string>,
41
 *  include: list<string>,
42
 *  scope: 'all'|'namespaced',
43
 *  strict: bool,
44
 * }
45
 *
46
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
47
 *
48
 * @author Andreas Möller <am@localheinz.com>
49
 */
50
final class NativeFunctionInvocationFixer extends AbstractFixer implements ConfigurableFixerInterface
51
{
52
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
53
    use ConfigurableFixerTrait;
54

55
    /**
56
     * @internal
57
     */
58
    public const SET_ALL = '@all';
59

60
    /**
61
     * Subset of SET_INTERNAL.
62
     *
63
     * Change function call to functions known to be optimized by the Zend engine.
64
     * For details:
65
     * - @see https://github.com/php/php-src/blob/php-7.2.6/Zend/zend_compile.c "zend_try_compile_special_func"
66
     * - @see https://github.com/php/php-src/blob/php-7.2.6/ext/opcache/Optimizer/pass1_5.c
67
     *
68
     * @internal
69
     */
70
    public const SET_COMPILER_OPTIMIZED = '@compiler_optimized';
71

72
    /**
73
     * @internal
74
     */
75
    public const SET_INTERNAL = '@internal';
76

77
    /**
78
     * @var callable
79
     */
80
    private $functionFilter;
81

82
    public function getDefinition(): FixerDefinitionInterface
83
    {
84
        return new FixerDefinition(
3✔
85
            'Add leading `\` before function invocation to speed up resolving.',
3✔
86
            [
3✔
87
                new CodeSample(
3✔
88
                    '<?php
3✔
89

90
function baz($options)
91
{
92
    if (!array_key_exists("foo", $options)) {
93
        throw new \InvalidArgumentException();
94
    }
95

96
    return json_encode($options);
97
}
98
'
3✔
99
                ),
3✔
100
                new CodeSample(
3✔
101
                    '<?php
3✔
102

103
function baz($options)
104
{
105
    if (!array_key_exists("foo", $options)) {
106
        throw new \InvalidArgumentException();
107
    }
108

109
    return json_encode($options);
110
}
111
',
3✔
112
                    [
3✔
113
                        'exclude' => [
3✔
114
                            'json_encode',
3✔
115
                        ],
3✔
116
                    ]
3✔
117
                ),
3✔
118
                new CodeSample(
3✔
119
                    '<?php
3✔
120
namespace space1 {
121
    echo count([1]);
122
}
123
namespace {
124
    echo count([1]);
125
}
126
',
3✔
127
                    ['scope' => 'all']
3✔
128
                ),
3✔
129
                new CodeSample(
3✔
130
                    '<?php
3✔
131
namespace space1 {
132
    echo count([1]);
133
}
134
namespace {
135
    echo count([1]);
136
}
137
',
3✔
138
                    ['scope' => 'namespaced']
3✔
139
                ),
3✔
140
                new CodeSample(
3✔
141
                    '<?php
3✔
142
myGlobalFunction();
143
count();
144
',
3✔
145
                    ['include' => ['myGlobalFunction']]
3✔
146
                ),
3✔
147
                new CodeSample(
3✔
148
                    '<?php
3✔
149
myGlobalFunction();
150
count();
151
',
3✔
152
                    ['include' => [self::SET_ALL]]
3✔
153
                ),
3✔
154
                new CodeSample(
3✔
155
                    '<?php
3✔
156
myGlobalFunction();
157
count();
158
',
3✔
159
                    ['include' => [self::SET_INTERNAL]]
3✔
160
                ),
3✔
161
                new CodeSample(
3✔
162
                    '<?php
3✔
163
$a .= str_repeat($a, 4);
164
$c = get_class($d);
165
',
3✔
166
                    ['include' => [self::SET_COMPILER_OPTIMIZED]]
3✔
167
                ),
3✔
168
            ],
3✔
169
            null,
3✔
170
            'Risky when any of the functions are overridden.'
3✔
171
        );
3✔
172
    }
173

174
    /**
175
     * {@inheritdoc}
176
     *
177
     * Must run before GlobalNamespaceImportFixer.
178
     * Must run after BacktickToShellExecFixer, MbStrFunctionsFixer, RegularCallableCallFixer, StrictParamFixer.
179
     */
180
    public function getPriority(): int
181
    {
182
        return 1;
1✔
183
    }
184

185
    public function isCandidate(Tokens $tokens): bool
186
    {
187
        return $tokens->isTokenKindFound(\T_STRING);
30✔
188
    }
189

190
    public function isRisky(): bool
191
    {
192
        return true;
1✔
193
    }
194

195
    protected function configurePostNormalisation(): void
196
    {
197
        $this->functionFilter = $this->getFunctionFilter();
50✔
198
    }
199

200
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
201
    {
202
        if ('all' === $this->configuration['scope']) {
30✔
203
            $this->fixFunctionCalls($tokens, $this->functionFilter, 0, \count($tokens) - 1, false);
21✔
204

205
            return;
21✔
206
        }
207

208
        $namespaces = $tokens->getNamespaceDeclarations();
10✔
209

210
        // 'scope' is 'namespaced' here
211
        foreach (array_reverse($namespaces) as $namespace) {
10✔
212
            $this->fixFunctionCalls($tokens, $this->functionFilter, $namespace->getScopeStartIndex(), $namespace->getScopeEndIndex(), $namespace->isGlobalNamespace());
10✔
213
        }
214
    }
215

216
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
217
    {
218
        return new FixerConfigurationResolver([
50✔
219
            (new FixerOptionBuilder('exclude', 'List of functions to ignore.'))
50✔
220
                ->setAllowedTypes(['string[]'])
50✔
221
                ->setAllowedValues([static function (array $value): bool {
50✔
222
                    foreach ($value as $functionName) {
50✔
223
                        if ('' === trim($functionName) || trim($functionName) !== $functionName) {
5✔
224
                            throw new InvalidOptionsException(\sprintf(
1✔
225
                                'Each element must be a non-empty, trimmed string, got "%s" instead.',
1✔
226
                                get_debug_type($functionName)
1✔
227
                            ));
1✔
228
                        }
229
                    }
230

231
                    return true;
50✔
232
                }])
50✔
233
                ->setDefault([])
50✔
234
                ->getOption(),
50✔
235
            (new FixerOptionBuilder('include', 'List of function names or sets to fix. Defined sets are `@internal` (all native functions), `@all` (all global functions) and `@compiler_optimized` (functions that are specially optimized by Zend).'))
50✔
236
                ->setAllowedTypes(['string[]'])
50✔
237
                ->setAllowedValues([static function (array $value): bool {
50✔
238
                    foreach ($value as $functionName) {
50✔
239
                        if ('' === trim($functionName) || trim($functionName) !== $functionName) {
50✔
240
                            throw new InvalidOptionsException(\sprintf(
1✔
241
                                'Each element must be a non-empty, trimmed string, got "%s" instead.',
1✔
242
                                get_debug_type($functionName)
1✔
243
                            ));
1✔
244
                        }
245

246
                        $sets = [
50✔
247
                            self::SET_ALL,
50✔
248
                            self::SET_INTERNAL,
50✔
249
                            self::SET_COMPILER_OPTIMIZED,
50✔
250
                        ];
50✔
251

252
                        if (str_starts_with($functionName, '@') && !\in_array($functionName, $sets, true)) {
50✔
253
                            throw new InvalidOptionsException(\sprintf('Unknown set "%s", known sets are %s.', $functionName, Utils::naturalLanguageJoin($sets)));
1✔
254
                        }
255
                    }
256

257
                    return true;
50✔
258
                }])
50✔
259
                ->setDefault([self::SET_COMPILER_OPTIMIZED])
50✔
260
                ->getOption(),
50✔
261
            (new FixerOptionBuilder('scope', 'Only fix function calls that are made within a namespace or fix all.'))
50✔
262
                ->setAllowedValues(['all', 'namespaced'])
50✔
263
                ->setDefault('all')
50✔
264
                ->getOption(),
50✔
265
            (new FixerOptionBuilder('strict', 'Whether leading `\` of function call not meant to have it should be removed.'))
50✔
266
                ->setAllowedTypes(['bool'])
50✔
267
                ->setDefault(true)
50✔
268
                ->getOption(),
50✔
269
        ]);
50✔
270
    }
271

272
    private function fixFunctionCalls(Tokens $tokens, callable $functionFilter, int $start, int $end, bool $tryToRemove): void
273
    {
274
        $functionsAnalyzer = new FunctionsAnalyzer();
30✔
275

276
        $tokensToInsert = [];
30✔
277
        for ($index = $start; $index < $end; ++$index) {
30✔
278
            if (!$functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
30✔
279
                continue;
30✔
280
            }
281

282
            $prevIndex = $tokens->getPrevMeaningfulToken($index);
25✔
283

284
            if (!$functionFilter($tokens[$index]->getContent()) || $tryToRemove) {
25✔
285
                if (false === $this->configuration['strict']) {
13✔
286
                    continue;
×
287
                }
288

289
                if ($tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
13✔
290
                    $tokens->clearTokenAndMergeSurroundingWhitespace($prevIndex);
2✔
291
                }
292

293
                continue;
13✔
294
            }
295

296
            if ($tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
20✔
297
                continue; // do not bother if previous token is already namespace separator
19✔
298
            }
299

300
            $tokensToInsert[$index] = new Token([\T_NS_SEPARATOR, '\\']);
18✔
301
        }
302

303
        $tokens->insertSlices($tokensToInsert);
30✔
304
    }
305

306
    private function getFunctionFilter(): callable
307
    {
308
        $exclude = $this->normalizeFunctionNames($this->configuration['exclude']);
50✔
309

310
        if (\in_array(self::SET_ALL, $this->configuration['include'], true)) {
50✔
311
            if (\count($exclude) > 0) {
6✔
312
                return static fn (string $functionName): bool => !isset($exclude[strtolower($functionName)]);
×
313
            }
314

315
            return static fn (): bool => true;
6✔
316
        }
317

318
        $include = [];
50✔
319

320
        if (\in_array(self::SET_INTERNAL, $this->configuration['include'], true)) {
50✔
321
            $include = $this->getAllInternalFunctionsNormalized();
2✔
322
        } elseif (\in_array(self::SET_COMPILER_OPTIMIZED, $this->configuration['include'], true)) {
50✔
323
            $include = $this->getAllCompilerOptimizedFunctionsNormalized(); // if `@internal` is set all compiler optimized function are already loaded
50✔
324
        }
325

326
        foreach ($this->configuration['include'] as $additional) {
50✔
327
            if (!str_starts_with($additional, '@')) {
50✔
328
                $include[strtolower($additional)] = true;
2✔
329
            }
330
        }
331

332
        if (\count($exclude) > 0) {
50✔
333
            return static fn (string $functionName): bool => isset($include[strtolower($functionName)]) && !isset($exclude[strtolower($functionName)]);
4✔
334
        }
335

336
        return static fn (string $functionName): bool => isset($include[strtolower($functionName)]);
50✔
337
    }
338

339
    /**
340
     * @return array<string, true> normalized function names of which the PHP compiler optimizes
341
     */
342
    private function getAllCompilerOptimizedFunctionsNormalized(): array
343
    {
344
        return $this->normalizeFunctionNames([
50✔
345
            // @see https://github.com/php/php-src/blob/PHP-7.4/Zend/zend_compile.c "zend_try_compile_special_func"
346
            'array_key_exists',
50✔
347
            'array_slice',
50✔
348
            'assert',
50✔
349
            'boolval',
50✔
350
            'call_user_func',
50✔
351
            'call_user_func_array',
50✔
352
            'chr',
50✔
353
            'count',
50✔
354
            'defined',
50✔
355
            'doubleval',
50✔
356
            'floatval',
50✔
357
            'func_get_args',
50✔
358
            'func_num_args',
50✔
359
            'get_called_class',
50✔
360
            'get_class',
50✔
361
            'gettype',
50✔
362
            'in_array',
50✔
363
            'intval',
50✔
364
            'is_array',
50✔
365
            'is_bool',
50✔
366
            'is_double',
50✔
367
            'is_float',
50✔
368
            'is_int',
50✔
369
            'is_integer',
50✔
370
            'is_long',
50✔
371
            'is_null',
50✔
372
            'is_object',
50✔
373
            'is_real',
50✔
374
            'is_resource',
50✔
375
            'is_scalar',
50✔
376
            'is_string',
50✔
377
            'ord',
50✔
378
            'sizeof',
50✔
379
            'sprintf',
50✔
380
            'strlen',
50✔
381
            'strval',
50✔
382
            // @see https://github.com/php/php-src/blob/php-7.2.6/ext/opcache/Optimizer/pass1_5.c
383
            // @see https://github.com/php/php-src/blob/PHP-8.1.2/Zend/Optimizer/block_pass.c
384
            // @see https://github.com/php/php-src/blob/php-8.1.3/Zend/Optimizer/zend_optimizer.c
385
            'constant',
50✔
386
            'define',
50✔
387
            'dirname',
50✔
388
            'extension_loaded',
50✔
389
            'function_exists',
50✔
390
            'is_callable',
50✔
391
            'ini_get',
50✔
392
        ]);
50✔
393
    }
394

395
    /**
396
     * @return array<string, true> normalized function names of all internal defined functions
397
     */
398
    private function getAllInternalFunctionsNormalized(): array
399
    {
400
        return $this->normalizeFunctionNames(get_defined_functions()['internal']);
2✔
401
    }
402

403
    /**
404
     * @param list<string> $functionNames
405
     *
406
     * @return array<string, true> all function names lower cased
407
     */
408
    private function normalizeFunctionNames(array $functionNames): array
409
    {
410
        $result = [];
50✔
411

412
        foreach ($functionNames as $functionName) {
50✔
413
            $result[strtolower($functionName)] = true;
50✔
414
        }
415

416
        return $result;
50✔
417
    }
418
}
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