Completed
Pull Request — master (#44)
by Björn
02:58
created

AbstractDocSniff::validateUCFirstDocComment()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
ccs 0
cts 0
cp 0
cc 4
nc 3
nop 2
crap 20
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()
0 ignored issues
show
Bug introduced by
It seems like $this->getDocHelper()->getBlockEndPosition() targeting BestIt\CodeSniffer\Helpe...::getBlockEndPosition() can also be of type boolean; however, SlevomatCodingStandard\H...enHelper::findNextAll() does only seem to accept integer|null, maybe add an additional type check?

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.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $summaryPos of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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