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

keradus / PHP-CS-Fixer / 15295226534

28 May 2025 08:23AM UTC coverage: 94.849% (-0.01%) from 94.859%
15295226534

push

github

keradus
DX: introduce `FCT` class for tokens not present in the lowest supported PHP version (#8706)

Co-authored-by: Dariusz Rumiński <dariusz.ruminski@gmail.com>

186 of 192 new or added lines in 52 files covered. (96.88%)

307 existing lines in 29 files now uncovered.

28099 of 29625 relevant lines covered (94.85%)

45.33 hits per line

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

99.26
/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
 * @author VeeWee <toonverwerft@gmail.com>
41
 * @author Tomas Jadrny <developer@tomasjadrny.cz>
42
 * @author Greg Korba <greg@codito.dev>
43
 * @author SpacePossum <possumfromspace@gmail.com>
44
 * @author Michael Vorisek <https://github.com/mvorisek>
45
 *
46
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
47
 *
48
 * @phpstan-type _AutogeneratedInputConfiguration array{
49
 *  import_symbols?: bool,
50
 *  leading_backslash_in_global_namespace?: bool,
51
 *  phpdoc_tags?: list<string>,
52
 * }
53
 * @phpstan-type _AutogeneratedComputedConfiguration array{
54
 *  import_symbols: bool,
55
 *  leading_backslash_in_global_namespace: bool,
56
 *  phpdoc_tags: list<string>,
57
 * }
58
 * @phpstan-type _Uses array{
59
 *   constant?: array<class-string, string>,
60
 *   class?: array<class-string, string>,
61
 *   function?: array<class-string, string>
62
 * }
63
 *
64
 * @phpstan-import-type _ImportType from \PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis
65
 */
66
final class FullyQualifiedStrictTypesFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
67
{
68
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
69
    use ConfigurableFixerTrait;
70

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

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

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

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

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

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

116
    /** @var _Uses */
117
    private array $cacheUseShortNameByNameLower;
118

119
    public function getDefinition(): FixerDefinitionInterface
120
    {
121
        return new FixerDefinition(
3✔
122
            'Removes the leading part of fully qualified symbol references if a given symbol is imported or belongs to the current namespace.',
3✔
123
            [
3✔
124
                new CodeSample(
3✔
125
                    '<?php
3✔
126

127
use Foo\Bar;
128
use Foo\Bar\Baz;
129
use Foo\OtherClass;
130
use Foo\SomeContract;
131
use Foo\SomeException;
132

133
/**
134
 * @see \Foo\Bar\Baz
135
 */
136
class SomeClass extends \Foo\OtherClass implements \Foo\SomeContract
137
{
138
    /**
139
     * @var \Foo\Bar\Baz
140
     */
141
    public $baz;
142

143
    /**
144
     * @param \Foo\Bar\Baz $baz
145
     */
146
    public function __construct($baz) {
147
        $this->baz = $baz;
148
    }
149

150
    /**
151
     * @return \Foo\Bar\Baz
152
     */
153
    public function getBaz() {
154
        return $this->baz;
155
    }
156

157
    public function doX(\Foo\Bar $foo, \Exception $e): \Foo\Bar\Baz
158
    {
159
        try {}
160
        catch (\Foo\SomeException $e) {}
161
    }
162
}
163
'
3✔
164
                ),
3✔
165
                new CodeSample(
3✔
166
                    '<?php
3✔
167

168
class SomeClass
169
{
170
    public function doY(Foo\NotImported $u, \Foo\NotImported $v)
171
    {
172
    }
173
}
174
',
3✔
175
                    ['leading_backslash_in_global_namespace' => true]
3✔
176
                ),
3✔
177
                new CodeSample(
3✔
178
                    '<?php
3✔
179
namespace {
180
    use Foo\A;
181
    try {
182
        foo();
183
    } catch (\Exception|\Foo\A $e) {
184
    }
185
}
186
namespace Foo\Bar {
187
    class SomeClass implements \Foo\Bar\Baz
188
    {
189
    }
190
}
191
',
3✔
192
                    ['leading_backslash_in_global_namespace' => true]
3✔
193
                ),
3✔
194
                new CodeSample(
3✔
195
                    '<?php
3✔
196

197
namespace Foo\Test;
198

199
class Foo extends \Other\BaseClass implements \Other\Interface1, \Other\Interface2
200
{
201
    /** @var \Other\PropertyPhpDoc */
202
    private $array;
203
    public function __construct(\Other\FunctionArgument $arg) {}
204
    public function foo(): \Other\FunctionReturnType
205
    {
206
        try {
207
            \Other\StaticFunctionCall::bar();
208
        } catch (\Other\CaughtThrowable $e) {}
209
    }
210
}
211
',
3✔
212
                    ['import_symbols' => true]
3✔
213
                ),
3✔
214
            ]
3✔
215
        );
3✔
216
    }
217

218
    /**
219
     * {@inheritdoc}
220
     *
221
     * Must run before NoSuperfluousPhpdocTagsFixer, OrderedAttributesFixer, OrderedImportsFixer, OrderedInterfacesFixer, StatementIndentationFixer.
222
     * Must run after ClassKeywordFixer, PhpUnitAttributesFixer, PhpdocToPropertyTypeFixer, PhpdocToReturnTypeFixer.
223
     */
224
    public function getPriority(): int
225
    {
226
        return 7;
1✔
227
    }
228

229
    public function isCandidate(Tokens $tokens): bool
230
    {
231
        return $tokens->isAnyTokenKindsFound([
149✔
232
            CT::T_USE_TRAIT,
149✔
233
            FCT::T_ATTRIBUTE,
149✔
234
            T_CATCH,
149✔
235
            T_DOUBLE_COLON,
149✔
236
            T_DOC_COMMENT,
149✔
237
            T_EXTENDS,
149✔
238
            T_FUNCTION,
149✔
239
            T_IMPLEMENTS,
149✔
240
            T_INSTANCEOF,
149✔
241
            T_NEW,
149✔
242
            T_VARIABLE,
149✔
243
        ]);
149✔
244
    }
245

246
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
247
    {
248
        return new FixerConfigurationResolver([
158✔
249
            (new FixerOptionBuilder(
158✔
250
                'leading_backslash_in_global_namespace',
158✔
251
                'Whether FQCN is prefixed with backslash when that FQCN is used in global namespace context.'
158✔
252
            ))
158✔
253
                ->setAllowedTypes(['bool'])
158✔
254
                ->setDefault(false)
158✔
255
                ->getOption(),
158✔
256
            (new FixerOptionBuilder(
158✔
257
                'import_symbols',
158✔
258
                'Whether FQCNs should be automatically imported.'
158✔
259
            ))
158✔
260
                ->setAllowedTypes(['bool'])
158✔
261
                ->setDefault(false)
158✔
262
                ->getOption(),
158✔
263
            (new FixerOptionBuilder(
158✔
264
                'phpdoc_tags',
158✔
265
                '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).'
158✔
266
            ))
158✔
267
                ->setAllowedTypes(['string[]'])
158✔
268
                ->setDefault([
158✔
269
                    'param',
158✔
270
                    'phpstan-param',
158✔
271
                    'phpstan-property',
158✔
272
                    'phpstan-property-read',
158✔
273
                    'phpstan-property-write',
158✔
274
                    'phpstan-return',
158✔
275
                    'phpstan-var',
158✔
276
                    'property',
158✔
277
                    'property-read',
158✔
278
                    'property-write',
158✔
279
                    'psalm-param',
158✔
280
                    'psalm-property',
158✔
281
                    'psalm-property-read',
158✔
282
                    'psalm-property-write',
158✔
283
                    'psalm-return',
158✔
284
                    'psalm-var',
158✔
285
                    'return',
158✔
286
                    'see',
158✔
287
                    'throws',
158✔
288
                    'var',
158✔
289
                ])
158✔
290
                ->getOption(),
158✔
291
        ]);
158✔
292
    }
293

294
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
295
    {
296
        $namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();
149✔
297
        $functionsAnalyzer = new FunctionsAnalyzer();
149✔
298

299
        $this->symbolsForImport = [];
149✔
300

301
        foreach ($tokens->getNamespaceDeclarations() as $namespaceIndex => $namespace) {
149✔
302
            $namespace = $tokens->getNamespaceDeclarations()[$namespaceIndex];
149✔
303

304
            $namespaceName = $namespace->getFullName();
149✔
305

306
            /** @var _Uses $uses */
307
            $uses = [];
149✔
308
            $lastUse = null;
149✔
309

310
            foreach ($namespaceUsesAnalyzer->getDeclarationsInNamespace($tokens, $namespace, true) as $use) {
149✔
311
                if (!$use->isClass()) {
95✔
312
                    continue;
6✔
313
                }
314

315
                $uses[$use->getHumanFriendlyType()][ltrim($use->getFullName(), '\\')] = $use->getShortName();
94✔
316
                $lastUse = $use;
94✔
317
            }
318

319
            $indexDiff = 0;
149✔
320
            foreach (true === $this->configuration['import_symbols'] ? [true, false] : [false] as $discoverSymbolsPhase) {
149✔
321
                $this->discoveredSymbols = $discoverSymbolsPhase ? [] : null;
149✔
322

323
                $openedCurlyBrackets = 0;
149✔
324
                $this->reservedIdentifiersByLevel = [];
149✔
325

326
                for ($index = $namespace->getScopeStartIndex(); $index < $namespace->getScopeEndIndex() + $indexDiff; ++$index) {
149✔
327
                    $origSize = \count($tokens);
149✔
328

329
                    if ($tokens[$index]->equals('{')) {
149✔
330
                        ++$openedCurlyBrackets;
116✔
331
                    } if ($tokens[$index]->equals('}')) {
149✔
332
                        unset($this->reservedIdentifiersByLevel[$openedCurlyBrackets]);
53✔
333
                        --$openedCurlyBrackets;
53✔
334
                    } elseif ($discoverSymbolsPhase && $tokens[$index]->isGivenKind(self::CLASSY_KINDS)) {
149✔
335
                        $this->fixNextName($tokens, $index, $uses, $namespaceName);
11✔
336
                    } elseif ($tokens[$index]->isGivenKind(T_FUNCTION)) {
149✔
337
                        $this->fixFunction($functionsAnalyzer, $tokens, $index, $uses, $namespaceName);
87✔
338
                    } elseif ($tokens[$index]->isGivenKind([T_EXTENDS, T_IMPLEMENTS])) {
149✔
339
                        $this->fixExtendsImplements($tokens, $index, $uses, $namespaceName);
14✔
340
                    } elseif ($tokens[$index]->isGivenKind(T_CATCH)) {
149✔
341
                        $this->fixCatch($tokens, $index, $uses, $namespaceName);
8✔
342
                    } elseif ($tokens[$index]->isGivenKind(T_DOUBLE_COLON)) {
149✔
343
                        $this->fixPrevName($tokens, $index, $uses, $namespaceName);
11✔
344
                    } elseif ($tokens[$index]->isGivenKind([T_INSTANCEOF, T_NEW, CT::T_USE_TRAIT, CT::T_TYPE_COLON])) {
149✔
345
                        $this->fixNextName($tokens, $index, $uses, $namespaceName);
66✔
346
                    } elseif ($tokens[$index]->isGivenKind(T_VARIABLE)) {
149✔
347
                        $prevIndex = $tokens->getPrevMeaningfulToken($index);
102✔
348
                        if (null !== $prevIndex && $tokens[$prevIndex]->isGivenKind(T_STRING)) {
102✔
349
                            $this->fixPrevName($tokens, $index, $uses, $namespaceName);
72✔
350
                        }
351
                    } elseif ($tokens[$index]->isGivenKind(FCT::T_ATTRIBUTE)) {
149✔
352
                        $this->fixAttribute($tokens, $index, $uses, $namespaceName);
2✔
353
                    } elseif ($discoverSymbolsPhase && $tokens[$index]->isComment() && Preg::match('/#\[\s*('.self::REGEX_CLASS.')/', $tokens[$index]->getContent(), $matches)) { // @TODO: drop when PHP 8.0+ is required
149✔
354
                        /** @var class-string $attributeClass */
UNCOV
355
                        $attributeClass = $matches[1];
×
UNCOV
356
                        $this->determineShortType($attributeClass, 'class', $uses, $namespaceName);
×
357
                    } elseif ($tokens[$index]->isGivenKind(T_DOC_COMMENT)) {
149✔
358
                        Preg::matchAll('/\*\h*@(?:psalm-|phpstan-)?(?:template(?:-covariant|-contravariant)?|(?:import-)?type)\h+('.TypeExpression::REGEX_IDENTIFIER.')(?!\S)/i', $tokens[$index]->getContent(), $matches);
35✔
359
                        foreach ($matches[1] as $reservedIdentifier) {
35✔
360
                            $this->reservedIdentifiersByLevel[$openedCurlyBrackets + 1][$reservedIdentifier] = true;
4✔
361
                        }
362

363
                        $this->fixPhpDoc($tokens, $index, $uses, $namespaceName);
35✔
364
                    }
365

366
                    $indexDiff += \count($tokens) - $origSize;
149✔
367
                }
368

369
                $this->reservedIdentifiersByLevel = [];
149✔
370

371
                if ($discoverSymbolsPhase) {
149✔
372
                    $this->setupUsesFromDiscoveredSymbols($uses, $namespaceName);
36✔
373
                }
374
            }
375

376
            if ([] !== $this->symbolsForImport) {
149✔
377
                if (null !== $lastUse) {
22✔
378
                    $atIndex = $lastUse->getEndIndex() + 1;
4✔
379
                } elseif (0 !== $namespace->getEndIndex()) {
18✔
380
                    $atIndex = $namespace->getEndIndex() + 1;
13✔
381
                } else {
382
                    $firstTokenIndex = $tokens->getNextMeaningfulToken($namespace->getScopeStartIndex());
5✔
383
                    if (null !== $firstTokenIndex && $tokens[$firstTokenIndex]->isGivenKind(T_DECLARE)) {
5✔
384
                        $atIndex = $tokens->getNextTokenOfKind($firstTokenIndex, [';']) + 1;
1✔
385
                    } else {
386
                        $atIndex = $namespace->getScopeStartIndex() + 1;
4✔
387
                    }
388
                }
389

390
                // Insert all registered FQCNs
391
                $this->createImportProcessor()->insertImports($tokens, $this->symbolsForImport, $atIndex);
22✔
392

393
                $this->symbolsForImport = [];
22✔
394
            }
395
        }
396
    }
397

398
    /**
399
     * @param _Uses $uses
400
     */
401
    private function refreshUsesCache(array $uses): void
402
    {
403
        if ($this->cacheUsesLast === $uses) {
140✔
404
            return;
138✔
405
        }
406

407
        $this->cacheUsesLast = $uses;
90✔
408

409
        $this->cacheUseNameByShortNameLower = [];
90✔
410
        $this->cacheUseShortNameByNameLower = [];
90✔
411

412
        foreach ($uses as $kind => $kindUses) {
90✔
413
            foreach ($kindUses as $useLongName => $useShortName) {
90✔
414
                $this->cacheUseNameByShortNameLower[$kind][strtolower($useShortName)] = $useLongName;
90✔
415

416
                /**
417
                 * @var class-string $normalisedUseLongName
418
                 *
419
                 * @phpstan-ignore varTag.nativeType
420
                 */
421
                $normalisedUseLongName = strtolower($useLongName);
90✔
422
                $this->cacheUseShortNameByNameLower[$kind][$normalisedUseLongName] = $useShortName;
90✔
423
            }
424
        }
425
    }
426

427
    private function isReservedIdentifier(string $symbol): bool
428
    {
429
        if (str_contains($symbol, '\\')) { // optimization only
144✔
430
            return false;
120✔
431
        }
432

433
        if ((new TypeAnalysis($symbol))->isReservedType()) {
134✔
434
            return true;
23✔
435
        }
436

437
        foreach ($this->reservedIdentifiersByLevel as $reservedIdentifiers) {
131✔
438
            if (isset($reservedIdentifiers[$symbol])) {
4✔
439
                return true;
4✔
440
            }
441
        }
442

443
        return false;
130✔
444
    }
445

446
    /**
447
     * Resolve absolute or relative symbol to normalized FQCN.
448
     *
449
     * @param _ImportType $importKind
450
     * @param _Uses       $uses
451
     *
452
     * @return class-string
453
     */
454
    private function resolveSymbol(string $symbol, string $importKind, array $uses, string $namespaceName): string
455
    {
456
        if (str_starts_with($symbol, '\\')) {
144✔
457
            return substr($symbol, 1); // @phpstan-ignore return.type
126✔
458
        }
459

460
        if ($this->isReservedIdentifier($symbol)) {
133✔
461
            return $symbol; // @phpstan-ignore return.type
25✔
462
        }
463

464
        $this->refreshUsesCache($uses);
127✔
465

466
        $symbolArr = explode('\\', $symbol, 2);
127✔
467
        $shortStartNameLower = strtolower($symbolArr[0]);
127✔
468
        if (isset($this->cacheUseNameByShortNameLower[$importKind][$shortStartNameLower])) {
127✔
469
            // @phpstan-ignore return.type
470
            return $this->cacheUseNameByShortNameLower[$importKind][$shortStartNameLower].(isset($symbolArr[1]) ? '\\'.$symbolArr[1] : '');
81✔
471
        }
472

473
        return ('' !== $namespaceName ? $namespaceName.'\\' : '').$symbol; // @phpstan-ignore return.type
80✔
474
    }
475

476
    /**
477
     * Shorten normalized FQCN as much as possible.
478
     *
479
     * @param _ImportType $importKind
480
     * @param _Uses       $uses
481
     */
482
    private function shortenSymbol(string $fqcn, string $importKind, array $uses, string $namespaceName): string
483
    {
484
        if ($this->isReservedIdentifier($fqcn)) {
144✔
485
            return $fqcn;
25✔
486
        }
487

488
        $this->refreshUsesCache($uses);
140✔
489

490
        $res = null;
140✔
491

492
        // try to shorten the name using namespace
493
        $iMin = 0;
140✔
494
        if (str_starts_with($fqcn, $namespaceName.'\\')) {
140✔
495
            $tmpRes = substr($fqcn, \strlen($namespaceName) + 1);
58✔
496
            if (!isset($this->cacheUseNameByShortNameLower[$importKind][strtolower(explode('\\', $tmpRes, 2)[0])]) && !$this->isReservedIdentifier($tmpRes)) {
58✔
497
                $res = $tmpRes;
54✔
498
                $iMin = substr_count($namespaceName, '\\') + 1;
54✔
499
            }
500
        }
501

502
        // try to shorten the name using uses
503
        $tmp = $fqcn;
140✔
504
        for ($i = substr_count($fqcn, '\\'); $i >= $iMin; --$i) {
140✔
505
            if (isset($this->cacheUseShortNameByNameLower[$importKind][strtolower($tmp)])) {
140✔
506
                $tmpRes = $this->cacheUseShortNameByNameLower[$importKind][strtolower($tmp)].substr($fqcn, \strlen($tmp));
81✔
507
                if (!$this->isReservedIdentifier($tmpRes)) {
81✔
508
                    $res = $tmpRes;
81✔
509

510
                    break;
81✔
511
                }
512
            }
513

514
            if ($i > 0) {
110✔
515
                $tmp = substr($tmp, 0, strrpos($tmp, '\\'));
84✔
516
            }
517
        }
518

519
        // shortening is not possible, add leading backslash if needed
520
        if (null === $res) {
140✔
521
            $res = $fqcn;
80✔
522
            if ('' !== $namespaceName
80✔
523
                || true === $this->configuration['leading_backslash_in_global_namespace']
80✔
524
                || isset($this->cacheUseNameByShortNameLower[$importKind][strtolower(explode('\\', $res, 2)[0])])
80✔
525
            ) {
526
                $res = '\\'.$res;
58✔
527
            }
528
        }
529

530
        return $res;
140✔
531
    }
532

533
    /**
534
     * @param _Uses $uses
535
     */
536
    private function setupUsesFromDiscoveredSymbols(array &$uses, string $namespaceName): void
537
    {
538
        foreach ($this->discoveredSymbols as $kind => $discoveredSymbols) {
36✔
539
            $discoveredFqcnByShortNameLower = [];
36✔
540

541
            if ('' === $namespaceName) {
36✔
542
                foreach ($discoveredSymbols as $symbol) {
8✔
543
                    if (!str_starts_with($symbol, '\\')) {
8✔
544
                        $shortStartName = explode('\\', ltrim($symbol, '\\'), 2)[0];
8✔
545
                        $shortStartNameLower = strtolower($shortStartName);
8✔
546
                        $discoveredFqcnByShortNameLower[$kind][$shortStartNameLower] = $this->resolveSymbol($shortStartName, $kind, $uses, $namespaceName);
8✔
547
                    }
548
                }
549
            }
550

551
            foreach ($uses[$kind] ?? [] as $useLongName => $useShortName) {
36✔
552
                $discoveredFqcnByShortNameLower[$kind][strtolower($useShortName)] = $useLongName;
24✔
553
            }
554

555
            $useByShortNameLower = [];
36✔
556
            foreach ($uses[$kind] ?? [] as $useShortName) {
36✔
557
                $useByShortNameLower[strtolower($useShortName)] = true;
24✔
558
            }
559

560
            uasort($discoveredSymbols, static function ($a, $b) {
36✔
561
                $res = str_starts_with($a, '\\') <=> str_starts_with($b, '\\');
34✔
562
                if (0 !== $res) {
34✔
563
                    return $res;
24✔
564
                }
565

566
                return substr_count($a, '\\') <=> substr_count($b, '\\');
28✔
567
            });
36✔
568
            foreach ($discoveredSymbols as $symbol) {
36✔
569
                while (true) {
36✔
570
                    $shortEndNameLower = strtolower(str_contains($symbol, '\\') ? substr($symbol, strrpos($symbol, '\\') + 1) : $symbol);
36✔
571
                    if (!isset($discoveredFqcnByShortNameLower[$kind][$shortEndNameLower])) {
36✔
572
                        $shortStartNameLower = strtolower(explode('\\', ltrim($symbol, '\\'), 2)[0]);
34✔
573
                        if (str_starts_with($symbol, '\\') || ('' === $namespaceName && !isset($useByShortNameLower[$shortStartNameLower]))
34✔
574
                            || !str_contains($symbol, '\\')
34✔
575
                        ) {
576
                            $discoveredFqcnByShortNameLower[$kind][$shortEndNameLower] = $this->resolveSymbol($symbol, $kind, $uses, $namespaceName);
34✔
577

578
                            break;
34✔
579
                        }
580
                    }
581
                    // else short name collision - keep unimported
582

583
                    if (str_starts_with($symbol, '\\') || '' === $namespaceName || !str_contains($symbol, '\\')) {
36✔
584
                        break;
36✔
585
                    }
586

587
                    $symbol = substr($symbol, 0, strrpos($symbol, '\\'));
5✔
588
                }
589
            }
590

591
            foreach ($uses[$kind] ?? [] as $useLongName => $useShortName) {
36✔
592
                $discoveredLongName = $discoveredFqcnByShortNameLower[$kind][strtolower($useShortName)] ?? null;
24✔
593
                if (strtolower($discoveredLongName) === strtolower($useLongName)) {
24✔
594
                    unset($discoveredFqcnByShortNameLower[$kind][strtolower($useShortName)]);
24✔
595
                }
596
            }
597

598
            foreach ($discoveredFqcnByShortNameLower[$kind] ?? [] as $fqcn) {
36✔
599
                $shortenedName = ltrim($this->shortenSymbol($fqcn, $kind, [], $namespaceName), '\\');
35✔
600
                if (str_contains($shortenedName, '\\')) { // prevent importing non-namespaced names in global namespace
35✔
601
                    $shortEndName = str_contains($fqcn, '\\') ? substr($fqcn, strrpos($fqcn, '\\') + 1) : $fqcn;
22✔
602
                    $uses[$kind][$fqcn] = $shortEndName;
22✔
603
                    $this->symbolsForImport[$kind][$shortEndName] = $fqcn;
22✔
604
                }
605
            }
606

607
            if (isset($this->symbolsForImport[$kind])) {
36✔
608
                ksort($this->symbolsForImport[$kind], SORT_NATURAL);
22✔
609
            }
610
        }
611
    }
612

613
    /**
614
     * @param _Uses $uses
615
     */
616
    private function fixFunction(FunctionsAnalyzer $functionsAnalyzer, Tokens $tokens, int $index, array $uses, string $namespaceName): void
617
    {
618
        $arguments = $functionsAnalyzer->getFunctionArguments($tokens, $index);
87✔
619

620
        foreach ($arguments as $i => $argument) {
87✔
621
            $argument = $functionsAnalyzer->getFunctionArguments($tokens, $index)[$i];
79✔
622

623
            if ($argument->hasTypeAnalysis()) {
79✔
624
                $this->replaceByShortType($tokens, $argument->getTypeAnalysis(), $uses, $namespaceName);
66✔
625
            }
626
        }
627

628
        $returnTypeAnalysis = $functionsAnalyzer->getFunctionReturnType($tokens, $index);
87✔
629

630
        if (null !== $returnTypeAnalysis) {
87✔
631
            $this->replaceByShortType($tokens, $returnTypeAnalysis, $uses, $namespaceName);
32✔
632
        }
633
    }
634

635
    /**
636
     * @param _Uses $uses
637
     */
638
    private function fixPhpDoc(Tokens $tokens, int $index, array $uses, string $namespaceName): void
639
    {
640
        $allowedTags = $this->configuration['phpdoc_tags'];
35✔
641

642
        if ([] === $allowedTags) {
35✔
643
            return;
1✔
644
        }
645

646
        $phpDoc = $tokens[$index];
34✔
647
        $phpDocContent = $phpDoc->getContent();
34✔
648
        $phpDocContentNew = Preg::replaceCallback('/([*{]\h*@)(\S+)(\h+)('.TypeExpression::REGEX_TYPES.')(?!(?!\})\S)/', function ($matches) use ($allowedTags, $uses, $namespaceName) {
34✔
649
            if (!\in_array($matches[2], $allowedTags, true)) {
28✔
650
                return $matches[0];
7✔
651
            }
652

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

656
        if ($phpDocContentNew !== $phpDocContent) {
34✔
657
            $tokens[$index] = new Token([T_DOC_COMMENT, $phpDocContentNew]);
21✔
658
        }
659
    }
660

661
    /**
662
     * @param _Uses $uses
663
     */
664
    private function fixPhpDocType(string $type, array $uses, string $namespaceName): string
665
    {
666
        $typeExpression = new TypeExpression($type, null, []);
27✔
667

668
        $typeExpression = $typeExpression->mapTypes(function (TypeExpression $type) use ($uses, $namespaceName) {
27✔
669
            $currentTypeValue = $type->toString();
27✔
670

671
            if ($type->isCompositeType() || !Preg::match('/^'.self::REGEX_CLASS.'$/', $currentTypeValue) || \in_array($currentTypeValue, ['min', 'max'], true)) {
27✔
672
                return $type;
11✔
673
            }
674

675
            /** @var class-string $currentTypeValue */
676
            $shortTokens = $this->determineShortType($currentTypeValue, 'class', $uses, $namespaceName);
27✔
677

678
            if (null === $shortTokens) {
27✔
679
                return $type;
27✔
680
            }
681

682
            $newTypeValue = implode('', array_map(
21✔
683
                static fn (Token $token) => $token->getContent(),
21✔
684
                $shortTokens
21✔
685
            ));
21✔
686

687
            return $currentTypeValue === $newTypeValue
21✔
UNCOV
688
                ? $type
×
689
                : new TypeExpression($newTypeValue, null, []);
21✔
690
        });
27✔
691

692
        return $typeExpression->toString();
27✔
693
    }
694

695
    /**
696
     * @param _Uses $uses
697
     */
698
    private function fixExtendsImplements(Tokens $tokens, int $index, array $uses, string $namespaceName): void
699
    {
700
        // We handle `extends` and `implements` with similar logic, but we need to exit the loop under different conditions.
701
        $isExtends = $tokens[$index]->equals([T_EXTENDS]);
14✔
702
        $index = $tokens->getNextMeaningfulToken($index);
14✔
703

704
        $typeStartIndex = null;
14✔
705
        $typeEndIndex = null;
14✔
706

707
        while (true) {
14✔
708
            if ($tokens[$index]->equalsAny([',', '{', [T_IMPLEMENTS]])) {
14✔
709
                if (null !== $typeStartIndex) {
14✔
710
                    $index += $this->shortenClassIfPossible($tokens, $typeStartIndex, $typeEndIndex, $uses, $namespaceName);
14✔
711
                }
712
                $typeStartIndex = null;
14✔
713

714
                if ($tokens[$index]->equalsAny($isExtends ? [[T_IMPLEMENTS], '{'] : ['{'])) {
14✔
715
                    break;
14✔
716
                }
717
            } else {
718
                if (null === $typeStartIndex) {
14✔
719
                    $typeStartIndex = $index;
14✔
720
                }
721
                $typeEndIndex = $index;
14✔
722
            }
723

724
            $index = $tokens->getNextMeaningfulToken($index);
14✔
725
        }
726
    }
727

728
    /**
729
     * @param _Uses $uses
730
     */
731
    private function fixCatch(Tokens $tokens, int $index, array $uses, string $namespaceName): void
732
    {
733
        $index = $tokens->getNextMeaningfulToken($index); // '('
8✔
734
        $index = $tokens->getNextMeaningfulToken($index); // first part of first exception class to be caught
8✔
735

736
        $typeStartIndex = null;
8✔
737
        $typeEndIndex = null;
8✔
738

739
        while (true) {
8✔
740
            if ($tokens[$index]->equalsAny([')', [T_VARIABLE], [CT::T_TYPE_ALTERNATION]])) {
8✔
741
                if (null === $typeStartIndex) {
8✔
742
                    break;
8✔
743
                }
744

745
                $index += $this->shortenClassIfPossible($tokens, $typeStartIndex, $typeEndIndex, $uses, $namespaceName);
8✔
746
                $typeStartIndex = null;
8✔
747

748
                if ($tokens[$index]->equals(')')) {
8✔
749
                    break;
1✔
750
                }
751
            } else {
752
                if (null === $typeStartIndex) {
8✔
753
                    $typeStartIndex = $index;
8✔
754
                }
755
                $typeEndIndex = $index;
8✔
756
            }
757

758
            $index = $tokens->getNextMeaningfulToken($index);
8✔
759
        }
760
    }
761

762
    /**
763
     * @param _Uses $uses
764
     */
765
    private function fixAttribute(Tokens $tokens, int $index, array $uses, string $namespaceName): void
766
    {
767
        $attributeAnalysis = AttributeAnalyzer::collectOne($tokens, $index);
2✔
768

769
        foreach ($attributeAnalysis->getAttributes() as $attribute) {
2✔
770
            $index = $attribute['start'];
2✔
771
            while ($tokens[$index]->equalsAny([[T_STRING], [T_NS_SEPARATOR]])) {
2✔
772
                $index = $tokens->getPrevMeaningfulToken($index);
2✔
773
            }
774
            $this->fixNextName($tokens, $index, $uses, $namespaceName);
2✔
775
        }
776
    }
777

778
    /**
779
     * @param _Uses $uses
780
     */
781
    private function fixPrevName(Tokens $tokens, int $index, array $uses, string $namespaceName): void
782
    {
783
        $typeStartIndex = null;
79✔
784
        $typeEndIndex = null;
79✔
785

786
        while (true) {
79✔
787
            $index = $tokens->getPrevMeaningfulToken($index);
79✔
788
            if ($tokens[$index]->isObjectOperator()) {
79✔
789
                break;
2✔
790
            }
791

792
            if ($tokens[$index]->equalsAny([[T_STRING], [T_NS_SEPARATOR]])) {
79✔
793
                $typeStartIndex = $index;
79✔
794
                if (null === $typeEndIndex) {
79✔
795
                    $typeEndIndex = $index;
79✔
796
                }
797
            } else {
798
                if (null !== $typeEndIndex) {
77✔
799
                    $index += $this->shortenClassIfPossible($tokens, $typeStartIndex, $typeEndIndex, $uses, $namespaceName);
77✔
800
                }
801

802
                break;
77✔
803
            }
804
        }
805
    }
806

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

815
        while (true) {
70✔
816
            $index = $tokens->getNextMeaningfulToken($index);
70✔
817

818
            if ($tokens[$index]->equalsAny([[T_STRING], [T_NS_SEPARATOR]])) {
70✔
819
                if (null === $typeStartIndex) {
65✔
820
                    $typeStartIndex = $index;
65✔
821
                }
822
                $typeEndIndex = $index;
65✔
823
            } else {
824
                if (null !== $typeStartIndex) {
70✔
825
                    $index += $this->shortenClassIfPossible($tokens, $typeStartIndex, $typeEndIndex, $uses, $namespaceName);
65✔
826
                }
827

828
                break;
70✔
829
            }
830
        }
831
    }
832

833
    /**
834
     * @param _Uses $uses
835
     */
836
    private function shortenClassIfPossible(Tokens $tokens, int $typeStartIndex, int $typeEndIndex, array $uses, string $namespaceName): int
837
    {
838
        /** @var class-string $content */
839
        $content = $tokens->generatePartialCode($typeStartIndex, $typeEndIndex);
125✔
840
        $newTokens = $this->determineShortType($content, 'class', $uses, $namespaceName);
125✔
841
        if (null === $newTokens) {
125✔
842
            return 0;
125✔
843
        }
844

845
        $tokens->overrideRange($typeStartIndex, $typeEndIndex, $newTokens);
42✔
846

847
        return \count($newTokens) - ($typeEndIndex - $typeStartIndex) - 1;
42✔
848
    }
849

850
    /**
851
     * @param _Uses $uses
852
     */
853
    private function replaceByShortType(Tokens $tokens, TypeAnalysis $type, array $uses, string $namespaceName): void
854
    {
855
        $typeStartIndex = $type->getStartIndex();
74✔
856

857
        if ($tokens[$typeStartIndex]->isGivenKind(CT::T_NULLABLE_TYPE)) {
74✔
858
            $typeStartIndex = $tokens->getNextMeaningfulToken($typeStartIndex);
2✔
859
        }
860

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

863
        foreach ($types as [$startIndex, $endIndex]) {
74✔
864
            /** @var class-string $content */
865
            $content = $tokens->generatePartialCode($startIndex, $endIndex);
74✔
866
            $newTokens = $this->determineShortType($content, 'class', $uses, $namespaceName);
74✔
867
            if (null !== $newTokens) {
74✔
868
                $tokens->overrideRange($startIndex, $endIndex, $newTokens);
46✔
869
            }
870
        }
871
    }
872

873
    /**
874
     * Determines short type based on FQCN, current namespace and imports (`use` declarations).
875
     *
876
     * @param class-string $typeName
877
     * @param _ImportType  $importKind
878
     * @param _Uses        $uses
879
     *
880
     * @return null|list<Token>
881
     */
882
    private function determineShortType(string $typeName, string $importKind, array $uses, string $namespaceName): ?array
883
    {
884
        if (null !== $this->discoveredSymbols) {
144✔
885
            if (!$this->isReservedIdentifier($typeName)) {
36✔
886
                $this->discoveredSymbols[$importKind][] = $typeName;
36✔
887
            }
888

889
            return null;
36✔
890
        }
891

892
        $fqcn = $this->resolveSymbol($typeName, $importKind, $uses, $namespaceName);
144✔
893
        $shortenedType = $this->shortenSymbol($fqcn, $importKind, $uses, $namespaceName);
144✔
894
        if ($shortenedType === $typeName) {
144✔
895
            return null;
144✔
896
        }
897

898
        return $this->namespacedStringToTokens($shortenedType);
101✔
899
    }
900

901
    /**
902
     * @return iterable<array{int, int}>
903
     */
904
    private function getTypes(Tokens $tokens, int $index, int $endIndex): iterable
905
    {
906
        $skipNextYield = false;
74✔
907
        $typeStartIndex = $typeEndIndex = null;
74✔
908
        while (true) {
74✔
909
            if ($tokens[$index]->isGivenKind(CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN)) {
74✔
910
                $index = $tokens->getNextMeaningfulToken($index);
1✔
911
                $typeStartIndex = $typeEndIndex = null;
1✔
912

913
                continue;
1✔
914
            }
915

916
            if (
917
                $tokens[$index]->isGivenKind([CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE])
74✔
918
                || $index > $endIndex
74✔
919
            ) {
920
                if (!$skipNextYield && null !== $typeStartIndex) {
74✔
921
                    $origCount = \count($tokens);
74✔
922

923
                    yield [$typeStartIndex, $typeEndIndex];
74✔
924

925
                    $endIndex += \count($tokens) - $origCount;
74✔
926

927
                    // type tokens were possibly updated, restart type match
928
                    $skipNextYield = true;
74✔
929
                    $index = $typeEndIndex = $typeStartIndex;
74✔
930
                } else {
931
                    $skipNextYield = false;
74✔
932
                    $index = $tokens->getNextMeaningfulToken($index);
74✔
933
                    $typeStartIndex = $typeEndIndex = null;
74✔
934
                }
935

936
                if ($index > $endIndex) {
74✔
937
                    break;
74✔
938
                }
939

940
                continue;
74✔
941
            }
942

943
            if (null === $typeStartIndex) {
74✔
944
                $typeStartIndex = $index;
74✔
945
            }
946
            $typeEndIndex = $index;
74✔
947

948
            $index = $tokens->getNextMeaningfulToken($index);
74✔
949
        }
950
    }
951

952
    /**
953
     * @return list<Token>
954
     */
955
    private function namespacedStringToTokens(string $input): array
956
    {
957
        $tokens = [];
101✔
958

959
        if (str_starts_with($input, '\\')) {
101✔
960
            $tokens[] = new Token([T_NS_SEPARATOR, '\\']);
11✔
961
            $input = substr($input, 1);
11✔
962
        }
963

964
        $parts = explode('\\', $input);
101✔
965
        foreach ($parts as $index => $part) {
101✔
966
            $tokens[] = new Token([T_STRING, $part]);
101✔
967

968
            if ($index !== \count($parts) - 1) {
101✔
969
                $tokens[] = new Token([T_NS_SEPARATOR, '\\']);
22✔
970
            }
971
        }
972

973
        return $tokens;
101✔
974
    }
975

976
    /**
977
     * We need to create import processor dynamically (not in costructor), because actual whitespace configuration
978
     * is set later, not when fixer's instance is created.
979
     */
980
    private function createImportProcessor(): ImportProcessor
981
    {
982
        return new ImportProcessor($this->whitespacesConfig);
22✔
983
    }
984
}
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