Skip to content

Commit

Permalink
Make it easy to configure the php_parser visitors.
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Oct 27, 2016
1 parent 6db571c commit a1cb599
Show file tree
Hide file tree
Showing 15 changed files with 351 additions and 72 deletions.
17 changes: 10 additions & 7 deletions grumphp.yml.dist
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
parameters:
bin_dir: "./vendor/bin"
git_dir: "."
# stop_on_failure: true
tasks:
phpcs:
standard: PSR2
Expand All @@ -21,10 +22,12 @@ parameters:
ignore_patterns:
- "#test/(.*).yml#"
phplint: ~
php_parser: ~

services:
grumphp.parser.php.visitor.declare_strict_types:
class: '\GrumPHP\Parser\Php\Visitor\DeclareStrictTypesVisitor'
tags:
- {name: 'php_parser.visitor'}
php_parser:
visitors:
nameresolver: ~
#declare_strict_types: ~
no_exit_statements: ~
forbidden_function_calls:
blacklist: [var_dump]
metadata:
priority: 100000
39 changes: 34 additions & 5 deletions resources/config/parsers.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,46 @@
services:
grumphp.parser.php.configurator.traverser:
class: 'GrumPHP\Parser\Php\Configurator\TraverserConfigurator'
arguments:
- '@service_container'

grumphp.parser.php.factory.parser:
class: 'GrumPHP\Parser\Php\Factory\ParserFactory'
arguments: []

grumphp.parser.php.factory.traverser:
class: 'GrumPHP\Parser\Php\Factory\TraverserFactory'
arguments:
- '@grumphp.parser.php.configurator.traverser'

grumphp.parser.php.parser:
class: 'GrumPHP\Parser\Php\PhpParser'
arguments:
- '@grumphp.parser.php.factory.parser'
- '@grumphp.parser.php.factory.traverser'

#
# AVAILABLE PHP PARSER VISITORS ...
#
grumphp.parser.php.visitor.declare_strict_types:
class: 'GrumPHP\Parser\Php\Visitor\DeclareStrictTypesVisitor'
tags:
- {name: 'php_parser.visitor'}

grumphp.parser.php.visitor.forbidden_function_calls:
class: 'GrumPHP\Parser\Php\Visitor\ForbiddenFunctionCallsVisitor'
arguments: []
tags:
- {name: 'php_parser.visitor'}

grumphp.parser.php.visitor.nameresolver:
class: 'PhpParser\NodeVisitor\NameResolver'
arguments: []
tags:
- {name: 'php_parser.visitor'}
grumphp.parser.php.parser:
class: 'GrumPHP\Parser\Php\PhpParser'
arguments:
- '@grumphp.parser.php.factory.parser'
- '@grumphp.parser.php.factory.traverser'

grumphp.parser.php.visitor.no_exit_statements:
class: 'GrumPHP\Parser\Php\Visitor\NoExitStatementsVisitor'
arguments: []
tags:
- {name: 'php_parser.visitor'}
2 changes: 1 addition & 1 deletion resources/config/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ services:
- {name: grumphp.task, config: phpmd}

task.phpparser:
class: GrumPHP\Task\Phpparser
class: GrumPHP\Task\PhpParser
arguments:
- '@config'
- '@grumphp.parser.php.parser'
Expand Down
18 changes: 9 additions & 9 deletions src/GrumPHP/Configuration/Compiler/PhpParserCompilerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
* Class PhpParserCompilerPass
Expand All @@ -16,22 +15,23 @@ class PhpParserCompilerPass implements CompilerPassInterface
const TAG = 'php_parser.visitor';

/**
* Sets the visitors as non shared services.
* This will make sure that the state of the visitor won't need to be reset after an iteration of the traverser.
*
* All visitor Ids are registered in the traverser configurator.
* The configurator will be used to apply the configured visitors to the traverser.
*
* @param ContainerBuilder $container
*
* @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
* @throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
*/
public function process(ContainerBuilder $container)
{
$traverserFactory = $container->findDefinition('grumphp.parser.php.factory.traverser');
$taggedServices = $container->findTaggedServiceIds('php_parser.visitor');

foreach ($taggedServices as $id => $tags) {
// Make sure to start with a fresh state on every parse:
$traverserConfigurator = $container->findDefinition('grumphp.parser.php.configurator.traverser');
foreach ($container->findTaggedServiceIds(self::TAG) as $id => $tags) {
$container->findDefinition($id)->setShared(false);

// Add the node visitor to the traverser factory:
$traverserFactory->addMethodCall('addNodeVisitor', array(new Reference($id)));
$traverserConfigurator->addMethodCall('registerVisitorId', array($id));
}
}
}
151 changes: 151 additions & 0 deletions src/GrumPHP/Parser/Php/Configurator/TraverserConfigurator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

namespace GrumPHP\Parser\Php\Configurator;

