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

keradus / PHP-CS-Fixer / 17855924389

19 Sep 2025 10:31AM UTC coverage: 94.55%. Remained the same
17855924389

push

github

web-flow
deps: bump phpstan/phpstan from 2.1.25 to 2.1.28 in /dev-tools in the phpstan group (#9072)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

28418 of 30056 relevant lines covered (94.55%)

45.5 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<non-empty-string, non-empty-string>,
52
 *   class?: array<non-empty-string, non-empty-string>,
53
 *   function?: array<non-empty-string, non-empty-string>
54
 * }
55
 *
56
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
57
 *
58
 * @author VeeWee <toonverwerft@gmail.com>
59
 * @author Tomas Jadrny <developer@tomasjadrny.cz>
60
 * @author Greg Korba <greg@codito.dev>
61
 * @author SpacePossum <possumfromspace@gmail.com>
62
 * @author Michael Vorisek <https://github.com/mvorisek>
63
 *
64
 * @phpstan-import-type _ImportType from \PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis
65
 *
66
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
67
 */
68
final class FullyQualifiedStrictTypesFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
69
{
70
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
71
    use ConfigurableFixerTrait;
72

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

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

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

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

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

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

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

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

130
                        use Foo\Bar;
131
                        use Foo\Bar\Baz;
132
                        use Foo\OtherClass;
133
                        use Foo\SomeContract;
134
                        use Foo\SomeException;
135

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

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

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

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

167
                        PHP
3✔
168
                ),
3✔
169
                new CodeSample(
3✔
170
                    <<<'PHP'
3✔
171
                        <?php
172

173
                        class SomeClass
174
                        {
175
                            public function doY(Foo\NotImported $u, \Foo\NotImported $v)
176
                            {
177
                            }
178
                        }
179

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

199
                        PHP,
3✔
200
                    ['leading_backslash_in_global_namespace' => true]
3✔
201
                ),
3✔
202
                new CodeSample(
3✔
203
                    <<<'PHP'
3✔
204
                        <?php
205

206
                        namespace Foo\Test;
207

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

221
                        PHP,
3✔
222
                    ['import_symbols' => true]
3✔
223
                ),
3✔
224
            ]
3✔
225
        );
3✔
226
    }
227

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

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

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

