Skip to content

Commit 4c37991

Browse files
committed
Added DisallowArrayTypeHintSyntaxSniff
1 parent 8fb212f commit 4c37991

File tree

8 files changed

+446
-53
lines changed

8 files changed

+446
-53
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,14 @@ Sniff provides the following settings:
426426
* `linesCountAfterLastUse`: allows to configure the number of lines after last `use`.
427427

428428

429+
#### SlevomatCodingStandard.TypeHints.DisallowArrayTypeHintSyntax 🔧
430+
431+
Disallows usage of array type hint syntax (eg. `int[]`, `bool[][]`) in phpDocs in favour of generic type hint syntax (eg. `array<int>`, `array<array<bool>>`).
432+
433+
Sniff provides the following settings:
434+
435+
* `traversableTypeHints`: helps fixer detect traversable type hints so `\Traversable|int[]` can be converted to `\Traversable<int>`.
436+
429437
#### SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint
430438

431439
Disallows usage of "mixed" type hint in phpDocs.

SlevomatCodingStandard/Helpers/AnnotationTypeHelper.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,95 @@ public static function getIdentifierTypeNodes(TypeNode $typeNode): array
6767
return [$typeNode];
6868
}
6969

70+
/**
71+
* @param \PHPStan\PhpDocParser\Ast\Type\TypeNode $typeNode
72+
* @return \PHPStan\PhpDocParser\Ast\Type\UnionTypeNode[]
73+
*/
74+
public static function getUnionTypeNodes(TypeNode $typeNode): array
75+
{
76+
if ($typeNode instanceof UnionTypeNode) {
77+
return [$typeNode];
78+
}
79+
80+
if ($typeNode instanceof NullableTypeNode) {
81+
return self::getUnionTypeNodes($typeNode->type);
82+
}
83+
84+
if ($typeNode instanceof ArrayTypeNode) {
85+
return self::getUnionTypeNodes($typeNode->type);
86+
}
87+
88+
if ($typeNode instanceof IntersectionTypeNode) {
89+
$unionTypeNodes = [];
90+
foreach ($typeNode->types as $innerTypeNode) {
91+
$unionTypeNodes = array_merge($unionTypeNodes, self::getUnionTypeNodes($innerTypeNode));
92+
}
93+
return $unionTypeNodes;
94+
}
95+
96+
if ($typeNode instanceof GenericTypeNode) {
97+
$unionTypeNodes = [];
98+
foreach ($typeNode->genericTypes as $innerTypeNode) {
99+
$unionTypeNodes = array_merge($unionTypeNodes, self::getUnionTypeNodes($innerTypeNode));
100+
}
101+
return $unionTypeNodes;
102+
}
103+
104+
if ($typeNode instanceof CallableTypeNode) {
105+
$unionTypeNodes = self::getUnionTypeNodes($typeNode->returnType);
106+
foreach ($typeNode->parameters as $callableParameterNode) {
107+
$unionTypeNodes = array_merge($unionTypeNodes, self::getUnionTypeNodes($callableParameterNode->type));
108+
}
109+
return $unionTypeNodes;
110+
}
111+
112+
return [];
113+
}
114+
115+
/**
116+
* @param \PHPStan\PhpDocParser\Ast\Type\TypeNode $typeNode
117+
* @return \PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode[]
118+
*/
119+
public static function getArrayTypeNodes(TypeNode $typeNode): array
120+
{
121+
if ($typeNode instanceof ArrayTypeNode) {
122+
return array_merge([$typeNode], self::getArrayTypeNodes($typeNode->type));
123+
}
124+
125+
if ($typeNode instanceof NullableTypeNode) {
126+
return self::getArrayTypeNodes($typeNode->type);
127+
}
128+
129+
if (
130+
$typeNode instanceof UnionTypeNode
131+
|| $typeNode instanceof IntersectionTypeNode
132+
) {
133+
$arrayTypeNodes = [];
134+
foreach ($typeNode->types as $innerTypeNode) {
135+
$arrayTypeNodes = array_merge($arrayTypeNodes, self::getArrayTypeNodes($innerTypeNode));
136+
}
137+
return $arrayTypeNodes;
138+
}
139+
140+
if ($typeNode instanceof GenericTypeNode) {
141+
$arrayTypeNodes = [];
142+
foreach ($typeNode->genericTypes as $innerTypeNode) {
143+
$arrayTypeNodes = array_merge($arrayTypeNodes, self::getArrayTypeNodes($innerTypeNode));
144+
}
145+
return $arrayTypeNodes;
146+
}
147+
148+
if ($typeNode instanceof CallableTypeNode) {
149+
$arrayTypeNodes = self::getArrayTypeNodes($typeNode->returnType);
150+
foreach ($typeNode->parameters as $callableParameterNode) {
151+
$arrayTypeNodes = array_merge($arrayTypeNodes, self::getArrayTypeNodes($callableParameterNode->type));
152+
}
153+
return $arrayTypeNodes;
154+
}
155+
156+
return [];
157+
}
158+
70159
/**
71160
* @param \PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode|\PHPStan\PhpDocParser\Ast\Type\ThisTypeNode $typeNode
72161
* @return string
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Sniffs\TypeHints;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\Sniff;
7+
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
8+
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
9+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
10+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
11+
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
12+
use SlevomatCodingStandard\Helpers\Annotation\GenericAnnotation;
13+
use SlevomatCodingStandard\Helpers\AnnotationHelper;
14+
use SlevomatCodingStandard\Helpers\AnnotationTypeHelper;
15+
use SlevomatCodingStandard\Helpers\NamespaceHelper;
16+
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
17+
use SlevomatCodingStandard\Helpers\TypeHintHelper;
18+
use function array_flip;
19+
use function array_key_exists;
20+
use function array_map;
21+
use function count;
22+
use function in_array;
23+
use function sprintf;
24+
use const T_DOC_COMMENT_OPEN_TAG;
25+
26+
class DisallowArrayTypeHintSyntaxSniff implements Sniff
27+
{
28+
29+
public const CODE_DISALLOWED_ARRAY_TYPE_HINT_SYNTAX = 'DisallowedArrayTypeHintSyntax';
30+
31+
/** @var string[] */
32+
public $traversableTypeHints = [];
33+
34+
/** @var array<string, int>|null */
35+
private $normalizedTraversableTypeHints;
36+
37+
/**
38+
* @return (int|string)[]
39+
*/
40+
public function register(): array
41+
{
42+
return [
43+
T_DOC_COMMENT_OPEN_TAG,
44+
];
45+
}
46+
47+
/**
48+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
49+
* @param \PHP_CodeSniffer\Files\File $phpcsFile
50+
* @param int $docCommentOpenPointer
51+
*/
52+
public function process(File $phpcsFile, $docCommentOpenPointer): void
53+
{
54+
$annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer);
55+
56+
foreach ($annotations as $annotationByName) {
57+
foreach ($annotationByName as $annotation) {
58+
if ($annotation instanceof GenericAnnotation) {
59+
continue;
60+
}
61+
62+
if ($annotation->isInvalid()) {
63+
continue;
64+
}
65+
66+
foreach (AnnotationHelper::getAnnotationTypes($annotation) as $annotationType) {
67+
$unionTypeNodes = AnnotationTypeHelper::getUnionTypeNodes($annotationType);
68+
foreach ($this->getArrayTypeNodes($annotationType) as $arrayTypeNode) {
69+
$fix = $phpcsFile->addFixableError(
70+
sprintf('Usage of array type hint syntax in "%s" is disallowed, use generic type hint syntax instead.', AnnotationTypeHelper::export($arrayTypeNode)),
71+
$annotation->getStartPointer(),
72+
self::CODE_DISALLOWED_ARRAY_TYPE_HINT_SYNTAX
73+
);
74+
75+
if (!$fix) {
76+
continue;
77+
}
78+
79+
$unionTypeNode = $this->findUnionTypeThatContainsArrayType($arrayTypeNode, $unionTypeNodes);
80+
81+
if ($unionTypeNode !== null) {
82+
$genericIdentifier = $this->findGenericIdentifier($phpcsFile, $annotation->getStartPointer(), $unionTypeNode);
83+
if ($genericIdentifier !== null) {
84+
$genericTypeNode = new GenericTypeNode(new IdentifierTypeNode($genericIdentifier), [$arrayTypeNode->type]);
85+
$fixedAnnotationContent = AnnotationHelper::fixAnnotation($phpcsFile, $annotation, $unionTypeNode, $genericTypeNode);
86+
} else {
87+
$genericTypeNode = new GenericTypeNode(new IdentifierTypeNode('array'), [$arrayTypeNode->type]);
88+
$fixedAnnotationContent = AnnotationHelper::fixAnnotation($phpcsFile, $annotation, $arrayTypeNode, $genericTypeNode);
89+
}
90+
} else {
91+
$genericTypeNode = new GenericTypeNode(new IdentifierTypeNode('array'), [$arrayTypeNode->type]);
92+
$fixedAnnotationContent = AnnotationHelper::fixAnnotation($phpcsFile, $annotation, $arrayTypeNode, $genericTypeNode);
93+
}
94+
95+
$phpcsFile->fixer->beginChangeset();
96+
$phpcsFile->fixer->replaceToken($annotation->getStartPointer(), $fixedAnnotationContent);
97+
for ($i = $annotation->getStartPointer() + 1; $i <= $annotation->getEndPointer(); $i++) {
98+
$phpcsFile->fixer->replaceToken($i, '');
99+
}
100+
$phpcsFile->fixer->endChangeset();
101+
}
102+
}
103+
}
104+
}
105+
}
106+
107+
/**
108+
* @param \PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode $arrayTypeNode
109+
* @param \PHPStan\PhpDocParser\Ast\Type\UnionTypeNode[] $unionTypeNodes
110+
* @return \PHPStan\PhpDocParser\Ast\Type\UnionTypeNode|null
111+
*/
112+
private function findUnionTypeThatContainsArrayType(ArrayTypeNode $arrayTypeNode, array $unionTypeNodes): ?UnionTypeNode
113+
{
114+
foreach ($unionTypeNodes as $unionTypeNode) {
115+
if (in_array($arrayTypeNode, $unionTypeNode->types, true)) {
116+
return $unionTypeNode;
117+
}
118+
}
119+
120+
return null;
121+
}
122+
123+
private function findGenericIdentifier(File $phpcsFile, int $pointer, UnionTypeNode $unionTypeNode): ?string
124+
{
125+
if (count($unionTypeNode->types) !== 2) {
126+
return null;
127+
}
128+
129+
if (
130+
$unionTypeNode->types[0] instanceof ArrayTypeNode
131+
&& $unionTypeNode->types[1] instanceof IdentifierTypeNode
132+
&& $this->isTraversableType(TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $pointer, $unionTypeNode->types[1]->name))
133+
) {
134+
return $unionTypeNode->types[1]->name;
135+
}
136+
137+
if (
138+
$unionTypeNode->types[1] instanceof ArrayTypeNode
139+
&& $unionTypeNode->types[0] instanceof IdentifierTypeNode
140+
&& $this->isTraversableType(TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $pointer, $unionTypeNode->types[0]->name))
141+
) {
142+
return $unionTypeNode->types[0]->name;
143+
}
144+
145+
return null;
146+
}
147+
148+
/**
149+
* @param \PHPStan\PhpDocParser\Ast\Type\TypeNode $typeNode
150+
* @return \PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode[]
151+
*/
152+
public function getArrayTypeNodes(TypeNode $typeNode): array
153+
{
154+
$arrayTypeNodes = AnnotationTypeHelper::getArrayTypeNodes($typeNode);
155+
156+
$arrayTypeNodesToIgnore = [];
157+
foreach ($arrayTypeNodes as $arrayTypeNode) {
158+
if (!($arrayTypeNode->type instanceof ArrayTypeNode)) {
159+
continue;
160+
}
161+
162+
$arrayTypeNodesToIgnore[] = $arrayTypeNode->type;
163+
}
164+
165+
foreach ($arrayTypeNodes as $no => $arrayTypeNode) {
166+
if (!in_array($arrayTypeNode, $arrayTypeNodesToIgnore, true)) {
167+
continue;
168+
}
169+
170+
unset($arrayTypeNodes[$no]);
171+
}
172+
173+
return $arrayTypeNodes;
174+
}
175+
176+
private function isTraversableType(string $type): bool
177+
{
178+
return TypeHintHelper::isSimpleIterableTypeHint($type) || array_key_exists($type, $this->getNormalizedTraversableTypeHints());
179+
}
180+
181+
/**
182+
* @return array<string, int>
183+
*/
184+
private function getNormalizedTraversableTypeHints(): array
185+
{
186+
if ($this->normalizedTraversableTypeHints === null) {
187+
$this->normalizedTraversableTypeHints = array_flip(array_map(function (string $typeHint): string {
188+
return NamespaceHelper::isFullyQualifiedName($typeHint) ? $typeHint : sprintf('%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $typeHint);
189+
}, SniffSettingsHelper::normalizeArray($this->traversableTypeHints)));
190+
}
191+
return $this->normalizedTraversableTypeHints;
192+
}
193+
194+
}

