Skip to content

Commit 0740604

Browse files
Merge pull request #57 from VincentLanglet/twigcs
✨ Add TwigCS
2 parents 1085655 + f89430d commit 0740604

33 files changed

+3225
-77
lines changed

TwigCS/Command/TwigCSCommand.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
namespace TwigCS\Command;
4+
5+
use Symfony\Component\Console\Command\Command;
6+
use Symfony\Component\Console\Input\InputArgument;
7+
use Symfony\Component\Console\Input\InputInterface;
8+
use Symfony\Component\Console\Input\InputOption;
9+
use Symfony\Component\Console\Output\OutputInterface;
10+
use Twig\Loader\ArrayLoader;
11+
use TwigCS\Config\Config;
12+
use TwigCS\Environment\StubbedEnvironment;
13+
use TwigCS\Linter;
14+
use TwigCS\Report\TextFormatter;
15+
use TwigCS\Ruleset\RulesetFactory;
16+
use TwigCS\Token\Tokenizer;
17+
18+
/**
19+
* TwigCS stands for "Twig Code Sniffer" and will check twig template of your project.
20+
* This is heavily inspired by the symfony lint command and PHP_CodeSniffer tool
21+
*
22+
* @see https://github.com/squizlabs/PHP_CodeSniffer
23+
*/
24+
class TwigCSCommand extends Command
25+
{
26+
protected function configure()
27+
{
28+
$this
29+
->setName('lint')
30+
->setDescription('Lints a template and outputs encountered errors')
31+
->setDefinition([
32+
new InputOption(
33+
'exclude',
34+
'e',
35+
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
36+
'Excludes, based on regex, paths of files and folders from parsing',
37+
['vendor/']
38+
),
39+
new InputOption(
40+
'level',
41+
'l',
42+
InputOption::VALUE_OPTIONAL,
43+
'Allowed values are: warning, error',
44+
'warning'
45+
),
46+
new InputOption(
47+
'working-dir',
48+
'd',
49+
InputOption::VALUE_OPTIONAL,
50+
'Run as if this was started in <working-dir> instead of the current working directory',
51+
getcwd()
52+
),
53+
])
54+
->addArgument(
55+
'paths',
56+
InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
57+
'Paths of files and folders to parse',
58+
null
59+
);
60+
}
61+
62+
/**
63+
* @param InputInterface $input
64+
* @param OutputInterface $output
65+
*
66+
* @return int
67+
*
68+
* @throws \Exception
69+
*/
70+
protected function execute(InputInterface $input, OutputInterface $output)
71+
{
72+
$paths = $input->getArgument('paths');
73+
$exclude = $input->getOption('exclude');
74+
$level = $input->getOption('level');
75+
$currentDir = $input->getOption('working-dir');
76+
77+
$config = new Config([
78+
'paths' => $paths,
79+
'exclude' => $exclude,
80+
'workingDirectory' => $currentDir,
81+
]);
82+
83+
$twig = new StubbedEnvironment(new ArrayLoader(), ['stub_tags' => $config->get('stub')]);
84+
$linter = new Linter($twig, new Tokenizer($twig));
85+
$factory = new RulesetFactory();
86+
$reporter = new TextFormatter($input, $output);
87+
$exitCode = 0;
88+
89+
// Get the rules to apply.
90+
$ruleset = $factory->createStandardRuleset();
91+
92+
// Execute the linter.
93+
$report = $linter->run($config->findFiles(), $ruleset);
94+
95+
// Format the output.
96+
$reporter->display($report, $level);
97+
98+
// Return a meaningful error code.
99+
if ($report->getTotalErrors()) {
100+
$exitCode = 1;
101+
}
102+
103+
return $exitCode;
104+
}
105+
}

