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

keradus / PHP-CS-Fixer / 17377459942

01 Sep 2025 12:19PM UTC coverage: 94.684% (-0.009%) from 94.693%
17377459942

push

github

web-flow
chore: `Tokens::offsetSet` - explicit validation of input (#9004)

1 of 5 new or added lines in 1 file covered. (20.0%)

306 existing lines in 60 files now uncovered.

28390 of 29984 relevant lines covered (94.68%)

45.5 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
                        <?php
65
                        class Foo {
66
                            /** @var int */
67
                            private $foo;
68
                            /** @var \Traversable */
69
                            private $bar;
70
                        }
71

72
                        PHP,
3✔
73
                ),
3✔
74
                new CodeSample(
3✔
75
                    <<<'PHP'
3✔
76
                        <?php
77
                        class Foo {
78
                            /** @var int */
79
                            private $foo;
80
                            /** @var \Traversable */
81
                            private $bar;
82
                        }
83

84
                        PHP,
3✔
85
                    ['scalar_types' => false]
3✔
86
                ),
3✔
87
                new CodeSample(
3✔
88
                    <<<'PHP'
3✔
89
                        <?php
90
                        class Foo {
91
                            /** @var int|string */
92
                            private $foo;
93
                            /** @var \Traversable */
94
                            private $bar;
95
                        }
96

97
                        PHP,
3✔
98
                    ['union_types' => false]
3✔
99
                ),
3✔
100
            ],
3✔
101
            null,
3✔
102
            '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✔
103
        );
3✔
104
    }
105

106
    public function isCandidate(Tokens $tokens): bool
107
    {
108
        return $tokens->isTokenKindFound(\T_DOC_COMMENT);
76✔
109
    }
110

111
    /**
112
     * {@inheritdoc}
113
     *
114
     * Must run before FullyQualifiedStrictTypesFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAlignFixer.
115
     * Must run after AlignMultilineCommentFixer, CommentToPhpdocFixer, PhpdocIndentFixer, PhpdocScalarFixer, PhpdocToCommentFixer, PhpdocTypesFixer.
116
     */
117
    public function getPriority(): int
118
    {
119
        return 8;
1✔
120
    }
121

122
    protected function isSkippedType(string $type): bool
123
    {
124
        return isset($this->skippedTypes[$type]);
66✔
125
    }
126

127
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
128
    {
129
        $tokensToInsert = [];
75✔
130
        $typesToExclude = [];
75✔
131

132
        foreach ($tokens as $index => $token) {
75✔
133
            if ($token->isGivenKind(\T_DOC_COMMENT)) {
75✔
134
                $typesToExclude = array_merge($typesToExclude, self::getTypesToExclude($token->getContent()));
75✔
135

136
                continue;
75✔
137
            }
138
            if ($tokens[$index]->isGivenKind([\T_CLASS, \T_TRAIT])) {
75✔
139
                $tokensToInsert += $this->fixClass($tokens, $index, $typesToExclude);
75✔
140
            }
141
        }
142

143
        $tokens->insertSlices($tokensToInsert);
75✔
144
    }
145

146
    protected function createTokensFromRawType(string $type): Tokens
147
    {
148
        $typeTokens = Tokens::fromCode(\sprintf(self::TYPE_CHECK_TEMPLATE, $type));
49✔
149
        $typeTokens->clearRange(0, 8);
49✔
150
        $typeTokens->clearRange(\count($typeTokens) - 5, \count($typeTokens) - 1);
49✔
151
        $typeTokens->clearEmptyTokens();
49✔
152

153
        return $typeTokens;
49✔
154
    }
155

156
    /**
157
     * @param list<string> $typesToExclude
158
     *
159
     * @return array<int, list<Token>>
160
     */
161
    private function fixClass(Tokens $tokens, int $index, array $typesToExclude): array
