204 lines
5.9 KiB
PHP
204 lines
5.9 KiB
PHP
<?php
|
|
|
|
namespace Phabel;
|
|
|
|
use PhpParser\Node;
|
|
use PhpParser\Parser;
|
|
use PhpParser\ParserFactory;
|
|
use SplQueue;
|
|
|
|
/**
|
|
* AST traverser.
|
|
*
|
|
* @author Daniil Gentili <daniil@daniil.it>
|
|
* @license MIT
|
|
*/
|
|
class Traverser
|
|
{
|
|
/**
|
|
* Plugin queue.
|
|
*
|
|
* @return SplQueue<SplQueue<Plugin>>
|
|
*/
|
|
private SplQueue $queue;
|
|
/**
|
|
* Parser instance.
|
|
*/
|
|
private Parser $parser;
|
|
/**
|
|
* Plugin queue for specific package.
|
|
*
|
|
* @return SplQueue<SplQueue<Plugin>>|null
|
|
*/
|
|
private ?SplQueue $packageQueue;
|
|
/**
|
|
* Generate traverser from basic plugin instances.
|
|
*
|
|
* @param Plugin ...$plugin Plugins
|
|
*
|
|
* @return self
|
|
*/
|
|
public static function fromPlugin(Plugin ...$plugin): self
|
|
{
|
|
$queue = new SplQueue;
|
|
foreach ($plugin as $p) {
|
|
$queue->enqueue($p);
|
|
}
|
|
$final = new SplQueue;
|
|
$final->enqueue($queue);
|
|
return new self($final);
|
|
}
|
|
/**
|
|
* AST traverser.
|
|
*
|
|
* @return SplQueue<SplQueue<Plugin>> $queue Plugin queue
|
|
*/
|
|
public function __construct(SplQueue $queue = null)
|
|
{
|
|
$this->queue = $queue ?? new SplQueue;
|
|
$this->parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
|
}
|
|
/**
|
|
* Set package name.
|
|
*
|
|
* @param string $package Package name
|
|
*
|
|
* @return void
|
|
*/
|
|
public function setPackage(string $package): void
|
|
{
|
|
$this->packageQueue = new SplQueue;
|
|
$newQueue = new SplQueue;
|
|
foreach ($this->queue as $queue) {
|
|
if ($newQueue->count()) {
|
|
$this->packageQueue->enqueue($newQueue);
|
|
$newQueue = new SplQueue;
|
|
}
|
|
/** @var Plugin */
|
|
foreach ($queue as $plugin) {
|
|
if ($plugin->shouldRun($package)) {
|
|
$newQueue->enqueue($plugin);
|
|
}
|
|
}
|
|
}
|
|
if ($newQueue->count()) {
|
|
$this->packageQueue->enqueue($newQueue);
|
|
}
|
|
}
|
|
/**
|
|
* Traverse AST of file.
|
|
*
|
|
* @param string $file File
|
|
*
|
|
* @return void
|
|
*/
|
|
public function traverse(string $file): void
|
|
{
|
|
/** @var SplQueue<SplQueue<Plugin>> */
|
|
$reducedQueue = new SplQueue;
|
|
$newQueue = new SplQueue;
|
|
foreach ($this->packageQueue ?? $this->queue as $queue) {
|
|
if ($newQueue->count()) {
|
|
$reducedQueue->enqueue($newQueue);
|
|
$newQueue = new SplQueue;
|
|
}
|
|
/** @var Plugin */
|
|
foreach ($queue as $plugin) {
|
|
if ($plugin->shouldRunFile($file)) {
|
|
$newQueue->enqueue($plugin);
|
|
}
|
|
}
|
|
}
|
|
if ($newQueue->count()) {
|
|
$reducedQueue->enqueue($newQueue);
|
|
} elseif (!$reducedQueue->count()) {
|
|
return;
|
|
}
|
|
$ast = new RootNode($this->parser->parse(\file_get_contents($file)) ?? []);
|
|
$this->traverseAst($ast, $reducedQueue);
|
|
}
|
|
/**
|
|
* Traverse AST.
|
|
*
|
|
* @param Node $node Initial node
|
|
* @param SplQueue $pluginQueue Plugin queue (optional)
|
|
*
|
|
* @return Context
|
|
*/
|
|
public function traverseAst(Node &$node, SplQueue $pluginQueue = null): Context
|
|
{
|
|
$context = new Context;
|
|
$context->push($node);
|
|
foreach ($pluginQueue ?? $this->packageQueue ?? $this->queue as $queue) {
|
|
$this->traverseNode($ast, $queue, $context);
|
|
}
|
|
return $context;
|
|
}
|
|
/**
|
|
* Traverse node.
|
|
*
|
|
* @param Node &$node Node
|
|
* @param SplQueue<Plugin> $plugins Plugins
|
|
* @param Context $context Context
|
|
*
|
|
* @return void
|
|
*/
|
|
public function traverseNode(Node &$node, SplQueue $plugins, Context $context): void
|
|
{
|
|
foreach ($plugins as $plugin) {
|
|
foreach (PluginCache::enterMethods(\get_class($plugin)) as $type => $methods) {
|
|
if (!$node instanceof $type) {
|
|
continue;
|
|
}
|
|
foreach ($methods as $method) {
|
|
$result = $plugin->{$method}($node, $context);
|
|
if ($result instanceof Node) {
|
|
if (!$result instanceof $node) {
|
|
$node = $result;
|
|
continue 2;
|
|
}
|
|
$node = $result;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$context->push($node);
|
|
foreach ($node->getSubNodeNames() as $name) {
|
|
$node->setAttribute('currentNode', $name);
|
|
|
|
$subNode = &$node->{$name};
|
|
if (\is_array($subNode)) {
|
|
for ($index = 0; $index < \count($subNode);) {
|
|
$node->setAttribute('currentNodeIndex', $index);
|
|
$this->traverseNode($subNodeNode, $plugins, $context);
|
|
$index = $node->getAttribute('currentNodeIndex');
|
|
do {
|
|
$index++;
|
|
} while (\in_array($index, $node->getAttribute('skipNodes', [])));
|
|
}
|
|
$node->setAttribute('skipNodes', []);
|
|
} else {
|
|
$this->traverseNode($subNode, $plugins, $context);
|
|
}
|
|
}
|
|
$context->pop();
|
|
foreach ($plugins as $plugin) {
|
|
foreach (PluginCache::leaveMethods(\get_class($plugin)) as $type => $methods) {
|
|
if (!$node instanceof $type) {
|
|
continue;
|
|
}
|
|
foreach ($methods as $method) {
|
|
$result = $plugin->{$method}($node, $context);
|
|
if ($result instanceof Node) {
|
|
if (!$result instanceof $node) {
|
|
$node = $result;
|
|
continue 2;
|
|
}
|
|
$node = $result;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|