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

keradus / PHP-CS-Fixer / 16303177127

15 Jul 2025 06:22PM UTC coverage: 94.758% (-0.05%) from 94.806%
16303177127

push

github

keradus
bumped version

28199 of 29759 relevant lines covered (94.76%)

45.91 hits per line

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

97.79
/src/Fixer/FunctionNotation/LambdaNotUsedImportFixer.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\CT;
24
use PhpCsFixer\Tokenizer\Tokens;
25
use PhpCsFixer\Tokenizer\TokensAnalyzer;
26

27
final class LambdaNotUsedImportFixer extends AbstractFixer
28
{
29
    private ArgumentsAnalyzer $argumentsAnalyzer;
30

31
    private FunctionsAnalyzer $functionAnalyzer;
32

33
    private TokensAnalyzer $tokensAnalyzer;
34

35
    public function getDefinition(): FixerDefinitionInterface
36
    {
37
        return new FixerDefinition(
3✔
38
            'Lambda must not import variables it doesn\'t use.',
3✔
39
            [new CodeSample("<?php\n\$foo = function() use (\$bar) {};\n")]
3✔
40
        );
3✔
41
    }
42

43
    /**
44
     * {@inheritdoc}
45
     *
46
     * Must run before MethodArgumentSpaceFixer, NoSpacesInsideParenthesisFixer, SpacesInsideParenthesesFixer.
47
     */
48
    public function getPriority(): int
49
    {
50
        return 31;
1✔
51
    }
52

53
    public function isCandidate(Tokens $tokens): bool
54
    {
55
        return $tokens->isAllTokenKindsFound([\T_FUNCTION, CT::T_USE_LAMBDA]);
24✔
56
    }
57

58
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
59
    {
60
        $this->argumentsAnalyzer = new ArgumentsAnalyzer();
24✔
61
        $this->functionAnalyzer = new FunctionsAnalyzer();
24✔
62
        $this->tokensAnalyzer = new TokensAnalyzer($tokens);
24✔
63

64
        for ($index = $tokens->count() - 4; $index > 0; --$index) {
24✔
65
            $lambdaUseIndex = $this->getLambdaUseIndex($tokens, $index);
24✔
66

67
            if (false !== $lambdaUseIndex) {
24✔
68
                $this->fixLambda($tokens, $lambdaUseIndex);
24✔
69
            }
70
        }
71
    }
72

73
    private function fixLambda(Tokens $tokens, int $lambdaUseIndex): void
74
    {
75
        $lambdaUseOpenBraceIndex = $tokens->getNextTokenOfKind($lambdaUseIndex, ['(']);
24✔
76
        $lambdaUseCloseBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $lambdaUseOpenBraceIndex);
24✔
77
        $arguments = $this->argumentsAnalyzer->getArguments($tokens, $lambdaUseOpenBraceIndex, $lambdaUseCloseBraceIndex);
24✔
78

79
        $imports = $this->filterArguments($tokens, $arguments);
24✔
80

81
        if (0 === \count($imports)) {
24✔
82
            return; // no imports to remove
1✔
83
        }
84

85
        $notUsedImports = $this->findNotUsedLambdaImports($tokens, $imports, $lambdaUseCloseBraceIndex);
23✔
86
        $notUsedImportsCount = \count($notUsedImports);
23✔
87

88
        if (0 === $notUsedImportsCount) {
23✔
89
            return; // no not used imports found
18✔
90
        }
91

92
        if ($notUsedImportsCount === \count($arguments)) {
10✔
93
            $this->clearImportsAndUse($tokens, $lambdaUseIndex, $lambdaUseCloseBraceIndex); // all imports are not used
8✔
94

95
            return;
8✔
96
        }
97

98
        $this->clearImports($tokens, array_reverse($notUsedImports));
2✔
99
    }
100

101
    /**
102
     * @param array<string, int> $imports
103
     *
104
     * @return array<string, int>
105
     */
106
    private function findNotUsedLambdaImports(Tokens $tokens, array $imports, int $lambdaUseCloseBraceIndex): array