SlevomatCodingStandard/Sniffs/TypeHints/NullTypeHintOnLastPositionSniff.php

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,11 @@
44

55
use PHP_CodeSniffer\Files\File;
66
use PHP_CodeSniffer\Sniffs\Sniff;
7-
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
8-
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
9-
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
107
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
11-
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
12-
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
13-
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
148
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
159
use SlevomatCodingStandard\Helpers\Annotation\GenericAnnotation;
1610
use SlevomatCodingStandard\Helpers\AnnotationHelper;
1711
use SlevomatCodingStandard\Helpers\AnnotationTypeHelper;
18-
use function array_merge;
1912
use function count;
2013
use function sprintf;
2114
use function strtolower;
@@ -56,7 +49,7 @@ public function process(File $phpcsFile, $docCommentOpenPointer): void
5649
}
5750

5851
foreach (AnnotationHelper::getAnnotationTypes($annotation) as $annotationType) {
59-
foreach ($this->getUnionTypeNodes($annotationType) as $unionTypeNode) {
52+
foreach (AnnotationTypeHelper::getUnionTypeNodes($annotationType) as $unionTypeNode) {
6053
$nullTypeNode = null;
6154
$nullPosition = 0;
6255
$position = 0;
@@ -119,49 +112,4 @@ public function process(File $phpcsFile, $docCommentOpenPointer): void
119112
}
120113
}
121114

122-
/**
123-
* @param \PHPStan\PhpDocParser\Ast\Type\TypeNode $typeNode
124-
* @return \PHPStan\PhpDocParser\Ast\Type\UnionTypeNode[]
125-
*/
126-
private function getUnionTypeNodes(TypeNode $typeNode): array
127-
{
128-
if ($typeNode instanceof UnionTypeNode) {
129-
return [$typeNode];
130-
}
131-
132-
if ($typeNode instanceof NullableTypeNode) {
133-
return $this->getUnionTypeNodes($typeNode->type);
134-
}
135-
136-
if ($typeNode instanceof ArrayTypeNode) {
137-
return $this->getUnionTypeNodes($typeNode->type);
138-
}
139-
140-
if ($typeNode instanceof IntersectionTypeNode) {
141-
$unionTypeNodes = [];
142-
foreach ($typeNode->types as $innerTypeNode) {
143-
$unionTypeNodes = array_merge($unionTypeNodes, $this->getUnionTypeNodes($innerTypeNode));
144-
}
145-
return $unionTypeNodes;
146-
}
147-
148-
if ($typeNode instanceof GenericTypeNode) {
149-
$unionTypeNodes = [];
150-
foreach ($typeNode->genericTypes as $innerTypeNode) {
151-
$unionTypeNodes = array_merge($unionTypeNodes, $this->getUnionTypeNodes($innerTypeNode));
152-
}
153-
return $unionTypeNodes;
154-
}
155-
156-
if ($typeNode instanceof CallableTypeNode) {
157-
$unionTypeNodes = $this->getUnionTypeNodes($typeNode->returnType);
158-
foreach ($typeNode->parameters as $callableParameterNode) {
159-
$unionTypeNodes = array_merge($unionTypeNodes, $this->getUnionTypeNodes($callableParameterNode->type));
160-
}
161-
return $unionTypeNodes;
162-
}
163-
164-
return [];
165-
}
166-
167115
}

0 commit comments

Comments
 (0)