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

keradus / PHP-CS-Fixer / 22042339290

15 Feb 2026 08:14PM UTC coverage: 92.957% (-0.2%) from 93.171%
22042339290

push

github

keradus
test: check PHP env in CI jobs

29302 of 31522 relevant lines covered (92.96%)

44.04 hits per line

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

99.3
/src/Fixer/Import/FullyQualifiedStrictTypesFixer.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\Import;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\DocBlock\TypeExpression;
19
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
20
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
21
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
22
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
23
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
24
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
25
use PhpCsFixer\FixerDefinition\CodeSample;
26
use PhpCsFixer\FixerDefinition\FixerDefinition;
27
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
28
use PhpCsFixer\Preg;
29
use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
30
use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
31
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
32
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
33
use PhpCsFixer\Tokenizer\CT;
34
use PhpCsFixer\Tokenizer\FCT;
35
use PhpCsFixer\Tokenizer\Processor\ImportProcessor;
36
use PhpCsFixer\Tokenizer\Token;
37
use PhpCsFixer\Tokenizer\Tokens;
38

39
/**
40
 * @phpstan-type _AutogeneratedInputConfiguration array{
41
 *  import_symbols?: bool,
42
 *  leading_backslash_in_global_namespace?: bool,
43
 *  phpdoc_tags?: list<string>,
44
 * }
45
 * @phpstan-type _AutogeneratedComputedConfiguration array{
46
 *  import_symbols: bool,
47
 *  leading_backslash_in_global_namespace: bool,
48
 *  phpdoc_tags: list<string>,
49
 * }
50
 * @phpstan-type _Uses array{
51
 *   constant?: array<non-empty-string, non-empty-string>,
52
 *   class?: array<non-empty-string, non-empty-string>,
53
 *   function?: array<non-empty-string, non-empty-string>
54
 * }
55
 *
56
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
57
 *
58
 * @author VeeWee <toonverwerft@gmail.com>
59
 * @author Tomas Jadrny <developer@tomasjadrny.cz>
60
 * @author Greg Korba <greg@codito.dev>
61
 * @author SpacePossum <possumfromspace@gmail.com>
62
 * @author Michael Vorisek <https://github.com/mvorisek>
63
 *
64
 * @phpstan-import-type _ImportType from \PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis
65
 *
66
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
67
 */
