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

keradus / PHP-CS-Fixer / 17253322895

26 Aug 2025 11:52PM UTC coverage: 94.753% (+0.008%) from 94.745%
17253322895

push

github

keradus
add to git-blame-ignore-revs

28316 of 29884 relevant lines covered (94.75%)

45.64 hits per line

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

97.5
/src/Fixer/FunctionNotation/PhpdocToPropertyTypeFixer.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\FunctionNotation;
16

17
use PhpCsFixer\AbstractPhpdocToTypeDeclarationFixer;
18
use PhpCsFixer\DocBlock\Annotation;
19
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
20
use PhpCsFixer\Fixer\ExperimentalFixerInterface;
21
use PhpCsFixer\FixerDefinition\CodeSample;
22
use PhpCsFixer\FixerDefinition\FixerDefinition;
23
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
24
use PhpCsFixer\Tokenizer\Token;
25
use PhpCsFixer\Tokenizer\Tokens;
26

27
/**
28
 * @phpstan-import-type _CommonTypeInfo from AbstractPhpdocToTypeDeclarationFixer
29
 *
30
 * @phpstan-type _AutogeneratedInputConfiguration array{
31
 *  scalar_types?: bool,
32
 *  types_map?: array<string, string>,
33
 *  union_types?: bool,
34
 * }
35
 * @phpstan-type _AutogeneratedComputedConfiguration array{
36
 *  scalar_types: bool,
37
 *  types_map: array<string, string>,
38
 *  union_types: bool,
39
 * }
40
 *
41
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
42
 *
43
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
44
 */
