• 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.14
/src/Fixer/ClassNotation/StaticPrivateMethodFixer.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\ClassNotation;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\FixerDefinition\CodeSample;
19
use PhpCsFixer\FixerDefinition\FixerDefinition;
20
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
21
use PhpCsFixer\Tokenizer\Token;
22
use PhpCsFixer\Tokenizer\Tokens;
23
use PhpCsFixer\Tokenizer\TokensAnalyzer;
24

25
/**
26
 * @author Filippo Tessarotto <zoeslam@gmail.com>
27
 *
28
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
29
 */
30
final class StaticPrivateMethodFixer extends AbstractFixer
31
{
32
    /**
33
     * @var array<string, true>
34
     */
35
    private const MAGIC_METHODS = [
36
        '__clone' => true,
37
        '__construct' => true,
38
        '__destruct' => true,
39
        '__wakeup' => true,
40
    ];
41

42
    public function getDefinition(): FixerDefinitionInterface
43
    {
44
        return new FixerDefinition(
3✔
45
            'Converts private methods to `static` where possible.',
3✔
46
            [
3✔
47
                new CodeSample(
3✔
48
                    <<<'PHP'
3✔
49
                        <?php
50
                        class Foo
51
                        {
52
                            public function bar()
53
                            {
54
                                return $this->baz();
55
                            }
56

57
                            private function baz()
58
                            {
59
                                return 1;
60
                            }
61
                        }
62

63
                        PHP
3✔
64
                ),
3✔
65
            ],
3✔
66
            null,
3✔
67
            'Risky when the method:'
3✔
68
            .' contains dynamic generated calls to the instance,'
3✔
69
            .' is dynamically referenced,'
3✔
70
            .' is referenced inside a Trait the class uses.'
3✔
71
        );
3✔
72
    }
73

74
    /**
75
     * {@inheritdoc}
76
     *
77
     * Must run before StaticLambdaFixer.
78
     * Must run after ProtectedToPrivateFixer.
79
     */
80
    public function getPriority(): int
81
    {
82
        return 1;
1✔
83
    }
84

85
    public function isCandidate(Tokens $tokens): bool
86
    {
87
        return $tokens->isAllTokenKindsFound([\T_CLASS, \T_PRIVATE, \T_FUNCTION]);
17✔
88
    }
89

90
    public function isRisky(): bool
91
    {
92
        return true;
1✔
93
    }
94

95
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
96
    {
97
        $tokensAnalyzer = new TokensAnalyzer($tokens);
17✔
98

99
        do {
100
            $anythingChanged = false;
17✔
101

102
            $end = \count($tokens) - 3; // min. number of tokens to form a class candidate to fix
17✔
103
            for ($index = $end; $index > 0; --$index) {
17✔
104
                if (!$tokens[$index]->isGivenKind(\T_CLASS)) {
17✔
105
                    continue;
17✔
106
                }
107

108
                $classOpen = $tokens->getNextTokenOfKind($index, ['{']);
17✔
109
                $classClose = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpen);
17✔
110

111
                $anythingChanged |= $this->fixClass($tokens, $tokensAnalyzer, $classOpen, $classClose);
17✔
112
            }
113
        } while ($anythingChanged);
17✔
114
    }
115

116
    private function fixClass(Tokens $tokens, TokensAnalyzer $tokensAnalyzer, int $classOpen, int $classClose): bool
117
    {
118
        $fixedMethods = [];
17✔
119
        foreach ($this->getClassMethods($tokens, $classOpen, $classClose) as $methodData) {
17✔
120
            [$functionKeywordIndex, $methodOpen, $methodClose] = $methodData;
17✔
121

122
            if ($this->skipMethod($tokens, $tokensAnalyzer, $functionKeywordIndex, $methodOpen, $methodClose)) {
17✔
123
                continue;
17✔
124
            }
125

126
            $methodNameIndex = $tokens->getNextMeaningfulToken($functionKeywordIndex);
12✔
127
            $methodName = $tokens[$methodNameIndex]->getContent();
12✔
128
            $fixedMethods[$methodName] = true;
12✔
129

130
            $tokens->insertSlices([$functionKeywordIndex => [new Token([\T_STATIC, 'static']), new Token([\T_WHITESPACE, ' '])]]);
12✔
131
        }
132

133
        if (0 === \count($fixedMethods)) {
17✔
134
            return false;
17✔
135
        }
136

137
        $classClose = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpen);
