diff --git a/MO4/Sniffs/Formatting/AlphabeticalUseStatementsSniff.php b/MO4/Sniffs/Formatting/AlphabeticalUseStatementsSniff.php index 79fe640b..10c64a2e 100644 --- a/MO4/Sniffs/Formatting/AlphabeticalUseStatementsSniff.php +++ b/MO4/Sniffs/Formatting/AlphabeticalUseStatementsSniff.php @@ -16,26 +16,55 @@ /** * Alphabetical Use Statements sniff. * - * Use statements must be in alphabetical order, grouped by empty lines + * Use statements must be in alphabetical order, grouped by empty lines. * * @category PHP * @package PHP_CodeSniffer-MO4 * @author Xaver Loppenstedt * @author Steffen Ritter * @author Christian Albrecht - * @copyright 2013-2014 Xaver Loppenstedt, some rights reserved. + * @copyright 2013-2017 Xaver Loppenstedt, some rights reserved. * @license http://spdx.org/licenses/MIT MIT License * @link https://github.com/Mayflower/mo4-coding-standard */ namespace MO4\Sniffs\Formatting; +use PHP_CodeSniffer\Exceptions\RuntimeException; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Standards\PSR2\Sniffs\Namespaces\UseDeclarationSniff; +use PHP_CodeSniffer\Util\Common; use PHP_CodeSniffer\Util\Tokens as PHP_CodeSniffer_Tokens; class AlphabeticalUseStatementsSniff extends UseDeclarationSniff { + + const NAMESPACE_SEPRATOR_STRING = '\\'; + + /** + * Sorting order, can be one of: + * 'dictionary', 'string', 'string-locale' or 'string-case-insensitive' + * + * Unknown types will be mapped to 'string'. + * + * @var string + */ + public $order = 'dictionary'; + + + /** + * Supported ordering methods + * + * @var array + */ + private $supportedOrderingMethods = [ + 'dictionary', + 'string', + 'string', + 'string-locale', + 'string-case-insensitive', + ]; + /** * Last import seen in group * @@ -58,6 +87,30 @@ class AlphabeticalUseStatementsSniff extends UseDeclarationSniff private $currentFile = null; + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException + */ + public function register() + { + if (in_array($this->order, $this->supportedOrderingMethods) === false) { + $error = sprintf( + "'%s' is not a valid order function for %s! Pick one of: %s", + $this->order, + Common::getSniffCode(__CLASS__), + implode(', ', $this->supportedOrderingMethods) + ); + + throw new RuntimeException($error); + } + + return parent::register(); + + }//end register() + + /** * Processes this test, when one of its tokens is encountered. * @@ -99,11 +152,11 @@ public function process(File $phpcsFile, $stackPtr) $fixable = false; if ($this->lastImport !== '' - && strcmp($this->lastImport, $currentImport) > 0 + && $this->compareString($this->lastImport, $currentImport) > 0 ) { - $msg = 'USE statements must be sorted alphabetically'; + $msg = 'USE statements must be sorted alphabetically, order %s'; $code = 'MustBeSortedAlphabetically'; - $fixable = $phpcsFile->addFixableError($msg, $currentPtr, $code); + $fixable = $phpcsFile->addFixableError($msg, $currentPtr, $code, [$this->order]); } if (true === $fixable) { @@ -280,7 +333,7 @@ private function findNewDestination( $prevLine = $tokens[$prevPtr]['line']; $prevImportArr = $this->getUseImport($phpcsFile, $prevPtr); } while ($prevLine === ($line - 1) - && (strcmp($prevImportArr['content'], $import) > 0) + && ($this->compareString($prevImportArr['content'], $import) > 0) ); return $ptr; @@ -288,4 +341,74 @@ private function findNewDestination( }//end findNewDestination() + /** + * Compare namespace strings according defined order function. + * + * @param string $a first namespace string + * @param string $b second namespace string + * + * @return int + */ + private function compareString($a, $b) + { + if ('dictionary' === $this->order) { + return $this->dictionaryCompare($a, $b); + } else if ('string' === $this->order) { + return strcmp($a, $b); + } else if ('string-locale' === $this->order) { + return strcoll($a, $b); + } else if ('string-case-insensitive' === $this->order) { + return strcasecmp($a, $b); + } else { + return $this->dictionaryCompare($a, $b); + } + + }//end compareString() + + + /** + * Lexicographical namespace string compare. + * + * Example: + * + * use Doctrine\ORM\Query; + * use Doctrine\ORM\Query\Expr; + * use Doctrine\ORM\QueryBuilder; + * + * @param string $a first namespace string + * @param string $b second namespace string + * + * @return int + */ + private function dictionaryCompare($a, $b) + { + $min = min(strlen($a), strlen($b)); + + for ($i = 0; $i < $min; $i++) { + if ($a[$i] === $b[$i]) { + continue; + } + + if ($a[$i] === self::NAMESPACE_SEPRATOR_STRING) { + return -1; + } + + if ($b[$i] === self::NAMESPACE_SEPRATOR_STRING) { + return 1; + } + + if ($a[$i] < $b[$i]) { + return -1; + } + + if ($a[$i] > $b[$i]) { + return 1; + } + }//end for + + return strcmp(substr($a, $min), substr($b, $min)); + + }//end dictionaryCompare() + + }//end class diff --git a/MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.fail.4.inc b/MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.fail.4.inc new file mode 100644 index 00000000..7772ebfb --- /dev/null +++ b/MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.fail.4.inc @@ -0,0 +1,26 @@ + 1, 15 => 1, ); + case 'AlphabeticalUseStatementsUnitTest.fail.4.inc': + return array( + 4 => 1, + 8 => 1, + 13 => 1, + 17 => 1, + 20 => 1, + 21 => 1, + ); }//end switch return null; diff --git a/README.md b/README.md index 4ef38279..a7f9d14b 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,52 @@ The MO4 Coding Standard is an extension of the [Symfony Coding Standard](http:// * in associative arrays, the `=>` operators must be aligned * in arrays, the key and `=>` operator must be on the same line * each consecutive variable assignment must align at the assignment operator -* use statements must be sorted lexicographically +* use statements must be sorted lexicographically, grouped by empty lines. The order function can be configured. * you should use the imported class name when it was imported with a use statement * interpolated variables in double quoted strings must be surrounded by `{ }`, e.g. `{$VAR}` instead of `$VAR` -* `sprintf` or `"{$VAR1} {$VAR2}"` must be used instead of the dot operator; concat operators are only allowed to concatenate constants and multi line strings, +* `sprintf` or `"{$VAR1} {$VAR2}"` must be used instead of the dot operator; concat operators are only allowed to concatenate constants and multi line strings * a whitespace is required after each typecast, e.g. `(int) $value` instead of `(int)$value` * doc blocks of class properties must be multiline and have exactly one `@var` annotation +## Configuration + +### MO4.Formatting.AlphabeticalUseStatements + +The `order` property of the `MO4.Formatting.AlphabeticalUseStatements` sniff defines +which function is used for ordering. + +Possible values for order: +* `dictionary` (default): based on [strcmp](http://php.net/strcmp), the namespace separator + precedes any other character + ```php + use Doctrine\ORM\Query; + use Doctrine\ORM\Query\Expr; + use Doctrine\ORM\QueryBuilder; + ``` +* `string`: binary safe string comparison using [strcmp](http://php.net/strcmp) + ```php + use Doctrine\ORM\Query; + use Doctrine\ORM\QueryBuilder; + use Doctrine\ORM\Query\Expr; + + use ExampleSub; + use Examples; + ``` +* `string-locale`: locale based string comparison using [strcoll](http://php.net/strcoll) +* `string-case-insenstive`: binary safe case-insensitive string comparison [strcasecmp](http://php.net/strcasecmp) + ```php + use Examples; + use ExampleSub; + ``` + +``` + + + + + +``` + ## Installation ### Composer