• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

webimpress / coding-standard / 4086684577

pending completion
4086684577

Pull #178

github

GitHub
Merge 75aa3b533 into 18aa29088
Pull Request #178: Bump phpunit/phpunit from 9.5.20 to 9.6.0

6985 of 6999 relevant lines covered (99.8%)

1.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.49
/src/WebimpressCodingStandard/Sniffs/Classes/TraitUsageSniff.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace WebimpressCodingStandard\Sniffs\Classes;
6

7
use PHP_CodeSniffer\Files\File;
8
use PHP_CodeSniffer\Sniffs\Sniff;
9
use PHP_CodeSniffer\Util\Tokens;
10
use WebimpressCodingStandard\CodingStandard;
11

12
use function array_column;
13
use function array_keys;
14
use function end;
15
use function sort;
16
use function str_replace;
17
use function strcasecmp;
18
use function usort;
19

20
use const T_ANON_CLASS;
21
use const T_CLASS;
22
use const T_CLOSE_CURLY_BRACKET;
23
use const T_COMMA;
24
use const T_OPEN_CURLY_BRACKET;
25
use const T_SEMICOLON;
26
use const T_TRAIT;
27
use const T_USE;
28
use const T_WHITESPACE;
29

30
class TraitUsageSniff implements Sniff
31
{
32
    /**
33
     * @return int[]
34
     */
35
    public function register() : array
36
    {
37
        return [T_USE];
1✔
38
    }
39

40
    /**
41
     * @param int $stackPtr
42
     */
43
    public function process(File $phpcsFile, $stackPtr)
44
    {
45
        if (! CodingStandard::isTraitUse($phpcsFile, $stackPtr)) {
1✔
46
            return;
1✔
47
        }
48

49
        $tokens = $phpcsFile->getTokens();
1✔
50

51
        $keys = array_keys($tokens[$stackPtr]['conditions']);
1✔
52
        $classPtr = end($keys);
1✔
53
        $scopeOpener = $tokens[$classPtr]['scope_opener'];
1✔
54

55
        $start = $scopeOpener;
1✔
56
        while ($next = $phpcsFile->findNext(Tokens::$emptyTokens, $start + 1, $stackPtr, true)) {
1✔
57
            if ($tokens[$next]['code'] === T_USE) {
1✔
58
                $start = $phpcsFile->findEndOfStatement($next, T_COMMA);
1✔
59
                continue;
1✔
60
            }
61

62
            break;
1✔
63
        }
64

65
        if ($next) {
1✔
66
            $error = 'Trait must be at the beginning of the class';
1✔
67
            $fix = $phpcsFile->addFixableError($error, $stackPtr, 'FirstInClass');
1✔
68

69
            if ($fix) {
1✔
70
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true);
1✔
71
                $end = $phpcsFile->findEndOfStatement($stackPtr, T_COMMA);
1✔
72
                $content = $phpcsFile->getTokensAsString($prev + 1, $end - $prev);
1✔
73

74
                $phpcsFile->fixer->beginChangeset();
1✔
75
                for ($i = $prev + 1; $i <= $end; ++$i) {
1✔
76
                    $phpcsFile->fixer->replaceToken($i, '');
1✔
77
                }
78
                $phpcsFile->fixer->addContent($start, $content);
1✔
79
                $phpcsFile->fixer->endChangeset();
1✔
80
            }
81

82
            return;
1✔
83
        }
84

85
        // No blank line before use keyword.
86
        $prev = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true);
1✔
87
        if ($tokens[$prev]['line'] + 1 !== $tokens[$stackPtr]['line']) {
1✔
88
            $error = 'Blank line is not allowed before trait declaration';
1✔
89
            $fix = $phpcsFile->addFixableError($error, $stackPtr, 'BlankLineBeforeTraits');
1✔
90

91
            if ($fix) {
1✔
92
                $phpcsFile->fixer->beginChangeset();
1✔
93
                for ($i = $prev + 1; $i < $stackPtr; ++$i) {
1✔
94
                    if ($tokens[$i]['line'] === $tokens[$stackPtr]['line']) {
1✔
95
                        break;
1✔
96
                    }
97

98
                    $phpcsFile->fixer->replaceToken($i, '');
1✔
99
                }
100
                $phpcsFile->fixer->addNewline($prev);
1✔
101
                $phpcsFile->fixer->endChangeset();
1✔
102
            }
103
        }
