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

keradus / PHP-CS-Fixer / 17377459942

01 Sep 2025 12:19PM UTC coverage: 94.684% (-0.009%) from 94.693%
17377459942

push

github

web-flow
chore: `Tokens::offsetSet` - explicit validation of input (#9004)

1 of 5 new or added lines in 1 file covered. (20.0%)

306 existing lines in 60 files now uncovered.

28390 of 29984 relevant lines covered (94.68%)

45.5 hits per line

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

96.67
/src/Fixer/Import/GlobalNamespaceImportFixer.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\Annotation;
19
use PhpCsFixer\DocBlock\DocBlock;
20
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
21
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
22
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
23
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
24
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
25
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
26
use PhpCsFixer\FixerDefinition\CodeSample;
27
use PhpCsFixer\FixerDefinition\FixerDefinition;
28
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
29
use PhpCsFixer\Preg;
30
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
31
use PhpCsFixer\Tokenizer\Analyzer\ClassyAnalyzer;
32
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
33
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
34
use PhpCsFixer\Tokenizer\CT;
35
use PhpCsFixer\Tokenizer\Processor\ImportProcessor;
36
use PhpCsFixer\Tokenizer\Token;
37
use PhpCsFixer\Tokenizer\Tokens;
38
use PhpCsFixer\Tokenizer\TokensAnalyzer;
39

40
/**
41
 * @phpstan-type _AutogeneratedInputConfiguration array{
42
 *  import_classes?: bool|null,
43
 *  import_constants?: bool|null,
44
 *  import_functions?: bool|null,
45
 * }
46
 * @phpstan-type _AutogeneratedComputedConfiguration array{
47
 *  import_classes: bool|null,
48
 *  import_constants: bool|null,
49
 *  import_functions: bool|null,
50
 * }
51
 *
52
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
53
 *
54
 * @author Gregor Harlan <gharlan@web.de>
55
 *
56
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
57
 */
58
final class GlobalNamespaceImportFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
59
{
60
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
61
    use ConfigurableFixerTrait;
62

63
    private ImportProcessor $importProcessor;
64

65
    public function __construct()
66
    {
67
        parent::__construct();
79✔
68

69
        $this->importProcessor = new ImportProcessor($this->whitespacesConfig);
79✔
70
    }
71

72
    public function getDefinition(): FixerDefinitionInterface
73
    {
74
        return new FixerDefinition(
3✔
75
            'Imports or fully qualifies global classes/functions/constants.',
3✔
76
            [
3✔
77
                new CodeSample(
3✔
78
                    <<<'PHP'
3✔
79
                        <?php
80

81
                        namespace Foo;
82

83
                        $d = new \DateTimeImmutable();
84

85
                        PHP
3✔
86
                ),
3✔
87
                new CodeSample(
3✔
88
                    <<<'PHP'
3✔
89
                        <?php
90

91
                        namespace Foo;
92

93
                        if (\count($x)) {
94
                            /** @var \DateTimeImmutable $d */
95
                            $d = new \DateTimeImmutable();
96
                            $p = \M_PI;
97
                        }
98

99
                        PHP,
3✔
100
                    ['import_classes' => true, 'import_constants' => true, 'import_functions' => true]
3✔
101
                ),
3✔
102
                new CodeSample(
3✔
103
                    <<<'PHP'
3✔
104
                        <?php
105

106
                        namespace Foo;
107

108
                        use DateTimeImmutable;
109
                        use function count;
110
                        use const M_PI;
111

112
                        if (count($x)) {
113
                            /** @var DateTimeImmutable $d */
114
                            $d = new DateTimeImmutable();
115
                            $p = M_PI;
116
                        }
117

118
                        PHP,
3✔
119
                    ['import_classes' => false, 'import_constants' => false, 'import_functions' => false]
3✔
120
                ),
3✔
121
            ]
3✔
122
        );
3✔
123
    }
124

125
    /**
126
     * {@inheritdoc}
127
     *
128
     * Must run before NoUnusedImportsFixer, OrderedImportsFixer, StatementIndentationFixer.
129
     * Must run after NativeConstantInvocationFixer, NativeFunctionInvocationFixer.
130
     */
131
    public function getPriority(): int
132
    {
133
        return 0;
1✔
134
    }
135

136
    public function isCandidate(Tokens $tokens): bool
137
    {
138
        return $tokens->isAnyTokenKindsFound([\T_DOC_COMMENT, \T_NS_SEPARATOR, \T_USE])
70✔
139
            && $tokens->isTokenKindFound(\T_NAMESPACE)
70✔
140
            && 1 === $tokens->countTokenKind(\T_NAMESPACE)
70✔
141
            && $tokens->isMonolithicPhp();
70✔
142
    }
143

144
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
145
    {
146
        $namespaceAnalyses = $tokens->getNamespaceDeclarations();
59✔
147

148
        if (1 !== \count($namespaceAnalyses) || $namespaceAnalyses[0]->isGlobalNamespace()) {
59✔
149
            return;
1✔
150
        }
151

152
        $useDeclarations = (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens);
58✔
153

154
        $newImports = [];
58✔
155

156
        if (true === $this->configuration['import_constants']) {
58✔
157
            $newImports['const'] = $this->importConstants($tokens, $useDeclarations);
16✔
158
        } elseif (false === $this->configuration['import_constants']) {
43✔
159
            $this->fullyQualifyConstants($tokens, $useDeclarations);
3✔
160
        }
161

162
        if (true === $this->configuration['import_functions']) {
58✔
163
            $newImports['function'] = $this->importFunctions($tokens, $useDeclarations);
16✔
164
        } elseif (false === $this->configuration['import_functions']) {
43✔
165
            $this->fullyQualifyFunctions($tokens, $useDeclarations);
3✔
166
        }
167

168
        if (true === $this->configuration['import_classes']) {
58✔
169
            $newImports['class'] = $this->importClasses($tokens, $useDeclarations);
51✔
170
        } elseif (false === $this->configuration['import_classes']) {
8✔
171
            $this->fullyQualifyClasses($tokens, $useDeclarations);
8✔
172
        }
173

174
        if (\count($newImports) > 0) {
58✔
175
            if (\count($useDeclarations) > 0) {
51✔
176
                $useDeclaration = end($useDeclarations);
39✔
177
                $atIndex = $useDeclaration->getEndIndex() + 1;
39✔
178
            } else {
179
                $namespace = $tokens->getNamespaceDeclarations()[0];
30✔
180
                $atIndex = $namespace->getEndIndex() + 1;
30✔
181
            }
182

183
            $this->importProcessor->insertImports($tokens, $newImports, $atIndex);
51✔
184
        }
185
    }
186

187
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
188
    {
189
        return new FixerConfigurationResolver([
79✔
190
            (new FixerOptionBuilder('import_constants', 'Whether to import, not import or ignore global constants.'))
79✔
191
                ->setDefault(null)
79✔
192
                ->setAllowedTypes(['null', 'bool'])
79✔
193
                ->getOption(),
79✔
194
            (new FixerOptionBuilder('import_functions', 'Whether to import, not import or ignore global functions.'))
79✔
195
                ->setDefault(null)
79✔
196
                ->setAllowedTypes(['null', 'bool'])
79✔
197
                ->getOption(),
79✔
198
            (new FixerOptionBuilder('import_classes', 'Whether to import, not import or ignore global classes.'))
79✔
199
                ->setDefault(true)
79✔
200
                ->setAllowedTypes(['null', 'bool'])
79✔
201
                ->getOption(),
79✔
202
        ]);
79✔
203
    }
204

205
    /**
206
     * @param list<NamespaceUseAnalysis> $useDeclarations
207
     *
208
     * @return array<string, class-string>
209
     */
210
    private function importConstants(Tokens $tokens, array $useDeclarations): array
211
    {
212
        [$global, $other] = $this->filterUseDeclarations($useDeclarations, static fn (NamespaceUseAnalysis $declaration): bool => $declaration->isConstant(), true);
16✔
213

214
        // find namespaced const declarations (`const FOO = 1`)
215
        // and add them to the not importable names (already used)
216
        for ($index = 0, $count = $tokens->count(); $index < $count; ++$index) {
16✔
217
            $token = $tokens[$index];
16✔
218

219
            if ($token->isClassy()) {
16✔
220
                $index = $tokens->getNextTokenOfKind($index, ['{']);
4✔
221
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
4✔
222

223
                continue;
4✔
224
            }
225

226
            if (!$token->isGivenKind(\T_CONST)) {
16✔
227
                continue;
16✔
228
            }
229

230
            $index = $tokens->getNextMeaningfulToken($index);
2✔
231
            $other[$tokens[$index]->getContent()] = true;
2✔
232
        }
233

234
        $analyzer = new TokensAnalyzer($tokens);
16✔
235
        $indices = [];
16✔
236

237
        for ($index = $tokens->count() - 1; $index >= 0; --$index) {
16✔
238
            $token = $tokens[$index];
16✔
239

240
            if (!$token->isGivenKind(\T_STRING)) {
16✔
241
                continue;
16✔
242
            }
243

244
            $name = $token->getContent();
16✔
245

246
            if (isset($other[$name])) {
16✔
247
                continue;
5✔
248
            }
249

250
            if (!$analyzer->isConstantInvocation($index)) {
16✔
251
                continue;
16✔
252
            }
253

254
            $nsSeparatorIndex = $tokens->getPrevMeaningfulToken($index);
12✔
255
            if (!$tokens[$nsSeparatorIndex]->isGivenKind(\T_NS_SEPARATOR)) {
12✔
256
                if (!isset($global[$name])) {
11✔
257
                    // found an unqualified constant invocation
258
                    // add it to the not importable names (already used)
259
                    $other[$name] = true;
3✔
260
                }
261

262
                continue;
11✔
263
            }
264

265
            $prevIndex = $tokens->getPrevMeaningfulToken($nsSeparatorIndex);
12✔
266
            if ($tokens[$prevIndex]->isGivenKind([CT::T_NAMESPACE_OPERATOR, \T_STRING])) {
12✔
267
                continue;
1✔
268
            }
269

270
            $indices[] = $index;
11✔
271
        }
272

273
        return $this->prepareImports($tokens, $indices, $global, $other, true);
16✔
274
    }
275

276
    /**
277
     * @param list<NamespaceUseAnalysis> $useDeclarations
278
     *
279
     * @return array<string, class-string>
280
     */
281
    private function importFunctions(Tokens $tokens, array $useDeclarations): array
282
    {
283
        [$global, $other] = $this->filterUseDeclarations($useDeclarations, static fn (NamespaceUseAnalysis $declaration): bool => $declaration->isFunction(), false);
16✔
284

285
        // find function declarations
286
        // and add them to the not importable names (already used)
287
        foreach ($this->findFunctionDeclarations($tokens, 0, $tokens->count() - 1) as $name) {
16✔
288
            $other[strtolower($name)] = true;
2✔
289
        }
290

291
        $analyzer = new FunctionsAnalyzer();
16✔
292
        $indices = [];
16✔
293

294
        for ($index = $tokens->count() - 1; $index >= 0; --$index) {
16✔
295
            $token = $tokens[$index];
16✔
296

297
            if (!$token->isGivenKind(\T_STRING)) {
16✔
298
                continue;
16✔
299
            }
300

301
            $name = strtolower($token->getContent());
16✔
302

303
            if (isset($other[$name])) {
16✔
304
                continue;
4✔
305
            }
306

307
            if (!$analyzer->isGlobalFunctionCall($tokens, $index)) {
16✔
308
                continue;
16✔
309
            }
310

311
            $nsSeparatorIndex = $tokens->getPrevMeaningfulToken($index);
11✔
312
            if (!$tokens[$nsSeparatorIndex]->isGivenKind(\T_NS_SEPARATOR)) {
11✔
313
                if (!isset($global[$name])) {
10✔
314
                    $other[$name] = true;
2✔
315
                }
316

317
                continue;
10✔
318
            }
319

320
            $indices[] = $index;
10✔
321
        }
322

323
        return $this->prepareImports($tokens, $indices, $global, $other, false);
16✔
324
    }
325

326
    /**
327
     * @param list<NamespaceUseAnalysis> $useDeclarations
328
     *
329
     * @return array<string, class-string>
330
     */
331
    private function importClasses(Tokens $tokens, array $useDeclarations): array
332
    {
333
        [$global, $other] = $this->filterUseDeclarations($useDeclarations, static fn (NamespaceUseAnalysis $declaration): bool => $declaration->isClass(), false);
51✔
334

335
        /** @var array<int, DocBlock> $docBlocks */
336
        $docBlocks = [];
51✔
337

338
        // find class declarations and class usages in docblocks
339
        // and add them to the not importable names (already used)
340
        for ($index = 0, $count = $tokens->count(); $index < $count; ++$index) {
51✔
341
            $token = $tokens[$index];
51✔
342

343
            if ($token->isGivenKind(\T_DOC_COMMENT)) {
51✔
344
                $docBlocks[$index] = new DocBlock($token->getContent());
10✔
345

346
                $this->traverseDocBlockTypes($docBlocks[$index], static function (string $type) use ($global, &$other): void {
10✔
347
                    if (str_contains($type, '\\')) {
10✔
348
                        return;
10✔
349
                    }
350

351
                    $name = strtolower($type);
6✔
352

353
                    if (!isset($global[$name])) {
6✔
354
                        $other[$name] = true;
4✔
355
                    }
356
                });
10✔
357
            }
358

359
            if (!$token->isClassy()) {
51✔
360
                continue;
51✔
361
            }
362

363
            $index = $tokens->getNextMeaningfulToken($index);
9✔
364

365
            if ($tokens[$index]->isGivenKind(\T_STRING)) {
9✔
366
                $other[strtolower($tokens[$index]->getContent())] = true;
9✔
367
            }
368
        }
369

370
        $analyzer = new ClassyAnalyzer();
51✔
371
        $indices = [];
51✔
372

373
        for ($index = $tokens->count() - 1; $index >= 0; --$index) {
51✔
374
            $token = $tokens[$index];
51✔
375

376
            if (!$token->isGivenKind(\T_STRING)) {
51✔
377
                continue;
51✔
378
            }
379

380
            $name = strtolower($token->getContent());
51✔
381

382
            if (isset($other[$name])) {
51✔
383
                continue;
14✔
384
            }
385

386
            if (!$analyzer->isClassyInvocation($tokens, $index)) {
51✔
387
                continue;
51✔
388
            }
389

390
            $nsSeparatorIndex = $tokens->getPrevMeaningfulToken($index);
16✔
391
            if (!$tokens[$nsSeparatorIndex]->isGivenKind(\T_NS_SEPARATOR)) {
16✔
392
                if (!isset($global[$name])) {
14✔
393
                    $other[$name] = true;
1✔
394
                }
395

396
                continue;
14✔
397
            }
398

399
            if ($tokens[$tokens->getPrevMeaningfulToken($nsSeparatorIndex)]->isGivenKind([CT::T_NAMESPACE_OPERATOR, \T_STRING])) {
16✔
400
                continue;
1✔
401
            }
402

403
            $indices[] = $index;
15✔
404
        }
405

406
        $imports = [];
51✔
407

408
        foreach ($docBlocks as $index => $docBlock) {
51✔
409
            $changed = $this->traverseDocBlockTypes($docBlock, static function (string $type) use ($global, $other, &$imports): string {
10✔
410
                if ('\\' !== $type[0]) {
10✔
411
                    return $type;
6✔
412
                }
413

414
                /** @var class-string $name */
415
                $name = substr($type, 1);
9✔
416

417
                $checkName = strtolower($name);
9✔
418

419
                if (str_contains($checkName, '\\') || isset($other[$checkName])) {
9✔
420
                    return $type;
4✔
421
                }
422

423
                if (isset($global[$checkName])) {
5✔
424
                    return \is_string($global[$checkName]) ? $global[$checkName] : $name;
2✔
425
                }
426

427
                $imports[$checkName] = $name;
3✔
428

429
                return $name;
3✔
430
            });
10✔
431

432
            if ($changed) {
10✔
433
                $tokens[$index] = new Token([\T_DOC_COMMENT, $docBlock->getContent()]);
5✔
434
            }
435
        }
436

437
        return array_merge($imports, $this->prepareImports($tokens, $indices, $global, $other, false));
51✔
438
    }
439

440
    /**
441
     * Removes the leading slash at the given indices (when the name is not already used).
442
     *
443
     * @param list<int>                  $indices
444
     * @param array<string, string|true> $global
445
     * @param array<string, true>        $other
446
     *
447
     * @return array<string, class-string> array keys contain the names that must be imported
448
     */
449
    private function prepareImports(Tokens $tokens, array $indices, array $global, array $other, bool $caseSensitive): array
450
    {
451
        $imports = [];
51✔
452

453
        foreach ($indices as $index) {
51✔
454
            /** @var class-string $name */
455
            $name = $tokens[$index]->getContent();
34✔
456
            $checkName = $caseSensitive ? $name : strtolower($name);
34✔
457

458
            if (isset($other[$checkName])) {
34✔
459
                continue;
3✔
460
            }
461

462
            if (!isset($global[$checkName])) {
31✔
463
                $imports[$checkName] = $name;
31✔
464
            } elseif (\is_string($global[$checkName])) {
9✔
465
                $tokens[$index] = new Token([\T_STRING, $global[$checkName]]);
3✔
466
            }
467

468
            $tokens->clearAt($tokens->getPrevMeaningfulToken($index));
31✔
469
        }
470

471
        return $imports;
51✔
472
    }
473

474
    /**
475
     * @param list<NamespaceUseAnalysis> $useDeclarations
476
     */
477
    private function fullyQualifyConstants(Tokens $tokens, array $useDeclarations): void
478
    {
479
        if (!$tokens->isTokenKindFound(CT::T_CONST_IMPORT)) {
3✔
480
            return;
×
481
        }
482

483
        [$global] = $this->filterUseDeclarations($useDeclarations, static fn (NamespaceUseAnalysis $declaration): bool => $declaration->isConstant() && !$declaration->isAliased(), true);
3✔
484

485
        if ([] === $global) {
3✔
UNCOV
486
            return;
×
487
        }
488

489
        $analyzer = new TokensAnalyzer($tokens);
3✔
490

491
        for ($index = $tokens->count() - 1; $index >= 0; --$index) {
3✔
492
            $token = $tokens[$index];
3✔
493

494
            if (!$token->isGivenKind(\T_STRING)) {
3✔
495
                continue;
3✔
496
            }
497

498
            if (!isset($global[$token->getContent()])) {
3✔
499
                continue;
3✔
500
            }
501

502
            if ($tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(\T_NS_SEPARATOR)) {
3✔
503
                continue;
2✔
504
            }
505

506
            if (!$analyzer->isConstantInvocation($index)) {
3✔
507
                continue;
3✔
508
            }
509

510
            $tokens->insertAt($index, new Token([\T_NS_SEPARATOR, '\\']));
3✔
511
        }
512
    }
513

514
    /**
515
     * @param list<NamespaceUseAnalysis> $useDeclarations
516
     */
517
    private function fullyQualifyFunctions(Tokens $tokens, array $useDeclarations): void
518
    {
519
        if (!$tokens->isTokenKindFound(CT::T_FUNCTION_IMPORT)) {
3✔
520
            return;
×
521
        }
522

523
        [$global] = $this->filterUseDeclarations($useDeclarations, static fn (NamespaceUseAnalysis $declaration): bool => $declaration->isFunction() && !$declaration->isAliased(), false);
3✔
524

525
        if ([] === $global) {
3✔
UNCOV
526
            return;
×
527
        }
528

529
        $analyzer = new FunctionsAnalyzer();
3✔
530

531
        for ($index = $tokens->count() - 1; $index >= 0; --$index) {
3✔
532
            $token = $tokens[$index];
3✔
533

534
            if (!$token->isGivenKind(\T_STRING)) {
3✔
535
                continue;
3✔
536
            }
537

538
            if (!isset($global[strtolower($token->getContent())])) {
3✔
539
                continue;
3✔
540
            }
541

542
            if ($tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(\T_NS_SEPARATOR)) {
3✔
543
                continue;
2✔
544
            }
545

546
            if (!$analyzer->isGlobalFunctionCall($tokens, $index)) {
3✔
547
                continue;
3✔
548
            }
549

550
            $tokens->insertAt($index, new Token([\T_NS_SEPARATOR, '\\']));
3✔
551
        }
552
    }
553

554
    /**
555
     * @param list<NamespaceUseAnalysis> $useDeclarations
556
     */
557
    private function fullyQualifyClasses(Tokens $tokens, array $useDeclarations): void
558
    {
559
        if (!$tokens->isTokenKindFound(\T_USE)) {
8✔
560
            return;
×
561
        }
562

563
        [$global] = $this->filterUseDeclarations($useDeclarations, static fn (NamespaceUseAnalysis $declaration): bool => $declaration->isClass() && !$declaration->isAliased(), false);
8✔
564

565
        if ([] === $global) {
8✔
UNCOV
566
            return;
×
567
        }
568

569
        $analyzer = new ClassyAnalyzer();
8✔
570

571
        for ($index = $tokens->count() - 1; $index >= 0; --$index) {
8✔
572
            $token = $tokens[$index];
8✔
573

574
            if ($token->isGivenKind(\T_DOC_COMMENT)) {
8✔
575
                $doc = new DocBlock($token->getContent());
3✔
576

577
                $changed = $this->traverseDocBlockTypes($doc, static function (string $type) use ($global): string {
3✔
578
                    if (!isset($global[strtolower($type)])) {
3✔
579
                        return $type;
2✔
580
                    }
581

582
                    return '\\'.$type;
3✔
583
                });
3✔
584

585
                if ($changed) {
3✔
586
                    $tokens[$index] = new Token([\T_DOC_COMMENT, $doc->getContent()]);
3✔
587
                }
588

589
                continue;
3✔
590
            }
591

592
            if (!$token->isGivenKind(\T_STRING)) {
8✔
593
                continue;
8✔
594
            }
595

596
            if (!isset($global[strtolower($token->getContent())])) {
8✔
597
                continue;
8✔
598
            }
599

600
            if ($tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(\T_NS_SEPARATOR)) {
8✔
601
                continue;
6✔
602
            }
603

604
            if (!$analyzer->isClassyInvocation($tokens, $index)) {
8✔
605
                continue;
8✔
606
            }
607

608
            $tokens->insertAt($index, new Token([\T_NS_SEPARATOR, '\\']));
7✔
609
        }
610
    }
611

612
    /**
613
     * @param list<NamespaceUseAnalysis> $declarations
614
     *
615
     * @return array{0: array<string, string|true>, 1: array<string, true>}
616
     */
617
    private function filterUseDeclarations(array $declarations, callable $callback, bool $caseSensitive): array
618
    {
619
        $global = [];
58✔
620
        $other = [];
58✔
621

622
        foreach ($declarations as $declaration) {
58✔
623
            if (!$callback($declaration)) {
47✔
624
                continue;
27✔
625
            }
626

627
            $fullName = ltrim($declaration->getFullName(), '\\');
47✔
628

629
            if (str_contains($fullName, '\\')) {
47✔
630
                $name = $caseSensitive ? $declaration->getShortName() : strtolower($declaration->getShortName());
3✔
631
                $other[$name] = true;
3✔
632

633
                continue;
3✔
634
            }
635

636
            $checkName = $caseSensitive ? $fullName : strtolower($fullName);
44✔
637
            $alias = $declaration->getShortName();
44✔
638
            $global[$checkName] = $alias === $fullName ? true : $alias;
44✔
639
        }
640

641
        return [$global, $other];
58✔
642
    }
643

644
    /**
645
     * @return iterable<string>
646
     */
647
    private function findFunctionDeclarations(Tokens $tokens, int $start, int $end): iterable
648
    {
649
        for ($index = $start; $index <= $end; ++$index) {
16✔
650
            $token = $tokens[$index];
16✔
651

652
            if ($token->isClassy()) {
16✔
653
                $classStart = $tokens->getNextTokenOfKind($index, ['{']);
5✔
654
                $classEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classStart);
5✔
655

656
                for ($index = $classStart; $index <= $classEnd; ++$index) {
5✔
657
                    if (!$tokens[$index]->isGivenKind(\T_FUNCTION)) {
5✔
658
                        continue;
5✔
659
                    }
660

661
                    $methodStart = $tokens->getNextTokenOfKind($index, ['{', ';']);
4✔
662

663
                    if ($tokens[$methodStart]->equals(';')) {
4✔
UNCOV
664
                        $index = $methodStart;
×
665

UNCOV
666
                        continue;
×
667
                    }
668

669
                    $methodEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $methodStart);
4✔
670

671
                    foreach ($this->findFunctionDeclarations($tokens, $methodStart, $methodEnd) as $function) {
4✔
672
                        yield $function;
1✔
673
                    }
674

675
                    $index = $methodEnd;
4✔
676
                }
677

678
                continue;
5✔
679
            }
680

681
            if (!$token->isGivenKind(\T_FUNCTION)) {
16✔
682
                continue;
16✔
683
            }
684

685
            $index = $tokens->getNextMeaningfulToken($index);
2✔
686

687
            if ($tokens[$index]->isGivenKind(CT::T_RETURN_REF)) {
2✔
UNCOV
688
                $index = $tokens->getNextMeaningfulToken($index);
×
689
            }
690

691
            if ($tokens[$index]->isGivenKind(\T_STRING)) {
2✔
692
                yield $tokens[$index]->getContent();
2✔
693
            }
694
        }
695
    }
