Skip to content

Commit 33aaace

Browse files
authored
Merge pull request mayflower#44 from xalopp/use_statement_dictionary_order
add configurable order function for Alphabetical Use Statement Sniff, fixes mayflower#5
2 parents f612402 + daafdf0 commit 33aaace

6 files changed

+236
-9
lines changed

MO4/Sniffs/Formatting/AlphabeticalUseStatementsSniff.php

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,55 @@
1616
/**
1717
* Alphabetical Use Statements sniff.
1818
*
19-
* Use statements must be in alphabetical order, grouped by empty lines
19+
* Use statements must be in alphabetical order, grouped by empty lines.
2020
*
2121
* @category PHP
2222
* @package PHP_CodeSniffer-MO4
2323
* @author Xaver Loppenstedt <[email protected]>
2424
* @author Steffen Ritter <[email protected]>
2525
* @author Christian Albrecht <[email protected]>
26-
* @copyright 2013-2014 Xaver Loppenstedt, some rights reserved.
26+
* @copyright 2013-2017 Xaver Loppenstedt, some rights reserved.
2727
* @license http://spdx.org/licenses/MIT MIT License
2828
* @link https://github.com/Mayflower/mo4-coding-standard
2929
*/
3030

3131
namespace MO4\Sniffs\Formatting;
3232

33+
use PHP_CodeSniffer\Exceptions\RuntimeException;
3334
use PHP_CodeSniffer\Files\File;
3435
use PHP_CodeSniffer\Standards\PSR2\Sniffs\Namespaces\UseDeclarationSniff;
36+
use PHP_CodeSniffer\Util\Common;
3537
use PHP_CodeSniffer\Util\Tokens as PHP_CodeSniffer_Tokens;
3638

3739
class AlphabeticalUseStatementsSniff extends UseDeclarationSniff
3840
{
41+
42+
const NAMESPACE_SEPRATOR_STRING = '\\';
43+
44+
/**
45+
* Sorting order, can be one of:
46+
* 'dictionary', 'string', 'string-locale' or 'string-case-insensitive'
47+
*
48+
* Unknown types will be mapped to 'string'.
49+
*
50+
* @var string
51+
*/
52+
public $order = 'dictionary';
53+
54+
55+
/**
56+
* Supported ordering methods
57+
*
58+
* @var array
59+
*/
60+
private $supportedOrderingMethods = [
61+
'dictionary',
62+
'string',
63+
'string',
64+
'string-locale',
65+
'string-case-insensitive',
66+
];
67+
3968
/**
4069
* Last import seen in group
4170
*
@@ -58,6 +87,30 @@ class AlphabeticalUseStatementsSniff extends UseDeclarationSniff
5887
private $currentFile = null;
5988

6089

90+
/**
91+
* Returns an array of tokens this test wants to listen for.
92+
*
93+
* @return array
94+
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException
95+
*/
96+
public function register()
97+
{
98+
if (in_array($this->order, $this->supportedOrderingMethods) === false) {
99+
$error = sprintf(
100+
"'%s' is not a valid order function for %s! Pick one of: %s",
101+
$this->order,
102+
Common::getSniffCode(__CLASS__),
103+
implode(', ', $this->supportedOrderingMethods)
104+
);
105+
106+
throw new RuntimeException($error);
107+
}
108+
109+
return parent::register();
110+
111+
}//end register()
112+
113+
61114
/**
62115
* Processes this test, when one of its tokens is encountered.
63116
*
@@ -99,11 +152,11 @@ public function process(File $phpcsFile, $stackPtr)
99152

100153
$fixable = false;
101154
if ($this->lastImport !== ''
102-
&& strcmp($this->lastImport, $currentImport) > 0
155+
&& $this->compareString($this->lastImport, $currentImport) > 0
103156
) {
104-
$msg = 'USE statements must be sorted alphabetically';
157+
$msg = 'USE statements must be sorted alphabetically, order %s';
105158
$code = 'MustBeSortedAlphabetically';
106-
$fixable = $phpcsFile->addFixableError($msg, $currentPtr, $code);
159+
$fixable = $phpcsFile->addFixableError($msg, $currentPtr, $code, [$this->order]);
107160
}
108161

109162
if (true === $fixable) {
@@ -280,12 +333,82 @@ private function findNewDestination(
280333
$prevLine = $tokens[$prevPtr]['line'];
281334
$prevImportArr = $this->getUseImport($phpcsFile, $prevPtr);
282335
} while ($prevLine === ($line - 1)
283-
&& (strcmp($prevImportArr['content'], $import) > 0)
336+
&& ($this->compareString($prevImportArr['content'], $import) > 0)
284337
);
285338

286339
return $ptr;
287340

288341
}//end findNewDestination()
289342

290343

344+
/**
345+
* Compare namespace strings according defined order function.
346+
*
347+
* @param string $a first namespace string
348+
* @param string $b second namespace string
349+
*
350+
* @return int
351+
*/
352+
private function compareString($a, $b)
353+
{
354+
if ('dictionary' === $this->order) {
355+
return $this->dictionaryCompare($a, $b);
356+
} else if ('string' === $this->order) {
357+
return strcmp($a, $b);
358+
} else if ('string-locale' === $this->order) {
359+
return strcoll($a, $b);
360+
} else if ('string-case-insensitive' === $this->order) {
361+
return strcasecmp($a, $b);
362+
} else {
363+
return $this->dictionaryCompare($a, $b);
364+
}
365+
366+
}//end compareString()
367+
368+
369+
/**
370+
* Lexicographical namespace string compare.
371+
*
372+
* Example:
373+
*
374+
* use Doctrine\ORM\Query;
375+
* use Doctrine\ORM\Query\Expr;
376+
* use Doctrine\ORM\QueryBuilder;
377+
*
378+
* @param string $a first namespace string
379+
* @param string $b second namespace string
380+
*
381+
* @return int
382+
*/
383+
private function dictionaryCompare($a, $b)
384+
{
385+
$min = min(strlen($a), strlen($b));
386+
387+
for ($i = 0; $i < $min; $i++) {
388+
if ($a[$i] === $b[$i]) {
389+
continue;
390+
}
391+
392+
if ($a[$i] === self::NAMESPACE_SEPRATOR_STRING) {
393+
return -1;
394+
}
395+
396+
if ($b[$i] === self::NAMESPACE_SEPRATOR_STRING) {
397+
return 1;
398+
}
399+
400+
if ($a[$i] < $b[$i]) {
401+
return -1;
402+
}
403+
404+
if ($a[$i] > $b[$i]) {
405+
return 1;
406+
}
407+
}//end for
408+
409+
return strcmp(substr($a, $min), substr($b, $min));
410+
411+
}//end dictionaryCompare()
412+
413+
291414
}//end class
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
use A1\B\C;
4+
use A1\B;
5+
use A1\BD;
6+
7+
use A2\BD;
8+
use A2\B;
9+
use A2\B\C;
10+
11+
use A3\B;
12+
use A3\BD;
13+
use A3\B\C;
14+
15+
use A4\B\C;
16+
use A4\BD;
17+
use A4\B;
18+
19+
use A5\BD;
20+
use A5\B\C;
21+
use A5\B;
22+
23+
class Foo
24+
{
25+
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
use A1\B;
4+
use A1\B\C;
5+
use A1\BD;
6+
7+
use A2\B;
8+
use A2\B\C;
9+
use A2\BD;
10+
11+
use A3\B;
12+
use A3\B\C;
13+
use A3\BD;
14+
15+
use A4\B;
16+
use A4\B\C;
17+
use A4\BD;
18+
19+
use A5\B;
20+
use A5\B\C;
21+
use A5\BD;
22+
23+
class Foo
24+
{
25+
26+
}

MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.pass.inc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ use Z;
1212

1313
use X;
1414

15+
use A\B;
16+
use A\B\C;
17+
use A\BD;
18+
1519
class Foo {
1620
$bar = 'foo';
1721

@@ -25,4 +29,4 @@ class Foo {
2529

2630
// test run away tokens
2731
use () {};
28-
}
32+
}

MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ protected function getErrorList($testFile='')
7171
10 => 1,
7272
15 => 1,
7373
);
74+
case 'AlphabeticalUseStatementsUnitTest.fail.4.inc':
75+
return array(
76+
4 => 1,
77+
8 => 1,
78+
13 => 1,
79+
17 => 1,
80+
20 => 1,
81+
21 => 1,
82+
);
7483
}//end switch
7584