104

105
        // One space after the use keyword.
106
        if ($tokens[$stackPtr + 1]['content'] !== ' ') {
1✔
107
            $error = 'There must be a single space after USE keyword';
1✔
108
            $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterUse');
1✔
109

110
            if ($fix) {
1✔
111
                if ($tokens[$stackPtr + 1]['code'] === T_WHITESPACE) {
1✔
112
                    $phpcsFile->fixer->replaceToken($stackPtr + 1, ' ');
1✔
113
                } else {
114
                    $phpcsFile->fixer->addContent($stackPtr, ' ');
×
115
                }
116
            }
117
        }
118

119
        $scopeOpener = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET, T_SEMICOLON], $stackPtr + 1);
1✔
120

121
        $comma = $phpcsFile->findNext(T_COMMA, $stackPtr + 1, $scopeOpener - 1);
1✔
122
        if ($comma) {
1✔
123
            $error = 'There must be one USE per declaration';
1✔
124
            $fix = $phpcsFile->addFixableError($error, $comma, 'OneUsePerDeclaration');
1✔
125

126
            if ($fix) {
1✔
127
                $phpcsFile->fixer->replaceToken($comma, ';' . $phpcsFile->eolChar . 'use ');
1✔
128
            }
129
        }
130

131
        // Check for T_WHITESPACE in trait name.
132
        $firstNotEmpty = $phpcsFile->findNext(
1✔
133
            T_WHITESPACE,
1✔
134
            $stackPtr + 1,
1✔
135
            $comma ?: $scopeOpener,
1✔
136
            true
1✔
137
        );
1✔
138
        $lastNotEmpty = $phpcsFile->findPrevious(
1✔
139
            T_WHITESPACE,
1✔
140
            ($comma ?: $scopeOpener) - 1,
1✔
141
            $stackPtr + 1,
1✔
142
            true
1✔
143
        );
1✔
144

145
        if ($firstNotEmpty !== $lastNotEmpty) {
1✔
146
            $emptyInName = $phpcsFile->findNext(
1✔
147
                Tokens::$emptyTokens,
1✔
148
                $firstNotEmpty + 1,
1✔
149
                $lastNotEmpty
1✔
150
            );
1✔
151
            if ($emptyInName) {
1✔
152
                $error = 'Empty token %s is not allowed in trait name';
1✔
153
                $data = [$tokens[$emptyInName]['type']];
1✔
154
                $fix = $phpcsFile->addFixableError($error, $emptyInName, 'EmptyToken', $data);
1✔
155

156
                if ($fix) {
1✔
157
                    $phpcsFile->fixer->replaceToken($emptyInName, '');
1✔
158
                }
159
            }
160
        }
161

162
        if ($tokens[$scopeOpener]['code'] === T_OPEN_CURLY_BRACKET) {
1✔
163
            $scopeCloser = $tokens[$scopeOpener]['scope_closer'];
1✔
164

165
            $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, $scopeOpener - 1, null, true);
1✔
166
            $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $scopeOpener + 1, null, true);
1✔
167

168
            if ($scopeCloser === $nextNonEmpty) {
1✔
169
                $error = 'Empty brackets with trait are redundant';
1✔
170
                $fix = $phpcsFile->addFixableError($error, $scopeOpener, 'EmptyBrackets');
1✔
171

172
                if ($fix) {
1✔
173
                    $phpcsFile->fixer->beginChangeset();
1✔
174
                    for ($i = $prev + 1; $i < $nextNonEmpty; ++$i) {
1✔
175
                        $phpcsFile->fixer->replaceToken($scopeOpener, '');
1✔
176
                        $phpcsFile->fixer->replaceToken($scopeCloser, ';');
1✔
177
                    }
178
                    $phpcsFile->fixer->endChangeset();
1✔
179
                }
180
            } elseif ($tokens[$prevNonEmpty]['line'] !== $tokens[$scopeOpener]['line']) {
1✔
181
                $error = 'There must be a single space before curly bracket';
1✔
182
                $fix = $phpcsFile->addFixableError($error, $scopeOpener, 'SpaceBeforeCurly');
1✔
183

184
                if ($fix) {
1✔
185
                    $phpcsFile->fixer->beginChangeset();
1✔
186
                    for ($i = $prevNonEmpty + 1; $i < $scopeOpener; ++$i) {
1✔
187
                        $phpcsFile->fixer->replaceToken($i, '');
1✔
188
                    }
189
                    $phpcsFile->fixer->addContentBefore($scopeOpener, ' ');
1✔
190
                    $phpcsFile->fixer->endChangeset();
1✔
191
                }
192
            } elseif ($tokens[$scopeOpener - 1]['content'] !== ' ') {
1✔
193
                $error = 'There must be a single space before curly bracket';
1✔
194
                $fix = $phpcsFile->addFixableError($error, $scopeOpener, 'SpaceBeforeCurly');
1✔
195

196
                if ($fix) {
1✔
197
                    if ($tokens[$scopeOpener - 1]['code'] === T_WHITESPACE) {
1✔
198
                        $phpcsFile->fixer->replaceToken($scopeOpener - 1, ' ');
1✔
199
                    } else {
200
                        $phpcsFile->fixer->addContent($scopeOpener - 1, ' ');
1✔
201
                    }
202
                }
203
            }