696

697
    private function traverseDocBlockTypes(DocBlock $doc, callable $callback): bool
698
    {
699
        $annotations = $doc->getAnnotationsOfType(Annotation::TAGS_WITH_TYPES);
12✔
700

701
        if (0 === \count($annotations)) {
12✔
UNCOV
702
            return false;
×
703
        }
704

705
        $changed = false;
12✔
706

707
        foreach ($annotations as $annotation) {
12✔
708
            $types = $new = $annotation->getTypes();
12✔
709

710
            foreach ($types as $i => $fullType) {
12✔
711
                $newFullType = $fullType;
12✔
712

713
                Preg::matchAll('/[\\\\\w]+(?![\\\\\w:])/', $fullType, $matches, \PREG_OFFSET_CAPTURE);
12✔
714

715
                foreach (array_reverse($matches[0]) as [$type, $offset]) {
12✔
716
                    $newType = $callback($type);
12✔
717

718
                    if (null !== $newType && $type !== $newType) {
12✔
719
                        $newFullType = substr_replace($newFullType, $newType, $offset, \strlen($type));
7✔
720
                    }
721
                }
722

723
                $new[$i] = $newFullType;
12✔
724
            }
725

726
            if ($types !== $new) {
12✔
727
                $annotation->setTypes($new);
7✔
728
                $changed = true;
7✔
729
            }
730
        }
731

732
        return $changed;
12✔
733
    }
734
}
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