Skip to content

Commit 23fc3c8

Browse files
authored
Replace invalid nullable ? in docblocks with |null verbosely. (#17)
* Replace Nullable ? in docblocks with |null verbosely.
1 parent dce0876 commit 23fc3c8

File tree

6 files changed

+295
-11
lines changed

6 files changed

+295
-11
lines changed

PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@
3232
use SlevomatCodingStandard\Helpers\TypeHintHelper;
3333

3434
/**
35-
* Fixed version of Slevomatic, touching collection objects the right way.
36-
*
37-
* @see https://github.com/slevomat/coding-standard/issues/1296
35+
* Disallows use of `?type` in favor of `type|null`. Reduces conflict or issues with other sniffs.
3836
*/
3937
class DisallowArrayTypeHintSyntaxSniff implements Sniff
4038
{
@@ -66,9 +64,9 @@ public function register(): array
6664
/**
6765
* @inheritDoc
6866
*/
69-
public function process(File $phpcsFile, $docCommentOpenPointer): void
67+
public function process(File $phpcsFile, $pointer): void
7068
{
71-
$annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer);
69+
$annotations = AnnotationHelper::getAnnotations($phpcsFile, $pointer);
7270

7371
foreach ($annotations as $annotation) {
7472
$arrayTypeNodes = $this->getArrayTypeNodes($annotation->getValue());
@@ -88,22 +86,22 @@ public function process(File $phpcsFile, $docCommentOpenPointer): void
8886
}
8987

9088
/** @var \SlevomatCodingStandard\Helpers\ParsedDocComment $parsedDocComment */
91-
$parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $docCommentOpenPointer);
89+
$parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $pointer);
9290

9391
/** @var list<\PHPStan\PhpDocParser\Ast\Type\UnionTypeNode> $unionTypeNodes */
9492
$unionTypeNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), UnionTypeNode::class);
9593
$unionTypeNode = $this->findUnionTypeThatContainsArrayType($arrayTypeNode, $unionTypeNodes);
9694

