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