Completed
Push — master ( afcead...0ae635 )
by Michael
02:32
created

AlphabeticalUseStatementsSniff::process()   C

Complexity

Conditions 9
Paths 15

Size

Total Lines 71
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 43
nc 15
nop 2
dl 0
loc 71
rs 6.0993
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of the mo4-coding-standard (phpcs standard)
5
 *
6
 * PHP version 5
7
 *
8
 * @category PHP
9
 * @package  PHP_CodeSniffer-MO4
10
 * @author   Xaver Loppenstedt <[email protected]>
11
 * @license  http://spdx.org/licenses/MIT MIT License
12
 * @version  GIT: master
13
 * @link     https://github.com/Mayflower/mo4-coding-standard
14
 */
15
16
namespace MO4\Sniffs\Formatting;
17
18
use PHP_CodeSniffer\Exceptions\RuntimeException;
19
use PHP_CodeSniffer\Files\File;
20
use PHP_CodeSniffer\Standards\PSR2\Sniffs\Namespaces\UseDeclarationSniff;
21
use PHP_CodeSniffer\Util\Common;
22
use PHP_CodeSniffer\Util\Tokens as PHP_CodeSniffer_Tokens;
23
24
/**
25
 * Alphabetical Use Statements sniff.
26
 *
27
 * Use statements must be in alphabetical order, grouped by empty lines.
28
 *
29
 * @category  PHP
30
 * @package   PHP_CodeSniffer-MO4
31
 * @author    Xaver Loppenstedt <[email protected]>
32
 * @author    Steffen Ritter <[email protected]>
33
 * @author    Christian Albrecht <[email protected]>
34
 * @copyright 2013-2017 Xaver Loppenstedt, some rights reserved.
35
 * @license   http://spdx.org/licenses/MIT MIT License
36
 * @link      https://github.com/Mayflower/mo4-coding-standard
37
 */
