From d960fd60da47c4c9f051797a069fef2c59a60f48 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 30 Oct 2022 01:29:25 +0200 Subject: [PATCH] :sparkles: New `Universal.Classes.ModifierKeywordOrder` sniff Sniff to standardize the modifier keyword order for class declarations, what with the `readonly` keyword being introduced in PHP 8.2. The sniff contains a `public` `$order` property which allows for configuring the preferred order. Allowed values: * `'extendability readonly'` (= default) * `'readonly extendability'` Includes fixer. Includes unit tests. Includes documentation. Includes metrics. Ref: https://wiki.php.net/rfc/readonly_classes --- .../Classes/ModifierKeywordOrderStandard.xml | 27 +++ .../Classes/ModifierKeywordOrderSniff.php | 219 ++++++++++++++++++ .../Classes/ModifierKeywordOrderUnitTest.inc | 93 ++++++++ .../ModifierKeywordOrderUnitTest.inc.fixed | 90 +++++++ .../Classes/ModifierKeywordOrderUnitTest.php | 52 +++++ 5 files changed, 481 insertions(+) create mode 100644 Universal/Docs/Classes/ModifierKeywordOrderStandard.xml create mode 100644 Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php create mode 100644 Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc create mode 100644 Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc.fixed create mode 100644 Universal/Tests/Classes/ModifierKeywordOrderUnitTest.php diff --git a/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml b/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml new file mode 100644 index 00000000..a80ae0da --- /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 00000000..234515d8 --- /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 00000000..a78f9711 --- /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 []; + } +}