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

PHP-CS-Fixer / PHP-CS-Fixer / 3721300657

pending completion
3721300657

push

github

GitHub
minor: Follow PSR12 ordered imports in Symfony ruleset (#6712)

9 of 9 new or added lines in 2 files covered. (100.0%)

22674 of 24281 relevant lines covered (93.38%)

39.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.93
/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\FixerConfiguration\FixerConfigurationResolver;
20
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
21
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
22
use PhpCsFixer\FixerDefinition\CodeSample;
23
use PhpCsFixer\FixerDefinition\FixerDefinition;
24
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
25
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
26
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
27
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
28
use PhpCsFixer\Tokenizer\Token;
29
use PhpCsFixer\Tokenizer\Tokens;
30
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
31

32
/**
33
 * @author Andreas Möller <am@localheinz.com>
34
 */
35
final class NativeFunctionInvocationFixer extends AbstractFixer implements ConfigurableFixerInterface
36
{
37
    /**
38
     * @internal
39
     */
40
    public const SET_ALL = '@all';
41

42
    /**
43
     * Subset of SET_INTERNAL.
44
     *
45
     * Change function call to functions known to be optimized by the Zend engine.
46
     * For details:
47
     * - @see https://github.com/php/php-src/blob/php-7.2.6/Zend/zend_compile.c "zend_try_compile_special_func"
48
     * - @see https://github.com/php/php-src/blob/php-7.2.6/ext/opcache/Optimizer/pass1_5.c
49
     *
50
     * @internal
51
     */
52
    public const SET_COMPILER_OPTIMIZED = '@compiler_optimized';
53

54
    /**
55
     * @internal
56
     */
57
    public const SET_INTERNAL = '@internal';
58

59
    /**
60
     * @var callable
61
     */
62
    private $functionFilter;
63

64
    public function configure(array $configuration): void
65
    {
66
        parent::configure($configuration);
48✔
67

68
        $this->functionFilter = $this->getFunctionFilter();
48✔
69
    }
70

71
    /**
72
     * {@inheritdoc}
73
     */
74
    public function getDefinition(): FixerDefinitionInterface
75
    {
76
        return new FixerDefinition(
3✔
77
            'Add leading `\` before function invocation to speed up resolving.',
3✔
78
            [
3✔
79
                new CodeSample(
3✔
80
                    '<?php
3✔
81

82
function baz($options)
83
{
84
    if (!array_key_exists("foo", $options)) {
85
        throw new \InvalidArgumentException();
86
    }
87

88
    return json_encode($options);
89
}
90
'
3✔
91
                ),
3✔
92
                new CodeSample(
3✔
93
                    '<?php
3✔
94

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

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

166
    /**
167
     * {@inheritdoc}
168
     *
169
     * Must run before GlobalNamespaceImportFixer.
170
     * Must run after BacktickToShellExecFixer, RegularCallableCallFixer, StrictParamFixer.
171
     */
172
    public function getPriority(): int
173
    {
174
        return 1;
1✔
175
    }
176

177
    /**
178
     * {@inheritdoc}
179
     */
180
    public function isCandidate(Tokens $tokens): bool
181
    {
182
        return $tokens->isTokenKindFound(T_STRING);
29✔
183
    }
184

185
    /**
186
     * {@inheritdoc}
187
     */
188
    public function isRisky(): bool
189
    {
190
        return true;
1✔
191
    }
192

193
    /**
194
     * {@inheritdoc}
195
     */
196
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
197
    {
198
        if ('all' === $this->configuration['scope']) {
29✔
199
            $this->fixFunctionCalls($tokens, $this->functionFilter, 0, \count($tokens) - 1, false);
20✔
200

201
            return;
20✔
202
        }
203

204
        $namespaces = (new NamespacesAnalyzer())->getDeclarations($tokens);
10✔
205

206
        // 'scope' is 'namespaced' here
207
        /** @var NamespaceAnalysis $namespace */
208
        foreach (array_reverse($namespaces) as $namespace) {
10✔
209
            $this->fixFunctionCalls($tokens, $this->functionFilter, $namespace->getScopeStartIndex(), $namespace->getScopeEndIndex(), $namespace->isGlobalNamespace());
10✔
210
        }
211
    }
212

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

231
                    return true;
48✔
232
                }])
48✔
233
                ->setDefault([])
48✔
234
                ->getOption(),
48✔
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).'))
48✔
236
                ->setAllowedTypes(['array'])
48✔
237
                ->setAllowedValues([static function (array $value): bool {
48✔
238
                    foreach ($value as $functionName) {
48✔
239
                        if (!\is_string($functionName) || '' === trim($functionName) || trim($functionName) !== $functionName) {
48✔
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 = [
48✔
247
                            self::SET_ALL,
48✔
248
                            self::SET_INTERNAL,
48✔
249
                            self::SET_COMPILER_OPTIMIZED,
48✔
250
                        ];
48✔
251

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

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

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

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

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

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

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

293
                continue;
14✔
294
            }
295

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

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

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

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

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

317
            return static function (): bool {
6✔
318
                return true;
2✔
319
            };
6✔
320
        }
321

322
        $include = [];
48✔
323

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

330
        foreach ($this->configuration['include'] as $additional) {
48✔
331
            if (!str_starts_with($additional, '@')) {
48✔
332
                $include[strtolower($additional)] = true;
3✔
333
            }
334
        }
335

336
        if (\count($exclude) > 0) {
48✔
337
            return static function (string $functionName) use ($include, $exclude): bool {
5✔
338
                return isset($include[strtolower($functionName)]) && !isset($exclude[strtolower($functionName)]);
5✔
339
            };
5✔
340
        }
341

342
        return static function (string $functionName) use ($include): bool {
48✔
343
            return isset($include[strtolower($functionName)]);
20✔
344
        };
48✔
345
    }
346

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

402
    /**
403
     * @return array<string, true> normalized function names of all internal defined functions
404
     */
405
    private function getAllInternalFunctionsNormalized(): array
406
    {
407
        return $this->normalizeFunctionNames(get_defined_functions()['internal']);
2✔
408
    }
409

410
    /**
411
     * @param string[] $functionNames
412
     *
413
     * @return array<string, true> all function names lower cased
414
     */
415
    private function normalizeFunctionNames(array $functionNames): array
416
    {
417
        foreach ($functionNames as $index => $functionName) {
48✔
418
            $functionNames[strtolower($functionName)] = true;
48✔
419
            unset($functionNames[$index]);
48✔
420
        }
421

422
        return $functionNames;
48✔
423
    }
424
}
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

© 2025 Coveralls, Inc