44
55namespace SymfonyCustom \Sniffs \NamingConventions ;
66
7+ use PHP_CodeSniffer \Exceptions \DeepExitException ;
78use PHP_CodeSniffer \Files \File ;
89use PHP_CodeSniffer \Sniffs \Sniff ;
9- use PHP_CodeSniffer \Util \Common ;
1010use SymfonyCustom \Helpers \SniffHelper ;
1111
1212/**
1313 * Throws errors if PHPDocs type hint are not valid.
1414 */
1515class ValidTypeHintSniff implements Sniff
1616{
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+ /**
2418 * <simple> is any non-array, non-generic, non-alternated type, eg `int` or `\Foo`
2519 * <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
2823 * <types> is one or more types alternated via `|`, like `int|bool[]|Collection<ItemKey, ItemVal>`
2924 */
3025 private const REGEX_TYPES = '
3126 (?<types>
3227 (?<type>
3328 (?<array>
34- (?&simple)(\[\])*
35- )
36- |
37- (?<simple>
38- [@$?]?[ \\\\\w]+
29+ (?¬Array)(?:
30+ \s*\[\s*\]
31+ )+
3932 )
4033 |
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+ )
4673 )
4774 )
4875 (?:
49- \|
50- (?:(?&simple)|(?&array)|(?&generic))
76+ \s*[\|&]\s*(?&type)
5177 )*
5278 )
5379 ' ;
5480
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+
55113 /**
56114 * @return array
57115 */
@@ -68,80 +126,166 @@ public function process(File $phpcsFile, $stackPtr): void
68126 {
69127 $ tokens = $ phpcsFile ->getTokens ();
70128
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+ }
77132
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+ );
80138
139+ $ content = 1 === $ matchingResult ? $ matches ['types ' ] : '' ;
140+ $ endOfContent = substr ($ tokens [$ stackPtr + 2 ]['content ' ], strlen ($ content ));
141+
142+ try {
81143 $ suggestedType = $ this ->getValidTypes ($ content );
144+ } catch (DeepExitException $ exception ) {
145+ $ phpcsFile ->addError (
146+ $ exception ->getMessage (),
147+ $ stackPtr + 2 ,
148+ 'Exception '
149+ );
82150
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+ );
90161
91- if ($ fix ) {
92- $ phpcsFile ->fixer ->replaceToken ($ stackPtr + 2 , $ suggestedType .$ endOfContent );
93- }
162+ if ($ fix ) {
163+ $ phpcsFile ->fixer ->replaceToken ($ stackPtr + 2 , $ suggestedType .$ endOfContent );
94164 }
95165 }
96166 }
97167
98168 /**
99169 * @param string $content
100170 *
101- * @return array
171+ * @return string
172+ *
173+ * @throws DeepExitException
102174 */
103- private function getTypes (string $ content ): array
175+ private function getValidTypes (string $ content ): string
104176 {
177+ $ content = preg_replace ('/\s/ ' , '' , $ content );
178+
105179 $ types = [];
180+ $ separators = [];
106181 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+ }
108195
109- $ types [] = $ matches ['type ' ];
196+ $ types [] = $ validType ;
197+
198+ $ separators [] = substr ($ content , strlen ($ matches ['type ' ]), 1 );
110199 $ content = substr ($ content , strlen ($ matches ['type ' ]) + 1 );
111200 }
112201
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+
113238 return $ types ;
114239 }
115240
116241 /**
117- * @param string $content
242+ * @param string $genericName
243+ * @param string $genericContent
118244 *
119245 * @return string
246+ *
247+ * @throws DeepExitException
120248 */
121- private function getValidTypes (string $ content ): string
249+ private function getValidGenericType (string $ genericName , string $ genericContent ): string
122250 {
123- $ types = $ this ->getTypes ( $ content ) ;
251+ $ validType = $ this ->getValidType ( $ genericName ). ' < ' ;
124252
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 );
127255
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+ }
131259
132- if ('' !== $ matches ['genericKey ' ]) {
133- $ validType .= $ this ->getValidTypes ($ matches ['genericKey ' ]).', ' ;
134- }
260+ return preg_replace ('/,\s$/ ' , '> ' , $ validType );
261+ }
135262
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 ];
139280 }
140281
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 );
142286 }
143287
144- return implode ( ' | ' , $ types );
288+ return preg_replace ( ' /,\s$/ ' , ' } ' , $ validType );
145289 }
146290
147291 /**
@@ -151,20 +295,16 @@ private function getValidTypes(string $content): string
151295 */
152296 private function getValidType (string $ typeName ): string
153297 {
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 ;
156301 }
157302
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 ];
166306 }
167307
168- return Common:: suggestType ( $ typeName) ;
308+ return $ typeName ;
169309 }
170310}
0 commit comments