68
final class FullyQualifiedStrictTypesFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
69
{
70
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
71
    use ConfigurableFixerTrait;
72

73
    private const REGEX_CLASS = '(?:\\\?+'.TypeExpression::REGEX_IDENTIFIER
74
        .'(\\\\'.TypeExpression::REGEX_IDENTIFIER.')*+)';
75
    private const CLASSY_KINDS = [\T_CLASS, \T_INTERFACE, \T_TRAIT, FCT::T_ENUM];
76

77
    /**
78
     * @var null|array{
79
     *     constant?: list<non-empty-string>,
80
     *     class?: list<non-empty-string>,
81
     *     function?: list<non-empty-string>
82
     * }
83
     */
84
    private ?array $discoveredSymbols;
85

86
    /**
87
     * @var array{
88
     *     constant?: array<string, non-empty-string>,
89
     *     class?: array<string, non-empty-string>,
90
     *     function?: array<string, non-empty-string>
91
     * }
92
     */
93
    private array $symbolsForImport = [];
94

95
    /**
96
     * @var array<int<0, max>, array<string, true>>
97
     */
98
    private array $reservedIdentifiersByLevel;
99

100
    /**
101
     * @var array{
102
     *     constant?: array<string, string>,
103
     *     class?: array<string, string>,
104
     *     function?: array<string, string>
105
     * }
106
     */
107
    private array $cacheUsesLast = [];
108

109
    /**
110
     * @var array{
111
     *     constant?: array<non-empty-lowercase-string, non-empty-string>,
112
     *     class?: array<non-empty-lowercase-string, non-empty-string>,
113
     *     function?: array<non-empty-lowercase-string, non-empty-string>
114
     * }
115
     */
116
    private array $cacheUseNameByShortNameLower;
117

118
    /** @var _Uses */
119
    private array $cacheUseShortNameByName;
120

121
    /** @var _Uses */
122
    private array $cacheUseShortNameByNormalizedName;
123

124
    public function getDefinition(): FixerDefinitionInterface
125
    {
126
        return new FixerDefinition(
3✔
127
            'Removes the leading part of fully qualified symbol references if a given symbol is imported or belongs to the current namespace.',
3✔
128
            [
3✔
129
                new CodeSample(
3✔
130
                    <<<'PHP'
3✔
131
                        <?php
132

133
                        use Foo\Bar;
134
                        use Foo\Bar\Baz;
135
                        use Foo\OtherClass;
136
                        use Foo\SomeContract;
137
                        use Foo\SomeException;
138

139
                        /**
140
                         * @see \Foo\Bar\Baz
141
                         */
142
                        class SomeClass extends \Foo\OtherClass implements \Foo\SomeContract
143
                        {
144
                            /**
145
                             * @var \Foo\Bar\Baz
146
                             */
147
                            public $baz;
148

149
                            /**
150
                             * @param \Foo\Bar\Baz $baz
151
                             */
152
                            public function __construct($baz) {
153
                                $this->baz = $baz;
154
                            }
155

156
                            /**
157
                             * @return \Foo\Bar\Baz
158
                             */
159
                            public function getBaz() {
160
                                return $this->baz;
161
                            }
162

163
                            public function doX(\Foo\Bar $foo, \Exception $e): \Foo\Bar\Baz
164
                            {
165
                                try {}
166
                                catch (\Foo\SomeException $e) {}
167
                            }
168
                        }
169

170
                        PHP,
3✔
171
                ),
3✔
172
                new CodeSample(
3✔
173
                    <<<'PHP'
3✔
174
                        <?php
175

176
                        class SomeClass
177
                        {
178
                            public function doY(Foo\NotImported $u, \Foo\NotImported $v)
179
                            {
180
                            }
181
                        }
182

183
                        PHP,
3✔
184
                    ['leading_backslash_in_global_namespace' => true],
3✔
185
                ),
3✔
186
                new CodeSample(
3✔
187
                    <<<'PHP'
3✔
188
                        <?php
189
                        namespace {
190
                            use Foo\A;
191
                            try {
192
                                foo();
193
                            } catch (\Exception|\Foo\A $e) {
194
                            }
195
                        }
196
                        namespace Foo\Bar {
197
                            class SomeClass implements \Foo\Bar\Baz
198
                            {
199
                            }
200
                        }
201

202
                        PHP,
3✔
203
                    ['leading_backslash_in_global_namespace' => true],
3✔
204
                ),
3✔
205
                new CodeSample(
3✔
206
                    <<<'PHP'
3✔
207
                        <?php
208

209
                        namespace Foo\Test;
210

211
                        class Foo extends \Other\BaseClass implements \Other\Interface1, \Other\Interface2
212
                        {
213
                            /** @var \Other\PropertyPhpDoc */
214
                            private $array;
215
                            public function __construct(\Other\FunctionArgument $arg) {}
216
                            public function foo(): \Other\FunctionReturnType
217
                            {
218
                                try {
219
                                    \Other\StaticFunctionCall::bar();
220
                                } catch (\Other\CaughtThrowable $e) {}
221
                            }
222
                        }
223

224
                        PHP,
3✔
225
                    ['import_symbols' => true],
3✔
226
                ),
3✔
227
            ],
3✔
228
        );
3✔
229
    }
230

231
    /**
232
     * {@inheritdoc}
233
     *
234
     * Must run before NoSuperfluousPhpdocTagsFixer, OrderedAttributesFixer, OrderedImportsFixer, OrderedInterfacesFixer, StatementIndentationFixer.
235
     * Must run after ClassKeywordFixer, PhpUnitAttributesFixer, PhpdocToPropertyTypeFixer, PhpdocToReturnTypeFixer.
236
     */
237
    public function getPriority(): int
238
    {
239
        return 7;
1✔
240
    }
241

242
    public function isCandidate(Tokens $tokens): bool
243
    {
244
        return $tokens->isAnyTokenKindsFound([
154✔
245
            CT::T_USE_TRAIT,
154✔
246
            FCT::T_ATTRIBUTE,
154✔
247
            \T_CATCH,
154✔
248
            \T_DOUBLE_COLON,
154✔
249
            \T_DOC_COMMENT,
154✔
250
            \T_EXTENDS,
154✔
251
            \T_FUNCTION,
154✔
252
            \T_IMPLEMENTS,
154✔
253
            \T_INSTANCEOF,
154✔
254
            \T_NEW,
154✔
255
            \T_VARIABLE,
154✔
256
        ]);
154✔
257
    }
258

259
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
260
    {
261
        return new FixerConfigurationResolver([
163✔
262
            (new FixerOptionBuilder(
163✔
263
                'leading_backslash_in_global_namespace',
163✔
264
                'Whether FQCN is prefixed with backslash when that FQCN is used in global namespace context.',
163✔
265
            ))
163✔
266
                ->setAllowedTypes(['bool'])
163✔
267
                ->setDefault(false)
163✔
268
                ->getOption(),
163✔
269
            (new FixerOptionBuilder(
163✔
270
                'import_symbols',
163✔
271
                'Whether FQCNs should be automatically imported.',
163✔
272
            ))
163✔
273
                ->setAllowedTypes(['bool'])
163✔
274
                ->setDefault(false)
163✔
275
                ->getOption(),
163✔
276
            (new FixerOptionBuilder(
163✔
277
                'phpdoc_tags',
163✔
278
                'Collection of PHPDoc annotation tags where FQCNs should be processed. As of now only simple tags with `@tag \F\Q\C\N` format are supported (no complex types).',
163✔
279
            ))
163✔
280
                ->setAllowedTypes(['string[]'])
163✔
281
                ->setDefault([
163✔
282
                    'param',
163✔
283
                    'phpstan-param',
163✔
284
                    'phpstan-property',
163✔
285
                    'phpstan-property-read',
163✔
286
                    'phpstan-property-write',
163✔
287
                    'phpstan-return',
163✔
288
                    'phpstan-var',
163✔
289
                    'property',
163✔
290
                    'property-read',
163✔
291
                    'property-write',
163✔
292
                    'psalm-param',
163✔
293
                    'psalm-property',
163✔
294
                    'psalm-property-read',
163✔
295
                    'psalm-property-write',
163✔
296
                    'psalm-return',
163✔
297
                    'psalm-var',
163✔
298
                    'return',
163✔
299
                    'see',
163✔
300
                    'throws',
163✔
301
                    'var',
163✔
302
                ])
163✔
303
                ->getOption(),
163✔
304
        ]);
163✔
305
    }
306

307
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
308
    {
309
        $namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();
154✔
310
        $functionsAnalyzer = new FunctionsAnalyzer();
154✔
311

312
        $this->symbolsForImport = [];
154✔
313

314
        foreach ($tokens->getNamespaceDeclarations() as $namespaceIndex => $namespace) {
154✔
315
            $namespace = $tokens->getNamespaceDeclarations()[$namespaceIndex];
154✔
316

317
            $namespaceName = $namespace->getFullName();
154✔
318

319
            $uses = [];
154✔
320
            $lastUse = null;
154✔
321

322
            foreach ($namespaceUsesAnalyzer->getDeclarationsInNamespace($tokens, $namespace, true) as $use) {
154✔
323
                if (!$use->isClass()) {
100✔
324
                    continue;
6✔
325
                }
326

327
                $fullName = ltrim($use->getFullName(), '\\');
99✔
328
                \assert('' !== $fullName);
99✔
329
                $uses[$use->getHumanFriendlyType()][$fullName] = $use->getShortName();
99✔
330
                $lastUse = $use;
99✔
331
            }
332

333
            $indexDiff = 0;
154✔
334
            foreach (true === $this->configuration['import_symbols'] ? [true, false] : [false] as $discoverSymbolsPhase) {
154✔
335
                $this->discoveredSymbols = $discoverSymbolsPhase ? [] : null;
154✔
336

337
                $openedCurlyBrackets = 0;
154✔
338
                $this->reservedIdentifiersByLevel = [];
154✔
339

340
                for ($index = $namespace->getScopeStartIndex(); $index < $namespace->getScopeEndIndex() + $indexDiff; ++$index) {
154✔
341
                    $origSize = \count($tokens);
154✔
342
                    $token = $tokens[$index];
154✔
343

344
                    if ($token->equals('{')) {
154✔
345
                        ++$openedCurlyBrackets;
120✔
346
                    } elseif ($token->equals('}')) {
154✔
347
                        unset($this->reservedIdentifiersByLevel[$openedCurlyBrackets]);
57✔
348
                        --$openedCurlyBrackets;
57✔
349
                        \assert($openedCurlyBrackets >= 0);
57✔
350
                    } elseif ($token->isGivenKind(\T_VARIABLE)) {
154✔
351
                        $prevIndex = $tokens->getPrevMeaningfulToken($index);
107✔
352
                        if (null !== $prevIndex && $tokens[$prevIndex]->isGivenKind(\T_STRING)) {
107✔
353
                            $this->fixPrevName($tokens, $index, $uses, $namespaceName);
72✔
354
                        }
355
                    } elseif ($token->isGivenKind(\T_DOUBLE_COLON)) {
154✔
356
                        $this->fixPrevName($tokens, $index, $uses, $namespaceName);
16✔
357
                    } elseif ($token->isGivenKind(\T_FUNCTION)) {
154✔
358
                        $this->fixFunction($functionsAnalyzer, $tokens, $index, $uses, $namespaceName);
91✔
359
                    } elseif ($token->isGivenKind(FCT::T_ATTRIBUTE)) {
154✔
360
                        $this->fixAttribute($tokens, $index, $uses, $namespaceName);
2✔
361
                    } elseif ($token->isGivenKind(\T_CATCH)) {
154✔
362
                        $this->fixCatch($tokens, $index, $uses, $namespaceName);
8✔
363
                    } elseif ($discoverSymbolsPhase && $token->isGivenKind(self::CLASSY_KINDS)) {
154✔
364
                        $this->fixNextName($tokens, $index, $uses, $namespaceName);
13✔
365
                    } elseif ($token->isGivenKind([\T_EXTENDS, \T_IMPLEMENTS])) {
154✔
366
                        $this->fixExtendsImplements($tokens, $index, $uses, $namespaceName);
18✔
367
                    } elseif ($token->isGivenKind([\T_INSTANCEOF, \T_NEW, CT::T_USE_TRAIT, CT::T_TYPE_COLON])) {
154✔
368
                        $this->fixNextName($tokens, $index, $uses, $namespaceName);
70✔
369
                    } elseif ($discoverSymbolsPhase && $token->isGivenKind(\T_COMMENT) && Preg::match('/#\[\s*('.self::REGEX_CLASS.')/', $token->getContent(), $matches)) { // @TODO: drop when PHP 8.0+ is required
154✔
370
                        $attributeClass = $matches[1];
×
371
                        $this->determineShortType($attributeClass, 'class', $uses, $namespaceName);
×
372
                    } elseif ($token->isGivenKind(\T_DOC_COMMENT)) {
154✔
373
                        Preg::matchAll('/\*\h*@(?:psalm-|phpstan-)?(?:template(?:-covariant|-contravariant)?|(?:import-)?type)\h+('.TypeExpression::REGEX_IDENTIFIER.')(?!\S)/i', $token->getContent(), $matches);
35✔
374
                        foreach ($matches[1] as $reservedIdentifier) {
35✔
375
                            $this->reservedIdentifiersByLevel[$openedCurlyBrackets + 1][$reservedIdentifier] = true;
4✔
376
                        }
377

378
                        $this->fixPhpDoc($tokens, $index, $uses, $namespaceName);
35✔
379
                    }
380

381
                    $indexDiff += \count($tokens) - $origSize;
154✔
382
                }
383

384
                $this->reservedIdentifiersByLevel = [];
154✔
385

386
                if ($discoverSymbolsPhase) {
154✔
387
                    $this->setupUsesFromDiscoveredSymbols($uses, $namespaceName);
39✔
388
                }
389
            }
390

391
            if ([] !== $this->symbolsForImport) { // @phpstan-ignore-line notIdentical.alwaysFalse PHP started to complain, please fix me
154✔
392
                if (null !== $lastUse) {
25✔
393
                    $atIndex = $lastUse->getEndIndex() + 1;
6✔
394
                } elseif (0 !== $namespace->getEndIndex()) {
19✔
395
                    $atIndex = $namespace->getEndIndex() + 1;
13✔
396
                } else {
397
                    $firstTokenIndex = $tokens->getNextMeaningfulToken($namespace->getScopeStartIndex());
6✔
398
                    if (null !== $firstTokenIndex && $tokens[$firstTokenIndex]->isGivenKind(\T_DECLARE)) {
6✔
399
                        $atIndex = $tokens->getNextTokenOfKind($firstTokenIndex, [';']) + 1;
1✔
400
                    } else {
401
                        $atIndex = $namespace->getScopeStartIndex() + 1;
5✔
402
                    }
403
                }
404

405
                // Insert all registered FQCNs
406
                $this->createImportProcessor()->insertImports($tokens, $this->symbolsForImport, $atIndex);
25✔
407

408
                $this->symbolsForImport = [];
25✔
409
            }
410
        }
411
    }
412

413
    /**
414
     * @param _Uses $uses
415
     */
416
    private function refreshUsesCache(array $uses): void
417
    {
418
        if ($this->cacheUsesLast === $uses) {
145✔
419
            return;
143✔
420
        }
421

422
        $this->cacheUsesLast = $uses;
95✔
423

424
        $this->cacheUseNameByShortNameLower = [];
95✔
425
        $this->cacheUseShortNameByName = [];
95✔
426
        $this->cacheUseShortNameByNormalizedName = [];
95✔
427

428
        foreach ($uses as $kind => $kindUses) {
95✔
429
            foreach ($kindUses as $useLongName => $useShortName) {
95✔
430
                $this->cacheUseNameByShortNameLower[$kind][strtolower($useShortName)] = $useLongName;
95✔
431
                $this->cacheUseShortNameByName[$kind][$useLongName] = $useShortName;
95✔
432

433
                /** @var non-empty-string */
434
                $normalizedUseLongName = $this->normalizeFqcn($useLongName);
95✔
435
                $this->cacheUseShortNameByNormalizedName[$kind][$normalizedUseLongName] = $useShortName;
95✔
436
            }
437
        }
438
    }
439

440
    private function isReservedIdentifier(string $symbol): bool
441
    {
442
        if (str_contains($symbol, '\\')) { // optimization only
149✔
443
            return false;
125✔
444
        }
445

446
        if ((new TypeAnalysis($symbol))->isReservedType()) {
139✔
447
            return true;
23✔
448
        }
449

450
        foreach ($this->reservedIdentifiersByLevel as $reservedIdentifiers) {
136✔
451
            if (isset($reservedIdentifiers[$symbol])) {
4✔
452
                return true;
4✔
453
            }
454
        }
455

456
        return false;
135✔
457
    }
458

459
    /**
460
     * Resolve absolute or relative symbol to normalized FQCN.
461
     *
462
     * @param _ImportType $importKind
463
     * @param _Uses       $uses
464
     *
465
     * @return non-empty-string
466
     */
467
    private function resolveSymbol(string $symbol, string $importKind, array $uses, string $namespaceName): string
468
    {
469
        if (str_starts_with($symbol, '\\')) {
149✔
470
            return substr($symbol, 1); // @phpstan-ignore return.type
130✔
471
        }
472

473
        if ($this->isReservedIdentifier($symbol)) {
138✔
474
            return $symbol; // @phpstan-ignore return.type
25✔
475
        }
476

477
        $this->refreshUsesCache($uses);
132✔
478

479
        $symbolArr = explode('\\', $symbol, 2);
132✔
480
        $shortStartNameLower = strtolower($symbolArr[0]);
132✔
481
        if (isset($this->cacheUseNameByShortNameLower[$importKind][$shortStartNameLower])) {
132✔
482
            return $this->cacheUseNameByShortNameLower[$importKind][$shortStartNameLower].(isset($symbolArr[1]) ? '\\'.$symbolArr[1] : '');
86✔
483
        }
484

485
        return ('' !== $namespaceName ? $namespaceName.'\\' : '').$symbol; // @phpstan-ignore return.type
83✔
486
    }
487

488
    /**
489
     * Shorten normalized FQCN as much as possible.
490
     *
491
     * @param _ImportType $importKind
492
     * @param _Uses       $uses
493
     */
494
    private function shortenSymbol(string $fqcn, string $importKind, array $uses, string $namespaceName): string
495
    {
496
        if ($this->isReservedIdentifier($fqcn)) {
149✔
497
            return $fqcn;
25✔
498
        }
499

500
        $this->refreshUsesCache($uses);
145✔
501

502
        $res = null;
145✔
503

504
        // try to shorten the name using namespace
505
        $iMin = 0;
145✔
506
        if (str_starts_with($fqcn, $namespaceName.'\\')) {
145✔
507
            $tmpRes = substr($fqcn, \strlen($namespaceName) + 1);
58✔
508
            if (!isset($this->cacheUseNameByShortNameLower[$importKind][strtolower(explode('\\', $tmpRes, 2)[0])]) && !$this->isReservedIdentifier($tmpRes)) {
58✔
509
                $res = $tmpRes;
54✔
510
                $iMin = substr_count($namespaceName, '\\') + 1;
54✔
511
            }
512
        }
513

514
        // try to shorten the name using uses
515
        $tmp = $fqcn;
145✔
516
        for ($i = substr_count($fqcn, '\\'); $i >= $iMin; --$i) {
145✔
517
            if (isset($this->cacheUseShortNameByName[$importKind][$tmp])) {
145✔
518
                $tmpRes = $this->cacheUseShortNameByName[$importKind][$tmp].substr($fqcn, \strlen($tmp));
86✔
519
                if (!$this->isReservedIdentifier($tmpRes)) {
86✔
520
                    $res = $tmpRes;
86✔
521

522
                    break;
86✔
523
                }
524
            }
525

526
            if ($i > 0) {
116✔
527
                $tmp = substr($tmp, 0, strrpos($tmp, '\\'));
90✔
528
            }
529
        }
530

531
        if (null === $res) {
145✔
532
            $normalizedFqcn = $this->normalizeFqcn($fqcn);
86✔
533
            $tmpRes = $this->cacheUseShortNameByNormalizedName[$importKind][$normalizedFqcn] ?? null;
86✔
534
            if (null !== $tmpRes && !$this->isReservedIdentifier($tmpRes)) {
86✔
535
                $res = $tmpRes;
1✔
536
            }
537
        }
538

539
        // shortening is not possible, add leading backslash if needed
540
        if (null === $res) {
145✔
541
            $res = $fqcn;
85✔
542
            if ('' !== $namespaceName
85✔
543
                || true === $this->configuration['leading_backslash_in_global_namespace']
85✔
544
                || isset($this->cacheUseNameByShortNameLower[$importKind][strtolower(explode('\\', $res, 2)[0])])
85✔
545
            ) {
546
                $res = '\\'.$res;
62✔
547
            }
548
        }
549

550
        return $res;
145✔
551
    }
552

553
    /**
554
     * @param _Uses $uses
555
     */
556
    private function setupUsesFromDiscoveredSymbols(array &$uses, string $namespaceName): void
557
    {
558
        foreach ($this->discoveredSymbols as $kind => $discoveredSymbols) {
39✔
559
            $discoveredFqcnByShortNameLower = [];
39✔
560

561
            if ('' === $namespaceName) {
39✔
562
                foreach ($discoveredSymbols as $symbol) {
11✔
563
                    if (!str_starts_with($symbol, '\\')) {
11✔
564
                        $shortStartName = explode('\\', ltrim($symbol, '\\'), 2)[0];
11✔
565
                        \assert('' !== $shortStartName);
11✔
566
                        $shortStartNameLower = strtolower($shortStartName);
11✔
567
                        $discoveredFqcnByShortNameLower[$kind][$shortStartNameLower] = $this->resolveSymbol($shortStartName, $kind, $uses, $namespaceName);
11✔
568
                    }
569
                }
570
            }
571

572
            foreach ($uses[$kind] ?? [] as $useLongName => $useShortName) {
39✔
573
                $discoveredFqcnByShortNameLower[$kind][strtolower($useShortName)] = $useLongName;
27✔
574
            }
575

576
            $useByShortNameLower = [];
39✔
577
            foreach ($uses[$kind] ?? [] as $useShortName) {
39✔
578
                $useByShortNameLower[strtolower($useShortName)] = true;
27✔
579
            }
580

581
            uasort($discoveredSymbols, static function ($a, $b) {
39✔
582
                $res = str_starts_with($a, '\\') <=> str_starts_with($b, '\\');
37✔
583
                if (0 !== $res) {
37✔
584
                    return $res;
26✔
585
                }
586

587
                return substr_count($a, '\\') <=> substr_count($b, '\\');
31✔
588
            });
39✔
589
            foreach ($discoveredSymbols as $symbol) {
39✔
590
                while (true) {
39✔
591
                    $shortEndNameLower = strtolower(str_contains($symbol, '\\') ? substr($symbol, (int) strrpos($symbol, '\\') + 1) : $symbol);
39✔
592
                    if (!isset($discoveredFqcnByShortNameLower[$kind][$shortEndNameLower])) {
39✔
593
                        $shortStartNameLower = strtolower(explode('\\', ltrim($symbol, '\\'), 2)[0]);
37✔
594
                        if (str_starts_with($symbol, '\\') || ('' === $namespaceName && !isset($useByShortNameLower[$shortStartNameLower]))
37✔
595
                            || !str_contains($symbol, '\\')
37✔
596
                        ) {
597
                            $discoveredFqcnByShortNameLower[$kind][$shortEndNameLower] = $this->resolveSymbol($symbol, $kind, $uses, $namespaceName);
37✔
598

599
                            break;
37✔
600
                        }
601
                    }
602
                    // else short name collision - keep unimported
603

604
                    if (str_starts_with($symbol, '\\') || '' === $namespaceName || !str_contains($symbol, '\\')) {
39✔
605
                        break;
39✔
606
                    }
607

608
                    $symbol = substr($symbol, 0, strrpos($symbol, '\\'));
5✔
609
                }
610
            }
611

612
            foreach ($uses[$kind] ?? [] as $useLongName => $useShortName) {
39✔
613
                $discoveredLongName = $discoveredFqcnByShortNameLower[$kind][strtolower($useShortName)] ?? null;
27✔
614
                if (strtolower($discoveredLongName) === strtolower($useLongName)) {
27✔
615
                    unset($discoveredFqcnByShortNameLower[$kind][strtolower($useShortName)]);
27✔
616
                }
617
            }
618

619
            foreach ($discoveredFqcnByShortNameLower[$kind] ?? [] as $fqcn) {
39✔
620
                $shortenedName = ltrim($this->shortenSymbol($fqcn, $kind, [], $namespaceName), '\\');
38✔
621
                if (str_contains($shortenedName, '\\')) { // prevent importing non-namespaced names in global namespace
38✔
622
                    $shortEndName = str_contains($fqcn, '\\') ? substr($fqcn, (int) strrpos($fqcn, '\\') + 1) : $fqcn;
25✔
623
                    \assert('' !== $shortEndName);
25✔
624
                    $uses[$kind][$fqcn] = $shortEndName;
25✔
625
                    $this->symbolsForImport[$kind][$shortEndName] = $fqcn;
25✔
626
                }
627
            }
628

629
            if (isset($this->symbolsForImport[$kind])) {
39✔
630
                ksort($this->symbolsForImport[$kind], \SORT_NATURAL);
25✔
631
            }
632
        }
633
    }
634

635
    /**
636
     * @param _Uses $uses
637
     */
638
    private function fixFunction(FunctionsAnalyzer $functionsAnalyzer, Tokens $tokens, int $index, array $uses, string $namespaceName): void
639
    {
640
        $arguments = $functionsAnalyzer->getFunctionArguments($tokens, $index);
91✔
641

642
        foreach ($arguments as $i => $argument) {
91✔
643
            $argument = $functionsAnalyzer->getFunctionArguments($tokens, $index)[$i];
79✔
644

645
            if ($argument->hasTypeAnalysis()) {
79✔
646
                $this->replaceByShortType($tokens, $argument->getTypeAnalysis(), $uses, $namespaceName);
66✔
647
            }
648
        }
649

650
        $returnTypeAnalysis = $functionsAnalyzer->getFunctionReturnType($tokens, $index);
91✔
651

652
        if (null !== $returnTypeAnalysis) {
91✔
653
            $this->replaceByShortType($tokens, $returnTypeAnalysis, $uses, $namespaceName);
32✔
654
        }
655
    }
656

657
    /**
658
     * @param _Uses $uses
659
     */
660
    private function fixPhpDoc(Tokens $tokens, int $index, array $uses, string $namespaceName): void
661
    {
662
        $allowedTags = $this->configuration['phpdoc_tags'];
35✔
663

664
        if ([] === $allowedTags) {
35✔
665
            return;
1✔
666
        }
667

668
        $phpDoc = $tokens[$index];
34✔
669
        $phpDocContent = $phpDoc->getContent();
34✔
670
        $phpDocContentNew = Preg::replaceCallback('/([*{]\h*@)(\S+)(\h+)('.TypeExpression::REGEX_TYPES.')(?!(?!\})\S)/', function ($matches) use ($allowedTags, $uses, $namespaceName) {
34✔
671
            if (!\in_array($matches[2], $allowedTags, true)) {
28✔
672
                return $matches[0];
7✔
673
            }
674

675
            return $matches[1].$matches[2].$matches[3].$this->fixPhpDocType($matches[4], $uses, $namespaceName);
27✔
676
        }, $phpDocContent);
34✔
677

678
        if (\in_array('see', $allowedTags, true)) {
34✔
679
            $phpDocContentNew = Preg::replaceCallback(
32✔
680
                '/([*{]\h*)@see(\h+)('.self::REGEX_CLASS.')(::(?:\$\w+|\w+\(\)))(?!(?!\})\S)/',
32✔
681
                fn ($matches) => $matches[1].'@see'.$matches[2].$this->fixPhpDocType($matches[3], $uses, $namespaceName).$matches[5],
32✔
682
                $phpDocContentNew,
32✔
683
            );
32✔
684
        }
685

686
        if ($phpDocContentNew !== $phpDocContent) {
34✔
687
            $tokens[$index] = new Token([\T_DOC_COMMENT, $phpDocContentNew]);
21✔
688
        }
689
    }
690

691
    /**
692
     * @param _Uses $uses
693
     */
694
    private function fixPhpDocType(string $type, array $uses, string $namespaceName): string
695
    {
696
        $typeExpression = new TypeExpression($type, null, []);
27✔
697

698
        $typeExpression = $typeExpression->mapTypes(function (TypeExpression $type) use ($uses, $namespaceName) {
27✔
699
            $currentTypeValue = $type->toString();
27✔
700

701
            if ($type->isCompositeType() || !Preg::match('/^'.self::REGEX_CLASS.'$/', $currentTypeValue) || \in_array($currentTypeValue, ['min', 'max'], true)) {
27✔
702
                return $type;
12✔
703
            }
704

705
            /** @var non-empty-string $currentTypeValue */
706
            $shortTokens = $this->determineShortType($currentTypeValue, 'class', $uses, $namespaceName);
27✔
707

708
            if (null === $shortTokens) {
27✔
709
                return $type;
27✔
710
            }
711

712
            $newTypeValue = implode('', array_map(
21✔
713
                static fn (Token $token) => $token->getContent(),
21✔
714
                $shortTokens,
21✔
715
            ));
21✔
716

717
            return $currentTypeValue === $newTypeValue
21✔
718
                ? $type
×
719
                : new TypeExpression($newTypeValue, null, []);
21✔
720
        });
27✔
721

722
        return $typeExpression->toString();
27✔
723
    }
724

725
    /**
726
     * @param _Uses $uses
727
     */
728
    private function fixExtendsImplements(Tokens $tokens, int $index, array $uses, string $namespaceName): void
729
    {
730
        // We handle `extends` and `implements` with similar logic, but we need to exit the loop under different conditions.
731
        $isExtends = $tokens[$index]->isGivenKind(\T_EXTENDS);
18✔
732
        $index = $tokens->getNextMeaningfulToken($index);
18✔
733

734
        $typeStartIndex = null;
18✔
735
        $typeEndIndex = null;
18✔
736

737
        while (true) {
18✔
738
            if ($tokens[$index]->equalsAny([',', '{', [\T_IMPLEMENTS]])) {
18✔
739
                if (null !== $typeStartIndex) {
18✔
740
                    $index += $this->shortenClassIfPossible($tokens, $typeStartIndex, $typeEndIndex, $uses, $namespaceName);
18✔
741
                }
742
                $typeStartIndex = null;
18✔
743

744
                if ($tokens[$index]->equalsAny($isExtends ? [[\T_IMPLEMENTS], '{'] : ['{'])) {
18✔
745
                    break;
18✔
746
                }
747
            } else {
748
                if (null === $typeStartIndex) {
18✔
749
                    $typeStartIndex = $index;
18✔
750
                }
751
                $typeEndIndex = $index;
18✔
752
            }
753

754
            $index = $tokens->getNextMeaningfulToken($index);
18✔
755
        }
756
    }
757

758
    /**
759
     * @param _Uses $uses
760
     */
761
    private function fixCatch(Tokens $tokens, int $index, array $uses, string $namespaceName): void
762
    {
763
        $index = $tokens->getNextMeaningfulToken($index); // '('
8✔
764
        $index = $tokens->getNextMeaningfulToken($index); // first part of first exception class to be caught
8✔
765

766
        $typeStartIndex = null;
8✔
767
        $typeEndIndex = null;
8✔
768

769
        while (true) {
8✔
770
            if ($tokens[$index]->equalsAny([')', [\T_VARIABLE], [CT::T_TYPE_ALTERNATION]])) {
8✔
771
                if (null === $typeStartIndex) {
8✔
772
                    break;
8✔
773
                }
774

775
                $index += $this->shortenClassIfPossible($tokens, $typeStartIndex, $typeEndIndex, $uses, $namespaceName);
8✔
776
                $typeStartIndex = null;
8✔
777

778
                if ($tokens[$index]->equals(')')) {
8✔
779
                    break;
1✔
780
                }
781
            } else {
782
                if (null === $typeStartIndex) {
8✔
783
                    $typeStartIndex = $index;
8✔
784
                }
785
                $typeEndIndex = $index;
8✔
786
            }
787

788
            $index = $tokens->getNextMeaningfulToken($index);
8✔
789
        }
790
    }
791

792
    /**
793
     * @param _Uses $uses
794
     */
795
    private function fixAttribute(Tokens $tokens, int $index, array $uses, string $namespaceName): void
796
    {
797
        $attributeAnalysis = AttributeAnalyzer::collectOne($tokens, $index);
2✔
798

799
        foreach ($attributeAnalysis->getAttributes() as $attribute) {
2✔
800
            $index = $attribute['start'];
2✔
801
            while ($tokens[$index]->isGivenKind([\T_STRING, \T_NS_SEPARATOR])) {
2✔
802
                $index = $tokens->getPrevMeaningfulToken($index);
2✔
803
            }
804
            $this->fixNextName($tokens, $index, $uses, $namespaceName);
2✔
805
        }
806
    }
807

808
    /**
809
     * @param _Uses $uses
810
     */
811
    private function fixPrevName(Tokens $tokens, int $index, array $uses, string $namespaceName): void
812
    {
813
        $typeStartIndex = null;
84✔
814
        $typeEndIndex = null;
84✔
815

816
        while (true) {
84✔
817
            $index = $tokens->getPrevMeaningfulToken($index);
84✔
818
            if ($tokens[$index]->isObjectOperator()) {
84✔
819
                break;
2✔
820
            }
821

822
            if ($tokens[$index]->isGivenKind([\T_STRING, \T_NS_SEPARATOR])) {
84✔
823
                $typeStartIndex = $index;
84✔
824
                if (null === $typeEndIndex) {
84✔
825
                    $typeEndIndex = $index;
84✔
826
                }
827
            } else {
828
                if (null !== $typeEndIndex) {
82✔
829
                    $this->shortenClassIfPossible($tokens, $typeStartIndex, $typeEndIndex, $uses, $namespaceName);
82✔
830
                }
831

832
                break;
82✔
833
            }
834
        }
835
    }
836

837
    /**
838
     * @param _Uses $uses
839
     */
840
    private function fixNextName(Tokens $tokens, int $index, array $uses, string $namespaceName): void
841
    {
842
        $typeStartIndex = null;
74✔
843
        $typeEndIndex = null;
74✔
844

845
        while (true) {
74✔
846
            $index = $tokens->getNextMeaningfulToken($index);
74✔
847

848
            if ($tokens[$index]->isGivenKind([\T_STRING, \T_NS_SEPARATOR])) {
74✔
849
                if (null === $typeStartIndex) {
69✔
850
                    $typeStartIndex = $index;
69✔
851
                }
852
                $typeEndIndex = $index;
69✔
853
            } else {
854
                if (null !== $typeStartIndex) {
74✔
855
                    $this->shortenClassIfPossible($tokens, $typeStartIndex, $typeEndIndex, $uses, $namespaceName);
69✔
856
                }
857

858
                break;
74✔
859
            }
860
        }
861
    }
862

863
    /**
864
     * @param _Uses $uses
865
     */
866
    private function shortenClassIfPossible(Tokens $tokens, int $typeStartIndex, int $typeEndIndex, array $uses, string $namespaceName): int
867
    {
868
        /** @var non-empty-string $content */
869
        $content = $tokens->generatePartialCode($typeStartIndex, $typeEndIndex);
130✔
870
        $newTokens = $this->determineShortType($content, 'class', $uses, $namespaceName);
130✔
871
        if (null === $newTokens) {
130✔
872
            return 0;
130✔
873
        }
874

875
        $tokens->overrideRange($typeStartIndex, $typeEndIndex, $newTokens);
45✔
876

877
        return \count($newTokens) - ($typeEndIndex - $typeStartIndex) - 1;
45✔
878
    }
879

880
    /**
881
     * @param _Uses $uses
882
     */
883
    private function replaceByShortType(Tokens $tokens, TypeAnalysis $type, array $uses, string $namespaceName): void
884
    {
885
        $typeStartIndex = $type->getStartIndex();
74✔
886

887
        if ($tokens[$typeStartIndex]->isGivenKind(CT::T_NULLABLE_TYPE)) {
74✔
888
            $typeStartIndex = $tokens->getNextMeaningfulToken($typeStartIndex);
2✔
889
        }
890

891
        $types = $this->getTypes($tokens, $typeStartIndex, $type->getEndIndex());
74✔
892

893
        foreach ($types as [$startIndex, $endIndex]) {
74✔
894
            /** @var non-empty-string $content */
895
            $content = $tokens->generatePartialCode($startIndex, $endIndex);
74✔
896
            $newTokens = $this->determineShortType($content, 'class', $uses, $namespaceName);
74✔
897
            if (null !== $newTokens) {
74✔
898
                $tokens->overrideRange($startIndex, $endIndex, $newTokens);
46✔
899
            }
900
        }
901
    }
902

903
    /**
904
     * Determines short type based on FQCN, current namespace and imports (`use` declarations).
905
     *
906
     * @param non-empty-string $typeName
907
     * @param _ImportType      $importKind
908
     * @param _Uses            $uses
909
     *
910
     * @return null|non-empty-list<Token>
911
     */
912
    private function determineShortType(string $typeName, string $importKind, array $uses, string $namespaceName): ?array
913
    {
914
        if (null !== $this->discoveredSymbols) {
149✔
915
            if (!$this->isReservedIdentifier($typeName)) {
39✔
916
                $this->discoveredSymbols[$importKind][] = $typeName;
39✔
917
            }
918

919
            return null;
39✔
920
        }
921

922
        $fqcn = $this->resolveSymbol($typeName, $importKind, $uses, $namespaceName);
149✔
923
        $shortenedType = $this->shortenSymbol($fqcn, $importKind, $uses, $namespaceName);
149✔
924
        if ($shortenedType === $typeName) {
149✔
925
            return null;
149✔
926
        }
927

928
        return $this->namespacedStringToTokens($shortenedType);
104✔
929
    }
930

931
    /**
932
     * @return iterable<array{int, int}>
933
     */
934
    private function getTypes(Tokens $tokens, int $index, int $endIndex): iterable
935
    {
936
        $skipNextYield = false;
74✔
937
        $typeStartIndex = $typeEndIndex = null;
74✔
938
        while (true) {
74✔
939
            if ($tokens[$index]->isGivenKind(CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN)) {
74✔
940
                $index = $tokens->getNextMeaningfulToken($index);
1✔
941
                $typeStartIndex = $typeEndIndex = null;
1✔
942

943
                continue;
1✔
944
            }
945

946
            if (
947
                $tokens[$index]->isGivenKind([CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE])
74✔
948
                || $index > $endIndex
74✔
949
            ) {
950
                if (!$skipNextYield && null !== $typeStartIndex) {
74✔
951
                    $origCount = \count($tokens);
74✔
952

953
                    yield [$typeStartIndex, $typeEndIndex];
74✔
954

955
                    $endIndex += \count($tokens) - $origCount;
74✔
956

957
                    // type tokens were possibly updated, restart type match
958
                    $skipNextYield = true;
74✔
959
                    $index = $typeEndIndex = $typeStartIndex;
74✔
960
                } else {
961
                    $skipNextYield = false;
74✔
962
                    $index = $tokens->getNextMeaningfulToken($index);
74✔
963
                    $typeStartIndex = $typeEndIndex = null;
74✔
964
                }
965

966
                if ($index > $endIndex) {
74✔
967
                    break;
74✔
968
                }
969

970
                continue;
74✔
971
            }
972

973
            if (null === $typeStartIndex) {
74✔
974
                $typeStartIndex = $index;
74✔
975
            }
976
            $typeEndIndex = $index;
74✔
977

978
            $index = $tokens->getNextMeaningfulToken($index);
74✔
979
        }
980
    }
981

982
    /**
983
     * @return non-empty-list<Token>
984
     */
985
    private function namespacedStringToTokens(string $input): array
986
    {
987
        $tokens = [];
104✔
988

989
        if (str_starts_with($input, '\\')) {
104✔
990
            $tokens[] = new Token([\T_NS_SEPARATOR, '\\']);
11✔
991
            $input = substr($input, 1);
11✔
992
        }
993

994
        $parts = explode('\\', $input);
104✔
995
        foreach ($parts as $index => $part) {
104✔
996
            $tokens[] = new Token([\T_STRING, $part]);
104✔
997

998
            if ($index !== \count($parts) - 1) {
104✔
999
                $tokens[] = new Token([\T_NS_SEPARATOR, '\\']);
22✔
1000
            }
1001
        }
1002

1003
        return $tokens;
104✔
1004
    }
1005

1006
    private function normalizeFqcn(string $input): string
1007
    {
1008
        $backslashPosition = strrpos($input, '\\');
134✔
1009
        if (false === $backslashPosition) {
134✔
1010
            return strtolower($input);
55✔
1011
        }
1012

1013
        $namespacePartEndPosition = $backslashPosition + 1;
116✔
1014
        $mainPart = substr($input, 0, $namespacePartEndPosition);
116✔
1015
        $lastPart = substr($input, $namespacePartEndPosition);
116✔
1016

1017
        return $mainPart.strtolower($lastPart);
116✔
1018
    }
1019

1020
    /**
1021
     * We need to create import processor dynamically (not in constructor), because actual whitespace configuration
1022
     * is set later, not when fixer's instance is created.
1023
     */
1024
    private function createImportProcessor(): ImportProcessor
1025
    {
1026
        return new ImportProcessor($this->whitespacesConfig);
25✔
1027
    }
1028
}
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