Passed
Pull Request — master (#77)
by
unknown
04:40
created

AbstractDocSniff::validateUCFirstDocComment()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 2
cts 2
cp 1
rs 9.7333
c 0
b 0
f 0
cc 4
nc 3
nop 2
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BestIt\Sniffs\Commenting;
6
7
use BestIt\CodeSniffer\CodeWarning;
8
use BestIt\CodeSniffer\Helper\TokenHelper;
9
use BestIt\Sniffs\AbstractSniff;
10
use BestIt\Sniffs\DocPosProviderTrait;
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
     * Every doc comment block SHOULD start ucfirst.
28
     */
29
    public const CODE_DOC_COMMENT_UC_FIRST = 'DocCommentUcFirst';
30
31
    /**
32
     * Every doc comment block (the summary or a long description paragrah) SHOULD finish with double newline.
33
     */
34
    public const CODE_NO_LINE_AFTER_DOC_COMMENT = 'NoLineAfterDocComment';
35
36
    /**
37
     * There SHOULD be a summary.
38
     */
39
    public const CODE_NO_SUMMARY = 'NoSummary';
40
41
    /**
42
     * The summary SHOULD be in one line.
43
     */
44
    public const CODE_SUMMARY_TOO_LONG = 'SummaryTooLong';
45
46
    /**
47
     * Message that the doc comments does not start with an capital letter.
48
     */
49
    private const MESSAGE_DOC_COMMENT_UC_FIRST = 'The first letter of the summary/long-description is not uppercase.';
50
51
    /**
52
     * Message that there is no line after the doc comment.
53
     */
54
    private const MESSAGE_NO_LINE_AFTER_DOC_COMMENT = 'There is no empty line after the summary/long-description.';
55
56
    /**
57
     * Message that there is no summary in doc comment.
58
     */
59
    private const MESSAGE_NO_SUMMARY = 'There must be a summary in the doc comment.';
60
61
    /**
62
     * The error message if the summary is too long.
63
     */
64
    private const MESSAGE_SUMMARY_TOO_LONG = 'The summary should fit in one line. If you want more, use the long desc.';
65
66
    /**
67
     * The cached position of the summary.
68
     */
69
    private ?int $summaryPosition = null;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected '?', expecting T_FUNCTION or T_CONST
Loading history...
70
71
    /**
72
     * Returns true if there is a doc block.
73
     *
74
     * @return bool
75
     */
76
    protected function areRequirementsMet(): bool
77
    {
78
        $docHelper = $this->getDocHelper();
79
80
        return $docHelper->hasDocBlock() && $docHelper->isMultiLine();
81
    }
82
83
    /**
84
     * Fixes the first letter of the doc comment, which must be uppercase.
85
     *
86
     * @param int $position
87
     * @param array $token
88
     *
89
     * @return void
90
     */
91
    private function fixDocCommentUcFirst(int $position, array $token): void
92
    {
93
        $this->file->fixer->beginChangeset();
94
        $this->file->fixer->replaceToken($position, ucfirst($token['content']));
95
        $this->file->fixer->endChangeset();
96
    }
97
98
    /**
99
     * Fixes no line after doc comment.
100
     *
101
     * @param int $position
102
     * @param array $token
103
     *
104
     * @return void
105
     */
106
    private function fixNoLineAfterDocComment(int $position, array $token): void
107
    {
108
        $this->file->fixer->beginChangeset();
109
110
        $this->file->fixer->addContent(
111
            $position,
112
            $this->file->getEolChar() . str_repeat('    ', $token['level']) . ' *'
113
        );
114
115
        $this->file->fixer->endChangeset();
116
    }
117
118
    /**
119
     * Returns the position of the summary or null.
120
     *
121
     * @return int|null
122
     */
123
    private function getSummaryPosition(): ?int
124
    {
125
        if ($this->summaryPosition === null) {
126
            $this->summaryPosition = $this->loadSummaryPosition();
127
        }
128
129
        return $this->summaryPosition;
130
    }
131
132
    /**
133
     * Returns true if the next line of the comment is empty.
134
     *
135
     * @param int $startPosition The position where to start the search.
136
     *
137
     * @return bool
138
     */
139
    private function isNextLineEmpty(int $startPosition): bool
140
    {
141
        $istNextLineEmpty = true;
142
        $nextRelevantPos = $this->loadNextDocBlockContent($startPosition);
143
144
        if ($nextRelevantPos !== false) {
145
            $istNextLineEmpty = $this->tokens[$startPosition]['line'] + 1 < $this->tokens[$nextRelevantPos]['line'];
146
        }
147
148
        return $istNextLineEmpty;
149
    }
150
151
    /**
152
     * Returns true if the prev line of the comment is empty.
153
     *
154
     * @param int $startPosition The position where to start the search.
155
     *
156
     * @return bool
157
     */
158
    private function isPrevLineEmpty(int $startPosition): bool
159
    {
160
        $isPrevLineEmpty = true;
161
        $posPrevContentPos = $this->loadPrevDocBlockContent($startPosition);
162
163
        if ($posPrevContentPos !== false) {
164
            $isPrevLineEmpty = $this->tokens[$startPosition]['line'] - 1 > $this->tokens[$posPrevContentPos]['line'];
165
        }
166
167
        return $isPrevLineEmpty;
168
    }
169
170
    /**
171
     * Is the given token a simple comment node?
172
     *
173
     * @param array $possCommentToken
174
     *
175
     * @return bool
176
     */
177
    private function isSimpleText(array $possCommentToken): bool
178
    {
179
        return $possCommentToken['code'] === T_DOC_COMMENT_STRING;
180
    }
181
182
    /**
183
     * Returns the position of the next whitespace or star of the comment for checking the line after that.
184
     *
185
     * @param int $startPosition
186
     *
187
     * @return int|bool
188
     */
189
    private function loadNextDocBlockContent(int $startPosition)
190
    {
191
        return $this->file->findNext(
192
            [
193
                T_DOC_COMMENT_WHITESPACE,
194
                T_DOC_COMMENT_STAR
195
            ],
196
            $startPosition + 1,
197
            $this->getDocHelper()->getBlockEndPosition(),
198
            true
199
        );
200
    }
201
202
    /**
203
     * Returns the position of the previous whitespace or star of the comment for checking the line after that.
204
     *
205
     * @param int $startPosition
206
     *
207
     * @return int|bool
208
     */
209
    private function loadPrevDocBlockContent(int $startPosition)
210
    {
211
        return $this->file->findPrevious(
212
            [
213
                T_DOC_COMMENT_OPEN_TAG,
214
                T_DOC_COMMENT_STAR,
215
                T_DOC_COMMENT_WHITESPACE,
216
            ],
217
            $startPosition - 1,
218
            $this->getDocCommentPos(),
219
            true
220
        );
221
    }
222
223
    /**
224
     * Loads the position of the summary token if possible.
225
     *
226
     * @return int|null
227
     */
228
    private function loadSummaryPosition(): ?int
229
    {
230
        $return = null;
231
        $possSummaryPos = $this->loadNextDocBlockContent($this->getDocCommentPos());
232
233
        if ((int) $possSummaryPos > 0) {
234
            $possSummaryToken = $this->tokens[$possSummaryPos];
235
236
            $return = $this->isSimpleText($possSummaryToken) ? $possSummaryPos : null;
237
        }
238
239
        return $return;
240
    }
241
242
    /**
243
     * Checks and registers errors  if there are invalid doc comments.
244
     *
245
     * @throws CodeWarning
246
     *
247
     * @return void
248
     */
249
    protected function processToken(): void
250
    {
251
        $this
252
            ->validateSummaryExistence()
253
            ->validateDescriptions();
254
    }
255
256
    /**
257
     * Resets the sniff after one processing.
258
     *
259
     * @return void
260
     */
261
    protected function tearDown(): void
262
    {
263
        $this->resetDocCommentPos();
264
        $this->summaryPosition = null;
265
    }
266
267
    /**
268
     * Validates the descriptions in the file.
269
     *
270
     * @return AbstractDocSniff
271
     */
272
    private function validateDescriptions(): self
273
    {
274
        $commentPoss = TokenHelper::findNextAll(
275
            $this->file->getBaseFile(),
276
            [T_DOC_COMMENT_STRING, T_DOC_COMMENT_TAG],
277
            $this->getDocCommentPos(),
278
            $this->getDocHelper()->getBlockEndPosition()
279
        );
280
281
        foreach ($commentPoss as $index => $commentPos) {
282
            $commentToken = $this->tokens[$commentPos];
283
            $skipNewLineCheck = false;
284
285
            // We only search till the tags.
286
            if ($commentToken['code'] === T_DOC_COMMENT_TAG) {
287
                break;
288
            }
289
290
            if ($isFirstDocString = $index === 0) {
291
                $skipNewLineCheck = !$this->validateOneLineSummary();
292
            }
293
294
            $this->validateUCFirstDocComment($commentPos, $commentToken);
295
296
            if (!$skipNewLineCheck) {
297
                $this->validateNewLineAfterDocComment($commentPos, $commentToken, $isFirstDocString);
298
            }
299
        }
300
301
        return $this;
302
303
        ;
304
    }
305
306
    /**
307
     * Checks if there is a line break after the comment block..
308
     *
309
     * @param int $position
310
     * @param array $token
311
     * @param bool $asSingleLine
312
     *
313
     * @return void
314
     */
315
    private function validateNewLineAfterDocComment(int $position, array $token, bool $asSingleLine = true): void
316
    {
317
        if (!$this->isNextLineEmpty($position)) {
318
            $nextRelevantPos = $this->loadNextDocBlockContent($position);
319
            $nextToken = $this->tokens[$nextRelevantPos];
320
321
            // Register an error if we force a single line or this is no long description with more then one line.
322
            if ($asSingleLine || ($nextToken['code'] !== T_DOC_COMMENT_STRING)) {
323
                $isFixing = $this->file->addFixableWarning(
324
                    self::MESSAGE_NO_LINE_AFTER_DOC_COMMENT,
325
                    $position,
326
                    static::CODE_NO_LINE_AFTER_DOC_COMMENT
327
                );
328
329
                if ($isFixing) {
330
                    $this->fixNoLineAfterDocComment($position, $token);
331
                }
332
            }
333
        }
334
    }
335
336
    /**
337
     * Checks if the summary is on line or registers a warning.
338
     *
339
     * @return bool We can skip the new line error, so return true if the one line summary is true.
340
     */
341
    private function validateOneLineSummary(): bool
342
    {
343
        $isValid = true;
344
        $summaryPos = $this->getSummaryPosition();
345
        $nextPossiblePos = $this->loadNextDocBlockContent($summaryPos);
346
347
        if ($nextPossiblePos) {
348
            $nextToken = $this->tokens[$nextPossiblePos];
349 121
350
            if (($nextToken['code'] === T_DOC_COMMENT_STRING) && !$this->isNextLineEmpty($summaryPos)) {
351 121
                $isValid = false;
352
353
                $this->file->addWarning(
354
                    self::MESSAGE_SUMMARY_TOO_LONG,
355
                    $nextPossiblePos,
356
                    static::CODE_SUMMARY_TOO_LONG
357
                );
358
            }
359 121
        }
360
361 121
        return $isValid;
362
    }
363 121
364 121
    /**
365
     * Returns position to the comment summary or null.
366 1
     *
367
     * @throws CodeWarning If there is no summary.
368
     *
369 121
     * @return $this
370 121
     */
371 121
    private function validateSummaryExistence(): self
372 121
    {
373 121
        $summaryPos = $this->getSummaryPosition();
374 121
375
        if (!$summaryPos) {
376 121
            throw new CodeWarning(
377
                static::CODE_NO_SUMMARY,
378 121
                self::MESSAGE_NO_SUMMARY,
379 121
                $this->getDocCommentPos()
380
            );
381 8
        }
382
383
        return $this;
384 113
    }
385
386 113
    /**
387 113
     * Checks if the first char of the doc comment is ucfirst.
388
     *
389
     * @param int $position
390 113
     * @param array $token
391 113
     *
392 113
     * @return void
393
     */
394 113
    private function validateUCFirstDocComment(int $position, array $token): void
395
    {
396
        $commentText = $token['content'];
397
398
        if (ucfirst($commentText) !== $commentText && $this->isPrevLineEmpty($position)) {
399
            $isFixing = $this->file->addFixableWarning(
400
                self::MESSAGE_DOC_COMMENT_UC_FIRST,
401 65
                $position,
402
                static::CODE_DOC_COMMENT_UC_FIRST
403 65
            );
404
405
            if ($isFixing) {
406
                $this->fixDocCommentUcFirst($position, $token);
407
            }
408
        }
409
    }
410
}
411