1
+ <?php
2
+
3
+ declare (strict_types=1 );
4
+
5
+ namespace BestIt \Sniffs \TypeHints ;
6
+
7
+ use PHP_CodeSniffer \Files \File ;
8
+ use PHP_CodeSniffer \Sniffs \Sniff ;
9
+ use PHPStan \PhpDocParser \Ast \Type \IdentifierTypeNode ;
10
+ use PHPStan \PhpDocParser \Ast \Type \IntersectionTypeNode ;
11
+ use PHPStan \PhpDocParser \Ast \Type \ThisTypeNode ;
12
+ use PHPStan \PhpDocParser \Ast \Type \TypeNode ;
13
+ use PHPStan \PhpDocParser \Ast \Type \UnionTypeNode ;
14
+ use SlevomatCodingStandard \Helpers \Annotation \VariableAnnotation ;
15
+ use SlevomatCodingStandard \Helpers \AnnotationHelper ;
16
+ use SlevomatCodingStandard \Helpers \IndentationHelper ;
17
+ use SlevomatCodingStandard \Helpers \TokenHelper ;
18
+ use SlevomatCodingStandard \Helpers \TypeHintHelper ;
19
+ use function array_key_exists ;
20
+ use function array_merge ;
21
+ use function array_reverse ;
22
+ use function array_unique ;
23
+ use function implode ;
24
+ use function in_array ;
25
+ use function sprintf ;
26
+ use function trim ;
27
+ use const T_AS ;
28
+ use const T_DOC_COMMENT_OPEN_TAG ;
29
+ use const T_EQUAL ;
30
+ use const T_FOREACH ;
31
+ use const T_LIST ;
32
+ use const T_OPEN_SHORT_ARRAY ;
33
+ use const T_SEMICOLON ;
34
+ use const T_VARIABLE ;
35
+ use const T_WHILE ;
36
+ use const T_WHITESPACE ;
37
+
38
+ /**
39
+ * Use assertion instead of inline documentation comment.
40
+ *
41
+ * THIS FILE IS "COPIED"!
42
+ *
43
+ * @author blange <[email protected] >
44
+ * @package BestIt\Sniffs\TypeHints
45
+ * @SuppressWarnings(PHPMD)
46
+ */
47
+ class ExplicitAssertionsSniff implements Sniff
48
+ {
49
+
50
+ public const CODE_REQUIRED_EXPLICIT_ASSERTION = 'RequiredExplicitAssertion ' ;
51
+
52
+ /**
53
+ * @return array<int, (int|string)>
54
+ */
55
+ public function register (): array
56
+ {
57
+ return [
58
+ T_DOC_COMMENT_OPEN_TAG ,
59
+ ];
60
+ }
61
+
62
+ /**
63
+ * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
64
+ * @param File $phpcsFile
65
+ * @param int $docCommentOpenPointer
66
+ */
67
+ public function process (File $ phpcsFile , $ docCommentOpenPointer ): void
68
+ {
69
+ $ tokens = $ phpcsFile ->getTokens ();
70
+
71
+ $ tokenCodes = [T_VARIABLE , T_FOREACH , T_WHILE , T_LIST , T_OPEN_SHORT_ARRAY ];
72
+ $ commentClosePointer = $ tokens [$ docCommentOpenPointer ]['comment_closer ' ];
73
+
74
+ $ codePointer = TokenHelper::findFirstNonWhitespaceOnNextLine ($ phpcsFile , $ commentClosePointer );
75
+
76
+ if ($ codePointer === null || !in_array ($ tokens [$ codePointer ]['code ' ], $ tokenCodes , true )) {
77
+ $ firstPointerOnPreviousLine = TokenHelper::findFirstNonWhitespaceOnPreviousLine ($ phpcsFile , $ docCommentOpenPointer );
78
+ if ($ firstPointerOnPreviousLine === null || !in_array ($ tokens [$ firstPointerOnPreviousLine ]['code ' ], $ tokenCodes , true )) {
79
+ return ;
80
+ }
81
+
82
+ $ codePointer = $ firstPointerOnPreviousLine ;
83
+ }
84
+
85
+ $ variableAnnotations = AnnotationHelper::getAnnotationsByName ($ phpcsFile , $ docCommentOpenPointer , '@var ' );
86
+ if (count ($ variableAnnotations ) === 0 ) {
87
+ return ;
88
+ }
89
+
90
+ /** @var VariableAnnotation $variableAnnotation */
91
+ foreach (array_reverse ($ variableAnnotations ) as $ variableAnnotation ) {
92
+ if ($ variableAnnotation ->isInvalid ()) {
93
+ continue ;
94
+ }
95
+
96
+ if ($ variableAnnotation ->getVariableName () === null ) {
97
+ continue ;
98
+ }
99
+
100
+ $ variableAnnotationType = $ variableAnnotation ->getType ();
101
+
102
+ if ($ variableAnnotationType instanceof UnionTypeNode || $ variableAnnotationType instanceof IntersectionTypeNode) {
103
+ foreach ($ variableAnnotationType ->types as $ typeNode ) {
104
+ if (!$ this ->isValidTypeNode ($ typeNode )) {
105
+ continue 2 ;
106
+ }
107
+ }
108
+ } elseif (!$ this ->isValidTypeNode ($ variableAnnotationType )) {
109
+ continue ;
110
+ }
111
+
112
+ if ($ tokens [$ codePointer ]['code ' ] === T_VARIABLE ) {
113
+ $ pointerAfterVariable = TokenHelper::findNextEffective ($ phpcsFile , $ codePointer + 1 );
114
+ if ($ tokens [$ pointerAfterVariable ]['code ' ] !== T_EQUAL ) {
115
+ continue ;
116
+ }
117
+
118
+ if ($ variableAnnotation ->getVariableName () !== $ tokens [$ codePointer ]['content ' ]) {
119
+ continue ;
120
+ }
121
+
122
+ $ pointerToAddAssertion = TokenHelper::findNext ($ phpcsFile , T_SEMICOLON , $ codePointer + 1 );
123
+ $ indentation = IndentationHelper::getIndentation ($ phpcsFile , $ docCommentOpenPointer );
124
+
125
+ } elseif ($ tokens [$ codePointer ]['code ' ] === T_LIST ) {
126
+ $ listParenthesisOpener = TokenHelper::findNextEffective ($ phpcsFile , $ codePointer + 1 );
127
+
128
+ $ variablePointerInList = TokenHelper::findNextContent ($ phpcsFile , T_VARIABLE , $ variableAnnotation ->getVariableName (), $ listParenthesisOpener + 1 , $ tokens [$ listParenthesisOpener ]['parenthesis_closer ' ]);
129
+ if ($ variablePointerInList === null ) {
130
+ continue ;
131
+ }
132
+
133
+ $ pointerToAddAssertion = TokenHelper::findNext ($ phpcsFile , T_SEMICOLON , $ codePointer + 1 );
134
+ $ indentation = IndentationHelper::getIndentation ($ phpcsFile , $ docCommentOpenPointer );
135
+
136
+ } elseif ($ tokens [$ codePointer ]['code ' ] === T_OPEN_SHORT_ARRAY ) {
137
+ $ pointerAfterList = TokenHelper::findNextEffective ($ phpcsFile , $ tokens [$ codePointer ]['bracket_closer ' ] + 1 );
138
+ if ($ tokens [$ pointerAfterList ]['code ' ] !== T_EQUAL ) {
139
+ continue ;
140
+ }
141
+
142
+ $ variablePointerInList = TokenHelper::findNextContent ($ phpcsFile , T_VARIABLE , $ variableAnnotation ->getVariableName (), $ codePointer + 1 , $ tokens [$ codePointer ]['bracket_closer ' ]);
143
+ if ($ variablePointerInList === null ) {
144
+ continue ;
145
+ }
146
+
147
+ $ pointerToAddAssertion = TokenHelper::findNext ($ phpcsFile , T_SEMICOLON , $ tokens [$ codePointer ]['bracket_closer ' ] + 1 );
148
+ $ indentation = IndentationHelper::getIndentation ($ phpcsFile , $ docCommentOpenPointer );
149
+
150
+ } else {
151
+ if ($ tokens [$ codePointer ]['code ' ] === T_WHILE ) {
152
+ $ variablePointerInWhile = TokenHelper::findNextContent ($ phpcsFile , T_VARIABLE , $ variableAnnotation ->getVariableName (), $ tokens [$ codePointer ]['parenthesis_opener ' ] + 1 , $ tokens [$ codePointer ]['parenthesis_closer ' ]);
153
+ if ($ variablePointerInWhile === null ) {
154
+ continue ;
155
+ }
156
+
157
+ $ pointerAfterVariableInWhile = TokenHelper::findNextEffective ($ phpcsFile , $ variablePointerInWhile + 1 );
158
+ if ($ tokens [$ pointerAfterVariableInWhile ]['code ' ] !== T_EQUAL ) {
159
+ continue ;
160
+ }
161
+ } else {
162
+ $ asPointer = TokenHelper::findNext ($ phpcsFile , T_AS , $ tokens [$ codePointer ]['parenthesis_opener ' ] + 1 , $ tokens [$ codePointer ]['parenthesis_closer ' ]);
163
+ $ variablePointerInForeach = TokenHelper::findNextContent ($ phpcsFile , T_VARIABLE , $ variableAnnotation ->getVariableName (), $ asPointer + 1 , $ tokens [$ codePointer ]['parenthesis_closer ' ]);
164
+ if ($ variablePointerInForeach === null ) {
165
+ continue ;
166
+ }
167
+ }
168
+
169
+ $ pointerToAddAssertion = $ tokens [$ codePointer ]['scope_opener ' ];
170
+ $ indentation = IndentationHelper::addIndentation (IndentationHelper::getIndentation ($ phpcsFile , $ codePointer ));
171
+ }
172
+
173
+ $ fix = $ phpcsFile ->addFixableError ('Use assertion instead of inline documentation comment. ' , $ variableAnnotation ->getStartPointer (), self ::CODE_REQUIRED_EXPLICIT_ASSERTION );
174
+ if (!$ fix ) {
175
+ continue ;
176
+ }
177
+
178
+ $ phpcsFile ->fixer ->beginChangeset ();
179
+
180
+ for ($ i = $ variableAnnotation ->getStartPointer (); $ i <= $ variableAnnotation ->getEndPointer (); $ i ++) {
181
+ $ phpcsFile ->fixer ->replaceToken ($ i , '' );
182
+ }
183
+
184
+ $ docCommentUseful = false ;
185
+ $ docCommentClosePointer = $ tokens [$ docCommentOpenPointer ]['comment_closer ' ];
186
+ for ($ i = $ docCommentOpenPointer + 1 ; $ i < $ docCommentClosePointer ; $ i ++) {
187
+ $ tokenContent = trim ($ phpcsFile ->fixer ->getTokenContent ($ i ));
188
+ if ($ tokenContent === '' || $ tokenContent === '* ' ) {
189
+ continue ;
190
+ }
191
+
192
+ $ docCommentUseful = true ;
193
+ break ;
194
+ }
195
+
196
+ $ pointerBeforeDocComment = TokenHelper::findPreviousContent ($ phpcsFile , T_WHITESPACE , $ phpcsFile ->eolChar , $ docCommentOpenPointer - 1 );
197
+ $ pointerAfterDocComment = TokenHelper::findNextContent ($ phpcsFile , T_WHITESPACE , $ phpcsFile ->eolChar , $ docCommentClosePointer + 1 );
198
+
199
+ if (!$ docCommentUseful ) {
200
+ for ($ i = $ pointerBeforeDocComment + 1 ; $ i <= $ pointerAfterDocComment ; $ i ++) {
201
+ $ phpcsFile ->fixer ->replaceToken ($ i , '' );
202
+ }
203
+ }
204
+
205
+ /** @var IdentifierTypeNode|ThisTypeNode|UnionTypeNode $variableAnnotationType */
206
+ $ variableAnnotationType = $ variableAnnotationType ;
207
+
208
+ $ assertion = $ this ->createAssert ($ variableAnnotation ->getVariableName (), $ variableAnnotationType );
209
+
210
+ if ($ pointerToAddAssertion < $ docCommentClosePointer && array_key_exists ($ pointerAfterDocComment + 1 , $ tokens )) {
211
+ $ phpcsFile ->fixer ->addContentBefore (
212
+ $ pointerAfterDocComment + 1 ,
213
+ $ indentation . $ assertion . $ phpcsFile ->eolChar
214
+ );
215
+ } else {
216
+ $ phpcsFile ->fixer ->addContent (
217
+ $ pointerToAddAssertion ,
218
+ $ phpcsFile ->eolChar . $ indentation . $ assertion
219
+ );
220
+ }
221
+
222
+ $ phpcsFile ->fixer ->endChangeset ();
223
+ }
224
+ }
225
+
226
+ private function isValidTypeNode (TypeNode $ typeNode ): bool
227
+ {
228
+ if ($ typeNode instanceof ThisTypeNode) {
229
+ return true ;
230
+ }
231
+
232
+ if (!$ typeNode instanceof IdentifierTypeNode) {
233
+ return false ;
234
+ }
235
+
236
+ return !in_array ($ typeNode ->name , ['mixed ' , 'static ' ], true );
237
+ }
238
+
239
+ /**
240
+ * @param string $variableName
241
+ * @param IdentifierTypeNode|ThisTypeNode|UnionTypeNode|IntersectionTypeNode $typeNode
242
+ * @return string
243
+ */
244
+ private function createAssert (string $ variableName , TypeNode $ typeNode ): string
245
+ {
246
+ $ conditions = [];
247
+
248
+ if ($ typeNode instanceof IdentifierTypeNode || $ typeNode instanceof ThisTypeNode) {
249
+ $ conditions = $ this ->createConditions ($ variableName , $ typeNode );
250
+ } else {
251
+ /** @var IdentifierTypeNode|ThisTypeNode $innerTypeNode */
252
+ foreach ($ typeNode ->types as $ innerTypeNode ) {
253
+ $ conditions = array_merge ($ conditions , $ this ->createConditions ($ variableName , $ innerTypeNode ));
254
+ }
255
+ }
256
+
257
+ $ operator = $ typeNode instanceof IntersectionTypeNode ? '&& ' : '|| ' ;
258
+
259
+ return sprintf ('assert(%s); ' , implode (sprintf (' %s ' , $ operator ), array_unique ($ conditions )));
260
+ }
261
+
262
+ /**
263
+ * @param string $variableName
264
+ * @param IdentifierTypeNode|ThisTypeNode $typeNode
265
+ * @return string[]
266
+ */
267
+ private function createConditions (string $ variableName , TypeNode $ typeNode ): array
268
+ {
269
+ if ($ typeNode instanceof ThisTypeNode) {
270
+ return [sprintf ('%s instanceof $this ' , $ variableName )];
271
+ }
272
+
273
+ if ($ typeNode ->name === 'self ' ) {
274
+ return [sprintf ('%s instanceof %s ' , $ variableName , $ typeNode ->name )];
275
+ }
276
+
277
+ if (TypeHintHelper::isSimpleTypeHint ($ typeNode ->name )) {
278
+ return [sprintf ('\is_%s(%s) ' , $ typeNode ->name , $ variableName )];
279
+ }
280
+
281
+ if (in_array ($ typeNode ->name , ['resource ' , 'object ' ], true )) {
282
+ return [sprintf ('\is_%s(%s) ' , $ typeNode ->name , $ variableName )];
283
+ }
284
+
285
+ if (in_array ($ typeNode ->name , ['true ' , 'false ' , 'null ' ], true )) {
286
+ return [sprintf ('%s === %s ' , $ variableName , $ typeNode ->name )];
287
+ }
288
+
289
+ if ($ typeNode ->name === 'numeric ' ) {
290
+ return [
291
+ sprintf ('\is_int(%s) ' , $ variableName ),
292
+ sprintf ('\is_float(%s) ' , $ variableName ),
293
+ ];
294
+ }
295
+
296
+ if ($ typeNode ->name === 'scalar ' ) {
297
+ return [
298
+ sprintf ('\is_int(%s) ' , $ variableName ),
299
+ sprintf ('\is_float(%s) ' , $ variableName ),
300
+ sprintf ('\is_bool(%s) ' , $ variableName ),
301
+ sprintf ('\is_string(%s) ' , $ variableName ),
302
+ ];
303
+ }
304
+
305
+ return [sprintf ('%s instanceof %s ' , $ variableName , $ typeNode ->name )];
306
+ }
307
+
308
+ }
0 commit comments