204

205
            if ($nextNonEmpty < $scopeCloser) {
1✔
206
                if ($tokens[$nextNonEmpty]['line'] !== $tokens[$scopeOpener]['line'] + 1) {
1✔
207
                    $error = 'Content must be in next line after opening curly bracket';
1✔
208
                    $fix = $phpcsFile->addFixableError($error, $scopeOpener, 'OpeningCurlyBracket');
1✔
209

210
                    if ($fix) {
1✔
211
                        $phpcsFile->fixer->beginChangeset();
1✔
212
                        for ($i = $scopeOpener + 1; $i < $nextNonEmpty; ++$i) {
1✔
213
                            $phpcsFile->fixer->replaceToken($i, '');
1✔
214
                        }
215
                        $phpcsFile->fixer->addContentBefore($nextNonEmpty, $phpcsFile->eolChar);
1✔
216
                        $phpcsFile->fixer->endChangeset();
1✔
217
                    }
218
                }
219

220
                $prevNonEmpty = $phpcsFile->findPrevious(
1✔
221
                    Tokens::$emptyTokens,
1✔
222
                    $scopeCloser - 1,
1✔
223
                    null,
1✔
224
                    true
1✔
225
                );
1✔
226
                if ($tokens[$prevNonEmpty]['line'] + 1 !== $tokens[$scopeCloser]['line']) {
1✔
227
                    $error = 'Close curly bracket must be in next line after content';
1✔
228
                    $fix = $phpcsFile->addFixableError($error, $scopeCloser, 'ClosingCurlyBracket');
1✔
229

230
                    if ($fix) {
1✔
231
                        $phpcsFile->fixer->beginChangeset();
1✔
232
                        for ($i = $prevNonEmpty + 1; $i < $scopeCloser; ++$i) {
1✔
233
                            $phpcsFile->fixer->replaceToken($i, '');
1✔
234
                        }
235
                        $phpcsFile->fixer->addContentBefore($scopeCloser, $phpcsFile->eolChar);
1✔
236
                        $phpcsFile->fixer->endChangeset();
1✔
237
                    }
238
                }
239
            }
240

241
            // Detect all statements inside curly brackets.
242
            $statements = [];
1✔
243
            $begin = $phpcsFile->findNext(Tokens::$emptyTokens, $scopeOpener + 1, null, true);
1✔
244
            while ($end = $phpcsFile->findNext([T_SEMICOLON], $begin + 1, $scopeCloser)) {
1✔
245
                $statements[] = [
1✔
246
                    'begin' => $begin,
1✔
247
                    'end' => $end,
1✔
248
                    'content' => $phpcsFile->getTokensAsString($begin, $end - $begin + 1),
1✔
249
                ];
1✔
250
                $begin = $phpcsFile->findNext(Tokens::$emptyTokens, $end + 1, null, true);
1✔
251
            }
252

253
            $lastStatement = null;
1✔
254
            foreach ($statements as $statement) {
1✔
255
                if (! $lastStatement) {
1✔
256
                    $lastStatement = $statement;
1✔
257
                    continue;
1✔
258
                }
259

260
                $order = $this->compareStatements($statement, $lastStatement);
1✔
261

262
                if ($order < 0) {
1✔
263
                    $error = 'Statements in trait are incorrectly ordered. The first wrong is %s';
1✔
264
                    $data = [$statement['content']];
1✔
265
                    $fix = $phpcsFile->addFixableError($error, $statement['begin'], 'TraitStatementsOrder', $data);
1✔
266

267
                    if ($fix) {
1✔
268
                        $this->fixAlphabeticalOrder($phpcsFile, $statements);
1✔
269
                    }
270

271
                    break;
1✔
272
                }
273

274
                $lastStatement = $statement;
1✔
275
            }