12✔
138
        foreach ($this->getClassMethods($tokens, $classOpen, $classClose) as $methodData) {
12✔
139
            [, $methodOpen, $methodClose] = $methodData;
12✔
140

141
            $this->fixReferencesInFunction($tokens, $tokensAnalyzer, $methodOpen, $methodClose, $fixedMethods);
12✔
142
        }
143

144
        return true;
12✔
145
    }
146

147
    private function skipMethod(Tokens $tokens, TokensAnalyzer $tokensAnalyzer, int $functionKeywordIndex, int $methodOpen, int $methodClose): bool
148
    {
149
        $methodNameIndex = $tokens->getNextMeaningfulToken($functionKeywordIndex);
17✔
150
        $methodName = strtolower($tokens[$methodNameIndex]->getContent());
17✔
151
        if (isset(self::MAGIC_METHODS[$methodName])) {
17✔
152
            return true;
1✔
153
        }
154

155
        $prevTokenIndex = $tokens->getPrevMeaningfulToken($functionKeywordIndex);
16✔
156
        if ($tokens[$prevTokenIndex]->isGivenKind(\T_FINAL)) {
16✔
157
            $prevTokenIndex = $tokens->getPrevMeaningfulToken($prevTokenIndex);
1✔
158
        }
159
        if (!$tokens[$prevTokenIndex]->isGivenKind(\T_PRIVATE)) {
16✔
160
            return true;
13✔
161
        }
162

163
        $prePrevTokenIndex = $tokens->getPrevMeaningfulToken($prevTokenIndex);
16✔
164
        if ($tokens[$prePrevTokenIndex]->isGivenKind(\T_STATIC)) {
16✔
165
            return true;
1✔
166
        }
167

168
        for ($index = $methodOpen + 1; $index < $methodClose - 1; ++$index) {
15✔
169
            if ($tokens[$index]->isGivenKind(\T_CLASS) && $tokensAnalyzer->isAnonymousClass($index)) {
13✔
170
                $anonymousClassOpen = $tokens->getNextTokenOfKind($index, ['{']);
1✔
171
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $anonymousClassOpen);
1✔
172

173
                continue;
1✔
174
            }
175

176
            if ($tokens[$index]->isGivenKind(\T_FUNCTION)) {
13✔
177
                return true;
1✔
178
            }
179

180
            if ($tokens[$index]->equals([\T_VARIABLE, '$this'])) {
13✔
181
                $operatorIndex = $tokens->getNextMeaningfulToken($index);
3✔
182
                $methodNameIndex = $tokens->getNextMeaningfulToken($operatorIndex);
3✔
183
                $argumentsBraceIndex = $tokens->getNextMeaningfulToken($methodNameIndex);
3✔
184

185
                if (
186
                    !$tokens[$operatorIndex]->isGivenKind(\T_OBJECT_OPERATOR)
3✔
187
                    || $methodName !== $tokens[$methodNameIndex]->getContent()
3✔
188
                    || !$tokens[$argumentsBraceIndex]->equals('(')
3✔
189
                ) {
190
                    return true;
2✔
191
                }
192
            }
193

194
            if ($tokens[$index]->equals([\T_STRING, 'debug_backtrace'])) {
13✔
195
                return true;
1✔
196
            }
197
        }
198

199
        return false;
12✔
200
    }
201

202
    /**
203
     * @param array<string, bool> $fixedMethods
204
     */
205
    private function fixReferencesInFunction(Tokens $tokens, TokensAnalyzer $tokensAnalyzer, int $methodOpen, int $methodClose, array $fixedMethods): void
