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

keradus / PHP-CS-Fixer / 16018263876

02 Jul 2025 06:58AM UTC coverage: 94.846% (-0.002%) from 94.848%
16018263876

push

github

keradus
debug2

28193 of 29725 relevant lines covered (94.85%)

45.34 hits per line

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

99.22
/src/Fixer/LanguageConstruct/NullableTypeDeclarationFixer.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\LanguageConstruct;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
20
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
22
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
23
use PhpCsFixer\FixerDefinition\FixerDefinition;
24
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
25
use PhpCsFixer\FixerDefinition\VersionSpecification;
26
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
27
use PhpCsFixer\Preg;
28
use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
29
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
30
use PhpCsFixer\Tokenizer\CT;
31
use PhpCsFixer\Tokenizer\FCT;
32
use PhpCsFixer\Tokenizer\Token;
33
use PhpCsFixer\Tokenizer\Tokens;
34
use PhpCsFixer\Tokenizer\TokensAnalyzer;
35

36
/**
37
 * @author John Paul E. Balandan, CPA <paulbalandan@gmail.com>
38
 *
39
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
40
 *
41
 * @phpstan-type _AutogeneratedInputConfiguration array{
42
 *  syntax?: 'question_mark'|'union',
43
 * }
44
 * @phpstan-type _AutogeneratedComputedConfiguration array{
45
 *  syntax: 'question_mark'|'union',
46
 * }
47
 */
