Skip to content

✨ Add TwigCS #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions TwigCS/Command/TwigCSCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace TwigCS\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
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\Token\Tokenizer;

/**
* TwigCS stands for "Twig Code Sniffer" and will check twig template of your project.
* This is heavily inspired by the symfony lint command and PHP_CodeSniffer tool
*
* @see https://github.com/squizlabs/PHP_CodeSniffer
*/
class TwigCSCommand extends Command
{
protected function configure()
{
$this
->setName('lint')
->setDescription('Lints a template and outputs encountered errors')
->setDefinition([
new InputOption(
'exclude',
'e',
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'Excludes, based on regex, paths of files and folders from parsing',
['vendor/']
),
new InputOption(
'level',
'l',
InputOption::VALUE_OPTIONAL,
'Allowed values are: warning, error',
'warning'
),
new InputOption(
'working-dir',
'd',
InputOption::VALUE_OPTIONAL,
'Run as if this was started in <working-dir> instead of the current working directory',
getcwd()
),
])
->addArgument(
'paths',
InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
'Paths of files and folders to parse',
null
);
}

/**
* @param InputInterface $input
* @param OutputInterface $output
*
* @return int
*
* @throws \Exception
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$paths = $input->getArgument('paths');
$exclude = $input->getOption('exclude');
$level = $input->getOption('level');
$currentDir = $input->getOption('working-dir');

$config = new Config([
'paths' => $paths,
'exclude' => $exclude,
'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();

// Execute the linter.
$report = $linter->run($config->findFiles(), $ruleset);

// Format the output.
$reporter->display($report, $level);

// Return a meaningful error code.
if ($report->getTotalErrors()) {
$exitCode = 1;
}

return $exitCode;
}
}
86 changes: 86 additions & 0 deletions TwigCS/Config/Config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace TwigCS\Config;

use Symfony\Component\Finder\Finder;

/**
* TwigCS configuration data.
*/
class Config
{
/**
* Default configuration.
*
* @var array
*/
public static $defaultConfig = [
'exclude' => [],
'pattern' => '*.twig',
'paths' => [],
'stub' => [],
'workingDirectory' => '',
];

/**
* Current configuration.
*
* @var array
*/
protected $config;

public function __construct()
{
$args = func_get_args();

$this->config = $this::$defaultConfig;
foreach ($args as $arg) {
$this->config = array_merge($this->config, $arg);
}
}

/**
* Find all files to process, based on a file or directory and exclude patterns.
*
* @return array
*/
public function findFiles()
{
$paths = $this->get('paths');
$exclude = $this->get('exclude');

// Build the finder.
$files = Finder::create()
->in($this->get('workingDirectory'))
->name($this->config['pattern'])
->files();

// Include all matching paths.
foreach ($paths as $path) {
$files->path($path);
}

// Exclude all matching paths.
if ($exclude) {
$files->exclude($exclude);
}

return $files;
}

/**
* Get a configuration value for the given $key.
*
* @param string $key
*
* @return mixed
*/
public function get($key)
{
if (!isset($this->config[$key])) {
throw new \Exception(sprintf('Configuration key "%s" does not exist', $key));
}

return $this->config[$key];
}
}
115 changes: 115 additions & 0 deletions TwigCS/Environment/StubbedEnvironment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace TwigCS\Environment;

use Twig\Environment;
use Twig\Loader\LoaderInterface;
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.
*/
class StubbedEnvironment extends Environment
{
/**
* @var TwigFilter[]
*/
private $stubFilters;

/**
* @var TwigFunction[]
*/
private $stubFunctions;

/**
* @var TwigTest[]
*/
private $stubTests;

/**
* @var \Closure
*/
private $stubCallable;

/**
* @param LoaderInterface|null $loader
* @param array $options
*/
public function __construct(LoaderInterface $loader = null, $options = [])
{
parent::__construct($loader, $options);

$this->stubCallable = function () {
/* This will be used as stub filter, function or test */
};

$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());
}

/**
* @param string $name
*
* @return TwigFilter
*/
public function getFilter($name)
{
if (!isset($this->stubFilters[$name])) {
$this->stubFilters[$name] = new TwigFilter('stub', $this->stubCallable);
}

return $this->stubFilters[$name];
}

/**
* @param string $name
*
* @return TwigFunction
*/
public function getFunction($name)
{
if (!isset($this->stubFunctions[$name])) {
$this->stubFunctions[$name] = new TwigFunction('stub', $this->stubCallable);
}

return $this->stubFunctions[$name];
}

/**
* @param string $name
*
* @return false|TwigTest
*/
public function getTest($name)
{
$test = parent::getTest($name);
if ($test) {
return $test;
}

if (isset($this->stubTests[$name])) {
return $this->stubTests[$name];
}

return false;
}
}
64 changes: 64 additions & 0 deletions TwigCS/Extension/SniffsExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace TwigCS\Extension;

use Twig\Extension\AbstractExtension;
use Twig\NodeVisitor\NodeVisitorInterface;
use TwigCS\Sniff\PostParserSniffInterface;

/**
* This extension is responsible of loading the sniffs into the twig environment.
*
* This class is only a bridge between the linter and the `SniffsNodeVisitor` that is
* actually doing the work when Twig parser is compiling a template.
*/
class SniffsExtension extends AbstractExtension
{
/**
* The actual node visitor.
*
* @var SniffsNodeVisitor
*/
protected $nodeVisitor;

public function __construct()
{
$this->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;
}
}
Loading