diff --git a/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml b/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml new file mode 100644 index 0000000..a80ae0d --- /dev/null +++ b/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml @@ -0,0 +1,27 @@ + + + + + + + + final readonly class Foo {} +abstract readonly class Bar {} + ]]> + + + readonly final class Foo {} +readonly abstract class Bar {} + ]]> + + + diff --git a/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php b/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php new file mode 100644 index 0000000..234515d --- /dev/null +++ b/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php @@ -0,0 +1,219 @@ +getTokens(); + $valid = Collections::classModifierKeywords() + Tokens::$emptyTokens; + $classProp = [ + 'abstract_token' => false, + 'final_token' => false, + 'readonly_token' => false, + ]; + + for ($i = ($stackPtr - 1); $i > 0; $i--) { + if (isset($valid[$tokens[$i]['code']]) === false) { + break; + } + + switch ($tokens[$i]['code']) { + case \T_ABSTRACT: + $classProp['abstract_token'] = $i; + break; + + case \T_FINAL: + $classProp['final_token'] = $i; + break; + + case \T_READONLY: + $classProp['readonly_token'] = $i; + break; + } + } + + if ($classProp['readonly_token'] === false + || ($classProp['final_token'] === false && $classProp['abstract_token'] === false) + ) { + /* + * Either no modifier keywords found at all; or only one type of modifier + * keyword (abstract/final or readonly) declared, but not both. No ordering needed. + */ + return; + } + + if ($classProp['final_token'] !== false && $classProp['abstract_token'] !== false) { + // Parse error. Ignore. + return; + } + + $readonly = $classProp['readonly_token']; + + if ($classProp['final_token'] !== false) { + $extendability = $classProp['final_token']; + } else { + $extendability = $classProp['abstract_token']; + } + + if ($readonly < $extendability) { + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, self::READONLY_EXTEND); + } else { + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, self::EXTEND_READONLY); + } + + $message = 'Class modifier keywords are not in the correct order. Expected: "%s", found: "%s"'; + + switch ($this->order) { + case self::READONLY_EXTEND: + if ($readonly < $extendability) { + // Order is correct. Nothing to do. + return; + } + + $this->handleError($phpcsFile, $extendability, $readonly); + break; + + case self::EXTEND_READONLY: + default: + if ($extendability < $readonly) { + // Order is correct. Nothing to do. + return; + } + + $this->handleError($phpcsFile, $readonly, $extendability); + break; + } + } + + /** + * Throw the error and potentially fix it. + * + * @since 1.0.0-alpha4 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $firstKeyword The position of the first keyword found. + * @param int $secondKeyword The position of the second keyword token. + * + * @return void + */ + private function handleError(File $phpcsFile, $firstKeyword, $secondKeyword) + { + $tokens = $phpcsFile->getTokens(); + + $message = 'Class modifier keywords are not in the correct order. Expected: "%s", found: "%s"'; + $data = [ + $tokens[$secondKeyword]['content'] . ' ' . $tokens[$firstKeyword]['content'], + $tokens[$firstKeyword]['content'] . ' ' . $tokens[$secondKeyword]['content'], + ]; + + $fix = $phpcsFile->addFixableError($message, $firstKeyword, 'Incorrect', $data); + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + $phpcsFile->fixer->replaceToken($secondKeyword, ''); + + // Prevent leaving behind trailing whitespace. + $i = ($secondKeyword + 1); + while ($tokens[$i]['code'] === \T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($i, ''); + $i++; + } + + // Use the original token content as the case used for keywords is not the concern of this sniff. + $phpcsFile->fixer->addContentBefore($firstKeyword, $tokens[$secondKeyword]['content'] . ' '); + + $phpcsFile->fixer->endChangeset(); + } + } +} diff --git a/Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc b/Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc new file mode 100644 index 0000000..a78f971 --- /dev/null +++ b/Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc @@ -0,0 +1,93 @@ + => + */ + public function getErrorList() + { + return [ + 44 => 1, + 46 => 1, + 48 => 1, + 56 => 1, + 71 => 1, + 73 => 1, + 86 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +}