Finish graph logic

This commit is contained in:
Daniil Gentili 2020-08-09 13:49:19 +02:00
parent 70710fb26d
commit 0fb5cf8d88
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
28 changed files with 740 additions and 358 deletions

7
.gitignore vendored
View File

@ -1,2 +1,7 @@
/vendor/ .vscode
build
composer.lock composer.lock
phpunit.xml
vendor
.php_cs.cache
coverage

13
.php_cs.dist Normal file
View File

@ -0,0 +1,13 @@
<?php
$config = new Amp\CodeStyle\Config();
$config->getFinder()
->in(__DIR__ . '/examples')
->in(__DIR__ . '/src')
->in(__DIR__ . '/test');
$cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__;
$config->setCacheFile($cacheDir . '/.php_cs.cache');
return $config;

View File

@ -5,11 +5,22 @@
"require": { "require": {
"nikic/php-parser": "^4.7" "nikic/php-parser": "^4.7"
}, },
"require-dev": {
"phpunit/phpunit": "^7 | ^8 | ^9",
"amphp/php-cs-fixer-config": "dev-master"
},
"license": "MIT", "license": "MIT",
"authors": [ "authors": [{
{ "name": "Daniil Gentili",
"name": "Daniil Gentili", "email": "daniil@daniil.it"
"email": "daniil@daniil.it" }],
} "scripts": {
] "check": [
"@cs",
"@test"
],
"cs": "php-cs-fixer fix -v --diff --dry-run",
"cs-fix": "php-cs-fixer fix -v --diff",
"test": "@php -dzend.assertions=1 -dassert.exception=1 ./vendor/bin/phpunit --coverage-text"
}
} }

View File

@ -4,5 +4,4 @@ namespace Phabel;
class Context class Context
{ {
} }

View File

@ -6,5 +6,4 @@ use Phabel\Plugin;
class UniqueId extends Plugin class UniqueId extends Plugin
{ {
} }

View File

@ -18,6 +18,16 @@ abstract class Plugin implements PluginInterface
* Configuration array. * Configuration array.
*/ */
private array $config = []; private array $config = [];
/**
* Set configuration array.
*
* @param array $config
* @return void
*/
public function setConfigArray(array $config)
{
$this->config = $config;
}
/** /**
* Replace node of one type with another. * Replace node of one type with another.
* *
@ -141,14 +151,28 @@ abstract class Plugin implements PluginInterface
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public static function needs(): array public static function runAfter(): array
{ {
return []; return [];
} }
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public static function extends(): array public static function runBefore(): array
{
return [];
}
/**
* {@inheritDoc}
*/
public static function runWithBefore(): array
{
return [];
}
/**
* {@inheritDoc}
*/
public static function runWithAfter(): array
{ {
return []; return [];
} }

View File

@ -4,7 +4,6 @@ namespace Spatie\Php7to5\NodeVisitors;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\NodeVisitorAbstract; use PhpParser\NodeVisitorAbstract;
use PhpParser\ParserFactory;
class YieldFromReplacer extends NodeVisitorAbstract class YieldFromReplacer extends NodeVisitorAbstract
{ {

View File

@ -3,13 +3,7 @@
namespace Spatie\Php7to5\NodeVisitors; namespace Spatie\Php7to5\NodeVisitors;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\Node\Stmt\Declare_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\NodeVisitorAbstract; use PhpParser\NodeVisitorAbstract;
use Spatie\Php7to5\Converter;
use Spatie\Php7to5\Exceptions\InvalidPhpCode;
class YieldReturnDetector extends NodeVisitorAbstract class YieldReturnDetector extends NodeVisitorAbstract
{ {
@ -26,13 +20,13 @@ class YieldReturnDetector extends NodeVisitorAbstract
if ($node instanceof Node\Expr\Yield_ || if ($node instanceof Node\Expr\Yield_ ||
$node instanceof Node\Expr\YieldFrom $node instanceof Node\Expr\YieldFrom
) { ) {
end($this->hasYield)->hasYield = true; \end($this->hasYield)->hasYield = true;
} }
} }
public function leaveNode(Node $node) public function leaveNode(Node $node)
{ {
if ($node instanceof Node\FunctionLike) { if ($node instanceof Node\FunctionLike) {
array_pop($this->hasYield); \array_pop($this->hasYield);
} }
} }
} }

View File