7685
return null;

README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,52 @@ The MO4 Coding Standard is an extension of the [Symfony Coding Standard](http://
1717
* in associative arrays, the `=>` operators must be aligned
1818
* in arrays, the key and `=>` operator must be on the same line
1919
* each consecutive variable assignment must align at the assignment operator
20-
* use statements must be sorted lexicographically
20+
* use statements must be sorted lexicographically, grouped by empty lines. The order function can be configured.
2121
* you should use the imported class name when it was imported with a use statement
2222
* interpolated variables in double quoted strings must be surrounded by `{ }`, e.g. `{$VAR}` instead of `$VAR`
23-
* `sprintf` or `"{$VAR1} {$VAR2}"` must be used instead of the dot operator; concat operators are only allowed to concatenate constants and multi line strings,
23+
* `sprintf` or `"{$VAR1} {$VAR2}"` must be used instead of the dot operator; concat operators are only allowed to concatenate constants and multi line strings
2424
* a whitespace is required after each typecast, e.g. `(int) $value` instead of `(int)$value`
2525
* doc blocks of class properties must be multiline and have exactly one `@var` annotation
2626

27+
## Configuration
28+
29+
### MO4.Formatting.AlphabeticalUseStatements
30+
31+
The `order` property of the `MO4.Formatting.AlphabeticalUseStatements` sniff defines
32+
which function is used for ordering.
33+
34+
Possible values for order:
35+
* `dictionary` (default): based on [strcmp](http://php.net/strcmp), the namespace separator
36+
precedes any other character
37+
```php
38+
use Doctrine\ORM\Query;
39+
use Doctrine\ORM\Query\Expr;
40+
use Doctrine\ORM\QueryBuilder;
41+
```
42+
* `string`: binary safe string comparison using [strcmp](http://php.net/strcmp)
43+
```php
44+
use Doctrine\ORM\Query;
45+
use Doctrine\ORM\QueryBuilder;
46+
use Doctrine\ORM\Query\Expr;
47+
48+
use ExampleSub;
49+
use Examples;
50+
```
51+
* `string-locale`: locale based string comparison using [strcoll](http://php.net/strcoll)
52+
* `string-case-insenstive`: binary safe case-insensitive string comparison [strcasecmp](http://php.net/strcasecmp)
53+
```php
54+
use Examples;
55+
use ExampleSub;
56+
```
57+
58+
```
59+
<rule ref="MO4.Formatting.AlphabeticalUseStatements">
60+
<properties>
61+
<property name="order" value="string-locale"/>
62+
</properties>
63+
</rule>
64+
```
65+
2766
## Installation
2867

2968
### Composer

0 commit comments

Comments
 (0)