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

removeOldUseStatements()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 10
rs 9.9332
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\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 $useStatements;
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::getUseStatements(
49
            $this->getFile()->getBaseFile(),
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)) {
0 ignored issues
show
Documentation introduced by
$nextStatement is of type object<SlevomatCodingSta...d\Helpers\UseStatement>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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()->getBaseFile();
175
176
        $file->fixer->beginChangeset();
177
178
        $this->removeOldUseStatements($firstUseStatement);
0 ignored issues
show
Security Bug introduced by
It seems like $firstUseStatement defined by \reset($this->useStatements) on line 172 can also be of type false; however, BestIt\Sniffs\Formatting...emoveOldUseStatements() does only seem to accept object<SlevomatCodingSta...d\Helpers\UseStatement>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
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()->getBaseFile();
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()->getBaseFile();
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