Completed
Push — master ( 14c836...906b18 )
by Björn
06:09 queued 03:52
created

TraitUseSpacingSniff::checkLinesBetweenUses()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 6
nop 0
dl 0
loc 48
rs 8.5123
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BestIt\Sniffs\Formatting;
6
7
use BestIt\CodeSniffer\Helper\ClassHelper;
8
use BestIt\CodeSniffer\Helper\TokenHelper;
9
use BestIt\Sniffs\AbstractSniff;
10
use BestIt\Sniffs\ClassRegistrationTrait;
11
use function substr_count;
12
use const T_CLOSE_CURLY_BRACKET;
13
use const T_OPEN_CURLY_BRACKET;
14
use const T_SEMICOLON;
15
use const T_WHITESPACE;
16
17
/**
18
 * Checks the newlines between the trait uses.
19
 *
20
 * This is a refactores copy of the slevomat code sniff.
21
 *
22
 * @author blange <[email protected]>
23
 * @package BestIt\Sniffs\Formatting
24
 */
25
class TraitUseSpacingSniff extends AbstractSniff
26
{
27
    use ClassRegistrationTrait;
28
29
    /**
30
     * You MUST not provide additional lines after your last rait usage.
31
     */
32
    public const CODE_INCORRECT_LINES_COUNT_AFTER_LAST_USE = 'IncorrectLinesCountAfterLastUse';
33
34
    /**
35
     * You MUST not provide additional new lines before your first trait use.
36
     */
37
    public const CODE_INCORRECT_LINES_COUNT_BEFORE_FIRST_USE = 'IncorrectLinesCountBeforeFirstUse';
38
39
    /**
40
     * You MUST not provide additional new lines between trait usages.
41
     */
42
    public const CODE_INCORRECT_LINES_COUNT_BETWEEN_USES = 'IncorrectLinesCountBetweenUses';
43
44
    /**
45
     * How many lines after the last use.
46
     */
47
    private const LINES_AFTER_LAST_USE = 1;
48
49
    /**
50
     * How many lines after the last use.
51
     */
52
    private const LINES_AFTER_LAST_USE_WHEN_LAST_IN_CLASS = 0;
53
54
    /**
55
     * How many use before the first one.
56
     */
57
    private const LINES_BEFORE_FIRST_USE = 0;
58
59
    /**
60
     * How many lines between the uses.
61
     */
62
    private const LINES_BETWEEN_USES = 0;
63
64
    /**
65
     * The message to the user for the error before usages.
66
     */
67
    private const MESSAGE_INCORRECT_LINES_COUNT_BEFORE_FIRST_USE =
68
        'Expected %d lines before first use statement, found %d.';
69
70
    /**
71
     * The message to the user for the error after the last usage.
72
     */
73
    private const MESSAGE_INCORRECT_LINES_COUNT_AFTER_LAST_USE =
74
        'Expected %d lines after last use statement, found %d.';
75
76
    /**
77
     * The message to the user for the error between uses.
78
     */
79
    private const MESSAGE_INCORRECT_LINES_COUNT_BETWEEN_USES =
80
        'Expected %d lines between same types of use statement, found %d.';
81
82
    /**
83
     * The use declarations positions of this "class".
84
     *
85
     * @var array
86
     */
87
    private array $uses;
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 T_ARRAY, expecting T_FUNCTION or T_CONST
Loading history...
88
89
    /**
90
     * Returns false if there are no uses.
91
     *
92
     * @return bool
93
     */
94
    protected function areRequirementsMet(): bool
95
    {
96
        return (bool) $this->uses = ClassHelper::getTraitUsePointers($this->getFile(), $this->getStackPos());
97
    }
98
99
    /**
100
     * Checks the line after the last use and registers an error if needed.
101
     *
102
     * @param int $lastUsePos
103
     *
104
     * @return void
105
     */
106
    private function checkLinesAfterLastUse(int $lastUsePos): void
107
    {
108
        $lastUseEndPos = $this->getLastUseEndPos($lastUsePos);
109
110
        list($realLinesAfterUse, $whitespaceEnd) = $this->getRealLinesAfterLastUse($lastUseEndPos);
111
112
        $requiredLinesAfter = $this->isEndOfClass($lastUseEndPos)
113
            ? self::LINES_AFTER_LAST_USE_WHEN_LAST_IN_CLASS
114
            : self::LINES_AFTER_LAST_USE;
115
116
        if ($realLinesAfterUse !== $requiredLinesAfter) {
117
            $fix = $this->getFile()->addFixableError(
118
                self::MESSAGE_INCORRECT_LINES_COUNT_AFTER_LAST_USE,
119
                $lastUsePos,
120
                self::CODE_INCORRECT_LINES_COUNT_AFTER_LAST_USE,
121
                [
122
                    $requiredLinesAfter,
123
                    $realLinesAfterUse
124
                ]
125
            );
126
127
            if ($fix) {
128
                $this->fixLineAfterLastUse($lastUseEndPos, $whitespaceEnd, $requiredLinesAfter);
129
            }
130
        }
131
    }
132
133
    /**
134
     * Checks the lines before the first usage and registers an error if needed.
135
     *
136
     * @param int $firstUsePos
137
     *
138
     * @return void
139
     */
140
    private function checkLinesBeforeFirstUse(int $firstUsePos): void
141
    {
142
        $posBeforeFirstUse = TokenHelper::findPreviousExcluding($this->getFile(), T_WHITESPACE, $firstUsePos - 1);
143
        $realLinesBeforeUse = $this->getRealLinesBeforeFirstUse($firstUsePos, $posBeforeFirstUse);
144
145
        if ($realLinesBeforeUse !== self::LINES_BEFORE_FIRST_USE) {
146
            $fix = $this->getFile()->addFixableError(
147
                self::MESSAGE_INCORRECT_LINES_COUNT_BEFORE_FIRST_USE,
148
                $firstUsePos,
149
                self::CODE_INCORRECT_LINES_COUNT_BEFORE_FIRST_USE,
150
                [
151
                    self::LINES_BEFORE_FIRST_USE,
152
                    $realLinesBeforeUse
153
                ]
154
            );
155
156
            if ($fix) {
157
                $this->fixLinesBeforeFirstUse($firstUsePos, $posBeforeFirstUse, self::LINES_BEFORE_FIRST_USE);
158
            }
159
        }
160
    }
161
162
    /**
163
     * Checks the lines between uses and registers an erro rif needed.
164
     *
165
     * @return void
166
     */
167
    private function checkLinesBetweenUses(): void
168
    {
169
        $file = $this->getFile();
170
        $previousUsePos = null;
171
172
        foreach ($this->uses as $usePos) {
173
            if ($previousUsePos === null) {
174
                $previousUsePos = $usePos;
175
                continue;
176
            }
177
178
            $posBeforeUse = TokenHelper::findPreviousEffective($file, $usePos - 1);
179
            $previousUseEndPos = TokenHelper::findNextLocal(
180
                $file,
181
                [T_SEMICOLON, T_OPEN_CURLY_BRACKET],
182
                $previousUsePos + 1
183
            );
184
185
            $realLinesBetweenUse = $this->getRealLinesBetweenUses(
186
                $previousUseEndPos,
187
                $usePos
188
            );
189
190
            $previousUsePos = $usePos;
191
192
            if ($realLinesBetweenUse !== self::LINES_BETWEEN_USES) {
193
                $errorParameters = [
194
                    self::MESSAGE_INCORRECT_LINES_COUNT_BETWEEN_USES,
195
                    $usePos,
196
                    self::CODE_INCORRECT_LINES_COUNT_BETWEEN_USES,
197
                    [
198
                        self::LINES_BETWEEN_USES,
199
                        $realLinesBetweenUse
200
                    ]
201
                ];
202
203
                if ($previousUseEndPos !== $posBeforeUse) {
204
                    $file->addError(...$errorParameters);
205
                } else {
206
                    $fix = $file->addFixableError(...$errorParameters);
207
208
                    if ($fix) {
209
                        $this->fixLinesBetweenUses($usePos, $previousUseEndPos, self::LINES_BETWEEN_USES);
210
                    }
211
                }
212
            }
213
        }
214
    }
215
216
    /**
217
     * Fixes the lines which are allowed after the last use.
218
     *
219
     * @param int $lastUseEndPos
220
     * @param int $whitespaceEnd
221
     * @param int $requiredLinesAfter
222
     *
223
     * @return void
224
     */
225
    private function fixLineAfterLastUse(
226
        int $lastUseEndPos,
227
        int $whitespaceEnd,
228
        int $requiredLinesAfter
229
    ): void {
230
        $file = $this->getFile();
231
232
        $file->fixer->beginChangeset();
233
234
        for ($i = $lastUseEndPos + 1; $i <= $whitespaceEnd; $i++) {
235
            $file->fixer->replaceToken($i, '');
236
        }
237
238
        for ($i = 0; $i <= $requiredLinesAfter; $i++) {
239
            $file->fixer->addNewline($lastUseEndPos);
240
        }
241
242
        $file->fixer->endChangeset();
243
    }
244
245
    /**
246
     * Fixes the lines before the first use.
247
     *
248
     * @param int $firstUsePos
249
     * @param int $posBeforeFirstUse
250
     *
251
     * @return void
252
     */
253
    private function fixLinesBeforeFirstUse(
254
        int $firstUsePos,
255
        int $posBeforeFirstUse
256
    ): void {
257
        $file = $this->getFile();
258
        $file->fixer->beginChangeset();
259
260
        $posBeforeIndentation = TokenHelper::findPreviousContent(
261
            $file,
262
            T_WHITESPACE,
263
            $file->eolChar,
264
            $firstUsePos,
265
            $posBeforeFirstUse
266
        );
267
268
        if ($posBeforeIndentation !== null) {
269
            for ($i = $posBeforeFirstUse + 1; $i <= $posBeforeIndentation; $i++) {
270
                $file->fixer->replaceToken($i, '');
271
            }
272
        }
273
        for ($i = 0; $i <= self::LINES_BEFORE_FIRST_USE; $i++) {
274
            $file->fixer->addNewline($posBeforeFirstUse);
275
        }
276
277
        $file->fixer->endChangeset();
278
    }
279
280
    /**
281
     * Fixes the lines between the uses.
282
     *
283
     * @param int $usePos
284
     * @param int $previousUseEndPos
285
     *
286
     * @return void
287
     */
288
    private function fixLinesBetweenUses(int $usePos, int $previousUseEndPos): void
289
    {
290
        $file = $this->getFile();
291
292
        $posBeforeIndentation = TokenHelper::findPreviousContent(
293
            $file,
294
            T_WHITESPACE,
295
            $file->eolChar,
296
            $usePos,
297
            $previousUseEndPos
298
        );
299
300
        $file->fixer->beginChangeset();
301
        if ($posBeforeIndentation !== null) {
302
            for ($i = $previousUseEndPos + 1; $i <= $posBeforeIndentation; $i++) {
303
                $file->fixer->replaceToken($i, '');
304
            }
305
        }
306
        for ($i = 0; $i <= self::LINES_BETWEEN_USES; $i++) {
307
            $file->fixer->addNewline($previousUseEndPos);
308
        }
309
        $file->fixer->endChangeset();
310
    }
311
312
    /**
313
     * Gets the position on which the last use ends.
314
     *
315
     * @param int $lastUsePos
316
     *
317
     * @return int
318
     */
319
    private function getLastUseEndPos(int $lastUsePos): int
320
    {
321
        $file = $this->getFile();
322
        $tokens = $file->getTokens();
323
324
        $lastUseEndPos = TokenHelper::findNextLocal(
325
            $file,
326
            [T_SEMICOLON, T_OPEN_CURLY_BRACKET],
327
            $lastUsePos + 1
328
        );
329
330
        if ($tokens[$lastUseEndPos]['code'] === T_OPEN_CURLY_BRACKET) {
331
            $lastUseEndPos = $tokens[$lastUseEndPos]['bracket_closer'];
332
        }
333
334
        return $lastUseEndPos;
335
    }
336
337
    /**
338
     * Gets the real lines after the last use.
339
     *
340
     * @param int $lastUseEndPos
341
     *
342
     * @return array The first element is the line count, and the second element is when the whitespace ends.
343
     */
344
    private function getRealLinesAfterLastUse(int $lastUseEndPos): array
345
    {
346
        $file = $this->getFile();
347
        $tokens = $file->getTokens();
348
        $whitespaceEnd = TokenHelper::findNextExcluding($file, T_WHITESPACE, $lastUseEndPos + 1) - 1;
349
350
        if ($lastUseEndPos !== $whitespaceEnd && $tokens[$whitespaceEnd]['content'] !== $file->eolChar) {
351
            $lastEolPos = TokenHelper::findPreviousContent(
352
                $file,
353
                T_WHITESPACE,
354
                $file->eolChar,
355
                $whitespaceEnd - 1,
356
                $lastUseEndPos
357
            );
358
            $whitespaceEnd = $lastEolPos ?? $lastUseEndPos;
359
        }
360
361
        $whitespaceAfterLastUse = TokenHelper::getContent($file, $lastUseEndPos + 1, $whitespaceEnd);
362
363
        $realLinesAfterUse = substr_count($whitespaceAfterLastUse, $file->eolChar) - 1;
364
365
        return [$realLinesAfterUse, $whitespaceEnd];
366
    }
367
368
    /**
369
     * Returns the real lines before the first use.
370
     *
371
     * @param int $firstUsePos
372
     * @param int $posBeforeFirstUse
373
     *
374
     * @return int
375
     */
376
    private function getRealLinesBeforeFirstUse(int $firstUsePos, int $posBeforeFirstUse): int
377
    {
378
        $file = $this->getFile();
379
        $whitespaceBeforeFirstUse = '';
380
381
        if ($posBeforeFirstUse + 1 !== $firstUsePos) {
382
            $whitespaceBeforeFirstUse .= TokenHelper::getContent(
383
                $file,
384
                $posBeforeFirstUse + 1,
385
                $firstUsePos - 1
386
            );
387
        }
388
389
        return substr_count($whitespaceBeforeFirstUse, $file->eolChar) - 1;
390
    }
391
392
    /**
393
     * Returns the real lines between the uses.
394
     *
395
     * @param int $previousUseEndPos
396
     * @param int $usePos
397
     *
398
     * @return int
399
     */
400
    private function getRealLinesBetweenUses(int &$previousUseEndPos, int $usePos): int
401
    {
402
        $tokens = $this->getFile()->getTokens();
403
404
        if ($tokens[$previousUseEndPos]['code'] === T_OPEN_CURLY_BRACKET) {
405
            $previousUseEndPos = $tokens[$previousUseEndPos]['bracket_closer'];
406
        }
407
408
        return $tokens[$usePos]['line'] - $tokens[$previousUseEndPos]['line'] - 1;
409
    }
410
411
    /**
412
     * Is the given Position the end of the class.
413
     *
414
     * @param int $lastUseEndPos
415
     *
416
     * @return bool
417
     */
418
    private function isEndOfClass(int $lastUseEndPos): bool
419
    {
420
        $file = $this->getFile();
421
        $tokens = $file->getTokens();
422
423
        $posAfterLastUse = TokenHelper::findNextEffective($file, $lastUseEndPos + 1);
424
425
        return $tokens[$posAfterLastUse]['code'] === T_CLOSE_CURLY_BRACKET;
426
    }
427
428
    /**
429
     * Processes the token.
430
     *
431
     * @return void
432
     */
433
    protected function processToken(): void
434
    {
435
        $this->checkLinesBeforeFirstUse($this->uses[0]);
436
        $this->checkLinesAfterLastUse($this->uses[count($this->uses) - 1]);
437
438
        if (count($this->uses) > 1) {
439
            $this->checkLinesBetweenUses();
440
        }
441
    }
442
}
443