From 2738135427a297a152988a7add50a346299624f2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 12 Jan 2024 10:18:47 +0100 Subject: [PATCH] Provide a way to ignore error on specific lines (#162) --- README.md | 112 +----------------- docs/command.md | 19 +++ docs/configuration.md | 100 ++++++++++++++++ docs/identifiers.md | 45 +++++++ src/Command/TwigCsFixerCommand.php | 8 +- src/Report/Reporter/CheckstyleReporter.php | 10 +- src/Report/Reporter/GithubReporter.php | 10 +- src/Report/Reporter/JUnitReporter.php | 10 +- src/Report/Reporter/NullReporter.php | 8 +- src/Report/Reporter/ReporterInterface.php | 7 +- src/Report/Reporter/TextReporter.php | 16 ++- src/Report/Violation.php | 31 +++-- src/Report/ViolationId.php | 72 +++++++++++ src/Rules/AbstractRule.php | 87 +++++++++----- src/Rules/AbstractSpacingRule.php | 6 +- src/Rules/RuleInterface.php | 7 +- src/Runner/Fixer.php | 4 +- src/Runner/Linter.php | 8 +- src/Token/Tokenizer.php | 66 +++++++++-- src/Token/TokenizerInterface.php | 3 +- tests/Command/TwigCsFixerCommandTest.php | 24 +++- .../Reporter/CheckstyleReporterTest.php | 81 +++++++++++-- tests/Report/Reporter/GithubReporterTest.php | 55 +++++++-- tests/Report/Reporter/JUnitReporterTest.php | 90 ++++++++++++-- tests/Report/Reporter/NullReporterTest.php | 8 +- tests/Report/Reporter/TextReporterTest.php | 74 +++++++++--- tests/Report/ViolationIdTest.php | 77 ++++++++++++ ...iffViolationTest.php => ViolationTest.php} | 8 +- tests/Rules/AbstractRuleTestCase.php | 10 +- .../BlockNameSpacing/BlockNameSpacingTest.php | 8 +- .../DelimiterSpacing/DelimiterSpacingTest.php | 8 +- tests/Rules/Fixtures/FakeRule.php | 7 ++ tests/Rules/Fixtures/disable0.twig | 0 tests/Rules/Fixtures/disable1.twig | 2 + tests/Rules/Fixtures/disable2.twig | 14 +++ .../OperatorNameSpacingTest.php | 6 +- .../OperatorSpacing/OperatorSpacingTest.php | 88 +++++++------- .../PunctuationSpacingTest.php | 30 ++--- .../TrailingCommaSingleLineTest.php | 6 +- tests/Rules/RuleTest.php | 50 ++++++++ .../Whitespace/BlankEOF/BlankEOFTest.php | 6 +- .../Whitespace/EmptyLines/EmptyLinesTest.php | 6 +- tests/Rules/Whitespace/Indent/IndentTest.php | 8 +- .../TrailingSpace/TrailingSpaceTest.php | 10 +- tests/Runner/FixerTest.php | 39 +++++- tests/Runner/LinterTest.php | 7 +- .../Fixtures/ignored_violations.twig | 5 + tests/Token/Tokenizer/TokenizerTest.php | 40 ++++++- 48 files changed, 1060 insertions(+), 336 deletions(-) create mode 100644 docs/command.md create mode 100644 docs/configuration.md create mode 100644 docs/identifiers.md create mode 100644 src/Report/ViolationId.php create mode 100644 tests/Report/ViolationIdTest.php rename tests/Report/{SniffViolationTest.php => ViolationTest.php} (88%) create mode 100644 tests/Rules/Fixtures/disable0.twig create mode 100644 tests/Rules/Fixtures/disable1.twig create mode 100644 tests/Rules/Fixtures/disable2.twig create mode 100644 tests/Token/Tokenizer/Fixtures/ignored_violations.twig diff --git a/README.md b/README.md index 9ff98870..03931599 100644 --- a/README.md +++ b/README.md @@ -59,112 +59,6 @@ Removes any space before and after opening and closing of arrays and hashes. ## Custom configuration -### Standard - -By default, the twig-cs-fixer standard is enabled with the twig coding standard rules and the following rules: - - - `BlankEOFRule`: ensures that files end with one blank line. - - `BlockNameSpacingRule`: ensure there is one space before and after block names. - - `EmptyLinesRule`: ensures that 2 empty lines do not follow each other. - - `IndentRule`: ensures that files are not indented with tabs. - - `TrailingCommaSingleLineRule`: ensures that single-line arrays, objects and argument lists do not have a trailing comma. - - `TrailingSpaceRule`: ensures that files have no trailing spaces. - -If you want to use the basic Twig standard, another standard and/or add/disable a rule, you can provide -your own configuration with a `.twig-cs-fixer.php` file which returns a `TwigCsFixer\Config\Config` class: - -```php -addStandard(new TwigCsFixer\Standard\Twig()); -$ruleset->addRule(\TwigCsFixer\Rules\Whitespace\EmptyLinesRule::class); - -$config = new TwigCsFixer\Config\Config(); -$config->setRuleset($ruleset); - -return $config; -``` - -If your config is not located in your current directory, you can specify its path using `--config` when running the command: - -```bash -vendor/bin/twig-cs-fixer lint --config=dir/.twig-cs-fixer.php /path/to/code -``` - -### Files - -By default, all `.twig` files in the current directory are linted, except the ones in the `vendor` directory. - -If you want to lint specific files or directories you can pass them as argument. If you want a more sophisticated -rule, you can configure it in the `.twig-cs-fixer.php` file: - -```php -exclude('myCustomDirectory'); - -$config = new TwigCsFixer\Config\Config(); -$config->setFinder($finder); - -return $config; -``` - -### Cache - -By default, cache is enabled and stored in `.twig-cs-fixer.cache`. Further runs are therefore much -faster. Cache is invalidated when a different PHP version, twig-cs-fixer version or ruleset is used. - -If you want a custom cache location you can configure it in `.twig-cs-fixer.php`: - -```php -setCacheFile('/tmp/.twig-cs-fixer.cache'); - -return $config; -``` - -To disable cache you can either pass `--no-cache` when running the command: - -```bash -vendor/bin/twig-cs-fixer lint --no-cache -``` - -or set the cache file to `null` in your config: - -```php -setCacheFile(null); - -return $config; -``` - -### Token parser - -If you're using custom token parsers or binary/unary operators, they can be added in your config: - -```php -addTwigExtension(new App\Twig\CustomTwigExtension()); -$config->addTokenParser(new App\Twig\CustomTokenParser()); - -return $config; -``` - -### Reporter - -The `--report` option allows to choose the output format for the linter report. - -Supported formats are: -- `text` selected by default. -- `checkstyle` following the common checkstyle XML schema. -- `github` if you want annotations on GitHub actions. -- `junit` following JUnit schema XML from Jenkins. -- `null` if you don't want any reporting. +- [CLI options](docs/command.md) +- [Configuration file](docs/configuration.md) +- [How to disable a rule on a specific file or line](docs/identifiers.md) diff --git a/docs/command.md b/docs/command.md new file mode 100644 index 00000000..3ce8c59c --- /dev/null +++ b/docs/command.md @@ -0,0 +1,19 @@ +# CLI options + +## Reporter + +The `--report` option allows to choose the output format for the linter report. + +Supported formats are: +- `text` selected by default. +- `checkstyle` following the common checkstyle XML schema. +- `github` if you want annotations on GitHub actions. +- `junit` following JUnit schema XML from Jenkins. +- `null` if you don't want any reporting. + +## Debug mode + +The `--debug` option displays error identifiers instead of messages. This is +useful if you want to disable a specific error with a comment in your code. + +See also [how to disable a rule on a specific file or line](identifiers.md). diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 00000000..7b2fa8a6 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,100 @@ +# Configuration file + +## Standard + +By default, the twig-cs-fixer standard is enabled with the twig coding standard rules and the following rules: + +- `BlankEOFRule`: ensures that files end with one blank line. +- `BlockNameSpacingRule`: ensure there is one space before and after block names. +- `EmptyLinesRule`: ensures that 2 empty lines do not follow each other. +- `IndentRule`: ensures that files are not indented with tabs. +- `TrailingCommaSingleLineRule`: ensures that single-line arrays, objects and argument lists do not have a trailing comma. +- `TrailingSpaceRule`: ensures that files have no trailing spaces. + +If you want to use the basic Twig standard, another standard and/or add/disable a rule, you can provide +your own configuration with a `.twig-cs-fixer.php` file which returns a `TwigCsFixer\Config\Config` class: + +```php +addStandard(new TwigCsFixer\Standard\Twig()); +$ruleset->addRule(\TwigCsFixer\Rules\Whitespace\EmptyLinesRule::class); + +$config = new TwigCsFixer\Config\Config(); +$config->setRuleset($ruleset); + +return $config; +``` + +If your config is not located in your current directory, you can specify its path using `--config` when running the command: + +```bash +vendor/bin/twig-cs-fixer lint --config=dir/.twig-cs-fixer.php /path/to/code +``` + +## Files + +By default, all `.twig` files in the current directory are linted, except the ones in the `vendor` directory. + +If you want to lint specific files or directories you can pass them as argument. If you want a more sophisticated +rule, you can configure it in the `.twig-cs-fixer.php` file: + +```php +exclude('myCustomDirectory'); + +$config = new TwigCsFixer\Config\Config(); +$config->setFinder($finder); + +return $config; +``` + +## Cache + +By default, cache is enabled and stored in `.twig-cs-fixer.cache`. Further runs are therefore much +faster. Cache is invalidated when a different PHP version, twig-cs-fixer version or ruleset is used. + +If you want a custom cache location you can configure it in `.twig-cs-fixer.php`: + +```php +setCacheFile('/tmp/.twig-cs-fixer.cache'); + +return $config; +``` + +To disable cache you can either pass `--no-cache` when running the command: + +```bash +vendor/bin/twig-cs-fixer lint --no-cache +``` + +or set the cache file to `null` in your config: + +```php +setCacheFile(null); + +return $config; +``` + +## Token parser & Twig Extension + +If you're using custom token parsers or binary/unary operators, they can be added in your config: + +```php +addTwigExtension(new App\Twig\CustomTwigExtension()); +$config->addTokenParser(new App\Twig\CustomTokenParser()); + +return $config; +``` diff --git a/docs/identifiers.md b/docs/identifiers.md new file mode 100644 index 00000000..60fc5cee --- /dev/null +++ b/docs/identifiers.md @@ -0,0 +1,45 @@ +# How to disable a rule on a specific file or line + +All errors have an identifier with the syntax: `A.B:C:D` with +- A: The rule short name (mainly made from the class name) +- B: The error identifier (like the error level or a specific name) +- C: The line the error occurs +- D: The position of the token in the line the error occurs + +NB: The four parts are optional, all those format are working +- A +- A.B +- A.B:C +- A.B:C:D +- A:C +- A:C:D +- A::D + +When you want to disable a rule, you can use of the following syntax: +```twig +{# twig-cs-fixer-disable A.B:C:D #} => Apply to the whole file +{# twig-cs-fixer-disable-line A.B:C:D #} => Apply to the line of the comment +{# twig-cs-fixer-disable-next-line A.B:C:D #} => Apply to the next line of the comment +``` + +For instance: +```twig +{# twig-cs-fixer-disable #} => Disable every rule for the whole file +{# twig-cs-fixer-disable-line #} => Disable every rule for the current line +{# twig-cs-fixer-disable-next-line #} => Disable every rule for the next line + +{# twig-cs-fixer-disable A #} => Disable the rule A for the whole file +{# twig-cs-fixer-disable A:42 #} => Disable the rule A for the line 42 of the file +{# twig-cs-fixer-disable-line A.B #} => Disable the error B of the rule A for the current line +{# twig-cs-fixer-disable-next-line A::42 #} => Disable the rule A for the next line but only for the token 42 +``` + +You can also disable multiple errors with a single comment, by separating them +with a space or a comma: +```twig +{# twig-cs-fixer-disable A B C #} => Disable A and B and C for the whole file +{# twig-cs-fixer-disable-line A.B,C.D #} => Disable A.B and C.D for the current line +``` + +If you need to know the errors identifier you have/want to ignore, you can run the +linter command with the `--debug` options. See also [the command options](command.md). diff --git a/src/Command/TwigCsFixerCommand.php b/src/Command/TwigCsFixerCommand.php index 6322e5aa..b2f4e208 100644 --- a/src/Command/TwigCsFixerCommand.php +++ b/src/Command/TwigCsFixerCommand.php @@ -70,6 +70,12 @@ protected function configure(): void InputOption::VALUE_NONE, 'Disable cache while running the fixer' ), + new InputOption( + 'debug', + '', + InputOption::VALUE_NONE, + 'Display error identifiers instead of messages', + ), ]) ; } @@ -129,7 +135,7 @@ private function runLinter(Config $config, InputInterface $input, OutputInterfac $reporterFactory = new ReporterFactory(); $reporter = $reporterFactory->getReporter($input->getOption('report')); - $reporter->display($output, $report, $input->getOption('level')); + $reporter->display($output, $report, $input->getOption('level'), $input->getOption('debug')); return $report; } diff --git a/src/Report/Reporter/CheckstyleReporter.php b/src/Report/Reporter/CheckstyleReporter.php index 5aff7409..fbea00b1 100644 --- a/src/Report/Reporter/CheckstyleReporter.php +++ b/src/Report/Reporter/CheckstyleReporter.php @@ -12,8 +12,12 @@ final class CheckstyleReporter implements ReporterInterface { public const NAME = 'checkstyle'; - public function display(OutputInterface $output, Report $report, ?string $level = null): void - { + public function display( + OutputInterface $output, + Report $report, + ?string $level, + bool $debug + ): void { $text = ''."\n"; $text .= ''."\n"; @@ -38,7 +42,7 @@ public function display(OutputInterface $output, Report $report, ?string $level $text .= ' column="'.$linePosition.'"'; } $text .= ' severity="'.strtolower(Violation::getLevelAsString($violation->getLevel())).'"'; - $text .= ' message="'.$this->xmlEncode($violation->getMessage()).'"'; + $text .= ' message="'.$this->xmlEncode($violation->getDebugMessage($debug)).'"'; if (null !== $ruleName) { $text .= ' source="'.$ruleName.'"'; } diff --git a/src/Report/Reporter/GithubReporter.php b/src/Report/Reporter/GithubReporter.php index 25089f55..4fbcf061 100644 --- a/src/Report/Reporter/GithubReporter.php +++ b/src/Report/Reporter/GithubReporter.php @@ -17,8 +17,12 @@ final class GithubReporter implements ReporterInterface { public const NAME = 'github'; - public function display(OutputInterface $output, Report $report, ?string $level = null): void - { + public function display( + OutputInterface $output, + Report $report, + ?string $level, + bool $debug + ): void { $violations = $report->getViolations($level); foreach ($violations as $violation) { $text = match ($violation->getLevel()) { @@ -40,7 +44,7 @@ public function display(OutputInterface $output, Report $report, ?string $level // newlines need to be encoded // see https://github.com/actions/starter-workflows/issues/68#issuecomment-581479448 - $text .= '::'.str_replace("\n", '%0A', $violation->getMessage()); + $text .= '::'.str_replace("\n", '%0A', $violation->getDebugMessage($debug)); $output->writeln($text); } diff --git a/src/Report/Reporter/JUnitReporter.php b/src/Report/Reporter/JUnitReporter.php index e986615c..c40f6fcd 100644 --- a/src/Report/Reporter/JUnitReporter.php +++ b/src/Report/Reporter/JUnitReporter.php @@ -12,8 +12,12 @@ final class JUnitReporter implements ReporterInterface { public const NAME = 'junit'; - public function display(OutputInterface $output, Report $report, ?string $level = null): void - { + public function display( + OutputInterface $output, + Report $report, + ?string $level, + bool $debug + ): void { $violations = $report->getViolations($level); $count = \count($violations); @@ -30,7 +34,7 @@ public function display(OutputInterface $output, Report $report, ?string $level $text .= $this->createTestCase( sprintf('%s:%s', $violation->getFilename(), $violation->getLine() ?? 0), strtolower(Violation::getLevelAsString($violation->getLevel())), - $violation->getMessage() + $violation->getDebugMessage($debug) ); } } else { diff --git a/src/Report/Reporter/NullReporter.php b/src/Report/Reporter/NullReporter.php index fbd417d6..5bb8894b 100644 --- a/src/Report/Reporter/NullReporter.php +++ b/src/Report/Reporter/NullReporter.php @@ -14,7 +14,11 @@ final class NullReporter implements ReporterInterface { public const NAME = 'null'; - public function display(OutputInterface $output, Report $report, ?string $level = null): void - { + public function display( + OutputInterface $output, + Report $report, + ?string $level, + bool $debug + ): void { } } diff --git a/src/Report/Reporter/ReporterInterface.php b/src/Report/Reporter/ReporterInterface.php index 05e719e8..cff3823d 100644 --- a/src/Report/Reporter/ReporterInterface.php +++ b/src/Report/Reporter/ReporterInterface.php @@ -9,5 +9,10 @@ interface ReporterInterface { - public function display(OutputInterface $output, Report $report, ?string $level = null): void; + public function display( + OutputInterface $output, + Report $report, + ?string $level, + bool $debug + ): void; } diff --git a/src/Report/Reporter/TextReporter.php b/src/Report/Reporter/TextReporter.php index 19898a6e..ec9a8d4e 100644 --- a/src/Report/Reporter/TextReporter.php +++ b/src/Report/Reporter/TextReporter.php @@ -23,8 +23,12 @@ final class TextReporter implements ReporterInterface private const ERROR_LINE_FORMAT = '%-5s| %s'; private const ERROR_LINE_WIDTH = 120; - public function display(OutputInterface $output, Report $report, ?string $level = null): void - { + public function display( + OutputInterface $output, + Report $report, + ?string $level, + bool $debug + ): void { $io = new SymfonyStyle(new ArrayInput([]), $output); if ( @@ -48,7 +52,7 @@ public function display(OutputInterface $output, Report $report, ?string $level $line = $violation->getLine(); if (null === $line || false === $content) { - $formattedText[] = $this->formatErrorMessage($violation); + $formattedText[] = $this->formatErrorMessage($violation, $debug); } else { $lines = $this->getContext($content, $line); foreach ($lines as $no => $code) { @@ -59,7 +63,7 @@ public function display(OutputInterface $output, Report $report, ?string $level ); if ($no === $violation->getLine()) { - $formattedText[] = $this->formatErrorMessage($violation); + $formattedText[] = $this->formatErrorMessage($violation, $debug); } } } @@ -119,12 +123,12 @@ private function getContext(string $template, int $line): array return array_map(fn (string $code): string => substr($code, min($indents)), $result); } - private function formatErrorMessage(Violation $message): string + private function formatErrorMessage(Violation $message, bool $debug): string { return sprintf( sprintf('%s', self::ERROR_LINE_FORMAT), self::ERROR_CURSOR_CHAR, - wordwrap($message->getMessage(), self::ERROR_LINE_WIDTH) + wordwrap($message->getDebugMessage($debug), self::ERROR_LINE_WIDTH) ); } } diff --git a/src/Report/Violation.php b/src/Report/Violation.php index 62c1e01b..c5102e80 100644 --- a/src/Report/Violation.php +++ b/src/Report/Violation.php @@ -20,9 +20,8 @@ public function __construct( private int $level, private string $message, private string $filename, - private ?int $line = null, - private ?int $linePosition = null, - private ?string $ruleName = null + private ?string $ruleName = null, + private ?ViolationId $identifier = null, ) { } @@ -62,9 +61,13 @@ public function getMessage(): string return $this->message; } - public function getLine(): ?int + public function getDebugMessage(bool $debug): string { - return $this->line; + if (!$debug) { + return $this->message; + } + + return $this->identifier?->toString() ?? $this->message; } public function getFilename(): string @@ -72,13 +75,23 @@ public function getFilename(): string return $this->filename; } - public function getLinePosition(): ?int + public function getRuleName(): ?string { - return $this->linePosition; + return $this->ruleName; } - public function getRuleName(): ?string + public function getIdentifier(): ?ViolationId { - return $this->ruleName; + return $this->identifier; + } + + public function getLine(): ?int + { + return $this->identifier?->getLine(); + } + + public function getLinePosition(): ?int + { + return $this->identifier?->getLinePosition(); } } diff --git a/src/Report/ViolationId.php b/src/Report/ViolationId.php new file mode 100644 index 00000000..a5e6a27e --- /dev/null +++ b/src/Report/ViolationId.php @@ -0,0 +1,72 @@ +line; + } + + public function getLinePosition(): ?int + { + return $this->linePosition; + } + + public static function fromString(string $string, ?int $line = null): self + { + $exploded = explode(':', $string); + $name = $exploded[0]; + $explodedName = '' !== $name ? explode('.', $name) : null; + + $line ??= isset($exploded[1]) && '' !== $exploded[1] ? (int) $exploded[1] : null; + $position = isset($exploded[2]) && '' !== $exploded[2] ? (int) $exploded[2] : null; + + return new self( + $explodedName[0] ?? null, + $explodedName[1] ?? null, + $line, + $position + ); + } + + public function toString(): string + { + $name = rtrim(sprintf( + '%s.%s', + $this->ruleIdentifier ?? '', + $this->messageIdentifier ?? '', + ), '.'); + + return rtrim(sprintf( + '%s:%s:%s', + $name, + $this->line ?? '', + $this->linePosition ?? '', + ), ':'); + } + + public function match(self $violationId): bool + { + return $this->matchValue($this->ruleIdentifier, $violationId->ruleIdentifier) + && $this->matchValue($this->messageIdentifier, $violationId->messageIdentifier) + && $this->matchValue($this->line, $violationId->line) + && $this->matchValue($this->linePosition, $violationId->linePosition); + } + + private function matchValue(string|int|null $self, string|int|null $other): bool + { + return null === $self || strtolower((string) $self) === strtolower((string) $other); + } +} diff --git a/src/Rules/AbstractRule.php b/src/Rules/AbstractRule.php index c665d54a..c8b658e8 100644 --- a/src/Rules/AbstractRule.php +++ b/src/Rules/AbstractRule.php @@ -4,8 +4,10 @@ namespace TwigCsFixer\Rules; +use ReflectionClass; use TwigCsFixer\Report\Report; use TwigCsFixer\Report\Violation; +use TwigCsFixer\Report\ViolationId; use TwigCsFixer\Runner\FixerInterface; use TwigCsFixer\Token\Token; @@ -15,25 +17,39 @@ abstract class AbstractRule implements RuleInterface private ?FixerInterface $fixer = null; + /** + * @var list + */ + private array $ignoredViolations = []; + public function getName(): string { return static::class; } - public function lintFile(array $stream, Report $report): void + public function getShortName(): string + { + $shortName = (new ReflectionClass($this))->getShortName(); + + return str_ends_with($shortName, 'Rule') ? substr($shortName, 0, -4) : $shortName; + } + + public function lintFile(array $stream, Report $report, array $ignoredViolations = []): void { $this->report = $report; $this->fixer = null; + $this->ignoredViolations = $ignoredViolations; foreach (array_keys($stream) as $index) { $this->process($index, $stream); } } - public function fixFile(array $stream, FixerInterface $fixer): void + public function fixFile(array $stream, FixerInterface $fixer, array $ignoredViolations = []): void { $this->report = null; $this->fixer = $fixer; + $this->ignoredViolations = $ignoredViolations; foreach (array_keys($stream) as $index) { $this->process($index, $stream); @@ -106,50 +122,63 @@ protected function findPrevious(int|string|array $type, array $tokens, int $star return $start - $i; } - protected function addWarning(string $message, Token $token): void + protected function addWarning(string $message, Token $token, ?string $messageId = null): bool { - $this->addMessage(Violation::LEVEL_WARNING, $message, $token); + return $this->addMessage(Violation::LEVEL_WARNING, $message, $token, $messageId); } - protected function addError(string $message, Token $token): void + protected function addError(string $message, Token $token, ?string $messageId = null): bool { - $this->addMessage(Violation::LEVEL_ERROR, $message, $token); + return $this->addMessage(Violation::LEVEL_ERROR, $message, $token, $messageId); } - protected function addFixableWarning(string $message, Token $token): ?FixerInterface + protected function addFixableWarning(string $message, Token $token, ?string $messageId = null): ?FixerInterface { - return $this->addFixableMessage(Violation::LEVEL_WARNING, $message, $token); - } + $added = $this->addWarning($message, $token, $messageId); + if (!$added) { + return null; + } - protected function addFixableError(string $message, Token $token): ?FixerInterface - { - return $this->addFixableMessage(Violation::LEVEL_ERROR, $message, $token); + return $this->fixer; } - private function addMessage(int $messageType, string $message, Token $token): void + protected function addFixableError(string $message, Token $token, ?string $messageId = null): ?FixerInterface { - $report = $this->report; - if (null === $report) { - // We are fixing the file. - return; + $added = $this->addError($message, $token, $messageId); + if (!$added) { + return null; } - $violation = new Violation( - $messageType, - $message, - $token->getFilename(), + return $this->fixer; + } + + private function addMessage(int $messageType, string $message, Token $token, ?string $messageId = null): bool + { + $id = new ViolationId( + $this->getShortName(), + $messageId ?? ucfirst(strtolower(Violation::getLevelAsString($messageType))), $token->getLine(), $token->getPosition(), - $this->getName(), ); + foreach ($this->ignoredViolations as $ignoredViolation) { + if ($ignoredViolation->match($id)) { + return false; + } + } - $report->addViolation($violation); - } - - private function addFixableMessage(int $messageType, string $message, Token $token): ?FixerInterface - { - $this->addMessage($messageType, $message, $token); + $report = $this->report; + if (null !== $report) { // The report is null when we are fixing the file. + $violation = new Violation( + $messageType, + $message, + $token->getFilename(), + $this->getName(), + $id, + ); + + $report->addViolation($violation); + } - return $this->fixer; + return true; } } diff --git a/src/Rules/AbstractSpacingRule.php b/src/Rules/AbstractSpacingRule.php index 67ae13d2..e099f828 100644 --- a/src/Rules/AbstractSpacingRule.php +++ b/src/Rules/AbstractSpacingRule.php @@ -60,7 +60,8 @@ private function checkSpaceAfter(int $tokenPosition, array $tokens, int $expecte $fixer = $this->addFixableError( sprintf('Expecting %d whitespace after "%s"; found %d', $expected, $token->getValue(), $count), - $token + $token, + 'After' ); if (null === $fixer) { @@ -99,7 +100,8 @@ private function checkSpaceBefore(int $tokenPosition, array $tokens, int $expect $fixer = $this->addFixableError( sprintf('Expecting %d whitespace before "%s"; found %d', $expected, $token->getValue(), $count), - $token + $token, + 'Before' ); if (null === $fixer) { diff --git a/src/Rules/RuleInterface.php b/src/Rules/RuleInterface.php index c2d3bae5..adb5d72c 100644 --- a/src/Rules/RuleInterface.php +++ b/src/Rules/RuleInterface.php @@ -5,6 +5,7 @@ namespace TwigCsFixer\Rules; use TwigCsFixer\Report\Report; +use TwigCsFixer\Report\ViolationId; use TwigCsFixer\Runner\FixerInterface; use TwigCsFixer\Token\Token; @@ -14,11 +15,13 @@ interface RuleInterface * Messages will be added to the given `$report` object. * * @param array $stream + * @param list $ignoredViolations */ - public function lintFile(array $stream, Report $report): void; + public function lintFile(array $stream, Report $report, array $ignoredViolations = []): void; /** * @param array $stream + * @param list $ignoredViolations */ - public function fixFile(array $stream, FixerInterface $fixer): void; + public function fixFile(array $stream, FixerInterface $fixer, array $ignoredViolations = []): void; } diff --git a/src/Runner/Fixer.php b/src/Runner/Fixer.php index 026b147a..701316d1 100644 --- a/src/Runner/Fixer.php +++ b/src/Runner/Fixer.php @@ -83,13 +83,13 @@ public function fixFile(string $content, Ruleset $ruleset): string $this->inConflict = false; $twigSource = new Source($content, 'TwigCsFixer'); - $stream = $this->tokenizer->tokenize($twigSource); + [$stream, $ignoredViolations] = $this->tokenizer->tokenize($twigSource); $this->startFile($stream); $rules = $ruleset->getRules(); foreach ($rules as $rule) { - $rule->fixFile($stream, $this); + $rule->fixFile($stream, $this, $ignoredViolations); } $this->loops++; diff --git a/src/Runner/Linter.php b/src/Runner/Linter.php index daa96c15..fa786ee7 100644 --- a/src/Runner/Linter.php +++ b/src/Runner/Linter.php @@ -14,6 +14,7 @@ use TwigCsFixer\Exception\CannotTokenizeException; use TwigCsFixer\Report\Report; use TwigCsFixer\Report\Violation; +use TwigCsFixer\Report\ViolationId; use TwigCsFixer\Ruleset\Ruleset; use TwigCsFixer\Token\TokenizerInterface; @@ -68,7 +69,8 @@ public function run(iterable $files, Ruleset $ruleset, ?FixerInterface $fixer = Violation::LEVEL_FATAL, sprintf('File is invalid: %s', $error->getRawMessage()), $filePath, - $error->getTemplateLine() + null, + new ViolationId(line: $error->getTemplateLine()) ); $report->addViolation($violation); @@ -108,7 +110,7 @@ public function run(iterable $files, Ruleset $ruleset, ?FixerInterface $fixer = $this->setErrorHandler($report, $filePath); try { $twigSource = new Source($content, $filePath); - $stream = $this->tokenizer->tokenize($twigSource); + [$stream, $ignoredViolations] = $this->tokenizer->tokenize($twigSource); } catch (CannotTokenizeException $exception) { $violation = new Violation( Violation::LEVEL_FATAL, @@ -123,7 +125,7 @@ public function run(iterable $files, Ruleset $ruleset, ?FixerInterface $fixer = $rules = $ruleset->getRules(); foreach ($rules as $rule) { - $rule->lintFile($stream, $report); + $rule->lintFile($stream, $report, $ignoredViolations); } // Only cache the file if there is no error in order to diff --git a/src/Token/Tokenizer.php b/src/Token/Tokenizer.php index 56d23f6f..ffdc0a50 100644 --- a/src/Token/Tokenizer.php +++ b/src/Token/Tokenizer.php @@ -8,6 +8,7 @@ use Twig\Environment; use Twig\Source; use TwigCsFixer\Exception\CannotTokenizeException; +use TwigCsFixer\Report\ViolationId; use Webmozart\Assert\Assert; /** @@ -59,13 +60,18 @@ final class Tokenizer implements TokenizerInterface */ private array $tokens = []; + /** + * @var list + */ + private array $ignoredViolations = []; + /** * @var list */ private array $expressionStarters = []; /** - * @var array, array}> + * @var array, array}> */ private array $state = []; @@ -87,7 +93,7 @@ public function __construct(Environment $env) } /** - * @return list + * @return array{list, list} * * @throws CannotTokenizeException */ @@ -153,7 +159,7 @@ public function tokenize(Source $source): array $this->pushToken(Token::EOF_TYPE); - return $this->tokens; + return [$this->tokens, $this->ignoredViolations]; } private function resetState(Source $source): void @@ -197,8 +203,8 @@ private function getState(): int } /** - * @param int<0, 5> $state - * @param array $data + * @param int<0, 5> $state + * @param array $data */ private function pushState(int $state, array $data = []): void { @@ -210,14 +216,14 @@ private function pushState(int $state, array $data = []): void * * @see https://github.com/vimeo/psalm/issues/8989 */ - private function setStateParam(string $name, string $value): void + private function setStateParam(string $name, int|string|bool $value): void { Assert::notEmpty($this->state, 'Cannot set state params without a current state.'); $this->state[\count($this->state) - 1][1][$name] = $value; } - private function getStateParam(string $name): ?string + private function getStateParam(string $name): int|string|bool|null { Assert::notEmpty($this->state, 'Cannot get state params without a current state.'); @@ -368,9 +374,15 @@ private function lexComment(): void throw CannotTokenizeException::unclosedComment($this->line); } if ($match[0][1] === $this->cursor) { + $this->processIgnoredViolations(); $this->pushToken(Token::COMMENT_END_TYPE, $match[0][0]); $this->popState(); } else { + if (null === $this->getStateParam('ignoredViolations')) { + $comment = substr($this->code, $this->cursor, $match[0][1]); + $this->extractIgnoredViolations($comment); + } + // Parse as text until the end position. $this->lexData($match[0][1]); } @@ -474,7 +486,7 @@ private function lexStart(): void } $this->pushToken($tokenType, $expressionStarter['fullMatch']); - $this->pushState($state); + $this->pushState($state, ['startLine' => $this->line]); } private function lexStartDqString(): void @@ -673,4 +685,42 @@ private function getOperatorRegex(Environment $env): string return '/'.implode('|', $regex).'/A'; } + + private function extractIgnoredViolations(string $comment): void + { + $comment = trim($comment); + if (1 === preg_match('/^twig-cs-fixer-disable(|-line|-next-line)\s+([\s\w,.:]*)/i', $comment, $match)) { + $this->setStateParam('ignoredViolations', preg_replace('/\s+/', ',', $match[2]) ?? ''); + $this->setStateParam('ignoredType', trim($match[1], '-')); + } else { + $this->setStateParam('ignoredViolations', false); + } + } + + private function processIgnoredViolations(): void + { + $ignoredViolations = $this->getStateParam('ignoredViolations'); + if (!\is_string($ignoredViolations)) { + return; + } + $line = match ($this->getStateParam('ignoredType')) { + 'line' => (int) $this->getStateParam('startLine'), + 'next-line' => $this->line + 1, + default => null, + }; + + if ('' === $ignoredViolations) { + $this->ignoredViolations[] = ViolationId::fromString($ignoredViolations, $line); + + return; + } + + $ignoredViolationsExploded = explode(',', $ignoredViolations); + foreach ($ignoredViolationsExploded as $ignoredViolation) { + if ('' === $ignoredViolation) { + continue; + } + $this->ignoredViolations[] = ViolationId::fromString($ignoredViolation, $line); + } + } } diff --git a/src/Token/TokenizerInterface.php b/src/Token/TokenizerInterface.php index 85ad9fb2..4d1afd52 100644 --- a/src/Token/TokenizerInterface.php +++ b/src/Token/TokenizerInterface.php @@ -6,11 +6,12 @@ use Twig\Source; use TwigCsFixer\Exception\CannotTokenizeException; +use TwigCsFixer\Report\ViolationId; interface TokenizerInterface { /** - * @return list + * @return array{list, list} * * @throws CannotTokenizeException */ diff --git a/tests/Command/TwigCsFixerCommandTest.php b/tests/Command/TwigCsFixerCommandTest.php index 4f38c40c..1709281d 100644 --- a/tests/Command/TwigCsFixerCommandTest.php +++ b/tests/Command/TwigCsFixerCommandTest.php @@ -61,6 +61,28 @@ public function testExecuteWithReportErrors(): void $display = $commandTester->getDisplay(); static::assertStringContainsString('directory/subdirectory/file.twig', $display); static::assertStringContainsString('directory/file.twig', $display); + static::assertStringNotContainsString('DelimiterSpacing.After', $display); + static::assertStringContainsString( + '[ERROR] Files linted: 3, notices: 0, warnings: 0, errors: 3', + $display + ); + static::assertSame(Command::FAILURE, $commandTester->getStatusCode()); + } + + public function testExecuteWithReportErrorsAndDebug(): void + { + $command = new TwigCsFixerCommand(); + + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'paths' => [$this->getTmpPath(__DIR__.'/Fixtures')], + '--debug' => true, + ]); + + $display = $commandTester->getDisplay(); + static::assertStringContainsString('directory/subdirectory/file.twig', $display); + static::assertStringContainsString('directory/file.twig', $display); + static::assertStringContainsString('DelimiterSpacing.After', $display); static::assertStringContainsString( '[ERROR] Files linted: 3, notices: 0, warnings: 0, errors: 3', $display @@ -152,7 +174,7 @@ public function testExecuteWithError(): void '--config' => $this->getTmpPath(__DIR__.'/Fixtures/.config-not-found.php'), ]); - static::assertStringStartsWith('Error: ', $commandTester->getDisplay()); + static::assertStringStartsWith('Error: Cannot find the config file', $commandTester->getDisplay()); static::assertSame(Command::INVALID, $commandTester->getStatusCode()); } diff --git a/tests/Report/Reporter/CheckstyleReporterTest.php b/tests/Report/Reporter/CheckstyleReporterTest.php index 3bb4c653..105ec0ae 100644 --- a/tests/Report/Reporter/CheckstyleReporterTest.php +++ b/tests/Report/Reporter/CheckstyleReporterTest.php @@ -11,13 +11,14 @@ use TwigCsFixer\Report\Report; use TwigCsFixer\Report\Reporter\CheckstyleReporter; use TwigCsFixer\Report\Violation; +use TwigCsFixer\Report\ViolationId; final class CheckstyleReporterTest extends TestCase { /** * @dataProvider displayDataProvider */ - public function testDisplayErrors(string $expected, ?string $level): void + public function testDisplayErrors(string $expected, ?string $level, bool $debug): void { $textFormatter = new CheckstyleReporter(); @@ -26,30 +27,66 @@ public function testDisplayErrors(string $expected, ?string $level): void $file3 = __DIR__.'/Fixtures/file3.twig'; $report = new Report([new SplFileInfo($file), new SplFileInfo($file2), new SplFileInfo($file3)]); - $violation0 = new Violation(Violation::LEVEL_NOTICE, 'Notice', $file, 1, 11, 'NoticeRule'); + $violation0 = new Violation( + Violation::LEVEL_NOTICE, + 'Notice', + $file, + 'NoticeRule', + new ViolationId('NoticeId', null, 1) + ); $report->addViolation($violation0); - $violation1 = new Violation(Violation::LEVEL_WARNING, 'Warning', $file, 2, 22, 'WarningRule'); + $violation1 = new Violation( + Violation::LEVEL_WARNING, + 'Warning', + $file, + 'WarningRule', + new ViolationId('WarningId', null, 2, 22) + ); $report->addViolation($violation1); - $violation2 = new Violation(Violation::LEVEL_ERROR, 'Error', $file, 3, 33, 'ErrorRule'); + $violation2 = new Violation( + Violation::LEVEL_ERROR, + 'Error', + $file, + 'ErrorRule', + new ViolationId('ErrorId', null, 3, 33) + ); $report->addViolation($violation2); - $violation3 = new Violation(Violation::LEVEL_FATAL, 'Fatal', $file); + $violation3 = new Violation( + Violation::LEVEL_FATAL, + 'Fatal', + $file, + null, + new ViolationId('FatalId') + ); $report->addViolation($violation3); - $violation4 = new Violation(Violation::LEVEL_NOTICE, 'Notice2', $file2, 1, 11, 'Notice2Rule'); + $violation4 = new Violation( + Violation::LEVEL_NOTICE, + 'Notice2', + $file2, + 'Notice2Rule', + new ViolationId('NoticeId', null, 1) + ); $report->addViolation($violation4); - $violation5 = new Violation(Violation::LEVEL_FATAL, '\'"<&>"\'', $file3); + $violation5 = new Violation( + Violation::LEVEL_FATAL, + '\'"<&>"\'', + $file3, + null, + new ViolationId('FatalId') + ); $report->addViolation($violation5); $output = new BufferedOutput(OutputInterface::VERBOSITY_NORMAL, true); - $textFormatter->display($output, $report, $level); + $textFormatter->display($output, $report, $level, $debug); $text = $output->fetch(); static::assertStringContainsString($expected, $text); } /** - * @return iterable + * @return iterable */ public static function displayDataProvider(): iterable { @@ -59,13 +96,13 @@ public static function displayDataProvider(): iterable - + - + @@ -75,6 +112,7 @@ public static function displayDataProvider(): iterable __DIR__ ), null, + false, ]; yield [ @@ -94,6 +132,27 @@ public static function displayDataProvider(): iterable __DIR__ ), Report::MESSAGE_TYPE_ERROR, + false, + ]; + + yield [ + sprintf( + << + + + + + + + + + + EOD, + __DIR__ + ), + Report::MESSAGE_TYPE_ERROR, + true, ]; } } diff --git a/tests/Report/Reporter/GithubReporterTest.php b/tests/Report/Reporter/GithubReporterTest.php index b31ce34c..8f928cce 100644 --- a/tests/Report/Reporter/GithubReporterTest.php +++ b/tests/Report/Reporter/GithubReporterTest.php @@ -11,44 +11,69 @@ use TwigCsFixer\Report\Report; use TwigCsFixer\Report\Reporter\GithubReporter; use TwigCsFixer\Report\Violation; +use TwigCsFixer\Report\ViolationId; final class GithubReporterTest extends TestCase { /** * @dataProvider displayDataProvider */ - public function testDisplayErrors(string $expected, ?string $level): void + public function testDisplayErrors(string $expected, ?string $level, bool $debug): void { $textFormatter = new GithubReporter(); $file = __DIR__.'/Fixtures/file.twig'; $report = new Report([new SplFileInfo($file)]); - $violation0 = new Violation(Violation::LEVEL_NOTICE, 'Notice', $file, 1, 11, 'NoticeRule'); + $violation0 = new Violation( + Violation::LEVEL_NOTICE, + 'Notice', + $file, + 'Rule', + new ViolationId('NoticeId', null, 1) + ); $report->addViolation($violation0); - $violation1 = new Violation(Violation::LEVEL_WARNING, 'Warning', $file, 2, 22, 'WarningRule'); + $violation1 = new Violation( + Violation::LEVEL_WARNING, + 'Warning', + $file, + 'Rule', + new ViolationId('WarningId', null, 2, 22) + ); $report->addViolation($violation1); - $violation2 = new Violation(Violation::LEVEL_ERROR, 'Error', $file, 3, 33, 'ErrorRule'); + $violation2 = new Violation( + Violation::LEVEL_ERROR, + 'Error', + $file, + 'Rule', + new ViolationId('ErrorId', null, 3, 33) + ); $report->addViolation($violation2); - $violation3 = new Violation(Violation::LEVEL_FATAL, 'Fatal'."\n".'with new line', $file); + $violation3 = new Violation( + Violation::LEVEL_FATAL, + 'Fatal'."\n".'with new line', + $file, + 'Rule', + new ViolationId('FatalId') + ); $report->addViolation($violation3); $output = new BufferedOutput(OutputInterface::VERBOSITY_NORMAL, true); - $textFormatter->display($output, $report, $level); + $textFormatter->display($output, $report, $level, $debug); $text = $output->fetch(); static::assertStringContainsString($expected, $text); } /** - * @return iterable + * @return iterable */ public static function displayDataProvider(): iterable { yield [ sprintf( <<addViolation($violation0); - $violation1 = new Violation(Violation::LEVEL_WARNING, 'Warning', $file, 2, 22, 'WarningRule'); + $violation1 = new Violation( + Violation::LEVEL_WARNING, + 'Warning', + $file, + 'Rule', + new ViolationId('WarningId', null, 2, 22) + ); $report->addViolation($violation1); - $violation2 = new Violation(Violation::LEVEL_ERROR, 'Error', $file, 3, 33, 'ErrorRule'); + $violation2 = new Violation( + Violation::LEVEL_ERROR, + 'Error', + $file, + 'Rule', + new ViolationId('ErrorId', null, 3, 33) + ); $report->addViolation($violation2); - $violation3 = new Violation(Violation::LEVEL_FATAL, 'Fatal', $file); + $violation3 = new Violation( + Violation::LEVEL_FATAL, + 'Fatal', + $file, + 'Rule', + new ViolationId('FatalId') + ); $report->addViolation($violation3); - $violation4 = new Violation(Violation::LEVEL_NOTICE, 'Notice2', $file2, 1, 11, 'Notice2Rule'); + $violation4 = new Violation( + Violation::LEVEL_NOTICE, + 'Notice2', + $file2, + 'Rule', + new ViolationId('NoticeId', null, 1) + ); $report->addViolation($violation4); - $violation5 = new Violation(Violation::LEVEL_FATAL, '\'"<&>"\'', $file3); + $violation5 = new Violation( + Violation::LEVEL_FATAL, + '\'"<&>"\'', + $file3, + 'Rule', + new ViolationId('FatalId') + ); $report->addViolation($violation5); $output = new BufferedOutput(OutputInterface::VERBOSITY_NORMAL, true); - $textFormatter->display($output, $report, $level); + $textFormatter->display($output, $report, $level, $debug); $text = $output->fetch(); static::assertStringContainsString($expected, $text); } /** - * @return iterable + * @return iterable */ public static function displayDataProvider(): iterable { @@ -83,6 +120,39 @@ public static function displayDataProvider(): iterable __DIR__ ), null, + false, + ]; + yield [ + sprintf( + << + + + + + + + + + + + + + + + + + + + + + + + EOD, + __DIR__ + ), + null, + true, ]; } @@ -96,7 +166,7 @@ public function testDisplaySuccess(): void $report = new Report([new SplFileInfo($file), new SplFileInfo($file2), new SplFileInfo($file3)]); $output = new BufferedOutput(OutputInterface::VERBOSITY_NORMAL, true); - $textFormatter->display($output, $report); + $textFormatter->display($output, $report, null, false); $expected = << diff --git a/tests/Report/Reporter/NullReporterTest.php b/tests/Report/Reporter/NullReporterTest.php index d6243b98..11c9fa65 100644 --- a/tests/Report/Reporter/NullReporterTest.php +++ b/tests/Report/Reporter/NullReporterTest.php @@ -24,17 +24,17 @@ public function testDisplayErrors(?string $level): void $file = __DIR__.'/Fixtures/file.twig'; $report = new Report([new SplFileInfo($file)]); - $violation0 = new Violation(Violation::LEVEL_NOTICE, 'Notice', $file, 1); + $violation0 = new Violation(Violation::LEVEL_NOTICE, 'Notice', $file); $report->addViolation($violation0); - $violation1 = new Violation(Violation::LEVEL_WARNING, 'Warning', $file, 2); + $violation1 = new Violation(Violation::LEVEL_WARNING, 'Warning', $file); $report->addViolation($violation1); - $violation2 = new Violation(Violation::LEVEL_ERROR, 'Error', $file, 3); + $violation2 = new Violation(Violation::LEVEL_ERROR, 'Error', $file); $report->addViolation($violation2); $violation3 = new Violation(Violation::LEVEL_FATAL, 'Fatal', $file); $report->addViolation($violation3); $output = new BufferedOutput(OutputInterface::VERBOSITY_NORMAL, true); - $textFormatter->display($output, $report, $level); + $textFormatter->display($output, $report, $level, false); $text = $output->fetch(); static::assertSame('', $text); diff --git a/tests/Report/Reporter/TextReporterTest.php b/tests/Report/Reporter/TextReporterTest.php index e354bf1d..3c095b6e 100644 --- a/tests/Report/Reporter/TextReporterTest.php +++ b/tests/Report/Reporter/TextReporterTest.php @@ -11,30 +11,55 @@ use TwigCsFixer\Report\Report; use TwigCsFixer\Report\Reporter\TextReporter; use TwigCsFixer\Report\Violation; +use TwigCsFixer\Report\ViolationId; final class TextReporterTest extends TestCase { /** * @dataProvider displayDataProvider */ - public function testDisplayErrors(string $expected, ?string $level): void + public function testDisplayErrors(string $expected, ?string $level, bool $debug): void { $textFormatter = new TextReporter(); $file = __DIR__.'/Fixtures/file.twig'; $report = new Report([new SplFileInfo($file)]); - $violation0 = new Violation(Violation::LEVEL_NOTICE, 'Notice', $file, 1); + $violation0 = new Violation( + Violation::LEVEL_NOTICE, + 'Notice', + $file, + 'Rule', + new ViolationId('NoticeId', null, 1) + ); $report->addViolation($violation0); - $violation1 = new Violation(Violation::LEVEL_WARNING, 'Warning', $file, 2); + $violation1 = new Violation( + Violation::LEVEL_WARNING, + 'Warning', + $file, + 'Rule', + new ViolationId('WarningId', null, 2) + ); $report->addViolation($violation1); - $violation2 = new Violation(Violation::LEVEL_ERROR, 'Error', $file, 3); + $violation2 = new Violation( + Violation::LEVEL_ERROR, + 'Error', + $file, + 'Rule', + new ViolationId('ErrorId', null, 3) + ); $report->addViolation($violation2); - $violation3 = new Violation(Violation::LEVEL_FATAL, 'Fatal', $file); + $violation3 = new Violation( + Violation::LEVEL_FATAL, + 'Fatal', + $file, + 'Rule', + new ViolationId('FatalId') + ); $report->addViolation($violation3); $output = new BufferedOutput(OutputInterface::VERBOSITY_NORMAL, true); - $textFormatter->display($output, $report, $level); + $textFormatter->display($output, $report, $level, $debug); $text = $output->fetch(); static::assertStringContainsString($expected, $text); @@ -42,7 +67,7 @@ public function testDisplayErrors(string $expected, ?string $level): void } /** - * @return iterable + * @return iterable */ public static function displayDataProvider(): iterable { @@ -71,6 +96,7 @@ public static function displayDataProvider(): iterable __DIR__ ), null, + false, ]; yield [ @@ -89,6 +115,26 @@ public static function displayDataProvider(): iterable __DIR__ ), Report::MESSAGE_TYPE_ERROR, + false, + ]; + + yield [ + sprintf( + <<> | ErrorId:3\e[39m + 4 | + ------- ----------------------------------- + \e[33mFATAL\e[39m \e[31m>> | FatalId\e[39m + ------- ----------------------------------- + EOD, + __DIR__ + ), + Report::MESSAGE_TYPE_ERROR, + true, ]; } @@ -100,7 +146,7 @@ public function testDisplaySuccess(): void $report = new Report([new SplFileInfo($file)]); $output = new BufferedOutput(); - $textFormatter->display($output, $report); + $textFormatter->display($output, $report, null, false); $text = $output->fetch(); static::assertStringNotContainsString(sprintf('KO %s/Fixtures/file.twig', __DIR__), $text); @@ -115,11 +161,11 @@ public function testDisplayMultipleFiles(): void $file2 = __DIR__.'/Fixtures/file2.twig'; $report = new Report([new SplFileInfo($file), new SplFileInfo($file2)]); - $violation = new Violation(Violation::LEVEL_ERROR, 'Error', $file, 3); + $violation = new Violation(Violation::LEVEL_ERROR, 'Error', $file, null, new ViolationId(line: 3)); $report->addViolation($violation); $output = new BufferedOutput(); - $textFormatter->display($output, $report); + $textFormatter->display($output, $report, null, false); static::assertStringContainsString( sprintf( @@ -147,11 +193,11 @@ public function testDisplayNotFoundFile(): void $file = __DIR__.'/Fixtures/fileNotFound.twig'; $report = new Report([new SplFileInfo($file)]); - $violation = new Violation(Violation::LEVEL_ERROR, 'Error', $file, 1); + $violation = new Violation(Violation::LEVEL_ERROR, 'Error', $file, null, new ViolationId(line: 1)); $report->addViolation($violation); $output = new BufferedOutput(); - $textFormatter->display($output, $report); + $textFormatter->display($output, $report, null, false); static::assertStringContainsString( sprintf( @@ -177,11 +223,11 @@ public function testDisplayBlock(string $expected, int $level): void $file = __DIR__.'/Fixtures/file.twig'; $report = new Report([new SplFileInfo($file)]); - $violation = new Violation($level, 'Message', $file, 1); + $violation = new Violation($level, 'Message', $file, null, new ViolationId(line: 1)); $report->addViolation($violation); $output = new BufferedOutput(OutputInterface::VERBOSITY_NORMAL, true); - $textFormatter->display($output, $report); + $textFormatter->display($output, $report, null, false); $text = $output->fetch(); static::assertStringContainsString($expected, $text); diff --git a/tests/Report/ViolationIdTest.php b/tests/Report/ViolationIdTest.php new file mode 100644 index 00000000..0ad0eec4 --- /dev/null +++ b/tests/Report/ViolationIdTest.php @@ -0,0 +1,77 @@ +toString()); + + $fromString = ViolationId::fromString($expected); + static::assertTrue($fromString->match($violationId)); + static::assertTrue($violationId->match($fromString)); + } + + /** + * @return iterable + */ + public static function toStringDataProvider(): iterable + { + yield [null, null, null, null, '']; + yield ['short', null, null, null, 'short']; + yield ['short', 'id', null, null, 'short.id']; + yield ['short', null, 1, null, 'short:1']; + yield ['short', null, null, 1, 'short::1']; + yield ['short', 'id', 1, null, 'short.id:1']; + yield ['short', 'id', 1, 1, 'short.id:1:1']; + } + + /** + * @dataProvider matchDataProvider + */ + public function testMatch(string $string1, string $string2, bool $expected): void + { + $violationId1 = ViolationId::fromString($string1); + $violationId2 = ViolationId::fromString($string2); + static::assertSame($expected, $violationId1->match($violationId2)); + } + + /** + * @return iterable + */ + public static function matchDataProvider(): iterable + { + yield ['', 'short', true]; + yield ['', 'short.id:1:1', true]; + yield ['short', 'short', true]; + yield ['short', 'short.id:1:1', true]; + yield ['short.id', 'short.id:1:1', true]; + yield ['short.notId', 'short.id:1:1', false]; + yield ['short.id', 'short', false]; + yield ['SHORT.ID', 'short.id:1:1', true]; + yield ['short.id:2:1', 'short.id:1:1', false]; + yield ['short.id:1:2', 'short.id:1:1', false]; + yield ['short::1', 'short.id:1:1', true]; + yield ['short.id::1', 'short.id:1:1', true]; + } +} diff --git a/tests/Report/SniffViolationTest.php b/tests/Report/ViolationTest.php similarity index 88% rename from tests/Report/SniffViolationTest.php rename to tests/Report/ViolationTest.php index 25a7563f..e6c14a8b 100644 --- a/tests/Report/SniffViolationTest.php +++ b/tests/Report/ViolationTest.php @@ -6,18 +6,22 @@ use PHPUnit\Framework\TestCase; use TwigCsFixer\Report\Violation; +use TwigCsFixer\Report\ViolationId; -final class SniffViolationTest extends TestCase +final class ViolationTest extends TestCase { public function testGetters(): void { - $violation = new Violation(Violation::LEVEL_WARNING, 'message', 'filename', 42, 33, 'name'); + $violationId = new ViolationId('nameId', null, 42, 33); + $violation = new Violation(Violation::LEVEL_WARNING, 'message', 'filename', 'name', $violationId); + static::assertSame(Violation::LEVEL_WARNING, $violation->getLevel()); static::assertSame('message', $violation->getMessage()); static::assertSame('filename', $violation->getFilename()); static::assertSame(42, $violation->getLine()); static::assertSame(33, $violation->getLinePosition()); static::assertSame('name', $violation->getRuleName()); + static::assertSame($violationId, $violation->getIdentifier()); } /** diff --git a/tests/Rules/AbstractRuleTestCase.php b/tests/Rules/AbstractRuleTestCase.php index fc492844..c1567839 100644 --- a/tests/Rules/AbstractRuleTestCase.php +++ b/tests/Rules/AbstractRuleTestCase.php @@ -19,7 +19,7 @@ abstract class AbstractRuleTestCase extends TestCase { /** - * @param array> $expects + * @param array $expects */ protected function checkRule( RuleInterface $rule, @@ -51,8 +51,8 @@ protected function checkRule( $messages = $report->getFileViolations($filePath); - /** @var array> $messagePositions */ - $messagePositions = []; + /** @var array $messageIds */ + $messageIds = []; foreach ($messages as $message) { if (Violation::LEVEL_FATAL === $message->getLevel()) { $errorMessage = $message->getMessage(); @@ -64,10 +64,10 @@ protected function checkRule( static::fail($errorMessage); } - $messagePositions[] = [$message->getLine() ?? 0 => $message->getLinePosition()]; + $messageIds[] = $message->getIdentifier()?->toString(); } - static::assertSame($expects, $messagePositions); + static::assertSame($expects, $messageIds); } private function generateFilePath(): string diff --git a/tests/Rules/Delimiter/BlockNameSpacing/BlockNameSpacingTest.php b/tests/Rules/Delimiter/BlockNameSpacing/BlockNameSpacingTest.php index 8129e850..cdc65109 100644 --- a/tests/Rules/Delimiter/BlockNameSpacing/BlockNameSpacingTest.php +++ b/tests/Rules/Delimiter/BlockNameSpacing/BlockNameSpacingTest.php @@ -12,10 +12,10 @@ final class BlockNameSpacingTest extends AbstractRuleTestCase public function testRule(): void { $this->checkRule(new BlockNameSpacingRule(), [ - [1 => 5], - [1 => 5], - [3 => 3], - [3 => 3], + 'BlockNameSpacing.After:1:5', + 'BlockNameSpacing.Before:1:5', + 'BlockNameSpacing.After:3:3', + 'BlockNameSpacing.Before:3:3', ]); } } diff --git a/tests/Rules/Delimiter/DelimiterSpacing/DelimiterSpacingTest.php b/tests/Rules/Delimiter/DelimiterSpacing/DelimiterSpacingTest.php index dbc011d8..deb42700 100644 --- a/tests/Rules/Delimiter/DelimiterSpacing/DelimiterSpacingTest.php +++ b/tests/Rules/Delimiter/DelimiterSpacing/DelimiterSpacingTest.php @@ -12,10 +12,10 @@ final class DelimiterSpacingTest extends AbstractRuleTestCase public function testRule(): void { $this->checkRule(new DelimiterSpacingRule(), [ - [15 => 1], - [15 => 12], - [15 => 15], - [15 => 25], + 'DelimiterSpacing.After:15:1', + 'DelimiterSpacing.Before:15:12', + 'DelimiterSpacing.After:15:15', + 'DelimiterSpacing.Before:15:25', ]); } } diff --git a/tests/Rules/Fixtures/FakeRule.php b/tests/Rules/Fixtures/FakeRule.php index e5238b77..a2b1b0f6 100644 --- a/tests/Rules/Fixtures/FakeRule.php +++ b/tests/Rules/Fixtures/FakeRule.php @@ -6,9 +6,16 @@ use TwigCsFixer\Rules\AbstractRule; +/** + * This rule reports an error for the first token of every line. + */ class FakeRule extends AbstractRule { public function process(int $tokenPosition, array $tokens): void { + $token = $tokens[$tokenPosition]; + if (1 === $token->getPosition()) { + $this->addError('First token of the line', $token); + } } } diff --git a/tests/Rules/Fixtures/disable0.twig b/tests/Rules/Fixtures/disable0.twig new file mode 100644 index 00000000..e69de29b diff --git a/tests/Rules/Fixtures/disable1.twig b/tests/Rules/Fixtures/disable1.twig new file mode 100644 index 00000000..de37cd9f --- /dev/null +++ b/tests/Rules/Fixtures/disable1.twig @@ -0,0 +1,2 @@ +{# twig-cs-fixer-disable Fake.Error #} +This comment disable for the whole file. diff --git a/tests/Rules/Fixtures/disable2.twig b/tests/Rules/Fixtures/disable2.twig new file mode 100644 index 00000000..3eec7145 --- /dev/null +++ b/tests/Rules/Fixtures/disable2.twig @@ -0,0 +1,14 @@ +{# twig-cs-fixer-disable Fake.Error:1 #} This comment disable for the first line. +{# twig-cs-fixer-disable Fake.Error:2:1 #} This comment disable for the first token of the line. +{# twig-cs-fixer-disable Fake.Error:3:2 #} This comment disable for the second token of the line. => ERROR ON THIS LINE +{# twig-cs-fixer-disable-line Fake.Error #} This comment disable for the line. +{# twig-cs-fixer-disable-line Foo.Error, Fake.Error #} This comment disable for the line. +{# +twig-cs-fixer-disable-line Fake.Error #} This comment disable for the line with "{ #" => ERROR ON THIS LINE +{# twig-cs-fixer-disable-line Fake.Error::1 #} This comment disable for the first token of the line. +{# twig-cs-fixer-disable-line Fake.Error::2 #} This comment disable for the second token of the line. => ERROR ON THIS LINE +{# twig-cs-fixer-disable-line #} This comment disable for the line for every rule +{# this comment use twig-cs-fixer-disable Fake.Error #} But not at the start of the comment => ERROR ON THIS LINE +{# twig-cs-fixer-disable-next-line Fake.Error #} This comment disable for the first token of the next-line. => ERROR ON THIS LINE +{# twig-cs-fixer-disable-next-line Fake.Error +#} This comment disable for the next line. => ERROR ON THIS LINE diff --git a/tests/Rules/Operator/OperatorNameSpacing/OperatorNameSpacingTest.php b/tests/Rules/Operator/OperatorNameSpacing/OperatorNameSpacingTest.php index b02b61e1..4a3678df 100644 --- a/tests/Rules/Operator/OperatorNameSpacing/OperatorNameSpacingTest.php +++ b/tests/Rules/Operator/OperatorNameSpacing/OperatorNameSpacingTest.php @@ -12,9 +12,9 @@ final class OperatorNameSpacingTest extends AbstractRuleTestCase public function testRule(): void { $this->checkRule(new OperatorNameSpacingRule(), [ - [2 => 13], - [3 => 13], - [4 => 10], + 'OperatorNameSpacing.Error:2:13', + 'OperatorNameSpacing.Error:3:13', + 'OperatorNameSpacing.Error:4:10', ]); } } diff --git a/tests/Rules/Operator/OperatorSpacing/OperatorSpacingTest.php b/tests/Rules/Operator/OperatorSpacing/OperatorSpacingTest.php index ffcd3609..499f3f79 100644 --- a/tests/Rules/Operator/OperatorSpacing/OperatorSpacingTest.php +++ b/tests/Rules/Operator/OperatorSpacing/OperatorSpacingTest.php @@ -12,50 +12,50 @@ final class OperatorSpacingTest extends AbstractRuleTestCase public function testRule(): void { $this->checkRule(new OperatorSpacingRule(), [ - [1 => 5], - [1 => 5], - [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 => 4], - [12 => 11], - [12 => 11], - [13 => 11], - [13 => 11], - [14 => 7], - [14 => 7], - [15 => 7], - [15 => 7], - [19 => 5], - [19 => 5], - [20 => 5], - [20 => 5], - [22 => 6], - [33 => 10], - [33 => 10], - [35 => 13], - [35 => 13], - [36 => 13], - [36 => 13], - [37 => 13], - [37 => 13], + 'OperatorSpacing.After:1:5', + 'OperatorSpacing.Before:1:5', + 'OperatorSpacing.After:2:5', + 'OperatorSpacing.Before:2:5', + 'OperatorSpacing.After:3:5', + 'OperatorSpacing.Before:3:5', + 'OperatorSpacing.After:4:5', + 'OperatorSpacing.Before:4:5', + 'OperatorSpacing.After:5:5', + 'OperatorSpacing.Before:5:5', + 'OperatorSpacing.After:6:5', + 'OperatorSpacing.Before:6:5', + 'OperatorSpacing.After:7:5', + 'OperatorSpacing.Before:7:5', + 'OperatorSpacing.After:8:7', + 'OperatorSpacing.Before:8:7', + 'OperatorSpacing.After:9:10', + 'OperatorSpacing.Before:9:10', + 'OperatorSpacing.After:9:19', + 'OperatorSpacing.Before:9:19', + 'OperatorSpacing.After:10:5', + 'OperatorSpacing.Before:10:5', + 'OperatorSpacing.After:11:4', + 'OperatorSpacing.After:12:11', + 'OperatorSpacing.Before:12:11', + 'OperatorSpacing.After:13:11', + 'OperatorSpacing.Before:13:11', + 'OperatorSpacing.After:14:7', + 'OperatorSpacing.Before:14:7', + 'OperatorSpacing.After:15:7', + 'OperatorSpacing.Before:15:7', + 'OperatorSpacing.After:19:5', + 'OperatorSpacing.Before:19:5', + 'OperatorSpacing.After:20:5', + 'OperatorSpacing.Before:20:5', + 'OperatorSpacing.After:22:6', + 'OperatorSpacing.After:33:10', + 'OperatorSpacing.Before:33:10', + 'OperatorSpacing.After:35:13', + 'OperatorSpacing.Before:35:13', + 'OperatorSpacing.After:36:13', + 'OperatorSpacing.Before:36:13', + 'OperatorSpacing.After:37:13', + 'OperatorSpacing.Before:37:13', ]); } diff --git a/tests/Rules/Punctuation/PunctuationSpacing/PunctuationSpacingTest.php b/tests/Rules/Punctuation/PunctuationSpacing/PunctuationSpacingTest.php index 0356e1e8..26bab185 100644 --- a/tests/Rules/Punctuation/PunctuationSpacing/PunctuationSpacingTest.php +++ b/tests/Rules/Punctuation/PunctuationSpacing/PunctuationSpacingTest.php @@ -12,21 +12,21 @@ final class PunctuationSpacingTest extends AbstractRuleTestCase public function testRule(): void { $this->checkRule(new PunctuationSpacingRule(), [ - [3 => 4], - [3 => 10], - [4 => 4], - [4 => 10], - [4 => 16], - [4 => 22], - [4 => 28], - [5 => 12], - [5 => 16], - [5 => 20], - [5 => 24], - [6 => 6], - [6 => 6], - [7 => 12], - [7 => 15], + 'PunctuationSpacing.After:3:4', + 'PunctuationSpacing.Before:3:10', + 'PunctuationSpacing.After:4:4', + 'PunctuationSpacing.Before:4:10', + 'PunctuationSpacing.Before:4:16', + 'PunctuationSpacing.Before:4:22', + 'PunctuationSpacing.Before:4:28', + 'PunctuationSpacing.After:5:12', + 'PunctuationSpacing.Before:5:16', + 'PunctuationSpacing.Before:5:20', + 'PunctuationSpacing.Before:5:24', + 'PunctuationSpacing.After:6:6', + 'PunctuationSpacing.Before:6:6', + 'PunctuationSpacing.Before:7:12', + 'PunctuationSpacing.Before:7:15', ]); } } diff --git a/tests/Rules/Punctuation/TrailingCommaSingleLine/TrailingCommaSingleLineTest.php b/tests/Rules/Punctuation/TrailingCommaSingleLine/TrailingCommaSingleLineTest.php index f2eb4367..8c8a6a59 100644 --- a/tests/Rules/Punctuation/TrailingCommaSingleLine/TrailingCommaSingleLineTest.php +++ b/tests/Rules/Punctuation/TrailingCommaSingleLine/TrailingCommaSingleLineTest.php @@ -12,9 +12,9 @@ final class TrailingCommaSingleLineTest extends AbstractRuleTestCase public function testRule(): void { $this->checkRule(new TrailingCommaSingleLineRule(), [ - [2 => 9], - [4 => 13], - [6 => 12], + 'TrailingCommaSingleLine.Error:2:9', + 'TrailingCommaSingleLine.Error:4:13', + 'TrailingCommaSingleLine.Error:6:12', ]); } } diff --git a/tests/Rules/RuleTest.php b/tests/Rules/RuleTest.php index e48abb7c..dfdf2b48 100644 --- a/tests/Rules/RuleTest.php +++ b/tests/Rules/RuleTest.php @@ -6,10 +6,15 @@ use PHPUnit\Framework\TestCase; use SplFileInfo; +use TwigCsFixer\Environment\StubbedEnvironment; use TwigCsFixer\Report\Report; +use TwigCsFixer\Report\Violation; use TwigCsFixer\Rules\AbstractRule; +use TwigCsFixer\Ruleset\Ruleset; +use TwigCsFixer\Runner\Linter; use TwigCsFixer\Tests\Rules\Fixtures\FakeRule; use TwigCsFixer\Token\Token; +use TwigCsFixer\Token\Tokenizer; final class RuleTest extends TestCase { @@ -41,6 +46,7 @@ public function testRuleName(): void { $rule = new FakeRule(); static::assertSame(FakeRule::class, $rule->getName()); + static::assertSame('Fake', $rule->getShortName()); } public function testRuleWithReport2(): void @@ -105,4 +111,48 @@ protected function process(int $tokenPosition, array $tokens): void static::assertSame(0, $report->getTotalWarnings()); static::assertSame(2, $report->getTotalErrors()); } + + /** + * @param array $expectedLines + * + * @dataProvider ignoredViolationsDataProvider + */ + public function testIgnoredViolations(string $filePath, array $expectedLines): void + { + $env = new StubbedEnvironment(); + $tokenizer = new Tokenizer($env); + $linter = new Linter($env, $tokenizer); + $ruleset = new Ruleset(); + + $ruleset->addRule(new FakeRule()); + $report = $linter->run([new SplFileInfo($filePath)], $ruleset); + $messages = $report->getFileViolations($filePath); + + static::assertSame( + $expectedLines, + array_map( + static fn (Violation $violation) => $violation->getLine(), + $messages, + ), + ); + } + + /** + * @return iterable}> + */ + public static function ignoredViolationsDataProvider(): iterable + { + yield [ + __DIR__.'/Fixtures/disable0.twig', + [1], + ]; + yield [ + __DIR__.'/Fixtures/disable1.twig', + [], + ]; + yield [ + __DIR__.'/Fixtures/disable2.twig', + [3, 7, 9, 11, 12, 14], + ]; + } } diff --git a/tests/Rules/Whitespace/BlankEOF/BlankEOFTest.php b/tests/Rules/Whitespace/BlankEOF/BlankEOFTest.php index bc10b01a..e51cb17e 100644 --- a/tests/Rules/Whitespace/BlankEOF/BlankEOFTest.php +++ b/tests/Rules/Whitespace/BlankEOF/BlankEOFTest.php @@ -12,11 +12,11 @@ final class BlankEOFTest extends AbstractRuleTestCase public function testRule(): void { $this->checkRule(new BlankEOFRule(), [ - [4 => 1], + 'BlankEOF.Error:4:1', ]); $this->checkRule(new BlankEOFRule(), [ - [2 => 7], + 'BlankEOF.Error:2:7', ], __DIR__.'/BlankEOFTest2.twig'); } @@ -25,7 +25,7 @@ public function testRuleForEmptyFile(): void $this->checkRule(new BlankEOFRule(), [], __DIR__.'/BlankEOFTest.empty.twig'); $this->checkRule(new BlankEOFRule(), [ - [3 => 1], + 'BlankEOF.Error:3:1', ], __DIR__.'/BlankEOFTest.empty2.twig'); } } diff --git a/tests/Rules/Whitespace/EmptyLines/EmptyLinesTest.php b/tests/Rules/Whitespace/EmptyLines/EmptyLinesTest.php index 86f17abe..7beea122 100644 --- a/tests/Rules/Whitespace/EmptyLines/EmptyLinesTest.php +++ b/tests/Rules/Whitespace/EmptyLines/EmptyLinesTest.php @@ -12,9 +12,9 @@ final class EmptyLinesTest extends AbstractRuleTestCase public function testRule(): void { $this->checkRule(new EmptyLinesRule(), [ - [2 => 1], - [5 => 1], - [10 => 1], + 'EmptyLines.Error:2:1', + 'EmptyLines.Error:5:1', + 'EmptyLines.Error:10:1', ]); } } diff --git a/tests/Rules/Whitespace/Indent/IndentTest.php b/tests/Rules/Whitespace/Indent/IndentTest.php index e7b8bb35..ddd101ef 100644 --- a/tests/Rules/Whitespace/Indent/IndentTest.php +++ b/tests/Rules/Whitespace/Indent/IndentTest.php @@ -18,8 +18,8 @@ public function testConfiguration(): void public function testRule(): void { $this->checkRule(new IndentRule(), [ - [2 => 1], - [4 => 1], + 'Indent.Error:2:1', + 'Indent.Error:4:1', ]); } @@ -28,8 +28,8 @@ public function testRuleWithSpaceRatio(): void $this->checkRule( new IndentRule(2), [ - [2 => 1], - [4 => 1], + 'Indent.Error:2:1', + 'Indent.Error:4:1', ], __DIR__.'/IndentTest.twig', __DIR__.'/IndentTest.fixed2.twig', diff --git a/tests/Rules/Whitespace/TrailingSpace/TrailingSpaceTest.php b/tests/Rules/Whitespace/TrailingSpace/TrailingSpaceTest.php index 9c988c4d..d6bad6b3 100644 --- a/tests/Rules/Whitespace/TrailingSpace/TrailingSpaceTest.php +++ b/tests/Rules/Whitespace/TrailingSpace/TrailingSpaceTest.php @@ -12,16 +12,16 @@ final class TrailingSpaceTest extends AbstractRuleTestCase public function testRule(): void { $this->checkRule(new TrailingSpaceRule(), [ - [2 => 33], - [4 => 23], + 'TrailingSpace.Error:2:33', + 'TrailingSpace.Error:4:23', ]); } public function testRuleWithTab(): void { $this->checkRule(new TrailingSpaceRule(), [ - [2 => 32], - [4 => 21], + 'TrailingSpace.Error:2:32', + 'TrailingSpace.Error:4:21', ], __DIR__.'/TrailingSpaceTest.tab.twig'); } @@ -34,7 +34,7 @@ public function testRuleWithEmptyFile(): void ); $this->checkRule(new TrailingSpaceRule(), [ - [1 => 2], + 'TrailingSpace.Error:1:2', ], __DIR__.'/TrailingSpaceTest.empty2.twig'); } } diff --git a/tests/Runner/FixerTest.php b/tests/Runner/FixerTest.php index c44f053f..5bb59a08 100644 --- a/tests/Runner/FixerTest.php +++ b/tests/Runner/FixerTest.php @@ -36,7 +36,10 @@ public function testValidFile(): void { $tokenizer = $this->createMock(TokenizerInterface::class); $tokenizer->expects(static::once())->method('tokenize')->willReturn([ - new Token(Token::EOF_TYPE, 0, 0, 'TwigCsFixer'), + [ + new Token(Token::EOF_TYPE, 0, 0, 'TwigCsFixer'), + ], + [], ]); $ruleset = new Ruleset(); @@ -203,6 +206,40 @@ protected function process(int $tokenPosition, array $tokens): void static::assertSame('test 2 2', $fixer->fixFile('test test test', $ruleset)); } + public function testIgnoredViolations(): void + { + $tokenizer = new Tokenizer(new StubbedEnvironment()); + + $rule = new class () extends AbstractRule { + public function getShortName(): string + { + return 'Rule'; + } + + protected function process(int $tokenPosition, array $tokens): void + { + $fixer = $this->addFixableWarning('Error', $tokens[$tokenPosition]); + if (null !== $fixer) { + $fixer->replaceToken($tokenPosition, 'a'); + } + + $fixer = $this->addFixableError('Error', $tokens[$tokenPosition]); + if (null !== $fixer) { + $fixer->replaceToken($tokenPosition, 'b'); + } + } + }; + + $ruleset = new Ruleset(); + $ruleset->addRule($rule); + + $fixer = new Fixer($tokenizer); + + $content = '{# twig-cs-fixer-disable Rule #}'; + // The rule should produce an infinite loop but the comment disable it + static::assertSame($content, $fixer->fixFile('{# twig-cs-fixer-disable Rule #}', $ruleset)); + } + /** * @dataProvider addContentMethodsDataProvider */ diff --git a/tests/Runner/LinterTest.php b/tests/Runner/LinterTest.php index 20979915..9f60954e 100644 --- a/tests/Runner/LinterTest.php +++ b/tests/Runner/LinterTest.php @@ -94,12 +94,13 @@ public function testUntokenizableFilesAreReported(): void $call = 0; $tokenizer->method('tokenize')->willReturnCallback( static function () use (&$call): array { + /** @psalm-suppress RedundantCondition https://github.com/vimeo/psalm/issues/10513 */ if (0 === $call) { $call++; throw CannotTokenizeException::unknownError(); } - return []; + return [[], []]; } ); $ruleset = new Ruleset(); @@ -128,7 +129,6 @@ public function testUserDeprecationAreReported(): void { $deprecations = 0; set_error_handler(static function () use (&$deprecations): bool { - /** @psalm-suppress MixedOperand,MixedAssignment https://github.com/vimeo/psalm/issues/9155 */ $deprecations++; return true; @@ -142,7 +142,7 @@ public function testUserDeprecationAreReported(): void @trigger_error('Default'); trigger_error('User Deprecation', \E_USER_DEPRECATED); - return []; + return [[], []]; }); $ruleset = new Ruleset(); @@ -198,6 +198,7 @@ public function testBuggyFixesAreReported( $fixer = self::createStub(FixerInterface::class); $fixer->method('fixFile')->willReturnCallback( static function () use (&$call, $exception): string { + /** @psalm-suppress RedundantCondition https://github.com/vimeo/psalm/issues/10513 */ if (0 === $call) { $call++; throw $exception; diff --git a/tests/Token/Tokenizer/Fixtures/ignored_violations.twig b/tests/Token/Tokenizer/Fixtures/ignored_violations.twig new file mode 100644 index 00000000..858ef03a --- /dev/null +++ b/tests/Token/Tokenizer/Fixtures/ignored_violations.twig @@ -0,0 +1,5 @@ +{# twig-cs-fixer-disable Foo.Bar #} +{# Twig-CS-Fixer-disable Foo.BarInsensitive #} +{# twig-cs-fixer-disable-line Foo.Bar #} +{# twig-cs-fixer-disable-next-line Foo.Bar Bar.Foo #} +{# twig-cs-fixer-disable-next-line #} diff --git a/tests/Token/Tokenizer/TokenizerTest.php b/tests/Token/Tokenizer/TokenizerTest.php index 2a098fa4..7d10b0c3 100644 --- a/tests/Token/Tokenizer/TokenizerTest.php +++ b/tests/Token/Tokenizer/TokenizerTest.php @@ -8,6 +8,7 @@ use Twig\Source; use TwigCsFixer\Environment\StubbedEnvironment; use TwigCsFixer\Exception\CannotTokenizeException; +use TwigCsFixer\Report\ViolationId; use TwigCsFixer\Tests\TestHelper; use TwigCsFixer\Tests\Token\Tokenizer\Fixtures\CustomTwigExtension; use TwigCsFixer\Token\Token; @@ -27,9 +28,12 @@ public function testTokenize(): void static::assertEquals( [ - new Token(Token::TEXT_TYPE, 1, 1, $filePath, '
test
'), - new Token(Token::EOL_TYPE, 1, 16, $filePath, "\n"), - new Token(Token::EOF_TYPE, 2, 1, $filePath), + [ + new Token(Token::TEXT_TYPE, 1, 1, $filePath, '
test
'), + new Token(Token::EOL_TYPE, 1, 16, $filePath, "\n"), + new Token(Token::EOF_TYPE, 2, 1, $filePath), + ], + [], ], $tokenizer->tokenize($source) ); @@ -64,7 +68,33 @@ public function testTokenizeWithCustomOperators(): void new Token(Token::EOL_TYPE, 1, 36, $filePath, "\n"), new Token(Token::EOF_TYPE, 2, 1, $filePath), ], - $tokenizer->tokenize($source) + $tokenizer->tokenize($source)[0] + ); + } + + public function testTokenizeIgnoredViolations(): void + { + $filePath = __DIR__.'/Fixtures/ignored_violations.twig'; + $content = file_get_contents($filePath); + static::assertNotFalse($content); + + $env = new StubbedEnvironment([new CustomTwigExtension()]); + $tokenizer = new Tokenizer($env); + $source = new Source($content, $filePath); + + static::assertEquals( + [ + 'Foo.Bar', + 'Foo.BarInsensitive', + 'Foo.Bar:3', + 'Foo.Bar:5', + 'Bar.Foo:5', + ':6', + ], + array_map( + static fn (ViolationId $validationId) => $validationId->toString(), + $tokenizer->tokenize($source)[1] + ) ); } @@ -82,7 +112,7 @@ public function testTokenizeTypes(string $filePath, array $expectedTokenTypes): $tokenizer = new Tokenizer($env); $source = new Source($content, $filePath); - $tokens = $tokenizer->tokenize($source); + $tokens = $tokenizer->tokenize($source)[0]; $tokenValues = array_map(static fn (Token $token): string => $token->getValue(), $tokens);