276
        } else {
277
            $scopeCloser = $scopeOpener;
1✔
278
        }
279

280
        $class = $phpcsFile->findPrevious([T_CLASS, T_TRAIT, T_ANON_CLASS], $stackPtr - 1);
1✔
281
        // Only interested in the last USE statement from here onwards.
282
        $nextUse = $stackPtr;
1✔
283
        do {
284
            $nextUse = $phpcsFile->findNext(T_USE, $nextUse + 1, $tokens[$class]['scope_closer']);
1✔
285
        } while ($nextUse !== false
1✔
286
            && (! CodingStandard::isTraitUse($phpcsFile, $nextUse)
1✔
287
                || ! isset($tokens[$nextUse]['conditions'][$class])
1✔
288
                || $tokens[$nextUse]['level'] !== $tokens[$class]['level'] + 1)
1✔
289
        );
290

291
        if ($nextUse !== false) {
1✔
292
            return;
1✔
293
        }
294

295
        // Find next (after traits) non-whitespace token.
296
        $next = $phpcsFile->findNext(T_WHITESPACE, $scopeCloser + 1, null, true);
1✔
297

298
        $diff = $tokens[$next]['line'] - $tokens[$scopeCloser]['line'] - 1;
1✔
299
        if ($diff !== 1
1✔
300
            && $tokens[$next]['code'] !== T_CLOSE_CURLY_BRACKET
1✔
301
        ) {
302
            $error = 'There must be one blank line after the last USE statement; %s found;';
1✔
303
            $data = [$diff];
1✔
304
            $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterLastUse', $data);
1✔
305

306
            if ($fix) {
1✔
307
                if ($diff === 0) {
1✔
308
                    $phpcsFile->fixer->addNewline($scopeCloser);
1✔
309
                } else {
310
                    $phpcsFile->fixer->beginChangeset();
1✔
311
                    for ($i = $scopeCloser + 1; $i < $next; ++$i) {
1✔
312
                        if ($tokens[$i]['line'] === $tokens[$next]['line']) {
1✔
313
                            break;
1✔
314
                        }
315

316
                        $phpcsFile->fixer->replaceToken($i, '');
1✔
317
                    }
318
                    $phpcsFile->fixer->addNewline($scopeCloser);
1✔
319
                    $phpcsFile->fixer->endChangeset();
1✔
320
                }
321
            }
322
        }
323
    }
324

325
    /**
326
     * Fix order of statements inside trait's curly brackets.
327
     *
328
     * @param string[] $statements
329
     */
330
    private function fixAlphabeticalOrder(File $phpcsFile, array $statements) : void
331
    {
332
        $phpcsFile->fixer->beginChangeset();
1✔
333
        foreach ($statements as $statement) {
1✔
334
            for ($i = $statement['begin']; $i <= $statement['end']; ++$i) {
1✔
335
                $phpcsFile->fixer->replaceToken($i, '');
1✔
336
            }
337
        }
338

339
        usort($statements, function (array $a, array $b) {
1✔
340
            return $this->compareStatements($a, $b);
1✔
341
        });
1✔
342

343
        $begins = array_column($statements, 'begin');
1✔
344
        sort($begins);
1✔
345

346
        foreach ($begins as $k => $begin) {
1✔
347
            $phpcsFile->fixer->addContent($begin, $statements[$k]['content']);
1✔
348
        }
349

350
        $phpcsFile->fixer->endChangeset();
1✔
351
    }
352

353
    /**
354
     * @param string[] $a
355
     * @param string[] $b
356
     */
357
    private function compareStatements(array $a, array $b) : int
358
    {
359
        return strcasecmp(
1✔
360
            $this->clearName($a['content']),
1✔
361
            $this->clearName($b['content'])
1✔
362
        );
1✔
363
    }
364

365
    private function clearName(string $name) : string
366
    {
367
        return str_replace('\\', ':', $name);
1✔
368
    }
369
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc