1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace BestIt\Sniffs\Commenting; |
6
|
|
|
|
7
|
|
|
use BestIt\CodeSniffer\CodeWarning; |
8
|
|
|
use BestIt\Sniffs\AbstractSniff; |
9
|
|
|
use BestIt\Sniffs\DocPosProviderTrait; |
10
|
|
|
use SlevomatCodingStandard\Helpers\TokenHelper; |
11
|
|
|
use function ucfirst; |
12
|
|
|
use const T_DOC_COMMENT_OPEN_TAG; |
13
|
|
|
use const T_DOC_COMMENT_STRING; |
14
|
|
|
use const T_DOC_COMMENT_TAG; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* The basic sniff for the summaries. |
18
|
|
|
* |
19
|
|
|
* @author blange <[email protected]> |
20
|
|
|
* @package BestIt\Sniffs\Commenting |
21
|
|
|
*/ |
22
|
|
|
abstract class AbstractDocSniff extends AbstractSniff |
23
|
|
|
{ |
24
|
|
|
use DocPosProviderTrait; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Code that the doc comment starts with an capital letter. |
28
|
|
|
* |
29
|
|
|
* @var string |
30
|
|
|
*/ |
31
|
|
|
public const CODE_DOC_COMMENT_UC_FIRST = 'DocCommentUcFirst'; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Code that there is no line after the doc comment. |
35
|
|
|
* |
36
|
|
|
* @var string |
37
|
|
|
*/ |
38
|
|
|
public const CODE_NO_LINE_AFTER_DOC_COMMENT = 'NoLineAfterDocComment'; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Code that there is no summary in doc comment. |
42
|
|
|
* |
43
|
|
|
* @var string |
44
|
|
|
*/ |
45
|
|
|
public const CODE_NO_SUMMARY = 'NoSummary'; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Error code if the summary is too long. |
49
|
|
|
* |
50
|
|
|
* @var string |
51
|
|
|
*/ |
52
|
|
|
public const CODE_SUMMARY_TOO_LONG = 'SummaryTooLong'; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Message that the doc comments does not start with an capital letter. |
56
|
|
|
* |
57
|
|
|
* @var string |
58
|
|
|
*/ |
59
|
|
|
private const MESSAGE_DOC_COMMENT_UC_FIRST = 'The first letter of the summary/long-description is not uppercase.'; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Message that there is no line after the doc comment. |
63
|
|
|
* |
64
|
|
|
* @var string |
65
|
|
|
*/ |
66
|
|
|
private const MESSAGE_NO_LINE_AFTER_DOC_COMMENT = 'There is no empty line after the summary/long-description.'; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Message that there is no summary in doc comment. |
70
|
|
|
* |
71
|
|
|
* @var string |
72
|
|
|
*/ |
73
|
|
|
private const MESSAGE_NO_SUMMARY = 'There must be a summary in the doc comment.'; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* The error message if the summary is too long. |
77
|
|
|
* |
78
|
|
|
* @var string |
79
|
|
|
*/ |
80
|
|
|
private const MESSAGE_SUMMARY_TOO_LONG = 'The summary should fit in one line. If you want more, use the long desc.'; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* The cached position of the summary. |
84
|
|
|
* |
85
|
|
|
* @var int|null |
86
|
|
|
*/ |
87
|
|
|
private $summaryPosition = -1; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Returns true if there is a doc block. |
91
|
|
|
* |
92
|
|
|
* @return bool |
93
|
|
|
*/ |
94
|
|
|
protected function areRequirementsMet(): bool |
95
|
|
|
{ |
96
|
|
|
$docHelper = $this->getDocHelper(); |
97
|
|
|
|
98
|
|
|
return $docHelper->hasDocBlock() && $docHelper->isMultiLine(); |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* Fixes the first letter of the doc comment, which must be uppercase. |
103
|
|
|
* |
104
|
|
|
* @param int $position |
105
|
|
|
* @param array $token |
106
|
|
|
* |
107
|
|
|
* @return void |
108
|
|
|
*/ |
109
|
|
|
private function fixDocCommentUcFirst(int $position, array $token) |
110
|
|
|
{ |
111
|
|
|
$this->file->fixer->beginChangeset(); |
112
|
|
|
$this->file->fixer->replaceToken($position, ucfirst($token['content'])); |
113
|
|
|
$this->file->fixer->endChangeset(); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Fixes no line after doc comment. |
118
|
|
|
* |
119
|
|
|
* @param int $position |
120
|
|
|
* @param array $token |
121
|
|
|
* |
122
|
|
|
* @return void |
123
|
|
|
*/ |
124
|
|
|
private function fixNoLineAfterDocComment(int $position, array $token) |
125
|
|
|
{ |
126
|
|
|
$this->file->fixer->beginChangeset(); |
127
|
|
|
|
128
|
|
|
$this->file->fixer->addContent( |
129
|
|
|
$position, |
130
|
|
|
$this->file->getEolChar() . str_repeat(' ', $token['level']) . ' *' |
131
|
|
|
); |
132
|
|
|
|
133
|
|
|
$this->file->fixer->endChangeset(); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* Returns the position of the summary or null. |
138
|
|
|
* |
139
|
|
|
* @return int|null |
140
|
|
|
*/ |
141
|
|
|
private function getSummaryPosition(): ?int |
142
|
|
|
{ |
143
|
|
|
if ($this->summaryPosition === -1) { |
144
|
|
|
$this->summaryPosition = $this->loadSummaryPosition(); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
return $this->summaryPosition; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Returns true if the next line of the comment is empty. |
152
|
|
|
* |
153
|
|
|
* @param int $startPosition The position where to start the search. |
154
|
|
|
* |
155
|
|
|
* @return bool |
156
|
|
|
*/ |
157
|
|
|
private function isNextLineEmpty(int $startPosition): bool |
158
|
|
|
{ |
159
|
|
|
$istNextLineEmpty = true; |
160
|
|
|
$nextRelevantPos = $this->loadNextDocBlockContent($startPosition); |
161
|
|
|
|
162
|
|
|
if ($nextRelevantPos !== -1) { |
163
|
|
|
$istNextLineEmpty = $this->tokens[$startPosition]['line'] + 1 < $this->tokens[$nextRelevantPos]['line']; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
return $istNextLineEmpty; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* Returns true if the prev line of the comment is empty. |
171
|
|
|
* |
172
|
|
|
* @param int $startPosition The position where to start the search. |
173
|
|
|
* |
174
|
|
|
* @return bool |
175
|
|
|
*/ |
176
|
|
|
private function isPrevLineEmpty(int $startPosition): bool |
177
|
|
|
{ |
178
|
|
|
$isPrevLineEmpty = true; |
179
|
|
|
$posPrevContentPos = $this->loadPrevDocBlockContent($startPosition); |
180
|
|
|
|
181
|
|
|
if ($posPrevContentPos !== -1) { |
182
|
|
|
$isPrevLineEmpty = $this->tokens[$startPosition]['line'] - 1 > $this->tokens[$posPrevContentPos]['line']; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
return $isPrevLineEmpty; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Is the given token a simple comment node? |
190
|
|
|
* |
191
|
|
|
* @param array $possCommentToken |
192
|
|
|
* |
193
|
|
|
* @return bool |
194
|
|
|
*/ |
195
|
|
|
private function isSimpleText(array $possCommentToken): bool |
196
|
|
|
{ |
197
|
|
|
return $possCommentToken['code'] === T_DOC_COMMENT_STRING; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Returns the position of the next whitespace or star of the comment for checking the line after that. |
202
|
|
|
* |
203
|
|
|
* @param int $startPosition |
204
|
|
|
* |
205
|
|
|
* @return int |
206
|
|
|
*/ |
207
|
|
|
private function loadNextDocBlockContent(int $startPosition): int |
208
|
|
|
{ |
209
|
|
|
return $this->file->findNext( |
210
|
|
|
[ |
211
|
|
|
T_DOC_COMMENT_WHITESPACE, |
212
|
|
|
T_DOC_COMMENT_STAR |
213
|
|
|
], |
214
|
|
|
$startPosition + 1, |
215
|
|
|
$this->getDocHelper()->getBlockEndPosition(), |
216
|
|
|
true |
217
|
|
|
); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Returns the position of the previous whitespace or star of the comment for checking the line after that. |
222
|
|
|
* |
223
|
|
|
* @param int $startPosition |
224
|
|
|
* |
225
|
|
|
* @return int |
226
|
|
|
*/ |
227
|
|
|
private function loadPrevDocBlockContent(int $startPosition): int |
228
|
|
|
{ |
229
|
|
|
return $this->file->findPrevious( |
230
|
|
|
[ |
231
|
|
|
T_DOC_COMMENT_OPEN_TAG, |
232
|
|
|
T_DOC_COMMENT_STAR, |
233
|
|
|
T_DOC_COMMENT_WHITESPACE, |
234
|
|
|
], |
235
|
|
|
$startPosition - 1, |
236
|
|
|
$this->getDocCommentPos(), |
237
|
|
|
true |
238
|
|
|
); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Loads the position of the summary token if possible. |
243
|
|
|
* |
244
|
|
|
* @return int|null |
245
|
|
|
*/ |
246
|
|
|
private function loadSummaryPosition(): ?int |
247
|
|
|
{ |
248
|
|
|
$return = null; |
249
|
|
|
$possSummaryPos = $this->loadNextDocBlockContent($this->getDocCommentPos()); |
250
|
|
|
|
251
|
|
|
if ((int) $possSummaryPos > 0) { |
252
|
|
|
$possSummaryToken = $this->tokens[$possSummaryPos]; |
253
|
|
|
|
254
|
|
|
$return = $this->isSimpleText($possSummaryToken) ? $possSummaryPos : null; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
return $return; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* Checks and registers errors if there are invalid doc comments. |
262
|
|
|
* |
263
|
|
|
* @throws CodeWarning |
264
|
|
|
* |
265
|
|
|
* @return void |
266
|
|
|
*/ |
267
|
|
|
protected function processToken(): void |
268
|
|
|
{ |
269
|
|
|
$this |
270
|
|
|
->validateSummaryExistence() |
271
|
|
|
->validateDescriptions(); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* Resets the sniff after one processing. |
276
|
|
|
* |
277
|
|
|
* @return void |
278
|
|
|
*/ |
279
|
|
|
protected function tearDown(): void |
280
|
|
|
{ |
281
|
|
|
$this->resetDocCommentPos(); |
282
|
|
|
$this->summaryPosition = -1; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Validates the descriptions in the file. |
287
|
|
|
* |
288
|
|
|
* @return AbstractDocSniff |
289
|
|
|
*/ |
290
|
|
|
private function validateDescriptions(): self |
291
|
|
|
{ |
292
|
|
|
$commentPoss = TokenHelper::findNextAll( |
293
|
|
|
$this->file->getBaseFile(), |
294
|
|
|
[T_DOC_COMMENT_STRING, T_DOC_COMMENT_TAG], |
295
|
|
|
$this->getDocCommentPos(), |
296
|
|
|
$this->getDocHelper()->getBlockEndPosition() |
|
|
|
|
297
|
|
|
); |
298
|
|
|
|
299
|
|
|
foreach ($commentPoss as $index => $commentPos) { |
300
|
|
|
$commentToken = $this->tokens[$commentPos]; |
301
|
|
|
$skipNewLineCheck = false; |
302
|
|
|
|
303
|
|
|
// We only search till the tags. |
304
|
|
|
if ($commentToken['code'] === T_DOC_COMMENT_TAG) { |
305
|
|
|
break; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
if ($isFirstDocString = $index === 0) { |
309
|
|
|
$skipNewLineCheck = !$this->validateOneLineSummary(); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
$this->validateUCFirstDocComment($commentPos, $commentToken); |
313
|
|
|
|
314
|
|
|
if (!$skipNewLineCheck) { |
315
|
|
|
$this->validateNewLineAfterDocComment($commentPos, $commentToken, $isFirstDocString); |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
return $this; |
320
|
|
|
|
321
|
|
|
; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* Checks if there is a line break after the comment block.. |
326
|
|
|
* |
327
|
|
|
* @param int $position |
328
|
|
|
* @param array $token |
329
|
|
|
* @param bool $asSingleLine |
330
|
|
|
* |
331
|
|
|
* @return void |
332
|
|
|
*/ |
333
|
|
|
private function validateNewLineAfterDocComment(int $position, array $token, bool $asSingleLine = true): void |
334
|
|
|
{ |
335
|
|
|
if (!$this->isNextLineEmpty($position)) { |
336
|
|
|
$nextRelevantPos = $this->loadNextDocBlockContent($position); |
337
|
|
|
$nextToken = $this->tokens[$nextRelevantPos]; |
338
|
|
|
|
339
|
|
|
// Register an error if we force a single line or this is no long description with more then one line. |
340
|
|
|
if ($asSingleLine || ($nextToken['code'] !== T_DOC_COMMENT_STRING)) { |
341
|
|
|
$isFixing = $this->file->addFixableWarning( |
342
|
|
|
self::MESSAGE_NO_LINE_AFTER_DOC_COMMENT, |
343
|
|
|
$position, |
344
|
|
|
static::CODE_NO_LINE_AFTER_DOC_COMMENT |
345
|
|
|
); |
346
|
|
|
|
347
|
|
|
if ($isFixing) { |
348
|
|
|
$this->fixNoLineAfterDocComment($position, $token); |
349
|
121 |
|
} |
350
|
|
|
} |
351
|
121 |
|
} |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* Checks if the summary is on line or registers a warning. |
356
|
|
|
* |
357
|
|
|
* @return bool We can skip the new line error, so return true if the one line summary is true. |
358
|
|
|
*/ |
359
|
121 |
|
private function validateOneLineSummary(): bool |
360
|
|
|
{ |
361
|
121 |
|
$isValid = true; |
362
|
|
|
$summaryPos = $this->getSummaryPosition(); |
363
|
121 |
|
$nextPossiblePos = $this->loadNextDocBlockContent($summaryPos); |
364
|
121 |
|
|
365
|
|
|
if ($nextPossiblePos > -1) { |
366
|
1 |
|
$nextToken = $this->tokens[$nextPossiblePos]; |
367
|
|
|
|
368
|
|
|
if (($nextToken['code'] === T_DOC_COMMENT_STRING) && !$this->isNextLineEmpty($summaryPos)) { |
369
|
121 |
|
$isValid = false; |
370
|
121 |
|
|
371
|
121 |
|
$this->file->addWarning( |
372
|
121 |
|
self::MESSAGE_SUMMARY_TOO_LONG, |
373
|
121 |
|
$nextPossiblePos, |
374
|
121 |
|
static::CODE_SUMMARY_TOO_LONG |
375
|
|
|
); |
376
|
121 |
|
} |
377
|
|
|
} |
378
|
121 |
|
|
379
|
121 |
|
return $isValid; |
380
|
|
|
} |
381
|
8 |
|
|
382
|
|
|
/** |
383
|
|
|
* Returns position to the comment summary or null. |
384
|
113 |
|
* |
385
|
|
|
* @throws CodeWarning If there is no summary. |
386
|
113 |
|
* |
387
|
113 |
|
* @return $this |
388
|
|
|
*/ |
389
|
|
|
private function validateSummaryExistence(): self |
390
|
113 |
|
{ |
391
|
113 |
|
$summaryPos = $this->getSummaryPosition(); |
392
|
113 |
|
|
393
|
|
|
if (!$summaryPos) { |
|
|
|
|
394
|
113 |
|
throw new CodeWarning( |
395
|
|
|
static::CODE_NO_SUMMARY, |
396
|
|
|
self::MESSAGE_NO_SUMMARY, |
397
|
|
|
$this->getDocCommentPos() |
398
|
|
|
); |
399
|
|
|
} |
400
|
|
|
|
401
|
65 |
|
return $this; |
402
|
|
|
} |
403
|
65 |
|
|
404
|
|
|
/** |
405
|
|
|
* Checks if the first char of the doc comment is ucfirst. |
406
|
|
|
* |
407
|
|
|
* @param int $position |
408
|
|
|
* @param array $token |
409
|
|
|
* |
410
|
|
|
* @return void |
411
|
|
|
*/ |
412
|
|
|
private function validateUCFirstDocComment(int $position, array $token): void |
413
|
|
|
{ |
414
|
|
|
$commentText = $token['content']; |
415
|
|
|
|
416
|
|
|
if (ucfirst($commentText) !== $commentText && $this->isPrevLineEmpty($position)) { |
417
|
|
|
$isFixing = $this->file->addFixableWarning( |
418
|
|
|
self::MESSAGE_DOC_COMMENT_UC_FIRST, |
419
|
|
|
$position, |
420
|
|
|
static::CODE_DOC_COMMENT_UC_FIRST |
421
|
|
|
); |
422
|
|
|
|
423
|
|
|
if ($isFixing) { |
424
|
|
|
$this->fixDocCommentUcFirst($position, $token); |
425
|
|
|
} |
426
|
|
|
} |
427
|
|
|
} |
428
|
|
|
} |
429
|
|
|
|
This check looks at variables that are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.