206
    {
207
        for ($index = $methodOpen + 1; $index < $methodClose - 1; ++$index) {
12✔
208
            if ($tokens[$index]->isGivenKind(\T_FUNCTION)) {
11✔
209
                $prevIndex = $tokens->getPrevMeaningfulToken($index);
2✔
210
                $closureStart = $tokens->getNextTokenOfKind($index, ['{']);
2✔
211
                $closureEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $closureStart);
2✔
212
                if (!$tokens[$prevIndex]->isGivenKind(\T_STATIC)) {
2✔
213
                    $this->fixReferencesInFunction($tokens, $tokensAnalyzer, $closureStart, $closureEnd, $fixedMethods);
2✔
214
                }
215

216
                $index = $closureEnd;
2✔
217

218
                continue;
2✔
219
            }
220

221
            if ($tokens[$index]->isGivenKind(\T_CLASS) && $tokensAnalyzer->isAnonymousClass($index)) {
11✔
222
                $anonymousClassOpen = $tokens->getNextTokenOfKind($index, ['{']);
3✔
223
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $anonymousClassOpen);
3✔
224

225
                continue;
3✔
226
            }
227

228
            if (!$tokens[$index]->equals([\T_VARIABLE, '$this'])) {
11✔
229
                continue;
11✔
230
            }
231

232
            $objectOperatorIndex = $tokens->getNextMeaningfulToken($index);
8✔
233
            if (!$tokens[$objectOperatorIndex]->isGivenKind(\T_OBJECT_OPERATOR)) {
8✔
UNCOV
234
                continue;
×
235
            }
236

237
            $methodNameIndex = $tokens->getNextMeaningfulToken($objectOperatorIndex);
8✔
238
            $argumentsBraceIndex = $tokens->getNextMeaningfulToken($methodNameIndex);
8✔
239
            if (!$tokens[$argumentsBraceIndex]->equals('(')) {
8✔
240
                continue;
4✔
241
            }
242

243
            $currentMethodName = $tokens[$methodNameIndex]->getContent();
8✔
244
            if (!isset($fixedMethods[$currentMethodName])) {
8✔
245
                continue;
1✔
246
            }
247

248
            $tokens[$index] = new Token([\T_STRING, 'self']);
8✔
249
            $tokens[$objectOperatorIndex] = new Token([\T_DOUBLE_COLON, '::']);
8✔
250
        }
251
    }
252

253
    /**
254
     * @return list<array{int, int, int}>
255
     */
256
    private function getClassMethods(Tokens $tokens, int $classOpen, int $classClose): array
257
    {
258
        $methods = [];
17✔
259
        for ($index = $classClose - 1; $index > $classOpen + 1; --$index) {
17✔
260
            if ($tokens[$index]->equals('}')) {
17✔
261
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
17✔
262

263
                continue;
17✔
264
            }
265

266
            if (!$tokens[$index]->isGivenKind(\T_FUNCTION)) {
17✔
267
                continue;
17✔
268
            }
269

270
            $functionKeywordIndex = $index;
17✔
271
            $prevTokenIndex = $tokens->getPrevMeaningfulToken($functionKeywordIndex);
17✔
272
            $prevPrevTokenIndex = $tokens->getPrevMeaningfulToken($prevTokenIndex);
17✔
273
            if ($tokens[$prevTokenIndex]->isGivenKind(\T_ABSTRACT) || $tokens[$prevPrevTokenIndex]->isGivenKind(\T_ABSTRACT)) {
17✔
274
                continue;
2✔
275
            }
276

277
            $methodOpen = $tokens->getNextTokenOfKind($functionKeywordIndex, ['{']);
17✔
278
            $methodClose = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $methodOpen);
17✔
279

280
            $methods[] = [$functionKeywordIndex, $methodOpen, $methodClose];
17✔
281
        }
282

283
        return $methods;
17✔
284
    }
285
}
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