Skip to content

Commit 980f00c

Browse files
committed
Replace Nullable ? in docblocks with |null verbosely.
1 parent dce0876 commit 980f00c

File tree

5 files changed

+245
-9
lines changed

5 files changed

+245
-9
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: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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\TypelessParamTagValueNode;
18+
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
19+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
20+
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
21+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
22+
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
23+
use PHPStan\PhpDocParser\Printer\Printer;
24+
25+
/**
26+
* Disallows use of `?type` in favor of `type|null`. Reduces conflict or issues with other sniffs.
27+
*/
28+
class DisallowShorthandNullableTypeHintSniff implements Sniff
29+
{
30+
use CommentingTrait;
31+
32+
/**
33+
* @var string
34+
*/
35+
public const CODE_DISALLOWED_SHORTHAND_TYPE_HINT = 'DisallowedShorthandTypeHint';
36+
37+
/**
38+
* @inheritDoc
39+
*/
40+
public function register(): array
41+
{
42+
return [
43+
T_DOC_COMMENT_STRING,
44+
];
45+
}
46+
47+
/**
48+
* @inheritDoc
49+
*/
50+
public function process(File $phpcsFile, $pointer): void
51+
{
52+
$tokens = $phpcsFile->getTokens();
53+
$docCommentContent = $tokens[$pointer]['content'];
54+
55+
/** @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\PropertyTagValueNode $valueNode */
56+
$valueNode = static::getValueNode($tokens[$pointer - 2]['content'], $docCommentContent);
57+
if ($valueNode instanceof InvalidTagValueNode || $valueNode instanceof TypelessParamTagValueNode) {
58+
return;
59+
}
60+
61+
$printer = new Printer();
62+
$before = $printer->print($valueNode);
63+
// Traverse and fix the nullable types
64+
$this->traversePhpDocNode($valueNode);
65+
66+
$after = $printer->print($valueNode);
67+
68+
if ($after === $before) {
69+
return;
70+
}
71+
72+
$message = sprintf('Shorthand nullable `%s` invalid, use `%s` instead.', $before, $after);
73+
$fixable = $phpcsFile->addFixableError($message, $pointer, static::CODE_DISALLOWED_SHORTHAND_TYPE_HINT);
74+
if ($fixable) {
75+
$phpcsFile->fixer->beginChangeset();
76+
$phpcsFile->fixer->replaceToken($pointer, $after);
77+
$phpcsFile->fixer->endChangeset();
78+
}
79+
}
80+
81+
/**
82+
* Traverse and transform the PHPDoc AST.
83+
*
84+
* @param \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode $phpDocNode
85+
*
86+
* @return void
87+
*/
88+
protected function traversePhpDocNode(PhpDocTagValueNode $phpDocNode): void
89+
{
90+
if (
91+
$phpDocNode instanceof ParamTagValueNode
92+
|| $phpDocNode instanceof ReturnTagValueNode
93+
|| $phpDocNode instanceof VarTagValueNode
94+
) {
95+
// Traverse the type node recursively
96+
$phpDocNode->type = $this->transformNullableType($phpDocNode->type);
97+
}
98+
}
99+
100+
/**
101+
* Traverse and transform nullable types.
102+
*
103+
* @param \PHPStan\PhpDocParser\Ast\Type\TypeNode $typeNode
104+
*
105+
* @return \PHPStan\PhpDocParser\Ast\Type\TypeNode
106+
*/
107+
protected function transformNullableType(TypeNode $typeNode): TypeNode
108+
{
109+
if ($typeNode instanceof NullableTypeNode) {
110+
$innerType = $typeNode->type;
111+
112+
// Convert `?Type` to `Type|null`
113+
return new UnionTypeNode([
114+
$innerType,
115+
new IdentifierTypeNode('null'),
116+
]);
117+
}
118+
119+
// Recursively handle UnionTypeNode (e.g., `Type|null`)
120+
if ($typeNode instanceof UnionTypeNode) {
121+
// Traverse each type in the union and transform nullable types
122+
foreach ($typeNode->types as &$subType) {
123+
$subType = $this->transformNullableType($subType);
124+
}
125+
126+
return $typeNode;
127+
}
128+
129+
// Recursively handle other nodes that might contain nested types
130+
if (property_exists($typeNode, 'types') && is_array($typeNode->types)) {
131+
foreach ($typeNode->types as &$subType) {
132+
$subType = $this->transformNullableType($subType);
133+
}
134+
}
135+
136+
return $typeNode;
137+
}
138+
139+
/**
140+
* @param array<string> $types
141+
*
142+
* @return bool
143+
*/
144+
protected function containsShorthand(array $types): bool
145+
{
146+
foreach ($types as $type) {
147+
if (str_starts_with($type, '?')) {
148+
return true;
149+
}
150+
}
151+
152+
return false;
153+
}
154+
}
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(), 3);
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)