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

keradus / PHP-CS-Fixer / 17253322895

26 Aug 2025 11:52PM UTC coverage: 94.753% (+0.008%) from 94.745%
17253322895

push

github

keradus
add to git-blame-ignore-revs

28316 of 29884 relevant lines covered (94.75%)

45.64 hits per line

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

99.07
/src/Fixer/FunctionNotation/RegularCallableCallFixer.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\FixerDefinition\CodeSample;
19
use PhpCsFixer\FixerDefinition\FixerDefinition;
20
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
21
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
22
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
23
use PhpCsFixer\Tokenizer\Token;
24
use PhpCsFixer\Tokenizer\Tokens;
25

26
/**
27
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
28
 *
29
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
30
 */
31
final class RegularCallableCallFixer extends AbstractFixer
32
{
33
    public function getDefinition(): FixerDefinitionInterface
34
    {
35
        return new FixerDefinition(
3✔
36
            'Callables must be called without using `call_user_func*` when possible.',
3✔
37
            [
3✔
38
                new CodeSample(
3✔
39
                    '<?php
3✔
40
    call_user_func("var_dump", 1, 2);
41

42
    call_user_func("Bar\Baz::d", 1, 2);
43

44
    call_user_func_array($callback, [1, 2]);
45
'
3✔
46
                ),
3✔
47
                new CodeSample(
3✔
48
                    '<?php
3✔
49
call_user_func(function ($a, $b) { var_dump($a, $b); }, 1, 2);
50

51
call_user_func(static function ($a, $b) { var_dump($a, $b); }, 1, 2);
52
'
3✔
53
                ),
3✔
54
            ],
3✔
55
            null,
3✔
56
            'Risky when the `call_user_func` or `call_user_func_array` function is overridden or when are used in constructions that should be avoided, like `call_user_func_array(\'foo\', [\'bar\' => \'baz\'])` or `call_user_func($foo, $foo = \'bar\')`.'
3✔
57
        );
3✔
58
    }
59

60
    /**
61
     * {@inheritdoc}
62
     *
63
     * Must run before NativeFunctionInvocationFixer.
64
     * Must run after NoBinaryStringFixer, NoUselessConcatOperatorFixer.
65
     */
66
    public function getPriority(): int
67
    {
68
        return 2;
1✔
69
    }
70

71
    public function isCandidate(Tokens $tokens): bool
72
    {
73
        return $tokens->isTokenKindFound(\T_STRING);
24✔
74
    }
75

76
    public function isRisky(): bool
77
    {
78
        return true;
1✔
79
    }
80

81
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
82
    {
83
        $functionsAnalyzer = new FunctionsAnalyzer();
24✔
84
        $argumentsAnalyzer = new ArgumentsAnalyzer();
24✔
85

86
        for ($index = $tokens->count() - 1; $index > 0; --$index) {
24✔
87
            if (!$tokens[$index]->equalsAny([[\T_STRING, 'call_user_func'], [\T_STRING, 'call_user_func_array']], false)) {
24✔
88
                continue;
24✔
89
            }
90

91
            if (!$functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
24✔
92
                continue; // redeclare/override
2✔
93
            }
94

95
            $openParenthesis = $tokens->getNextMeaningfulToken($index);
22✔
96
            $closeParenthesis = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesis);
22✔
97
            $arguments = $argumentsAnalyzer->getArguments($tokens, $openParenthesis, $closeParenthesis);
22✔
98

99
            if (1 > \count($arguments)) {
22✔
100
                return; // no arguments!
×
101
            }
102

103
            $this->processCall($tokens, $index, $arguments);
22✔
104
        }
105
    }
106

107
    /**
108
     * @param non-empty-array<int, int> $arguments
109
     */
110
    private function processCall(Tokens $tokens, int $index, array $arguments): void
