Passed
Branch master (ccf1c2)
by Christian
03:57
created

AlphabeticalUseStatementsSniff::process()   B

Complexity

Conditions 8
Paths 14

Size

Total Lines 60
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 60
rs 7.0677
c 0
b 0
f 0
cc 8
eloc 36
nc 14
nop 2

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_SEPRATOR_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
    /**
55
     * Supported ordering methods
56
     *
57
     * @var array
58
     */
59
    private $supportedOrderingMethods = [
60
                                         'dictionary',
61
                                         'string',
62
                                         'string',
63
                                         'string-locale',
64
                                         'string-case-insensitive',
65
                                        ];
66
67
    /**
68
     * Last import seen in group
69
     *
70
     * @var string
71
     */
72
    private $lastImport = '';
73
74
    /**
75
     * Line number of the last seen use statement
76
     *
77
     * @var integer
78
     */
79
    private $lastLine = -1;
80
81
    /**
82
     * Current file
83
     *
84
     * @var string
85
     */
86
    private $currentFile = null;
87
88
89
    /**
90
     * Returns an array of tokens this test wants to listen for.
91
     *
92
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use integer[].

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
93
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException
94
     */
95
    public function register()
96
    {
97
        if (in_array($this->order, $this->supportedOrderingMethods, true) === false) {
98
            $error = sprintf(
99
                "'%s' is not a valid order function for %s! Pick one of: %s",
100
                $this->order,
101
                Common::getSniffCode(__CLASS__),
102
                implode(', ', $this->supportedOrderingMethods)
103
            );
104
105
            throw new RuntimeException($error);
106
        }
107
108
        return parent::register();
109
110
    }//end register()
111
112
113
    /**
114
     * Processes this test, when one of its tokens is encountered.
115
     *
116
     * @param File $phpcsFile The file being scanned.
117
     * @param int  $stackPtr  The position of the current token in
118
     *                        the stack passed in $tokens.
119
     *
120
     * @return void
121
     */
122
    public function process(File $phpcsFile, $stackPtr)
123
    {
124
        parent::process($phpcsFile, $stackPtr);
125
126
        if ($this->currentFile !== $phpcsFile->getFilename()) {
127
            $this->lastLine    = -1;
128
            $this->lastImport  = '';
129
            $this->currentFile = $phpcsFile->getFilename();
130
        }
131
132
        $tokens = $phpcsFile->getTokens();
133
        $line   = $tokens[$stackPtr]['line'];
134
135
        // Ignore function () use () {...}.
136
        $isNonImportUse = $this->checkIsNonImportUse($phpcsFile, $stackPtr);
137
        if (true === $isNonImportUse) {
138
            return;
139
        }
140
141
        $currentImportArr = $this->getUseImport($phpcsFile, $stackPtr);
142
        if ($currentImportArr === false) {
143
            return;
144
        }
145
146
        $currentPtr    = $currentImportArr['startPtr'];
147
        $currentImport = $currentImportArr['content'];
148
149
        if (($this->lastLine + 1) < $line) {
150
            $this->lastLine   = $line;
151
            $this->lastImport = $currentImport;
152
153
            return;
154
        }
155
156
        $fixable = false;
157
        if ($this->lastImport !== ''
158
            && $this->compareString($this->lastImport, $currentImport) > 0
159
        ) {
160
            $msg     = 'USE statements must be sorted alphabetically, order %s';
161
            $code    = 'MustBeSortedAlphabetically';
162
            $fixable = $phpcsFile->addFixableError($msg, $currentPtr, $code, [$this->order]);
163
        }
164
165
        if (true === $fixable) {
166
            // Find the correct position in current use block.
167
            $newDestinationPtr
168
                = $this->findNewDestination($phpcsFile, $stackPtr, $currentImport);
169
170
            $currentUseStr = $this->getUseStatementAsString($phpcsFile, $stackPtr);
171
172
            $phpcsFile->fixer->beginChangeset();
173
            $phpcsFile->fixer->addContentBefore($newDestinationPtr, $currentUseStr);
174
            $this->fixerClearLine($phpcsFile, $stackPtr);
175
            $phpcsFile->fixer->endChangeset();
176
        }//end if
177
178
        $this->lastImport = $currentImport;
179
        $this->lastLine   = $line;
180
181
    }//end process()