38
class AlphabeticalUseStatementsSniff extends UseDeclarationSniff
39
{
40
41
    const NAMESPACE_SEPARATOR_STRING = '\\';
42
43
    /**
44
     * Sorting order, can be one of:
45
     *   'dictionary', 'string', 'string-locale' or 'string-case-insensitive'
46
     *
47
     * Unknown types will be mapped to 'string'.
48
     *
49
     * @var string
50
     */
51
    public $order = 'dictionary';
52
53
    /**
54
     * Supported ordering methods
55
     *
56
     * @var array
57
     */
58
    private $supportedOrderingMethods = [
59
        'dictionary',
60
        'string',
61
        'string',
62
        'string-locale',
63
        'string-case-insensitive',
64
    ];
65
66
    /**
67
     * Last import seen in group
68
     *
69
     * @var string
70
     */
71
    private $lastImport = '';
72
73
    /**
74
     * Line number of the last seen use statement
75
     *
76
     * @var integer
77
     */
78
    private $lastLine = -1;
79
80
    /**
81
     * Current file
82
     *
83
     * @var string
84
     */
85
    private $currentFile;
86
87
88
    /**
89
     * Processes this test, when one of its tokens is encountered.
90
     *
91
     * @param File $phpcsFile The file being scanned.
92
     * @param int  $stackPtr  The position of the current token in
93
     *                        the stack passed in $tokens.
94
     *
95
     * @return void
96
     */
97
    public function process(File $phpcsFile, $stackPtr)
98
    {
99
        if (in_array($this->order, $this->supportedOrderingMethods, true) === false) {
100
            $error = sprintf(
101
                "'%s' is not a valid order function for %s! Pick one of: %s",
102
                $this->order,
103
                Common::getSniffCode(__CLASS__),
104
                implode(', ', $this->supportedOrderingMethods)
105
            );
106
107
            $phpcsFile->addError($error, $stackPtr, 'InvalidOrder');
108
109
            return;
110
        }
111
112
        parent::process($phpcsFile, $stackPtr);
113
114
        if ($this->currentFile !== $phpcsFile->getFilename()) {
115
            $this->lastLine    = -1;
116
            $this->lastImport  = '';
117
            $this->currentFile = $phpcsFile->getFilename();
118
        }
119
120
        $tokens = $phpcsFile->getTokens();
121
        $line   = $tokens[$stackPtr]['line'];
122
123
        // Ignore function () use () {...}.
124
        $isNonImportUse = $this->checkIsNonImportUse($phpcsFile, $stackPtr);
125
        if (true === $isNonImportUse) {
126
            return;
127
        }
128
129
        $currentImportArr = $this->getUseImport($phpcsFile, $stackPtr);
130
        if ($currentImportArr === false) {
131
            return;
132
        }
133
134
        $currentPtr    = $currentImportArr['startPtr'];
135
        $currentImport = $currentImportArr['content'];
136
137
        if (($this->lastLine + 1) < $line) {
138
            $this->lastLine   = $line;
139
            $this->lastImport = $currentImport;
140
141
            return;
142
        }
143
144
        $fixable = false;
145
        if ($this->lastImport !== ''
146
            && $this->compareString($this->lastImport, $currentImport) > 0
147
        ) {
148
            $msg     = 'USE statements must be sorted alphabetically, order %s';
149
            $code    = 'MustBeSortedAlphabetically';
150
            $fixable = $phpcsFile->addFixableError($msg, $currentPtr, $code, [$this->order]);
151
        }
152
153
        if (true === $fixable) {
154
            // Find the correct position in current use block.
155
            $newDestinationPtr
156
                = $this->findNewDestination($phpcsFile, $stackPtr, $currentImport);
157
158
            $currentUseStr = $this->getUseStatementAsString($phpcsFile, $stackPtr);
159
160
            $phpcsFile->fixer->beginChangeset();
161
            $phpcsFile->fixer->addContentBefore($newDestinationPtr, $currentUseStr);
162
            $this->fixerClearLine($phpcsFile, $stackPtr);
163
            $phpcsFile->fixer->endChangeset();
164
        }//end if
165
166
        $this->lastImport = $currentImport;
167
        $this->lastLine   = $line;
168
169
    }//end process()
170
171
172
    /**
173
     * Get the import class name for use statement pointed by $stackPtr.
174
     *
175
     * @param File $phpcsFile PHP CS File
176
     * @param int  $stackPtr  pointer
177
     *
178
     * @return array|false
179
     */
180
    private function getUseImport(File $phpcsFile, $stackPtr)
181
    {
182
        $importTokens = [
183
            T_NS_SEPARATOR,
184
            T_STRING,
185
        ];
186
187
        $start = $phpcsFile->findNext(
188
            PHP_CodeSniffer_Tokens::$emptyTokens,
189
            ($stackPtr + 1),
190
            null,
191
            true
192
        );
193
        // $start is false when "use" is the last token in file...
194
        if ($start === false) {
195
            return false;
196
        }
197
198
        $start  = (int) $start;
199
        $end    = $phpcsFile->findNext($importTokens, $start, null, true);
200
        $import = $phpcsFile->getTokensAsString($start, ($end - $start));
201
202
        return [
203
            'startPtr' => $start,
204
            'content'  => $import,
205
        ];
206
207
    }//end getUseImport()
208
209
210
    /**
211
     * Get the full use statement as string, including trailing white space.
212
     *
213
     * @param File $phpcsFile PHP CS File
214
     * @param int  $stackPtr  pointer
215
     *
216
     * @return string
217
     */
218
    private function getUseStatementAsString(
219
        File $phpcsFile,
220
        $stackPtr
221
    ) {
222
        $tokens = $phpcsFile->getTokens();
223
224
        $useEndPtr = $phpcsFile->findNext([T_SEMICOLON], ($stackPtr + 2));
225
        $useLength = ($useEndPtr - $stackPtr + 1);
226
        if ($tokens[($useEndPtr + 1)]['code'] === T_WHITESPACE) {
227
            $useLength++;
228
        }
229
230
        return $phpcsFile->getTokensAsString($stackPtr, $useLength);
231
232
    }//end getUseStatementAsString()
233
234
235
    /**
236
     * Check if "use" token is not used for import.
237
     * E.g. function () use () {...}.
238
     *
239
     * @param File $phpcsFile PHP CS File
240
     * @param int  $stackPtr  pointer
241
     *
242
     * @return bool
243
     */
244
    private function checkIsNonImportUse(File $phpcsFile, $stackPtr)
245
    {
246
        $tokens = $phpcsFile->getTokens();
247
248
        $prev = $phpcsFile->findPrevious(
249
            PHP_CodeSniffer_Tokens::$emptyTokens,
250
            ($stackPtr - 1),
251
            null,
252
            true,
253
            null,
254
            true
255
        );
256
257
        if (false !== $prev) {
258
            $prevToken = $tokens[$prev];
259
260
            if ($prevToken['code'] === T_CLOSE_PARENTHESIS) {
261
                return true;
262
            }
263
        }
264
265
        return false;
266
267
    }//end checkIsNonImportUse()
268
269
270
    /**
271
     * Replace all the token in same line as the element pointed to by $stackPtr
272
     * the by the empty string.
273
     * This will delete the line.
274
     *
275
     * @param File $phpcsFile PHP CS file
276
     * @param int  $stackPtr  pointer
277
     *
278
     * @return void
279
     */
280
    private function fixerClearLine(File $phpcsFile, $stackPtr)
281
    {
282
        $tokens = $phpcsFile->getTokens();
283
        $line   = $tokens[$stackPtr]['line'];
284
285 View Code Duplication
        for ($i = ($stackPtr - 1); $tokens[$i]['line'] === $line; $i--) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
286
            $phpcsFile->fixer->replaceToken($i, '');
287
        }
288
289 View Code Duplication
        for ($i = $stackPtr; $tokens[$i]['line'] === $line; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
290
            $phpcsFile->fixer->replaceToken($i, '');
291
        }
292
293
    }//end fixerClearLine()
