• 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

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
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
51
 */
52
final class NativeFunctionInvocationFixer extends AbstractFixer implements ConfigurableFixerInterface
53
{
54
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
55
    use ConfigurableFixerTrait;
56

57
    /**
58
     * @internal
59
     */
60
    public const SET_ALL = '@all';
61

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

74
    /**
75
     * @internal
76
     */
77
    public const SET_INTERNAL = '@internal';
78

79
    /**
80
     * @var callable
81
     */
82
    private $functionFilter;
83

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

93
                        function baz($options)
94
                        {
95
                            if (!array_key_exists("foo", $options)) {
96
                                throw new \InvalidArgumentException();
97
                            }
98

99
                            return json_encode($options);
100
                        }
101

102
                        PHP
3✔
103
                ),
3✔
104
                new CodeSample(
3✔
105
                    <<<'PHP'
3✔
106
                        <?php
107

108
                        function baz($options)
109
                        {
110
                            if (!array_key_exists("foo", $options)) {
111
                                throw new \InvalidArgumentException();
112
                            }
113

114
                            return json_encode($options);
115
                        }
116

117
                        PHP,
3✔
118
                    [
3✔
119
                        'exclude' => [
3✔
120
                            'json_encode',
3✔
121
                        ],
3✔
122
                    ]
3✔
123
                ),
3✔
124
                new CodeSample(
3✔
125
                    <<<'PHP'
3✔
126
                        <?php
127
                        namespace space1 {
128
                            echo count([1]);
129
                        }
130
                        namespace {
131
                            echo count([1]);
132
                        }
133

134
                        PHP,
3✔
135
                    ['scope' => 'all']
3✔
136
                ),
3✔
137
                new CodeSample(
3✔
138
                    <<<'PHP'
3✔
139
                        <?php
140
                        namespace space1 {
141
                            echo count([1]);
142
                        }
143
                        namespace {
144
                            echo count([1]);
145
                        }
146

147
                        PHP,
3✔
148
                    ['scope' => 'namespaced']
3✔
149
                ),
3✔
150
                new CodeSample(
3✔
151
                    <<<'PHP'
3✔
152
                        <?php
153
                        myGlobalFunction();
154
                        count();
155

156
                        PHP,
3✔
157
                    ['include' => ['myGlobalFunction']]
3✔
158
                ),
3✔
159
                new CodeSample(
3✔
160
                    <<<'PHP'
3✔
161
                        <?php
162
                        myGlobalFunction();
163
                        count();
164

165
                        PHP,
3✔
166
                    ['include' => [self::SET_ALL]]
3✔
167
                ),
3✔
168
                new CodeSample(
3✔
169
                    <<<'PHP'
3✔
170
                        <?php
171
                        myGlobalFunction();
172
                        count();
173

174
                        PHP,
3✔
175
                    ['include' => [self::SET_INTERNAL]]
3✔
176
                ),
3✔
177
                new CodeSample(
3✔
178
                    <<<'PHP'
3✔
179
                        <?php
180
                        $a .= str_repeat($a, 4);
181
                        $c = get_class($d);
182

183
                        PHP,
3✔
184
                    ['include' => [self::SET_COMPILER_OPTIMIZED]]
3✔
185
                ),
3✔
186
            ],
3✔
187
            null,
3✔
188
            'Risky when any of the functions are overridden.'
3✔
189
        );
3✔
190
    }
191

192
    /**
193
     * {@inheritdoc}
194
     *
195
     * Must run before GlobalNamespaceImportFixer.
196
     * Must run after BacktickToShellExecFixer, MbStrFunctionsFixer, RegularCallableCallFixer, StrictParamFixer.
197
     */
198
    public function getPriority(): int
199
    {
200
        return 1;
1✔
201
    }
202

203
    public function isCandidate(Tokens $tokens): bool
204
    {
205
        return $tokens->isTokenKindFound(\T_STRING);
30✔
206
    }
207

208
    public function isRisky(): bool
209
    {
210
        return true;
1✔
211
    }
212

213
    protected function configurePostNormalisation(): void
214
    {
215
        $this->functionFilter = $this->getFunctionFilter();
50✔
216
    }
217