111
    {
112
        $firstArgIndex = $tokens->getNextMeaningfulToken(
22✔
113
            $tokens->getNextMeaningfulToken($index)
22✔
114
        );
22✔
115

116
        $firstArgToken = $tokens[$firstArgIndex];
22✔
117

118
        if ($firstArgToken->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
22✔
119
            $afterFirstArgIndex = $tokens->getNextMeaningfulToken($firstArgIndex);
14✔
120

121
            if (!$tokens[$afterFirstArgIndex]->equalsAny([',', ')'])) {
14✔
122
                return; // first argument is an expression like `call_user_func("foo"."bar", ...)`, not supported!
3✔
123
            }
124

125
            $firstArgTokenContent = $firstArgToken->getContent();
13✔
126

127
            if (!$this->isValidFunctionInvoke($firstArgTokenContent)) {
13✔
128
                return;
5✔
129
            }
130

131
            $newCallTokens = Tokens::fromCode('<?php '.substr(str_replace('\\\\', '\\', $firstArgToken->getContent()), 1, -1).'();');
8✔
132
            $newCallTokensSize = $newCallTokens->count();
8✔
133
            $newCallTokens->clearAt(0);
8✔
134
            $newCallTokens->clearRange($newCallTokensSize - 3, $newCallTokensSize - 1);
8✔
135
            $newCallTokens->clearEmptyTokens();
8✔
136

137
            $this->replaceCallUserFuncWithCallback($tokens, $index, $newCallTokens, $firstArgIndex, $firstArgIndex);
8✔
138
        } elseif (
139
            $firstArgToken->isGivenKind(\T_FUNCTION)
10✔
140
            || (
141
                $firstArgToken->isGivenKind(\T_STATIC)
10✔
142
                && $tokens[$tokens->getNextMeaningfulToken($firstArgIndex)]->isGivenKind(\T_FUNCTION)
10✔
143
            )
144
        ) {
145
            $firstArgEndIndex = $tokens->findBlockEnd(
3✔
146
                Tokens::BLOCK_TYPE_CURLY_BRACE,
3✔
147
                $tokens->getNextTokenOfKind($firstArgIndex, ['{'])
3✔
148
            );
3✔
149

150
            $newCallTokens = $this->getTokensSubcollection($tokens, $firstArgIndex, $firstArgEndIndex);
3✔
151
            $newCallTokens->insertAt($newCallTokens->count(), new Token(')'));
3✔
152
            $newCallTokens->insertAt(0, new Token('('));
3✔
153
            $this->replaceCallUserFuncWithCallback($tokens, $index, $newCallTokens, $firstArgIndex, $firstArgEndIndex);
3✔
154
        } elseif ($firstArgToken->isGivenKind(\T_VARIABLE)) {
9✔
155
            $firstArgEndIndex = reset($arguments);
7✔
156

157
            // check if the same variable is used multiple times and if so do not fix
158

159
            foreach ($arguments as $argumentStart => $argumentEnd) {
7✔
160
                if ($firstArgEndIndex === $argumentEnd) {
7✔
161
                    continue;
7✔
162
                }
163

164
                for ($i = $argumentStart; $i <= $argumentEnd; ++$i) {
6✔
165
                    if ($tokens[$i]->equals($firstArgToken)) {
6✔
166
                        return;
1✔
167
                    }
168
                }
169
            }
170

171
            // check if complex statement and if so wrap the call in () if on PHP 7 or up, else do not fix
172

173
            $newCallTokens = $this->getTokensSubcollection($tokens, $firstArgIndex, $firstArgEndIndex);
6✔
174
            $complex = false;
6✔
175

176
            for ($newCallIndex = \count($newCallTokens) - 1; $newCallIndex >= 0; --$newCallIndex) {
6✔
177
                if ($newCallTokens[$newCallIndex]->isGivenKind([\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_VARIABLE])) {
6✔
178
                    continue;
4✔
179
                }
180

181
                $blockType = Tokens::detectBlockType($newCallTokens[$newCallIndex]);
4✔
182

183
                if (null !== $blockType && (Tokens::BLOCK_TYPE_ARRAY_INDEX_CURLY_BRACE === $blockType['type'] || Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE === $blockType['type'])) {
4✔
184
                    $newCallIndex = $newCallTokens->findBlockStart($blockType['type'], $newCallIndex);
2✔
185

186
                    continue;
2✔
187
                }
188

189
                $complex = true;
3✔
190

191
                break;
3✔
192
            }
193

194
            if ($complex) {
6✔
195
                $newCallTokens->insertAt($newCallTokens->count(), new Token(')'));
3✔
196
                $newCallTokens->insertAt(0, new Token('('));
3✔
197
            }
198
            $this->replaceCallUserFuncWithCallback($tokens, $index, $newCallTokens, $firstArgIndex, $firstArgEndIndex);
6✔
199
        }
200
    }
201

202
    private function replaceCallUserFuncWithCallback(Tokens $tokens, int $callIndex, Tokens $newCallTokens, int $firstArgStartIndex, int $firstArgEndIndex): void
203
    {
204
        $tokens->clearRange($firstArgStartIndex, $firstArgEndIndex);
14✔
205

206
        $afterFirstArgIndex = $tokens->getNextMeaningfulToken($firstArgEndIndex);
14✔
207
        $afterFirstArgToken = $tokens[$afterFirstArgIndex];
14✔
208

209
        if ($afterFirstArgToken->equals(',')) {
14✔
210
            $useEllipsis = $tokens[$callIndex]->equals([\T_STRING, 'call_user_func_array'], false);
13✔
211

212
            if ($useEllipsis) {
13✔
213
                $secondArgIndex = $tokens->getNextMeaningfulToken($afterFirstArgIndex);
5✔
214
                $tokens->insertAt($secondArgIndex, new Token([\T_ELLIPSIS, '...']));
5✔
215
            }
216

217
            $tokens->clearAt($afterFirstArgIndex);
13✔
218
            $tokens->removeTrailingWhitespace($afterFirstArgIndex);
13✔
219
        }
220

221
        $tokens->overrideRange($callIndex, $callIndex, $newCallTokens);
14✔
222
        $prevIndex = $tokens->getPrevMeaningfulToken($callIndex);
14✔
223

224
        if ($tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
14✔
225
            $tokens->clearTokenAndMergeSurroundingWhitespace($prevIndex);
4✔
226
        }
227
    }
228

229
    private function getTokensSubcollection(Tokens $tokens, int $indexStart, int $indexEnd): Tokens
230
    {
231
        $size = $indexEnd - $indexStart + 1;
7✔
232
        $subCollection = new Tokens($size);
7✔
233

234
        for ($i = 0; $i < $size; ++$i) {
7✔
235
            $toClone = $tokens[$i + $indexStart];
7✔
236
            $subCollection[$i] = clone $toClone;
7✔
237
        }
238

239
        return $subCollection;
7✔
240
    }
241

242
    private function isValidFunctionInvoke(string $name): bool
243
    {
244
        if (\strlen($name) < 3 || 'b' === $name[0] || 'B' === $name[0]) {
13✔
245
            return false;
3✔
246
        }
247

248
        $name = substr($name, 1, -1);
10✔
249

250
        if ($name !== trim($name)) {
10✔
251
            return false;
2✔
252
        }
253

254
        return true;
8✔
255
    }
256
}
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