107
    {
108
        // figure out where the lambda starts ...
109
        $lambdaOpenIndex = $tokens->getNextTokenOfKind($lambdaUseCloseBraceIndex, ['{']);
23✔
110
        $curlyBracesLevel = 0;
23✔
111

112
        for ($index = $lambdaOpenIndex;; ++$index) { // go through the body of the lambda and keep count of the (possible) usages of the imported variables
23✔
113
            $token = $tokens[$index];
23✔
114

115
            if ($token->equals('{')) {
23✔
116
                ++$curlyBracesLevel;
23✔
117

118
                continue;
23✔
119
            }
120

121
            if ($token->equals('}')) {
23✔
122
                --$curlyBracesLevel;
10✔
123

124
                if (0 === $curlyBracesLevel) {
10✔
125
                    break;
10✔
126
                }
127

128
                continue;
×
129
            }
130

131
            if ($token->isGivenKind(\T_STRING) && 'compact' === strtolower($token->getContent()) && $this->functionAnalyzer->isGlobalFunctionCall($tokens, $index)) {
19✔
132
                return []; // wouldn't touch it with a ten-foot pole
2✔
133
            }
134

135
            if ($token->isGivenKind([
19✔
136
                CT::T_DYNAMIC_VAR_BRACE_OPEN,
19✔
137
                \T_EVAL,
19✔
138
                \T_INCLUDE,
19✔
139
                \T_INCLUDE_ONCE,
19✔
140
                \T_REQUIRE,
19✔
141
                \T_REQUIRE_ONCE,
19✔
142
            ])) {
19✔
143
                return [];
6✔
144
            }
145

146
            if ($token->equals('$')) {
19✔
147
                $nextIndex = $tokens->getNextMeaningfulToken($index);
2✔
148

149
                if ($tokens[$nextIndex]->isGivenKind(\T_VARIABLE)) {
2✔
150
                    return []; // "$$a" case
1✔
151
                }
152
            }
153

154
            if ($token->isGivenKind(\T_VARIABLE)) {
19✔
155
                $content = $token->getContent();
9✔
156

157
                if (isset($imports[$content])) {
9✔
158
                    unset($imports[$content]);
7✔
159

160
                    if (0 === \count($imports)) {
7✔
161
                        return $imports;
7✔
162
                    }
163
                }
164
            }
165

166
            if ($token->isGivenKind(\T_STRING_VARNAME)) {
19✔
167
                $content = '$'.$token->getContent();
1✔
168

169
                if (isset($imports[$content])) {
1✔
170
                    unset($imports[$content]);
1✔
171

172
                    if (0 === \count($imports)) {
1✔
173
                        return $imports;
1✔
174
                    }
175
                }
176
            }
177

178
            if ($token->isClassy()) { // is anonymous class
19✔
179
                // check if used as argument in the constructor of the anonymous class
180
                $index = $tokens->getNextTokenOfKind($index, ['(', '{']);
2✔
181

182
                if ($tokens[$index]->equals('(')) {
2✔
183
                    $closeBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
2✔
184
                    $arguments = $this->argumentsAnalyzer->getArguments($tokens, $index, $closeBraceIndex);
2✔
185

186
                    $imports = $this->countImportsUsedAsArgument($tokens, $imports, $arguments);
2✔
187

188
                    $index = $tokens->getNextTokenOfKind($closeBraceIndex, ['{']);
2✔
189
                }
190

191
                // skip body
192
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
2✔
193

194
                continue;
2✔
195
            }
196

197
            if ($token->isGivenKind(\T_FUNCTION)) {
19✔
198
                // check if used as argument
199
                $lambdaUseOpenBraceIndex = $tokens->getNextTokenOfKind($index, ['(']);
2✔
200
                $lambdaUseCloseBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $lambdaUseOpenBraceIndex);
2✔
201
                $arguments = $this->argumentsAnalyzer->getArguments($tokens, $lambdaUseOpenBraceIndex, $lambdaUseCloseBraceIndex);
2✔
202

203
                $imports = $this->countImportsUsedAsArgument($tokens, $imports, $arguments);
2✔
204

205
                // check if used as import
206
                $index = $tokens->getNextTokenOfKind($index, [[CT::T_USE_LAMBDA], '{']);
2✔
207

208
                if ($tokens[$index]->isGivenKind(CT::T_USE_LAMBDA)) {
2✔
209
                    $lambdaUseOpenBraceIndex = $tokens->getNextTokenOfKind($index, ['(']);
2✔
210
                    $lambdaUseCloseBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $lambdaUseOpenBraceIndex);
2✔
211
                    $arguments = $this->argumentsAnalyzer->getArguments($tokens, $lambdaUseOpenBraceIndex, $lambdaUseCloseBraceIndex);
2✔
212

213
                    $imports = $this->countImportsUsedAsArgument($tokens, $imports, $arguments);
2✔
214

215
                    $index = $tokens->getNextTokenOfKind($lambdaUseCloseBraceIndex, ['{']);
2✔
216
                }
217

218
                // skip body
219
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
2✔
220

221
                continue;
2✔
222
            }
223
        }
224

225
        return $imports;
10✔
226
    }
227

228
    /**
229
     * @param array<string, int> $imports
230
     * @param array<int, int>    $arguments
231
     *
232
     * @return array<string, int>
233
     */
