diff --git a/.github/workflows/qa.yaml b/.github/workflows/qa.yaml index 2d585472..3aaf2f75 100644 --- a/.github/workflows/qa.yaml +++ b/.github/workflows/qa.yaml @@ -54,5 +54,5 @@ jobs: dependency-versions: highest composer-options: --prefer-dist --prefer-stable - - name: Run PHP Code Sniffer + - name: Run Psalm run: vendor/bin/psalm --no-progress --output-format=github --shepherd diff --git a/src/Command/TwigCsFixerCommand.php b/src/Command/TwigCsFixerCommand.php index b2f4e208..2642bda1 100644 --- a/src/Command/TwigCsFixerCommand.php +++ b/src/Command/TwigCsFixerCommand.php @@ -107,7 +107,7 @@ private function resolveConfig(InputInterface $input, OutputInterface $output): $config = $configResolver->resolveConfig( $input->getArgument('paths'), $input->getOption('config'), - $input->getOption('no-cache') + $input->getOption('no-cache'), ); $cacheFile = $config->getCacheFile(); @@ -130,7 +130,7 @@ private function runLinter(Config $config, InputInterface $input, OutputInterfac $report = $linter->run( $config->getFinder(), $config->getRuleset(), - $input->getOption('fix') ? new Fixer($tokenizer) : null + $input->getOption('fix') ? new Fixer($tokenizer) : null, ); $reporterFactory = new ReporterFactory(); diff --git a/src/Config/Config.php b/src/Config/Config.php index 40fa78aa..4ae9f535 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -39,6 +39,8 @@ final class Config */ private array $tokenParsers = []; + private bool $useOnlyFixableRules = true; + public function __construct(private string $name = 'Default') { $this->ruleset = new Ruleset(); @@ -143,4 +145,19 @@ public function getTokenParsers(): array { return $this->tokenParsers; } + + /** + * @return $this + */ + public function useOnlyFixableRules(bool $useOnlyFixableRules = true): self + { + $this->useOnlyFixableRules = $useOnlyFixableRules; + + return $this; + } + + public function getUseOnlyFixableRules(): bool + { + return $this->useOnlyFixableRules; + } } diff --git a/src/Config/ConfigResolver.php b/src/Config/ConfigResolver.php index aa5a30aa..aa115898 100644 --- a/src/Config/ConfigResolver.php +++ b/src/Config/ConfigResolver.php @@ -34,11 +34,14 @@ public function __construct(private string $workingDir) public function resolveConfig( array $paths = [], ?string $configPath = null, - bool $disableCache = false + bool $disableCache = false, ): Config { $config = $this->getConfig($configPath); $config->setFinder($this->resolveFinder($config->getFinder(), $paths)); + // Override ruleset with config + $config->getRuleset()->useOnlyFixableRules($config->getUseOnlyFixableRules()); + if ($disableCache) { $config->setCacheFile(null); $config->setCacheManager(null); @@ -46,7 +49,7 @@ public function resolveConfig( $config->setCacheManager($this->resolveCacheManager( $config->getCacheManager(), $config->getCacheFile(), - $config->getRuleset() + $config->getRuleset(), )); } @@ -132,7 +135,7 @@ private function resolveFinder(Finder $finder, array $paths): Finder private function resolveCacheManager( ?CacheManagerInterface $cacheManager, ?string $cacheFile, - Ruleset $ruleset + Ruleset $ruleset, ): ?CacheManagerInterface { if (null !== $cacheManager) { return $cacheManager; @@ -147,7 +150,7 @@ private function resolveCacheManager( Signature::fromRuleset( \PHP_VERSION, InstalledVersions::getReference(self::PACKAGE_NAME) ?? '0', - $ruleset + $ruleset, ) ); } diff --git a/src/Rules/AbstractFixableRule.php b/src/Rules/AbstractFixableRule.php new file mode 100644 index 00000000..7438d48d --- /dev/null +++ b/src/Rules/AbstractFixableRule.php @@ -0,0 +1,58 @@ +fixer = $fixer; + } + + public function fixFile(array $stream, FixerInterface $fixer, array $ignoredViolations = []): void + { + $this->init(null, $ignoredViolations, $fixer); + + foreach (array_keys($stream) as $index) { + $this->process($index, $stream); + } + } + + protected function addFixableWarning( + string $message, + Token $token, + ?string $messageId = null + ): ?FixerInterface { + $added = $this->addWarning($message, $token, $messageId); + if (!$added) { + return null; + } + + return $this->fixer; + } + + protected function addFixableError( + string $message, + Token $token, + ?string $messageId = null + ): ?FixerInterface { + $added = $this->addError($message, $token, $messageId); + if (!$added) { + return null; + } + + return $this->fixer; + } +} diff --git a/src/Rules/AbstractRule.php b/src/Rules/AbstractRule.php index 45b58a9c..d51b84c8 100644 --- a/src/Rules/AbstractRule.php +++ b/src/Rules/AbstractRule.php @@ -8,15 +8,12 @@ use TwigCsFixer\Report\Report; use TwigCsFixer\Report\Violation; use TwigCsFixer\Report\ViolationId; -use TwigCsFixer\Runner\FixerInterface; use TwigCsFixer\Token\Token; abstract class AbstractRule implements RuleInterface { private ?Report $report = null; - private ?FixerInterface $fixer = null; - /** * @var list */ @@ -36,24 +33,20 @@ public function getShortName(): string public function lintFile(array $stream, Report $report, array $ignoredViolations = []): void { - $this->report = $report; - $this->fixer = null; - $this->ignoredViolations = $ignoredViolations; + $this->init($report, $ignoredViolations); foreach (array_keys($stream) as $index) { $this->process($index, $stream); } } - public function fixFile(array $stream, FixerInterface $fixer, array $ignoredViolations = []): void + /** + * @param list $ignoredViolations + */ + protected function init(?Report $report, array $ignoredViolations = []): void { - $this->report = null; - $this->fixer = $fixer; + $this->report = $report; $this->ignoredViolations = $ignoredViolations; - - foreach (array_keys($stream) as $index) { - $this->process($index, $stream); - } } /** @@ -170,32 +163,6 @@ protected function addFileError(string $message, Token $token, ?string $messageI ); } - protected function addFixableWarning( - string $message, - Token $token, - ?string $messageId = null - ): ?FixerInterface { - $added = $this->addWarning($message, $token, $messageId); - if (!$added) { - return null; - } - - return $this->fixer; - } - - protected function addFixableError( - string $message, - Token $token, - ?string $messageId = null - ): ?FixerInterface { - $added = $this->addError($message, $token, $messageId); - if (!$added) { - return null; - } - - return $this->fixer; - } - private function addMessage( int $messageType, string $message, diff --git a/src/Rules/AbstractSpacingRule.php b/src/Rules/AbstractSpacingRule.php index 3e379689..6d4e3996 100644 --- a/src/Rules/AbstractSpacingRule.php +++ b/src/Rules/AbstractSpacingRule.php @@ -9,7 +9,7 @@ /** * Ensures there is one space before or after some tokens */ -abstract class AbstractSpacingRule extends AbstractRule +abstract class AbstractSpacingRule extends AbstractFixableRule { protected function process(int $tokenPosition, array $tokens): void { diff --git a/src/Rules/ConfigurableRuleInterface.php b/src/Rules/ConfigurableRuleInterface.php index 679c55d8..92480167 100644 --- a/src/Rules/ConfigurableRuleInterface.php +++ b/src/Rules/ConfigurableRuleInterface.php @@ -7,7 +7,7 @@ interface ConfigurableRuleInterface extends RuleInterface { /** - * @return array + * @return array */ public function getConfiguration(): array; } diff --git a/src/Rules/FixableRuleInterface.php b/src/Rules/FixableRuleInterface.php new file mode 100644 index 00000000..ce4d68e8 --- /dev/null +++ b/src/Rules/FixableRuleInterface.php @@ -0,0 +1,18 @@ + $stream + * @param list $ignoredViolations + */ + public function fixFile(array $stream, FixerInterface $fixer, array $ignoredViolations = []): void; +} diff --git a/src/Rules/Operator/OperatorNameSpacingRule.php b/src/Rules/Operator/OperatorNameSpacingRule.php index 2a36610f..e0b4cf0d 100644 --- a/src/Rules/Operator/OperatorNameSpacingRule.php +++ b/src/Rules/Operator/OperatorNameSpacingRule.php @@ -4,13 +4,13 @@ namespace TwigCsFixer\Rules\Operator; -use TwigCsFixer\Rules\AbstractRule; +use TwigCsFixer\Rules\AbstractFixableRule; use TwigCsFixer\Token\Token; /** * Ensures there is no consecutive spaces inside operator names. */ -final class OperatorNameSpacingRule extends AbstractRule +final class OperatorNameSpacingRule extends AbstractFixableRule { protected function process(int $tokenPosition, array $tokens): void { diff --git a/src/Rules/Punctuation/TrailingCommaSingleLineRule.php b/src/Rules/Punctuation/TrailingCommaSingleLineRule.php index 3717ea9b..8d94f920 100644 --- a/src/Rules/Punctuation/TrailingCommaSingleLineRule.php +++ b/src/Rules/Punctuation/TrailingCommaSingleLineRule.php @@ -4,14 +4,14 @@ namespace TwigCsFixer\Rules\Punctuation; -use TwigCsFixer\Rules\AbstractRule; +use TwigCsFixer\Rules\AbstractFixableRule; use TwigCsFixer\Token\Token; use Webmozart\Assert\Assert; /** * Ensures that single-line arrays, objects and argument lists do not have a trailing comma. */ -final class TrailingCommaSingleLineRule extends AbstractRule +final class TrailingCommaSingleLineRule extends AbstractFixableRule { protected function process(int $tokenPosition, array $tokens): void { diff --git a/src/Rules/RuleInterface.php b/src/Rules/RuleInterface.php index adb5d72c..dbf9579b 100644 --- a/src/Rules/RuleInterface.php +++ b/src/Rules/RuleInterface.php @@ -6,7 +6,6 @@ use TwigCsFixer\Report\Report; use TwigCsFixer\Report\ViolationId; -use TwigCsFixer\Runner\FixerInterface; use TwigCsFixer\Token\Token; interface RuleInterface @@ -18,10 +17,4 @@ interface RuleInterface * @param list $ignoredViolations */ public function lintFile(array $stream, Report $report, array $ignoredViolations = []): void; - - /** - * @param array $stream - * @param list $ignoredViolations - */ - public function fixFile(array $stream, FixerInterface $fixer, array $ignoredViolations = []): void; } diff --git a/src/Rules/Variable/VariableNameRule.php b/src/Rules/Variable/VariableNameRule.php index 4525692b..03f59938 100644 --- a/src/Rules/Variable/VariableNameRule.php +++ b/src/Rules/Variable/VariableNameRule.php @@ -8,6 +8,7 @@ use TwigCsFixer\Rules\AbstractRule; use TwigCsFixer\Rules\ConfigurableRuleInterface; use TwigCsFixer\Token\Token; +use Webmozart\Assert\Assert; final class VariableNameRule extends AbstractRule implements ConfigurableRuleInterface { @@ -38,10 +39,8 @@ protected function process(int $tokenPosition, array $tokens): void return; } - $nameTokenPosition = $this->findNext(Token::NAME_TYPE, $tokens, $tokenPosition + 1); - if (false === $nameTokenPosition) { - return; - } + $nameTokenPosition = $this->findNext(Token::NAME_TYPE, $tokens, $tokenPosition); + Assert::notFalse($nameTokenPosition, 'A BLOCK_NAME_TYPE set must be followed by a name'); $name = $tokens[$nameTokenPosition]->getValue(); $expected = match ($this->case) { diff --git a/src/Rules/Whitespace/BlankEOFRule.php b/src/Rules/Whitespace/BlankEOFRule.php index ccea4573..1b8bf8c1 100644 --- a/src/Rules/Whitespace/BlankEOFRule.php +++ b/src/Rules/Whitespace/BlankEOFRule.php @@ -4,13 +4,13 @@ namespace TwigCsFixer\Rules\Whitespace; -use TwigCsFixer\Rules\AbstractRule; +use TwigCsFixer\Rules\AbstractFixableRule; use TwigCsFixer\Token\Token; /** * Ensures that files end with one blank line. */ -final class BlankEOFRule extends AbstractRule +final class BlankEOFRule extends AbstractFixableRule { protected function process(int $tokenPosition, array $tokens): void { diff --git a/src/Rules/Whitespace/EmptyLinesRule.php b/src/Rules/Whitespace/EmptyLinesRule.php index 1ede69e9..1703f66d 100644 --- a/src/Rules/Whitespace/EmptyLinesRule.php +++ b/src/Rules/Whitespace/EmptyLinesRule.php @@ -4,14 +4,14 @@ namespace TwigCsFixer\Rules\Whitespace; -use TwigCsFixer\Rules\AbstractRule; +use TwigCsFixer\Rules\AbstractFixableRule; use TwigCsFixer\Token\Token; use Webmozart\Assert\Assert; /** * Ensures that 2 empty lines do not follow each other. */ -final class EmptyLinesRule extends AbstractRule +final class EmptyLinesRule extends AbstractFixableRule { protected function process(int $tokenPosition, array $tokens): void { diff --git a/src/Rules/Whitespace/IndentRule.php b/src/Rules/Whitespace/IndentRule.php index 0d88ff70..47e75690 100644 --- a/src/Rules/Whitespace/IndentRule.php +++ b/src/Rules/Whitespace/IndentRule.php @@ -4,14 +4,14 @@ namespace TwigCsFixer\Rules\Whitespace; -use TwigCsFixer\Rules\AbstractRule; +use TwigCsFixer\Rules\AbstractFixableRule; use TwigCsFixer\Rules\ConfigurableRuleInterface; use TwigCsFixer\Token\Token; /** * Ensures that files are not indented with tabs. */ -final class IndentRule extends AbstractRule implements ConfigurableRuleInterface +final class IndentRule extends AbstractFixableRule implements ConfigurableRuleInterface { public function __construct(private int $spaceRatio = 4) { diff --git a/src/Rules/Whitespace/TrailingSpaceRule.php b/src/Rules/Whitespace/TrailingSpaceRule.php index 17a97215..21ed6529 100644 --- a/src/Rules/Whitespace/TrailingSpaceRule.php +++ b/src/Rules/Whitespace/TrailingSpaceRule.php @@ -4,13 +4,13 @@ namespace TwigCsFixer\Rules\Whitespace; -use TwigCsFixer\Rules\AbstractRule; +use TwigCsFixer\Rules\AbstractFixableRule; use TwigCsFixer\Token\Token; /** * Ensures that files have no trailing spaces. */ -final class TrailingSpaceRule extends AbstractRule +final class TrailingSpaceRule extends AbstractFixableRule { protected function process(int $tokenPosition, array $tokens): void { diff --git a/src/Ruleset/Ruleset.php b/src/Ruleset/Ruleset.php index 92e02984..10e1768a 100644 --- a/src/Ruleset/Ruleset.php +++ b/src/Ruleset/Ruleset.php @@ -4,6 +4,7 @@ namespace TwigCsFixer\Ruleset; +use TwigCsFixer\Rules\FixableRuleInterface; use TwigCsFixer\Rules\RuleInterface; use TwigCsFixer\Standard\StandardInterface; @@ -17,11 +18,27 @@ final class Ruleset */ private array $rules = []; + private bool $useOnlyFixableRules = false; + + public function useOnlyFixableRules(bool $useOnlyFixableRules = true): self + { + $this->useOnlyFixableRules = $useOnlyFixableRules; + + return $this; + } + /** * @return array, RuleInterface> */ public function getRules(): array { + if ($this->useOnlyFixableRules) { + return array_filter( + $this->rules, + static fn (RuleInterface $rule): bool => $rule instanceof FixableRuleInterface, + ); + } + return $this->rules; } diff --git a/src/Runner/Fixer.php b/src/Runner/Fixer.php index 701316d1..6273961a 100644 --- a/src/Runner/Fixer.php +++ b/src/Runner/Fixer.php @@ -7,6 +7,7 @@ use BadMethodCallException; use Twig\Source; use TwigCsFixer\Exception\CannotFixFileException; +use TwigCsFixer\Rules\FixableRuleInterface; use TwigCsFixer\Ruleset\Ruleset; use TwigCsFixer\Token\Token; use TwigCsFixer\Token\TokenizerInterface; @@ -89,7 +90,9 @@ public function fixFile(string $content, Ruleset $ruleset): string $rules = $ruleset->getRules(); foreach ($rules as $rule) { - $rule->fixFile($stream, $this, $ignoredViolations); + if ($rule instanceof FixableRuleInterface) { + $rule->fixFile($stream, $this, $ignoredViolations); + } } $this->loops++; diff --git a/src/Token/Tokenizer.php b/src/Token/Tokenizer.php index ffdc0a50..b1dab986 100644 --- a/src/Token/Tokenizer.php +++ b/src/Token/Tokenizer.php @@ -12,7 +12,7 @@ use Webmozart\Assert\Assert; /** - * An override of Twig's Lexer to add whitespace and new line detection. + * An override of Twig\Lexer to add whitespace and new line detection. */ final class Tokenizer implements TokenizerInterface { diff --git a/tests/Rules/RuleTest.php b/tests/Rules/RuleTest.php index 669484b4..7f320fa8 100644 --- a/tests/Rules/RuleTest.php +++ b/tests/Rules/RuleTest.php @@ -9,7 +9,7 @@ use TwigCsFixer\Environment\StubbedEnvironment; use TwigCsFixer\Report\Report; use TwigCsFixer\Report\Violation; -use TwigCsFixer\Rules\AbstractRule; +use TwigCsFixer\Rules\AbstractFixableRule; use TwigCsFixer\Ruleset\Ruleset; use TwigCsFixer\Runner\Linter; use TwigCsFixer\Tests\Rules\Fixtures\FakeRule; @@ -22,7 +22,7 @@ public function testRuleWithReport(): void { $report = new Report([new SplFileInfo('fakeFile.html.twig')]); - $rule = new class () extends AbstractRule { + $rule = new class () extends AbstractFixableRule { protected function process(int $tokenPosition, array $tokens): void { $token = $tokens[$tokenPosition]; @@ -55,7 +55,7 @@ public function testRuleWithReport2(): void { $report = new Report([new SplFileInfo('fakeFile.html.twig')]); - $rule = new class () extends AbstractRule { + $rule = new class () extends AbstractFixableRule { protected function process(int $tokenPosition, array $tokens): void { $token = $tokens[$tokenPosition]; diff --git a/tests/Runner/FixerTest.php b/tests/Runner/FixerTest.php index 5bb59a08..f92c8d47 100644 --- a/tests/Runner/FixerTest.php +++ b/tests/Runner/FixerTest.php @@ -9,6 +9,7 @@ use TwigCsFixer\Environment\StubbedEnvironment; use TwigCsFixer\Exception\CannotFixFileException; use TwigCsFixer\Exception\CannotTokenizeException; +use TwigCsFixer\Rules\AbstractFixableRule; use TwigCsFixer\Rules\AbstractRule; use TwigCsFixer\Ruleset\Ruleset; use TwigCsFixer\Runner\Fixer; @@ -51,7 +52,7 @@ public function testReplaceToken(): void { $tokenizer = new Tokenizer(new StubbedEnvironment()); - $rule = new class () extends AbstractRule { + $rule = new class () extends AbstractFixableRule { private bool $isAlreadyExecuted = false; protected function process(int $tokenPosition, array $tokens): void @@ -97,7 +98,7 @@ public function testReplaceTokenIsDesignedAgainstInfiniteLoop(): void { $tokenizer = new Tokenizer(new StubbedEnvironment()); - $rule = new class () extends AbstractRule { + $rule = new class () extends AbstractFixableRule { protected function process(int $tokenPosition, array $tokens): void { $fixer = $this->addFixableError('Error', $tokens[$tokenPosition]); @@ -122,7 +123,7 @@ public function testReplaceTokenIsDesignedAgainstConflict(): void { $tokenizer = new Tokenizer(new StubbedEnvironment()); - $rule1 = new class () extends AbstractRule { + $rule1 = new class () extends AbstractFixableRule { private bool $isAlreadyExecuted = false; protected function process(int $tokenPosition, array $tokens): void @@ -140,7 +141,7 @@ protected function process(int $tokenPosition, array $tokens): void $fixer->replaceToken($tokenPosition, 'rule'); } }; - $rule2 = new class () extends AbstractRule { + $rule2 = new class () extends AbstractFixableRule { private int $error = 0; protected function process(int $tokenPosition, array $tokens): void @@ -172,7 +173,7 @@ protected function process(int $tokenPosition, array $tokens): void $fixer->endChangeSet(); } }; - $rule3 = new class () extends AbstractRule { + $rule3 = new class () extends AbstractFixableRule { private bool $isAlreadyExecuted = false; protected function process(int $tokenPosition, array $tokens): void @@ -210,7 +211,7 @@ public function testIgnoredViolations(): void { $tokenizer = new Tokenizer(new StubbedEnvironment()); - $rule = new class () extends AbstractRule { + $rule = new class () extends AbstractFixableRule { public function getShortName(): string { return 'Rule'; @@ -237,7 +238,7 @@ protected function process(int $tokenPosition, array $tokens): void $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)); + static::assertSame($content, $fixer->fixFile($content, $ruleset)); } /** @@ -247,7 +248,7 @@ public function testAddContentMethods(string $content, string $expected): void { $tokenizer = new Tokenizer(new StubbedEnvironment()); - $rule = new class () extends AbstractRule { + $rule = new class () extends AbstractFixableRule { private bool $isAlreadyExecuted = false; protected function process(int $tokenPosition, array $tokens): void @@ -308,4 +309,24 @@ public function testEndChangeSetException(): void $this->expectException(BadMethodCallException::class); $fixer->endChangeSet(); } + + public function testNonFixableRulesAreSkipped(): void + { + $tokenizer = new Tokenizer(new StubbedEnvironment()); + + $rule = new class () extends AbstractRule { + protected function process(int $tokenPosition, array $tokens): void + { + throw new \LogicException('Should be skipped'); + } + }; + + $ruleset = new Ruleset(); + $ruleset->addRule($rule); + + $fixer = new Fixer($tokenizer); + + $content = ''; + static::assertSame($content, $fixer->fixFile($content, $ruleset)); + } }