45
final class PhpdocToPropertyTypeFixer extends AbstractPhpdocToTypeDeclarationFixer implements ConfigurableFixerInterface, ExperimentalFixerInterface
46
{
47
    private const TYPE_CHECK_TEMPLATE = '<?php class A { private %s $b; }';
48

49
    /**
50
     * @var array<string, true>
51
     */
52
    private array $skippedTypes = [
53
        'resource' => true,
54
        'null' => true,
55
    ];
56

57
    public function getDefinition(): FixerDefinitionInterface
58
    {
59
        return new FixerDefinition(
3✔
60
            'Takes `@var` annotation of non-mixed types and adjusts accordingly the property signature..',
3✔
61
            [
3✔
62
                new CodeSample(
3✔
63
                    '<?php
3✔
64
class Foo {
65
    /** @var int */
66
    private $foo;
67
    /** @var \Traversable */
68
    private $bar;
69
}
70
',
3✔
71
                ),
3✔
72
                new CodeSample(
3✔
73
                    '<?php
3✔
74
class Foo {
75
    /** @var int */
76
    private $foo;
77
    /** @var \Traversable */
78
    private $bar;
79
}
80
',
3✔
81
                    ['scalar_types' => false]
3✔
82
                ),
3✔
83
                new CodeSample(
3✔
84
                    '<?php
3✔
85
class Foo {
86
    /** @var int|string */
87
    private $foo;
88
    /** @var \Traversable */
89
    private $bar;
90
}
91
',
3✔
92
                    ['union_types' => false]
3✔
93
                ),
3✔
94
            ],
3✔
95
            null,
3✔
96
            'The `@var` annotation is mandatory for the fixer to make changes, signatures of properties without it (no docblock) will not be fixed. Manual actions might be required for newly typed properties that are read before initialization.'
3✔
97
        );
3✔
98
    }
99

100
    public function isCandidate(Tokens $tokens): bool
101
    {
102
        return $tokens->isTokenKindFound(\T_DOC_COMMENT);
76✔
103
    }
104

105
    /**
106
     * {@inheritdoc}
107
     *
108
     * Must run before FullyQualifiedStrictTypesFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAlignFixer.
109
     * Must run after AlignMultilineCommentFixer, CommentToPhpdocFixer, PhpdocIndentFixer, PhpdocScalarFixer, PhpdocToCommentFixer, PhpdocTypesFixer.
110
     */
111
    public function getPriority(): int
112
    {
113
        return 8;
1✔
114
    }
115

116
    protected function isSkippedType(string $type): bool
117
    {
118
        return isset($this->skippedTypes[$type]);
66✔
119
    }
120

121
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
122
    {
123
        $tokensToInsert = [];
75✔
124
        $typesToExclude = [];
75✔
125

126
        foreach ($tokens as $index => $token) {
75✔
127
            if ($token->isGivenKind(\T_DOC_COMMENT)) {
75✔
128
                $typesToExclude = array_merge($typesToExclude, self::getTypesToExclude($token->getContent()));
75✔
129

130
                continue;
75✔
131
            }
132
            if ($tokens[$index]->isGivenKind([\T_CLASS, \T_TRAIT])) {
75✔
133
                $tokensToInsert += $this->fixClass($tokens, $index, $typesToExclude);
75✔
134
            }
135
        }
136

137
        $tokens->insertSlices($tokensToInsert);
75✔
138
    }
139

140
    protected function createTokensFromRawType(string $type): Tokens
141
    {
142
        $typeTokens = Tokens::fromCode(\sprintf(self::TYPE_CHECK_TEMPLATE, $type));
49✔
143
        $typeTokens->clearRange(0, 8);
49✔
144
        $typeTokens->clearRange(\count($typeTokens) - 5, \count($typeTokens) - 1);
49✔
145
        $typeTokens->clearEmptyTokens();
49✔
146

147
        return $typeTokens;
49✔
148
    }
149

150
    /**
151
     * @param list<string> $typesToExclude
152
     *
153
     * @return array<int, list<Token>>
154
     */
155
    private function fixClass(Tokens $tokens, int $index, array $typesToExclude): array
156
    {
157
        $tokensToInsert = [];
75✔
158

159
        $index = $tokens->getNextTokenOfKind($index, ['{']);
75✔
160
        $classEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
75✔
161

162
        for (; $index < $classEndIndex; ++$index) {
75✔
163
            if ($tokens[$index]->isGivenKind(\T_FUNCTION)) {
75✔
164
                $index = $tokens->getNextTokenOfKind($index, ['{', ';']);
1✔
165

166
                if ($tokens[$index]->equals('{')) {
1✔
167
                    $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
×
168
                }
169

170
                continue;
1✔
171
            }
172

173
            if (!$tokens[$index]->isGivenKind(\T_DOC_COMMENT)) {
75✔
174
                continue;
75✔
175
            }
176

177
            $docCommentIndex = $index;
75✔
178
            $propertyIndices = $this->findNextUntypedPropertiesDeclaration($tokens, $docCommentIndex);
75✔
179

180
            if ([] === $propertyIndices) {
75✔
181
                continue;
49✔
182
            }
183

184
            $typeInfo = $this->resolveApplicableType(
74✔
185
                $propertyIndices,
74✔
186
                $this->getAnnotationsFromDocComment('var', $tokens, $docCommentIndex)
74✔
187
            );
74✔
188

189
            if (null === $typeInfo) {
74✔
190
                continue;
19✔
191
            }
192

193
            $propertyType = $typeInfo['commonType'];
56✔
194
            $isNullable = $typeInfo['isNullable'];
56✔
195

196
            if (\in_array($propertyType, ['callable', 'never', 'void'], true)) {
56✔
197
                continue;
3✔
198
            }
199

200
            if (\in_array($propertyType, $typesToExclude, true)) {
53✔
201
                continue;
6✔
202
            }
203

204
            if (!$this->isValidSyntax(\sprintf(self::TYPE_CHECK_TEMPLATE, $propertyType))) {
49✔
205
                continue;
×
206
            }
207

208
            $newTokens = array_merge(
49✔
209
                $this->createTypeDeclarationTokens($propertyType, $isNullable),
49✔
210
                [new Token([\T_WHITESPACE, ' '])]
49✔
211
            );
49✔
212

213
            $tokensToInsert[current($propertyIndices)] = $newTokens;
49✔
214

215
            $index = max($propertyIndices) + 1;
49✔
216
        }
217

218
        return $tokensToInsert;
75✔
219
    }
220

221
    /**
222
     * @return array<string, int>
223
     */
224
    private function findNextUntypedPropertiesDeclaration(Tokens $tokens, int $index): array
225
    {
226
        do {
227
            $index = $tokens->getNextMeaningfulToken($index);
75✔
228
        } while ($tokens[$index]->isGivenKind([
75✔
229
            \T_PRIVATE,
75✔
230
            \T_PROTECTED,
75✔
231
            \T_PUBLIC,
75✔
232
            \T_STATIC,
75✔
233
            \T_VAR,
75✔
234
        ]));
75✔
235

236
        if (!$tokens[$index]->isGivenKind(\T_VARIABLE)) {
75✔
237
            return [];
49✔
238
        }
239

240
        $properties = [];
74✔
241

242
        while (!$tokens[$index]->equals(';')) {
74✔
243
            if ($tokens[$index]->isGivenKind(\T_VARIABLE)) {
74✔
244
                $properties[$tokens[$index]->getContent()] = $index;
74✔
245
            }
246

247
            $index = $tokens->getNextMeaningfulToken($index);
74✔
248
        }
249

250
        return $properties;
74✔
251
    }
252

253
    /**
254
     * @param array<string, int> $propertyIndices
255
     * @param list<Annotation>   $annotations
256
     *
257
     * @return ?_CommonTypeInfo
258
     */
259
    private function resolveApplicableType(array $propertyIndices, array $annotations): ?array
260
    {
261
        $propertyTypes = [];
74✔
262

263
        foreach ($annotations as $annotation) {
74✔
264
            $propertyName = $annotation->getVariableName();
74✔
265

266
            if (null === $propertyName) {
74✔
267
                if (1 !== \count($propertyIndices)) {
70✔
268
                    continue;
3✔
269
                }
270

271
                $propertyName = array_key_first($propertyIndices);
67✔
272
            }
273

274
            if (!isset($propertyIndices[$propertyName])) {
74✔
275
                continue;
×
276
            }
277

278
            $typesExpression = $annotation->getTypeExpression();
74✔
279

280
            if (null === $typesExpression) {
74✔
281
                continue;
3✔
282
            }
283

284
            $typeInfo = $this->getCommonTypeInfo($typesExpression, false);
71✔
285
            $unionTypes = null;
71✔
286

287
            if (null === $typeInfo) {
71✔
288
                $unionTypes = $this->getUnionTypes($typesExpression, false);
13✔
289
            }
290

291
            if (null === $typeInfo && null === $unionTypes) {
71✔
292
                continue;
10✔
293
            }
294

295
            if (null !== $unionTypes) {
62✔
296
                $typeInfo = ['commonType' => $unionTypes, 'isNullable' => false];
3✔
297
            }
298

299
            if (\array_key_exists($propertyName, $propertyTypes) && $typeInfo !== $propertyTypes[$propertyName]) {
62✔
300
                return null;
2✔
301
            }
302

303
            $propertyTypes[$propertyName] = $typeInfo;
62✔
304
        }
305

306
        if (\count($propertyTypes) !== \count($propertyIndices)) {
72✔
307
            return null;
16✔
308
        }
309

310
        $type = array_shift($propertyTypes);
57✔
311

312
        foreach ($propertyTypes as $propertyType) {
57✔
313
            if ($propertyType !== $type) {
3✔
314
                return null;
1✔
315
            }
316
        }
317

318
        return $type;
56✔
319
    }
320
}
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