182
183
184
    /**
185
     * Get the import class name for use statement pointed by $stackPtr.
186
     *
187
     * @param File $phpcsFile PHP CS File
188
     * @param int  $stackPtr  pointer
189
     *
190
     * @return array|false
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|array<string,integer|string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
191
     */
192
    private function getUseImport(File $phpcsFile, $stackPtr)
193
    {
194
        $importTokens = array(
195
                         T_NS_SEPARATOR,
196
                         T_STRING,
197
                        );
198
199
        $start = $phpcsFile->findNext(
200
            PHP_CodeSniffer_Tokens::$emptyTokens,
201
            ($stackPtr + 1),
202
            null,
203
            true
204
        );
205
        // $start is false when "use" is the last token in file...
206
        if ($start === false) {
207
            return false;
208
        }
209
210
        $start  = (int) $start;
211
        $end    = $phpcsFile->findNext($importTokens, $start, null, true);
212
        $import = $phpcsFile->getTokensAsString($start, ($end - $start));
213
214
        return array(
215
                'startPtr' => $start,
216
                'content'  => $import,
217
               );
218
219
    }//end getUseImport()
220
221
222
    /**
223
     * Get the full use statement as string, including trailing white space.
224
     *
225
     * @param File $phpcsFile PHP CS File
226
     * @param int  $stackPtr  pointer
227
     *
228
     * @return string
229
     */
230
    private function getUseStatementAsString(
231
        File $phpcsFile,
232
        $stackPtr
233
    ) {
234
        $tokens = $phpcsFile->getTokens();
235
236
        $useEndPtr = $phpcsFile->findNext(array(T_SEMICOLON), ($stackPtr + 2));
237
        $useLength = ($useEndPtr - $stackPtr + 1);
238
        if ($tokens[($useEndPtr + 1)]['code'] === T_WHITESPACE) {
239
            $useLength++;
240
        }
241
242
        $useStr = $phpcsFile->getTokensAsString($stackPtr, $useLength);
243
244
        return $useStr;
245
246
    }//end getUseStatementAsString()
247
248
249
    /**
250
     * Check if "use" token is not used for import.
251
     * E.g. function () use () {...}.
252
     *
253
     * @param File $phpcsFile PHP CS File
254
     * @param int  $stackPtr  pointer
255
     *
256
     * @return bool
257
     */
258
    private function checkIsNonImportUse(File $phpcsFile, $stackPtr)
0 ignored issues
show
Coding Style introduced by
function checkIsNonImportUse() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
259
    {
260
        $tokens = $phpcsFile->getTokens();
261
262
        $prev = $phpcsFile->findPrevious(
263
            PHP_CodeSniffer_Tokens::$emptyTokens,
264
            ($stackPtr - 1),
265
            null,
266
            true,
267
            null,
268
            true
269
        );
270
271
        if (false !== $prev) {
272
            $prevToken = $tokens[$prev];
273
274
            if ($prevToken['code'] === T_CLOSE_PARENTHESIS) {
275
                return true;
276
            }
277
        }
278
279
        return false;
280
281
    }//end checkIsNonImportUse()
282
283
284
    /**
285
     * Replace all the token in same line as the element pointed to by $stackPtr
286
     * the by the empty string.
287
     * This will delete the line.
288
     *
289
     * @param File $phpcsFile PHP CS file
290
     * @param int  $stackPtr  pointer
291
     *
292
     * @return void
293
     */
