diff --git a/README.md b/README.md index 31d1aad..d0cee2d 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Tips: You can use multiple _exclude_ parameters. By default TwigCS will output all lines that have violations regardless of whether they match the severity level specified or not. If you only want to see violations that are greater than or equal to the severity level you've specified -you can use the `--display` option. For example. +you can use the `--display` option. For example. ```bash twigcs /path/to/views --severity error --display blocking @@ -86,7 +86,7 @@ You can create a class implementing `RulesetInterface` and supply it as a `--rul twigcs /path/to/views --ruleset \MyApp\TwigCsRuleset ``` -*Note:* `twigcs` needs to be used via composer and the ruleset class must be reachable via composer's autoloader for this feature to work. +_Note:_ `twigcs` needs to be used via composer and the ruleset class must be reachable via composer's autoloader for this feature to work. Also note that depending on your shell, you might need to escape backslashes in the fully qualified class name: ```bash @@ -148,6 +148,43 @@ If you explicitly supply a path to the CLI, it will be added to the list of lint twigcs ~/dirC # This will lint ~/dirA, ~/dirB and ~/dirC using the configuration file of the current directory. ``` +## Template resolution + +Using file based configuration, you can provide a way for twigcs to resolve template. This enables better unused variable/macro detection. Here's the +simplest example when you have only one directory of templates. + +```php +setTemplateResolver(new FileResolver(__DIR__)) + ->setRuleSet(FriendsOfTwig\Twigcs\Ruleset\Official::class) +; +``` + +Here is a more complex example that uses a chain resolver and a namespaced resolver to handle vendor templates: + +``` +setFinder($finder) + ->setTemplateResolver(new TemplateResolver\ChainResolver([ + new TemplateResolver\FileResolver(__DIR__ . '/templates'), + new TemplateResolver\NamespacedResolver([ + 'acme' => new TemplateResolver\FileResolver(__DIR__ . '/vendor/Acme/AcmeLib/templates') + ]), + ])) +; +``` + +This handles twig namespaces of the form `@acme/`. + ## Upgrading If you're upgrading from 3.x to 4.x or later, please read the [upgrade guide](doc/upgrade.md). diff --git a/src/Config/Config.php b/src/Config/Config.php index fd1f36b..3cd584a 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -3,24 +3,28 @@ namespace FriendsOfTwig\Twigcs\Config; use FriendsOfTwig\Twigcs\Ruleset\Official; +use FriendsOfTwig\Twigcs\TemplateResolver\NullResolver; +use FriendsOfTwig\Twigcs\TemplateResolver\TemplateResolverInterface; /** * Special thanks to https://github.com/c33s/twigcs/ which this feature was inspired from. */ class Config implements ConfigInterface { - private $name; - private $finders; - private $severity = 'warning'; - private $reporter = 'console'; - private $ruleset = Official::class; - private $specificRulesets = []; + private string $name; + private array $finders; + private ?TemplateResolverInterface $loader; + private string $severity = 'warning'; + private string $reporter = 'console'; + private string $ruleset = Official::class; + private array $specificRulesets = []; private $display = ConfigInterface::DISPLAY_ALL; - public function __construct($name = 'default') + public function __construct(string $name = 'default') { $this->name = $name; $this->finders = []; + $this->loader = new NullResolver(); } /** @@ -136,6 +140,24 @@ public function setRuleset(string $ruleSet): self return $this; } + /** + * {@inheritdoc} + */ + public function getTemplateResolver(): TemplateResolverInterface + { + return $this->loader; + } + + /** + * {@inheritdoc} + */ + public function setTemplateResolver(TemplateResolverInterface $loader): self + { + $this->loader = $loader; + + return $this; + } + public function getSpecificRulesets(): array { return $this->specificRulesets; diff --git a/src/Config/ConfigInterface.php b/src/Config/ConfigInterface.php index 2121a3a..613107a 100644 --- a/src/Config/ConfigInterface.php +++ b/src/Config/ConfigInterface.php @@ -2,6 +2,8 @@ namespace FriendsOfTwig\Twigcs\Config; +use FriendsOfTwig\Twigcs\TemplateResolver\TemplateResolverInterface; + /** * Special thanks to https://github.com/c33s/twigcs/ which this feature was inspired from. * @@ -62,4 +64,11 @@ public function getSpecificRuleSets(): array; * @return self */ public function setSpecificRuleSets(array $ruleSet); + + public function getTemplateResolver(): TemplateResolverInterface; + + /** + * @return self + */ + public function setTemplateResolver(TemplateResolverInterface $loader); } diff --git a/src/Config/ConfigResolver.php b/src/Config/ConfigResolver.php index 3357e89..7da973e 100644 --- a/src/Config/ConfigResolver.php +++ b/src/Config/ConfigResolver.php @@ -5,6 +5,7 @@ use FriendsOfTwig\Twigcs\Container; use FriendsOfTwig\Twigcs\Finder\TemplateFinder; use FriendsOfTwig\Twigcs\Ruleset\RulesetInterface; +use FriendsOfTwig\Twigcs\Ruleset\TemplateResolverAwareInterface; use FriendsOfTwig\Twigcs\Validator\Violation; use Symfony\Component\Filesystem\Filesystem; use function fnmatch; @@ -117,7 +118,13 @@ public function getRuleset(string $file) throw new \InvalidArgumentException('Ruleset class must implement '.RulesetInterface::class); } - return new $rulesetClassName($this->options['twig-version']); + $instance = new $rulesetClassName($this->options['twig-version']); + + if ($instance instanceof TemplateResolverAwareInterface) { + $instance->setTemplateResolver($this->config->getTemplateResolver()); + } + + return $instance; } /** diff --git a/src/Container.php b/src/Container.php index c00dee8..1c69f50 100644 --- a/src/Container.php +++ b/src/Container.php @@ -7,9 +7,9 @@ use FriendsOfTwig\Twigcs\Reporter\CsvReporter; use FriendsOfTwig\Twigcs\Reporter\EmacsReporter; use FriendsOfTwig\Twigcs\Reporter\GithubActionReporter; +use FriendsOfTwig\Twigcs\Reporter\JsonReporter; use FriendsOfTwig\Twigcs\Reporter\JUnitReporter; use FriendsOfTwig\Twigcs\Validator\Validator; -use FriendsOfTwig\Twigcs\Reporter\JsonReporter; class Container extends \ArrayObject { diff --git a/src/RegEngine/RulesetBuilder.php b/src/RegEngine/RulesetBuilder.php index 33851be..aa002b8 100644 --- a/src/RegEngine/RulesetBuilder.php +++ b/src/RegEngine/RulesetBuilder.php @@ -287,7 +287,7 @@ public function build(): array ['<➀embed➊$➋ignore missing➌with➍&➎only➁>', $this ->argTag() ->delegate('$', 'expr') - ->delegate('&', 'hash') + ->delegate('&', 'with') ->enforceSize('➊', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the source.') ->enforceSize('➋', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the "ignore missing".') ->enforceSize('➌', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the "with".') @@ -309,7 +309,7 @@ public function build(): array ], ['<➀embed➊$➌with➍&➎only➁>', $this ->argTag() - ->delegate('$', 'expr')->delegate('&', 'hash') + ->delegate('$', 'expr')->delegate('&', 'with') ->enforceSize('➊', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the source.') ->enforceSize('➌', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the "with".') ->enforceSize('➍', $this->config['from']['before_source'], 'There should be %quantity% space(s) after the "with".') @@ -317,7 +317,7 @@ public function build(): array ], ['<➀embed➊$➌with➍&➁>', $this ->argTag() - ->delegate('$', 'expr')->delegate('&', 'hash') + ->delegate('$', 'expr')->delegate('&', 'with') ->enforceSize('➊', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the source.') ->enforceSize('➌', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the "with".') ->enforceSize('➍', $this->config['from']['before_source'], 'There should be %quantity% space(s) after the "with".'), @@ -338,7 +338,7 @@ public function build(): array ['<➀include➊$➋ignore missing➌with➍&➎only➁>', $this ->argTag() ->delegate('$', 'expr') - ->delegate('&', 'hash') + ->delegate('&', 'with') ->enforceSize('➊', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the source.') ->enforceSize('➋', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the "ignore missing".') ->enforceSize('➌', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the "with".') @@ -361,7 +361,7 @@ public function build(): array ['<➀include➊$➌with➍&➎only➁>', $this ->argTag() ->delegate('$', 'expr') - ->delegate('&', 'hash') + ->delegate('&', 'with') ->enforceSize('➊', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the source.') ->enforceSize('➌', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the "with".') ->enforceSize('➍', $this->config['from']['before_source'], 'There should be %quantity% space(s) after the "with".') @@ -370,7 +370,7 @@ public function build(): array ['<➀include➊$➌with➍&➁>', $this ->argTag() ->delegate('$', 'expr') - ->delegate('&', 'hash') + ->delegate('&', 'with') ->enforceSize('➊', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the source.') ->enforceSize('➌', $this->config['from']['before_source'], 'There should be %quantity% space(s) before the "with".') ->enforceSize('➍', $this->config['from']['before_source'], 'There should be %quantity% space(s) after the "with".'), @@ -695,11 +695,16 @@ public function build(): array ], ]); + $with = $this->using(self::OP_VARS, [ + ['$', $this->handle()->noop()], + ]); + return [ 'expr' => array_merge($tags, $ops, $fallback), 'list' => $list, 'argsList' => $argsList, 'hash' => array_merge($hash, $hashFallback), + 'with' => array_merge($hash, $with), 'imports' => $imports, 'arrayOrSlice' => array_merge($slice, $array), ]; diff --git a/src/Rule/AbstractRule.php b/src/Rule/AbstractRule.php index ab17971..1246f6c 100644 --- a/src/Rule/AbstractRule.php +++ b/src/Rule/AbstractRule.php @@ -4,6 +4,7 @@ use FriendsOfTwig\Twigcs\TwigPort\Token; use FriendsOfTwig\Twigcs\TwigPort\TokenStream; +use FriendsOfTwig\Twigcs\Util\StreamNavigator; use FriendsOfTwig\Twigcs\Validator\Violation; /** @@ -19,10 +20,7 @@ abstract class AbstractRule */ protected $severity; - /** - * @param int $severity - */ - public function __construct($severity) + public function __construct(int $severity) { $this->severity = $severity; } @@ -34,109 +32,31 @@ public function collect(): array public function createViolation(string $filename, int $line, int $column, string $reason): Violation { - return new Violation($filename, $line, $column, $reason, $this->severity, get_called_class()); + return new Violation($filename, $line, $column, $reason, $this->severity, static::class); } - /** - * @param int $skip - * - * @return Token|null - */ - protected function getPreviousSignificantToken(TokenStream $tokens, $skip = 0) + protected function getPreviousSignificantToken(TokenStream $tokens, int $skip = 0): ?Token { - $i = 1; - $token = null; - - while ($token = $tokens->look(-$i)) { - if (!in_array($token->getType(), [Token::WHITESPACE_TYPE, Token::NEWLINE_TYPE], true)) { - if (0 === $skip) { - return $token; - } - - --$skip; - } - - ++$i; - } - - return null; + return StreamNavigator::getPreviousSignificantToken($tokens, $skip); } - /** - * @param int $skip - * - * @return Token|null - */ - protected function getNextSignificantToken(TokenStream $tokens, $skip = 0) + protected function getNextSignificantToken(TokenStream $tokens, int $skip = 0): ?Token { - $i = 1; - $token = null; - - while ($token = $tokens->look($i)) { - if (!in_array($token->getType(), [Token::WHITESPACE_TYPE, Token::NEWLINE_TYPE], true)) { - if (0 === $skip) { - return $token; - } - - --$skip; - } - - ++$i; - } - - return null; + return StreamNavigator::getNextSignificantToken($tokens, $skip); } protected function skipTo(TokenStream $tokens, int $tokenType, string $tokenValue = null) { - while (!$tokens->isEOF()) { - $continue = $tokens->getCurrent()->getType() !== $tokenType; - - if (null !== $tokenValue) { - $continue |= $tokens->getCurrent()->getValue() !== $tokenValue; - } - - if (!$continue) { - return; - } - - $tokens->next(); - } + return StreamNavigator::skipTo($tokens, $tokenType, $tokenValue); } protected function skipToOneOf(TokenStream $tokens, array $possibilities) { - while (!$tokens->isEOF()) { - foreach ($possibilities as $possibility) { - $tokenValue = $possibility['value'] ?? null; - $tokenType = $possibility['type'] ?? null; - $found = true; - - if ($tokenType) { - $found &= $tokenType === $tokens->getCurrent()->getType(); - } - - if ($tokenValue) { - $found &= $tokenValue === $tokens->getCurrent()->getValue(); - } - - if ($found) { - return; - } - } - - $tokens->next(); - } + return StreamNavigator::skipToOneOf($tokens, $possibilities); } protected function skip(TokenStream $tokens, int $amount) { - while (!$tokens->isEOF()) { - --$amount; - $tokens->next(); - if (0 === $amount) { - return; - } - } + return StreamNavigator::skip($tokens, $amount); } } diff --git a/src/Rule/UnusedMacro.php b/src/Rule/UnusedMacro.php index 3d102de..2b01d80 100644 --- a/src/Rule/UnusedMacro.php +++ b/src/Rule/UnusedMacro.php @@ -2,112 +2,32 @@ namespace FriendsOfTwig\Twigcs\Rule; -use FriendsOfTwig\Twigcs\Scope\Scope; -use FriendsOfTwig\Twigcs\TwigPort\Token; +use FriendsOfTwig\Twigcs\Scope\ScopeBuilder; +use FriendsOfTwig\Twigcs\TemplateResolver\NullResolver; +use FriendsOfTwig\Twigcs\TemplateResolver\TemplateResolverInterface; use FriendsOfTwig\Twigcs\TwigPort\TokenStream; class UnusedMacro extends AbstractRule implements RuleInterface { + public function __construct(int $severity, TemplateResolverInterface $loader = null) + { + $this->loader = $loader ?: new NullResolver(); + + parent::__construct($severity); + } + /** * {@inheritdoc} */ public function check(TokenStream $tokens) { - $scope = new Scope('file', 'root'); - $root = $scope; - - $violations = []; - - while (!$tokens->isEOF()) { - $token = $tokens->getCurrent(); - - if (Token::BLOCK_START_TYPE === $token->getType()) { - $blockType = $tokens->look(2)->getValue(); - - if (in_array($blockType, ['block', 'for', 'embed', 'macro'], true)) { - if ('block' === $blockType) { - $scope = $scope->spawn($blockType, $tokens->look(4)->getValue()); - } else { - $scope = $scope->spawn($blockType, 'noname'); - } - if ('macro' === $blockType) { - $scope->isolate(); - } - } + $builder = ScopeBuilder::createMacroScopeBuilder($this->loader); - if (in_array($blockType, ['endblock', 'endfor', 'endembed', 'endmacro'], true)) { - $scope = $scope->leave(); - } - } + $root = $builder->build($tokens); - if (Token::BLOCK_START_TYPE === $token->getType()) { - $blockType = $tokens->look(2)->getValue(); - - switch ($blockType) { - case 'from': - if ('as' === $tokens->look(10)->getValue()) { - $forward = 12; // Extracts token position from block of form {% import foo as bar %} - } else { - $forward = 8; // Extracts token position from block of form {% import foo %} - } - - $this->skip($tokens, $forward); - - // Handles single or multiple imports ( {% from "file.twig" import foo as bar, baz %} ) - while (in_array($tokens->getCurrent()->getType(), [Token::NAME_TYPE, Token::PUNCTUATION_TYPE, Token::WHITESPACE_TYPE], true)) { - $next = $tokens->getCurrent(); - if (Token::NAME_TYPE === $next->getType()) { - $scope->declare($next->getValue(), $next); - } - $tokens->next(); - } - break; - case 'import': - $this->skipTo($tokens, Token::NAME_TYPE, 'as'); - $this->skip($tokens, 2); - - // Handles single or multiple imports ( {% import foo as bar, baz %} ) - while (in_array($tokens->getCurrent()->getType(), [Token::NAME_TYPE, Token::PUNCTUATION_TYPE, Token::WHITESPACE_TYPE], true)) { - $next = $tokens->getCurrent(); - if (Token::NAME_TYPE === $next->getType()) { - $scope->declare($next->getValue(), $next); - } - $tokens->next(); - } - break; - case 'if': - case 'elseif': - case 'for': - $this->skip($tokens, 3); - break; - case 'set': - $this->skipToOneOf($tokens, [ - ['type' => Token::OPERATOR_TYPE, 'value' => '='], - ['type' => Token::BLOCK_END_TYPE], - ]); - break; - default: - $this->skipTo($tokens, Token::BLOCK_END_TYPE); - } - } elseif (Token::NAME_TYPE === $token->getType()) { - $previous = $this->getPreviousSignificantToken($tokens); - $next = $this->getNextSignificantToken($tokens); - - $isSubProperty = in_array($previous->getValue(), ['.', '|'], true); - $directUsage = in_array($next->getValue(), ['('], true); - $dotUsage = (Token::NAME_TYPE === $this->getNextSignificantToken($tokens, 1)->getType()) && in_array($this->getNextSignificantToken($tokens, 2)->getValue(), ['('], true); - - if (!$isSubProperty && ($directUsage || $dotUsage)) { - $scope->use($token->getValue()); - } - - $tokens->next(); - } else { - $tokens->next(); - } - } + $violations = []; - foreach ($root->flatten()->getUnusedDeclarations() as $declaration) { + foreach ($root->flatten()->getRootUnusedDeclarations() as $declaration) { $token = $declaration->getToken(); $violations[] = $this->createViolation( diff --git a/src/Rule/UnusedVariable.php b/src/Rule/UnusedVariable.php index cfcf175..f4e74ec 100644 --- a/src/Rule/UnusedVariable.php +++ b/src/Rule/UnusedVariable.php @@ -2,131 +2,32 @@ namespace FriendsOfTwig\Twigcs\Rule; -use FriendsOfTwig\Twigcs\Scope\Scope; -use FriendsOfTwig\Twigcs\TwigPort\Token; +use FriendsOfTwig\Twigcs\Scope\ScopeBuilder; +use FriendsOfTwig\Twigcs\TemplateResolver\NullResolver; +use FriendsOfTwig\Twigcs\TemplateResolver\TemplateResolverInterface; use FriendsOfTwig\Twigcs\TwigPort\TokenStream; class UnusedVariable extends AbstractRule implements RuleInterface { + public function __construct(int $severity, TemplateResolverInterface $loader = null) + { + $this->loader = $loader ?: new NullResolver(); + + parent::__construct($severity); + } + /** * {@inheritdoc} */ public function check(TokenStream $tokens) { - $scope = new Scope('file', 'root'); - $root = $scope; - - $violations = []; - - while (!$tokens->isEOF()) { - $token = $tokens->getCurrent(); - - if (Token::BLOCK_START_TYPE === $token->getType()) { - $blockType = $tokens->look(2)->getValue(); - - if (in_array($blockType, ['block', 'for', 'embed', 'macro'], true)) { - if ('block' === $blockType) { - $scope = $scope->spawn($blockType, $tokens->look(4)->getValue()); - } else { - $scope = $scope->spawn($blockType, 'noname'); - } - if ('macro' === $blockType) { - $scope->isolate(); - } - } - - if (in_array($blockType, ['endblock', 'endfor', 'endembed', 'endmacro'], true)) { - $scope = $scope->leave(); - } - } - - if (Token::BLOCK_START_TYPE === $token->getType()) { - $blockType = $tokens->look(2)->getValue(); + $builder = ScopeBuilder::createVariableScopeBuilder($this->loader); - switch ($blockType) { - case 'embed': - case 'include': - $templateName = $tokens->look(4); + $root = $builder->build($tokens); - if (Token::NAME_TYPE === $templateName->getType()) { // {% import varName ... %} - $scope->use($templateName->getValue()); - } - - if ('with' === $tokens->look(6)->getValue()) { - $this->skip($tokens, 8); - } else { - $this->skipTo($tokens, Token::BLOCK_END_TYPE); - } - break; - case 'from': - $from = $tokens->look(4); - - if (Token::NAME_TYPE === $from->getType()) { // {% from varName import ... %} - $scope->use($from->getValue()); - } - $this->skipTo($tokens, Token::BLOCK_END_TYPE); - break; - case 'set': - $scope->declare($tokens->look(4)->getValue(), $tokens->look(4)); - $this->skipToOneOf($tokens, [ - ['type' => Token::OPERATOR_TYPE, 'value' => '='], - ['type' => Token::BLOCK_END_TYPE], - ]); - break; - case 'if': - case 'elseif': - $this->skip($tokens, 3); - break; - case 'for': - $scope->declare($tokens->look(4)->getValue(), $tokens->look(4)); - $this->skip($tokens, 5); - break; - default: - $this->skipTo($tokens, Token::BLOCK_END_TYPE); - } - } elseif (Token::NAME_TYPE === $token->getType()) { - $previous = $this->getPreviousSignificantToken($tokens); - $next = $this->getNextSignificantToken($tokens); - - $isHashKey = in_array($previous->getValue(), [',', '{'], true) && ':' === $next->getValue(); - $isFilter = '|' === $previous->getValue(); - $isProperty = '.' === $previous->getValue(); - $isFunctionCall = '(' === $next->getValue(); - $isTest = ('is' === $previous->getValue()) || ('is not' === $previous->getValue()); - $isReserved = in_array($token->getValue(), ['null', 'true', 'false'], true); - - if ($isFunctionCall && 'block' === $token->getValue()) { - $i = 0; - $blockNameToken = $tokens->look($i); - // Scans for the name of the nested block. - while (Token::BLOCK_END_TYPE !== $blockNameToken->getType() && Token::STRING_TYPE !== $blockNameToken->getType()) { - $blockNameToken = $tokens->look($i); - ++$i; - } - $scope->referenceBlock($blockNameToken->getValue()); - } - - if (!$isHashKey && !$isFilter && !$isProperty && !$isFunctionCall && !$isTest && !$isReserved) { - $scope->use($token->getValue()); - } - - $tokens->next(); - } elseif (Token::COMMENT_TYPE === $token->getType()) { - if (0 === strpos($token->getValue(), 'twigcs use-var ')) { - $names = explode(',', str_replace('twigcs use-var ', '', $token->getValue())); - - foreach ($names as $name) { - $scope->use(trim($name)); - } - } - - $tokens->next(); - } else { - $tokens->next(); - } - } + $violations = []; - foreach ($root->flatten()->getUnusedDeclarations() as $declaration) { + foreach ($root->flatten()->getRootUnusedDeclarations() as $declaration) { $token = $declaration->getToken(); $violations[] = $this->createViolation( diff --git a/src/Ruleset/Official.php b/src/Ruleset/Official.php index 361563e..3fd8157 100644 --- a/src/Ruleset/Official.php +++ b/src/Ruleset/Official.php @@ -5,6 +5,8 @@ use FriendsOfTwig\Twigcs\RegEngine\RulesetBuilder; use FriendsOfTwig\Twigcs\RegEngine\RulesetConfigurator; use FriendsOfTwig\Twigcs\Rule; +use FriendsOfTwig\Twigcs\TemplateResolver\NullResolver; +use FriendsOfTwig\Twigcs\TemplateResolver\TemplateResolverInterface; use FriendsOfTwig\Twigcs\Validator\Violation; /** @@ -12,13 +14,16 @@ * * @author Tristan Maindron */ -class Official implements RulesetInterface +class Official implements RulesetInterface, TemplateResolverAwareInterface { private $twigMajorVersion; + private TemplateResolverInterface $resolver; + public function __construct(int $twigMajorVersion) { $this->twigMajorVersion = $twigMajorVersion; + $this->resolver = new NullResolver(); } /** @@ -34,8 +39,13 @@ public function getRules() new Rule\LowerCaseVariable(Violation::SEVERITY_ERROR), new Rule\RegEngineRule(Violation::SEVERITY_ERROR, $builder->build()), new Rule\TrailingSpace(Violation::SEVERITY_ERROR), - new Rule\UnusedMacro(Violation::SEVERITY_WARNING), - new Rule\UnusedVariable(Violation::SEVERITY_WARNING), + new Rule\UnusedMacro(Violation::SEVERITY_WARNING, $this->resolver), + new Rule\UnusedVariable(Violation::SEVERITY_WARNING, $this->resolver), ]; } + + public function setTemplateResolver(TemplateResolverInterface $resolver) + { + $this->resolver = $resolver; + } } diff --git a/src/Ruleset/RulesetInterface.php b/src/Ruleset/RulesetInterface.php index 453668c..4b658d2 100644 --- a/src/Ruleset/RulesetInterface.php +++ b/src/Ruleset/RulesetInterface.php @@ -4,6 +4,9 @@ use FriendsOfTwig\Twigcs\Rule\RuleInterface; +/** + * @author Tristan Maindron + */ interface RulesetInterface { public function __construct(int $twigMajorVersion); diff --git a/src/Ruleset/TemplateResolverAwareInterface.php b/src/Ruleset/TemplateResolverAwareInterface.php new file mode 100644 index 0000000..5b96b2e --- /dev/null +++ b/src/Ruleset/TemplateResolverAwareInterface.php @@ -0,0 +1,13 @@ + + */ +interface TemplateResolverAwareInterface +{ + public function setTemplateResolver(TemplateResolverInterface $resolver); +} diff --git a/src/Scope/Declaration.php b/src/Scope/Declaration.php index 4dd0e3d..f0b7946 100644 --- a/src/Scope/Declaration.php +++ b/src/Scope/Declaration.php @@ -16,10 +16,16 @@ class Declaration */ private $token; - public function __construct(string $name, Token $token) + /** + * @var Scope + */ + private $origin; + + public function __construct(string $name, Token $token, Scope $origin) { $this->name = $name; $this->token = $token; + $this->origin = $origin; } public function getName(): string @@ -32,6 +38,11 @@ public function getToken(): Token return $this->token; } + public function getOrigin(): Scope + { + return $this->origin; + } + public function __toString() { return sprintf('declaration of %s', $this->name); diff --git a/src/Scope/FlattenedScope.php b/src/Scope/FlattenedScope.php index 7a43086..b920c81 100644 --- a/src/Scope/FlattenedScope.php +++ b/src/Scope/FlattenedScope.php @@ -76,6 +76,23 @@ public function getQueue(): array return $this->queue; } + public function getRootUnusedDeclarations(): array + { + $unused = $this->getUnusedDeclarations(); + + $unused = array_filter($unused, function (Declaration $declaration) { + $scope = $declaration->getOrigin(); + + while ('file' !== $scope->getType() && $scope->getParent()) { + $scope = $scope->getParent(); + } + + return 'root' === $scope->getName(); + }); + + return $unused; + } + public function getUnusedDeclarations(): array { $unused = []; diff --git a/src/Scope/Scope.php b/src/Scope/Scope.php index 067148a..09310f1 100644 --- a/src/Scope/Scope.php +++ b/src/Scope/Scope.php @@ -31,6 +31,11 @@ class Scope */ private $isolated; + /** + * @var Scope|null + */ + private $extends; + public function __construct(string $type, string $name) { $this->type = $type; @@ -71,6 +76,21 @@ public function spawn(string $type, string $name): self return $scope; } + public function nest(self $scope): self + { + $scope->parent = $this; + $this->queue[] = $scope; + + return $scope; + } + + public function extends(self $scope): self + { + $this->extends = $scope; + + return $scope; + } + public function leave(): self { return $this->parent ?? $this; @@ -78,7 +98,7 @@ public function leave(): self public function declare(string $name, Token $token) { - $this->queue[] = new Declaration($name, $token); + $this->queue[] = new Declaration($name, $token, $this); } public function use(string $name) @@ -93,9 +113,18 @@ public function referenceBlock(string $blockName) public function getQueue(): array { + if ($this->extends) { + return [...$this->queue, $this->extends]; + } + return $this->queue; } + public function getParent(): self + { + return $this->parent; + } + public function flatten(): FlattenedScope { return new FlattenedScope($this); diff --git a/src/Scope/ScopeBuilder.php b/src/Scope/ScopeBuilder.php new file mode 100644 index 0000000..977c509 --- /dev/null +++ b/src/Scope/ScopeBuilder.php @@ -0,0 +1,253 @@ + + */ +class ScopeBuilder +{ + const MODE_MACRO = 0; + const MODE_VARIABLE = 1; + + private int $mode; + private int $maxDepth = 5; + private TemplateResolverInterface $loader; + + public static function createVariableScopeBuilder(TemplateResolverInterface $loader) + { + return new self($loader, self::MODE_VARIABLE); + } + + public static function createMacroScopeBuilder(TemplateResolverInterface $loader) + { + return new self($loader, self::MODE_MACRO); + } + + public function __construct(TemplateResolverInterface $loader, int $mode = 0) + { + $this->mode = $mode; + $this->loader = $loader; + } + + private function subScope(string $twigPath, int $depth): Scope + { + if ($depth > $this->maxDepth) { + return new Scope('file', $twigPath); + } + + $lexer = new Lexer(); + + $source = $this->loader->load($twigPath); + + try { + $tokens = $lexer->tokenize($source); + $scope = $this->doBuild($tokens, $twigPath, $this->maxDepth + 1); + + return $scope; + } catch (SyntaxError $e) { + return new Scope('file', $twigPath); + } + } + + public function build(TokenStream $tokens) + { + return $this->doBuild($tokens); + } + + private function doBuild(TokenStream $tokens, $name = 'root', $depth = 0): Scope + { + $scope = new Scope('file', $name); + $root = $scope; + + while (!$tokens->isEOF()) { + $token = $tokens->getCurrent(); + + if (Token::BLOCK_START_TYPE === $token->getType()) { + $blockType = $tokens->look(2)->getValue(); + + if (in_array($blockType, ['block', 'for', 'embed', 'macro'], true)) { + if ('block' === $blockType) { + $scope = $scope->spawn($blockType, $tokens->look(4)->getValue()); + } else { + $scope = $scope->spawn($blockType, 'noname'); + } + if ('macro' === $blockType) { + $scope->isolate(); + } + } + + if (in_array($blockType, ['endblock', 'endfor', 'endembed', 'endmacro'], true)) { + $scope = $scope->leave(); + } + } + + if (Token::BLOCK_START_TYPE === $token->getType()) { + $blockType = $tokens->look(2)->getValue(); + + switch ($blockType) { + case 'extends': + $templateName = $tokens->look(4); + + if (Token::NAME_TYPE === $templateName->getType()) { // {% import varName ... %} + $scope->use($templateName->getValue()); + } + $file = $tokens->look(4)->getValue(); + + $scope->extends($this->subScope($file, $depth)); + + StreamNavigator::skipToOneOf($tokens, [ + ['type' => Token::BLOCK_END_TYPE], + ]); + break; + case 'embed': + case 'include': + $templateName = $tokens->look(4); + + if (Token::NAME_TYPE === $templateName->getType()) { // {% import varName ... %} + $scope->use($templateName->getValue()); + } + $file = $tokens->look(4)->getValue(); + + $scope->nest($this->subScope($file, $depth)); + + StreamNavigator::skipToOneOf($tokens, [ + ['value' => 'with'], + ['type' => Token::BLOCK_END_TYPE], + ]); + break; + case 'from': + $from = $tokens->look(4); + + if (Token::NAME_TYPE === $from->getType()) { // {% from varName import ... %} + if ($this->handleVariables()) { + $scope->use($from->getValue()); + } + } + + if ('as' === $tokens->look(10)->getValue()) { + $forward = 12; // Extracts token position from block of form {% import foo as bar %} + } else { + $forward = 8; // Extracts token position from block of form {% import foo %} + } + + StreamNavigator::skip($tokens, $forward); + + // Handles single or multiple imports ( {% from "file.twig" import foo as bar, baz %} ) + while (in_array($tokens->getCurrent()->getType(), [Token::NAME_TYPE, Token::PUNCTUATION_TYPE, Token::WHITESPACE_TYPE], true)) { + $next = $tokens->getCurrent(); + if (Token::NAME_TYPE === $next->getType() && $this->handleMacros()) { + $scope->declare($next->getValue(), $next); + } + $tokens->next(); + } + + StreamNavigator::skipTo($tokens, Token::BLOCK_END_TYPE); + break; + case 'import': + StreamNavigator::skipTo($tokens, Token::NAME_TYPE, 'as'); + StreamNavigator::skip($tokens, 2); + + // Handles single or multiple imports ( {% import foo as bar, baz %} ) + while (in_array($tokens->getCurrent()->getType(), [Token::NAME_TYPE, Token::PUNCTUATION_TYPE, Token::WHITESPACE_TYPE], true)) { + $next = $tokens->getCurrent(); + if (Token::NAME_TYPE === $next->getType() && $this->handleMacros()) { + $scope->declare($next->getValue(), $next); + } + $tokens->next(); + } + break; + case 'set': + if ($this->handleVariables()) { + $scope->declare($tokens->look(4)->getValue(), $tokens->look(4)); + } + StreamNavigator::skipToOneOf($tokens, [ + ['type' => Token::OPERATOR_TYPE, 'value' => '='], + ['type' => Token::BLOCK_END_TYPE], + ]); + break; + case 'if': + case 'elseif': + StreamNavigator::skip($tokens, 3); + break; + case 'for': + if ($this->handleVariables()) { + $scope->declare($tokens->look(4)->getValue(), $tokens->look(4)); + } + StreamNavigator::skip($tokens, 5); + break; + default: + StreamNavigator::skipTo($tokens, Token::BLOCK_END_TYPE); + } + } elseif (Token::NAME_TYPE === $token->getType()) { + $previous = StreamNavigator::getPreviousSignificantToken($tokens); + $next = StreamNavigator::getNextSignificantToken($tokens); + + $isHashKey = in_array($previous->getValue(), [',', '{'], true) && ':' === $next->getValue(); + $isFilter = '|' === $previous->getValue(); + $isProperty = '.' === $previous->getValue(); + $isFunctionCall = '(' === $next->getValue(); + $isTest = ('is' === $previous->getValue()) || ('is not' === $previous->getValue()); + $isReserved = in_array($token->getValue(), ['null', 'true', 'false'], true); + + if ($isFunctionCall && 'block' === $token->getValue()) { + $i = 0; + $blockNameToken = $tokens->look($i); + // Scans for the name of the nested block. + while (Token::BLOCK_END_TYPE !== $blockNameToken->getType() && Token::STRING_TYPE !== $blockNameToken->getType()) { + $blockNameToken = $tokens->look($i); + ++$i; + } + $scope->referenceBlock($blockNameToken->getValue()); + } + + if (!$isHashKey && !$isFilter && !$isProperty && !$isFunctionCall && !$isTest && !$isReserved && $this->handleVariables()) { + $scope->use($token->getValue()); + } + + $isSubProperty = in_array($previous->getValue(), ['.', '|'], true); + $directUsage = in_array($next->getValue(), ['('], true); + $dotUsage = (Token::NAME_TYPE === StreamNavigator::getNextSignificantToken($tokens, 1)->getType()) && in_array(StreamNavigator::getNextSignificantToken($tokens, 2)->getValue(), ['('], true); + + if (!$isSubProperty && ($directUsage || $dotUsage) && $this->handleMacros()) { + $scope->use($token->getValue()); + } + + $tokens->next(); + } elseif (Token::COMMENT_TYPE === $token->getType()) { + if (0 === strpos($token->getValue(), 'twigcs use-var ')) { + $names = explode(',', str_replace('twigcs use-var ', '', $token->getValue())); + + foreach ($names as $name) { + if ($this->handleVariables()) { + $scope->use(trim($name)); + } + } + } + + $tokens->next(); + } else { + $tokens->next(); + } + } + + return $root; + } + + private function handleVariables() + { + return self::MODE_VARIABLE === $this->mode; + } + + private function handleMacros() + { + return self::MODE_MACRO === $this->mode; + } +} diff --git a/src/TemplateResolver/ChainResolver.php b/src/TemplateResolver/ChainResolver.php new file mode 100644 index 0000000..7593583 --- /dev/null +++ b/src/TemplateResolver/ChainResolver.php @@ -0,0 +1,40 @@ + + */ +class ChainResolver implements TemplateResolverInterface +{ + private array $chain; + + public function __construct(array $chain = []) + { + $this->chain = $chain; + } + + public function load(string $path): Source + { + foreach ($this->chain as $loader) { + if ($loader->exists($path)) { + return $loader->load($path); + } + } + + throw new \RuntimeException(sprintf('Template "%s" could not be resolved.', $path)); + } + + public function exists(string $path): bool + { + foreach ($this->chain as $loader) { + if ($loader->exists($path)) { + return true; + } + } + + return false; + } +} diff --git a/src/TemplateResolver/FileResolver.php b/src/TemplateResolver/FileResolver.php new file mode 100644 index 0000000..baefbb1 --- /dev/null +++ b/src/TemplateResolver/FileResolver.php @@ -0,0 +1,32 @@ + + */ +class FileResolver implements TemplateResolverInterface +{ + private string $basePath; + + public function __construct(string $basePath = '') + { + $this->basePath = $basePath; + } + + public function exists(string $path): bool + { + return file_exists(sprintf('%s/%s', $this->basePath, $path)); + } + + public function load(string $path): Source + { + $realPath = sprintf('%s/%s', $this->basePath, $path); + + $content = @file_get_contents($realPath); + + return new Source($content, $realPath, $realPath); + } +} diff --git a/src/TemplateResolver/NamespacedResolver.php b/src/TemplateResolver/NamespacedResolver.php new file mode 100644 index 0000000..4c9dfe8 --- /dev/null +++ b/src/TemplateResolver/NamespacedResolver.php @@ -0,0 +1,44 @@ + + */ +class NamespacedResolver implements TemplateResolverInterface +{ + private array $namespaces; + + public function __construct(array $namespaces = []) + { + $this->namespaces = $namespaces; + } + + public function load(string $path): Source + { + $namespace = $this->getNamespace($path); + $subPath = substr($path, strlen($namespace) + 1); + + if ($namespace && ($this->namespaces[$namespace] ?? false) && $this->namespaces[$namespace]->exists($subPath)) { + return $this->namespaces[$namespace]->load($subPath); + } + + throw new \RuntimeException(sprintf('Template "%s" could not be resolved.', $path)); + } + + public function exists(string $path): bool + { + $namespace = $this->getNamespace($path); + + return array_key_exists($namespace, $this->namespaces); + } + + private function getNamespace($path): ?string + { + $namespace = explode('/', $path)[0]; + + return '@' === ($namespace[0] ?? null) ? substr($namespace, 1) : null; + } +} diff --git a/src/TemplateResolver/NullResolver.php b/src/TemplateResolver/NullResolver.php new file mode 100644 index 0000000..1a9b38a --- /dev/null +++ b/src/TemplateResolver/NullResolver.php @@ -0,0 +1,21 @@ + + */ +class NullResolver implements TemplateResolverInterface +{ + public function exists(string $path): bool + { + return true; + } + + public function load(string $path): Source + { + return new Source('', $path, $path); + } +} diff --git a/src/TemplateResolver/TemplateResolverInterface.php b/src/TemplateResolver/TemplateResolverInterface.php new file mode 100644 index 0000000..66101cd --- /dev/null +++ b/src/TemplateResolver/TemplateResolverInterface.php @@ -0,0 +1,15 @@ + + */ +interface TemplateResolverInterface +{ + public function exists(string $path): bool; + + public function load(string $path): Source; +} diff --git a/src/Util/StreamNavigator.php b/src/Util/StreamNavigator.php new file mode 100644 index 0000000..3078f60 --- /dev/null +++ b/src/Util/StreamNavigator.php @@ -0,0 +1,102 @@ +look(-$i)) { + if (!in_array($token->getType(), [Token::WHITESPACE_TYPE, Token::NEWLINE_TYPE], true)) { + if (0 === $skip) { + return $token; + } + + --$skip; + } + + ++$i; + } + + return null; + } + + public static function getNextSignificantToken(TokenStream $tokens, int $skip = 0): ?Token + { + $i = 1; + $token = null; + + while ($token = $tokens->look($i)) { + if (!in_array($token->getType(), [Token::WHITESPACE_TYPE, Token::NEWLINE_TYPE], true)) { + if (0 === $skip) { + return $token; + } + + --$skip; + } + + ++$i; + } + + return null; + } + + public static function skipTo(TokenStream $tokens, int $tokenType, string $tokenValue = null) + { + while (!$tokens->isEOF()) { + $continue = $tokens->getCurrent()->getType() !== $tokenType; + + if (null !== $tokenValue) { + $continue |= $tokens->getCurrent()->getValue() !== $tokenValue; + } + + if (!$continue) { + return; + } + + $tokens->next(); + } + } + + public static function skipToOneOf(TokenStream $tokens, array $possibilities) + { + while (!$tokens->isEOF()) { + foreach ($possibilities as $possibility) { + $tokenValue = $possibility['value'] ?? null; + $tokenType = $possibility['type'] ?? null; + $found = true; + + if ($tokenType) { + $found &= $tokenType === $tokens->getCurrent()->getType(); + } + + if ($tokenValue) { + $found &= $tokenValue === $tokens->getCurrent()->getValue(); + } + + if ($found) { + return; + } + } + + $tokens->next(); + } + } + + public static function skip(TokenStream $tokens, int $amount) + { + while (!$tokens->isEOF()) { + --$amount; + $tokens->next(); + if (0 === $amount) { + return; + } + } + } +} diff --git a/tests/Console/LintCommandTest.php b/tests/Console/LintCommandTest.php index 576eb71..4db4b50 100644 --- a/tests/Console/LintCommandTest.php +++ b/tests/Console/LintCommandTest.php @@ -267,4 +267,28 @@ public function testConfigFileSamePathWithRulesetOverrides() ] }', $output); } + + public function testUnusedWithFileLoader() + { + $this->commandTester->execute([ + '--config' => 'tests/data/config/loaders/.twig_cs.dist', + ]); + + $output = $this->commandTester->getDisplay(); + $statusCode = $this->commandTester->getStatusCode(); + $this->assertSame($statusCode, 1); + $this->assertStringContainsString('tests/data/config/loaders/src/embed/child.html.twig +l.3 c.7 : WARNING Unused variable "unused_child". +tests/data/config/loaders/src/embed/parent.html.twig +l.2 c.7 : WARNING Unused variable "unused_parent". +tests/data/config/loaders/src/extends/child.html.twig +l.5 c.7 : WARNING Unused variable "unused_child". +tests/data/config/loaders/src/extends/parent.html.twig +l.7 c.7 : WARNING Unused variable "unused_parent". +tests/data/config/loaders/src/include/child.html.twig +l.3 c.7 : WARNING Unused variable "unused_child". +tests/data/config/loaders/src/include/parent.html.twig +l.2 c.7 : WARNING Unused variable "unused_parent". +6 violation(s) found', $output); + } } diff --git a/tests/Twig3FunctionalTest.php b/tests/Twig3FunctionalTest.php index 914d9da..3c364e4 100644 --- a/tests/Twig3FunctionalTest.php +++ b/tests/Twig3FunctionalTest.php @@ -154,15 +154,15 @@ public function getData() ['{{ foo <=> -1 }}', null], ['{{ foo <=> -1 }}', 'There should be 1 space between the "<=>" operator and its left operand.'], ['{{ foo <=> -1 }}', 'There should be 1 space between the "<=>" operator and its right operand.'], - ["{{ (test == 3) }}", null], - ["{{ (test == 3) }}", 'There should be 1 space between the "==" operator and its left operand.'], - ["{{ (test == 3) }}", 'There should be 1 space between the "==" operator and its right operand.'], - ["{{ function(foo, bar == false) }}", null], - ["{{ function(foo, bar == false) }}", 'There should be 1 space between the "==" operator and its left operand.'], - ["{{ function(foo, bar == false) }}", 'There should be 1 space between the "==" operator and its right operand.'], + ['{{ (test == 3) }}', null], + ['{{ (test == 3) }}', 'There should be 1 space between the "==" operator and its left operand.'], + ['{{ (test == 3) }}', 'There should be 1 space between the "==" operator and its right operand.'], + ['{{ function(foo, bar == false) }}', null], + ['{{ function(foo, bar == false) }}', 'There should be 1 space between the "==" operator and its left operand.'], + ['{{ function(foo, bar == false) }}', 'There should be 1 space between the "==" operator and its right operand.'], ['{{ function(foo, bar == false, baz) }}', null], - ["{{ function(foo, bar == false, baz) }}", 'There should be 1 space between the "==" operator and its left operand.'], - ["{{ function(foo, bar == false, baz) }}", 'There should be 1 space between the "==" operator and its right operand.'], + ['{{ function(foo, bar == false, baz) }}', 'There should be 1 space between the "==" operator and its left operand.'], + ['{{ function(foo, bar == false, baz) }}', 'There should be 1 space between the "==" operator and its right operand.'], ['{{ -1 }}', null], ['{{ -10 }}', null], ['{{ (-10) }}', null], @@ -473,6 +473,10 @@ public function getData() 3, ] %}{{ columns }}', null], ['{% set foo = {a: 1 , b: 2} %}{{ foo }}', 'There should be 0 space between the value and the following ",".'], + + // Check regression from https://github.com/friendsoftwig/twigcs/issues/121 + ['{% for entry in data %}{% include "@namespace/" ~ entry.something ~ "/" ~ entry.something ~ ".twig" with entry.data %}{% endfor %}', null], + ['{% for entry in data %}{% embed "@namespace/" ~ entry.something ~ "/" ~ entry.something ~ ".twig" with entry.data %}{% endfor %}', null], ]; } } diff --git a/tests/data/config/loaders/.twig_cs.dist b/tests/data/config/loaders/.twig_cs.dist new file mode 100644 index 0000000..c768313 --- /dev/null +++ b/tests/data/config/loaders/.twig_cs.dist @@ -0,0 +1,22 @@ +in(__DIR__.'/src') + ->sortByName() +; + +return \FriendsOfTwig\Twigcs\Config\Config::create() + ->setFinder($finder) + ->setTemplateResolver(new TemplateResolver\ChainResolver([ + new TemplateResolver\FileResolver(__DIR__ . '/src'), + new TemplateResolver\NamespacedResolver([ + 'acme' => new TemplateResolver\FileResolver(__DIR__ . '/acme') + ]), + ])) + ->setSeverity('warning') + ->setReporter('console') + ->setName('my-config') + ->setRuleSet(FriendsOfTwig\Twigcs\Ruleset\Official::class) +; diff --git a/tests/data/config/loaders/acme/base.html.twig b/tests/data/config/loaders/acme/base.html.twig new file mode 100644 index 0000000..11d9257 --- /dev/null +++ b/tests/data/config/loaders/acme/base.html.twig @@ -0,0 +1,3 @@ +{{ used_vendor_bar }} + +{% set unused_vendor_var = 1 %} diff --git a/tests/data/config/loaders/src/embed/child.html.twig b/tests/data/config/loaders/src/embed/child.html.twig new file mode 100644 index 0000000..055f6d2 --- /dev/null +++ b/tests/data/config/loaders/src/embed/child.html.twig @@ -0,0 +1,3 @@ +{{ foo }} + +{% set unused_child = 1 %} diff --git a/tests/data/config/loaders/src/embed/parent.html.twig b/tests/data/config/loaders/src/embed/parent.html.twig new file mode 100644 index 0000000..a635e77 --- /dev/null +++ b/tests/data/config/loaders/src/embed/parent.html.twig @@ -0,0 +1,6 @@ +{% set foo = 1 %} +{% set unused_parent = 2 %} +{% set used_vendor_bar = 3 %} + +{% embed 'embed/child.html.twig' %} +{% embed '@acme/base.html.twig' %} \ No newline at end of file diff --git a/tests/data/config/loaders/src/extends/child.html.twig b/tests/data/config/loaders/src/extends/child.html.twig new file mode 100644 index 0000000..aa45231 --- /dev/null +++ b/tests/data/config/loaders/src/extends/child.html.twig @@ -0,0 +1,5 @@ +{% extends 'extends/parent.html.twig' %} + +{% set title = 'My other title' %} + +{% set unused_child = true %} diff --git a/tests/data/config/loaders/src/extends/parent.html.twig b/tests/data/config/loaders/src/extends/parent.html.twig new file mode 100644 index 0000000..a30f2c0 --- /dev/null +++ b/tests/data/config/loaders/src/extends/parent.html.twig @@ -0,0 +1,7 @@ +{% set title = title|default('My Title') %} + +{% block title %} +

