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

keradus / PHP-CS-Fixer / 16253708939

13 Jul 2025 09:37PM UTC coverage: 94.807% (+0.001%) from 94.806%
16253708939

push

github

web-flow
Merge branch 'master' into native_constant_invocation__CS

2259 of 2334 new or added lines in 338 files covered. (96.79%)

7 existing lines in 5 files now uncovered.

28260 of 29808 relevant lines covered (94.81%)

45.39 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([
×
NEW
115
            \T_COMMENT,
×
NEW
116
            \T_ABSTRACT,
×
NEW
117
            \T_FINAL,
×
NEW
118
            \T_PRIVATE,
×
NEW
119
            \T_PROTECTED,
×
NEW
120
            \T_PUBLIC,
×
NEW
121
            \T_STATIC,
×
UNCOV
122
        ]));
×
123

NEW
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) {
×
NEW
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
            $commonType = $typeExpression->getCommonType();
×
272

273
            if (!$containsOtherThanIterableType && !\in_array($commonType, ['array', \Traversable::class, 'iterable'], true)) {
×
274
                $containsOtherThanIterableType = true;
×
275
            }
276
            if ($isReturnType && !$containsOtherThanEmptyType && !\in_array($commonType, ['null', 'void', 'never'], true)) {
×
277
                $containsOtherThanEmptyType = true;
×
278
            }
279

280
            if (!$isNullable && $typesExpression->allowsNull()) {
×
281
                $isNullable = true;
×
282
            }
283

284
            $unionTypes[] = $commonType;
×
285
        }
286

287
        if (!$containsOtherThanIterableType) {
×
288
            return null;
×
289
        }
290
        if ($isReturnType && !$containsOtherThanEmptyType) {
×
291
            return null;
×
292
        }
293

294
        if ($isNullable) {
×
295
            $unionTypes[] = 'null';
×
296
        }
297

298
        return implode($typesExpression->getTypesGlue(), array_unique($unionTypes));
×
299
    }
300

301
    final protected function isValidSyntax(string $code): bool
302
    {
303
        if (!isset(self::$syntaxValidationCache[$code])) {
×
304
            try {
305
                Tokens::fromCode($code);
×
306
                self::$syntaxValidationCache[$code] = true;
×
307
            } catch (\ParseError $e) {
×
308
                self::$syntaxValidationCache[$code] = false;
×
309
            }
310
        }
311

312
        return self::$syntaxValidationCache[$code];
×
313
    }
314
}
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