304
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
305
    {
306
        $namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();
149✔
307
        $functionsAnalyzer = new FunctionsAnalyzer();
149✔
308

309
        $this->symbolsForImport = [];
149✔
310

311
        foreach ($tokens->getNamespaceDeclarations() as $namespaceIndex => $namespace) {
149✔
312
            $namespace = $tokens->getNamespaceDeclarations()[$namespaceIndex];
149✔
313

314
            $namespaceName = $namespace->getFullName();
149✔
315

316
            $uses = [];
149✔
317
            $lastUse = null;
149✔
318

319
            foreach ($namespaceUsesAnalyzer->getDeclarationsInNamespace($tokens, $namespace, true) as $use) {
149✔
320
                if (!$use->isClass()) {
95✔
321
                    continue;
6✔
322
                }
323

324
                $fullName = ltrim($use->getFullName(), '\\');
94✔
325
                \assert('' !== $fullName);
94✔
326
                $uses[$use->getHumanFriendlyType()][$fullName] = $use->getShortName();
94✔
327
                $lastUse = $use;
94✔
328
            }
329

330
            $indexDiff = 0;
149✔
331
            foreach (true === $this->configuration['import_symbols'] ? [true, false] : [false] as $discoverSymbolsPhase) {
149✔
332
                $this->discoveredSymbols = $discoverSymbolsPhase ? [] : null;
149✔
333

334
                $openedCurlyBrackets = 0;
149✔
335
                $this->reservedIdentifiersByLevel = [];
149✔
336

337
                for ($index = $namespace->getScopeStartIndex(); $index < $namespace->getScopeEndIndex() + $indexDiff; ++$index) {
149✔
338
                    $origSize = \count($tokens);
149✔
339
                    $token = $tokens[$index];
149✔
340

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

375
                        $this->fixPhpDoc($tokens, $index, $uses, $namespaceName);
35✔
376
                    }
377

378
                    $indexDiff += \count($tokens) - $origSize;
149✔
379
                }
380

381
                $this->reservedIdentifiersByLevel = [];
149✔
382

383
                if ($discoverSymbolsPhase) {
149✔
384
                    $this->setupUsesFromDiscoveredSymbols($uses, $namespaceName);
36✔
385
                }
386
            }
387

388
            if ([] !== $this->symbolsForImport) {
149✔
389
                if (null !== $lastUse) {
22✔
390
                    $atIndex = $lastUse->getEndIndex() + 1;
4✔
391
                } elseif (0 !== $namespace->getEndIndex()) {
18✔
392
                    $atIndex = $namespace->getEndIndex() + 1;
13✔
393
                } else {
394
                    $firstTokenIndex = $tokens->getNextMeaningfulToken($namespace->getScopeStartIndex());
5✔
395
                    if (null !== $firstTokenIndex && $tokens[$firstTokenIndex]->isGivenKind(\T_DECLARE)) {
5✔
396
                        $atIndex = $tokens->getNextTokenOfKind($firstTokenIndex, [';']) + 1;
1✔
397
                    } else {
398
                        $atIndex = $namespace->getScopeStartIndex() + 1;
4✔
399
                    }
400
                }
401

402
                // Insert all registered FQCNs
403
                $this->createImportProcessor()->insertImports($tokens, $this->symbolsForImport, $atIndex);
22✔
404

405
                $this->symbolsForImport = [];
22✔
406
            }
407
        }
408
    }
409

410
    /**
411
     * @param _Uses $uses
412
     */
413
    private function refreshUsesCache(array $uses): void
414
    {
415
        if ($this->cacheUsesLast === $uses) {
140✔
416
            return;
138✔
417
        }
418

419
        $this->cacheUsesLast = $uses;
90✔
420

421
        $this->cacheUseNameByShortNameLower = [];
90✔
422
        $this->cacheUseShortNameByNameLower = [];
90✔
423

424
        foreach ($uses as $kind => $kindUses) {
90✔
425
            foreach ($kindUses as $useLongName => $useShortName) {
90✔
426
                $this->cacheUseNameByShortNameLower[$kind][strtolower($useShortName)] = $useLongName;
90✔
427
                $this->cacheUseShortNameByNameLower[$kind][strtolower($useLongName)] = $useShortName;
90✔
428
            }
429
        }
430
    }
431

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

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

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

448
        return false;
130✔
449
    }
450

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

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

469
        $this->refreshUsesCache($uses);
127✔
470

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

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

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

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

494
        $res = null;
140✔
495

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

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

514
                    break;
81✔
515
                }
516
            }
517

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

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

534
        return $res;
140✔
535
    }
536

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

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

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

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

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

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

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

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

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

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

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

613
            if (isset($this->symbolsForImport[$kind])) {
36✔
614
                ksort($this->symbolsForImport[$kind], \SORT_NATURAL);
22✔
615
            }
616
        }
617
    }
618

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

626
        foreach ($arguments as $i => $argument) {
87✔
627
            $argument = $functionsAnalyzer->getFunctionArguments($tokens, $index)[$i];
79✔
628

629
            if ($argument->hasTypeAnalysis()) {
79✔
630
                $this->replaceByShortType($tokens, $argument->getTypeAnalysis(), $uses, $namespaceName);
66✔
631
            }
632
        }
633

634
        $returnTypeAnalysis = $functionsAnalyzer->getFunctionReturnType($tokens, $index);
87✔
635

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

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

648
        if ([] === $allowedTags) {
35✔
649
            return;
1✔
650
        }
651

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

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