48
final class NullableTypeDeclarationFixer extends AbstractFixer implements ConfigurableFixerInterface
49
{
50
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
51
    use ConfigurableFixerTrait;
52

53
    private const OPTION_SYNTAX_UNION = 'union';
54
    private const OPTION_SYNTAX_QUESTION_MARK = 'question_mark';
55
    private const PROPERTY_MODIFIERS = [T_PRIVATE, T_PROTECTED, T_PUBLIC, T_STATIC, T_VAR, FCT::T_READONLY, FCT::T_PRIVATE_SET, FCT::T_PROTECTED_SET, FCT::T_PUBLIC_SET];
56

57
    private int $candidateTokenKind;
58

59
    public function getDefinition(): FixerDefinitionInterface
60
    {
61
        return new FixerDefinition(
3✔
62
            'Nullable single type declaration should be standardised using configured syntax.',
3✔
63
            [
3✔
64
                new VersionSpecificCodeSample(
3✔
65
                    "<?php\nfunction bar(null|int \$value, null|\\Closure \$callable): int|null {}\n",
3✔
66
                    new VersionSpecification(8_00_00)
3✔
67
                ),
3✔
68
                new VersionSpecificCodeSample(
3✔
69
                    "<?php\nfunction baz(?int \$value, ?\\stdClass \$obj, ?array \$config): ?int {}\n",
3✔
70
                    new VersionSpecification(8_00_00),
3✔
71
                    ['syntax' => self::OPTION_SYNTAX_UNION]
3✔
72
                ),
3✔
73
                new VersionSpecificCodeSample(
3✔
74
                    '<?php
3✔
75
class ValueObject
76
{
77
    public null|string $name;
78
    public ?int $count;
79
    public null|bool $internal;
80
    public null|\Closure $callback;
81
}
82
',
3✔
83
                    new VersionSpecification(8_00_00),
3✔
84
                    ['syntax' => self::OPTION_SYNTAX_QUESTION_MARK]
3✔
85
                ),
3✔
86
            ]
3✔
87
        );
3✔
88
    }
89

90
    public function isCandidate(Tokens $tokens): bool
91
    {
92
        return \PHP_VERSION_ID >= 8_00_00 && $tokens->isTokenKindFound($this->candidateTokenKind);
23✔
93
    }
94

95
    /**
96
     * {@inheritdoc}
97
     *
98
     * Must run before OrderedTypesFixer, TypesSpacesFixer.
99
     * Must run after NullableTypeDeclarationForDefaultNullValueFixer.
100
     */
101
    public function getPriority(): int
102
    {
103
        return 2;
1✔
104
    }
105

106
    protected function configurePostNormalisation(): void
107
    {
108
        $this->candidateTokenKind = self::OPTION_SYNTAX_QUESTION_MARK === $this->configuration['syntax']
32✔
109
            ? CT::T_TYPE_ALTERNATION // `|` -> `?`
32✔
110
            : CT::T_NULLABLE_TYPE; // `?` -> `|`
13✔
111
    }
112

113
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
114
    {
115
        return new FixerConfigurationResolver([
32✔
116
            (new FixerOptionBuilder('syntax', 'Whether to use question mark (`?`) or explicit `null` union for nullable type.'))
32✔
117
                ->setAllowedValues([self::OPTION_SYNTAX_UNION, self::OPTION_SYNTAX_QUESTION_MARK])
32✔
118
                ->setDefault(self::OPTION_SYNTAX_QUESTION_MARK)
32✔
119
                ->getOption(),
32✔
120
        ]);
32✔
121
    }
122

123
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
124
    {
125
        $functionsAnalyzer = new FunctionsAnalyzer();
21✔
126

127
        foreach (array_reverse($this->getElements($tokens), true) as $index => $type) {
21✔
128
            if ('property' === $type) {
21✔
129
                $this->normalizePropertyType($tokens, $index);
10✔
130

131
                continue;
10✔
132
            }
133

134
            $this->normalizeMethodReturnType($functionsAnalyzer, $tokens, $index);
13✔
135
            $this->normalizeMethodArgumentType($functionsAnalyzer, $tokens, $index);
13✔
136
        }
137
    }
138

139
    /**
140
     * @return array<int, string>
141
     *
142
     * @phpstan-return array<int, 'function'|'property'>
143
     */
144
    private function getElements(Tokens $tokens): array
145
    {
146
        $tokensAnalyzer = new TokensAnalyzer($tokens);
21✔
147

148
        $elements = array_map(
21✔
149
            static fn (array $element): string => 'method' === $element['type'] ? 'function' : $element['type'],
21✔
150
            array_filter(
21✔
151
                $tokensAnalyzer->getClassyElements(),
21✔
152
                static fn (array $element): bool => \in_array($element['type'], ['method', 'property'], true)
21✔
153
            )
21✔
154
        );
21✔
155

156
        foreach ($tokens as $index => $token) {
21✔
157
            if (
158
                $token->isGivenKind(T_FN)
21✔
159
                || ($token->isGivenKind(T_FUNCTION) && !isset($elements[$index]))
21✔
160
            ) {
161
                $elements[$index] = 'function';
9✔
162
            }
163
        }
164

165
        return $elements;
21✔
166
    }
167

168
    private function collectTypeAnalysis(Tokens $tokens, int $startIndex, int $endIndex): ?TypeAnalysis
169
    {
170
        $type = '';
10✔
171
        $typeStartIndex = $tokens->getNextMeaningfulToken($startIndex);
10✔
172
        $typeEndIndex = $typeStartIndex;
10✔
173

174
        for ($i = $typeStartIndex; $i < $endIndex; ++$i) {
10✔
175
            if ($tokens[$i]->isWhitespace() || $tokens[$i]->isComment()) {
10✔
176
                continue;
10✔
177
            }
178

179
            $type .= $tokens[$i]->getContent();
10✔
180
            $typeEndIndex = $i;
10✔
181
        }
182

183
        return '' !== $type ? new TypeAnalysis($type, $typeStartIndex, $typeEndIndex) : null;
10✔
184
    }
185

186
    private function isTypeNormalizable(TypeAnalysis $typeAnalysis): bool
187
    {
188
        $type = $typeAnalysis->getName();
21✔
189

190
        if ('null' === strtolower($type) || !$typeAnalysis->isNullable()) {
21✔
191
            return false;
9✔
192
        }
193

194
        if (str_contains($type, '&')) {
21✔
195
            return false; // skip DNF types
1✔
196
        }
197

198
        if (!str_contains($type, '|')) {
20✔
199
            return true;
13✔
200
        }
201

202
        return 1 === substr_count($type, '|') && Preg::match('/(?:\|null$|^null\|)/i', $type);
9✔
203
    }
204

205
    private function normalizePropertyType(Tokens $tokens, int $index): void
206
    {
207
        $propertyEndIndex = $index;
10✔
208
        do {
209
            $index = $tokens->getPrevMeaningfulToken($index);
10✔
210
        } while (!$tokens[$index]->isGivenKind(self::PROPERTY_MODIFIERS));
10✔
211

212
        $propertyType = $this->collectTypeAnalysis($tokens, $index, $propertyEndIndex);
10✔
213

214
        if (null === $propertyType || !$this->isTypeNormalizable($propertyType)) {
10✔
215
            return;
1✔
216
        }
217

218
        $this->normalizeNullableType($tokens, $propertyType);
9✔
219
    }
220

221
    private function normalizeMethodArgumentType(FunctionsAnalyzer $functionsAnalyzer, Tokens $tokens, int $index): void
222
    {
223
        foreach (array_reverse($functionsAnalyzer->getFunctionArguments($tokens, $index), true) as $argumentInfo) {
13✔
224
            $argumentType = $argumentInfo->getTypeAnalysis();
13✔
225

226
            if (null === $argumentType || !$this->isTypeNormalizable($argumentType)) {
13✔
227
                continue;
4✔
228
            }
229

230
            $this->normalizeNullableType($tokens, $argumentType);
12✔
231
        }
232
    }
233

234
    private function normalizeMethodReturnType(FunctionsAnalyzer $functionsAnalyzer, Tokens $tokens, int $index): void
235
    {
236
        $returnType = $functionsAnalyzer->getFunctionReturnType($tokens, $index);
13✔
237

238
        if (null === $returnType || !$this->isTypeNormalizable($returnType)) {
13✔
239
            return;
7✔
240
        }
241

242
        $this->normalizeNullableType($tokens, $returnType);
6✔
243
    }
244

245
    private function normalizeNullableType(Tokens $tokens, TypeAnalysis $typeAnalysis): void
246
    {
247
        $type = $typeAnalysis->getName();
19✔
248

249
        if (!str_contains($type, '|') && !str_contains($type, '&')) {
19✔
250
            $type = ($typeAnalysis->isNullable() ? '?' : '').$type;
13✔
251
        }
252

253
        $isQuestionMarkSyntax = self::OPTION_SYNTAX_QUESTION_MARK === $this->configuration['syntax'];
19✔
254

255
        if ($isQuestionMarkSyntax) {
19✔
256
            $normalizedType = $this->convertToNullableType($type);
8✔
257
            $normalizedTypeAsString = implode('', $normalizedType);
8✔
258
        } else {
259
            $normalizedType = $this->convertToExplicitUnionType($type);
12✔
260
            $normalizedTypeAsString = implode('|', $normalizedType);
12✔
261
        }
262

263
        if ($normalizedTypeAsString === $type) {
19✔
264
            return; // nothing to fix
2✔
265
        }
266

267
        $tokens->overrideRange(
19✔
268
            $typeAnalysis->getStartIndex(),
19✔
269
            $typeAnalysis->getEndIndex(),
19✔
270
            $this->createTypeDeclarationTokens($normalizedType, $isQuestionMarkSyntax)
19✔
271
        );
19✔
272

273
        $prevStartIndex = $typeAnalysis->getStartIndex() - 1;
19✔
274
        if (!$tokens[$prevStartIndex]->isWhitespace() && !$tokens[$prevStartIndex]->equals('(')) {
19✔
275
            $tokens->ensureWhitespaceAtIndex($prevStartIndex, 1, ' ');
1✔
276
        }
277
    }
278

279
    /**
280
     * @return list<string>
281
     */
282
    private function convertToNullableType(string $type): array
283
    {
284
        if (str_starts_with($type, '?')) {
8✔
285
            return [$type]; // no need to convert; already fixed
2✔
286
        }
287

288
        return ['?', Preg::replace('/(?:\|null$|^null\|)/i', '', $type)];
8✔
289
    }
290

291
    /**
292
     * @return list<string>
293
     */
294
    private function convertToExplicitUnionType(string $type): array
295
    {
296
        if (str_contains($type, '|')) {
12✔
297
            return [$type]; // no need to convert; already fixed
×
298
        }
299

300
        return ['null', substr($type, 1)];
12✔
301
    }
302

303
    /**
304
     * @param list<string> $types
305
     *
306
     * @return list<Token>
307
     */
308
    private function createTypeDeclarationTokens(array $types, bool $isQuestionMarkSyntax): array
309
    {
310
        static $specialTypes = [
19✔
311
            '?' => CT::T_NULLABLE_TYPE,
19✔
312
            'array' => CT::T_ARRAY_TYPEHINT,
19✔
313
            'callable' => T_CALLABLE,
19✔
314
            'static' => T_STATIC,
19✔
315
        ];
19✔
316

317
        $count = \count($types);
19✔
318
        $newTokens = [];
19✔
319

320
        foreach ($types as $index => $type) {
19✔
321
            if (isset($specialTypes[strtolower($type)])) {
19✔
322
                $newTokens[] = new Token([$specialTypes[strtolower($type)], $type]);
14✔
323
            } else {
324
                foreach (explode('\\', $type) as $nsIndex => $value) {
18✔
325
                    if (0 === $nsIndex && '' === $value) {
18✔
326
                        continue;
4✔
327
                    }
328

329
                    if ($nsIndex > 0) {
18✔
330
                        $newTokens[] = new Token([T_NS_SEPARATOR, '\\']);
4✔
331
                    }
332

333
                    $newTokens[] = new Token([T_STRING, $value]);
18✔
334
                }
335
            }
336

337
            if ($index <= $count - 2 && !$isQuestionMarkSyntax) {
19✔
338
                $newTokens[] = new Token([CT::T_TYPE_ALTERNATION, '|']);
12✔
339
            }
340
        }
341

342
        return $newTokens;
19✔
343
    }
344
}
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