use GrumPHP\Exception\RuntimeException;
use GrumPHP\Parser\Php\Context\ParserContext;
use GrumPHP\Parser\Php\Visitor\ConfigurableVisitorInterface;
use GrumPHP\Parser\Php\Visitor\ContextAwareVisitorInterface;
use PhpParser\NodeTraverserInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Class TraverserConfigurator
*
* @package GrumPHP\Parser\Php\Configurator
*/
class TraverserConfigurator
{

const CONFIGURATION_PREFIX = 'grumphp.parser.php.visitor.';

/**
* @var string[]
*/
private $registeredVisitorIds = [];

/**
* @var array
*/
private $options = [];

/**
* @var ParserContext
*/
private $context;

/**
* TraverserConfigurator constructor.
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

/**
* @param $visitorId
*/
public function registerVisitorId($visitorId)
{
$this->registeredVisitorIds[] = (string) $visitorId;
}

/**
* @param array $options
*/
public function registerOptions(array $options)
{
$this->options = $options;
}

/**
* @param ParserContext $context
*/
public function registerContext(ParserContext $context)
{
$this->context = $context;
}

/**
* @param NodeTraverserInterface $traverser
*
* @throws \GrumPHP\Exception\RuntimeException
*/
public function configure(NodeTraverserInterface $traverser)
{
$this->guardTaskHasVisitors();
$this->guardContextIsRegistered();

$configuredVisitors = $this->normalizeConfiguredVisitors($this->options['visitors']);
$configuredVisitorIds = array_keys($configuredVisitors);
$visitorIds = array_values(array_intersect($this->registeredVisitorIds, $configuredVisitorIds));
$unknownConfiguredVisitorIds = array_diff($configuredVisitorIds, $this->registeredVisitorIds);

if (count($unknownConfiguredVisitorIds)) {
throw new RuntimeException(
sprintf('Found unknown php_parser visitors: %s', implode(',', $unknownConfiguredVisitorIds))
);
}

foreach ($visitorIds as $visitorId) {
$visitor = $this->container->get($visitorId);

if ($visitor instanceof ContextAwareVisitorInterface) {
$visitor->setContext($this->context);
}

$options = $configuredVisitors[$visitorId];
if ($visitor instanceof ConfigurableVisitorInterface && is_array($options)) {
$visitor->configure($options);
}

$traverser->addVisitor($visitor);
}

// Reset context to make sure the next configure call will actually run in the correct context:
$this->context = null;
}

/**
* Add the configuration prefix to a string if it is not added yet.
* This makes it possible to use short visitor names in the configuration.
*
* @param array $visitorIds
*
* @return array
*/
private function normalizeConfiguredVisitors($visitorIds)
{
$matcher = '/^' . preg_quote(self::CONFIGURATION_PREFIX, '/') . '/';
$newVisitors = [];
foreach ($visitorIds as $visitorId => $config) {
$newVisitorId = preg_match($matcher, $visitorId) ? $visitorId : self::CONFIGURATION_PREFIX . $visitorId;
$newVisitors[$newVisitorId] = $config;
}

return $newVisitors;
}

/**
*
* @throws \GrumPHP\Exception\RuntimeException
*/
private function guardTaskHasVisitors()
{
if (!isset($this->options['visitors'])) {
throw new RuntimeException('The parser context is not set. Please register it to the configurator!');
}
}

/**
* @throws \GrumPHP\Exception\RuntimeException
*/
private function guardContextIsRegistered()
{
if (!$this->context instanceof ParserContext) {
throw new RuntimeException('The parser context is not set. Please register it to the configurator!');
}
}
}
19 changes: 2 additions & 17 deletions src/GrumPHP/Parser/Php/Factory/ParserFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

namespace GrumPHP\Parser\Php\Factory;

use GrumPHP\Parser\Php\PhpParser;
use GrumPHP\Task\PhpParser;
use PhpParser\ParserFactory as PhpParserFactory;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* Class ParserFactory
Expand All @@ -20,23 +19,9 @@ class ParserFactory
*/
public function createFromOptions(array $options)
{
$config = $this->getConfigurableOptions()->resolve($options);
$kind = ($config['kind'] === PhpParser::KIND_PHP5)
$kind = ($options['kind'] === PhpParser::KIND_PHP5)
? PhpParserFactory::PREFER_PHP5 : PhpParserFactory::PREFER_PHP7;

return (new PhpParserFactory())->create($kind);
}

/**
* @return OptionsResolver
*/
public function getConfigurableOptions()
{
$resolver = new OptionsResolver();
$resolver->setRequired('kind');
$resolver->setAllowedTypes('kind', 'string');
$resolver->setAllowedValues('kind', [PhpParser::KIND_PHP5, PhpParser::KIND_PHP7]);

return $resolver;
}
}
30 changes: 15 additions & 15 deletions src/GrumPHP/Parser/Php/Factory/TraverserFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

namespace GrumPHP\Parser\Php\Factory;

use GrumPHP\Parser\Php\Configurator\TraverserConfigurator;
use GrumPHP\Parser\Php\Context\ParserContext;
use GrumPHP\Parser\Php\Visitor\ContextAwareVisitorInterface;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor;

/**
* Class TraverserFactory
Expand All @@ -15,33 +14,34 @@
class TraverserFactory
{
/**
* @var NodeVisitor[]
* @var TraverserConfigurator
*/
private $visitors = [];
private $configurator;

/**
* @param NodeVisitor $visitor
* TraverserFactory constructor.
*
* @param TraverserConfigurator $configurator
*/
public function addNodeVisitor(NodeVisitor $visitor)
public function __construct(TraverserConfigurator $configurator)
{
$this->visitors[] = $visitor;
$this->configurator = $configurator;
}

/**
* @param array $parserOptions
* @param ParserContext $context
*
* @return NodeTraverser
* @throws \GrumPHP\Exception\RuntimeException
*/
public function createForContext(ParserContext $context)
public function createForTaskContext(array $parserOptions, ParserContext $context)
{
$traverser = new NodeTraverser();
foreach ($this->visitors as $visitor) {
$traverser->addVisitor($visitor);
$this->configurator->registerOptions($parserOptions);
$this->configurator->registerContext($context);

if ($visitor instanceof ContextAwareVisitorInterface) {
$visitor->setContext($context);
}
}
$traverser = new NodeTraverser();
$this->configurator->configure($traverser);

return $traverser;
}
Expand Down
Loading

0 comments on commit a1cb599

Please sign in to comment.