234
    private function countImportsUsedAsArgument(Tokens $tokens, array $imports, array $arguments): array
235
    {
236
        foreach ($arguments as $start => $end) {
4✔
237
            $info = $this->argumentsAnalyzer->getArgumentInfo($tokens, $start, $end);
4✔
238
            $content = $info->getName();
4✔
239

240
            if (isset($imports[$content])) {
4✔
241
                unset($imports[$content]);
2✔
242

243
                if (0 === \count($imports)) {
2✔
244
                    return $imports;
2✔
245
                }
246
            }
247
        }
248

249
        return $imports;
4✔
250
    }
251

252
    /**
253
     * @return false|int
254
     */
255
    private function getLambdaUseIndex(Tokens $tokens, int $index)
256
    {
257
        if (!$tokens[$index]->isGivenKind(\T_FUNCTION) || !$this->tokensAnalyzer->isLambda($index)) {
24✔
258
            return false;
24✔
259
        }
260

261
        $lambdaUseIndex = $tokens->getNextMeaningfulToken($index); // we are @ '(' or '&' after this
24✔
262

263
        if ($tokens[$lambdaUseIndex]->isGivenKind(CT::T_RETURN_REF)) {
24✔
264
            $lambdaUseIndex = $tokens->getNextMeaningfulToken($lambdaUseIndex);
1✔
265
        }
266

267
        $lambdaUseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $lambdaUseIndex); // we are @ ')' after this
24✔
268
        $lambdaUseIndex = $tokens->getNextMeaningfulToken($lambdaUseIndex);
24✔
269

270
        if (!$tokens[$lambdaUseIndex]->isGivenKind(CT::T_USE_LAMBDA)) {
24✔
271
            return false;
3✔
272
        }
273

274
        return $lambdaUseIndex;
24✔
275
    }
276

277
    /**
278
     * @param array<int, int> $arguments
279
     *
280
     * @return array<string, int>
281
     */
282
    private function filterArguments(Tokens $tokens, array $arguments): array
283
    {
284
        $imports = [];
24✔
285

286
        foreach ($arguments as $start => $end) {
24✔
287
            $info = $this->argumentsAnalyzer->getArgumentInfo($tokens, $start, $end);
24✔
288
            $argument = $info->getNameIndex();
24✔
289

290
            if ($tokens[$tokens->getPrevMeaningfulToken($argument)]->equals('&')) {
24✔
291
                continue;
2✔
292
            }
293

294
            $argumentCandidate = $tokens[$argument];
23✔
295

296
            if ('$this' === $argumentCandidate->getContent()) {
23✔
297
                continue;
×
298
            }
299

300
            if ($this->tokensAnalyzer->isSuperGlobal($argument)) {
23✔
301
                continue;
×
302
            }
303

304
            $imports[$argumentCandidate->getContent()] = $argument;
23✔
305
        }
306

307
        return $imports;
24✔
308
    }
309

310
    /**
311
     * @param array<string, int> $imports
312
     */
313
    private function clearImports(Tokens $tokens, array $imports): void
314
    {
315
        foreach ($imports as $removeIndex) {
2✔
316
            $tokens->clearTokenAndMergeSurroundingWhitespace($removeIndex);
2✔
317
            $previousRemoveIndex = $tokens->getPrevMeaningfulToken($removeIndex);
2✔
318

319
            if ($tokens[$previousRemoveIndex]->equals(',')) {
2✔
320
                $tokens->clearTokenAndMergeSurroundingWhitespace($previousRemoveIndex);
1✔
321
            } elseif ($tokens[$previousRemoveIndex]->equals('(')) {
1✔
322
                $tokens->clearTokenAndMergeSurroundingWhitespace($tokens->getNextMeaningfulToken($removeIndex)); // next is always ',' here
1✔
323
            }
324
        }
325
    }
326

327
    /**
328
     * Remove `use` and all imported variables.
329
     */
330
    private function clearImportsAndUse(Tokens $tokens, int $lambdaUseIndex, int $lambdaUseCloseBraceIndex): void
331
    {
332
        for ($i = $lambdaUseCloseBraceIndex; $i >= $lambdaUseIndex; --$i) {
8✔
333
            if ($tokens[$i]->isComment()) {
8✔
334
                continue;
1✔
335
            }
336

337
            if ($tokens[$i]->isWhitespace()) {
8✔
338
                $previousIndex = $tokens->getPrevNonWhitespace($i);
8✔
339

340
                if ($tokens[$previousIndex]->isComment()) {
8✔
341
                    continue;
1✔
342
                }
343
            }
344

345
            $tokens->clearTokenAndMergeSurroundingWhitespace($i);
8✔
346
        }
347
    }
348
}
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