@@ -60,6 +60,7 @@ public function process(File $phpCsFile, $stackPointer): void
60
60
}
61
61
62
62
$ docBlockParams = [];
63
+ $ hasMissingTypes = false ;
63
64
for ($ i = $ docBlockStartIndex + 1 ; $ i < $ docBlockEndIndex ; $ i ++) {
64
65
if ($ tokens [$ i ]['type ' ] !== 'T_DOC_COMMENT_TAG ' ) {
65
66
continue ;
@@ -72,12 +73,21 @@ public function process(File $phpCsFile, $stackPointer): void
72
73
73
74
if ($ tokens [$ classNameIndex ]['type ' ] !== 'T_DOC_COMMENT_STRING ' ) {
74
75
$ phpCsFile ->addError ('Missing type in param doc block ' , $ i , 'MissingType ' );
76
+ $ hasMissingTypes = true ;
75
77
76
78
continue ;
77
79
}
78
80
79
81
$ content = $ tokens [$ classNameIndex ]['content ' ];
80
82
83
+ // Check if the content starts with $ (missing type)
84
+ if (str_starts_with ($ content , '$ ' )) {
85
+ $ phpCsFile ->addError ('Missing type in param doc block ' , $ i , 'MissingType ' );
86
+ $ hasMissingTypes = true ;
87
+
88
+ continue ;
89
+ }
90
+
81
91
$ appendix = '' ;
82
92
$ spacePos = strpos ($ content , ' ' );
83
93
if ($ spacePos ) {
@@ -96,7 +106,27 @@ public function process(File $phpCsFile, $stackPointer): void
96
106
];
97
107
}
98
108
109
+ // If no @param annotations found, check if all parameters are fully typed
110
+ // Only skip validation if all parameters have type declarations
111
+ if (count ($ docBlockParams ) === 0 ) {
112
+ if ($ this ->areAllParametersFullyTyped ($ methodSignature )) {
113
+ return ;
114
+ }
115
+ }
116
+
99
117
if (count ($ docBlockParams ) !== count ($ methodSignature )) {
118
+ // Check if we can fix by adding missing params (when all method params are typed and no missing types in existing params)
119
+ if (!$ hasMissingTypes && count ($ docBlockParams ) < count ($ methodSignature ) && $ this ->canAddMissingParams ($ phpCsFile , $ docBlockStartIndex , $ docBlockEndIndex , $ docBlockParams , $ methodSignature )) {
120
+ return ;
121
+ }
122
+
123
+ // Check if we have extra params that can be removed
124
+ if (count ($ docBlockParams ) > count ($ methodSignature )) {
125
+ $ this ->handleExtraParams ($ phpCsFile , $ docBlockStartIndex , $ docBlockEndIndex , $ docBlockParams , $ methodSignature );
126
+
127
+ return ;
128
+ }
129
+
100
130
$ phpCsFile ->addError ('Doc Block params do not match method signature ' , $ stackPointer , 'SignatureMismatch ' );
101
131
102
132
return ;
@@ -140,7 +170,227 @@ protected function assertNoParams(File $phpCsFile, int $docBlockStartIndex, int
140
170
continue ;
141
171
}
142
172
143
- $ phpCsFile ->addError ('Doc Block param does not match method signature and should be removed ' , $ i , 'ExtraParam ' );
173
+ $ fix = $ phpCsFile ->addFixableError ('Doc Block param does not match method signature and should be removed ' , $ i , 'ExtraParam ' );
174
+
175
+ if ($ fix === true ) {
176
+ $ this ->removeParamLine ($ phpCsFile , $ i );
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Check if all method parameters are fully typed.
183
+ *
184
+ * @param array<int, array<string, mixed>> $methodSignature
185
+ *
186
+ * @return bool
187
+ */
188
+ protected function areAllParametersFullyTyped (array $ methodSignature ): bool
189
+ {
190
+ foreach ($ methodSignature as $ param ) {
191
+ // Parameter must have a type hint
192
+ if (empty ($ param ['typehint ' ])) {
193
+ return false ;
194
+ }
195
+ }
196
+
197
+ return true ;
198
+ }
199
+
200
+ /**
201
+ * @param \PHP_CodeSniffer\Files\File $phpCsFile
202
+ * @param int $docBlockStartIndex
203
+ * @param int $docBlockEndIndex
204
+ * @param array<array<string, mixed>> $docBlockParams
205
+ * @param array<int, array<string, mixed>> $methodSignature
206
+ *
207
+ * @return bool
208
+ */
209
+ protected function canAddMissingParams (File $ phpCsFile , int $ docBlockStartIndex , int $ docBlockEndIndex , array $ docBlockParams , array $ methodSignature ): bool
210
+ {
211
+ $ tokens = $ phpCsFile ->getTokens ();
212
+
213
+ // Check if all params have types so we can add them
214
+ foreach ($ methodSignature as $ param ) {
215
+ if (empty ($ param ['typehintFull ' ])) {
216
+ return false ;
217
+ }
218
+ }
219
+
220
+ // Find the position to insert new params (after last @param or before close comment)
221
+ $ insertPosition = $ docBlockEndIndex - 1 ;
222
+ $ lastParamIndex = null ;
223
+
224
+ for ($ i = $ docBlockStartIndex + 1 ; $ i < $ docBlockEndIndex ; $ i ++) {
225
+ if ($ tokens [$ i ]['type ' ] === 'T_DOC_COMMENT_TAG ' && $ tokens [$ i ]['content ' ] === '@param ' ) {
226
+ $ lastParamIndex = $ i ;
227
+ // Find the end of this param line
228
+ for ($ j = $ i + 1 ; $ j < $ docBlockEndIndex ; $ j ++) {
229
+ if ($ tokens [$ j ]['content ' ] === "\n" || $ tokens [$ j ]['type ' ] === 'T_DOC_COMMENT_CLOSE_TAG ' ) {
230
+ $ insertPosition = $ j ;
231
+
232
+ break ;
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ $ fix = $ phpCsFile ->addFixableError ('Doc Block params do not match method signature ' , $ docBlockStartIndex + 1 , 'SignatureMismatch ' );
239
+
240
+ if ($ fix === true ) {
241
+ $ phpCsFile ->fixer ->beginChangeset ();
242
+
243
+ // Build list of existing param variables
244
+ $ existingVars = [];
245
+ foreach ($ docBlockParams as $ param ) {
246
+ $ existingVars [] = $ param ['variable ' ];
247
+ }
248
+
249
+ // Add missing params
250
+ foreach ($ methodSignature as $ methodParam ) {
251
+ $ variable = $ tokens [$ methodParam ['variableIndex ' ]]['content ' ];
252
+ if (!in_array ($ variable , $ existingVars , true )) {
253
+ $ indent = $ this ->getIndentForParam ($ phpCsFile , $ docBlockStartIndex , $ docBlockEndIndex );
254
+ $ paramLine = "\n" . $ indent . '* @param ' . $ methodParam ['typehintFull ' ] . ' ' . $ variable ;
255
+
256
+ $ phpCsFile ->fixer ->addContentBefore ($ insertPosition , $ paramLine );
257
+ }
258
+ }
259
+
260
+ $ phpCsFile ->fixer ->endChangeset ();
261
+ }
262
+
263
+ return true ;
264
+ }
265
+
266
+ /**
267
+ * @param \PHP_CodeSniffer\Files\File $phpCsFile
268
+ * @param int $paramTagIndex
269
+ *
270
+ * @return void
271
+ */
272
+ protected function removeParamLine (File $ phpCsFile , int $ paramTagIndex ): void
273
+ {
274
+ $ tokens = $ phpCsFile ->getTokens ();
275
+
276
+ $ phpCsFile ->fixer ->beginChangeset ();
277
+
278
+ // Find the start of the line
279
+ $ lineStart = $ paramTagIndex ;
280
+ for ($ i = $ paramTagIndex - 1 ; $ i >= 0 ; $ i --) {
281
+ if ($ tokens [$ i ]['content ' ] === "\n" ) {
282
+ break ;
283
+ }
284
+ $ lineStart = $ i ;
285
+ }
286
+
287
+ // Find the end of the line
288
+ $ lineEnd = $ paramTagIndex ;
289
+ $ count = count ($ tokens );
290
+ for ($ i = $ paramTagIndex + 1 ; $ i < $ count ; $ i ++) {
291
+ $ lineEnd = $ i ;
292
+ if ($ tokens [$ i ]['content ' ] === "\n" ) {
293
+ break ;
294
+ }
295
+ }
296
+
297
+ // Remove the entire line
298
+ for ($ i = $ lineStart ; $ i <= $ lineEnd ; $ i ++) {
299
+ $ phpCsFile ->fixer ->replaceToken ($ i , '' );
300
+ }
301
+
302
+ $ phpCsFile ->fixer ->endChangeset ();
303
+ }
304
+
305
+ /**
306
+ * @param \PHP_CodeSniffer\Files\File $phpCsFile
307
+ * @param int $docBlockStartIndex
308
+ * @param int $docBlockEndIndex
309
+ *
310
+ * @return string
311
+ */
312
+ protected function getIndentForParam (File $ phpCsFile , int $ docBlockStartIndex , int $ docBlockEndIndex ): string
313
+ {
314
+ $ tokens = $ phpCsFile ->getTokens ();
315
+
316
+ // Find an existing @param or use the doc block start
317
+ for ($ i = $ docBlockStartIndex + 1 ; $ i < $ docBlockEndIndex ; $ i ++) {
318
+ if ($ tokens [$ i ]['type ' ] === 'T_DOC_COMMENT_TAG ' ) {
319
+ // Get the indent from this line
320
+ for ($ j = $ i - 1 ; $ j >= 0 ; $ j --) {
321
+ if ($ tokens [$ j ]['content ' ] === "\n" ) {
322
+ if (isset ($ tokens [$ j + 1 ]) && $ tokens [$ j + 1 ]['type ' ] === 'T_DOC_COMMENT_WHITESPACE ' ) {
323
+ return $ tokens [$ j + 1 ]['content ' ];
324
+ }
325
+
326
+ break ;
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ return ' ' ;
333
+ }
334
+
335
+ /**
336
+ * @param \PHP_CodeSniffer\Files\File $phpCsFile
337
+ * @param int $docBlockStartIndex
338
+ * @param int $docBlockEndIndex
339
+ * @param array<array<string, mixed>> $docBlockParams
340
+ * @param array<int, array<string, mixed>> $methodSignature
341
+ *
342
+ * @return void
343
+ */
344
+ protected function handleExtraParams (File $ phpCsFile , int $ docBlockStartIndex , int $ docBlockEndIndex , array $ docBlockParams , array $ methodSignature ): void
345
+ {
346
+ $ tokens = $ phpCsFile ->getTokens ();
347
+
348
+ // Build list of expected param variables
349
+ $ expectedVars = [];
350
+ foreach ($ methodSignature as $ param ) {
351
+ $ expectedVars [] = $ tokens [$ param ['variableIndex ' ]]['content ' ];
352
+ }
353
+
354
+ // Find and mark extra params for removal
355
+ $ hasFixableError = false ;
356
+ for ($ i = $ docBlockStartIndex + 1 ; $ i < $ docBlockEndIndex ; $ i ++) {
357
+ if ($ tokens [$ i ]['type ' ] !== 'T_DOC_COMMENT_TAG ' || $ tokens [$ i ]['content ' ] !== '@param ' ) {
358
+ continue ;
359
+ }
360
+
361
+ // Find the variable name for this @param
362
+ $ variable = null ;
363
+ $ classNameIndex = $ i + 2 ;
364
+ if (isset ($ tokens [$ classNameIndex ]) && $ tokens [$ classNameIndex ]['type ' ] === 'T_DOC_COMMENT_STRING ' ) {
365
+ $ content = $ tokens [$ classNameIndex ]['content ' ];
366
+
367
+ // Check if content starts with $ (missing type)
368
+ if (str_starts_with ($ content , '$ ' )) {
369
+ $ variable = explode (' ' , $ content )[0 ];
370
+ } else {
371
+ // Extract variable from content
372
+ $ spacePos = strpos ($ content , ' ' );
373
+ if ($ spacePos ) {
374
+ $ appendix = substr ($ content , $ spacePos );
375
+ preg_match ('/\$[^\s]+/ ' , $ appendix , $ matches );
376
+ $ variable = $ matches ? $ matches [0 ] : null ;
377
+ }
378
+ }
379
+ }
380
+
381
+ // If this param is not in the expected list, mark for removal
382
+ if ($ variable && !in_array ($ variable , $ expectedVars , true )) {
383
+ $ fix = $ phpCsFile ->addFixableError ('Doc Block param does not match method signature and should be removed ' , $ i , 'ExtraParam ' );
384
+
385
+ if ($ fix === true ) {
386
+ $ hasFixableError = true ;
387
+ $ this ->removeParamLine ($ phpCsFile , $ i );
388
+ }
389
+ }
390
+ }
391
+
392
+ if (!$ hasFixableError ) {
393
+ $ phpCsFile ->addError ('Doc Block params do not match method signature ' , $ docBlockStartIndex + 1 , 'SignatureMismatch ' );
144
394
}
145
395
}
146
396
}
0 commit comments