9795
if ($unionTypeNode !== null) {
9896
if ($this->isUnionTypeGenericObjectCollection($unionTypeNodes[0])) {
99-
$this->fixGenericObjectCollection($phpcsFile, $annotation, $docCommentOpenPointer, $arrayTypeNode, $unionTypeNodes);
97+
$this->fixGenericObjectCollection($phpcsFile, $annotation, $pointer, $arrayTypeNode, $unionTypeNodes);
10098

10199
continue;
102100
}
103101

104102
$genericIdentifier = $this->findGenericIdentifier(
105103
$phpcsFile,
106-
$docCommentOpenPointer,
104+
$pointer,
107105
$unionTypeNode,
108106
$annotation->getValue(),
109107
);
@@ -136,7 +134,7 @@ public function process(File $phpcsFile, $docCommentOpenPointer): void
136134
} else {
137135
$genericIdentifier = $this->findGenericIdentifier(
138136
$phpcsFile,
139-
$docCommentOpenPointer,
137+
$pointer,
140138
$arrayTypeNode,
141139
$annotation->getValue(),
142140
) ?? 'array';
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
<?php
2+
3+
/**
4+
* MIT License
5+
* For full license information, please view the LICENSE file that was distributed with this source code.
6+
*/
7+
8+
namespace PhpCollective\Sniffs\Commenting;
9+
10+
use PHP_CodeSniffer\Files\File;
11+
use PHP_CodeSniffer\Sniffs\Sniff;
12+
use PhpCollective\Traits\CommentingTrait;
13+
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
14+
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
15+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
16+
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
17+
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
18+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
19+
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
20+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
21+
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
22+
use PHPStan\PhpDocParser\Printer\Printer;
23+
24+
/**
25+
* Disallows use of `?type` in favor of `type|null`. Reduces conflict or issues with other sniffs.
26+
*/
27+
class DisallowShorthandNullableTypeHintSniff implements Sniff
28+
{
29+
use CommentingTrait;
30+
31+
/**
32+
* @var string
33+
*/
34+
public const CODE_DISALLOWED_SHORTHAND_TYPE_HINT = 'DisallowedShorthandTypeHint';
35+
36+
/**
37+
* @inheritDoc
38+
*/
39+
public function register(): array
40+
{
41+
return [
42+
T_DOC_COMMENT_STRING,
43+
];
44+
}
45+
46+
/**
47+
* @inheritDoc
48+
*/
49+
public function process(File $phpcsFile, $pointer): void
50+
{
51+
$tokens = $phpcsFile->getTokens();
52+
$docCommentContent = $tokens[$pointer]['content'];
53+
54+
/** @var \PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\TypelessParamTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode $valueNode */
55+
$valueNode = static::getValueNode($tokens[$pointer - 2]['content'], $docCommentContent);
56+
57+
$printer = new Printer();
58+
$before = $printer->print($valueNode);
59+
60+
// Check if the value node is invalid and handle it
61+
if ($valueNode instanceof InvalidTagValueNode) {
62+
// Attempt to clean up and process invalid types
63+
$fixedNode = $this->fixInvalidTagValueNode($valueNode);
64+
if ($fixedNode) {
65+
$valueNode = $fixedNode;
66+
}
67+
}
68+
69+
if ($valueNode instanceof InvalidTagValueNode) {
70+
return;
71+
}
72+
73+
// Traverse and fix the nullable types
74+
$this->traversePhpDocNode($valueNode);
75+
76+
$after = $printer->print($valueNode);
77+
78+
if ($after === $before) {
79+
return;
80+
}
81+
82+
$message = sprintf('Shorthand nullable `%s` invalid, use `%s` instead.', $before, $after);
83+
$fixable = $phpcsFile->addFixableError($message, $pointer, static::CODE_DISALLOWED_SHORTHAND_TYPE_HINT);
84+
if ($fixable) {
85+
$phpcsFile->fixer->beginChangeset();
86+
$phpcsFile->fixer->replaceToken($pointer, $after);
87+
$phpcsFile->fixer->endChangeset();
88+
}
89+
}
90+
91+
/**
92+
* Attempt to fix an InvalidTagValueNode by parsing and correcting the types manually.
93+
*
94+
* @param \PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode $invalidNode
95+
*
96+
* @return \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode|null
97+
*/
98+
protected function fixInvalidTagValueNode(InvalidTagValueNode $invalidNode): ?PhpDocTagValueNode
99+
{
100+
$value = $invalidNode->value;
101+
$rest = '';
102+
if (str_contains($value, '$')) {
103+
$string = trim(substr($value, 0, (int)strpos($value, '$')));
104+
$rest = trim(substr($value, strlen($string)));
105+
$value = $string;
106+
}
107+
108+
// Try to parse and correct the invalid node's type (e.g., `?string|null`)
109+
if (str_contains($value, '|')) {
110+
// Split the types
111+
$types = explode('|', $value);
112+
113+
$transformedTypes = [];
114+
$hasNullable = false;
115+
116+
foreach ($types as $type) {
117+
$type = trim($type);
118+
119+
// Handle `?Type` shorthand
120+
if (str_starts_with($type, '?')) {
121+
$type = substr($type, 1); // Remove leading '?'
122+
$transformedTypes[] = new IdentifierTypeNode($type);
123+
$hasNullable = true; // Mark as nullable
124+
} elseif (strtolower($type) === 'null') {
125+
// If 'null' is encountered, mark as nullable but don't add now
126+
$hasNullable = true;
127+
} else {
128+
$transformedTypes[] = new IdentifierTypeNode($type);
129+
}
130+
}
131+
132+
// Add `null` at the end if the type is nullable
133+
if ($hasNullable) {
134+
$transformedTypes[] = new IdentifierTypeNode('null');
135+
}
136+
137+
// Create a new UnionTypeNode with the transformed types
138+
return new ParamTagValueNode(
139+
new UnionTypeNode($transformedTypes),
140+
false,
141+
$rest,
142+
'',
143+
);
144+
}
145+
146+
return null;
147+
}
148+
149+
/**
150+
* Traverse and transform the PHPDoc AST.
151+
*
152+
* @param \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode $phpDocNode
153+
*
154+
* @return void
155+
*/
156+
protected function traversePhpDocNode(PhpDocTagValueNode $phpDocNode): void
157+
{
158+
if (
159+
$phpDocNode instanceof ParamTagValueNode
160+
|| $phpDocNode instanceof ReturnTagValueNode
161+
|| $phpDocNode instanceof VarTagValueNode
162+
) {
163+
echo PHP_EOL . 'processing...' . PHP_EOL;
164+
$phpDocNode->type = $this->transformNullableType($phpDocNode->type);
165+
}
166+
167+
echo PHP_EOL . PHP_EOL;
168+
}
169+
170+
/**
171+
* Traverse and transform nullable types.
172+
*
173+
* @param \PHPStan\PhpDocParser\Ast\Type\TypeNode $typeNode
174+
*
175+
* @return \PHPStan\PhpDocParser\Ast\Type\TypeNode
176+
*/
177+
protected function transformNullableType(TypeNode $typeNode): TypeNode
178+
{
179+
if ($typeNode instanceof NullableTypeNode) {
180+
$innerType = $typeNode->type;
181+
182+
// Convert `?Type` to `Type|null`
183+
return new UnionTypeNode([
184+
$innerType,
185+
new IdentifierTypeNode('null'),
186+
]);
187+
}
188+
189+
// Handle UnionTypeNode (e.g., `Type|null`)
190+
if ($typeNode instanceof UnionTypeNode) {
191+
$transformedTypes = [];
192+
foreach ($typeNode->types as $subType) {
193+
$transformedTypes[] = $this->transformNullableType($subType); // Recursively transform
194+
}
195+
196+
return new UnionTypeNode($transformedTypes);
197+
}
198+
199+
return $typeNode;
200+
}
201+
}

docs/sniffs.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# PhpCollective Code Sniffer
22

33

4-
The PhpCollectiveStrict standard contains 216 sniffs
4+
The PhpCollectiveStrict standard contains 217 sniffs
55

66
Generic (25 sniffs)
77
-------------------
@@ -38,7 +38,7 @@ PEAR (4 sniffs)
3838
- PEAR.Functions.ValidDefaultValue
3939
- PEAR.NamingConventions.ValidClassName
4040

41-
PhpCollective (79 sniffs)
41+
PhpCollective (80 sniffs)
4242
-------------------------
4343
- PhpCollective.Arrays.DisallowImplicitArrayCreation
4444
- PhpCollective.Classes.ClassFileName
@@ -52,6 +52,7 @@ PhpCollective (79 sniffs)
5252
- PhpCollective.Classes.SelfAccessor
5353
- PhpCollective.Commenting.Attributes
5454
- PhpCollective.Commenting.DisallowArrayTypeHintSyntax
55+
- PhpCollective.Commenting.DisallowShorthandNullableTypeHint
5556
- PhpCollective.Commenting.DocBlock
5657
- PhpCollective.Commenting.DocBlockConst
5758
- PhpCollective.Commenting.DocBlockConstructor
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/**
4+
* MIT License
5+
* For full license information, please view the LICENSE file that was distributed with this source code.
6+
*/
7+
8+
namespace PhpCollective\Test\PhpCollective\Sniffs\Commenting;
9+
10+
use PhpCollective\Sniffs\Commenting\DisallowShorthandNullableTypeHintSniff;
11+
use PhpCollective\Test\TestCase;
12+
13+
class DisallowShorthandNullableTypeHintSniffTest extends TestCase
14+
{
15+
/**
16+
* @return void
17+
*/
18+
public function testDocBlockConstSniffer(): void
19+
{
20+
$this->assertSnifferFindsErrors(new DisallowShorthandNullableTypeHintSniff(), 5);
21+
}
22+
23+
/**
24+
* @return void
25+
*/
26+
public function testDocBlockConstFixer(): void
27+
{
28+
$this->assertSnifferCanFixErrors(new DisallowShorthandNullableTypeHintSniff());
29+
}
30+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PhpCollective;
4+
5+
class FixMe
6+
{
7+
/**
8+
* @var string|null Some Comment
9+
*/
10+
protected $string1 = null;
11+
12+
/**
13+
* @var string|int|null
14+
*/
15+
protected $string2 = null;
16+
17+
/**
18+
* @param string|null $string1
19+
* @param string|null $string2
20+
*
21+
* @return string|null Some Comment
22+
*/
23+
public function doSth(?string $string1, ?string $string2 = null): ?string
24+
{
25+
return $string1 ?: $string2;
26+
}
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PhpCollective;
4+
5+
class FixMe
6+
{
7+
/**
8+
* @var ?string Some Comment
9+
*/
10+
protected $string1 = null;
11+
12+
/**
13+
* @var ?string|int
14+
*/
15+
protected $string2 = null;
16+
17+
/**
18+
* @param ?string $string1
19+
* @param ?string|null $string2
20+
*
21+
* @return ?string Some Comment
22+
*/
23+
public function doSth(?string $string1, ?string $string2 = null): ?string
24+
{
25+
return $string1 ?: $string2;
26+
}
27+
}

0 commit comments

Comments
 (0)