162
    {
163
        $tokensToInsert = [];
75✔
164

165
        $index = $tokens->getNextTokenOfKind($index, ['{']);
75✔
166
        $classEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
75✔
167

168
        for (; $index < $classEndIndex; ++$index) {
75✔
169
            if ($tokens[$index]->isGivenKind(\T_FUNCTION)) {
75✔
170
                $index = $tokens->getNextTokenOfKind($index, ['{', ';']);
1✔
171

172
                if ($tokens[$index]->equals('{')) {
1✔
UNCOV
173
                    $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
×
174
                }
175

176
                continue;
1✔
177
            }
178

179
            if (!$tokens[$index]->isGivenKind(\T_DOC_COMMENT)) {
75✔
180
                continue;
75✔
181
            }
182

183
            $docCommentIndex = $index;
75✔
184
            $propertyIndices = $this->findNextUntypedPropertiesDeclaration($tokens, $docCommentIndex);
75✔
185

186
            if ([] === $propertyIndices) {
75✔
187
                continue;
49✔
188
            }
189

190
            $typeInfo = $this->resolveApplicableType(
74✔
191
                $propertyIndices,
74✔
192
                $this->getAnnotationsFromDocComment('var', $tokens, $docCommentIndex)
74✔
193
            );
74✔
194

195
            if (null === $typeInfo) {
74✔
196
                continue;
19✔
197
            }
198

199
            $propertyType = $typeInfo['commonType'];
56✔
200
            $isNullable = $typeInfo['isNullable'];
56✔
201

202
            if (\in_array($propertyType, ['callable', 'never', 'void'], true)) {
56✔
203
                continue;
3✔
204
            }
205

206
            if (\in_array($propertyType, $typesToExclude, true)) {
53✔
207
                continue;
6✔
208
            }
209

210
            if (!$this->isValidSyntax(\sprintf(self::TYPE_CHECK_TEMPLATE, $propertyType))) {
49✔
UNCOV
211
                continue;
×
212
            }
213

214
            $newTokens = array_merge(
49✔
215
                $this->createTypeDeclarationTokens($propertyType, $isNullable),
49✔
216
                [new Token([\T_WHITESPACE, ' '])]
49✔
217
            );
49✔
218

219
            $tokensToInsert[current($propertyIndices)] = $newTokens;
49✔
220

221
            $index = max($propertyIndices) + 1;
49✔
222
        }
223

224
        return $tokensToInsert;
75✔
225
    }
226

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

242
        if (!$tokens[$index]->isGivenKind(\T_VARIABLE)) {
75✔
243
            return [];
49✔
244
        }
245

246
        $properties = [];
74✔
247

248
        while (!$tokens[$index]->equals(';')) {
74✔
249
            if ($tokens[$index]->isGivenKind(\T_VARIABLE)) {
74✔
250
                $properties[$tokens[$index]->getContent()] = $index;
74✔
251
            }
252

253
            $index = $tokens->getNextMeaningfulToken($index);
74✔
254
        }
255

256
        return $properties;
74✔
257
    }
258

259
    /**
260
     * @param array<string, int> $propertyIndices
261
     * @param list<Annotation>   $annotations
262
     *
263
     * @return ?_CommonTypeInfo
264
     */
265
    private function resolveApplicableType(array $propertyIndices, array $annotations): ?array
266
    {
267
        $propertyTypes = [];
74✔
268

269
        foreach ($annotations as $annotation) {
74✔
270
            $propertyName = $annotation->getVariableName();
74✔
271

272
            if (null === $propertyName) {
74✔
273
                if (1 !== \count($propertyIndices)) {
70✔
274
                    continue;
3✔
275
                }
276

277
                $propertyName = array_key_first($propertyIndices);
67✔
278
            }
279

280
            if (!isset($propertyIndices[$propertyName])) {
74✔
UNCOV
281
                continue;
×
282
            }
283

284
            $typesExpression = $annotation->getTypeExpression();
74✔
285

286
            if (null === $typesExpression) {
74✔
287
                continue;
3✔
288
            }
289

290
            $typeInfo = $this->getCommonTypeInfo($typesExpression, false);
71✔
291
            $unionTypes = null;
71✔
292

293
            if (null === $typeInfo) {
71✔
294
                $unionTypes = $this->getUnionTypes($typesExpression, false);
13✔
295
            }
296

297
            if (null === $typeInfo && null === $unionTypes) {
71✔
298
                continue;
10✔
299
            }
300

301
            if (null !== $unionTypes) {
62✔
302
                $typeInfo = ['commonType' => $unionTypes, 'isNullable' => false];
3✔
303
            }
304

305
            if (\array_key_exists($propertyName, $propertyTypes) && $typeInfo !== $propertyTypes[$propertyName]) {
62✔
306
                return null;
2✔
307
            }
308

309
            $propertyTypes[$propertyName] = $typeInfo;
62✔
310
        }
311

312
        if (\count($propertyTypes) !== \count($propertyIndices)) {
72✔
313
            return null;
16✔
314
        }
315

316
        $type = array_shift($propertyTypes);
57✔
317

318
        foreach ($propertyTypes as $propertyType) {
57✔
319
            if ($propertyType !== $type) {
3✔
320
                return null;
1✔
321
            }
322
        }
323

324
        return $type;
56✔
325
    }
326
}
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