@ -24,7 +24,7 @@ class YieldReturnReplacer extends NodeVisitorAbstract
public function leaveNode(Node $node) public function leaveNode(Node $node)
{ {
if ($node instanceof Node\FunctionLike) { if ($node instanceof Node\FunctionLike) {
array_pop($this->functions); \array_pop($this->functions);
return; return;
} }
if (!$node instanceof Node\Stmt\Return_) { if (!$node instanceof Node\Stmt\Return_) {
@ -34,7 +34,7 @@ class YieldReturnReplacer extends NodeVisitorAbstract
return new Node\Stmt\Return_(); return new Node\Stmt\Return_();
} }
if (!(end($this->functions)->hasYield ?? false)) { if (!(\end($this->functions)->hasYield ?? false)) {
return; return;
} }

157
src/PluginCache.php Normal file
View File

@ -0,0 +1,157 @@
<?php
namespace Phabel;
/**
* Caches plugin information.
*/
class PluginCache
{
/**
* Enter method names for each plugin.
*
* @var array<class-string<PluginInterface>, string[]>
*/
private static array $enterMethods = [];
/**
* Leave method names.
*
* @var array<class-string<PluginInterface>, string[]>
*/
private static array $leaveMethods = [];
/**
* Cache method information.
*
* @param class-string<PluginInterface> $plugin Plugin
*
* @return void
*/
private static function cacheMethods(string $plugin): void
{
if (!isset(self::$enterMethods[$plugin])) {
self::$enterMethods[$plugin] = [];
self::$leaveMethods[$plugin] = [];
foreach (\get_class_methods($plugin) as $method) {
if (\str_starts_with($method, 'enter')) {
self::$enterMethods[$plugin] []= $method;
} elseif (\str_starts_with($method, 'leave')) {
self::$leaveMethods[$plugin] []= $method;
}
}
}
}
/**
* Return whether this plugin can be required by another plugin.
*
* If false, this plugin should only be extended by other plugins, to reduce complexity.
*
* @param class-string<PluginInterface> $plugin Plugin
*
* @return boolean
*/
public static function canBeRequired(string $plugin): bool
{
self::cacheMethods($plugin);
return empty(self::$leaveMethods[$plugin]);
}
/**
* Get enter methods array.
*
* @param class-string<PluginInterface> $plugin Plugin name
*
* @return string[]
*/
public static function enterMethods(string $plugin): array
{
self::cacheMethods($plugin);
return self::$enterMethods[$plugin];
}
/**
* Get leave methods array.
*
* @param class-string<PluginInterface> $plugin Plugin name
*
* @return string[]
*/
public static function leaveMethods(string $plugin): array
{
self::cacheMethods($plugin);
return self::$leaveMethods[$plugin];
}
/**
* Get runBefore requirements.
*
* @param class-string<PluginInterface> $plugin Plugin
*
* @return array<class-string<PluginInterface>, array>
*/
public static function runBefore(string $plugin): array
{
/** @var array<class-string<PluginInterface>, array<class-string<PluginInterface>, array>> */
static $cache = [];
if (isset($cache[$plugin])) {
return $cache[$plugin];
}
return $cache[$plugin] = self::simplify($plugin::runBefore());
}
/**
* Get runAfter requirements.
*
* @param class-string<PluginInterface> $plugin Plugin
*
* @return array<class-string<PluginInterface>, array>
*/
public static function runAfter(string $plugin): array
{
/** @var array<class-string<PluginInterface>, array<class-string<PluginInterface>, array>> */
static $cache = [];
if (isset($cache[$plugin])) {
return $cache[$plugin];
}
return $cache[$plugin] = self::simplify($plugin::runAfter());
}
/**
* Get runWithBefore requirements.
*
* @param class-string<PluginInterface> $plugin Plugin
*
* @return array<class-string<PluginInterface>, array>
*/
public static function runWithBefore(string $plugin): array
{
/** @var array<class-string<PluginInterface>, array<class-string<PluginInterface>, array>> */
static $cache = [];
if (isset($cache[$plugin])) {
return $cache[$plugin];
}
return $cache[$plugin] = self::simplify($plugin::runWithBefore());
}
/**
* Get runWithAfter requirements.
*
* @param class-string<PluginInterface> $plugin Plugin
*
* @return array<class-string<PluginInterface>, array>
*/
public static function runWithAfter(string $plugin): array
{
/** @var array<class-string<PluginInterface>, array<class-string<PluginInterface>, array>> */
static $cache = [];
if (isset($cache[$plugin])) {
return $cache[$plugin];
}
return $cache[$plugin] = self::simplify($plugin::runWithAfter());
}
/**
* Simplify requirements.
*
* @param (array<class-string<PluginInterface>, array>|class-string<PluginInterface>[]) $requirements Requirements
*
* @return array<class-string<PluginInterface>, array>
*/
private static function simplify(array $requirements): array
{
return isset($requirements[0]) ? \array_fill_keys($requirements, []) : $requirements;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Phabel\PluginGraph;
use Phabel\PluginInterface;
class CircularException extends \Exception
{
/**
* Plugin array.
*
* @var class-string<PluginInterface>[]
*/
private array $plugins = [];
/**
* Constructor.
*
* @param class-string<PluginInterface>[] $plugins Plugin array
* @param \Throwable $previous Previous exception
*/
public function __construct(array $plugins, \Throwable $previous = null)
{
$this->plugins = $plugins;
parent::__construct("Detected circular reference: ".\implode(" => ", $plugins), 0, $previous);
}
/**
* Get plugins.
*
* @return class-string<PluginInterface>[]
*/
public function getPlugins(): array
{
return $this->plugins;
}
}

View File

@ -2,24 +2,22 @@
namespace Phabel\PluginGraph; namespace Phabel\PluginGraph;
use Phabel\PluginInterface; /**
* Graph API wrapper.
*/
class Graph class Graph
{ {
/** /**
* Plugin nodes, indexed by plugin name+config. * Graph instance.
*
* @var array<class-string<PluginInterface>, array<string, Node>>
*/ */
private array $plugins = []; private GraphInternal $graph;
/** /**
* Package contexts. * Constructr.
*
* @var PackageContext[]
*/ */
private array $packageContexts = []; public function __construct()
{
$this->graph = new GraphInternal;
}
/** /**
* Get new package context. * Get new package context.
* *
@ -27,9 +25,7 @@ class Graph
*/ */
public function getPackageContext(): PackageContext public function getPackageContext(): PackageContext
{ {
$packageContext = new PackageContext; return $this->graph->getPackageContext();
$this->packageContexts []= $packageContext;
return $packageContext;
} }
/** /**
@ -45,31 +41,15 @@ class Graph
*/ */
public function addPlugin(string $plugin, array $config, PackageContext $ctx): array public function addPlugin(string $plugin, array $config, PackageContext $ctx): array
{ {
$configs = $plugin::splitConfig($config); return $this->graph->addPlugin($plugin, $config, $ctx);
$nodes = [];
foreach ($configs as $config) {
$nodes []= $this->addPluginInternal($plugin, $config, $ctx);
}
return $nodes;
} }
/** /**
* Add plugin. * Flatten graph.
* *
* @param string $plugin Plugin to add * @return SplQueue<SplQueue<Plugin>>
* @param array $config Plugin configuration
* @param PackageContext $ctx Package context
*
* @psalm-param class-string<PluginInterface> $plugin Plugin name
*
* @return Node
*/ */
private function addPluginInternal(string $plugin, array $config, PackageContext $ctx): Node public function flatten(): \SplQueue
{ {
$configStr = \var_export($config, true); return $this->graph->flatten();
if (isset($this->plugins[$plugin][$configStr])) {
return $this->plugins[$plugin][$configStr]->addPackageContext($ctx);
}
$this->plugins[$plugin][$configStr] = $node = new Node;
return $node->init($this, $plugin, $config, $ctx);
} }
} }

View File

@ -0,0 +1,132 @@
<?php
namespace Phabel\PluginGraph;
use Phabel\PluginInterface;
use SplObjectStorage;
use SplQueue;
class GraphInternal
{
/**
* Plugin nodes, indexed by plugin name+config.
*
* @var array<class-string<PluginInterface>, array<string, Node>>
*/
private array $plugins = [];
/**
* Package contexts.
*
* @var PackageContext[]
*/
private array $packageContexts = [];
/**
* Stores list of Nodes that are not required by any other node.
*
* @var SplObjectStorage<Node, void>
*/
private SplObjectStorage $unlinkedNodes;
/**
* Constructor.
*/
public function __construct()
{
$this->unlinkedNodes = new SplObjectStorage;
}
/**
* Get new package context.
*
* @return PackageContext
*/
public function getPackageContext(): PackageContext
{
$packageContext = new PackageContext;
$this->packageContexts []= $packageContext;
return $packageContext;
}
/**
* Add plugin.
*
* @param class-string<PluginInterface> $plugin Plugin to add
* @param array $config Plugin configuration
* @param PackageContext $ctx Package context
*
* @return Node[]
*/
public function addPlugin(string $plugin, array $config, PackageContext $ctx): array
{
if ($config === ['*']) {
if (isset($this->plugins[$plugin])) {
return \array_map(fn (Node $node) => $node->addPackageContext($ctx), $this->plugins[$plugin]);
}
$config = [];
}
$configs = $plugin::splitConfig($config);
$nodes = [];
foreach ($configs as $config) {
$nodes []= $this->addPluginInternal($plugin, $config, $ctx);
}
return $nodes;
}
/**
* Add plugin.
*
* @param string $plugin Plugin to add
* @param array $config Plugin configuration
* @param PackageContext $ctx Package context
*
* @psalm-param class-string<PluginInterface> $plugin Plugin name
*
* @return Node
*/
private function addPluginInternal(string $plugin, array $config, PackageContext $ctx): Node
{
$configStr = \var_export($config, true);
if (isset($this->plugins[$plugin][$configStr])) {
return $this->plugins[$plugin][$configStr]->addPackageContext($ctx);
}
$this->plugins[$plugin][$configStr] = $node = new Node($this);
$this->unlinkedNodes->attach($node);
return $node->init($plugin, $config, $ctx);
}
/**
* Set unlinked node as linked.
*
* @param Node $node
*
* @return void
*/
public function linkNode(Node $node): void
{
$this->unlinkedNodes->detach($node);
}
/**
* Flatten graph.
*
* @return SplQueue<SplQueue<Plugin>>
*/
public function flatten(): \SplQueue
{
if (!$this->plugins) {
return new \SplQueue;
}
if ($this->unlinkedNodes->count()) {
foreach ($this->unlinkedNodes as $node) {
if (isset($initNode)) {
$node = $initNode->merge($node);
}
/** @var Node */
$initNode = $node;
}
return $initNode->circular()->flatten();
}
return \array_values(\array_values($this->plugins)[0])[0]->circular()->flatten();
}
}

View File

@ -2,8 +2,10 @@
namespace Phabel\PluginGraph; namespace Phabel\PluginGraph;
use Phabel\PluginCache;
use Phabel\PluginInterface; use Phabel\PluginInterface;
use SplObjectStorage; use SplObjectStorage;
use SplQueue;
/** /**
* Represents a plugin with a certain configuration. * Represents a plugin with a certain configuration.
@ -11,66 +13,85 @@ use SplObjectStorage;
class Node class Node
{ {
/** /**
* Plugin name. * Plugins and configs.
*
* @var Plugin
*/ */
private string $plugin = ''; private Plugin $plugin;
/** /**
* Plugin configuration. * Original plugin name.
*
* @var class-string<PluginInterface>
*/ */
private array $config = []; private string $name;
/** /**
* Associated package contexts. * Associated package contexts.
*
* @var SplObjectStorage<PackageContext, void>
*/ */
private SplObjectStorage $packageContexts; private SplObjectStorage $packageContexts;
/** /**
* Nodes that this node requires. * Nodes that this node requires.
* *
* @var Node[] * @var SplObjectStorage<Node, void>
*/ */
private array $requires = []; private SplObjectStorage $requires;
/** /**
* Nodes that this node extends. * Nodes that this node extends.
* *
* @var Node[] * @var SplObjectStorage<Node, void>
*/ */
private array $extends = []; private SplObjectStorage $extends;
/** /**
* Nodes that require this node. * Nodes that require this node.
* *
* @var Node[] * @var SplObjectStorage<Node, void>
*/ */
private array $requiredBy = []; private SplObjectStorage $requiredBy;
/** /**
* Nodes that extend this node. * Nodes that extend this node.
* *
* @var Node[] * @var SplObjectStorage<Node, void>
*/ */
private array $extendedBy = []; private SplObjectStorage $extendedBy;
/** /**
* Whether this node was visited when looking for circular requirement references. * Graph instance.
*/ */
private bool $visitedRequires = false; private GraphInternal $graph;
/** /**
* Whether this node was visited when looking for circular requirement references. * Whether this node was visited when looking for circular requirements.
*/ */
private bool $visitedExtends = false; private bool $visitedCircular = false;
/**
* Whether this node can be required, or only extended.
*/
private bool $canBeRequired = true;
/** /**
* Constructor. * Constructor.
*
* @param GraphInternal $graph Graph instance
*/ */
public function __construct() public function __construct(GraphInternal $graph)
{ {
$this->graph = $graph;
$this->packageContexts = new SplObjectStorage; $this->packageContexts = new SplObjectStorage;
$this->requiredBy = new SplObjectStorage;
$this->extendedBy = new SplObjectStorage;
$this->requires = new SplObjectStorage;
$this->extends = new SplObjectStorage;
} }
/** /**
* Initialization function. * Initialization function.
* *
* @param Graph $graph Graph instance
* @param string $plugin Plugin name * @param string $plugin Plugin name
* @param array $config Plugin configuration * @param array $config Plugin configuration
* @param PackageContext $ctx Context * @param PackageContext $ctx Context
@ -79,25 +100,31 @@ class Node
* *
* @return self * @return self
*/ */
public function init(Graph $graph, string $plugin, array $config, PackageContext $ctx): self public function init(string $plugin, array $config, PackageContext $ctx): self
{ {
$this->plugin = $plugin; $this->name = $plugin;
$this->config = $config; $this->plugin = new Plugin($plugin, $config);
$this->packageContexts->attach($ctx); $this->packageContexts->attach($ctx);
$requirements = self::simplify($plugin::needs()); $this->canBeRequired = PluginCache::canBeRequired($plugin);
$extends = self::simplify($plugin::extends()); foreach (PluginCache::runAfter($plugin) as $class => $config) {
foreach ($this->graph->addPlugin($class, $config, $ctx) as $node) {
foreach ($requirements as $class => $config) { $this->require($node);
foreach ($graph->addPlugin($class, $config, $ctx) as $node) {
$this->requires []= $node;
$node->requiredBy []= $this;
} }
} }
foreach ($extends as $class => $config) { foreach (PluginCache::runBefore($plugin) as $class => $config) {
foreach ($graph->addPlugin($class, $config, $ctx) as $node) { foreach ($this->graph->addPlugin($class, $config, $ctx) as $node) {
$this->extends []= $node; $node->require($this);
$node->extendedBy []= $this; }
}
foreach (PluginCache::runWithAfter($plugin) as $class => $config) {
foreach ($this->graph->addPlugin($class, $config, $ctx) as $node) {
$this->extend($node);
}
}
foreach (PluginCache::runWithBefore($plugin) as $class => $config) {
foreach ($this->graph->addPlugin($class, $config, $ctx) as $node) {
$node->extend($this);
} }
} }
@ -105,15 +132,43 @@ class Node
} }
/** /**
* Simplify requirements. * Make node require another node.
* *
* @param (array<class-string<PluginInterface>, array>|class-string<PluginInterface>[]) $requirements Requirements * @param self $node Node
* *
* @return array<class-string<PluginInterface>, array> * @return void
*/ */
private static function simplify(array $requirements): array private function require(self $node): void
{ {
return isset($requirements[0]) ? \array_fill_keys($requirements, []) : $requirements; if (!$node->canBeRequired) {
$this->extend($node);
return;
}
if ($this->extends->contains($node) || $node->extendedBy->contains($this)) {
$this->extends->detach($node);
$node->extendedBy->detach($this);
}
$this->requires->attach($node);
$node->requiredBy->attach($this);
$this->graph->linkNode($this);
}
/**
* Make node extend another node.
*
* @param self $node Node
*
* @return void
*/
private function extend(self $node): void
{
if ($this->requires->contains($node) || $node->requiredBy->contains($this)) {
return;
}
$this->extends->attach($node);
$node->extendedBy->attach($this);
$this->graph->linkNode($this);
} }
/** /**
@ -139,21 +194,133 @@ class Node
} }
/** /**
* Check if this node requires only one other node. * Merge node with another node.
* *
* @return boolean * @param self $other Other node
*
* @return Node
*/ */
private function oneRequire(): bool public function merge(self $other): Node
{ {
return \count($this->requires) === 1 && empty($this->extends); $this->plugin->merge($other->plugin);
foreach ($other->requiredBy as $node) {
$node->require($this);
$node->requires->detach($other);
}
foreach ($other->extendedBy as $node) {
$node->extend($this);
$node->extends->detach($other);
}
return $this;
}
/**
* Flatten tree.
*
* @return SplQueue<SplQueue<PluginInterface>>
*/
public function flatten(): SplQueue
{
/** @var SplQueue<PluginInterface> */
$initQueue = new SplQueue;
/** @var SplQueue<SplQueue<PluginInterface>> */
$queue = new SplQueue;
$queue->enqueue($initQueue);
$this->flattenInternal($queue);
return $queue;
} }
/** /**
* Check if this node extends only one other node. * Look for circular references.
* *
* @return boolean * @return self
*/ */
private function oneExtend(): bool public function circular(): self
{ {
return \count($this->extends) === 1; if ($this->visitedCircular) {
$plugins = [$this->name];
foreach (\debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, DEBUG_BACKTRACE_PROVIDE_OBJECT) as $frame) {
$plugins []= $frame['object']->name;
if ($frame['object'] === $this) {
break;
}
}
throw new CircularException($plugins);
}
$this->visitedCircular = true;
foreach ($this->requiredBy as $node) {
$node->circular();
}
foreach ($this->extendedBy as $node) {
$node->circular();
}
$this->visitedCircular = false;
return $this;
}
/**
* Internal flattening.
*
* @param SplQueue<SplQueue<PluginInterface>> $splQueue Queue
*
* @return void
*/
private function flattenInternal(SplQueue $splQueue): void
{
$queue = $splQueue->top();
$this->plugin->enqueue($queue);
/** @var SplQueue<Node> */
$extendedBy = new SplQueue;
$prevNode = null;
foreach ($this->extendedBy as $node) {
if (\count($node->requires) + \count($node->extends) === 1) {
if ($prevNode instanceof self) {
$node->merge($prevNode);
}
$prevNode = $node;
} else {
$extendedBy->enqueue($node);
}
}
if ($prevNode) {
$extendedBy->enqueue($prevNode);
}
/** @var SplQueue<Node> */
$requiredBy = new SplQueue;
$prevNode = null;
foreach ($this->requiredBy as $node) {
if (\count($node->requires) + \count($node->extends) === 1) {
if ($prevNode instanceof self) {
$node->merge($prevNode);
}
$prevNode = $node;
} else {
$requiredBy->enqueue($node);
}
}
if ($prevNode) {
$requiredBy->enqueue($prevNode);
}
foreach ($extendedBy as $node) {
$node->extends->detach($this);
if (\count($node->extends) + \count($node->requires) === 0) {
$node->flattenInternal($splQueue);
}
}
foreach ($requiredBy as $node) {
$node->requires->detach($this);
if (\count($node->extends) + \count($node->requires) === 0) {
$splQueue->enqueue(new SplQueue);
$node->flattenInternal($splQueue);
}
}
} }
} }

View File

@ -25,7 +25,7 @@ class PackageContext
$this->packages[$package] = null; $this->packages[$package] = null;
} }
/** /**
* Merge two contexts * Merge two contexts.
* *
* @param self $other Other context * @param self $other Other context
* *
@ -38,12 +38,12 @@ class PackageContext
} }
/** /**
* Get package list * Get package list.
* *
* @return array * @return array
*/ */
public function getPackages(): array public function getPackages(): array
{ {
return array_values($this->packages); return \array_values($this->packages);
} }
} }

