4
4
5
5
namespace SymfonyCustom \Sniffs \NamingConventions ;
6
6
7
+ use PHP_CodeSniffer \Exceptions \DeepExitException ;
7
8
use PHP_CodeSniffer \Files \File ;
8
9
use PHP_CodeSniffer \Sniffs \Sniff ;
9
- use PHP_CodeSniffer \Util \Common ;
10
10
use SymfonyCustom \Helpers \SniffHelper ;
11
11
12
12
/**
13
13
* Throws errors if PHPDocs type hint are not valid.
14
14
*/
15
15
class ValidTypeHintSniff implements Sniff
16
16
{
17
- private const TEXT = '[ \\\\a-z0-9] ' ;
18
- private const OPENER = '\<|\[|\{|\( ' ;
19
- private const MIDDLE = '\,|\:|\=\> ' ;
20
- private const CLOSER = '\>|\]|\}|\) ' ;
21
- private const SEPARATOR = '\&|\| ' ;
22
-
23
- /*
17
+ /**
24
18
* <simple> is any non-array, non-generic, non-alternated type, eg `int` or `\Foo`
25
19
* <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>`
20
+ * <generic> is generic collection type, like `array<string, int>`, `Collection<Item>` or more complex`
21
+ * <object> is array key => value type, like `array{type: string, name: string, value: mixed}`
22
+ * <type> is <simple>, <array>, <object>, <generic> type
28
23
* <types> is one or more types alternated via `|`, like `int|bool[]|Collection<ItemKey, ItemVal>`
29
24
*/
30
25
private const REGEX_TYPES = '
31
26
(?<types>
32
27
(?<type>
33
28
(?<array>
34
- (?&simple)(\[\])*
35
- )
36
- |
37
- (?<simple>
38
- [@$?]?[ \\\\\w]+
29
+ (?¬Array)(?:
30
+ \s*\[\s*\]
31
+ )+
39
32
)
40
33
|
41
- (?<generic>
42
- (?<genericName>(?&simple))
43
- <
44
- (?:(?<genericKey>(?&types)),\s*)?(?<genericValue>(?&types)|(?&generic))
45
- >
34
+ (?<notArray>
35
+ (?<multiple>
36
+ \(\s*(?<mutipleContent>
37
+ (?&types)
38
+ )\s*\)
39
+ )
40
+ |
41
+ (?<generic>
42
+ (?<genericName>
43
+ (?&simple)
44
+ )
45
+ \s*<\s*
46
+ (?<genericContent>
47
+ (?:(?&types)\s*,\s*)*
48
+ (?&types)
49
+ )
50
+ \s*>
51
+ )
52
+ |
53
+ (?<object>
54
+ array\s*{\s*
55
+ (?<objectContent>
56
+ (?:
57
+ (?<objectKeyValue>
58
+ (?:\w+\s*\??:\s*)?
59
+ (?&types)
60
+ )
61
+ \s*,\s*
62
+ )*
63
+ (?&objectKeyValue)
64
+ )
65
+ \s*}
66
+ )
67
+ |
68
+ (?<simple>
69
+ \\\\?\w+(?: \\\\\w+)*
70
+ |
71
+ \$this
72
+ )
46
73
)
47
74
)
48
75
(?:
49
- \|
50
- (?:(?&simple)|(?&array)|(?&generic))
76
+ \s*[\|&]\s*(?&type)
51
77
)*
52
78
)
53
79
' ;
54
80
81
+ /**
82
+ * False if the type is not a reserved keyword and the check can't be case insensitive
83
+ **/
84
+ private const TYPES = [
85
+ 'array ' => true ,
86
+ 'bool ' => true ,
87
+ 'callable ' => true ,
88
+ 'false ' => true ,
89
+ 'float ' => true ,
90
+ 'int ' => true ,
91
+ 'iterable ' => true ,
92
+ 'mixed ' => false ,
93
+ 'null ' => true ,
94
+ 'number ' => false ,
95
+ 'object ' => true ,
96
+ 'resource ' => false ,
97
+ 'self ' => true ,
98
+ 'static ' => true ,
99
+ 'string ' => true ,
100
+ 'true ' => true ,
101
+ 'void ' => true ,
102
+ '$this ' => true ,
103
+ ];
104
+
105
+ private const ALIAS_TYPES = [
106
+ 'boolean ' => 'bool ' ,
107
+ 'integer ' => 'int ' ,
108
+ 'double ' => 'float ' ,
109
+ 'real ' => 'float ' ,
110
+ 'callback ' => 'callable ' ,
111
+ ];
112
+
55
113
/**
56
114
* @return array
57
115
*/
@@ -68,80 +126,166 @@ public function process(File $phpcsFile, $stackPtr): void
68
126
{
69
127
$ tokens = $ phpcsFile ->getTokens ();
70
128
71
- if (in_array ($ tokens [$ stackPtr ]['content ' ], SniffHelper::TAGS_WITH_TYPE )) {
72
- $ matchingResult = preg_match (
73
- '{^ ' .self ::REGEX_TYPES .'(?:[ \t].*)?$}sx ' ,
74
- $ tokens [$ stackPtr + 2 ]['content ' ],
75
- $ matches
76
- );
129
+ if (!in_array ($ tokens [$ stackPtr ]['content ' ], SniffHelper::TAGS_WITH_TYPE )) {
130
+ return ;
131
+ }
77
132
78
- $ content = 1 === $ matchingResult ? $ matches ['types ' ] : '' ;
79
- $ endOfContent = preg_replace ('/ ' .preg_quote ($ content , '/ ' ).'/ ' , '' , $ tokens [$ stackPtr + 2 ]['content ' ], 1 );
133
+ $ matchingResult = preg_match (
134
+ '{^ ' .self ::REGEX_TYPES .'(?:[\s\t].*)?$}six ' ,
135
+ $ tokens [$ stackPtr + 2 ]['content ' ],
136
+ $ matches
137
+ );
80
138
139
+ $ content = 1 === $ matchingResult ? $ matches ['types ' ] : '' ;
140
+ $ endOfContent = substr ($ tokens [$ stackPtr + 2 ]['content ' ], strlen ($ content ));
141
+
142
+ try {
81
143
$ suggestedType = $ this ->getValidTypes ($ content );
144
+ } catch (DeepExitException $ exception ) {
145
+ $ phpcsFile ->addError (
146
+ $ exception ->getMessage (),
147
+ $ stackPtr + 2 ,
148
+ 'Exception '
149
+ );
82
150
83
- if ($ content !== $ suggestedType ) {
84
- $ fix = $ phpcsFile ->addFixableError (
85
- 'For type-hinting in PHPDocs, use %s instead of %s ' ,
86
- $ stackPtr + 2 ,
87
- 'Invalid ' ,
88
- [$ suggestedType , $ content ]
89
- );
151
+ return ;
152
+ }
153
+
154
+ if ($ content !== $ suggestedType ) {
155
+ $ fix = $ phpcsFile ->addFixableError (
156
+ 'For type-hinting in PHPDocs, use %s instead of %s ' ,
157
+ $ stackPtr + 2 ,
158
+ 'Invalid ' ,
159
+ [$ suggestedType , $ content ]
160
+ );
90
161
91
- if ($ fix ) {
92
- $ phpcsFile ->fixer ->replaceToken ($ stackPtr + 2 , $ suggestedType .$ endOfContent );
93
- }
162
+ if ($ fix ) {
163
+ $ phpcsFile ->fixer ->replaceToken ($ stackPtr + 2 , $ suggestedType .$ endOfContent );
94
164
}
95
165
}
96
166
}
97
167
98
168
/**
99
169
* @param string $content
100
170
*
101
- * @return array
171
+ * @return string
172
+ *
173
+ * @throws DeepExitException
102
174
*/
103
- private function getTypes (string $ content ): array
175
+ private function getValidTypes (string $ content ): string
104
176
{
177
+ $ content = preg_replace ('/\s/ ' , '' , $ content );
178
+
105
179
$ types = [];
180
+ $ separators = [];
106
181
while ('' !== $ content && false !== $ content ) {
107
- preg_match ('{^ ' .self ::REGEX_TYPES .'$}x ' , $ content , $ matches );
182
+ preg_match ('{^ ' .self ::REGEX_TYPES .'$}ix ' , $ content , $ matches );
183
+
184
+ if (isset ($ matches ['array ' ]) && '' !== $ matches ['array ' ]) {
185
+ $ validType = $ this ->getValidTypes (substr ($ matches ['array ' ], 0 , -2 )).'[] ' ;
186
+ } elseif (isset ($ matches ['multiple ' ]) && '' !== $ matches ['multiple ' ]) {
187
+ $ validType = '( ' .$ this ->getValidTypes ($ matches ['mutipleContent ' ]).') ' ;
188
+ } elseif (isset ($ matches ['generic ' ]) && '' !== $ matches ['generic ' ]) {
189
+ $ validType = $ this ->getValidGenericType ($ matches ['genericName ' ], $ matches ['genericContent ' ]);
190
+ } elseif (isset ($ matches ['object ' ]) && '' !== $ matches ['object ' ]) {
191
+ $ validType = $ this ->getValidObjectType ($ matches ['objectContent ' ]);
192
+ } else {
193
+ $ validType = $ this ->getValidType ($ matches ['type ' ]);
194
+ }
108
195
109
- $ types [] = $ matches ['type ' ];
196
+ $ types [] = $ validType ;
197
+
198
+ $ separators [] = substr ($ content , strlen ($ matches ['type ' ]), 1 );
110
199
$ content = substr ($ content , strlen ($ matches ['type ' ]) + 1 );
111
200
}
112
201
202
+ // Remove last separator since it's an empty string
203
+ array_pop ($ separators );
204
+
205
+ $ uniqueSeparators = array_unique ($ separators );
206
+ switch (count ($ uniqueSeparators )) {
207
+ case 0 :
208
+ return implode ('' , $ types );
209
+ case 1 :
210
+ return implode ($ uniqueSeparators [0 ], $ this ->orderTypes ($ types ));
211
+ default :
212
+ throw new DeepExitException (
213
+ 'Union and intersection types must be grouped with parenthesis when used in the same expression '
214
+ );
215
+ }
216
+ }
217
+
218
+ /**
219
+ * @param array $types
220
+ *
221
+ * @return array
222
+ */
223
+ private function orderTypes (array $ types ): array
224
+ {
225
+ $ types = array_unique ($ types );
226
+ usort ($ types , function ($ type1 , $ type2 ) {
227
+ if ('null ' === $ type1 ) {
228
+ return 1 ;
229
+ }
230
+
231
+ if ('null ' === $ type2 ) {
232
+ return -1 ;
233
+ }
234
+
235
+ return 0 ;
236
+ });
237
+
113
238
return $ types ;
114
239
}
115
240
116
241
/**
117
- * @param string $content
242
+ * @param string $genericName
243
+ * @param string $genericContent
118
244
*
119
245
* @return string
246
+ *
247
+ * @throws DeepExitException
120
248
*/
121
- private function getValidTypes (string $ content ): string
249
+ private function getValidGenericType (string $ genericName , string $ genericContent ): string
122
250
{
123
- $ types = $ this ->getTypes ( $ content ) ;
251
+ $ validType = $ this ->getValidType ( $ genericName ). ' < ' ;
124
252
125
- foreach ( $ types as $ index => $ type ) {
126
- $ type = str_replace ( ' ' , '' , $ type );
253
+ while ( '' !== $ genericContent && false !== $ genericContent ) {
254
+ preg_match ( ' {^ ' . self :: REGEX_TYPES . ' ,?}ix ' , $ genericContent , $ matches );
127
255
128
- preg_match ( ' {^ ' . self :: REGEX_TYPES . ' $}x ' , $ type , $ matches ) ;
129
- if ( isset ($ matches ['generic ' ])) {
130
- $ validType = $ this -> getValidType ( $ matches [ ' genericName ' ]). ' < ' ;
256
+ $ validType .= $ this -> getValidTypes ( $ matches [ ' types ' ]). ' , ' ;
257
+ $ genericContent = substr ( $ genericContent , strlen ($ matches ['types ' ]) + 1 );
258
+ }
131
259
132
- if ('' !== $ matches ['genericKey ' ]) {
133
- $ validType .= $ this ->getValidTypes ($ matches ['genericKey ' ]).', ' ;
134
- }
260
+ return preg_replace ('/,\s$/ ' , '> ' , $ validType );
261
+ }
135
262
136
- $ validType .= $ this ->getValidTypes ($ matches ['genericValue ' ]).'> ' ;
137
- } else {
138
- $ validType = $ this ->getValidType ($ type );
263
+ /**
264
+ * @param string $objectContent
265
+ *
266
+ * @return string
267
+ *
268
+ * @throws DeepExitException
269
+ */
270
+ private function getValidObjectType (string $ objectContent ): string
271
+ {
272
+ $ validType = 'array{ ' ;
273
+
274
+ while ('' !== $ objectContent && false !== $ objectContent ) {
275
+ $ split = preg_split ('/(\??:|,)/ ' , $ objectContent , 2 , PREG_SPLIT_DELIM_CAPTURE );
276
+
277
+ if (isset ($ split [1 ]) && ', ' !== $ split [1 ]) {
278
+ $ validType .= $ split [0 ].$ split [1 ].' ' ;
279
+ $ objectContent = $ split [2 ];
139
280
}
140
281
141
- $ types [$ index ] = $ validType ;
282
+ preg_match ('{^ ' .self ::REGEX_TYPES .',?}ix ' , $ objectContent , $ matches );
283
+
284
+ $ validType .= $ this ->getValidTypes ($ matches ['types ' ]).', ' ;
285
+ $ objectContent = substr ($ objectContent , strlen ($ matches ['types ' ]) + 1 );
142
286
}
143
287
144
- return implode ( ' | ' , $ types );
288
+ return preg_replace ( ' /,\s$/ ' , ' } ' , $ validType );
145
289
}
146
290
147
291
/**
@@ -151,20 +295,16 @@ private function getValidTypes(string $content): string
151
295
*/
152
296
private function getValidType (string $ typeName ): string
153
297
{
154
- if ('[] ' === substr ($ typeName , -2 )) {
155
- return $ this ->getValidType (substr ($ typeName , 0 , -2 )).'[] ' ;
298
+ $ lowerType = strtolower ($ typeName );
299
+ if (isset (self ::TYPES [$ lowerType ])) {
300
+ return self ::TYPES [$ lowerType ] ? $ lowerType : $ typeName ;
156
301
}
157
302
158
- $ lowerType = strtolower ($ typeName );
159
- switch ($ lowerType ) {
160
- case 'bool ' :
161
- case 'boolean ' :
162
- return 'bool ' ;
163
- case 'int ' :
164
- case 'integer ' :
165
- return 'int ' ;
303
+ // This can't be case insensitive since this is not reserved keyword
304
+ if (isset (self ::ALIAS_TYPES [$ typeName ])) {
305
+ return self ::ALIAS_TYPES [$ typeName ];
166
306
}
167
307
168
- return Common:: suggestType ( $ typeName) ;
308
+ return $ typeName ;
169
309
}
170
310
}
0 commit comments