diff --git a/TwigCS/Report/Report.php b/TwigCS/Report/Report.php index 6b74751..ba7a4da 100644 --- a/TwigCS/Report/Report.php +++ b/TwigCS/Report/Report.php @@ -10,6 +10,7 @@ class Report const MESSAGE_TYPE_NOTICE = 0; const MESSAGE_TYPE_WARNING = 1; const MESSAGE_TYPE_ERROR = 2; + const MESSAGE_TYPE_FATAL = 3; /** * @var SniffViolation[] @@ -61,6 +62,7 @@ public function addMessage(SniffViolation $sniffViolation) ++$this->totalWarnings; break; case self::MESSAGE_TYPE_ERROR: + case self::MESSAGE_TYPE_FATAL: ++$this->totalErrors; break; } @@ -77,7 +79,7 @@ public function addMessage(SniffViolation $sniffViolation) */ public function getMessages(array $filters = []) { - if (empty($filters)) { + if (0 === count($filters)) { // Return all messages, without filtering. return $this->messages; } diff --git a/TwigCS/Report/SniffViolation.php b/TwigCS/Report/SniffViolation.php index 070f9a9..86ff9d3 100644 --- a/TwigCS/Report/SniffViolation.php +++ b/TwigCS/Report/SniffViolation.php @@ -2,6 +2,7 @@ namespace TwigCS\Report; +use \LogicException; use TwigCS\Sniff\SniffInterface; /** @@ -9,6 +10,11 @@ */ class SniffViolation { + const LEVEL_NOTICE = 'NOTICE'; + const LEVEL_WARNING = 'WARNING'; + const LEVEL_ERROR = 'ERROR'; + const LEVEL_FATAL = 'FATAL'; + /** * Level of the message among `notice`, `warning`, `error` * @@ -86,13 +92,16 @@ public function getLevel() public function getLevelAsString() { switch ($this->level) { - case SniffInterface::MESSAGE_TYPE_NOTICE: - return 'NOTICE'; - case SniffInterface::MESSAGE_TYPE_WARNING: - return 'WARNING'; - case SniffInterface::MESSAGE_TYPE_ERROR: + case Report::MESSAGE_TYPE_NOTICE: + return self::LEVEL_NOTICE; + case Report::MESSAGE_TYPE_WARNING: + return self::LEVEL_WARNING; + case Report::MESSAGE_TYPE_ERROR: + return self::LEVEL_ERROR; + case Report::MESSAGE_TYPE_FATAL: + return self::LEVEL_FATAL; default: - return 'ERROR'; + throw new LogicException(); } } @@ -106,13 +115,16 @@ public function getLevelAsString() public static function getLevelAsInt(string $level) { switch (strtoupper($level)) { - case 'NOTICE': - return SniffInterface::MESSAGE_TYPE_NOTICE; - case 'WARNING': - return SniffInterface::MESSAGE_TYPE_WARNING; - case 'ERROR': + case self::LEVEL_NOTICE: + return Report::MESSAGE_TYPE_NOTICE; + case self::LEVEL_WARNING: + return Report::MESSAGE_TYPE_WARNING; + case self::LEVEL_ERROR: + return Report::MESSAGE_TYPE_ERROR; + case self::LEVEL_FATAL: + return Report::MESSAGE_TYPE_FATAL; default: - return SniffInterface::MESSAGE_TYPE_ERROR; + throw new LogicException(); } } diff --git a/TwigCS/Ruleset/Generic/BlankEOFSniff.php b/TwigCS/Ruleset/Generic/BlankEOFSniff.php index c8ceef1..0088770 100644 --- a/TwigCS/Ruleset/Generic/BlankEOFSniff.php +++ b/TwigCS/Ruleset/Generic/BlankEOFSniff.php @@ -33,8 +33,7 @@ public function process(int $tokenPosition, array $tokens) if (1 !== $i) { // Either 0 or 2+ blank lines. - $fix = $this->addFixableMessage( - $this::MESSAGE_TYPE_ERROR, + $fix = $this->addFixableError( sprintf('A file must end with 1 blank line; found %d', $i), $token ); diff --git a/TwigCS/Ruleset/Generic/DelimiterSpacingSniff.php b/TwigCS/Ruleset/Generic/DelimiterSpacingSniff.php index 40c9d47..422c653 100644 --- a/TwigCS/Ruleset/Generic/DelimiterSpacingSniff.php +++ b/TwigCS/Ruleset/Generic/DelimiterSpacingSniff.php @@ -2,119 +2,35 @@ namespace TwigCS\Ruleset\Generic; -use \Exception; -use TwigCS\Sniff\AbstractSniff; +use TwigCS\Sniff\AbstractSpacingSniff; use TwigCS\Token\Token; /** - * Ensure there is one space before and after a delimiter {{, {%, {#, }}, %} and #} + * Ensure there is one space before {{, {%, {#, and after }}, %} and #} */ -class DelimiterSpacingSniff extends AbstractSniff +class DelimiterSpacingSniff extends AbstractSpacingSniff { /** - * @param int $tokenPosition - * @param Token[] $tokens + * @param Token $token * - * @return Token - * - * @throws Exception + * @return bool */ - public function process(int $tokenPosition, array $tokens) + protected function shouldHaveSpaceBefore(Token $token) { - $token = $tokens[$tokenPosition]; - - if ($this->isTokenMatching($token, Token::VAR_START_TYPE) - || $this->isTokenMatching($token, Token::BLOCK_START_TYPE) - || $this->isTokenMatching($token, Token::COMMENT_START_TYPE) - ) { - $this->processStart($tokenPosition, $tokens); - } - - if ($this->isTokenMatching($token, Token::VAR_END_TYPE) + return $this->isTokenMatching($token, Token::VAR_END_TYPE) || $this->isTokenMatching($token, Token::BLOCK_END_TYPE) - || $this->isTokenMatching($token, Token::COMMENT_END_TYPE) - ) { - $this->processEnd($tokenPosition, $tokens); - } - - return $token; - } - - /** - * @param int $tokenPosition - * @param Token[] $tokens - * - * @throws Exception - */ - public function processStart(int $tokenPosition, array $tokens) - { - $token = $tokens[$tokenPosition]; - - // Ignore new line - $next = $this->findNext(Token::WHITESPACE_TYPE, $tokens, $tokenPosition + 1, true); - if ($this->isTokenMatching($tokens[$next], Token::EOL_TYPE)) { - return; - } - - if ($this->isTokenMatching($tokens[$tokenPosition + 1], Token::WHITESPACE_TYPE)) { - $count = strlen($tokens[$tokenPosition + 1]->getValue()); - } else { - $count = 0; - } - - if (1 !== $count) { - $fix = $this->addFixableMessage( - $this::MESSAGE_TYPE_ERROR, - sprintf('Expecting 1 whitespace after "%s"; found %d', $token->getValue(), $count), - $token - ); - - if ($fix) { - if (0 === $count) { - $this->fixer->addContent($tokenPosition, ' '); - } else { - $this->fixer->replaceToken($tokenPosition + 1, ' '); - } - } - } + || $this->isTokenMatching($token, Token::COMMENT_END_TYPE); } /** - * @param int $tokenPosition - * @param Token[] $tokens + * @param Token $token * - * @throws Exception + * @return bool */ - public function processEnd(int $tokenPosition, array $tokens) + protected function shouldHaveSpaceAfter(Token $token) { - $token = $tokens[$tokenPosition]; - - // Ignore new line - $previous = $this->findPrevious(Token::WHITESPACE_TYPE, $tokens, $tokenPosition - 1, true); - if ($this->isTokenMatching($tokens[$previous], Token::EOL_TYPE)) { - return; - } - - if ($this->isTokenMatching($tokens[$tokenPosition - 1], Token::WHITESPACE_TYPE)) { - $count = strlen($tokens[$tokenPosition - 1]->getValue()); - } else { - $count = 0; - } - - if (1 !== $count) { - $fix = $this->addFixableMessage( - $this::MESSAGE_TYPE_ERROR, - sprintf('Expecting 1 whitespace before "%s"; found %d', $token->getValue(), $count), - $token - ); - - if ($fix) { - if (0 === $count) { - $this->fixer->addContentBefore($tokenPosition, ' '); - } else { - $this->fixer->replaceToken($tokenPosition - 1, ' '); - } - } - } + return $this->isTokenMatching($token, Token::VAR_START_TYPE) + || $this->isTokenMatching($token, Token::BLOCK_START_TYPE) + || $this->isTokenMatching($token, Token::COMMENT_START_TYPE); } } diff --git a/TwigCS/Ruleset/Generic/DisallowCommentedCodeSniff.php b/TwigCS/Ruleset/Generic/DisallowCommentedCodeSniff.php index 90f9a66..0cdef56 100644 --- a/TwigCS/Ruleset/Generic/DisallowCommentedCodeSniff.php +++ b/TwigCS/Ruleset/Generic/DisallowCommentedCodeSniff.php @@ -43,8 +43,7 @@ public function process(int $tokenPosition, array $tokens) } if ($found) { - $this->addMessage( - $this::MESSAGE_TYPE_WARNING, + $this->addWarning( 'Probable commented code found; keeping commented code is not advised', $token ); diff --git a/TwigCS/Ruleset/Generic/EmptyLinesSniff.php b/TwigCS/Ruleset/Generic/EmptyLinesSniff.php index 7606626..0b91a8b 100644 --- a/TwigCS/Ruleset/Generic/EmptyLinesSniff.php +++ b/TwigCS/Ruleset/Generic/EmptyLinesSniff.php @@ -32,8 +32,7 @@ public function process(int $tokenPosition, array $tokens) } if (1 < $i) { - $fix = $this->addFixableMessage( - $this::MESSAGE_TYPE_ERROR, + $fix = $this->addFixableError( sprintf('More than 1 empty lines are not allowed, found %d', $i), $token ); diff --git a/TwigCS/Ruleset/Generic/OperatorSpacingSniff.php b/TwigCS/Ruleset/Generic/OperatorSpacingSniff.php new file mode 100644 index 0000000..4525845 --- /dev/null +++ b/TwigCS/Ruleset/Generic/OperatorSpacingSniff.php @@ -0,0 +1,32 @@ +isTokenMatching($token, Token::OPERATOR_TYPE); + } + + /** + * @param Token $token + * + * @return bool + */ + protected function shouldHaveSpaceAfter(Token $token) + { + return $this->isTokenMatching($token, Token::OPERATOR_TYPE); + } +} diff --git a/TwigCS/Runner/Fixer.php b/TwigCS/Runner/Fixer.php index d0faaa2..0207bcd 100644 --- a/TwigCS/Runner/Fixer.php +++ b/TwigCS/Runner/Fixer.php @@ -327,7 +327,7 @@ public function rollbackChangeset() $this->inChangeset = false; $this->inConflict = false; - if (empty($this->changeset) === false) { + if (count($this->changeset) > 0) { $this->changeset = []; } } diff --git a/TwigCS/Runner/Linter.php b/TwigCS/Runner/Linter.php index 9ee3cca..62f18fa 100644 --- a/TwigCS/Runner/Linter.php +++ b/TwigCS/Runner/Linter.php @@ -126,7 +126,7 @@ public function processTemplate(string $file, Ruleset $ruleset, Report $report) $this->env->parse($this->env->tokenize($twigSource)); } catch (Error $e) { $sniffViolation = new SniffViolation( - SniffInterface::MESSAGE_TYPE_ERROR, + Report::MESSAGE_TYPE_FATAL, $e->getRawMessage(), $e->getSourceContext()->getName(), $e->getTemplateLine() @@ -142,8 +142,8 @@ public function processTemplate(string $file, Ruleset $ruleset, Report $report) $stream = $this->tokenizer->tokenize($twigSource); } catch (Exception $e) { $sniffViolation = new SniffViolation( - SniffInterface::MESSAGE_TYPE_ERROR, - sprintf('Unable to tokenize file'), + Report::MESSAGE_TYPE_FATAL, + sprintf('Unable to tokenize file: %s', $e->getMessage()), $file ); @@ -170,7 +170,7 @@ protected function setErrorHandler(Report $report, string $file = null) set_error_handler(function ($type, $message) use ($report, $file) { if (E_USER_DEPRECATED === $type) { $sniffViolation = new SniffViolation( - SniffInterface::MESSAGE_TYPE_NOTICE, + Report::MESSAGE_TYPE_NOTICE, $message, $file ); diff --git a/TwigCS/Sniff/AbstractSniff.php b/TwigCS/Sniff/AbstractSniff.php index 3045b2f..e67cf0e 100644 --- a/TwigCS/Sniff/AbstractSniff.php +++ b/TwigCS/Sniff/AbstractSniff.php @@ -100,38 +100,28 @@ public function findPrevious(int $type, array $tokens, int $start, bool $exclude } /** - * Adds a violation to the current report for the given token. - * - * @param int $messageType * @param string $message * @param Token $token * * @throws Exception */ - public function addMessage(int $messageType, string $message, Token $token) + public function addWarning(string $message, Token $token) { - if (null === $this->report) { - if (null !== $this->fixer) { - // We are fixing the file, ignore this - return; - } - - throw new Exception('Sniff is disabled!'); - } - - $sniffViolation = new SniffViolation( - $messageType, - $message, - $token->getFilename(), - $token->getLine() - ); - $sniffViolation->setLinePosition($token->getPosition()); + $this->addMessage(Report::MESSAGE_TYPE_WARNING, $message, $token); + } - $this->report->addMessage($sniffViolation); + /** + * @param string $message + * @param Token $token + * + * @throws Exception + */ + public function addError(string $message, Token $token) + { + $this->addMessage(Report::MESSAGE_TYPE_ERROR, $message, $token); } /** - * @param int $messageType * @param string $message * @param Token $token * @@ -139,11 +129,22 @@ public function addMessage(int $messageType, string $message, Token $token) * * @throws Exception */ - public function addFixableMessage(int $messageType, string $message, Token $token) + public function addFixableWarning(string $message, Token $token) { - $this->addMessage($messageType, $message, $token); + return $this->addFixableMessage(Report::MESSAGE_TYPE_WARNING, $message, $token); + } - return null !== $this->fixer; + /** + * @param string $message + * @param Token $token + * + * @return bool + * + * @throws Exception + */ + public function addFixableError(string $message, Token $token) + { + return $this->addFixableMessage(Report::MESSAGE_TYPE_ERROR, $message, $token); } /** @@ -175,4 +176,49 @@ public function processFile(array $stream) * @param Token[] $stream */ abstract protected function process(int $tokenPosition, array $stream); + + /** + * @param int $messageType + * @param string $message + * @param Token $token + * + * @throws Exception + */ + private function addMessage(int $messageType, string $message, Token $token) + { + if (null === $this->report) { + if (null !== $this->fixer) { + // We are fixing the file, ignore this + return; + } + + throw new Exception('Sniff is disabled!'); + } + + $sniffViolation = new SniffViolation( + $messageType, + $message, + $token->getFilename(), + $token->getLine() + ); + $sniffViolation->setLinePosition($token->getPosition()); + + $this->report->addMessage($sniffViolation); + } + + /** + * @param int $messageType + * @param string $message + * @param Token $token + * + * @return bool + * + * @throws Exception + */ + private function addFixableMessage(int $messageType, string $message, Token $token) + { + $this->addMessage($messageType, $message, $token); + + return null !== $this->fixer; + } } diff --git a/TwigCS/Sniff/AbstractSpacingSniff.php b/TwigCS/Sniff/AbstractSpacingSniff.php new file mode 100644 index 0000000..52e408f --- /dev/null +++ b/TwigCS/Sniff/AbstractSpacingSniff.php @@ -0,0 +1,125 @@ +shouldHaveSpaceAfter($token)) { + $this->checkSpaceAfter($tokenPosition, $tokens); + } + + if ($this->shouldHaveSpaceBefore($token)) { + $this->checkSpaceBefore($tokenPosition, $tokens); + } + + return $token; + } + + /** + * @param Token $token + * + * @return bool + */ + abstract protected function shouldHaveSpaceAfter(Token $token); + + /** + * @param Token $token + * + * @return bool + */ + abstract protected function shouldHaveSpaceBefore(Token $token); + + /** + * @param int $tokenPosition + * @param Token[] $tokens + * + * @throws Exception + */ + protected function checkSpaceAfter(int $tokenPosition, array $tokens) + { + $token = $tokens[$tokenPosition]; + + // Ignore new line + $next = $this->findNext(Token::WHITESPACE_TYPE, $tokens, $tokenPosition + 1, true); + if ($this->isTokenMatching($tokens[$next], Token::EOL_TYPE)) { + return; + } + + if ($this->isTokenMatching($tokens[$tokenPosition + 1], Token::WHITESPACE_TYPE)) { + $count = strlen($tokens[$tokenPosition + 1]->getValue()); + } else { + $count = 0; + } + + if (1 !== $count) { + $fix = $this->addFixableError( + sprintf('Expecting 1 whitespace after "%s"; found %d', $token->getValue(), $count), + $token + ); + + if ($fix) { + if (0 === $count) { + $this->fixer->addContent($tokenPosition, ' '); + } else { + $this->fixer->replaceToken($tokenPosition + 1, ' '); + } + } + } + } + + /** + * @param int $tokenPosition + * @param Token[] $tokens + * + * @throws Exception + */ + protected function checkSpaceBefore(int $tokenPosition, array $tokens) + { + $token = $tokens[$tokenPosition]; + + // Ignore new line + $previous = $this->findPrevious(Token::WHITESPACE_TYPE, $tokens, $tokenPosition - 1, true); + if ($this->isTokenMatching($tokens[$previous], Token::EOL_TYPE)) { + return; + } + + if ($this->isTokenMatching($tokens[$tokenPosition - 1], Token::WHITESPACE_TYPE)) { + $count = strlen($tokens[$tokenPosition - 1]->getValue()); + } else { + $count = 0; + } + + if (1 !== $count) { + $fix = $this->addFixableError( + sprintf('Expecting 1 whitespace before "%s"; found %d', $token->getValue(), $count), + $token + ); + + if ($fix) { + if (0 === $count) { + $this->fixer->addContentBefore($tokenPosition, ' '); + } else { + $this->fixer->replaceToken($tokenPosition - 1, ' '); + } + } + } + } +} diff --git a/TwigCS/Sniff/SniffInterface.php b/TwigCS/Sniff/SniffInterface.php index 4c08263..0b44207 100644 --- a/TwigCS/Sniff/SniffInterface.php +++ b/TwigCS/Sniff/SniffInterface.php @@ -11,10 +11,6 @@ */ interface SniffInterface { - const MESSAGE_TYPE_NOTICE = 0; - const MESSAGE_TYPE_WARNING = 1; - const MESSAGE_TYPE_ERROR = 2; - /** * Enable the sniff report. * diff --git a/TwigCS/Tests/AbstractSniffTest.php b/TwigCS/Tests/AbstractSniffTest.php index 5a52b15..8158ce6 100644 --- a/TwigCS/Tests/AbstractSniffTest.php +++ b/TwigCS/Tests/AbstractSniffTest.php @@ -6,7 +6,7 @@ use \ReflectionClass; use PHPUnit\Framework\TestCase; use TwigCS\Environment\StubbedEnvironment; -use TwigCS\Report\SniffViolation; +use TwigCS\Report\Report; use TwigCS\Ruleset\Ruleset; use TwigCS\Runner\Fixer; use TwigCS\Runner\Linter; @@ -47,17 +47,23 @@ protected function checkGenericSniff(SniffInterface $sniff, array $expects) return; } - $this->assertEquals(count($expects), $report->getTotalWarnings() + $report->getTotalErrors()); - if ($expects) { - $messagePositions = array_map(function (SniffViolation $message) { - return [ - $message->getLine(), - $message->getLinePosition(), - ]; - }, $report->getMessages()); + $messages = $report->getMessages(); + $messagePositions = []; - $this->assertEquals($expects, $messagePositions); + foreach ($messages as $message) { + if (Report::MESSAGE_TYPE_FATAL === $message->getLevel()) { + $errorMessage = $message->getMessage(); + $line = $message->getLine(); + + if (null !== $line) { + $errorMessage = sprintf('Line %s: %s', $line, $errorMessage); + } + $this->fail($errorMessage); + } + + $messagePositions[] = [$message->getLine() => $message->getLinePosition()]; } + $this->assertEquals($expects, $messagePositions); $fixedFile = __DIR__.'/Fixtures/'.$className.'.fixed.twig'; if (file_exists($fixedFile)) { diff --git a/TwigCS/Tests/Fixtures/OperatorSpacingTest.fixed.twig b/TwigCS/Tests/Fixtures/OperatorSpacingTest.fixed.twig new file mode 100644 index 0000000..f76f2e7 --- /dev/null +++ b/TwigCS/Tests/Fixtures/OperatorSpacingTest.fixed.twig @@ -0,0 +1,20 @@ +{{ 1 + 2 }} +{{ 1 - 2 }} +{{ 1 / 2 }} +{{ 1 * 2 }} +{{ 1 % 2 }} +{{ 1 // 2 }} +{{ 1 ** 2 }} +{{ foo ~ bar }} +{{ true ? true : false }} +{{ 1 == 2 }} +{{ not true }} +{{ false and true }} +{{ false or true }} +{{ a in array }} +{{ a is array }} + +Untouch +-/*%==: + +{{ [1, 2, 3] }} +{{ {'foo': 'bar'} }} diff --git a/TwigCS/Tests/Fixtures/OperatorSpacingTest.twig b/TwigCS/Tests/Fixtures/OperatorSpacingTest.twig new file mode 100644 index 0000000..8bb3808 --- /dev/null +++ b/TwigCS/Tests/Fixtures/OperatorSpacingTest.twig @@ -0,0 +1,20 @@ +{{ 1+2 }} +{{ 1-2 }} +{{ 1/2 }} +{{ 1*2 }} +{{ 1%2 }} +{{ 1//2 }} +{{ 1**2 }} +{{ foo~bar }} +{{ true ? true : false }} +{{ 1==2 }} +{{ not true }} +{{ false and true }} +{{ false or true }} +{{ a in array }} +{{ a is array }} + +Untouch +-/*%==: + +{{ [1, 2, 3] }} +{{ {'foo': 'bar'} }} diff --git a/TwigCS/Tests/Ruleset/Generic/BlankEOFTest.php b/TwigCS/Tests/Ruleset/Generic/BlankEOFTest.php index 0599d12..2c9d1eb 100644 --- a/TwigCS/Tests/Ruleset/Generic/BlankEOFTest.php +++ b/TwigCS/Tests/Ruleset/Generic/BlankEOFTest.php @@ -13,7 +13,7 @@ class BlankEOFTest extends AbstractSniffTest public function testSniff() { $this->checkGenericSniff(new BlankEOFSniff(), [ - [4, 1], + [4 => 1], ]); } } diff --git a/TwigCS/Tests/Ruleset/Generic/DelimiterSpacingTest.php b/TwigCS/Tests/Ruleset/Generic/DelimiterSpacingTest.php index 43a16fc..ddbf71a 100644 --- a/TwigCS/Tests/Ruleset/Generic/DelimiterSpacingTest.php +++ b/TwigCS/Tests/Ruleset/Generic/DelimiterSpacingTest.php @@ -13,10 +13,10 @@ class DelimiterSpacingTest extends AbstractSniffTest public function testSniff() { $this->checkGenericSniff(new DelimiterSpacingSniff(), [ - [12, 1], - [12, 12], - [12, 15], - [12, 25], + [12 => 1], + [12 => 12], + [12 => 15], + [12 => 25], ]); } } diff --git a/TwigCS/Tests/Ruleset/Generic/DisallowCommentedCodeTest.php b/TwigCS/Tests/Ruleset/Generic/DisallowCommentedCodeTest.php index 559f6dd..1be777d 100644 --- a/TwigCS/Tests/Ruleset/Generic/DisallowCommentedCodeTest.php +++ b/TwigCS/Tests/Ruleset/Generic/DisallowCommentedCodeTest.php @@ -13,7 +13,7 @@ class DisallowCommentedCodeTest extends AbstractSniffTest public function testSniff() { $this->checkGenericSniff(new DisallowCommentedCodeSniff(), [ - [2, 5], + [2 => 5], ]); } } diff --git a/TwigCS/Tests/Ruleset/Generic/EmptyLinesTest.php b/TwigCS/Tests/Ruleset/Generic/EmptyLinesTest.php index ddc51f0..c89c547 100644 --- a/TwigCS/Tests/Ruleset/Generic/EmptyLinesTest.php +++ b/TwigCS/Tests/Ruleset/Generic/EmptyLinesTest.php @@ -13,7 +13,7 @@ class EmptyLinesTest extends AbstractSniffTest public function testSniff() { $this->checkGenericSniff(new EmptyLinesSniff(), [ - [3, 1], + [3 => 1], ]); } } diff --git a/TwigCS/Tests/Ruleset/Generic/OperatorSpacingTest.php b/TwigCS/Tests/Ruleset/Generic/OperatorSpacingTest.php new file mode 100644 index 0000000..8a07284 --- /dev/null +++ b/TwigCS/Tests/Ruleset/Generic/OperatorSpacingTest.php @@ -0,0 +1,50 @@ +checkGenericSniff(new OperatorSpacingSniff(), [ + [1 => 4], + [1 => 4], + [2 => 5], + [2 => 5], + [3 => 5], + [3 => 5], + [4 => 5], + [4 => 5], + [5 => 5], + [5 => 5], + [6 => 5], + [6 => 5], + [7 => 5], + [7 => 5], + [8 => 7], + [8 => 7], + [9 => 10], + [9 => 10], + [9 => 19], + [9 => 19], + [10 => 5], + [10 => 5], + [11 => 6], + [11 => 6], + [12 => 11], + [12 => 11], + [13 => 11], + [13 => 11], + [14 => 7], + [14 => 7], + [15 => 7], + [15 => 7], + ]); + } +} diff --git a/TwigCS/Token/Tokenizer.php b/TwigCS/Token/Tokenizer.php index e585e8c..93a4ef2 100644 --- a/TwigCS/Token/Tokenizer.php +++ b/TwigCS/Token/Tokenizer.php @@ -23,7 +23,7 @@ class Tokenizer const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As'; const REGEX_DQ_STRING_DELIM = '/"/A'; const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; - const PUNCTUATION = '()[]{}?:.,|'; + const PUNCTUATION = '()[]{}:.,|'; /** * @var array @@ -73,7 +73,7 @@ class Tokenizer /** * @var array */ - protected $brackets; + protected $bracketsAndTernary; /** * @var string @@ -183,7 +183,7 @@ protected function resetState(Source $source) */ protected function getState() { - return !empty($this->state) ? $this->state[count($this->state) - 1] : self::STATE_DATA; + return count($this->state) > 0 ? $this->state[count($this->state) - 1] : self::STATE_DATA; } /** @@ -232,7 +232,7 @@ protected function preflightSource(string $code) */ protected function getTokenPosition(int $offset = 0) { - if (empty($this->tokenPositions) + if (count($this->tokenPositions) === 0 || !isset($this->tokenPositions[$this->currentPosition + $offset]) ) { return null; @@ -274,6 +274,7 @@ protected function pushToken(int $type, string $value = null) protected function lexExpression() { $currentToken = $this->code[$this->cursor]; + if (preg_match('/\t/', $currentToken)) { $this->lexTab(); } elseif (' ' === $currentToken) { @@ -281,9 +282,7 @@ protected function lexExpression() } elseif (PHP_EOL === $currentToken) { $this->lexEOL(); } elseif (preg_match($this->regexes['operator'], $this->code, $match, null, $this->cursor)) { - // operators - $this->pushToken(Token::OPERATOR_TYPE, $match[0]); - $this->moveCursor($match[0]); + $this->lexOperator($match[0]); } elseif (preg_match(self::REGEX_NAME, $this->code, $match, null, $this->cursor)) { // names $this->pushToken(Token::NAME_TYPE, $match[0]); @@ -297,22 +296,7 @@ protected function lexExpression() $this->pushToken(Token::NUMBER_TYPE, $number); $this->moveCursor($match[0]); } elseif (false !== strpos(self::PUNCTUATION, $this->code[$this->cursor])) { - // punctuation - if (false !== strpos('([{', $this->code[$this->cursor])) { - // opening bracket - $this->brackets[] = [$this->code[$this->cursor], $this->line]; - } elseif (false !== strpos(')]}', $this->code[$this->cursor])) { - // closing bracket - if (empty($this->brackets)) { - throw new Exception(sprintf('Unexpected "%s"', $this->code[$this->cursor])); - } - $expect = array_pop($this->brackets)[0]; - if (strtr($expect, '([{', ')]}') !== $this->code[$this->cursor]) { - throw new Exception(sprintf('Unclosed "%s"', $expect)); - } - } - $this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]); - $this->moveCursor($this->code[$this->cursor]); + $this->lexPunctuation(); } elseif (preg_match(self::REGEX_STRING, $this->code, $match, null, $this->cursor)) { // strings $this->pushToken(Token::STRING_TYPE, addcslashes(stripcslashes($match[0]), '\\')); @@ -331,7 +315,7 @@ protected function lexBlock() $endRegex = $this->regexes['lex_block']; preg_match($endRegex, $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor); - if (!empty($this->brackets) || !isset($match[0])) { + if (count($this->bracketsAndTernary) > 0 || !isset($match[0])) { $this->lexExpression(); } else { $this->pushToken(Token::BLOCK_END_TYPE, $match[0][0]); @@ -349,7 +333,7 @@ protected function lexVariable() $endRegex = $this->regexes['lex_variable']; preg_match($endRegex, $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor); - if (!empty($this->brackets) || !isset($match[0])) { + if (count($this->bracketsAndTernary) > 0 || !isset($match[0])) { $this->lexExpression(); } else { $this->pushToken(Token::VAR_END_TYPE, $match[0][0]); @@ -471,4 +455,54 @@ protected function lexEOL() $this->pushToken(Token::EOL_TYPE, $this->code[$this->cursor]); $this->moveCursor($this->code[$this->cursor]); } + + /** + * @param string $operator + */ + protected function lexOperator($operator) + { + if ('?' === $operator) { + $this->bracketsAndTernary[] = [$operator, $this->line]; + } + + // operators + $this->pushToken(Token::OPERATOR_TYPE, $operator); + $this->moveCursor($operator); + } + + /** + * @throws Exception + */ + protected function lexPunctuation() + { + $currentToken = $this->code[$this->cursor]; + + $lastBracket = end($this->bracketsAndTernary); + if (false !== $lastBracket && '?' === $lastBracket[0] && ':' === $currentToken) { + // This is a ternary instead + $this->lexOperator($currentToken); + array_pop($this->bracketsAndTernary); + + return; + } + + if (false !== strpos('([{', $currentToken)) { + $this->bracketsAndTernary[] = [$currentToken, $this->line]; + } elseif (false !== strpos(')]}', $currentToken)) { + if (0 === count($this->bracketsAndTernary)) { + throw new Exception(sprintf('Unexpected "%s"', $currentToken)); + } + + $expect = array_pop($this->bracketsAndTernary)[0]; + if ('?' === $expect) { + throw new Exception('Unclosed ternary'); + } + if (strtr($expect, '([{', ')]}') !== $currentToken) { + throw new Exception(sprintf('Unclosed "%s"', $expect)); + } + } + + $this->pushToken(Token::PUNCTUATION_TYPE, $currentToken); + $this->moveCursor($currentToken); + } } diff --git a/TwigCS/Token/TokenizerHelper.php b/TwigCS/Token/TokenizerHelper.php index b8e939d..5c1cd5a 100644 --- a/TwigCS/Token/TokenizerHelper.php +++ b/TwigCS/Token/TokenizerHelper.php @@ -83,7 +83,7 @@ public function getTokensStartRegex() public function getOperatorRegex() { $operators = array_merge( - ['='], + ['=', '?'], array_keys($this->env->getUnaryOperators()), array_keys($this->env->getBinaryOperators()) );