View File

@ -0,0 +1,66 @@
<?php
namespace Phabel\PluginGraph;
use Phabel\PluginInterface;
use SplQueue;
/**
* Representation of multiple plugins+configs.
*/
class Plugin
{
/**
* Plugin configs, indexed by plugin name.
*
* @var array<class-string<PluginInterface>, array[]>
*/
private array $plugins = [];
/**
* Constructor.
*
* @param class-string<PluginInterface> $plugin Plugin
* @param array $config Config
*/
public function __construct(string $plugin, array $config)
{
$this->plugins[$plugin] = [$config];
}
/**
* Merge with other plugins.
*
* @param self $other Plugins
*
* @return void
*/
public function merge(self $other): void
{
foreach ($other->plugins as $plugin => $configs) {
if (isset($this->plugins[$plugin])) {
$this->plugins[$plugin] = \array_merge($this->plugins[$plugin], $configs);
} else {
$this->plugins[$plugin] = $configs;
}
}
}
/**
* Enqueue plugins.
*
* @param SplQueue<PluginInterface> $queue
*
* @return void
*/
public function enqueue(SplQueue $queue): void
{
foreach ($this->plugins as $plugin => $configs) {
foreach ($plugin::mergeConfigs(...$configs) as $config) {
$pluginObj = new $plugin;
$pluginObj->setConfigArray($config);
$queue->enqueue($pluginObj);
}
}
}
}

