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

keradus / PHP-CS-Fixer / 17319949156

29 Aug 2025 09:20AM UTC coverage: 94.696% (-0.05%) from 94.744%
17319949156

push

github

keradus
CS

28333 of 29920 relevant lines covered (94.7%)

45.63 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
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
49
 */
50
abstract class AbstractPhpdocToTypeDeclarationFixer extends AbstractFixer implements ConfigurableFixerInterface
51
{
52
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
53
    use ConfigurableFixerTrait;
54

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

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

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

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

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

89
    abstract protected function isSkippedType(string $type): bool;
90

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

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

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

130
        return null;
×
131
    }
132

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

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

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

150
        return $doc->getAnnotationsOfType($name);
×
151
    }
152

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

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

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

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

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

184
        return array_values($newTokens);
×
185
    }
186

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

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

204
        if (null === $commonType) {
×
205
            return null;
×
206
        }
207

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

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

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

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

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

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

236
        return ['commonType' => $commonType, 'isNullable' => $isNullable];
×
237
    }
238

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

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

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

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

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

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

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

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

275
            if (null === $commonTypeInfo) {
×
276
                return null;
×
277
            }
278

279
            $commonType = $commonTypeInfo['commonType'];
×
280

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

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

292
            $unionTypes[] = $commonType;
×
293
        }
294

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

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

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

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

320
        return self::$syntaxValidationCache[$code];
×
321
    }
322

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

330
        $docBlock = new DocBlock($content);
×
331

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

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

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

346
        return $typesToExclude;
×
347
    }
348
}
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