@@ -20,6 +20,38 @@ class ValidTypeHintSniff implements Sniff
20
20
private const CLOSER = '\>|\]|\}|\) ' ;
21
21
private const SEPARATOR = '\&|\| ' ;
22
22
23
+ /*
24
+ * <simple> is any non-array, non-generic, non-alternated type, eg `int` or `\Foo`
25
+ * <array> is array of <simple>, eg `int[]` or `\Foo[]`
26
+ * <generic> is generic collection type, like `array<string, int>`, `Collection<Item>` and more complex like `Collection<int, \null|SubCollection<string>>`
27
+ * <type> is <simple>, <array> or <generic> type, like `int`, `bool[]` or `Collection<ItemKey, ItemVal>`
28
+ * <types> is one or more types alternated via `|`, like `int|bool[]|Collection<ItemKey, ItemVal>`
29
+ */
30
+ private const REGEX_TYPES = '
31
+ (?<types>
32
+ (?<type>
33
+ (?<array>
34
+ (?&simple)(\[\])*
35
+ )
36
+ |
37
+ (?<simple>
38
+ [@$?]?[ \\\\\w]+
39
+ )
40
+ |
41
+ (?<generic>
42
+ (?<genericName>(?&simple))
43
+ <
44
+ (?:(?<genericKey>(?&types)),\s*)?(?<genericValue>(?&types)|(?&generic))
45
+ >
46
+ )
47
+ )
48
+ (?:
49
+ \|
50
+ (?:(?&simple)|(?&array)|(?&generic))
51
+ )*
52
+ )
53
+ ' ;
54
+
23
55
/**
24
56
* @return array
25
57
*/
@@ -37,90 +69,92 @@ public function process(File $phpcsFile, $stackPtr): void
37
69
$ tokens = $ phpcsFile ->getTokens ();
38
70
39
71
if (in_array ($ tokens [$ stackPtr ]['content ' ], SniffHelper::TAGS_WITH_TYPE )) {
40
- preg_match (
41
- '`^((?: '
42
- .'(?: ' .self ::OPENER .'| ' .self ::MIDDLE .'| ' .self ::SEPARATOR .')\s+ '
43
- .'(?= ' .self ::TEXT .'| ' .self ::OPENER .'| ' .self ::MIDDLE .'| ' .self ::CLOSER .'| ' .self ::SEPARATOR .') '
44
- .'| ' .self ::OPENER .'| ' .self ::MIDDLE .'| ' .self ::SEPARATOR
45
- .'|(?: ' .self ::TEXT .'| ' .self ::CLOSER .')\s+ '
46
- .'(?= ' .self ::OPENER .'| ' .self ::MIDDLE .'| ' .self ::CLOSER .'| ' .self ::SEPARATOR .') '
47
- .'| ' .self ::TEXT .'| ' .self ::CLOSER .''
48
- .')+)(.*)?`i ' ,
49
- $ tokens [($ stackPtr + 2 )]['content ' ],
50
- $ match
72
+ $ matchingResult = preg_match (
73
+ '{^ ' .self ::REGEX_TYPES .'(?:[ \t].*)?$}sx ' ,
74
+ $ tokens [$ stackPtr + 2 ]['content ' ],
75
+ $ matches
51
76
);
52
77
53
- if (isset ($ match [1 ]) === false ) {
54
- return ;
55
- }
78
+ $ content = 1 === $ matchingResult ? $ matches ['types ' ] : '' ;
79
+ $ endOfContent = preg_replace ('/ ' .preg_quote ($ content , '/ ' ).'/ ' , '' , $ tokens [$ stackPtr + 2 ]['content ' ], 1 );
56
80
57
- $ type = $ match [ 1 ] ;
58
- $ suggestedType = $ this -> getValidTypeName ( $ type );
59
- if ($ type !== $ suggestedType ) {
81
+ $ suggestedType = $ this -> getValidTypes ( $ content ) ;
82
+
83
+ if ($ content !== $ suggestedType ) {
60
84
$ fix = $ phpcsFile ->addFixableError (
61
85
'For type-hinting in PHPDocs, use %s instead of %s ' ,
62
86
$ stackPtr + 2 ,
63
87
'Invalid ' ,
64
- [$ suggestedType , $ type ]
88
+ [$ suggestedType , $ content ]
65
89
);
66
90
67
91
if ($ fix ) {
68
- $ replacement = $ suggestedType ;
69
- if (isset ($ match [2 ])) {
70
- $ replacement .= $ match [2 ];
71
- }
72
-
73
- $ phpcsFile ->fixer ->replaceToken ($ stackPtr + 2 , $ replacement );
92
+ $ phpcsFile ->fixer ->replaceToken ($ stackPtr + 2 , $ suggestedType .$ endOfContent );
74
93
}
75
94
}
76
95
}
77
96
}
78
97
79
98
/**
80
- * @param string $typeName
99
+ * @param string $content
100
+ *
101
+ * @return array
102
+ */
103
+ private function getTypes (string $ content ): array
104
+ {
105
+ $ types = [];
106
+ while ('' !== $ content && false !== $ content ) {
107
+ preg_match ('{^ ' .self ::REGEX_TYPES .'$}x ' , $ content , $ matches );
108
+
109
+ $ types [] = $ matches ['type ' ];
110
+ $ content = substr ($ content , strlen ($ matches ['type ' ]) + 1 );
111
+ }
112
+
113
+ return $ types ;
114
+ }
115
+
116
+ /**
117
+ * @param string $content
81
118
*
82
119
* @return string
83
120
*/
84
- private function getValidTypeName (string $ typeName ): string
121
+ private function getValidTypes (string $ content ): string
85
122
{
86
- $ typeNameWithoutSpace = str_replace (' ' , '' , $ typeName );
87
- $ parts = preg_split (
88
- '/( ' .self ::OPENER .'| ' .self ::MIDDLE .'| ' .self ::CLOSER .'| ' .self ::SEPARATOR .')/ ' ,
89
- $ typeNameWithoutSpace ,
90
- -1 ,
91
- PREG_SPLIT_DELIM_CAPTURE
92
- );
93
- $ partsNumber = count ($ parts ) - 1 ;
94
-
95
- $ validType = '' ;
96
- for ($ i = 0 ; $ i < $ partsNumber ; $ i += 2 ) {
97
- $ validType .= $ this ->suggestType ($ parts [$ i ]);
98
-
99
- if ('=> ' === $ parts [$ i + 1 ]) {
100
- $ validType .= ' ' ;
101
- }
123
+ $ types = $ this ->getTypes ($ content );
124
+
125
+ foreach ($ types as $ index => $ type ) {
126
+ $ type = str_replace (' ' , '' , $ type );
102
127
103
- $ validType .= $ parts [$ i + 1 ];
128
+ preg_match ('{^ ' .self ::REGEX_TYPES .'$}x ' , $ type , $ matches );
129
+ if (isset ($ matches ['generic ' ])) {
130
+ $ validType = $ this ->getValidType ($ matches ['genericName ' ]).'< ' ;
104
131
105
- if (preg_match ('/ ' .self ::MIDDLE .'/ ' , $ parts [$ i + 1 ])) {
106
- $ validType .= ' ' ;
132
+ if ('' !== $ matches ['genericKey ' ]) {
133
+ $ validType .= $ this ->getValidTypes ($ matches ['genericKey ' ]).', ' ;
134
+ }
135
+
136
+ $ validType .= $ this ->getValidTypes ($ matches ['genericValue ' ]).'> ' ;
137
+ } else {
138
+ $ validType = $ this ->getValidType ($ type );
107
139
}
108
- }
109
140
110
- if ('' !== $ parts [$ partsNumber ]) {
111
- $ validType .= $ this ->suggestType ($ parts [$ partsNumber ]);
141
+ $ types [$ index ] = $ validType ;
112
142
}
113
143
114
- return trim ( $ validType );
144
+ return implode ( ' | ' , $ types );
115
145
}
116
146
117
147
/**
118
148
* @param string $typeName
119
149
*
120
150
* @return string
121
151
*/
122
- private function suggestType (string $ typeName ): string
152
+ private function getValidType (string $ typeName ): string
123
153
{
154
+ if ('[] ' === substr ($ typeName , -2 )) {
155
+ return $ this ->getValidType (substr ($ typeName , 0 , -2 )).'[] ' ;
156
+ }
157
+
124
158
$ lowerType = strtolower ($ typeName );
125
159
switch ($ lowerType ) {
126
160
case 'bool ' :
0 commit comments