294
295
296
    /**
297
     * Find a new destination pointer for the given import string in current
298
     * use block.
299
     *
300
     * @param File   $phpcsFile PHP CS File
301
     * @param int    $stackPtr  pointer
302
     * @param string $import    import string requiring new position
303
     *
304
     * @return int
305
     */
306
    private function findNewDestination(
307
        File $phpcsFile,
308
        $stackPtr,
309
        $import
310
    ) {
311
        $tokens = $phpcsFile->getTokens();
312
313
        $line     = $tokens[$stackPtr]['line'];
314
        $prevLine = false;
315
        $prevPtr  = $stackPtr;
316
        do {
317
            $ptr = $prevPtr;
318
            // Use $line for the first iteration.
319
            if ($prevLine !== false) {
320
                $line = $prevLine;
321
            }
322
323
            $prevPtr = $phpcsFile->findPrevious(T_USE, ($ptr - 1));
324
            if ($prevPtr === false) {
325
                break;
326
            }
327
328
            $prevLine      = $tokens[$prevPtr]['line'];
329
            $prevImportArr = $this->getUseImport($phpcsFile, (int) $prevPtr);
330
        } while ($prevLine === ($line - 1)
331
            && ($this->compareString($prevImportArr['content'], $import) > 0)
332
        );
333
334
        return $ptr;
335
336
    }//end findNewDestination()
337
338
339
    /**
340
     * Compare namespace strings according defined order function.
341
     *
342
     * @param string $a first namespace string
343
     * @param string $b second namespace string
344
     *
345
     * @return int
346
     */
347
    private function compareString($a, $b)
348
    {
349
        switch ($this->order) {
350
        case 'string':
351
            return strcmp($a, $b);
352
        case 'string-locale':
353
            return strcoll($a, $b);
354
        case 'string-case-insensitive':
355
            return strcasecmp($a, $b);
356
        default:
357
            // Default is 'dictionary'.
358
            return $this->dictionaryCompare($a, $b);
359
        }
360
361
    }//end compareString()
362
363
364
    /**
365
     * Lexicographical namespace string compare.
366
     *
367
     * Example:
368
     *
369
     *   use Doctrine\ORM\Query;
370
     *   use Doctrine\ORM\Query\Expr;
371
     *   use Doctrine\ORM\QueryBuilder;
372
     *
373
     * @param string $a first namespace string
374
     * @param string $b second namespace string
375
     *
376
     * @return int
377
     */
378
    private function dictionaryCompare($a, $b)
379
    {
380
        $min = min(strlen($a), strlen($b));
381
382
        for ($i = 0; $i < $min; $i++) {
383
            if ($a[$i] === $b[$i]) {
384
                continue;
385
            }
386
387
            if ($a[$i] === self::NAMESPACE_SEPARATOR_STRING) {
388
                return -1;
389
            }
390
391
            if ($b[$i] === self::NAMESPACE_SEPARATOR_STRING) {
392
                return 1;
393
            }
394
395
            if ($a[$i] < $b[$i]) {
396
                return -1;
397
            }
398
399
            if ($a[$i] > $b[$i]) {
400
                return 1;
401
            }
402
        }//end for
403
404
        return strcmp(substr($a, $min), substr($b, $min));
0 ignored issues
show
Bug introduced by
It seems like substr($a, $min) can also be of type false; however, parameter $str1 of strcmp() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

404
        return strcmp(/** @scrutinizer ignore-type */ substr($a, $min), substr($b, $min));
Loading history...
Bug introduced by
It seems like substr($b, $min) can also be of type false; however, parameter $str2 of strcmp() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

404
        return strcmp(substr($a, $min), /** @scrutinizer ignore-type */ substr($b, $min));
Loading history...
405
406
    }//end dictionaryCompare()
407
408
409
}//end class
410