diff --git a/MO4/Sniffs/Formatting/AlphabeticalUseStatementsSniff.php b/MO4/Sniffs/Formatting/AlphabeticalUseStatementsSniff.php deleted file mode 100644 index 796a656..0000000 --- a/MO4/Sniffs/Formatting/AlphabeticalUseStatementsSniff.php +++ /dev/null @@ -1,373 +0,0 @@ - - * - * @license http://spdx.org/licenses/MIT MIT License - * - * @link https://github.com/mayflower/mo4-coding-standard - */ - -declare(strict_types=1); - -namespace MO4\Sniffs\Formatting; - -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; - -/** - * Alphabetical Use Statements sniff. - * - * Use statements must be in alphabetical order, grouped by empty lines. - * - * @author Xaver Loppenstedt - * @author Steffen Ritter - * @author Christian Albrecht - * - * @copyright 2013-2017 Xaver Loppenstedt, some rights reserved. - * - * @license http://spdx.org/licenses/MIT MIT License - * - * @link https://github.com/mayflower/mo4-coding-standard - * - * @psalm-api - */ -class AlphabeticalUseStatementsSniff extends UseDeclarationSniff -{ - private const NAMESPACE_SEPARATOR_STRING = '\\'; - - private const SUPPORTED_ORDERING_METHODS = [ - 'dictionary', - 'string', - 'string', - 'string-locale', - 'string-case-insensitive', - ]; - - /** - * Sorting order, see SUPPORTED_ORDERING_METHODS for possible settings - * - * Unknown types will be mapped to 'string'. - * - * @var string - */ - public $order = 'dictionary'; - - /** - * Last import seen in group - * - * @var string - */ - private $lastImport = ''; - - /** - * Line number of the last seen use statement - * - * @var int - */ - private $lastLine = -1; - - /** - * Current file - * - * @var string - */ - private $currentFile = ''; - - /** - * Processes this test, when one of its tokens is encountered. - * - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint - * - * @param File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in - * the stack passed in $tokens. - * - */ - public function process(File $phpcsFile, $stackPtr): void - { - if (!\in_array($this->order, self::SUPPORTED_ORDERING_METHODS, true)) { - $error = \sprintf( - "'%s' is not a valid order function for %s! Pick one of: %s", - $this->order, - Common::getSniffCode(self::class), - \implode(', ', self::SUPPORTED_ORDERING_METHODS) - ); - - $phpcsFile->addError($error, $stackPtr, 'InvalidOrder'); - - return; - } - - parent::process($phpcsFile, $stackPtr); - - if (true === $this->checkIsNonImportUse($phpcsFile, $stackPtr)) { - return; - } - - if ($this->currentFile !== $phpcsFile->getFilename()) { - $this->lastLine = -1; - $this->lastImport = ''; - $this->currentFile = $phpcsFile->getFilename(); - } - - $tokens = $phpcsFile->getTokens(); - $line = $tokens[$stackPtr]['line']; - - $currentImportArr = $this->getUseImport($phpcsFile, $stackPtr); - - if (false === $currentImportArr) { - return; - } - - $currentPtr = $currentImportArr['startPtr']; - $currentImport = $currentImportArr['content']; - - if (($this->lastLine + 1) < $line) { - $this->lastLine = $line; - $this->lastImport = $currentImport; - - return; - } - - $fixable = false; - - if ('' !== $this->lastImport - && $this->compareString($this->lastImport, $currentImport) > 0 - ) { - $msg = 'USE statements must be sorted alphabetically, order %s'; - $code = 'MustBeSortedAlphabetically'; - $fixable = $phpcsFile->addFixableError($msg, $currentPtr, $code, [$this->order]); - } - - if (true === $fixable) { - // Find the correct position in current use block. - $newDestinationPtr - = $this->findNewDestination($phpcsFile, $stackPtr, $currentImport); - - $currentUseStr = $this->getUseStatementAsString($phpcsFile, $stackPtr); - - $phpcsFile->fixer->beginChangeset(); - $phpcsFile->fixer->addContentBefore($newDestinationPtr, $currentUseStr); - $this->fixerClearLine($phpcsFile, $stackPtr); - $phpcsFile->fixer->endChangeset(); - } - - $this->lastImport = $currentImport; - $this->lastLine = $line; - } - - /** - * Get the import class name for use statement pointed by $stackPtr. - * - * @param File $phpcsFile PHP CS File - * @param int $stackPtr pointer - * - * @return array{startPtr: int, content: string}|false - */ - private function getUseImport(File $phpcsFile, int $stackPtr) - { - $importTokens = [ - T_NAME_FULLY_QUALIFIED, - T_NAME_QUALIFIED, - T_STRING, - ]; - - $start = $phpcsFile->findNext( - PHP_CodeSniffer_Tokens::EMPTY_TOKENS, - ($stackPtr + 1), - null, - true - ); - - // $start is false when "use" is the last token in file... - if (false === $start) { - return false; - } - - $end = (int) $phpcsFile->findNext($importTokens, $start, null, true); - $import = $phpcsFile->getTokensAsString($start, ($end - $start)); - - return [ - 'startPtr' => $start, - 'content' => $import, - ]; - } - - /** - * Get the full use statement as string, including trailing white space. - * - * @param File $phpcsFile PHP CS File - * @param int $stackPtr pointer - * - */ - private function getUseStatementAsString(File $phpcsFile, int $stackPtr): string - { - $tokens = $phpcsFile->getTokens(); - - $useEndPtr = (int) $phpcsFile->findNext([T_SEMICOLON], ($stackPtr + 2)); - $useLength = ($useEndPtr - $stackPtr + 1); - - if (T_WHITESPACE === $tokens[($useEndPtr + 1)]['code']) { - $useLength++; - } - - return $phpcsFile->getTokensAsString($stackPtr, $useLength); - } - - /** - * Check if "use" token is not used for import. - * E.g. function () use () {...}. - * - * @param File $phpcsFile PHP CS File - * @param int $stackPtr pointer - * - */ - private function checkIsNonImportUse(File $phpcsFile, int $stackPtr): bool - { - $tokens = $phpcsFile->getTokens(); - - // Ignore USE keywords for closures. - if (true === isset($tokens[$stackPtr]['parenthesis_owner'])) { - return true; - } - - // Ignore USE keywords for traits (inside class/trait/enum bodies). - if (true === $phpcsFile->hasCondition($stackPtr, [T_CLASS, T_TRAIT, T_ENUM])) { - return true; - } - - return false; - } - - /** - * Replace all the token in same line as the element pointed to by $stackPtr - * the by the empty string. - * This will delete the line. - * - * @param File $phpcsFile PHP CS file - * @param int $stackPtr pointer - * - */ - private function fixerClearLine(File $phpcsFile, int $stackPtr): void - { - $tokens = $phpcsFile->getTokens(); - $line = $tokens[$stackPtr]['line']; - - for ($i = ($stackPtr - 1); $tokens[$i]['line'] === $line; $i--) { - $phpcsFile->fixer->replaceToken($i, ''); - } - - for ($i = $stackPtr; $tokens[$i]['line'] === $line; $i++) { - $phpcsFile->fixer->replaceToken($i, ''); - } - } - - /** - * Find a new destination pointer for the given import string in current - * use block. - * - * @param File $phpcsFile PHP CS File - * @param int $stackPtr pointer - * @param string $import import string requiring new position - * - */ - private function findNewDestination(File $phpcsFile, int $stackPtr, string $import): int - { - $tokens = $phpcsFile->getTokens(); - - $line = $tokens[$stackPtr]['line']; - /** @var int|bool $prevLine */ - $prevLine = false; - $prevPtr = $stackPtr; - - do { - $ptr = $prevPtr; - - // Use $line for the first iteration. - if (false !== $prevLine) { - $line = $prevLine; - } - - $prevPtr = $phpcsFile->findPrevious(T_USE, ($ptr - 1)); - - if (false === $prevPtr) { - break; - } - - $prevLine = $tokens[$prevPtr]['line']; - // phpcs:disable - /** @var array $prevImportArr */ - $prevImportArr = $this->getUseImport($phpcsFile, $prevPtr); - // phpcs:enable - } while ($prevLine === ($line - 1) - && ($this->compareString($prevImportArr['content'], $import) > 0) - ); - - return $ptr; - } - - /** - * Compare namespace strings according defined order function. - * - * @param string $a first namespace string - * @param string $b second namespace string - * - */ - private function compareString(string $a, string $b): int - { - return match ($this->order) { - 'string' => \strcmp($a, $b), - 'string-locale' => \strcoll($a, $b), - 'string-case-insensitive' => \strcasecmp($a, $b), - // Default is 'dictionary'. - default => $this->dictionaryCompare($a, $b), - }; - } - - /** - * 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 - * - */ - private function dictionaryCompare(string $a, string $b): int - { - $min = \min(\strlen($a), \strlen($b)); - - for ($i = 0; $i < $min; $i++) { - if ($a[$i] === $b[$i]) { - continue; - } - - if (self::NAMESPACE_SEPARATOR_STRING === $a[$i]) { - return -1; - } - - if (self::NAMESPACE_SEPARATOR_STRING === $b[$i]) { - return 1; - } - - if ($a[$i] < $b[$i]) { - return -1; - } - - if ($a[$i] > $b[$i]) { - return 1; - } - } - - return \strcmp(\substr($a, $min), \substr($b, $min)); - } -} diff --git a/MO4/Tests/Commenting/PropertyCommentUnitTest.php b/MO4/Tests/Commenting/PropertyCommentUnitTest.php index 659a006..06f3c91 100644 --- a/MO4/Tests/Commenting/PropertyCommentUnitTest.php +++ b/MO4/Tests/Commenting/PropertyCommentUnitTest.php @@ -17,7 +17,7 @@ use MO4\Tests\AbstractMo4SniffUnitTest; /** - * Unit test class for the AlphabeticalUseStatements sniff. + * Unit test class for the PropertyComment sniff. * * A sniff unit test checks a .inc file for expected violations of a single * coding standard. Expected errors and warnings are stored in this class. diff --git a/MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.fail.1.inc b/MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.fail.1.inc deleted file mode 100644 index a0b2d92..0000000 --- a/MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.fail.1.inc +++ /dev/null @@ -1,16 +0,0 @@ - */ - use GenericTrait; - /** @use AnotherTrait */ - use AnotherTrait; -} - diff --git a/MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.pass.3.inc b/MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.pass.3.inc deleted file mode 100644 index 7a304d4..0000000 --- a/MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.pass.3.inc +++ /dev/null @@ -1,8 +0,0 @@ - - * - * @license http://spdx.org/licenses/MIT MIT License - * - * @link https://github.com/mayflower/mo4-coding-standard - */ - -declare(strict_types=1); - -namespace MO4\Tests\Formatting; - -use MO4\Tests\AbstractMo4SniffUnitTest; - -/** - * Unit test class for the AlphabeticalUseStatements sniff. - * - * A sniff unit test checks a .inc file for expected violations of a single - * coding standard. Expected errors and warnings are stored in this class. - * - * @author Xaver Loppenstedt - * - * @copyright 2013-2021 Xaver Loppenstedt, some rights reserved. - * - * @license http://spdx.org/licenses/MIT MIT License - * - * @link https://github.com/mayflower/mo4-coding-standard - */ -final class AlphabeticalUseStatementsUnitTest extends AbstractMo4SniffUnitTest -{ - protected $expectedErrorList = [ - 'AlphabeticalUseStatementsUnitTest.pass.inc' => [], - 'AlphabeticalUseStatementsUnitTest.pass.1.inc' => [], - 'AlphabeticalUseStatementsUnitTest.pass.2.inc' => [], - 'AlphabeticalUseStatementsUnitTest.pass.3.inc' => [], - 'AlphabeticalUseStatementsUnitTest.fail.1.inc' => [ - 4 => 1, - 5 => 1, - 8 => 1, - 9 => 1, - 12 => 1, - ], - // Take care, more than one fix will be applied. - 'AlphabeticalUseStatementsUnitTest.fail.2.inc' => [ - 6 => 1, - 8 => 1, - ], - 'AlphabeticalUseStatementsUnitTest.fail.3.inc' => [ - 7 => 1, - 8 => 1, - 10 => 1, - 15 => 1, - ], - 'AlphabeticalUseStatementsUnitTest.fail.4.inc' => [ - 4 => 1, - 8 => 1, - 13 => 1, - 17 => 1, - 20 => 1, - 21 => 1, - ], - 'AlphabeticalUseStatementsUnitTest.fail.6.inc' => [5 => 1], - ]; -} diff --git a/MO4/ruleset.xml b/MO4/ruleset.xml index 8b3fa1c..743f753 100644 --- a/MO4/ruleset.xml +++ b/MO4/ruleset.xml @@ -192,6 +192,12 @@ + + + + + + diff --git a/README.md b/README.md index 10b0737..f0a5dd2 100644 --- a/README.md +++ b/README.md @@ -29,47 +29,6 @@ The MO4 Coding Standard is an extension of the [Symfony Coding Standard](http:// ### MO4.Commenting.PropertyComment * doc blocks of class properties must be multiline and have exactly one `@var` annotation -### MO4.Formatting.AlphabeticalUseStatements -* `use` statements must be sorted lexicographically. The order function can be configured. - -#### Configuration -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-insensitive`: binary safe case-insensitive string comparison [strcasecmp](http://php.net/strcasecmp) - ```php - use Examples; - use ExampleSub; - ``` - -To change the sorting order for your project, add this snippet to your custom `ruleset.xml`: - -```xml - - - - - -``` - ### MO4.Formatting.UnnecessaryNamespaceUsage * The imported class name must be used, when it was imported with a `use` statement.