44
55namespace SymfonyCustom \Sniffs \NamingConventions ;
66
7+ use PHP_CodeSniffer \Exceptions \DeepExitException ;
78use PHP_CodeSniffer \Files \File ;
89use PHP_CodeSniffer \Sniffs \Sniff ;
910use PHP_CodeSniffer \Util \Common ;
@@ -26,51 +27,59 @@ class ValidTypeHintSniff implements Sniff
2627 private const REGEX_TYPES = '
2728 (?<types>
2829 (?<type>
29- (?<generic>
30- (?<genericName>
31- (?&simple)
32- )
33- \s*<\s*
34- (?<genericContent>
35- (?:(?&types)\s*,\s*)*
36- (?&types)
37- )
38- \s*>
39- )
40- |
41- (?<object>
42- array\s*{\s*
43- (?<objectContent>
44- (?:
45- (?<objectKeyValue>
46- (?:\w+\s*\??:\s*)?
47- (?&types)
48- )
49- \s*,\s*
50- )*
51- (?&objectKeyValue)
52- )
53- \s*}
54- )
55- |
5630 (?<array>
57- (?&simple )(?:
31+ (?¬Array )(?:
5832 \s*\[\s*\]
5933 )+
6034 )
6135 |
62- (?<classString>
63- class-string(?:
64- \s*<\s*[ \\\\\w]+\s*>
65- )?
66- )
67- |
68- (?<simple>
69- [@$?]?[ \\\\\w]+
36+ (?<notArray>
37+ (?<multiple>
38+ \(\s*(?<mutipleContent>
39+ (?&types)
40+ )\s*\)
41+ )
42+ |
43+ (?<generic>
44+ (?<genericName>
45+ (?&simple)
46+ )
47+ \s*<\s*
48+ (?<genericContent>
49+ (?:(?&types)\s*,\s*)*
50+ (?&types)
51+ )
52+ \s*>
53+ )
54+ |
55+ (?<object>
56+ array\s*{\s*
57+ (?<objectContent>
58+ (?:
59+ (?<objectKeyValue>
60+ (?:\w+\s*\??:\s*)?
61+ (?&types)
62+ )
63+ \s*,\s*
64+ )*
65+ (?&objectKeyValue)
66+ )
67+ \s*}
68+ )
69+ |
70+ (?<classString>
71+ class-string(?:
72+ \s*<\s*[ \\\\\w]+\s*>
73+ )?
74+ )
75+ |
76+ (?<simple>
77+ [@$?]?[ \\\\\w]+
78+ )
7079 )
7180 )
7281 (?:
73- \s*\| \s*(?&type)
82+ \s*[\|&] \s*(?&type)
7483 )*
7584 )
7685 ' ;
@@ -91,29 +100,41 @@ public function process(File $phpcsFile, $stackPtr): void
91100 {
92101 $ tokens = $ phpcsFile ->getTokens ();
93102
94- if (in_array ($ tokens [$ stackPtr ]['content ' ], SniffHelper::TAGS_WITH_TYPE )) {
95- $ matchingResult = preg_match (
96- '{^ ' .self ::REGEX_TYPES .'(?:[ \t].*)?$}sx ' ,
97- $ tokens [$ stackPtr + 2 ]['content ' ],
98- $ matches
99- );
103+ if (!in_array ($ tokens [$ stackPtr ]['content ' ], SniffHelper::TAGS_WITH_TYPE )) {
104+ return ;
105+ }
106+
107+ $ matchingResult = preg_match (
108+ '{^ ' .self ::REGEX_TYPES .'(?:[\s\t].*)?$}sx ' ,
109+ $ tokens [$ stackPtr + 2 ]['content ' ],
110+ $ matches
111+ );
100112
101- $ content = 1 === $ matchingResult ? $ matches ['types ' ] : '' ;
102- $ endOfContent = substr ($ tokens [$ stackPtr + 2 ]['content ' ], strlen ($ content ));
113+ $ content = 1 === $ matchingResult ? $ matches ['types ' ] : '' ;
114+ $ endOfContent = substr ($ tokens [$ stackPtr + 2 ]['content ' ], strlen ($ content ));
103115
116+ try {
104117 $ suggestedType = $ this ->getValidTypes ($ content );
118+ } catch (DeepExitException $ exception ) {
119+ $ phpcsFile ->addError (
120+ $ exception ->getMessage (),
121+ $ stackPtr + 2 ,
122+ 'Exception '
123+ );
105124
106- if ($ content !== $ suggestedType ) {
107- $ fix = $ phpcsFile ->addFixableError (
108- 'For type-hinting in PHPDocs, use %s instead of %s ' ,
109- $ stackPtr + 2 ,
110- 'Invalid ' ,
111- [$ suggestedType , $ content ]
112- );
125+ return ;
126+ }
127+
128+ if ($ content !== $ suggestedType ) {
129+ $ fix = $ phpcsFile ->addFixableError (
130+ 'For type-hinting in PHPDocs, use %s instead of %s ' ,
131+ $ stackPtr + 2 ,
132+ 'Invalid ' ,
133+ [$ suggestedType , $ content ]
134+ );
113135
114- if ($ fix ) {
115- $ phpcsFile ->fixer ->replaceToken ($ stackPtr + 2 , $ suggestedType .$ endOfContent );
116- }
136+ if ($ fix ) {
137+ $ phpcsFile ->fixer ->replaceToken ($ stackPtr + 2 , $ suggestedType .$ endOfContent );
117138 }
118139 }
119140 }
@@ -122,26 +143,57 @@ public function process(File $phpcsFile, $stackPtr): void
122143 * @param string $content
123144 *
124145 * @return string
146+ *
147+ * @throws DeepExitException
125148 */
126149 private function getValidTypes (string $ content ): string
127150 {
128151 $ content = preg_replace ('/\s/ ' , '' , $ content );
129- $ types = $ this ->getTypes ($ content );
130152
131- foreach ($ types as $ index => $ type ) {
132- preg_match ('{^ ' .self ::REGEX_TYPES .'$}x ' , $ type , $ matches );
153+ $ types = [];
154+ $ separators = [];
155+ while ('' !== $ content && false !== $ content ) {
156+ preg_match ('{^ ' .self ::REGEX_TYPES .'$}x ' , $ content , $ matches );
133157
134- if (isset ($ matches ['generic ' ]) && '' !== $ matches ['generic ' ]) {
158+ if (isset ($ matches ['multiple ' ]) && '' !== $ matches ['multiple ' ]) {
159+ $ validType = '( ' .$ this ->getValidTypes ($ matches ['mutipleContent ' ]).') ' ;
160+ } elseif (isset ($ matches ['generic ' ]) && '' !== $ matches ['generic ' ]) {
135161 $ validType = $ this ->getValidGenericType ($ matches ['genericName ' ], $ matches ['genericContent ' ]);
136162 } elseif (isset ($ matches ['object ' ]) && '' !== $ matches ['object ' ]) {
137163 $ validType = $ this ->getValidObjectType ($ matches ['objectContent ' ]);
138164 } else {
139- $ validType = $ this ->getValidType ($ type );
165+ $ validType = $ this ->getValidType ($ matches [ ' type ' ] );
140166 }
141167
142- $ types [$ index ] = $ validType ;
168+ $ types [] = $ validType ;
169+
170+ $ separators [] = substr ($ content , strlen ($ matches ['type ' ]), 1 );
171+ $ content = substr ($ content , strlen ($ matches ['type ' ]) + 1 );
143172 }
144173
174+ // Remove last separator since it's an empty string
175+ array_pop ($ separators );
176+
177+ $ uniqueSeparators = array_unique ($ separators );
178+ switch (count ($ uniqueSeparators )) {
179+ case 0 :
180+ return implode ('' , $ types );
181+ case 1 :
182+ return implode ($ uniqueSeparators [0 ], $ this ->orderTypes ($ types ));
183+ default :
184+ throw new DeepExitException (
185+ 'Union and intersection types must be grouped with parenthesis when used in the same expression '
186+ );
187+ }
188+ }
189+
190+ /**
191+ * @param array $types
192+ *
193+ * @return array
194+ */
195+ private function orderTypes (array $ types ): array
196+ {
145197 $ types = array_unique ($ types );
146198 usort ($ types , function ($ type1 , $ type2 ) {
147199 if ('null ' === $ type1 ) {
@@ -155,24 +207,6 @@ private function getValidTypes(string $content): string
155207 return 0 ;
156208 });
157209
158- return implode ('| ' , $ types );
159- }
160-
161- /**
162- * @param string $content
163- *
164- * @return array
165- */
166- private function getTypes (string $ content ): array
167- {
168- $ types = [];
169- while ('' !== $ content && false !== $ content ) {
170- preg_match ('{^ ' .self ::REGEX_TYPES .'$}x ' , $ content , $ matches );
171-
172- $ types [] = $ matches ['type ' ];
173- $ content = substr ($ content , strlen ($ matches ['type ' ]) + 1 );
174- }
175-
176210 return $ types ;
177211 }
178212
0 commit comments