View File

@ -16,7 +16,15 @@ interface PluginInterface
* *
* @psalm-return class-string<PluginInterface>[]|array<class-string<PluginInterface>, array> * @psalm-return class-string<PluginInterface>[]|array<class-string<PluginInterface>, array>
*/ */
public static function needs(): array; public static function runAfter(): array;
/**
* Specify which plugins should run before this plugin.
*
* @return array Plugin name(s)
*
* @psalm-return class-string<PluginInterface>[]|array<class-string<PluginInterface>, array>
*/
public static function runBefore(): array;
/** /**
* Specify which plugins does this plugin extend. * Specify which plugins does this plugin extend.
* *
@ -28,7 +36,14 @@ interface PluginInterface
* *
* @psalm-return class-string<PluginInterface>[]|array<class-string<PluginInterface>, array> * @psalm-return class-string<PluginInterface>[]|array<class-string<PluginInterface>, array>
*/ */
public static function extends(): array; public static function runWithBefore(): array;
/**
*
* @return array Plugin name(s)
*
* @psalm-return class-string<PluginInterface>[]|array<class-string<PluginInterface>, array>
*/
public static function runWithAfter(): array;
/** /**
* Specify a list of composer dependencies. * Specify a list of composer dependencies.
@ -36,6 +51,14 @@ interface PluginInterface
* @return array * @return array
*/ */
public static function composerRequires(): array; public static function composerRequires(): array;
/**
* Set configuration array.
*
* @param array $config
*
* @return void
*/
public function setConfigArray(array $config): void;
/** /**
* Get configuration key. * Get configuration key.
* *

View File

@ -53,7 +53,7 @@ class AnonymousClassReplacer extends NodeVisitorAbstract
*/ */
public function afterTraverse(array $nodes) public function afterTraverse(array $nodes)
{ {
if (count($this->anonymousClassNodes) === 0) { if (\count($this->anonymousClassNodes) === 0) {
return $nodes; return $nodes;
} }
@ -106,10 +106,10 @@ class AnonymousClassReplacer extends NodeVisitorAbstract
*/ */
protected function moveAnonymousClassesToHook(array $nodes, $hookIndex, $anonymousClassStatements) protected function moveAnonymousClassesToHook(array $nodes, $hookIndex, $anonymousClassStatements)
{ {
$preStatements = array_slice($nodes, 0, $hookIndex); $preStatements = \array_slice($nodes, 0, $hookIndex);
$postStatements = array_slice($nodes, $hookIndex); $postStatements = \array_slice($nodes, $hookIndex);
return array_merge($preStatements, $anonymousClassStatements, $postStatements); return \array_merge($preStatements, $anonymousClassStatements, $postStatements);
} }
/** /**

View File

@ -12,7 +12,7 @@ use PhpParser\Node\Name;
class ClosureCallReplacer extends Plugin class ClosureCallReplacer extends Plugin
{ {
/** /**
* Replace composite function calls * Replace composite function calls.
* *
* @param FuncCall $node Function call * @param FuncCall $node Function call
* *

View File

@ -14,7 +14,7 @@ use PhpParser\Node\Stmt\Const_;
class DefineArrayReplacer extends Plugin class DefineArrayReplacer extends Plugin
{ {
/** /**
* Convert define() arrays into const arrays * Convert define() arrays into const arrays.
* *
* @param FuncCall $node Node * @param FuncCall $node Node
* *

View File

@ -11,7 +11,7 @@ use PhpParser\Node\Stmt\UseUse;
class GroupUseReplacer extends Plugin class GroupUseReplacer extends Plugin
{ {
/** /**
* Replace group use with multiple use statements * Replace group use with multiple use statements.
* *
* @param GroupUse $node Group use statement * @param GroupUse $node Group use statement
* *

View File

@ -12,7 +12,7 @@ class ScalarTypeHintsRemover extends Plugin
* *
* @return array * @return array
*/ */
public static function needs(): array public static function runAfter(): array
{ {
return [ return [
TypeHintStripper::class => [ TypeHintStripper::class => [

View File

@ -56,18 +56,18 @@ class ThrowableReplacer extends Plugin
{ {
foreach ($node->catches as $catch) { foreach ($node->catches as $catch) {
$alreadyHasError = false; $alreadyHasError = false;
$needs = false; $runAfter = false;
foreach ($catch->types as &$type) { foreach ($catch->types as &$type) {
if ($type instanceof FullyQualified && if ($type instanceof FullyQualified &&
$type->getLast() === "Error") { $type->getLast() === "Error") {
$alreadyHasError = true; $alreadyHasError = true;
} }
if ($this->isThrowable($type)) { if ($this->isThrowable($type)) {
$needs = true; $runAfter = true;
$type = new FullyQualified('Exception'); $type = new FullyQualified('Exception');
} }
} }
if ($needs && !$alreadyHasError) { if ($runAfter && !$alreadyHasError) {
$catch->types[] = new FullyQualified('Error'); $catch->types[] = new FullyQualified('Error');
} }
} }
@ -78,7 +78,7 @@ class ThrowableReplacer extends Plugin
* *
* @return array * @return array
*/ */
public static function extends(): array public static function runWithBefore(): array
{ {
return [ return [
TypeHintStripper::class => [ TypeHintStripper::class => [

View File

@ -4,7 +4,6 @@ namespace Phabel\Target\Php71;
use Phabel\Plugin; use Phabel\Plugin;
use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\List_; use PhpParser\Node\Expr\List_;
use PhpParser\Node\Stmt\Foreach_; use PhpParser\Node\Stmt\Foreach_;
@ -110,7 +109,7 @@ class ListKey extends Plugin
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public static function needs(): array public static function runAfter(): array
{ {
return [ArrayList::class]; return [ArrayList::class];
} }

View File

@ -36,13 +36,13 @@ class MultipleCatchReplacer extends Plugin
$node->catches = $catches; $node->catches = $catches;
} }
/** /**
* Extends throwable replacer * Extends throwable replacer.
* *
* @return string * @return string
* *
* @psalm-return class-string * @psalm-return class-string
*/ */
public static function extends(): string public static function runWithBefore(): string
{ {
return [ThrowableReplacer::class]; return [ThrowableReplacer::class];
} }

View File

@ -8,11 +8,11 @@ use Phabel\Plugin\TypeHintStripper;
class NullableTypeRemover extends Plugin class NullableTypeRemover extends Plugin
{ {
/** /**
* Remove nullable typehint * Remove nullable typehint.
* *
* @return array * @return array
*/ */
public static function needs(): array public static function runAfter(): array
{ {
return [ return [
TypeHintStripper::class => [ TypeHintStripper::class => [

View File

@ -4,126 +4,4 @@ namespace Phabel;
class Traverser class Traverser
{ {
/**
* Plugins by level.
*
* @var array<int, array<class-string<PluginInterface>, array>>
*/
private array $plugins = [];
/**
* Excluded plugins by level.
*
* @var array<int, array<class-string<PluginInterface>, bool>>
*/
private array $excludedPlugins = [];
/**
* Files indexed by level.
*
* @var array<int, string[]>
*/
private array $files = [];
/**
* Add plugin at a certain dependency level.
*
* @param class-string<PluginInterface> $plugin Plugin to add
* @param array $config Plugin configuration
* @param integer $level Dependency level
*
* @return void
*/
public function addPlugin(string $plugin, array $config, int $level): void
{
$this->plugins[$level][$plugin] = $config;
}
/**
* Exclude plugin at a certain dependency level.
*
* @param class-string<PluginInterface> $plugin Plugin to exclude
* @param bool $excludeNextLevels Whether to exclude plugin from next levels, too
* @param integer $level Dependency level
*
* @return void
*/
public function excludePlugin(string $plugin, bool $excludeNextLevels, int $level): void
{
$this->excludedPlugins[$level][$plugin] = $excludeNextLevels;
}
/**
* Add file.
*
* @param string $path
* @param integer $level
* @return void
*/
public function addFile(string $path, int $level): void
{
if (\in_array($path, $this->files[$level])) {
return;
}
$this->files[$level][] = $path;
}
/**
* Start traversing files.
*
* @return void
*/
public function traverse(): void
{
}
private function resolveCycle(string $class, bool $need, array &$stack): void
{
$allPlugins = [];
foreach ($this->plugins as $level => $plugins) {
foreach ($plugins as $plugin => $config) {
$needs = self::simpleNeeds($plugin);
foreach ($needs as $class => $config) {
if ()
}
}
}
}
/**
* Simplify need requirements
*
* @param class-string<PluginInterface> $class Class to resolve
*
* @return array<class-string<PluginInterface>, array>
*/
private static function simpleNeeds(string $class): array
{
/**
* @var array<class-string<PluginInterface>, array>[]
*/
static $cache = [];
if (isset($cache[$class])) {
return $cache[$class];
}
$needs = $class::needs();
return $cache[$class] = isset($needs[0]) ? array_fill_keys($needs, []) : $needs
}
/**
* Simplify extend requirements
*
* @param class-string<PluginInterface> $class Class to resolve
*
* @return array<class-string<PluginInterface>, array>
*/
private static function simpleExtends(string $class): array
{
/**
* @var array<class-string<PluginInterface>, array>[]
*/
static $cache = [];
if (isset($cache[$class])) {
return $cache[$class];
}
$needs = $class::extends();
return $cache[$class] = isset($needs[0]) ? array_fill_keys($needs, []) : $needs
}
} }

View File

@ -1,99 +0,0 @@
<?php
namespace Phabel;
class TraverserConfig
{
/**
* Plugin configurations, indexed by level.
*
* @var array<int, array<class-string<PluginInterface>, array<0: bool, 1: array>[]>>
*/
private array $plugins = [];
/**
* Excluded plugins by level.
*
* @var array<int, array<class-string<PluginInterface>, array[]>>
*/
private array $excludedPlugins = [];
/**
* Files indexed by level.
*
* @var array<int, string[]>
*/
private array $files = [];
/**
* Add plugin at a certain dependency level.
*
* @param class-string<PluginInterface> $plugin Plugin to add
* @param array $config Plugin configuration
* @param integer $level Dependency level
*
* @return void
*/
public function addPlugin(string $plugin, array $config, int $level): void
{
$this->plugins[$level][$plugin] []= $config;
}
/**
* Get all plugins at a certain level.
*
* @param integer $level Level
*
* @return array
*/
public function getPlugins(int $level): array
{
return $this->plugins[$level];
}
/**
* Exclude plugin at a certain dependency level.
*
* @param class-string<PluginInterface> $plugin Plugin to exclude
* @param bool $excludeNextLevels Whether to exclude plugin from next levels, too
* @param integer $level Dependency level
*
* @return void
*/
public function excludePlugin(string $plugin, bool $excludeNextLevels, int $level): void
{
$this->excludedPlugins[$level][$plugin][] = $excludeNextLevels;
}
/**
* Get final plugin array.
*
* @return array<int, array<class-string<PluginInterface, array>>>
*/
public function final(): array
{
\ksort($this->plugins);
\ksort($this->excludedPlugins);
return [];
}
/**
* Trickle plugins down the dependency tree.
*
* @return void Insert 1% joke
*/
private function trickleDown(): void
{
$met = [];
$maxLevel = \array_key_last($this->plugins);
foreach ($this->plugins as $level => $plugins) {
foreach ($plugins as $plugin => $config) {
$found = false;
for ($checkLevel = $level + 1; $checkLevel <= $maxLevel; $checkLevel++) {
if (isset($this->plugins[$checkLevel][$plugin])
&& $this->plugins[$checkLevel][$plugin] !== $config) {
$found = true;
break;
}
$this->plugins[$checkLevel][$plugin] = $config;
}
$checkLevel--;
}
}
}
}