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

keradus / PHP-CS-Fixer / 16999983712

15 Aug 2025 09:42PM UTC coverage: 94.75% (-0.09%) from 94.839%
16999983712

push

github

keradus
ci: more self-fixing checks on lowest/highest PHP

28263 of 29829 relevant lines covered (94.75%)

45.88 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
final class PhpdocToPropertyTypeFixer extends AbstractPhpdocToTypeDeclarationFixer implements ConfigurableFixerInterface, ExperimentalFixerInterface
44
{
45
    private const TYPE_CHECK_TEMPLATE = '<?php class A { private %s $b; }';
46

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

55
    public function getDefinition(): FixerDefinitionInterface
56
    {
57
        return new FixerDefinition(
3✔
58
            'Takes `@var` annotation of non-mixed types and adjusts accordingly the property signature..',
3✔
59
            [
3✔
60
                new CodeSample(
3✔
61
                    '<?php
3✔
62
class Foo {
63
    /** @var int */
64
    private $foo;
65
    /** @var \Traversable */
66
    private $bar;
67
}
68
',
3✔
69
                ),
3✔
70
                new CodeSample(
3✔
71
                    '<?php
3✔
72
class Foo {
73
    /** @var int */
74
    private $foo;
75
    /** @var \Traversable */
76
    private $bar;
77
}
78
',
3✔
79
                    ['scalar_types' => false]
3✔
80
                ),
3✔
81
                new CodeSample(
3✔
82
                    '<?php
3✔
83
class Foo {
84
    /** @var int|string */
85
    private $foo;
86
    /** @var \Traversable */
87
    private $bar;
88
}
89
',
3✔
90
                    ['union_types' => false]
3✔
91
                ),
3✔
92
            ],
3✔
93
            null,
3✔
94
            '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✔
95
        );
3✔
96
    }
97

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

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

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

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

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

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

135
        $tokens->insertSlices($tokensToInsert);
75✔
136
    }
137

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

145
        return $typeTokens;
49✔
146
    }
147

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

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

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

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

168
                continue;
1✔
169
            }
170

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

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

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

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

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

191
            $propertyType = $typeInfo['commonType'];
56✔
192
            $isNullable = $typeInfo['isNullable'];
56✔
193

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

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

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

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

211
            $tokensToInsert[current($propertyIndices)] = $newTokens;
49✔
212

213
            $index = max($propertyIndices) + 1;
49✔
214
        }
215

216
        return $tokensToInsert;
75✔
217
    }
218

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

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

238
        $properties = [];
74✔
239

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

245
            $index = $tokens->getNextMeaningfulToken($index);
74✔
246
        }
247

248
        return $properties;
74✔
249
    }
250

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

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

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

269
                $propertyName = array_key_first($propertyIndices);
67✔
270
            }
271

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

276
            $typesExpression = $annotation->getTypeExpression();
74✔
277

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

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

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

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

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

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

301
            $propertyTypes[$propertyName] = $typeInfo;
62✔
302
        }
303

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

308
        $type = array_shift($propertyTypes);
57✔
309

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

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