• 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

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
                        <?php
41
                            call_user_func("var_dump", 1, 2);
42

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

45
                            call_user_func_array($callback, [1, 2]);
46

47
                        PHP
3✔
48
                ),
3✔
49
                new CodeSample(
3✔
50
                    <<<'PHP'
3✔
51
                        <?php
52
                        call_user_func(function ($a, $b) { var_dump($a, $b); }, 1, 2);
53

54
                        call_user_func(static function ($a, $b) { var_dump($a, $b); }, 1, 2);
55

56
                        PHP
3✔
57
                ),
3✔
58
            ],
3✔
59
            null,
3✔
60
            '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✔
61
        );
3✔
62
    }
63

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

75
    public function isCandidate(Tokens $tokens): bool
76
    {
77
        return $tokens->isTokenKindFound(\T_STRING);
24✔
78
    }
79

80
    public function isRisky(): bool
81
    {
82
        return true;
1✔
83
    }
84

85
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
86
    {
87
        $functionsAnalyzer = new FunctionsAnalyzer();
24✔
88
        $argumentsAnalyzer = new ArgumentsAnalyzer();
24✔
89

90
        for ($index = $tokens->count() - 1; $index > 0; --$index) {
24✔
91
            if (!$tokens[$index]->equalsAny([[\T_STRING, 'call_user_func'], [\T_STRING, 'call_user_func_array']], false)) {
24✔
92
                continue;
24✔
93
            }
94

95
            if (!$functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
24✔
96
                continue; // redeclare/override
2✔
97
            }
98

99
            $openParenthesis = $tokens->getNextMeaningfulToken($index);
22✔
100
            $closeParenthesis = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesis);
22✔
101
            $arguments = $argumentsAnalyzer->getArguments($tokens, $openParenthesis, $closeParenthesis);
22✔
102

103
            if (1 > \count($arguments)) {
22✔
UNCOV
104
                return; // no arguments!
×
105
            }
106

107
            $this->processCall($tokens, $index, $arguments);
22✔
108
        }
109
    }
110

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

120
        $firstArgToken = $tokens[$firstArgIndex];
22✔
121

122
        if ($firstArgToken->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
22✔
123
            $afterFirstArgIndex = $tokens->getNextMeaningfulToken($firstArgIndex);
14✔
124

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

129
            $firstArgTokenContent = $firstArgToken->getContent();
13✔
130

131
            if (!$this->isValidFunctionInvoke($firstArgTokenContent)) {
13✔
132
                return;
5✔
133
            }
134

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

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

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

161
            // check if the same variable is used multiple times and if so do not fix
162

163
            foreach ($arguments as $argumentStart => $argumentEnd) {
7✔
164
                if ($firstArgEndIndex === $argumentEnd) {
7✔
165
                    continue;
7✔
166
                }
167

168
                for ($i = $argumentStart; $i <= $argumentEnd; ++$i) {
6✔
169
                    if ($tokens[$i]->equals($firstArgToken)) {
6✔
170
                        return;
1✔
171
                    }
172
                }
173
            }
174

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

177
            $newCallTokens = $this->getTokensSubcollection($tokens, $firstArgIndex, $firstArgEndIndex);
6✔
178
            $complex = false;
6✔
179

180
            for ($newCallIndex = \count($newCallTokens) - 1; $newCallIndex >= 0; --$newCallIndex) {
6✔
181
                if ($newCallTokens[$newCallIndex]->isGivenKind([\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_VARIABLE])) {
6✔
182
                    continue;
4✔
183
                }
184

185
                $blockType = Tokens::detectBlockType($newCallTokens[$newCallIndex]);
4✔
186

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

190
                    continue;
2✔
191
                }
192

193
                $complex = true;
3✔
194

195
                break;
3✔
196
            }
197

198
            if ($complex) {
6✔
199
                $newCallTokens->insertAt($newCallTokens->count(), new Token(')'));
3✔
200
                $newCallTokens->insertAt(0, new Token('('));
3✔
201
            }
202
            $this->replaceCallUserFuncWithCallback($tokens, $index, $newCallTokens, $firstArgIndex, $firstArgEndIndex);
6✔
203
        }
204
    }
205

206
    private function replaceCallUserFuncWithCallback(Tokens $tokens, int $callIndex, Tokens $newCallTokens, int $firstArgStartIndex, int $firstArgEndIndex): void
207
    {
208
        $tokens->clearRange($firstArgStartIndex, $firstArgEndIndex);
14✔
209

210
        $afterFirstArgIndex = $tokens->getNextMeaningfulToken($firstArgEndIndex);
14✔
211
        $afterFirstArgToken = $tokens[$afterFirstArgIndex];
14✔
212

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

216
            if ($useEllipsis) {
13✔
217
                $secondArgIndex = $tokens->getNextMeaningfulToken($afterFirstArgIndex);
5✔
218
                $tokens->insertAt($secondArgIndex, new Token([\T_ELLIPSIS, '...']));
5✔
219
            }
220

221
            $tokens->clearAt($afterFirstArgIndex);
13✔
222
            $tokens->removeTrailingWhitespace($afterFirstArgIndex);
13✔
223
        }
224

225
        $tokens->overrideRange($callIndex, $callIndex, $newCallTokens);
14✔
226
        $prevIndex = $tokens->getPrevMeaningfulToken($callIndex);
14✔
227

228
        if ($tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
14✔
229
            $tokens->clearTokenAndMergeSurroundingWhitespace($prevIndex);
4✔
230
        }
231
    }
232

233
    private function getTokensSubcollection(Tokens $tokens, int $indexStart, int $indexEnd): Tokens
234
    {
235
        $size = $indexEnd - $indexStart + 1;
7✔
236
        $subCollection = new Tokens($size);
7✔
237

238
        for ($i = 0; $i < $size; ++$i) {
7✔
239
            $toClone = $tokens[$i + $indexStart];
7✔
240
            $subCollection[$i] = clone $toClone;
7✔
241
        }
242

243
        return $subCollection;
7✔
244
    }
245

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

252
        $name = substr($name, 1, -1);
10✔
253

254
        if ($name !== trim($name)) {
10✔
255
            return false;
2✔
256
        }
257

258
        return true;
8✔
259
    }
260
}
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