218
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
219
    {
220
        if ('all' === $this->configuration['scope']) {
30✔
221
            $this->fixFunctionCalls($tokens, $this->functionFilter, 0, \count($tokens) - 1, false);
21✔
222

223
            return;
21✔
224
        }
225

226
        $namespaces = $tokens->getNamespaceDeclarations();
10✔
227

228
        // 'scope' is 'namespaced' here
229
        foreach (array_reverse($namespaces) as $namespace) {
10✔
230
            $this->fixFunctionCalls($tokens, $this->functionFilter, $namespace->getScopeStartIndex(), $namespace->getScopeEndIndex(), $namespace->isGlobalNamespace());
10✔
231
        }
232
    }
233

234
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
235
    {
236
        return new FixerConfigurationResolver([
50✔
237
            (new FixerOptionBuilder('exclude', 'List of functions to ignore.'))
50✔
238
                ->setAllowedTypes(['string[]'])
50✔
239
                ->setAllowedValues([static function (array $value): bool {
50✔
240
                    foreach ($value as $functionName) {
50✔
241
                        if ('' === trim($functionName) || trim($functionName) !== $functionName) {
5✔
242
                            throw new InvalidOptionsException(\sprintf(
1✔
243
                                'Each element must be a non-empty, trimmed string, got "%s" instead.',
1✔
244
                                get_debug_type($functionName)
1✔
245
                            ));
1✔
246
                        }
247
                    }
248

249
                    return true;
50✔
250
                }])
50✔
251
                ->setDefault([])
50✔
252
                ->getOption(),
50✔
253
            (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✔
254
                ->setAllowedTypes(['string[]'])
50✔
255
                ->setAllowedValues([static function (array $value): bool {
50✔
256
                    foreach ($value as $functionName) {
50✔
257
                        if ('' === trim($functionName) || trim($functionName) !== $functionName) {
50✔
258
                            throw new InvalidOptionsException(\sprintf(
1✔
259
                                'Each element must be a non-empty, trimmed string, got "%s" instead.',
1✔
260
                                get_debug_type($functionName)
1✔
261
                            ));
1✔
262
                        }
263

264
                        $sets = [
50✔
265
                            self::SET_ALL,
50✔
266
                            self::SET_INTERNAL,
50✔
267
                            self::SET_COMPILER_OPTIMIZED,
50✔
268
                        ];
50✔
269

270
                        if (str_starts_with($functionName, '@') && !\in_array($functionName, $sets, true)) {
50✔
271
                            throw new InvalidOptionsException(\sprintf('Unknown set "%s", known sets are %s.', $functionName, Utils::naturalLanguageJoin($sets)));
1✔
272
                        }
273
                    }
274

275
                    return true;
50✔
276
                }])
50✔
277
                ->setDefault([self::SET_COMPILER_OPTIMIZED])
50✔
278
                ->getOption(),
50✔
279
            (new FixerOptionBuilder('scope', 'Only fix function calls that are made within a namespace or fix all.'))
50✔
280
                ->setAllowedValues(['all', 'namespaced'])
50✔
281
                ->setDefault('all')
50✔
282
                ->getOption(),
50✔
283
            (new FixerOptionBuilder('strict', 'Whether leading `\` of function call not meant to have it should be removed.'))
50✔
284
                ->setAllowedTypes(['bool'])
50✔
285
                ->setDefault(true)
50✔
286
                ->getOption(),
50✔
287
        ]);
50✔
288
    }
289

290
    private function fixFunctionCalls(Tokens $tokens, callable $functionFilter, int $start, int $end, bool $tryToRemove): void
291
    {
292
        $functionsAnalyzer = new FunctionsAnalyzer();
30✔
293

294
        $tokensToInsert = [];
30✔
295
        for ($index = $start; $index < $end; ++$index) {
30✔
296
            if (!$functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
30✔
297
                continue;
30✔
298
            }
299

300
            $prevIndex = $tokens->getPrevMeaningfulToken($index);
25✔
301

302
            if (!$functionFilter($tokens[$index]->getContent()) || $tryToRemove) {
25✔
303
                if (false === $this->configuration['strict']) {
13✔
UNCOV
304
                    continue;
×
305
                }
306

307
                if ($tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
13✔
308
                    $tokens->clearTokenAndMergeSurroundingWhitespace($prevIndex);
2✔
309
                }
310

311
                continue;
13✔
312
            }
313

314
            if ($tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
20✔
315
                continue; // do not bother if previous token is already namespace separator
19✔
316
            }
317

318
            $tokensToInsert[$index] = new Token([\T_NS_SEPARATOR, '\\']);
18✔
319
        }
320

321
        $tokens->insertSlices($tokensToInsert);
30✔
322
    }
323

324
    private function getFunctionFilter(): callable
325
    {
326
        $exclude = $this->normalizeFunctionNames($this->configuration['exclude']);
50✔
327

328
        if (\in_array(self::SET_ALL, $this->configuration['include'], true)) {
50✔
329
            if (\count($exclude) > 0) {
6✔
UNCOV
330
                return static fn (string $functionName): bool => !isset($exclude[strtolower($functionName)]);
×
331
            }
332

333
            return static fn (): bool => true;
6✔
334
        }
335

336
        $include = [];
50✔
337

338
        if (\in_array(self::SET_INTERNAL, $this->configuration['include'], true)) {
50✔
339
            $include = $this->getAllInternalFunctionsNormalized();
2✔
340
        } elseif (\in_array(self::SET_COMPILER_OPTIMIZED, $this->configuration['include'], true)) {
50✔
341
            $include = $this->getAllCompilerOptimizedFunctionsNormalized(); // if `@internal` is set all compiler optimized function are already loaded
50✔
342
        }
343

344
        foreach ($this->configuration['include'] as $additional) {
50✔
345
            if (!str_starts_with($additional, '@')) {
50✔
346
                $include[strtolower($additional)] = true;
2✔
347
            }
348
        }
349

350
        if (\count($exclude) > 0) {
50✔
351
            return static fn (string $functionName): bool => isset($include[strtolower($functionName)]) && !isset($exclude[strtolower($functionName)]);
4✔
352
        }
353

354
        return static fn (string $functionName): bool => isset($include[strtolower($functionName)]);
50✔
355
    }
356

357
    /**
358
     * @return array<string, true> normalized function names of which the PHP compiler optimizes
359
     */
360
    private function getAllCompilerOptimizedFunctionsNormalized(): array
361
    {
362
        return $this->normalizeFunctionNames([
50✔
363
            // @see https://github.com/php/php-src/blob/PHP-7.4/Zend/zend_compile.c "zend_try_compile_special_func"
364
            'array_key_exists',
50✔
365
            'array_slice',
50✔
366
            'assert',
50✔
367
            'boolval',
50✔
368
            'call_user_func',
50✔
369
            'call_user_func_array',
50✔
370
            'chr',
50✔
371
            'count',
50✔
372
            'defined',
50✔
373
            'doubleval',
50✔
374
            'floatval',
50✔
375
            'func_get_args',
50✔
376
            'func_num_args',
50✔
377
            'get_called_class',
50✔
378
            'get_class',
50✔
379
            'gettype',
50✔
380
            'in_array',
50✔
381
            'intval',
50✔
382
            'is_array',
50✔
383
            'is_bool',
50✔
384
            'is_double',
50✔
385
            'is_float',
50✔
386
            'is_int',
50✔
387
            'is_integer',
50✔
388
            'is_long',
50✔
389
            'is_null',
50✔
390
            'is_object',
50✔
391
            'is_real',
50✔
392
            'is_resource',
50✔
393
            'is_scalar',
50✔
394
            'is_string',
50✔
395
            'ord',
50✔
396
            'sizeof',
50✔
397
            'sprintf',
50✔
398
            'strlen',
50✔
399
            'strval',
50✔
400
            // @see https://github.com/php/php-src/blob/php-7.2.6/ext/opcache/Optimizer/pass1_5.c
401
            // @see https://github.com/php/php-src/blob/PHP-8.1.2/Zend/Optimizer/block_pass.c
402
            // @see https://github.com/php/php-src/blob/php-8.1.3/Zend/Optimizer/zend_optimizer.c
403
            'constant',
50✔
404
            'define',
50✔
405
            'dirname',
50✔
406
            'extension_loaded',
50✔
407
            'function_exists',
50✔
408
            'is_callable',
50✔
409
            'ini_get',
50✔
410
        ]);
50✔
411
    }
412

413
    /**
414
     * @return array<string, true> normalized function names of all internal defined functions
415
     */
416
    private function getAllInternalFunctionsNormalized(): array
417
    {
418
        return $this->normalizeFunctionNames(get_defined_functions()['internal']);
2✔
419
    }
420

421
    /**
422
     * @param list<string> $functionNames
423
     *
424
     * @return array<string, true> all function names lower cased
425
     */
426
    private function normalizeFunctionNames(array $functionNames): array
427
    {
428
        $result = [];
50✔
429

430
        foreach ($functionNames as $functionName) {
50✔
431
            $result[strtolower($functionName)] = true;
50✔
432
        }
433

434
        return $result;
50✔
435
    }
436
}
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