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

AlphabeticallySortedUsesSniff   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 282
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 0
loc 282
rs 10
c 0
b 0
f 0
wmc 22
lcom 1
cbo 9
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BestIt\Sniffs\Formatting;
6
7
use BestIt\CodeSniffer\CodeError;
8
use BestIt\CodeSniffer\CodeWarning;
9
use BestIt\CodeSniffer\Helper\TokenHelper;
10
use BestIt\CodeSniffer\Helper\UseStatementHelper;
11
use BestIt\Sniffs\AbstractSniff;
12
use SlevomatCodingStandard\Helpers\NamespaceHelper;
13
use SlevomatCodingStandard\Helpers\UseStatement;
14
use function end;
15
use function reset;
16
use function strcasecmp;
17
use function uasort;
18
use const T_OPEN_TAG;
19
use const T_SEMICOLON;
20
21
/**
22
 * Checks if the use statements are sorted alphabetically by PSR-12 Standard.
23
 *
24
 * @author blange <[email protected]>
25
 * @package BestIt\Sniffs\Formatting
26
 */
27
class AlphabeticallySortedUsesSniff extends AbstractSniff
28
{
29
    /**
30
     * You MUST provide your imports in alphabetically order, PSR-12 compatible.
31
     */
32
    public const CODE_INCORRECT_ORDER = 'IncorrectlyOrderedUses';
33
34
    /**
35
     * The found use statements.
36
     *
37
     * @var UseStatement[]
38
     */
39
    private array $useStatements;
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...
40
41
    /**
42
     * Returns true if we have use statements.
43
     *
44
     * @return bool
45
     */
46
    protected function areRequirementsMet(): bool
47
    {
48
        return (bool) $this->useStatements = UseStatementHelper::getUseStatementsForPointer(
49
            $this->getFile(),
50
            $this->getStackPos()
51
        );
52
    }
53
54
    /**
55
     * Will removing a compare marker for the given use statements.
56
     *
57
     * @param UseStatement $prevStatement
58
     * @param UseStatement $nextStatement
59
     *
60
     * @return int 1 <=> -1 To move statements in a direction.
61
     */
62
    private function compareUseStatements(UseStatement $prevStatement, UseStatement $nextStatement): int
63
    {
64
        $callbacks = [
65
            'compareUseStatementsByType',
66
            'compareUseStatementsByContent',
67
            // This will return something in any case!
68
            'compareUseStatementsByNamespaceCount'
69
        ];
70
71
        foreach ($callbacks as $callback) {
72
            $compared = $this->$callback($prevStatement, $nextStatement);
73
74
            if ($compared !== null) {
75
                return $compared;
76
            }
77
        }
78
    }
79
80
    /**
81
     * Compares the given use statements by their string content.
82
     *
83
     * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
84
     *
85
     * @param UseStatement $prevStatement
86
     * @param UseStatement $nextStatement
87
     *
88
     * @return int|null 1 <=> -1 To move statements in a direction.
89
     */
90
    private function compareUseStatementsByContent(UseStatement $prevStatement, UseStatement $nextStatement): ?int
91
    {
92
        $compareByContent = null;
93
94
        $prevParts = explode(NamespaceHelper::NAMESPACE_SEPARATOR, $prevStatement->getFullyQualifiedTypeName());
95
        $nextParts = explode(NamespaceHelper::NAMESPACE_SEPARATOR, $nextStatement->getFullyQualifiedTypeName());
96
97
        $minPartsCount = min(count($prevParts), count($nextParts));
98
99
        for ($i = 0; $i < $minPartsCount; ++$i) {
100
            $comparison = strcasecmp($prevParts[$i], $nextParts[$i]);
101
102
            if ($comparison) {
103
                $compareByContent = $comparison;
104
                break;
105
            }
106
        }
107
108
        return $compareByContent;
109
    }
110
111
    /**
112
     * The shorted usage comes at top.
113
     *
114
     * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
115
     *
116
     * @param UseStatement $prevStatement
117
     * @param UseStatement $nextStatement
118
     *
119
     * @return int 1 <=> -1 To move statements in a direction.
120
     */
121
    private function compareUseStatementsByNamespaceCount(
122
        UseStatement $prevStatement,
123
        UseStatement $nextStatement
124
    ): int {
125
        $aNameParts = explode(NamespaceHelper::NAMESPACE_SEPARATOR, $prevStatement->getFullyQualifiedTypeName());
126
        $bNameParts = explode(NamespaceHelper::NAMESPACE_SEPARATOR, $nextStatement->getFullyQualifiedTypeName());
127
128
        return count($aNameParts) <=> count($bNameParts);
129
    }
130
131
    /**
132
     * Classes to the top, functions next, constants last.
133
     *
134
     * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
135
     *
136
     * @param UseStatement $prevStatement
137
     * @param UseStatement $nextStatement
138
     *
139
     * @return int|null 1 <=> -1 To move statements in a direction.
140
     */
141
    private function compareUseStatementsByType(UseStatement $prevStatement, UseStatement $nextStatement): ?int
142
    {
143
        $comparedByType = null;
144
145
        if (!$prevStatement->hasSameType($nextStatement)) {
146
            $order = [
147
                UseStatement::TYPE_DEFAULT => 1,
148
                UseStatement::TYPE_FUNCTION => 2,
149
                UseStatement::TYPE_CONSTANT => 3,
150
            ];
151
152
            $file = $this->getFile();
153
            $comparedByType = $order[UseStatementHelper::getType($file, $prevStatement)] <=>
154
                $order[UseStatementHelper::getType($file, $nextStatement)];
155
        }
156
157
        return $comparedByType;
158
    }
159
160
    /**
161
     * Sorts the uses correctly and saves them in the file.
162
     *
163
     * @param CodeWarning $error
164
     *
165
     * @return void
166
     */
167
    protected function fixDefaultProblem(CodeWarning $error): void
168
    {
169
        // Satisfy phpmd
170
        unset($error);
171
172
        $firstUseStatement = reset($this->useStatements);
173
174
        $file = $this->getFile();
175
176
        $file->fixer->beginChangeset();
177
178
        $this->removeOldUseStatements($firstUseStatement);
179
180
        $file->fixer->addContent(
181
            $firstUseStatement->getPointer(),
182
            $this->getNewUseStatements()
183
        );
184
185
        $file->fixer->endChangeset();
186
    }
187
188
    /**
189
     * Returns the new statements as a string for fixing.
190
     *
191
     * @return string
192
     */
193
    private function getNewUseStatements(): string
194
    {
195
        $this->sortUseStatements();
196
197
        $file = $this->getFile();
198
199
        return implode(
200
            $file->eolChar,
201
            array_map(
202
                function (UseStatement $useStatement) use ($file): string {
203
                    $unqualifiedName = NamespaceHelper::getUnqualifiedNameFromFullyQualifiedName(
204
                        $useStatement->getFullyQualifiedTypeName()
205
                    );
206
207
                    $useTypeName = UseStatementHelper::getTypeName($file, $useStatement);
208
                    $useTypeFormatted = $useTypeName ? sprintf('%s ', $useTypeName) : '';
209
210
                    return ($unqualifiedName === $useStatement->getNameAsReferencedInFile())
211
                        ? sprintf(
212
                            'use %s%s;',
213
                            $useTypeFormatted,
214
                            $useStatement->getFullyQualifiedTypeName()
215
                        )
216
                        : sprintf(
217
                            'use %s%s as %s;',
218
                            $useTypeFormatted,
219
                            $useStatement->getFullyQualifiedTypeName(),
220
                            $useStatement->getNameAsReferencedInFile()
221
                        );
222
                },
223
                $this->useStatements
224
            )
225
        );
226
    }
227
228
    /**
229
     * Checks if the uses are in the correct order.
230
     *
231
     * @throws CodeError
232
     *
233
     * @return void
234
     */
235
    protected function processToken(): void
236
    {
237
        $prevStatement = null;
238
239
        foreach ($this->useStatements as $useStatement) {
240
            if ($prevStatement && ($this->compareUseStatements($prevStatement, $useStatement) > 0)) {
241
                $exception = new CodeError(
242
                    static::CODE_INCORRECT_ORDER,
243
                    'Use statements should be sorted alphabetically. The first wrong one is %s.',
244
                    $useStatement->getPointer()
245
                );
246
247
                $exception
248
                    ->setPayload([$useStatement->getFullyQualifiedTypeName()])
249
                    ->isFixable(true);
250
251
                throw $exception;
252
            }
253
254
            $prevStatement = $useStatement;
255
        }
256
    }
257
258
    /**
259
     * Registers the tokens that this sniff wants to listen for.
260
     *
261
     * An example return value for a sniff that wants to listen for whitespace
262
     * and any comments would be:
263
     *
264
     * <code>
265
     *    return array(
266
     *            T_WHITESPACE,
267
     *            T_DOC_COMMENT,
268
     *            T_COMMENT,
269
     *           );
270
     * </code>
271
     *
272
     * @return int[]
273
     */
274
    public function register(): array
275
    {
276
        return [T_OPEN_TAG];
277
    }
278
279
    /**
280
     * Removes the lines of the old statements.
281
     *
282
     * @param UseStatement $firstUseStatement
283
     *
284
     * @return void
285
     */
286
    private function removeOldUseStatements(UseStatement $firstUseStatement): void
287
    {
288
        $file = $this->getFile();
289
        $lastUseStatement = end($this->useStatements);
290
        $lastSemicolonPointer = TokenHelper::findNext($file, T_SEMICOLON, $lastUseStatement->getPointer());
291
292
        for ($i = $firstUseStatement->getPointer(); $i <= $lastSemicolonPointer; $i++) {
293
            $file->fixer->replaceToken($i, '');
294
        }
295
    }
296
297
    /**
298
     * Saves the use-property by the compare function.
299
     *
300
     * @return void
301
     */
302
    private function sortUseStatements(): void
303
    {
304
        uasort($this->useStatements, function (UseStatement $prevStatement, UseStatement $nextStatement) {
305
            return $this->compareUseStatements($prevStatement, $nextStatement);
306
        });
307
    }
308
}
309