662
        if ($phpDocContentNew !== $phpDocContent) {
34✔
663
            $tokens[$index] = new Token([\T_DOC_COMMENT, $phpDocContentNew]);
21✔
664
        }
665
    }
666

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

674
        $typeExpression = $typeExpression->mapTypes(function (TypeExpression $type) use ($uses, $namespaceName) {
27✔
675
            $currentTypeValue = $type->toString();
27✔
676

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

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

684
            if (null === $shortTokens) {
27✔
685
                return $type;
27✔
686
            }
687

688
            $newTypeValue = implode('', array_map(
21✔
689
                static fn (Token $token) => $token->getContent(),
21✔
690
                $shortTokens
21✔
691
            ));
21✔
692

693
            return $currentTypeValue === $newTypeValue
21✔
694
                ? $type
×
695
                : new TypeExpression($newTypeValue, null, []);
21✔
696
        });
27✔
697

698
        return $typeExpression->toString();
27✔
699
    }
700

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

710
        $typeStartIndex = null;
14✔
711
        $typeEndIndex = null;
14✔
712

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

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

730
            $index = $tokens->getNextMeaningfulToken($index);
14✔
731
        }
732
    }
733

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

742
        $typeStartIndex = null;
8✔
743
        $typeEndIndex = null;
8✔
744

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

751
                $index += $this->shortenClassIfPossible($tokens, $typeStartIndex, $typeEndIndex, $uses, $namespaceName);
8✔
752
                $typeStartIndex = null;
8✔
753

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

764
            $index = $tokens->getNextMeaningfulToken($index);
8✔
765
        }
766
    }
767

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

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

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

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

798
            if ($tokens[$index]->isGivenKind([\T_STRING, \T_NS_SEPARATOR])) {
79✔
799
                $typeStartIndex = $index;
79✔
800
                if (null === $typeEndIndex) {
79✔
801
                    $typeEndIndex = $index;
79✔
802
                }
803
            } else {
804
                if (null !== $typeEndIndex) {
77✔
805
                    $this->shortenClassIfPossible($tokens, $typeStartIndex, $typeEndIndex, $uses, $namespaceName);
77✔
806
                }
807

808
                break;
77✔
809
            }
810
        }
811
    }
812

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

821
        while (true) {
70✔
822
            $index = $tokens->getNextMeaningfulToken($index);
70✔
823

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

834
                break;
70✔
835
            }
836
        }
837
    }
838

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

851
        $tokens->overrideRange($typeStartIndex, $typeEndIndex, $newTokens);
42✔
852

853
        return \count($newTokens) - ($typeEndIndex - $typeStartIndex) - 1;
42✔
854
    }
855

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

863
        if ($tokens[$typeStartIndex]->isGivenKind(CT::T_NULLABLE_TYPE)) {
74✔
864
            $typeStartIndex = $tokens->getNextMeaningfulToken($typeStartIndex);
2✔
865
        }
866

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

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

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

895
            return null;
36✔
896
        }
897

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

904
        return $this->namespacedStringToTokens($shortenedType);
101✔
905
    }
906

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

919
                continue;
1✔
920
            }
921

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

929
                    yield [$typeStartIndex, $typeEndIndex];
74✔
930

931
                    $endIndex += \count($tokens) - $origCount;
74✔
932

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

942
                if ($index > $endIndex) {
74✔
943
                    break;
74✔
944
                }
945

946
                continue;
74✔
947
            }
948

949
            if (null === $typeStartIndex) {
74✔
950
                $typeStartIndex = $index;
74✔
951
            }
952
            $typeEndIndex = $index;
74✔
953

954
            $index = $tokens->getNextMeaningfulToken($index);
74✔
955
        }
956
    }
957

958
    /**
959
     * @return non-empty-list<Token>
960
     */
961
    private function namespacedStringToTokens(string $input): array
962
    {
963
        $tokens = [];
101✔
964

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

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

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

979
        return $tokens;
101✔
980
    }
981

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