TwigCS/Config/Config.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace TwigCS\Config;
4+
5+
use Symfony\Component\Finder\Finder;
6+
7+
/**
8+
* TwigCS configuration data.
9+
*/
10+
class Config
11+
{
12+
/**
13+
* Default configuration.
14+
*
15+
* @var array
16+
*/
17+
public static $defaultConfig = [
18+
'exclude' => [],
19+
'pattern' => '*.twig',
20+
'paths' => [],
21+
'stub' => [],
22+
'workingDirectory' => '',
23+
];
24+
25+
/**
26+
* Current configuration.
27+
*
28+
* @var array
29+
*/
30+
protected $config;
31+
32+
public function __construct()
33+
{
34+
$args = func_get_args();
35+
36+
$this->config = $this::$defaultConfig;
37+
foreach ($args as $arg) {
38+
$this->config = array_merge($this->config, $arg);
39+
}
40+
}
41+
42+
/**
43+
* Find all files to process, based on a file or directory and exclude patterns.
44+
*
45+
* @return array
46+
*/
47+
public function findFiles()
48+
{
49+
$paths = $this->get('paths');
50+
$exclude = $this->get('exclude');
51+
52+
// Build the finder.
53+
$files = Finder::create()
54+
->in($this->get('workingDirectory'))
55+
->name($this->config['pattern'])
56+
->files();
57+
58+
// Include all matching paths.
59+
foreach ($paths as $path) {
60+
$files->path($path);
61+
}
62+
63+
// Exclude all matching paths.
64+
if ($exclude) {
65+
$files->exclude($exclude);
66+
}
67+
68+
return $files;
69+
}
70+
71+
/**
72+
* Get a configuration value for the given $key.
73+
*
74+
* @param string $key
75+
*
76+
* @return mixed
77+
*/
78+
public function get($key)
79+
{
80+
if (!isset($this->config[$key])) {
81+
throw new \Exception(sprintf('Configuration key "%s" does not exist', $key));
82+
}
83+
84+
return $this->config[$key];
85+
}
86+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
namespace TwigCS\Environment;
4+
5+
use Twig\Environment;
6+
use Twig\Loader\LoaderInterface;
7+
use Twig\TwigFilter;
8+
use Twig\TwigFunction;
9+
use Twig\TwigTest;
10+
use TwigCS\Extension\SniffsExtension;
11+
use TwigCS\Token\TokenParser;
12+
13+
/**
14+
* Provide stubs for all filters, functions, tests and tags that are not defined in twig's core.
15+
*/
16+
class StubbedEnvironment extends Environment
17+
{
18+
/**
19+
* @var TwigFilter[]
20+
*/
21+
private $stubFilters;
22+
23+
/**
24+
* @var TwigFunction[]
25+
*/
26+
private $stubFunctions;
27+
28+
/**
29+
* @var TwigTest[]
30+
*/
31+
private $stubTests;
32+
33+
/**
34+
* @var \Closure
35+
*/
36+
private $stubCallable;
37+
38+
/**
39+
* @param LoaderInterface|null $loader
40+
* @param array $options
41+
*/
42+
public function __construct(LoaderInterface $loader = null, $options = [])
43+
{
44+
parent::__construct($loader, $options);
45+
46+
$this->stubCallable = function () {
47+
/* This will be used as stub filter, function or test */
48+
};
49+
50+
$this->stubFilters = [];
51+
$this->stubFunctions = [];
52+
53+
if (isset($options['stub_tags'])) {
54+
foreach ($options['stub_tags'] as $tag) {
55+
$this->addTokenParser(new TokenParser($tag));
56+
}
57+
}
58+
59+
$this->stubTests = [];
60+
if (isset($options['stub_tests'])) {
61+
foreach ($options['stub_tests'] as $test) {
62+
$this->stubTests[$test] = new TwigTest('stub', $this->stubCallable);
63+
}
64+
}
65+
66+
$this->addExtension(new SniffsExtension());
67+
}
68+
69+
/**
70+
* @param string $name
71+
*
72+
* @return TwigFilter
73+
*/
74+
public function getFilter($name)
75+
{
76+
if (!isset($this->stubFilters[$name])) {
77+
$this->stubFilters[$name] = new TwigFilter('stub', $this->stubCallable);
78+
}
79+
80+
return $this->stubFilters[$name];
81+
}
82+
83+
/**
84+
* @param string $name
85+
*
86+
* @return TwigFunction
87+
*/
88+
public function getFunction($name)
89+
{
90+
if (!isset($this->stubFunctions[$name])) {
91+
$this->stubFunctions[$name] = new TwigFunction('stub', $this->stubCallable);
92+
}
93+
94+
return $this->stubFunctions[$name];
95+
}
96+
97+
/**
98+
* @param string $name
99+
*
100+
* @return false|TwigTest
101+
*/
102+
public function getTest($name)
103+
{
104+
$test = parent::getTest($name);
105+
if ($test) {
106+
return $test;
107+
}
108+
109+
if (isset($this->stubTests[$name])) {
110+
return $this->stubTests[$name];
111+
}
112+
113+
return false;
114+
}
115+
}

TwigCS/Extension/SniffsExtension.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
namespace TwigCS\Extension;
4+
5+
use Twig\Extension\AbstractExtension;
6+
use Twig\NodeVisitor\NodeVisitorInterface;
7+
use TwigCS\Sniff\PostParserSniffInterface;
8+
9+
/**
10+
* This extension is responsible of loading the sniffs into the twig environment.
11+
*
12+
* This class is only a bridge between the linter and the `SniffsNodeVisitor` that is
13+
* actually doing the work when Twig parser is compiling a template.
14+
*/
15+
class SniffsExtension extends AbstractExtension
16+
{
17+
/**
18+
* The actual node visitor.
19+
*
20+
* @var SniffsNodeVisitor
21+
*/
22+
protected $nodeVisitor;
23+
24+
public function __construct()
25+
{
26+
$this->nodeVisitor = new SniffsNodeVisitor();
27+
}
28+
29+
/**
30+
* @return NodeVisitorInterface[]
31+
*/
32+
public function getNodeVisitors()
33+
{
34+
return [$this->nodeVisitor];
35+
}
36+
37+
/**
38+
* Register a sniff in the node visitor.
39+
*
40+
* @param PostParserSniffInterface $sniff
41+
*
42+
* @return self
43+
*/
44+
public function addSniff(PostParserSniffInterface $sniff)
45+
{
46+
$this->nodeVisitor->addSniff($sniff);
47+
48+
return $this;
49+
}
50+
51+
/**
52+
* Remove a sniff from the node visitor.
53+
*
54+
* @param PostParserSniffInterface $sniff
55+
*
56+
* @return self
57+
*/
58+
public function removeSniff(PostParserSniffInterface $sniff)
59+
{
60+
$this->nodeVisitor->removeSniff($sniff);
61+
62+
return $this;
63+
}
64+
}

0 commit comments

Comments
 (0)