diff --git a/SymfonyCustom/Sniffs/Commenting/FunctionCommentSniff.php b/SymfonyCustom/Sniffs/Commenting/FunctionCommentSniff.php index 15bc6f7..1e6a81b 100644 --- a/SymfonyCustom/Sniffs/Commenting/FunctionCommentSniff.php +++ b/SymfonyCustom/Sniffs/Commenting/FunctionCommentSniff.php @@ -269,7 +269,7 @@ protected function processParams( * * @return bool True if the return does not return anything */ - protected function isMatchingReturn($tokens, $returnPos) + protected function isMatchingReturn(array $tokens, $returnPos) { do { $returnPos++; diff --git a/TwigCS/Command/TwigCSCommand.php b/TwigCS/Command/TwigCSCommand.php index d8b472b..fe4b044 100644 --- a/TwigCS/Command/TwigCSCommand.php +++ b/TwigCS/Command/TwigCSCommand.php @@ -8,12 +8,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Twig\Loader\ArrayLoader; use TwigCS\Config\Config; use TwigCS\Environment\StubbedEnvironment; use TwigCS\Linter; use TwigCS\Report\TextFormatter; -use TwigCS\Ruleset\RulesetFactory; +use TwigCS\Ruleset\Ruleset; use TwigCS\Token\Tokenizer; /** @@ -33,7 +32,7 @@ protected function configure() new InputOption( 'exclude', 'e', - InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Excludes, based on regex, paths of files and folders from parsing', ['vendor/'] ), @@ -41,8 +40,8 @@ protected function configure() 'level', 'l', InputOption::VALUE_OPTIONAL, - 'Allowed values are: warning, error', - 'warning' + 'Allowed values are notice, warning or error', + 'notice' ), new InputOption( 'working-dir', @@ -81,26 +80,24 @@ protected function execute(InputInterface $input, OutputInterface $output) 'workingDirectory' => $currentDir, ]); - $twig = new StubbedEnvironment(new ArrayLoader(), ['stub_tags' => $config->get('stub')]); - $linter = new Linter($twig, new Tokenizer($twig)); - $factory = new RulesetFactory(); - $reporter = new TextFormatter($input, $output); - $exitCode = 0; - // Get the rules to apply. - $ruleset = $factory->createStandardRuleset(); + $ruleset = new Ruleset(); + $ruleset->addStandard(); // Execute the linter. + $twig = new StubbedEnvironment(); + $linter = new Linter($twig, new Tokenizer($twig)); $report = $linter->run($config->findFiles(), $ruleset); // Format the output. + $reporter = new TextFormatter($input, $output); $reporter->display($report, $level); // Return a meaningful error code. if ($report->getTotalErrors()) { - $exitCode = 1; + return 1; } - return $exitCode; + return 0; } } diff --git a/TwigCS/Config/Config.php b/TwigCS/Config/Config.php index 00bca4f..b0648d3 100644 --- a/TwigCS/Config/Config.php +++ b/TwigCS/Config/Config.php @@ -19,7 +19,6 @@ class Config 'exclude' => [], 'pattern' => '*.twig', 'paths' => [], - 'stub' => [], 'workingDirectory' => '', ]; @@ -30,14 +29,12 @@ class Config */ protected $config; - public function __construct() + /** + * @param array $config + */ + public function __construct(array $config = []) { - $args = func_get_args(); - - $this->config = $this::$defaultConfig; - foreach ($args as $arg) { - $this->config = array_merge($this->config, $arg); - } + $this->config = array_merge($this::$defaultConfig, $config); } /** @@ -68,7 +65,7 @@ public function findFiles() $files->exclude($exclude); } - return $files; + return iterator_to_array($files, false); } /** @@ -80,7 +77,7 @@ public function findFiles() * * @throws Exception */ - public function get($key) + public function get(string $key) { if (!isset($this->config[$key])) { throw new Exception(sprintf('Configuration key "%s" does not exist', $key)); diff --git a/TwigCS/Environment/StubbedEnvironment.php b/TwigCS/Environment/StubbedEnvironment.php index 53596ae..04cbde4 100644 --- a/TwigCS/Environment/StubbedEnvironment.php +++ b/TwigCS/Environment/StubbedEnvironment.php @@ -2,7 +2,6 @@ namespace TwigCS\Environment; -use \Closure; use Symfony\Bridge\Twig\TokenParser\DumpTokenParser; use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser; use Symfony\Bridge\Twig\TokenParser\StopwatchTokenParser; @@ -10,12 +9,10 @@ use Symfony\Bridge\Twig\TokenParser\TransDefaultDomainTokenParser; use Symfony\Bridge\Twig\TokenParser\TransTokenParser; use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; -use TwigCS\Extension\SniffsExtension; -use TwigCS\Token\TokenParser; /** * Provide stubs for all filters, functions, tests and tags that are not defined in twig's core. @@ -37,18 +34,9 @@ class StubbedEnvironment extends Environment */ private $stubTests; - /** - * @var Closure - */ - private $stubCallable; - - /** - * @param LoaderInterface|null $loader - * @param array $options - */ - public function __construct(LoaderInterface $loader = null, $options = []) + public function __construct() { - parent::__construct($loader, $options); + parent::__construct(new ArrayLoader()); $this->addTokenParser(new DumpTokenParser()); $this->addTokenParser(new FormThemeTokenParser()); @@ -57,27 +45,9 @@ public function __construct(LoaderInterface $loader = null, $options = []) $this->addTokenParser(new TransDefaultDomainTokenParser()); $this->addTokenParser(new TransTokenParser()); - $this->stubCallable = function () { - /* This will be used as stub filter, function or test */ - }; - - $this->stubFilters = []; + $this->stubFilters = []; $this->stubFunctions = []; - - if (isset($options['stub_tags'])) { - foreach ($options['stub_tags'] as $tag) { - $this->addTokenParser(new TokenParser($tag)); - } - } - $this->stubTests = []; - if (isset($options['stub_tests'])) { - foreach ($options['stub_tests'] as $test) { - $this->stubTests[$test] = new TwigTest('stub', $this->stubCallable); - } - } - - $this->addExtension(new SniffsExtension()); } /** @@ -88,7 +58,7 @@ public function __construct(LoaderInterface $loader = null, $options = []) public function getFilter($name) { if (!isset($this->stubFilters[$name])) { - $this->stubFilters[$name] = new TwigFilter('stub', $this->stubCallable); + $this->stubFilters[$name] = new TwigFilter('stub'); } return $this->stubFilters[$name]; @@ -102,7 +72,7 @@ public function getFilter($name) public function getFunction($name) { if (!isset($this->stubFunctions[$name])) { - $this->stubFunctions[$name] = new TwigFunction('stub', $this->stubCallable); + $this->stubFunctions[$name] = new TwigFunction('stub'); } return $this->stubFunctions[$name]; @@ -111,19 +81,14 @@ public function getFunction($name) /** * @param string $name * - * @return false|TwigTest + * @return TwigTest */ public function getTest($name) { - $test = parent::getTest($name); - if ($test) { - return $test; - } - - if (isset($this->stubTests[$name])) { - return $this->stubTests[$name]; + if (!isset($this->stubTests[$name])) { + $this->stubTests[$name] = new TwigTest('stub'); } - return false; + return $this->stubTests[$name]; } } diff --git a/TwigCS/Extension/SniffsExtension.php b/TwigCS/Extension/SniffsExtension.php deleted file mode 100644 index 1947436..0000000 --- a/TwigCS/Extension/SniffsExtension.php +++ /dev/null @@ -1,64 +0,0 @@ -nodeVisitor = new SniffsNodeVisitor(); - } - - /** - * @return NodeVisitorInterface[] - */ - public function getNodeVisitors() - { - return [$this->nodeVisitor]; - } - - /** - * Register a sniff in the node visitor. - * - * @param PostParserSniffInterface $sniff - * - * @return self - */ - public function addSniff(PostParserSniffInterface $sniff) - { - $this->nodeVisitor->addSniff($sniff); - - return $this; - } - - /** - * Remove a sniff from the node visitor. - * - * @param PostParserSniffInterface $sniff - * - * @return self - */ - public function removeSniff(PostParserSniffInterface $sniff) - { - $this->nodeVisitor->removeSniff($sniff); - - return $this; - } -} diff --git a/TwigCS/Extension/SniffsNodeVisitor.php b/TwigCS/Extension/SniffsNodeVisitor.php deleted file mode 100644 index f9c6bab..0000000 --- a/TwigCS/Extension/SniffsNodeVisitor.php +++ /dev/null @@ -1,139 +0,0 @@ -sniffs = []; - $this->enabled = true; - } - - /** - * @return int - */ - public function getPriority() - { - return 0; - } - - /** - * Register a sniff to be executed. - * - * @param PostParserSniffInterface $sniff - */ - public function addSniff(PostParserSniffInterface $sniff) - { - $this->sniffs[] = $sniff; - } - - /** - * Remove a sniff from the node visitor. - * - * @param PostParserSniffInterface $toBeRemovedSniff - * - * @return self - */ - public function removeSniff(PostParserSniffInterface $toBeRemovedSniff) - { - foreach ($this->sniffs as $index => $sniff) { - if ($toBeRemovedSniff === $sniff) { - unset($this->sniffs[$index]); - } - } - - return $this; - } - - /** - * Get all registered sniffs. - * - * @return array - */ - public function getSniffs() - { - return $this->sniffs; - } - - /** - * Enable this node visitor. - * - * @return self - */ - public function enable() - { - $this->enabled = true; - - return $this; - } - - /** - * Disable this node visitor. - * - * @return self - */ - public function disable() - { - $this->enabled = false; - - return $this; - } - - /** - * @param Node $node - * @param Environment $env - * - * @return Node - */ - protected function doEnterNode(Node $node, Environment $env) - { - if (!$this->enabled) { - return $node; - } - - foreach ($this->getSniffs() as $sniff) { - $sniff->process($node, $env); - } - - return $node; - } - - /** - * @param Node $node - * @param Environment $env - * - * @return Node - */ - protected function doLeaveNode(Node $node, Environment $env) - { - return $node; - } -} diff --git a/TwigCS/Linter.php b/TwigCS/Linter.php index b762e74..63f2c8d 100644 --- a/TwigCS/Linter.php +++ b/TwigCS/Linter.php @@ -3,16 +3,12 @@ namespace TwigCS; use \Exception; -use \Traversable; use Twig\Environment; use Twig\Error\Error; use Twig\Source; -use TwigCS\Extension\SniffsExtension; use TwigCS\Report\Report; use TwigCS\Report\SniffViolation; use TwigCS\Ruleset\Ruleset; -use TwigCS\Sniff\PostParserSniffInterface; -use TwigCS\Sniff\PreParserSniffInterface; use TwigCS\Sniff\SniffInterface; use TwigCS\Token\Tokenizer; @@ -26,11 +22,6 @@ class Linter */ protected $env; - /** - * @var SniffsExtension - */ - protected $sniffsExtension; - /** * @var Tokenizer */ @@ -44,37 +35,27 @@ public function __construct(Environment $env, Tokenizer $tokenizer) { $this->env = $env; - $this->sniffsExtension = $this->env->getExtension('TwigCS\Extension\SniffsExtension'); $this->tokenizer = $tokenizer; } /** * Run the linter on the given $files against the given $ruleset. * - * @param array|string $files List of files to process. - * @param Ruleset $ruleset Set of rules to check. + * @param array $files List of files to process. + * @param Ruleset $ruleset Set of rules to check. * * @return Report an object with all violations and stats. * * @throws Exception */ - public function run($files, Ruleset $ruleset) + public function run(array $files, Ruleset $ruleset) { - if (!is_array($files) && !$files instanceof Traversable) { - $files = [$files]; - } - if (empty($files)) { throw new Exception('No files to process, provide at least one file to be linted'); } - // setUp $report = new Report(); foreach ($ruleset->getSniffs() as $sniff) { - if ($sniff instanceof PostParserSniffInterface) { - $this->sniffsExtension->addSniff($sniff); - } - $sniff->enable($report); } @@ -90,10 +71,6 @@ public function run($files, Ruleset $ruleset) // tearDown foreach ($ruleset->getSniffs() as $sniff) { - if ($sniff instanceof PostParserSniffInterface) { - $this->sniffsExtension->removeSniff($sniff); - } - $sniff->disable(); } @@ -109,7 +86,7 @@ public function run($files, Ruleset $ruleset) * * @return bool */ - public function processTemplate($file, $ruleset, $report) + public function processTemplate(string $file, Ruleset $ruleset, Report $report) { $twigSource = new Source(file_get_contents($file), $file, $file); @@ -136,7 +113,7 @@ public function processTemplate($file, $ruleset, $report) $sniffViolation = new SniffViolation( SniffInterface::MESSAGE_TYPE_ERROR, sprintf('Unable to tokenize file'), - (string) $file + $file ); $report->addMessage($sniffViolation); @@ -144,8 +121,8 @@ public function processTemplate($file, $ruleset, $report) return false; } - /** @var PreParserSniffInterface[] $sniffs */ - $sniffs = $ruleset->getSniffs(SniffInterface::TYPE_PRE_PARSER); + /** @var SniffInterface[] $sniffs */ + $sniffs = $ruleset->getSniffs(); foreach ($sniffs as $sniff) { foreach ($stream as $index => $token) { $sniff->process($token, $index, $stream); @@ -159,7 +136,7 @@ public function processTemplate($file, $ruleset, $report) * @param Report $report * @param string|null $file */ - protected function setErrorHandler(Report $report, $file = null) + protected function setErrorHandler(Report $report, string $file = null) { set_error_handler(function ($type, $message) use ($report, $file) { if (E_USER_DEPRECATED === $type) { diff --git a/TwigCS/Report/Report.php b/TwigCS/Report/Report.php index f507067..6b74751 100644 --- a/TwigCS/Report/Report.php +++ b/TwigCS/Report/Report.php @@ -75,7 +75,7 @@ public function addMessage(SniffViolation $sniffViolation) * * @return SniffViolation[] */ - public function getMessages($filters = []) + public function getMessages(array $filters = []) { if (empty($filters)) { // Return all messages, without filtering. @@ -100,7 +100,7 @@ public function getMessages($filters = []) /** * @param string $file */ - public function addFile($file) + public function addFile(string $file) { $this->files[] = $file; } diff --git a/TwigCS/Report/SniffViolation.php b/TwigCS/Report/SniffViolation.php index 29644eb..070f9a9 100644 --- a/TwigCS/Report/SniffViolation.php +++ b/TwigCS/Report/SniffViolation.php @@ -57,7 +57,7 @@ class SniffViolation * @param string $filename * @param int|null $line */ - public function __construct($level, $message, $filename, $line = null) + public function __construct(int $level, string $message, string $filename, int $line = null) { $this->level = $level; $this->message = $message; @@ -99,11 +99,11 @@ public function getLevelAsString() /** * Get the integer value for a given string $level. * - * @param int $level + * @param string $level * * @return int */ - public static function getLevelAsInt($level) + public static function getLevelAsInt(string $level) { switch (strtoupper($level)) { case 'NOTICE': @@ -143,7 +143,7 @@ public function getLine() */ public function getFilename() { - return (string) $this->filename; + return $this->filename; } /** @@ -153,7 +153,7 @@ public function getFilename() * * @return self */ - public function setLinePosition($linePosition) + public function setLinePosition(int $linePosition) { $this->linePosition = $linePosition; diff --git a/TwigCS/Report/TextFormatter.php b/TwigCS/Report/TextFormatter.php index ed765ab..079fcd8 100644 --- a/TwigCS/Report/TextFormatter.php +++ b/TwigCS/Report/TextFormatter.php @@ -29,7 +29,7 @@ class TextFormatter * @param InputInterface $input * @param OutputInterface $output */ - public function __construct($input, $output) + public function __construct(InputInterface $input, OutputInterface $output) { $this->io = new SymfonyStyle($input, $output); } @@ -38,7 +38,7 @@ public function __construct($input, $output) * @param Report $report * @param string|null $level */ - public function display(Report $report, $level = null) + public function display(Report $report, string $level = null) { foreach ($report->getFiles() as $file) { $fileMessages = $report->getMessages([ @@ -106,7 +106,7 @@ public function display(Report $report, $level = null) * * @return array */ - protected function getContext($template, $line, $context) + protected function getContext(string $template, int $line, int $context) { $lines = explode("\n", $template); diff --git a/TwigCS/Sniff/Standard/EnsureBlankAtEOFSniff.php b/TwigCS/Ruleset/Generic/BlankEOFSniff.php similarity index 82% rename from TwigCS/Sniff/Standard/EnsureBlankAtEOFSniff.php rename to TwigCS/Ruleset/Generic/BlankEOFSniff.php index cbbfc81..876ce32 100644 --- a/TwigCS/Sniff/Standard/EnsureBlankAtEOFSniff.php +++ b/TwigCS/Ruleset/Generic/BlankEOFSniff.php @@ -1,15 +1,15 @@ isTokenMatching($token, Token::EOF_TYPE)) { $i = 0; diff --git a/TwigCS/Ruleset/Generic/DelimiterSpacingSniff.php b/TwigCS/Ruleset/Generic/DelimiterSpacingSniff.php new file mode 100644 index 0000000..82671c1 --- /dev/null +++ b/TwigCS/Ruleset/Generic/DelimiterSpacingSniff.php @@ -0,0 +1,99 @@ +isTokenMatching($token, Token::VAR_START_TYPE) + || $this->isTokenMatching($token, Token::BLOCK_START_TYPE) + || $this->isTokenMatching($token, Token::COMMENT_START_TYPE) + ) { + $this->processStart($token, $tokenPosition, $tokens); + } + + if ($this->isTokenMatching($token, Token::VAR_END_TYPE) + || $this->isTokenMatching($token, Token::BLOCK_END_TYPE) + || $this->isTokenMatching($token, Token::COMMENT_END_TYPE) + ) { + $this->processEnd($token, $tokenPosition, $tokens); + } + + return $token; + } + + /** + * @param Token $token + * @param int $tokenPosition + * @param Token[] $tokens + * + * @throws Exception + */ + public function processStart(Token $token, $tokenPosition, $tokens) + { + // Ignore new line + if ($this->isTokenMatching($tokens[$tokenPosition + 1], Token::EOL_TYPE)) { + return; + } + + if ($this->isTokenMatching($tokens[$tokenPosition + 1], Token::WHITESPACE_TYPE)) { + $count = strlen($tokens[$tokenPosition + 1]->getValue()); + } else { + $count = 0; + } + + if (1 !== $count) { + $this->addMessage( + $this::MESSAGE_TYPE_ERROR, + sprintf('Expecting 1 whitespace after "%s"; found %d', $token->getValue(), $count), + $token + ); + } + } + + /** + * @param Token $token + * @param int $tokenPosition + * @param Token[] $tokens + * + * @throws Exception + */ + public function processEnd(Token $token, $tokenPosition, $tokens) + { + // Ignore new line + if ($this->isTokenMatching($tokens[$tokenPosition - 1], Token::EOL_TYPE)) { + return; + } + + if ($this->isTokenMatching($tokens[$tokenPosition - 1], Token::WHITESPACE_TYPE)) { + $count = strlen($tokens[$tokenPosition - 1]->getValue()); + } else { + $count = 0; + } + + if (1 !== $count) { + $this->addMessage( + $this::MESSAGE_TYPE_ERROR, + sprintf('Expecting 1 whitespace before "%s"; found %d', $token->getValue(), $count), + $token + ); + } + } +} diff --git a/TwigCS/Sniff/Standard/DisallowCommentedCodeSniff.php b/TwigCS/Ruleset/Generic/DisallowCommentedCodeSniff.php similarity index 83% rename from TwigCS/Sniff/Standard/DisallowCommentedCodeSniff.php rename to TwigCS/Ruleset/Generic/DisallowCommentedCodeSniff.php index 3845c3c..f9523d8 100644 --- a/TwigCS/Sniff/Standard/DisallowCommentedCodeSniff.php +++ b/TwigCS/Ruleset/Generic/DisallowCommentedCodeSniff.php @@ -1,9 +1,9 @@ isTokenMatching($token, Token::COMMENT_START_TYPE)) { $i = $tokenPosition; @@ -44,7 +44,7 @@ public function process(Token $token, $tokenPosition, $tokens) if ($found) { $this->addMessage( $this::MESSAGE_TYPE_WARNING, - 'Probable commented code found; keeping commented code is usually not advised', + 'Probable commented code found; keeping commented code is not advised', $token ); } diff --git a/TwigCS/Ruleset/Ruleset.php b/TwigCS/Ruleset/Ruleset.php index 485b044..9866dca 100644 --- a/TwigCS/Ruleset/Ruleset.php +++ b/TwigCS/Ruleset/Ruleset.php @@ -3,12 +3,12 @@ namespace TwigCS\Ruleset; use \Exception; -use TwigCS\Sniff\PostParserSniffInterface; -use TwigCS\Sniff\PreParserSniffInterface; +use \SplFileInfo; +use Symfony\Component\Finder\Finder; use TwigCS\Sniff\SniffInterface; /** - * Set of rules to be used by TwigCS and contains all sniffs (pre or post). + * Set of rules to be used by TwigCS and contains all sniffs. */ class Ruleset { @@ -23,31 +23,19 @@ public function __construct() } /** - * @param array|null $types - * * @return SniffInterface[] */ - public function getSniffs($types = null) + public function getSniffs() { - if (null === $types) { - $types = [SniffInterface::TYPE_PRE_PARSER, SniffInterface::TYPE_POST_PARSER]; - } - - if (null !== $types && !is_array($types)) { - $types = [$types]; - } - - return array_filter($this->sniffs, function (SniffInterface $sniff) use ($types) { - return in_array($sniff->getType(), $types); - }); + return $this->sniffs; } /** - * @param PreParserSniffInterface $sniff + * @param SniffInterface $sniff * * @return $this */ - public function addPreParserSniff(PreParserSniffInterface $sniff) + public function addSniff(SniffInterface $sniff) { $this->sniffs[get_class($sniff)] = $sniff; @@ -55,58 +43,29 @@ public function addPreParserSniff(PreParserSniffInterface $sniff) } /** - * @param PostParserSniffInterface $sniff + * Create a new set of rule. * - * @return $this - */ - public function addPostParserSniff(PostParserSniffInterface $sniff) - { - $this->sniffs[get_class($sniff)] = $sniff; - - return $this; - } - - /** - * @param SniffInterface $sniff + * @param string $standardName * - * @return $this + * @return Ruleset * * @throws Exception */ - public function addSniff(SniffInterface $sniff) + public function addStandard(string $standardName = 'Generic') { - if (SniffInterface::TYPE_PRE_PARSER === $sniff->getType()) { - // Store this type of sniff locally. - /** @var PreParserSniffInterface $sniff */ - $this->addPreParserSniff($sniff); - - return $this; - } - - if (SniffInterface::TYPE_POST_PARSER === $sniff->getType()) { - // Store this type of sniff locally. - /** @var PostParserSniffInterface $sniff */ - $this->addPostParserSniff($sniff); - - return $this; + try { + $finder = Finder::create()->in(__DIR__.'/'.$standardName)->files(); + } catch (Exception $e) { + throw new Exception(sprintf('The standard "%s" is not found.', $standardName)); } - throw new Exception(sprintf( - 'Unknown type of sniff "%s", expected one of: "%s"', - $sniff->getType(), - implode(', ', [SniffInterface::TYPE_PRE_PARSER, SniffInterface::TYPE_POST_PARSER]) - )); - } + /** @var SplFileInfo $file */ + foreach ($finder as $file) { + $class = __NAMESPACE__.'\\'.$standardName.'\\'.$file->getBasename('.php'); - /** - * @param string $sniffClass - * - * @return $this - */ - public function removeSniff($sniffClass) - { - if (isset($this->sniffs[$sniffClass])) { - unset($this->sniffs[$sniffClass]); + if (class_exists($class)) { + $this->addSniff(new $class()); + } } return $this; diff --git a/TwigCS/Ruleset/RulesetFactory.php b/TwigCS/Ruleset/RulesetFactory.php deleted file mode 100644 index b85ebfc..0000000 --- a/TwigCS/Ruleset/RulesetFactory.php +++ /dev/null @@ -1,35 +0,0 @@ -in(__DIR__.'/../Sniff/Standard')->files(); - - /** @var SplFileInfo $file */ - foreach ($finder as $file) { - $class = 'TwigCS\Sniff\Standard\\'.explode('.', $file->getFilename())[0]; - $ruleset->addSniff(new $class()); - } - - return $ruleset; - } -} diff --git a/TwigCS/Sniff/AbstractPostParserSniff.php b/TwigCS/Sniff/AbstractPostParserSniff.php deleted file mode 100644 index ab6f6b5..0000000 --- a/TwigCS/Sniff/AbstractPostParserSniff.php +++ /dev/null @@ -1,171 +0,0 @@ -getTemplateName($node), - $this->getTemplateLine($node) - ); - - $this->getReport()->addMessage($sniffViolation); - - return $this; - } - - /** - * @param Node $node - * - * @return int|null - */ - public function getTemplateLine(Node $node) - { - if (method_exists($node, 'getTemplateLine')) { - return $node->getTemplateLine(); - } - - if (method_exists($node, 'getLine')) { - return $node->getLine(); - } - - return null; - } - - /** - * @param Node $node - * - * @return string - */ - public function getTemplateName(Node $node) - { - if (method_exists($node, 'getTemplateName')) { - return $node->getTemplateName(); - } - - if (method_exists($node, 'getFilename')) { - return $node->getFilename(); - } - - if ($node->hasAttribute('filename')) { - return $node->getAttribute('filename'); - } - - return ''; - } - - /** - * @param Node $node - * @param string $type - * @param string|null $name - * - * @return bool - */ - public function isNodeMatching(Node $node, $type, $name = null) - { - $typeToClass = [ - 'filter' => function (Node $node, $type, $name) { - return $node instanceof FilterExpression - && $name === $node->getNode($type)->getAttribute('value'); - }, - 'function' => function (Node $node, $type, $name) { - return $node instanceof FunctionExpression - && $name === $node->getAttribute('name'); - }, - 'include' => function (Node $node, $type, $name) { - return $node instanceof IncludeNode; - }, - 'tag' => function (Node $node, $type, $name) { - return $node->getNodeTag() === $name; - }, - ]; - - if (!isset($typeToClass[$type])) { - return false; - } - - return $typeToClass[$type]($node, $type, $name); - } - - /** - * @param mixed $value - * - * @return string - */ - public function stringifyValue($value) - { - if (null === $value) { - return 'null'; - } - - if (is_bool($value)) { - return ($value) ? 'true' : 'false'; - } - - return (string) $value; - } - - /** - * @param Node $node - * - * @return mixed|string - */ - public function stringifyNode(Node $node) - { - $stringValue = ''; - - if ($node instanceof GetAttrExpression) { - return $node->getNode('node')->getAttribute('name').'.'.$this->stringifyNode($node->getNode('attribute')); - } - if ($node instanceof ConcatBinary) { - return $this->stringifyNode($node->getNode('left')).' ~ '.$this->stringifyNode($node->getNode('right')); - } - if ($node instanceof ConstantExpression) { - return $node->getAttribute('value'); - } - - return $stringValue; - } -} diff --git a/TwigCS/Sniff/AbstractPreParserSniff.php b/TwigCS/Sniff/AbstractPreParserSniff.php deleted file mode 100644 index 930d0fd..0000000 --- a/TwigCS/Sniff/AbstractPreParserSniff.php +++ /dev/null @@ -1,81 +0,0 @@ -getType() === $type - && (null === $value || (null !== $value && $token->getValue() === $value)); - } - - /** - * Adds a violation to the current report for the given token. - * - * @param int $messageType - * @param string $message - * @param Token $token - * - * @return self - * - * @throws Exception - */ - public function addMessage($messageType, $message, Token $token) - { - $sniffViolation = new SniffViolation( - $messageType, - $message, - $token->getFilename(), - $token->getLine() - ); - $sniffViolation->setLinePosition($token->getPosition()); - - $this->getReport()->addMessage($sniffViolation); - - return $this; - } - - /** - * @param Token $token - * - * @return string - */ - public function stringifyValue($token) - { - if ($token->getType() === Token::STRING_TYPE) { - return $token->getValue(); - } - - return '\''.$token->getValue().'\''; - } -} diff --git a/TwigCS/Sniff/AbstractSniff.php b/TwigCS/Sniff/AbstractSniff.php index 0b58109..beccd90 100644 --- a/TwigCS/Sniff/AbstractSniff.php +++ b/TwigCS/Sniff/AbstractSniff.php @@ -4,6 +4,8 @@ use \Exception; use TwigCS\Report\Report; +use TwigCS\Report\SniffViolation; +use TwigCS\Token\Token; /** * Base for all sniff. @@ -65,7 +67,56 @@ public function getReport() } /** + * Helper method to match a token of a given type and value. + * + * @param Token $token + * @param int $type + * @param string $value + * + * @return bool + */ + public function isTokenMatching(Token $token, int $type, string $value = null) + { + return $token->getType() === $type && (null === $value || $token->getValue() === $value); + } + + /** + * Adds a violation to the current report for the given token. + * + * @param int $messageType + * @param string $message + * @param Token $token + * + * @return self + * + * @throws Exception + */ + public function addMessage(int $messageType, string $message, Token $token) + { + $sniffViolation = new SniffViolation( + $messageType, + $message, + $token->getFilename(), + $token->getLine() + ); + $sniffViolation->setLinePosition($token->getPosition()); + + $this->getReport()->addMessage($sniffViolation); + + return $this; + } + + /** + * @param Token $token + * * @return string */ - abstract public function getType(); + public function stringifyValue(Token $token) + { + if ($token->getType() === Token::STRING_TYPE) { + return $token->getValue(); + } + + return '\''.$token->getValue().'\''; + } } diff --git a/TwigCS/Sniff/PostParserSniffInterface.php b/TwigCS/Sniff/PostParserSniffInterface.php deleted file mode 100644 index 5997e95..0000000 --- a/TwigCS/Sniff/PostParserSniffInterface.php +++ /dev/null @@ -1,18 +0,0 @@ -getMockBuilder(LoaderInterface::class)->getMock(); - $this->env = new StubbedEnvironment( - $twigLoaderInterface, - [ - 'stub_tags' => ['render', 'some_other_block', 'stylesheets'], - 'stub_tests' => ['some_test'], - ] - ); + $this->env = new StubbedEnvironment(); $this->lint = new Linter($this->env, new Tokenizer($this->env)); } /** - * @param string $filename + * Should call $this->checkGenericSniff(new Sniff(), [...]); + */ + abstract public function testSniff(); + + /** * @param SniffInterface $sniff * @param array $expects */ - protected function checkGenericSniff($filename, SniffInterface $sniff, array $expects) + protected function checkGenericSniff(SniffInterface $sniff, array $expects) { - $file = __DIR__.'/Fixtures/'.$filename; - $ruleset = new Ruleset(); try { + $class = new ReflectionClass(get_called_class()); + $file = __DIR__.'/Fixtures/'.$class->getShortName().'.twig'; + $ruleset->addSniff($sniff); - $report = $this->lint->run($file, $ruleset); + $report = $this->lint->run([$file], $ruleset); } catch (Exception $e) { $this->fail($e->getMessage()); diff --git a/TwigCS/Tests/Fixtures/ensureBlankAtEOF.twig b/TwigCS/Tests/Fixtures/BlankEOFTest.twig similarity index 100% rename from TwigCS/Tests/Fixtures/ensureBlankAtEOF.twig rename to TwigCS/Tests/Fixtures/BlankEOFTest.twig diff --git a/TwigCS/Tests/Fixtures/DelimiterSpacingTest.twig b/TwigCS/Tests/Fixtures/DelimiterSpacingTest.twig new file mode 100644 index 0000000..273ea3d --- /dev/null +++ b/TwigCS/Tests/Fixtures/DelimiterSpacingTest.twig @@ -0,0 +1,14 @@ +{{ foo }} +{# comment #} +{% if foo %}{% endif %} + +{{- foo -}} +{#- comment -#} +{%- if foo -%}{%- endif -%} + +{{ + shouldNotCareAboutNewLine +}} +{%-if foo -%}{%- endif-%} + +{{ foo({'bar': {'baz': 'shouldNotCareAboutDoubleHashes'}}) }} diff --git a/TwigCS/Tests/Fixtures/disallowCommentedCode.twig b/TwigCS/Tests/Fixtures/DisallowCommentedCodeTest.twig similarity index 100% rename from TwigCS/Tests/Fixtures/disallowCommentedCode.twig rename to TwigCS/Tests/Fixtures/DisallowCommentedCodeTest.twig diff --git a/TwigCS/Tests/Ruleset/Generic/BlankEOFTest.php b/TwigCS/Tests/Ruleset/Generic/BlankEOFTest.php new file mode 100644 index 0000000..0599d12 --- /dev/null +++ b/TwigCS/Tests/Ruleset/Generic/BlankEOFTest.php @@ -0,0 +1,19 @@ +checkGenericSniff(new BlankEOFSniff(), [ + [4, 1], + ]); + } +} diff --git a/TwigCS/Tests/Ruleset/Generic/DelimiterSpacingTest.php b/TwigCS/Tests/Ruleset/Generic/DelimiterSpacingTest.php new file mode 100644 index 0000000..43a16fc --- /dev/null +++ b/TwigCS/Tests/Ruleset/Generic/DelimiterSpacingTest.php @@ -0,0 +1,22 @@ +checkGenericSniff(new DelimiterSpacingSniff(), [ + [12, 1], + [12, 12], + [12, 15], + [12, 25], + ]); + } +} diff --git a/TwigCS/Tests/Ruleset/Generic/DisallowCommentedCodeTest.php b/TwigCS/Tests/Ruleset/Generic/DisallowCommentedCodeTest.php new file mode 100644 index 0000000..559f6dd --- /dev/null +++ b/TwigCS/Tests/Ruleset/Generic/DisallowCommentedCodeTest.php @@ -0,0 +1,19 @@ +checkGenericSniff(new DisallowCommentedCodeSniff(), [ + [2, 5], + ]); + } +} diff --git a/TwigCS/Tests/Sniff/DisallowCommentedCodeTest.php b/TwigCS/Tests/Sniff/DisallowCommentedCodeTest.php deleted file mode 100644 index 40e5b53..0000000 --- a/TwigCS/Tests/Sniff/DisallowCommentedCodeTest.php +++ /dev/null @@ -1,19 +0,0 @@ -checkGenericSniff('disallowCommentedCode.twig', new DisallowCommentedCodeSniff(), [ - [2, 5], - ]); - } -} diff --git a/TwigCS/Tests/Sniff/EnsureBlankAtEOFTest.php b/TwigCS/Tests/Sniff/EnsureBlankAtEOFTest.php deleted file mode 100644 index 7b8b7c4..0000000 --- a/TwigCS/Tests/Sniff/EnsureBlankAtEOFTest.php +++ /dev/null @@ -1,19 +0,0 @@ -checkGenericSniff('ensureBlankAtEOF.twig', new EnsureBlankAtEOFSniff(), [ - [4, 1], - ]); - } -} diff --git a/TwigCS/Token/Token.php b/TwigCS/Token/Token.php index 1c471af..7d980cb 100644 --- a/TwigCS/Token/Token.php +++ b/TwigCS/Token/Token.php @@ -63,8 +63,13 @@ class Token * @param string $filename * @param string|null $value */ - public function __construct($type, $line, $position, $filename, $value = null) - { + public function __construct( + int $type, + int $line, + int $position, + string $filename, + string $value = null + ) { $this->type = $type; $this->line = $line; $this->position = $position; diff --git a/TwigCS/Token/TokenParser.php b/TwigCS/Token/TokenParser.php index afe83a8..0114498 100644 --- a/TwigCS/Token/TokenParser.php +++ b/TwigCS/Token/TokenParser.php @@ -21,7 +21,7 @@ class TokenParser extends AbstractTokenParser /** * @param string $name */ - public function __construct($name) + public function __construct(string $name) { $this->name = $name; } diff --git a/TwigCS/Token/Tokenizer.php b/TwigCS/Token/Tokenizer.php index 68aa19d..4341041 100644 --- a/TwigCS/Token/Tokenizer.php +++ b/TwigCS/Token/Tokenizer.php @@ -190,7 +190,7 @@ protected function getState() /** * @param int $state */ - protected function pushState($state) + protected function pushState(int $state) { $this->state[] = $state; } @@ -209,7 +209,7 @@ protected function popState() /** * @param string $code */ - protected function preflightSource($code) + protected function preflightSource(string $code) { $tokenPositions = []; preg_match_all($this->regexes['lex_tokens_start'], $code, $tokenPositions, PREG_OFFSET_CAPTURE); @@ -231,7 +231,7 @@ protected function preflightSource($code) * * @return array|null */ - protected function getTokenPosition($offset = 0) + protected function getTokenPosition(int $offset = 0) { if (empty($this->tokenPositions) || !isset($this->tokenPositions[$this->currentPosition + $offset]) @@ -245,7 +245,7 @@ protected function getTokenPosition($offset = 0) /** * @param int $value */ - protected function moveCurrentPosition($value = 1) + protected function moveCurrentPosition(int $value = 1) { $this->currentPosition += $value; } @@ -253,7 +253,7 @@ protected function moveCurrentPosition($value = 1) /** * @param string $value */ - protected function moveCursor($value) + protected function moveCursor(string $value) { $this->cursor += strlen($value); $this->line += substr_count($value, "\n"); @@ -263,39 +263,12 @@ protected function moveCursor($value) * @param int $type * @param string|null $value */ - protected function pushToken($type, $value = null) + protected function pushToken(int $type, string $value = null) { $tokenPositionInLine = $this->cursor - strrpos(substr($this->code, 0, $this->cursor), PHP_EOL); $this->tokens[] = new Token($type, $this->line, $tokenPositionInLine, $this->filename, $value); } - /** - * @param int $endType - * @param string $endRegex - * - * @throws Exception - */ - protected function lex($endType, $endRegex) - { - preg_match($endRegex, $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor); - - if (!isset($match[0])) { - $this->lexExpression(); - } elseif ($match[0][1] === $this->cursor) { - $this->pushToken($endType, $match[0][0]); - $this->moveCursor($match[0][0]); - $this->moveCurrentPosition(); - $this->popState(); - } elseif ($this->getState() === self::STATE_COMMENT) { - // Parse as text until the end position. - $this->lexData($match[0][1]); - } else { - while ($this->cursor < $match[0][1]) { - $this->lexExpression(); - } - } - } - /** * @throws Exception */ @@ -356,7 +329,17 @@ protected function lexExpression() */ protected function lexBlock() { - $this->lex(Token::BLOCK_END_TYPE, $this->regexes['lex_block']); + $endRegex = $this->regexes['lex_block']; + preg_match($endRegex, $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor); + + if (!empty($this->brackets) || !isset($match[0])) { + $this->lexExpression(); + } else { + $this->pushToken(Token::BLOCK_END_TYPE, $match[0][0]); + $this->moveCursor($match[0][0]); + $this->moveCurrentPosition(); + $this->popState(); + } } /** @@ -364,7 +347,17 @@ protected function lexBlock() */ protected function lexVariable() { - $this->lex(Token::VAR_END_TYPE, $this->regexes['lex_variable']); + $endRegex = $this->regexes['lex_variable']; + preg_match($endRegex, $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor); + + if (!empty($this->brackets) || !isset($match[0])) { + $this->lexExpression(); + } else { + $this->pushToken(Token::VAR_END_TYPE, $match[0][0]); + $this->moveCursor($match[0][0]); + $this->moveCurrentPosition(); + $this->popState(); + } } /** @@ -372,13 +365,27 @@ protected function lexVariable() */ protected function lexComment() { - $this->lex(Token::COMMENT_END_TYPE, $this->regexes['lex_comment']); + $endRegex = $this->regexes['lex_comment']; + preg_match($endRegex, $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor); + + if (!isset($match[0])) { + throw new Exception('Unclosed comment'); + } + if ($match[0][1] === $this->cursor) { + $this->pushToken(Token::COMMENT_END_TYPE, $match[0][0]); + $this->moveCursor($match[0][0]); + $this->moveCurrentPosition(); + $this->popState(); + } else { + // Parse as text until the end position. + $this->lexData($match[0][1]); + } } /** * @param int $limit */ - protected function lexData($limit = 0) + protected function lexData(int $limit = 0) { $nextToken = $this->getTokenPosition(); if (0 === $limit && null !== $nextToken) { @@ -434,14 +441,30 @@ protected function lexStart() protected function lexTab() { - $this->pushToken(Token::TAB_TYPE); - $this->moveCursor($this->code[$this->cursor]); + $currentToken = $this->code[$this->cursor]; + $whitespace = ''; + + while (preg_match('/\t/', $currentToken)) { + $whitespace .= $currentToken; + $this->moveCursor($currentToken); + $currentToken = $this->code[$this->cursor]; + } + + $this->pushToken(Token::TAB_TYPE, $whitespace); } protected function lexWhitespace() { - $this->pushToken(Token::WHITESPACE_TYPE, $this->code[$this->cursor]); - $this->moveCursor($this->code[$this->cursor]); + $currentToken = $this->code[$this->cursor]; + $whitespace = ''; + + while (' ' === $currentToken) { + $whitespace .= $currentToken; + $this->moveCursor($currentToken); + $currentToken = $this->code[$this->cursor]; + } + + $this->pushToken(Token::WHITESPACE_TYPE, $whitespace); } protected function lexEOL() diff --git a/composer.lock b/composer.lock index c768e94..856185b 100644 --- a/composer.lock +++ b/composer.lock @@ -1,7 +1,7 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], "content-hash": "0e3e5b63c84c3a395b6bb4f2a7478780",