diff --git a/docs/rules.md b/docs/rules.md index ea9871c1..6531cce1 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -77,6 +77,10 @@ Ensures that strings use single quotes when possible. Options are: - `skipStringContainingSingleQuote`: ignore double-quoted strings that contains single-quotes (default true). +- **StrictComparisonOperatorRule**: + + Ensures that strict comparison operators `===` and `!==` are used instead of `same as` and `not same as`. + - **TrailingCommaMultiLineRule** (Configurable): Ensures that multi-line arrays, objects and argument lists have a trailing comma. Options are: @@ -193,6 +197,7 @@ new TwigCsFixer\Rules\Whitespace\IndentRule(3); - IncludeFunctionRule - IndentRule - SingleQuoteRule +- StrictComparisonOperatorRule - TrailingCommaMultiLineRule - TrailingCommaSingleLineRule - TrailingSpaceRule diff --git a/src/Rules/Operator/StrictComparisonOperatorRule.php b/src/Rules/Operator/StrictComparisonOperatorRule.php new file mode 100644 index 00000000..8042aa48 --- /dev/null +++ b/src/Rules/Operator/StrictComparisonOperatorRule.php @@ -0,0 +1,108 @@ +get($tokenIndex); + + if (!$token->isMatching(Token::OPERATOR_TYPE, ['is', 'is not'])) { + return; + } + + $isNegated = $token->isMatching(Token::OPERATOR_TYPE, 'is not'); + + $sameAsIndex = $tokens->findNext(Token::WHITESPACE_TOKENS, $tokenIndex + 1, exclude: true); + if (false === $sameAsIndex) { + return; + } + + $sameAsToken = $tokens->get($sameAsIndex); + $targetIndex = $sameAsIndex; + + if (!$sameAsToken->isMatching(Token::TEST_NAME_TYPE, 'same')) { + return; + } + + $asIndex = $tokens->findNext(Token::WHITESPACE_TOKENS, $targetIndex + 1, exclude: true); + if (false === $asIndex || !$tokens->get($asIndex)->isMatching(Token::TEST_NAME_TYPE, 'as')) { + return; + } + + $openParenthesisIndex = $tokens->findNext(Token::WHITESPACE_TOKENS, $asIndex + 1, exclude: true); + if (false === $openParenthesisIndex || !$tokens->get($openParenthesisIndex)->isMatching(Token::PUNCTUATION_TYPE, '(')) { + return; + } + + $closeParenthesisIndex = $this->findMatchingParenthesis($tokens, $openParenthesisIndex); + if (false === $closeParenthesisIndex) { + return; + } + + $fixer = $this->addFixableError( + 'Use strict comparison operators === / !== instead of same as / not same as.', + $token + ); + + if (null === $fixer) { + return; + } + + $fixer->beginChangeSet(); + + $replacement = $isNegated ? '!==' : '==='; + + $fixer->replaceToken($tokenIndex, $replacement); + + for ($i = $tokenIndex + 1; $i <= $asIndex; ++$i) { + $fixer->replaceToken($i, ''); + } + + // We want only one whitespace between the operator and the parenthesis content. + // If there is already a whitespace between "as" and "(", we can remove the "(". + // Otherwise, we replace "(" by a whitespace. + $hasWhitespaceBeforeParen = $tokens->get($openParenthesisIndex - 1)->isMatching(Token::WHITESPACE_TOKENS); + $fixer->replaceToken($openParenthesisIndex, $hasWhitespaceBeforeParen ? '' : ' '); + $fixer->replaceToken($closeParenthesisIndex, ''); + + $fixer->endChangeSet(); + } + + private function findMatchingParenthesis(Tokens $tokens, int $openParenthesisIndex): int|false + { + $level = 0; + $tokensArray = $tokens->toArray(); + $count = \count($tokensArray); + + for ($i = $openParenthesisIndex; $i < $count; ++$i) { + $token = $tokensArray[$i]; + + if ($token->isMatching(Token::PUNCTUATION_TYPE, '(')) { + ++$level; + + continue; + } + + if ($token->isMatching(Token::PUNCTUATION_TYPE, ')')) { + --$level; + + if (0 === $level) { + return $i; + } + } + } + + return false; + } +} diff --git a/src/Standard/TwigCsFixer.php b/src/Standard/TwigCsFixer.php index 1fcec1bf..19eda5d2 100644 --- a/src/Standard/TwigCsFixer.php +++ b/src/Standard/TwigCsFixer.php @@ -9,6 +9,7 @@ use TwigCsFixer\Rules\Literal\CompactHashRule; use TwigCsFixer\Rules\Literal\HashQuoteRule; use TwigCsFixer\Rules\Literal\SingleQuoteRule; +use TwigCsFixer\Rules\Operator\StrictComparisonOperatorRule; use TwigCsFixer\Rules\Punctuation\TrailingCommaMultiLineRule; use TwigCsFixer\Rules\Punctuation\TrailingCommaSingleLineRule; use TwigCsFixer\Rules\Whitespace\BlankEOFRule; @@ -33,6 +34,7 @@ public function getRules(): array new IncludeFunctionRule(), new IndentRule(), new SingleQuoteRule(), + new StrictComparisonOperatorRule(), new TrailingCommaMultiLineRule(), new TrailingCommaSingleLineRule(), new TrailingSpaceRule(), diff --git a/tests/Rules/Operator/StrictComparisonOperator/StrictComparisonOperatorRuleTest.fixed.twig b/tests/Rules/Operator/StrictComparisonOperator/StrictComparisonOperatorRuleTest.fixed.twig new file mode 100644 index 00000000..093abb8b --- /dev/null +++ b/tests/Rules/Operator/StrictComparisonOperator/StrictComparisonOperatorRuleTest.fixed.twig @@ -0,0 +1,6 @@ +{{ a === b }} +{{ a !== b }} +{{ a === b }} +{{ a !== b }} +{{ a === b }} +{{ a !== b }} diff --git a/tests/Rules/Operator/StrictComparisonOperator/StrictComparisonOperatorRuleTest.php b/tests/Rules/Operator/StrictComparisonOperator/StrictComparisonOperatorRuleTest.php new file mode 100644 index 00000000..f42c8b4f --- /dev/null +++ b/tests/Rules/Operator/StrictComparisonOperator/StrictComparisonOperatorRuleTest.php @@ -0,0 +1,21 @@ +checkRule(new StrictComparisonOperatorRule(), [ + 'StrictComparisonOperator.Error:1:6' => 'Use strict comparison operators === / !== instead of same as / not same as.', + 'StrictComparisonOperator.Error:2:6' => 'Use strict comparison operators === / !== instead of same as / not same as.', + 'StrictComparisonOperator.Error:3:6' => 'Use strict comparison operators === / !== instead of same as / not same as.', + 'StrictComparisonOperator.Error:4:6' => 'Use strict comparison operators === / !== instead of same as / not same as.', + ]); + } +} diff --git a/tests/Rules/Operator/StrictComparisonOperator/StrictComparisonOperatorRuleTest.twig b/tests/Rules/Operator/StrictComparisonOperator/StrictComparisonOperatorRuleTest.twig new file mode 100644 index 00000000..7b2c885a --- /dev/null +++ b/tests/Rules/Operator/StrictComparisonOperator/StrictComparisonOperatorRuleTest.twig @@ -0,0 +1,6 @@ +{{ a is same as(b) }} +{{ a is not same as(b) }} +{{ a is same as (b) }} +{{ a is not same as (b) }} +{{ a === b }} +{{ a !== b }} diff --git a/tests/Standard/TwigCsFixerTest.php b/tests/Standard/TwigCsFixerTest.php index 44102b82..a4a7b1be 100644 --- a/tests/Standard/TwigCsFixerTest.php +++ b/tests/Standard/TwigCsFixerTest.php @@ -17,6 +17,7 @@ use TwigCsFixer\Rules\Literal\SingleQuoteRule; use TwigCsFixer\Rules\Operator\OperatorNameSpacingRule; use TwigCsFixer\Rules\Operator\OperatorSpacingRule; +use TwigCsFixer\Rules\Operator\StrictComparisonOperatorRule; use TwigCsFixer\Rules\Punctuation\PunctuationSpacingRule; use TwigCsFixer\Rules\Punctuation\TrailingCommaMultiLineRule; use TwigCsFixer\Rules\Punctuation\TrailingCommaSingleLineRule; @@ -51,6 +52,7 @@ public function testGetRules(): void new IncludeFunctionRule(), new IndentRule(), new SingleQuoteRule(), + new StrictComparisonOperatorRule(), new TrailingCommaMultiLineRule(), new TrailingCommaSingleLineRule(), new TrailingSpaceRule(),