294
    private function fixerClearLine(File $phpcsFile, $stackPtr)
295
    {
296
        $tokens = $phpcsFile->getTokens();
297
        $line   = $tokens[$stackPtr]['line'];
298
299 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...
300
            $phpcsFile->fixer->replaceToken($i, '');
301
        }
302
303 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...
304
            $phpcsFile->fixer->replaceToken($i, '');
305
        }
306
307
    }//end fixerClearLine()
308
309
310
    /**
311
     * Find a new destination pointer for the given import string in current
312
     * use block.
313
     *
314
     * @param File   $phpcsFile PHP CS File
315
     * @param int    $stackPtr  pointer
316
     * @param string $import    import string requiring new position
317
     *
318
     * @return int
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
319
     */
320
    private function findNewDestination(
321
        File $phpcsFile,
322
        $stackPtr,
323
        $import
324
    ) {
325
        $tokens = $phpcsFile->getTokens();
326
327
        $line     = $tokens[$stackPtr]['line'];
328
        $prevLine = false;
329
        $prevPtr  = $stackPtr;
330
        do {
331
            $ptr = $prevPtr;
332
            // Use $line for the first iteration.
333
            if ($prevLine !== false) {
334
                $line = $prevLine;
335
            }
336
337
            $prevPtr = $phpcsFile->findPrevious(T_USE, ($ptr - 1));
338
            if ($prevPtr === false) {
339
                break;
340
            }
341
342
            $prevLine      = $tokens[$prevPtr]['line'];
343
            $prevImportArr = $this->getUseImport($phpcsFile, (int) $prevPtr);
344
        } while ($prevLine === ($line - 1)
345
            && ($this->compareString($prevImportArr['content'], $import) > 0)
346
        );
347
348
        return $ptr;
349
350
    }//end findNewDestination()
351
352
353
    /**
354
     * Compare namespace strings according defined order function.
355
     *
356
     * @param string $a first namespace string
357
     * @param string $b second namespace string
358
     *
359
     * @return int
360
     */
361
    private function compareString($a, $b)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $a. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
Comprehensibility introduced by
Avoid variables with short names like $b. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
362
    {
363
        switch ($this->order) {
364
        case 'string':
365
            return strcmp($a, $b);
366
        case 'string-locale':
367
            return strcoll($a, $b);
368
        case 'string-case-insensitive':
369
            return strcasecmp($a, $b);
370
        default:
371
            // Default is 'dictionary'.
372
            return $this->dictionaryCompare($a, $b);
373
        }
374
375
    }//end compareString()
376
377
378
    /**
379
     * Lexicographical namespace string compare.
380
     *
381
     * Example:
382
     *
383
     *   use Doctrine\ORM\Query;
384
     *   use Doctrine\ORM\Query\Expr;
385
     *   use Doctrine\ORM\QueryBuilder;
386
     *
387
     * @param string $a first namespace string
388
     * @param string $b second namespace string
389
     *
390
     * @return int
391
     */
392
    private function dictionaryCompare($a, $b)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $a. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
Comprehensibility introduced by
Avoid variables with short names like $b. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
393
    {
394
        $min = min(strlen($a), strlen($b));
395
396
        for ($i = 0; $i < $min; $i++) {
397
            if ($a[$i] === $b[$i]) {
398
                continue;
399
            }
400
401
            if ($a[$i] === self::NAMESPACE_SEPRATOR_STRING) {
402
                return -1;
403
            }
404
405
            if ($b[$i] === self::NAMESPACE_SEPRATOR_STRING) {
406
                return 1;
407
            }
408
409
            if ($a[$i] < $b[$i]) {
410
                return -1;
411
            }
412
413
            if ($a[$i] > $b[$i]) {
414
                return 1;
415
            }
416
        }//end for
417
418
        return strcmp(substr($a, $min), substr($b, $min));
419
420
    }//end dictionaryCompare()
421
422
423
}//end class
424