Completed
Branch master (269e9e)
by Björn
06:04
created

TraitUseSpacingSniff::fixLinesBetweenUses()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 2
dl 0
loc 23
rs 9.552
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 $uses;
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);
0 ignored issues
show
Unused Code introduced by
The call to TraitUseSpacingSniff::fixLinesBeforeFirstUse() has too many arguments starting with self::LINES_BEFORE_FIRST_USE.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
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);
0 ignored issues
show
Unused Code introduced by
The call to TraitUseSpacingSniff::fixLinesBetweenUses() has too many arguments starting with self::LINES_BETWEEN_USES.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
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