{{ title }}

+{% endblock %} + +{% set unused_parent = true %} diff --git a/tests/data/config/loaders/src/include/child.html.twig b/tests/data/config/loaders/src/include/child.html.twig new file mode 100644 index 0000000..055f6d2 --- /dev/null +++ b/tests/data/config/loaders/src/include/child.html.twig @@ -0,0 +1,3 @@ +{{ foo }} + +{% set unused_child = 1 %} diff --git a/tests/data/config/loaders/src/include/parent.html.twig b/tests/data/config/loaders/src/include/parent.html.twig new file mode 100644 index 0000000..32ba640 --- /dev/null +++ b/tests/data/config/loaders/src/include/parent.html.twig @@ -0,0 +1,6 @@ +{% set foo = 1 %} +{% set unused_parent = 2 %} +{% set used_vendor_bar = 3 %} + +{% include 'include/child.html.twig' %} +{% include '@acme/base.html.twig' %} \ No newline at end of file diff --git a/tests/data/config/namespaced_loaders/.twig_cs.dist b/tests/data/config/namespaced_loaders/.twig_cs.dist new file mode 100644 index 0000000..92a46fb --- /dev/null +++ b/tests/data/config/namespaced_loaders/.twig_cs.dist @@ -0,0 +1,20 @@ +in(__DIR__) +; + +return \FriendsOfTwig\Twigcs\Config\Config::create() + ->setFinder($finder) + ->setTemplateResolver(new NamespacedResolver([ + 'a' => new FileResolver(__DIR__.'/a'), + 'b' => new FileResolver(__DIR__.'/b'), + ])) + ->setSeverity('warning') + ->setReporter('console') + ->setName('my-config') + ->setRuleSet(FriendsOfTwig\Twigcs\Ruleset\Official::class) +; diff --git a/tests/data/config/namespaced_loaders/a/child.html.twig b/tests/data/config/namespaced_loaders/a/child.html.twig new file mode 100644 index 0000000..055f6d2 --- /dev/null +++ b/tests/data/config/namespaced_loaders/a/child.html.twig @@ -0,0 +1,3 @@ +{{ foo }} + +{% set unused_child = 1 %} diff --git a/tests/data/config/namespaced_loaders/b/parent.html.twig b/tests/data/config/namespaced_loaders/b/parent.html.twig new file mode 100644 index 0000000..3b8c850 --- /dev/null +++ b/tests/data/config/namespaced_loaders/b/parent.html.twig @@ -0,0 +1,4 @@ +{% set foo = 1 %} +{% set unused_parent = 2 %} + +{% embed '@c/child.html.twig' %} \ No newline at end of file