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

keradus / PHP-CS-Fixer / 16271081827

14 Jul 2025 03:29PM UTC coverage: 94.805% (-0.001%) from 94.806%
16271081827

push

github

keradus
move back to integration test, to avoid memory/cpu issues under utest with coverage or mutations

28271 of 29820 relevant lines covered (94.81%)

45.39 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
 * @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<class-string, string>,
52
 *   class?: array<class-string, string>,
53
 *   function?: array<class-string, 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
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
                    $token = $tokens[$index];
149✔
329

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

444
        return false;
130✔
445
    }
446

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

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

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

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

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

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

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

491
        $res = null;
140✔
492

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

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

511
                    break;
81✔
512
                }
513
            }
514

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

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

531
        return $res;
140✔
532
    }
533

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

890
            return null;
36✔
891
        }
892

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

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

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

914
                continue;
1✔
915
            }
916

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

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

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

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

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

941
                continue;
74✔
942
            }
943

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

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

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

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

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

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

974
        return $tokens;
101✔
975
    }
976

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