• 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.78
/src/Fixer/FunctionNotation/PhpdocToParamTypeFixer.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-type _AutogeneratedInputConfiguration array{
29
 *  scalar_types?: bool,
30
 *  types_map?: array<string, string>,
31
 *  union_types?: bool,
32
 * }
33
 * @phpstan-type _AutogeneratedComputedConfiguration array{
34
 *  scalar_types: bool,
35
 *  types_map: array<string, string>,
36
 *  union_types: bool,
37
 * }
38
 *
39
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
40
 *
41
 * @phpstan-import-type _PhpTokenArray from Token
42
 *
43
 * @author Jan Gantzert <jan@familie-gantzert.de>
44
 *
45
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
46
 */
47
final class PhpdocToParamTypeFixer extends AbstractPhpdocToTypeDeclarationFixer implements ConfigurableFixerInterface, ExperimentalFixerInterface
48
{
49
    private const TYPE_CHECK_TEMPLATE = '<?php function f(%s $x) {}';
50

51
    /**
52
     * @var non-empty-list<_PhpTokenArray>
53
     */
54
    private const EXCLUDE_FUNC_NAMES = [
55
        [\T_STRING, '__clone'],
56
        [\T_STRING, '__destruct'],
57
    ];
58

59
    /**
60
     * @var array<string, true>
61
     */
62
    private const SKIPPED_TYPES = [
63
        'resource' => true,
64
        'static' => true,
65
        'void' => true,
66
    ];
67

68
    public function getDefinition(): FixerDefinitionInterface
69
    {
70
        return new FixerDefinition(
3✔
71
            'Takes `@param` annotations of non-mixed types and adjusts accordingly the function signature.',
3✔
72
            [
3✔
73
                new CodeSample(
3✔
74
                    <<<'PHP'
3✔
75
                        <?php
76

77
                        /**
78
                         * @param string $foo
79
                         * @param string|null $bar
80
                         */
81
                        function f($foo, $bar)
82
                        {}
83

84
                        PHP
3✔
85
                ),
3✔
86
                new CodeSample(
3✔
87
                    <<<'PHP'
3✔
88
                        <?php
89

90
                        /** @param Foo $foo */
91
                        function foo($foo) {}
92
                        /** @param string $foo */
93
                        function bar($foo) {}
94

95
                        PHP,
3✔
96
                    ['scalar_types' => false]
3✔
97
                ),
3✔
98
                new CodeSample(
3✔
99
                    <<<'PHP'
3✔
100
                        <?php
101

102
                        /** @param Foo $foo */
103
                        function foo($foo) {}
104
                        /** @param int|string $foo */
105
                        function bar($foo) {}
106

107
                        PHP,
3✔
108
                    ['union_types' => false]
3✔
109
                ),
3✔
110
            ],
3✔
111
            null,
3✔
112
            'The `@param` annotation is mandatory for the fixer to make changes, signatures of methods without it (no docblock, inheritdocs) will not be fixed. Manual actions are required if inherited signatures are not properly documented.'
3✔
113
        );
3✔
114
    }
115

116
    public function isCandidate(Tokens $tokens): bool
117
    {
118
        return $tokens->isAnyTokenKindsFound([\T_FUNCTION, \T_FN]);
83✔
119
    }
120

121
    /**
122
     * {@inheritdoc}
123
     *
124
     * Must run before NoSuperfluousPhpdocTagsFixer, PhpdocAlignFixer.
125
     * Must run after AlignMultilineCommentFixer, CommentToPhpdocFixer, PhpdocIndentFixer, PhpdocScalarFixer, PhpdocToCommentFixer, PhpdocTypesFixer.
126
     */
127
    public function getPriority(): int
128
    {
129
        return 8;
1✔
130
    }
131

132
    protected function isSkippedType(string $type): bool
133
    {
134
        return isset(self::SKIPPED_TYPES[$type]);
71✔
135
    }
136

137
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
138
    {
139
        $tokensToInsert = [];
83✔
140
        $typesToExclude = [];
83✔
141

142
        foreach ($tokens as $index => $token) {
83✔
143
            if ($token->isGivenKind(\T_DOC_COMMENT)) {
83✔
144
                $typesToExclude = array_merge($typesToExclude, self::getTypesToExclude($token->getContent()));
82✔
145

146
                continue;
82✔
147
            }
148

149
            if (!$token->isGivenKind([\T_FUNCTION, \T_FN])) {
83✔
150
                continue;
83✔
151
            }
152

153
            $funcName = $tokens->getNextMeaningfulToken($index);
83✔
154
            if ($tokens[$funcName]->equalsAny(self::EXCLUDE_FUNC_NAMES, false)) {
83✔
UNCOV
155
                continue;
×
156
            }
157

158
            $docCommentIndex = $this->findFunctionDocComment($tokens, $index);
83✔
159

160
            if (null === $docCommentIndex) {
83✔
161
                continue;
3✔
162
            }
163

164
            foreach ($this->getAnnotationsFromDocComment('param', $tokens, $docCommentIndex) as $paramTypeAnnotation) {
82✔
165
                $typesExpression = $paramTypeAnnotation->getTypeExpression();
82✔
166

167
                if (null === $typesExpression) {
82✔
168
                    continue;
8✔
169
                }
170

171
                $typeInfo = $this->getCommonTypeInfo($typesExpression, false);
75✔
172
                $unionTypes = null;
75✔
173

174
                if (null === $typeInfo) {
75✔
175
                    $unionTypes = $this->getUnionTypes($typesExpression, false);
16✔
176
                }
177

178
                if (null === $typeInfo && null === $unionTypes) {
75✔
179
                    continue;
10✔
180
                }
181

182
                if (null !== $typeInfo) {
67✔
183
                    $paramType = $typeInfo['commonType'];
61✔
184
                    $isNullable = $typeInfo['isNullable'];
61✔
185
                } elseif (null !== $unionTypes) {
6✔
186
                    $paramType = $unionTypes;
6✔
187
                    $isNullable = false;
6✔
188
                }
189

190
                if (!isset($paramType, $isNullable)) {
67✔
UNCOV
191
                    continue;
×
192
                }
193

194
                if (\in_array($paramType, $typesToExclude, true)) {
67✔
195
                    continue;
6✔
196
                }
197

198
                $startIndex = $tokens->getNextTokenOfKind($index, ['(']);
63✔
199
                $variableIndex = $this->findCorrectVariable($tokens, $startIndex, $paramTypeAnnotation);
63✔
200

201
                if (null === $variableIndex) {
63✔
202
                    continue;
4✔
203
                }
204

205
                $byRefIndex = $tokens->getPrevMeaningfulToken($variableIndex);
59✔
206
                \assert(\is_int($byRefIndex));
59✔
207

208
                if ($tokens[$byRefIndex]->equals('&')) {
59✔
209
                    $variableIndex = $byRefIndex;
3✔
210
                }
211

212
                if ($this->hasParamTypeHint($tokens, $variableIndex)) {
59✔
213
                    continue;
57✔
214
                }
215

216
                if (!$this->isValidSyntax(\sprintf(self::TYPE_CHECK_TEMPLATE, $paramType))) {
56✔
217
                    continue;
1✔
218
                }
219

220
                $tokensToInsert[$variableIndex] = array_merge(
55✔
221
                    $this->createTypeDeclarationTokens($paramType, $isNullable),
55✔
222
                    [new Token([\T_WHITESPACE, ' '])]
55✔
223
                );
55✔
224
            }
225
        }
226

227
        $tokens->insertSlices($tokensToInsert);
83✔
228
    }
229

230
    protected function createTokensFromRawType(string $type): Tokens
231
    {
232
        $typeTokens = Tokens::fromCode(\sprintf(self::TYPE_CHECK_TEMPLATE, $type));
55✔
233
        $typeTokens->clearRange(0, 4);
55✔
234
        $typeTokens->clearRange(\count($typeTokens) - 6, \count($typeTokens) - 1);
55✔
235
        $typeTokens->clearEmptyTokens();
55✔
236

237
        return $typeTokens;
55✔
238
    }
239

240
    private function findCorrectVariable(Tokens $tokens, int $startIndex, Annotation $paramTypeAnnotation): ?int
241
    {
242
        $endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startIndex);
63✔
243

244
        for ($index = $startIndex + 1; $index < $endIndex; ++$index) {
63✔
245
            if (!$tokens[$index]->isGivenKind(\T_VARIABLE)) {
61✔
246
                continue;
58✔
247
            }
248

249
            $variableName = $tokens[$index]->getContent();
61✔
250

251
            if ($paramTypeAnnotation->getVariableName() === $variableName) {
61✔
252
                return $index;
59✔
253
            }
254
        }
255

256
        return null;
4✔
257
    }
258

259
    /**
260
     * Determine whether the function already has a param type hint.
261
     *
262
     * @param int $index The index of the end of the function definition line, EG at { or ;
263
     */
264
    private function hasParamTypeHint(Tokens $tokens, int $index): bool
265
    {
266
        $prevIndex = $tokens->getPrevMeaningfulToken($index);
59✔
267

268
        return !$tokens[$prevIndex]->equalsAny([',', '(']);
59✔
269
    }
270
}
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