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

keradus / PHP-CS-Fixer / 12733371366

12 Jan 2025 12:12PM UTC coverage: 94.975%. Remained the same
12733371366

push

github

keradus
chore: `FullyQualifiedStrictTypesFixer` - reduce conditions count

1 of 1 new or added line in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

27839 of 29312 relevant lines covered (94.97%)

43.05 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\Processor\ImportProcessor;
35
use PhpCsFixer\Tokenizer\Token;
36
use PhpCsFixer\Tokenizer\Tokens;
37

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

70
    private const REGEX_CLASS = '(?:\\\?+'.TypeExpression::REGEX_IDENTIFIER
71
        .'(\\\\'.TypeExpression::REGEX_IDENTIFIER.')*+)';
72

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

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

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

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

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

114
    /** @var _Uses */
115
    private array $cacheUseShortNameByNameLower;
116

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

125
use Foo\Bar;
126
use Foo\Bar\Baz;
127
use Foo\OtherClass;
128
use Foo\SomeContract;
129
use Foo\SomeException;
130

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

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

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

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

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

195
namespace Foo\Test;
196

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

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

227
    public function isCandidate(Tokens $tokens): bool
228
    {
229
        return $tokens->isAnyTokenKindsFound([
149✔
230
            CT::T_USE_TRAIT,
149✔
231
            ...(\defined('T_ATTRIBUTE') ? [T_ATTRIBUTE] : []), // @TODO: drop condition when PHP 8.0+ is required
149✔
232
            T_CATCH,
149✔
233
            T_DOUBLE_COLON,
149✔
234
            T_DOC_COMMENT,
149✔
235
            T_EXTENDS,
149✔
236
            T_FUNCTION,
149✔
237
            T_IMPLEMENTS,
149✔
238
            T_INSTANCEOF,
149✔
239
            T_NEW,
149✔
240
            T_VARIABLE,
149✔
241
        ]);
149✔
242
    }
243

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

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

297
        $this->symbolsForImport = [];
149✔
298

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

302
            $namespaceName = $namespace->getFullName();
149✔
303

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

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

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

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

321
                $classyKinds = [T_CLASS, T_INTERFACE, T_TRAIT];
149✔
322
                if (\defined('T_ENUM')) { // @TODO: drop condition when PHP 8.1+ is required
149✔
323
                    $classyKinds[] = T_ENUM;
149✔
324
                }
325

326
                $openedCurlyBrackets = 0;
149✔
327
                $this->reservedIdentifiersByLevel = [];
149✔
328

329
                for ($index = $namespace->getScopeStartIndex(); $index < $namespace->getScopeEndIndex() + $indexDiff; ++$index) {
149✔
330
                    $origSize = \count($tokens);
149✔
331

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

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

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

372
                $this->reservedIdentifiersByLevel = [];
149✔
373

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

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

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

396
                $this->symbolsForImport = [];
22✔
397
            }
398
        }
399
    }
400

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

410
        $this->cacheUsesLast = $uses;
90✔
411

412
        $this->cacheUseNameByShortNameLower = [];
90✔
413
        $this->cacheUseShortNameByNameLower = [];
90✔
414

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

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

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

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

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

446
        return false;
130✔
447
    }
448

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

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

467
        $this->refreshUsesCache($uses);
127✔
468

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

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

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

491
        $this->refreshUsesCache($uses);
140✔
492

493
        $res = null;
140✔
494

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

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

513
                    break;
81✔
514
                }
515
            }
516

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

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

533
        return $res;
140✔
534
    }
535

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

631
        $returnTypeAnalysis = $functionsAnalyzer->getFunctionReturnType($tokens, $index);
87✔
632

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

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

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

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

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

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

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

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

674
            if ($type->isCompositeType() || !Preg::match('/^'.self::REGEX_CLASS.'$/', $currentTypeValue)) {
27✔
675
                return $type;
11✔
676
            }
677

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

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

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

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

695
        return $typeExpression->toString();
27✔
696
    }
697

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

707
        $typeStartIndex = null;
14✔
708
        $typeEndIndex = null;
14✔
709

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

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

727
            $index = $tokens->getNextMeaningfulToken($index);
14✔
728
        }
729
    }
730

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

739
        $typeStartIndex = null;
8✔
740
        $typeEndIndex = null;
8✔
741

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

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

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

761
            $index = $tokens->getNextMeaningfulToken($index);
8✔
762
        }
763
    }
764

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

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

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

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

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

805
                break;
77✔
806
            }
807
        }
808
    }
809

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

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

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

831
                break;
70✔
832
            }
833
        }
834
    }
835

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

848
        $tokens->overrideRange($typeStartIndex, $typeEndIndex, $newTokens);
42✔
849

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

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

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

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

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

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

892
            return null;
36✔
893
        }
894

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

901
        return $this->namespacedStringToTokens($shortenedType);
101✔
902
    }
903

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

916
                continue;
1✔
917
            }
918

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

926
                    yield [$typeStartIndex, $typeEndIndex];
74✔
927

928
                    $endIndex += \count($tokens) - $origCount;
74✔
929

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

939
                if ($index > $endIndex) {
74✔
940
                    break;
74✔
941
                }
942

943
                continue;
74✔
944
            }
945

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

951
            $index = $tokens->getNextMeaningfulToken($index);
74✔
952
        }
953
    }
954

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

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

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

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

976
        return $tokens;
101✔
977
    }
978

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