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

keradus / PHP-CS-Fixer / 17013625939

15 Aug 2025 09:45PM UTC coverage: 94.74% (+0.01%) from 94.73%
17013625939

push

github

web-flow
chore: extract token types for PHPStan (#8925)

28260 of 29829 relevant lines covered (94.74%)

45.88 hits per line

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

0.0
/src/AbstractPhpdocToTypeDeclarationFixer.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;
16

17
use PhpCsFixer\DocBlock\Annotation;
18
use PhpCsFixer\DocBlock\DocBlock;
19
use PhpCsFixer\DocBlock\TypeExpression;
20
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
21
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
22
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
23
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
24
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
25
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
26
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
27
use PhpCsFixer\Tokenizer\CT;
28
use PhpCsFixer\Tokenizer\Token;
29
use PhpCsFixer\Tokenizer\Tokens;
30

31
/**
32
 * @internal
33
 *
34
 * @phpstan-type _CommonTypeInfo array{commonType: string, isNullable: bool}
35
 * @phpstan-type _AutogeneratedInputConfiguration array{
36
 *  scalar_types?: bool,
37
 *  types_map?: array<string, string>,
38
 *  union_types?: bool,
39
 * }
40
 * @phpstan-type _AutogeneratedComputedConfiguration array{
41
 *  scalar_types: bool,
42
 *  types_map: array<string, string>,
43
 *  union_types: bool,
44
 * }
45
 *
46
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
47
 */
48
abstract class AbstractPhpdocToTypeDeclarationFixer extends AbstractFixer implements ConfigurableFixerInterface
49
{
50
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
51
    use ConfigurableFixerTrait;
52

53
    private const REGEX_CLASS = '(?:\\\?+'.TypeExpression::REGEX_IDENTIFIER
54
        .'(\\\\'.TypeExpression::REGEX_IDENTIFIER.')*+)';
55

56
    /**
57
     * @var array<string, int>
58
     */
59
    private array $versionSpecificTypes = [
60
        'void' => 7_01_00,
61
        'iterable' => 7_01_00,
62
        'object' => 7_02_00,
63
        'mixed' => 8_00_00,
64
        'never' => 8_01_00,
65
    ];
66

67
    /**
68
     * @var array<string, bool>
69
     */
70
    private array $scalarTypes = [
71
        'bool' => true,
72
        'float' => true,
73
        'int' => true,
74
        'string' => true,
75
    ];
76

77
    /**
78
     * @var array<string, bool>
79
     */
80
    private static array $syntaxValidationCache = [];
81

82
    public function isRisky(): bool
83
    {
84
        return true;
×
85
    }
86

87
    abstract protected function isSkippedType(string $type): bool;
88

89
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
90
    {
91
        return new FixerConfigurationResolver([
×
92
            (new FixerOptionBuilder('scalar_types', 'Fix also scalar types; may have unexpected behaviour due to PHP bad type coercion system.'))
×
93
                ->setAllowedTypes(['bool'])
×
94
                ->setDefault(true)
×
95
                ->getOption(),
×
96
            (new FixerOptionBuilder('union_types', 'Fix also union types; turned on by default on PHP >= 8.0.0.'))
×
97
                ->setAllowedTypes(['bool'])
×
98
                ->setDefault(\PHP_VERSION_ID >= 8_00_00)
×
99
                ->getOption(),
×
100
            (new FixerOptionBuilder('types_map', 'Map of custom types, e.g. template types from PHPStan.'))
×
101
                ->setAllowedTypes(['array<string, string>'])
×
102
                ->setDefault([])
×
103
                ->getOption(),
×
104
        ]);
×
105
    }
106

107
    /**
108
     * @param int $index The index of the function token
109
     */
110
    protected function findFunctionDocComment(Tokens $tokens, int $index): ?int
111
    {
112
        do {
113
            $index = $tokens->getPrevNonWhitespace($index);
×
114
        } while ($tokens[$index]->isGivenKind([
×
115
            \T_COMMENT,
×
116
            \T_ABSTRACT,
×
117
            \T_FINAL,
×
118
            \T_PRIVATE,
×
119
            \T_PROTECTED,
×
120
            \T_PUBLIC,
×
121
            \T_STATIC,
×
122
        ]));
×
123

124
        if ($tokens[$index]->isGivenKind(\T_DOC_COMMENT)) {
×
125
            return $index;
×
126
        }
127

128
        return null;
×
129
    }
130

131
    /**
132
     * @return list<Annotation>
133
     */
134
    protected function getAnnotationsFromDocComment(string $name, Tokens $tokens, int $docCommentIndex): array
135
    {
136
        $namespacesAnalyzer = new NamespacesAnalyzer();
×
137
        $namespace = $namespacesAnalyzer->getNamespaceAt($tokens, $docCommentIndex);
×
138

139
        $namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();
×
140
        $namespaceUses = $namespaceUsesAnalyzer->getDeclarationsInNamespace($tokens, $namespace);
×
141

142
        $doc = new DocBlock(
×
143
            $tokens[$docCommentIndex]->getContent(),
×
144
            $namespace,
×
145
            $namespaceUses
×
146
        );
×
147

148
        return $doc->getAnnotationsOfType($name);
×
149
    }
150

151
    /**
152
     * @return list<Token>
153
     */
154
    protected function createTypeDeclarationTokens(string $type, bool $isNullable): array
155
    {
156
        $newTokens = [];
×
157

158
        if (true === $isNullable && 'mixed' !== $type) {
×
159
            $newTokens[] = new Token([CT::T_NULLABLE_TYPE, '?']);
×
160
        }
161

162
        $newTokens = array_merge(
×
163
            $newTokens,
×
164
            $this->createTokensFromRawType($type)->toArray()
×
165
        );
×
166

167
        // 'scalar's, 'void', 'iterable' and 'object' must be unqualified
168
        foreach ($newTokens as $i => $token) {
×
169
            if ($token->isGivenKind(\T_STRING)) {
×
170
                $typeUnqualified = $token->getContent();
×
171

172
                if (
173
                    (isset($this->scalarTypes[$typeUnqualified]) || isset($this->versionSpecificTypes[$typeUnqualified]))
×
174
                    && isset($newTokens[$i - 1])
×
175
                    && '\\' === $newTokens[$i - 1]->getContent()
×
176
                ) {
177
                    unset($newTokens[$i - 1]);
×
178
                }
179
            }
180
        }
181

182
        return array_values($newTokens);
×
183
    }
184

185
    /**
186
     * Each fixer inheriting from this class must define a way of creating token collection representing type
187
     * gathered from phpDoc, e.g. `Foo|Bar` should be transformed into 3 tokens (`Foo`, `|` and `Bar`).
188
     * This can't be standardised, because some types may be allowed in one place, and invalid in others.
189
     *
190
     * @param string $type Type determined (and simplified) from phpDoc
191
     */
192
    abstract protected function createTokensFromRawType(string $type): Tokens;
193

194
    /**
195
     * @return ?_CommonTypeInfo
196
     */
197
    protected function getCommonTypeInfo(TypeExpression $typesExpression, bool $isReturnType): ?array
198
    {
199
        $commonType = $typesExpression->getCommonType();
×
200
        $isNullable = $typesExpression->allowsNull();
×
201

202
        if (null === $commonType) {
×
203
            return null;
×
204
        }
205

206
        if ($isNullable && 'void' === $commonType) {
×
207
            return null;
×
208
        }
209

210
        if ('static' === $commonType && (!$isReturnType || \PHP_VERSION_ID < 8_00_00)) {
×
211
            $commonType = 'self';
×
212
        }
213

214
        if ($this->isSkippedType($commonType)) {
×
215
            return null;
×
216
        }
217

218
        if (isset($this->versionSpecificTypes[$commonType]) && \PHP_VERSION_ID < $this->versionSpecificTypes[$commonType]) {
×
219
            return null;
×
220
        }
221

222
        if (\array_key_exists($commonType, $this->configuration['types_map'])) {
×
223
            $commonType = $this->configuration['types_map'][$commonType];
×
224
        }
225

226
        if (isset($this->scalarTypes[$commonType])) {
×
227
            if (false === $this->configuration['scalar_types']) {
×
228
                return null;
×
229
            }
230
        } elseif (!Preg::match('/^'.self::REGEX_CLASS.'$/', $commonType)) {
×
231
            return null;
×
232
        }
233

234
        return ['commonType' => $commonType, 'isNullable' => $isNullable];
×
235
    }
236

237
    protected function getUnionTypes(TypeExpression $typesExpression, bool $isReturnType): ?string
238
    {
239
        if (\PHP_VERSION_ID < 8_00_00) {
×
240
            return null;
×
241
        }
242

243
        if (!$typesExpression->isUnionType()) {
×
244
            return null;
×
245
        }
246

247
        if (false === $this->configuration['union_types']) {
×
248
            return null;
×
249
        }
250

251
        $types = $typesExpression->getTypes();
×
252
        $isNullable = $typesExpression->allowsNull();
×
253
        $unionTypes = [];
×
254
        $containsOtherThanIterableType = false;
×
255
        $containsOtherThanEmptyType = false;
×
256

257
        foreach ($types as $type) {
×
258
            if ('null' === $type) {
×
259
                continue;
×
260
            }
261

262
            if ($this->isSkippedType($type)) {
×
263
                return null;
×
264
            }
265

266
            if (isset($this->versionSpecificTypes[$type]) && \PHP_VERSION_ID < $this->versionSpecificTypes[$type]) {
×
267
                return null;
×
268
            }
269

270
            $typeExpression = new TypeExpression($type, null, []);
×
271
            $commonTypeInfo = $this->getCommonTypeInfo($typeExpression, $isReturnType);
×
272

273
            if (null === $commonTypeInfo) {
×
274
                return null;
×
275
            }
276

277
            $commonType = $commonTypeInfo['commonType'];
×
278

279
            if (!$containsOtherThanIterableType && !\in_array($commonType, ['array', \Traversable::class, 'iterable'], true)) {
×
280
                $containsOtherThanIterableType = true;
×
281
            }
282
            if ($isReturnType && !$containsOtherThanEmptyType && !\in_array($commonType, ['null', 'void', 'never'], true)) {
×
283
                $containsOtherThanEmptyType = true;
×
284
            }
285

286
            if (!$isNullable && $commonTypeInfo['isNullable']) {
×
287
                $isNullable = true;
×
288
            }
289

290
            $unionTypes[] = $commonType;
×
291
        }
292

293
        if (!$containsOtherThanIterableType) {
×
294
            return null;
×
295
        }
296
        if ($isReturnType && !$containsOtherThanEmptyType) {
×
297
            return null;
×
298
        }
299

300
        if ($isNullable) {
×
301
            $unionTypes[] = 'null';
×
302
        }
303

304
        return implode($typesExpression->getTypesGlue(), array_unique($unionTypes));
×
305
    }
306

307
    final protected function isValidSyntax(string $code): bool
308
    {
309
        if (!isset(self::$syntaxValidationCache[$code])) {
×
310
            try {
311
                Tokens::fromCode($code);
×
312
                self::$syntaxValidationCache[$code] = true;
×
313
            } catch (\ParseError $e) {
×
314
                self::$syntaxValidationCache[$code] = false;
×
315
            }
316
        }
317

318
        return self::$syntaxValidationCache[$code];
×
319
    }
320

321
    /**
322
     * @return list<string>
323
     */
324
    final protected static function getTypesToExclude(string $content): array
325
    {
326
        $typesToExclude = [];
×
327

328
        $docBlock = new DocBlock($content);
×
329

330
        foreach ($docBlock->getAnnotationsOfType(['phpstan-type', 'psalm-type']) as $annotation) {
×
331
            $typesToExclude[] = $annotation->getTypeExpression()->toString();
×
332
        }
333

334
        foreach ($docBlock->getAnnotationsOfType(['phpstan-import-type', 'psalm-import-type']) as $annotation) {
×
335
            $content = trim($annotation->getContent());
×
336
            if (Preg::match('/\bas\s+('.TypeExpression::REGEX_IDENTIFIER.')$/', $content, $matches)) {
×
337
                $typesToExclude[] = $matches[1];
×
338

339
                continue;
×
340
            }
341
            $typesToExclude[] = $annotation->getTypeExpression()->toString();
×
342
        }
343

344
        return $typesToExclude;
×
345
    }
346
}
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