Skip to content

Commit fabb4fa

Browse files
committed
PropertyTypeHintSniff: Improved support for union types
1 parent 47bbe49 commit fabb4fa

File tree

5 files changed

+106
-31
lines changed

5 files changed

+106
-31
lines changed

SlevomatCodingStandard/Sniffs/TypeHints/PropertyTypeHintSniff.php

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHP_CodeSniffer\Sniffs\Sniff;
77
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
88
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
9+
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
910
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
1011
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
1112
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
@@ -23,6 +24,8 @@
2324
use SlevomatCodingStandard\Helpers\TokenHelper;
2425
use SlevomatCodingStandard\Helpers\TypeHintHelper;
2526
use function array_map;
27+
use function array_unique;
28+
use function array_values;
2629
use function count;
2730
use function in_array;
2831
use function sprintf;
@@ -136,15 +139,7 @@ private function checkTypeHint(
136139

137140
$typeNode = $propertyAnnotation->getType();
138141

139-
$annotationContainsOneType = AnnotationTypeHelper::containsOneType($typeNode);
140-
if (
141-
!$annotationContainsOneType
142-
&& !AnnotationTypeHelper::containsJustTwoTypes($typeNode)
143-
) {
144-
return;
145-
}
146-
147-
if ($annotationContainsOneType) {
142+
if (AnnotationTypeHelper::containsOneType($typeNode)) {
148143
/** @var ArrayTypeNode|ArrayShapeNode|GenericTypeNode|IdentifierTypeNode|ThisTypeNode $typeNode */
149144
$typeNode = $typeNode;
150145
$possibleTypeHint = $typeNode instanceof ArrayTypeNode || $typeNode instanceof ArrayShapeNode
@@ -153,35 +148,67 @@ private function checkTypeHint(
153148
$nullableTypeHint = false;
154149

155150
} else {
156-
/** @var UnionTypeNode|IntersectionTypeNode $typeNode */
157-
$typeNode = $typeNode;
151+
$possibleTypeHint = null;
152+
$nullableTypeHint = false;
158153

159-
if (
160-
!AnnotationTypeHelper::containsNullType($typeNode)
161-
&& !AnnotationTypeHelper::containsTraversableType($typeNode, $phpcsFile, $propertyPointer, $this->getTraversableTypeHints())
162-
) {
163-
return;
164-
}
154+
if ($typeNode instanceof UnionTypeNode && !AnnotationTypeHelper::containsJustTwoTypes($typeNode)) {
155+
$typeHints = [];
156+
foreach ($typeNode->types as $innerTypeNode) {
157+
if (!($innerTypeNode instanceof CallableTypeNode
158+
|| $innerTypeNode instanceof GenericTypeNode
159+
|| $innerTypeNode instanceof IdentifierTypeNode
160+
|| $innerTypeNode instanceof ThisTypeNode)
161+
) {
162+
return;
163+
}
164+
165+
$typeHints[] = AnnotationTypeHelper::getTypeHintFromOneType($innerTypeNode);
166+
}
167+
168+
$typeHints = array_values(array_unique($typeHints));
165169

166-
if (AnnotationTypeHelper::containsNullType($typeNode)) {
167-
/** @var ArrayTypeNode|ArrayShapeNode|IdentifierTypeNode|ThisTypeNode|GenericTypeNode $notNullTypeHintNode */
168-
$notNullTypeHintNode = AnnotationTypeHelper::getTypeFromNullableType($typeNode);
169-
$possibleTypeHint = $notNullTypeHintNode instanceof ArrayTypeNode || $notNullTypeHintNode instanceof ArrayShapeNode
170-
? 'array'
171-
: AnnotationTypeHelper::getTypeHintFromOneType($notNullTypeHintNode);
172-
$nullableTypeHint = true;
173-
} else {
174-
$itemsSpecificationTypeHint = AnnotationTypeHelper::getItemsSpecificationTypeFromType($typeNode, $this->getTraversableTypeHints());
175-
if (!$itemsSpecificationTypeHint instanceof ArrayTypeNode) {
170+
if (count($typeHints) === 1) {
171+
$possibleTypeHint = $typeHints[0];
172+
$nullableTypeHint = false;
173+
} elseif (count($typeHints) === 2 && ($typeHints[0] === 'null' || $typeHints[1] === 'null')) {
174+
$possibleTypeHint = $typeHints[0] === 'null' ? $typeHints[1] : $typeHints[0];
175+
$nullableTypeHint = true;
176+
} else {
176177
return;
177178
}
179+
}
178180

179-
$possibleTypeHint = AnnotationTypeHelper::getTraversableTypeHintFromType($typeNode, $this->getTraversableTypeHints());
180-
$nullableTypeHint = false;
181+
if ($possibleTypeHint === null) {
182+
/** @var UnionTypeNode|IntersectionTypeNode $typeNode */
183+
$typeNode = $typeNode;
181184

182-
if (!TypeHintHelper::isTraversableType(TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $propertyPointer, $possibleTypeHint), $this->getTraversableTypeHints())) {
185+
if (
186+
!AnnotationTypeHelper::containsNullType($typeNode)
187+
&& !AnnotationTypeHelper::containsTraversableType($typeNode, $phpcsFile, $propertyPointer, $this->getTraversableTypeHints())
188+
) {
183189
return;
184190
}
191+
192+
if (AnnotationTypeHelper::containsNullType($typeNode)) {
193+
/** @var ArrayTypeNode|ArrayShapeNode|IdentifierTypeNode|ThisTypeNode|GenericTypeNode $notNullTypeHintNode */
194+
$notNullTypeHintNode = AnnotationTypeHelper::getTypeFromNullableType($typeNode);
195+
$possibleTypeHint = $notNullTypeHintNode instanceof ArrayTypeNode || $notNullTypeHintNode instanceof ArrayShapeNode
196+
? 'array'
197+
: AnnotationTypeHelper::getTypeHintFromOneType($notNullTypeHintNode);
198+
$nullableTypeHint = true;
199+
} else {
200+
$itemsSpecificationTypeHint = AnnotationTypeHelper::getItemsSpecificationTypeFromType($typeNode, $this->getTraversableTypeHints());
201+
if (!$itemsSpecificationTypeHint instanceof ArrayTypeNode) {
202+
return;
203+
}
204+
205+
$possibleTypeHint = AnnotationTypeHelper::getTraversableTypeHintFromType($typeNode, $this->getTraversableTypeHints());
206+
$nullableTypeHint = false;
207+
208+
if (!TypeHintHelper::isTraversableType(TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $propertyPointer, $possibleTypeHint), $this->getTraversableTypeHints())) {
209+
return;
210+
}
211+
}
185212
}
186213
}
187214

tests/Sniffs/TypeHints/PropertyTypeHintSniffTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function testEnabledNativeErrors(): void
4949
'traversableTypeHints' => ['Traversable'],
5050
]);
5151

52-
self::assertSame(25, $report->getErrorCount());
52+
self::assertSame(28, $report->getErrorCount());
5353

5454
self::assertSniffError($report, 6, PropertyTypeHintSniff::CODE_MISSING_ANY_TYPE_HINT);
5555
self::assertSniffError($report, 11, PropertyTypeHintSniff::CODE_MISSING_NATIVE_TYPE_HINT);
@@ -76,6 +76,9 @@ public function testEnabledNativeErrors(): void
7676
self::assertSniffError($report, 94, PropertyTypeHintSniff::CODE_USELESS_ANNOTATION);
7777
self::assertSniffError($report, 99, PropertyTypeHintSniff::CODE_MISSING_NATIVE_TYPE_HINT);
7878
self::assertSniffError($report, 102, PropertyTypeHintSniff::CODE_MISSING_NATIVE_TYPE_HINT);
79+
self::assertSniffError($report, 107, PropertyTypeHintSniff::CODE_MISSING_NATIVE_TYPE_HINT);
80+
self::assertSniffError($report, 112, PropertyTypeHintSniff::CODE_MISSING_NATIVE_TYPE_HINT);
81+
self::assertSniffError($report, 117, PropertyTypeHintSniff::CODE_MISSING_NATIVE_TYPE_HINT);
7982

8083
self::assertAllFixedInFile($report);
8184
}

tests/Sniffs/TypeHints/data/propertyTypeHintEnabledNativeErrors.fixed.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,19 @@ class Whatever
8888

8989
public static int $staticSecond;
9090

91+
/**
92+
* @var array<string>|array<int>|array<bool>
93+
*/
94+
public array $unionWithSameBase;
95+
96+
/**
97+
* @var array<int>|bool[]
98+
*/
99+
public array $unionWithSameBaseToo;
100+
101+
/**
102+
* @var array<string>|array<int>|array<bool>|null
103+
*/
104+
public ?array $unionWithSameNullableBase = null;
105+
91106
}

tests/Sniffs/TypeHints/data/propertyTypeHintEnabledNativeErrors.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,19 @@ class Whatever
101101
/** @var int */
102102
public static $staticSecond;
103103

104+
/**
105+
* @var array<string>|array<int>|array<bool>
106+
*/
107+
public $unionWithSameBase;
108+
109+
/**
110+
* @var array<int>|bool[]
111+
*/
112+
public $unionWithSameBaseToo;
113+
114+
/**
115+
* @var array<string>|array<int>|array<bool>|null
116+
*/
117+
public $unionWithSameNullableBase;
118+
104119
}

tests/Sniffs/TypeHints/data/propertyTypeHintEnabledNativeNoErrors.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,19 @@ class Whatever
103103
*/
104104
public $callable;
105105

106+
/**
107+
* @var Whatever|Something|Anything
108+
*/
109+
public $unionWithDifferentBase;
110+
111+
/**
112+
* @var array<int>|array<bool>|(A&B)
113+
*/
114+
public $unionWithMoreDifferentBase;
115+
116+
/**
117+
* @var Whatever|Something|Anything|null
118+
*/
119+
public $unionWithDifferentNullableBase;
120+
106121
}

0 commit comments

Comments
 (0)