@@ -20,6 +20,38 @@ class ValidTypeHintSniff implements Sniff
2020 private const CLOSER = '\>|\]|\}|\) ' ;
2121 private const SEPARATOR = '\&|\| ' ;
2222
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+
2355 /**
2456 * @return array
2557 */
@@ -37,90 +69,92 @@ public function process(File $phpcsFile, $stackPtr): void
3769 $ tokens = $ phpcsFile ->getTokens ();
3870
3971 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
5176 );
5277
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 );
5680
57- $ type = $ match [ 1 ] ;
58- $ suggestedType = $ this -> getValidTypeName ( $ type );
59- if ($ type !== $ suggestedType ) {
81+ $ suggestedType = $ this -> getValidTypes ( $ content ) ;
82+
83+ if ($ content !== $ suggestedType ) {
6084 $ fix = $ phpcsFile ->addFixableError (
6185 'For type-hinting in PHPDocs, use %s instead of %s ' ,
6286 $ stackPtr + 2 ,
6387 'Invalid ' ,
64- [$ suggestedType , $ type ]
88+ [$ suggestedType , $ content ]
6589 );
6690
6791 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 );
7493 }
7594 }
7695 }
7796 }
7897
7998 /**
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
81118 *
82119 * @return string
83120 */
84- private function getValidTypeName (string $ typeName ): string
121+ private function getValidTypes (string $ content ): string
85122 {
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 );
102127
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 ' ]).'< ' ;
104131
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 );
107139 }
108- }
109140
110- if ('' !== $ parts [$ partsNumber ]) {
111- $ validType .= $ this ->suggestType ($ parts [$ partsNumber ]);
141+ $ types [$ index ] = $ validType ;
112142 }
113143
114- return trim ( $ validType );
144+ return implode ( ' | ' , $ types );
115145 }
116146
117147 /**
118148 * @param string $typeName
119149 *
120150 * @return string
121151 */
122- private function suggestType (string $ typeName ): string
152+ private function getValidType (string $ typeName ): string
123153 {
154+ if ('[] ' === substr ($ typeName , -2 )) {
155+ return $ this ->getValidType (substr ($ typeName , 0 , -2 )).'[] ' ;
156+ }
157+
124158 $ lowerType = strtolower ($ typeName );
125159 switch ($ lowerType ) {
126160 case 'bool ' :
0 commit comments