• 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

97.39
/src/Fixer/Basic/PsrAutoloadingFixer.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\Basic;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\DocBlock\TypeExpression;
19
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
20
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
22
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
23
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
24
use PhpCsFixer\FixerDefinition\FileSpecificCodeSample;
25
use PhpCsFixer\FixerDefinition\FixerDefinition;
26
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
27
use PhpCsFixer\Preg;
28
use PhpCsFixer\StdinFileInfo;
29
use PhpCsFixer\Tokenizer\Token;
30
use PhpCsFixer\Tokenizer\Tokens;
31
use PhpCsFixer\Tokenizer\TokensAnalyzer;
32

33
/**
34
 * @phpstan-type _AutogeneratedInputConfiguration array{
35
 *  dir?: null|string,
36
 * }
37
 * @phpstan-type _AutogeneratedComputedConfiguration array{
38
 *  dir: null|string,
39
 * }
40
 *
41
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
42
 *
43
 * @author Jordi Boggiano <j.boggiano@seld.be>
44
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
45
 * @author Bram Gotink <bram@gotink.me>
46
 * @author Graham Campbell <hello@gjcampbell.co.uk>
47
 * @author Kuba Werłos <werlos@gmail.com>
48
 *
49
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
50
 */
51
final class PsrAutoloadingFixer extends AbstractFixer implements ConfigurableFixerInterface
52
{
53
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
54
    use ConfigurableFixerTrait;
55

56
    public function getDefinition(): FixerDefinitionInterface
57
    {
58
        return new FixerDefinition(
3✔
59
            'Classes must be in a path that matches their namespace, be at least one namespace deep and the class name should match the file name.',
3✔
60
            [
3✔
61
                new FileSpecificCodeSample(
3✔
62
                    <<<'PHP'
3✔
63
                        <?php
64
                        namespace PhpCsFixer\FIXER\Basic;
65
                        class InvalidName {}
66

67
                        PHP,
3✔
68
                    new \SplFileInfo(__FILE__)
3✔
69
                ),
3✔
70
                new FileSpecificCodeSample(
3✔
71
                    <<<'PHP'
3✔
72
                        <?php
73
                        namespace PhpCsFixer\FIXER\Basic;
74
                        class InvalidName {}
75

76
                        PHP,
3✔
77
                    new \SplFileInfo(__FILE__),
3✔
78
                    ['dir' => './src']
3✔
79
                ),
3✔
80
            ],
3✔
81
            null,
3✔
82
            'This fixer may change your class name, which will break the code that depends on the old name.'
3✔
83
        );
3✔
84
    }
85

86
    public function isCandidate(Tokens $tokens): bool
87
    {
88
        return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
47✔
89
    }
90

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

96
    /**
97
     * {@inheritdoc}
98
     *
99
     * Must run before SelfAccessorFixer.
100
     */
101
    public function getPriority(): int
102
    {
103
        return -10;
1✔
104
    }
105

106
    public function supports(\SplFileInfo $file): bool
107
    {
108
        if ($file instanceof StdinFileInfo) {
199✔
UNCOV
109
            return false;
×
110
        }
111

112
        if (
113
            // ignore file with extension other than php
114
            ('php' !== $file->getExtension())
199✔
115
            // ignore file with name that cannot be a class name
116
            || !Preg::match('/^'.TypeExpression::REGEX_IDENTIFIER.'$/', $file->getBasename('.php'))
199✔
117
        ) {
118
            return false;
4✔
119
        }
120

121
        try {
122
            $tokens = Tokens::fromCode(\sprintf('<?php class %s {}', $file->getBasename('.php')));
195✔
123

124
            if ($tokens[3]->isKeyword() || $tokens[3]->isMagicConstant()) {
47✔
125
                // name cannot be a class name - detected by PHP 5.x
126
                return false;
47✔
127
            }
128
        } catch (\ParseError $e) {
148✔
129
            // name cannot be a class name - detected by PHP 7.x
130
            return false;
148✔
131
        }
132

133
        // ignore stubs/fixtures, since they typically contain invalid files for various reasons
134
        return !Preg::match('{[/\\\](stub|fixture)s?[/\\\]}i', $file->getRealPath());
47✔
135
    }
136

137
    protected function configurePostNormalisation(): void
138
    {
139
        if (null !== $this->configuration['dir']) {
208✔
140
            $realpath = realpath($this->configuration['dir']);
14✔
141

142
            if (false === $realpath) {
14✔
UNCOV
143
                throw new \InvalidArgumentException(\sprintf('Failed to resolve configured directory "%s".', $this->configuration['dir']));
×
144
            }
145

146
            $this->configuration['dir'] = $realpath;
14✔
147
        }
148
    }
149

150
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
151
    {
152
        return new FixerConfigurationResolver([
208✔
153
            (new FixerOptionBuilder('dir', 'If provided, the directory where the project code is placed.'))
208✔
154
                ->setAllowedTypes(['null', 'string'])
208✔
155
                ->setDefault(null)
208✔
156
                ->getOption(),
208✔
157
        ]);
208✔
158
    }
159

160
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
161
    {
162
        $tokenAnalyzer = new TokensAnalyzer($tokens);
47✔
163

164
        if (null !== $this->configuration['dir'] && !str_starts_with($file->getRealPath(), $this->configuration['dir'])) {
47✔
165
            return;
1✔
166
        }
167

168
        $namespace = null;
46✔
169
        $namespaceStartIndex = null;
46✔
170
        $namespaceEndIndex = null;
46✔
171

172
        $classyName = null;
46✔
173
        $classyIndex = null;
46✔
174

175
        foreach ($tokens as $index => $token) {
46✔
176
            if ($token->isGivenKind(\T_NAMESPACE)) {
46✔
177
                if (null !== $namespace) {
30✔
178
                    return;
2✔
179
                }
180

181
                $namespaceStartIndex = $tokens->getNextMeaningfulToken($index);
30✔
182
                $namespaceEndIndex = $tokens->getNextTokenOfKind($namespaceStartIndex, [';']);
30✔
183
                $namespace = trim($tokens->generatePartialCode($namespaceStartIndex, $namespaceEndIndex - 1));
30✔
184
            } elseif ($token->isClassy()) {
46✔
185
                if ($tokenAnalyzer->isAnonymousClass($index)) {
45✔
186
                    continue;
5✔
187
                }
188

189
                if (null !== $classyName) {
42✔
190
                    return;
3✔
191
                }
192

193
                $classyIndex = $tokens->getNextMeaningfulToken($index);
42✔
194
                $classyName = $tokens[$classyIndex]->getContent();
42✔
195
            }
196
        }
197

198
        if (null === $classyName) {
41✔
199
            return;
3✔
200
        }
201

202
        $expectedClassyName = $this->calculateClassyName($file, $namespace, $classyName);
38✔
203

204
        if ($classyName !== $expectedClassyName) {
38✔
205
            $tokens[$classyIndex] = new Token([\T_STRING, $expectedClassyName]);
24✔
206
        }
207

208
        if (null === $this->configuration['dir'] || null === $namespace) {
38✔
209
            return;
29✔
210
        }
211

212
        if (!is_dir($this->configuration['dir'])) {
10✔
UNCOV
213
            return;
×
214
        }
215

216
        $configuredDir = realpath($this->configuration['dir']);
10✔
217
        $fileDir = \dirname($file->getRealPath());
10✔
218

219
        if (\strlen($configuredDir) >= \strlen($fileDir)) {
10✔
220
            return;
2✔
221
        }
222

223
        $newNamespace = substr(str_replace('/', '\\', $fileDir), \strlen($configuredDir) + 1);
8✔
224
        $originalNamespace = substr($namespace, -\strlen($newNamespace));
8✔
225

226
        if ($originalNamespace !== $newNamespace && strtolower($originalNamespace) === strtolower($newNamespace)) {
8✔
227
            $tokens->clearRange($namespaceStartIndex, $namespaceEndIndex);
7✔
228
            $namespace = substr($namespace, 0, -\strlen($newNamespace)).$newNamespace;
7✔
229

230
            $newNamespace = Tokens::fromCode('<?php namespace '.$namespace.';');
7✔
231
            $newNamespace->clearRange(0, 2);
7✔
232
            $newNamespace->clearEmptyTokens();
7✔
233

234
            $tokens->insertAt($namespaceStartIndex, $newNamespace);
7✔
235
        }
236
    }
237

238
    private function calculateClassyName(\SplFileInfo $file, ?string $namespace, string $currentName): string
239
    {
240
        $name = $file->getBasename('.php');
38✔
241
        $maxNamespace = $this->calculateMaxNamespace($file, $namespace);
38✔
242

243
        if (null !== $this->configuration['dir']) {
38✔
244
            return ('' !== $maxNamespace ? (str_replace('\\', '_', $maxNamespace).'_') : '').$name;
13✔
245
        }
246

247
        $namespaceParts = array_reverse(explode('\\', $maxNamespace));
26✔
248

249
        foreach ($namespaceParts as $namespacePart) {
26✔
250
            $nameCandidate = \sprintf('%s_%s', $namespacePart, $name);
26✔
251

252
            if (strtolower($nameCandidate) !== strtolower(substr($currentName, -\strlen($nameCandidate)))) {
26✔
253
                break;
26✔
254
            }
255

256
            $name = $nameCandidate;
2✔
257
        }
258

259
        return $name;
26✔
260
    }
261

262
    private function calculateMaxNamespace(\SplFileInfo $file, ?string $namespace): string
263
    {
264
        if (null === $this->configuration['dir']) {
38✔
265
            $root = \dirname($file->getRealPath());
26✔
266

267
            while ($root !== \dirname($root)) {
26✔
268
                $root = \dirname($root);
26✔
269
            }
270
        } else {
271
            $root = realpath($this->configuration['dir']);
13✔
272
        }
273

274
        $namespaceAccordingToFileLocation = trim(str_replace(\DIRECTORY_SEPARATOR, '\\', substr(\dirname($file->getRealPath()), \strlen($root))), '\\');
38✔
275

276
        if (null === $namespace) {
38✔
277
            return $namespaceAccordingToFileLocation;
15✔
278
        }
279

280
        $namespaceAccordingToFileLocationPartsReversed = array_reverse(explode('\\', $namespaceAccordingToFileLocation));
23✔
281
        $namespacePartsReversed = array_reverse(explode('\\', $namespace));
23✔
282

283
        foreach ($namespacePartsReversed as $key => $namespaceParte) {
23✔
284
            if (!isset($namespaceAccordingToFileLocationPartsReversed[$key])) {
23✔
285
                break;
7✔
286
            }
287

288
            if (strtolower($namespaceParte) !== strtolower($namespaceAccordingToFileLocationPartsReversed[$key])) {
23✔
289
                break;
16✔
290
            }
291

292
            unset($namespaceAccordingToFileLocationPartsReversed[$key]);
17✔
293
        }
294

295
        return implode('\\', array_reverse($namespaceAccordingToFileLocationPartsReversed));
23✔
296
    }
297
}
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