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

keradus / PHP-CS-Fixer / 17252691116

26 Aug 2025 11:09PM UTC coverage: 94.743% (-0.01%) from 94.755%
17252691116

push

github

keradus
chore: apply phpdoc_tag_no_named_arguments

28313 of 29884 relevant lines covered (94.74%)

45.64 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

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

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

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

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

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

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

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

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

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

207
            return;
21✔
208
        }
209

210
        $namespaces = $tokens->getNamespaceDeclarations();
10✔
211

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

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

233
                    return true;
50✔
234
                }])
50✔
235
                ->setDefault([])
50✔
236
                ->getOption(),
50✔
237
            (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✔
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) {
50✔
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
                        $sets = [
50✔
249
                            self::SET_ALL,
50✔
250
                            self::SET_INTERNAL,
50✔
251
                            self::SET_COMPILER_OPTIMIZED,
50✔
252
                        ];
50✔
253

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

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

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

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

284
            $prevIndex = $tokens->getPrevMeaningfulToken($index);
25✔
285

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

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

295
                continue;
13✔
296
            }
297

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

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

305
        $tokens->insertSlices($tokensToInsert);
30✔
306
    }
307

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

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

317
            return static fn (): bool => true;
6✔
318
        }
319

320
        $include = [];
50✔
321

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

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

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

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

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

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

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

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

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