Commence writing resolution logic

This commit is contained in:
Daniil Gentili 2020-08-05 14:52:49 +02:00
parent c43eac2b19
commit 70710fb26d
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
20 changed files with 602 additions and 81 deletions

View File

@ -113,9 +113,9 @@ abstract class Plugin implements PluginInterface
/**
* {@inheritDoc}
*/
public function getConfig(string $key)
public function getConfig(string $key, $default)
{
return $this->config[$key];
return $this->config[$key] ?? $default;
}
/**
* {@inheritDoc}
@ -127,14 +127,28 @@ abstract class Plugin implements PluginInterface
/**
* {@inheritDoc}
*/
public function needs()
public static function mergeConfigs(array ...$configs): array
{
return $configs;
}
/**
* {@inheritDoc}
*/
public static function composerRequires(): array
{
return [];
}
/**
* {@inheritDoc}
*/
public function extends()
public static function needs(): array
{
return [];
}
/**
* {@inheritDoc}
*/
public static function extends(): array
{
return [];
}

View File

@ -26,13 +26,12 @@ class TypeHintStripper extends Plugin
if (!$type || $type instanceof UnionType) {
return;
}
if ($type instanceof NullableType && $this->getConfig('nullable')) {
if ($type instanceof NullableType && $this->getConfig('nullable', false)) {
$type = null;
return;
}
$throwableType = $type instanceof NullableType ? $type->type : $type;
// Make this less ugly when we implement a namespace context
if (\in_array($throwableType->toString(), $this->getConfig('types'))) {
if (\in_array($throwableType->toString(), $this->getConfig('types', []))) {
$type = null;
}
}

75
src/PluginGraph/Graph.php Normal file
View File

@ -0,0 +1,75 @@
<?php
namespace Phabel\PluginGraph;
use Phabel\PluginInterface;
class Graph
{
/**
* 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 = [];
/**
* Get new package context.
*
* @return PackageContext
*/
public function getPackageContext(): PackageContext
{
$packageContext = new PackageContext;
$this->packageContexts []= $packageContext;
return $packageContext;
}
/**
* 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[]
*/
public function addPlugin(string $plugin, array $config, PackageContext $ctx): array
{
$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;
return $node->init($this, $plugin, $config, $ctx);
}
}

159
src/PluginGraph/Node.php Normal file
View File

@ -0,0 +1,159 @@
<?php
namespace Phabel\PluginGraph;
use Phabel\PluginInterface;
use SplObjectStorage;
/**
* Represents a plugin with a certain configuration.
*/
class Node
{
/**
* Plugin name.
*/
private string $plugin = '';
/**
* Plugin configuration.
*/
private array $config = [];
/**
* Associated package contexts.
*/
private SplObjectStorage $packageContexts;
/**
* Nodes that this node requires.
*
* @var Node[]
*/
private array $requires = [];
/**
* Nodes that this node extends.
*
* @var Node[]
*/
private array $extends = [];
/**
* Nodes that require this node.
*
* @var Node[]
*/
private array $requiredBy = [];
/**
* Nodes that extend this node.
*
* @var Node[]
*/
private array $extendedBy = [];
/**
* Whether this node was visited when looking for circular requirement references.
*/
private bool $visitedRequires = false;
/**
* Whether this node was visited when looking for circular requirement references.
*/
private bool $visitedExtends = false;
/**
* Constructor.
*/
public function __construct()
{
$this->packageContexts = new SplObjectStorage;
}
/**
* Initialization function.
*
* @param Graph $graph Graph instance
* @param string $plugin Plugin name
* @param array $config Plugin configuration
* @param PackageContext $ctx Context
*
* @psalm-param class-string<PluginInterface> $plugin Plugin name
*
* @return self
*/
public function init(Graph $graph, string $plugin, array $config, PackageContext $ctx): self
{
$this->plugin = $plugin;
$this->config = $config;
$this->packageContexts->attach($ctx);
$requirements = self::simplify($plugin::needs());
$extends = self::simplify($plugin::extends());
foreach ($requirements as $class => $config) {
foreach ($graph->addPlugin($class, $config, $ctx) as $node) {
$this->requires []= $node;
$node->requiredBy []= $this;
}
}
foreach ($extends as $class => $config) {
foreach ($graph->addPlugin($class, $config, $ctx) as $node) {
$this->extends []= $node;
$node->extendedBy []= $this;
}
}
return $this;
}
/**
* 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;
}
/**
* Add package context.
*
* @param PackageContext $ctx Context
*
* @return self
*/
public function addPackageContext(PackageContext $ctx): self
{
if ($this->packageContexts->contains($ctx)) {
return $this;
}
$this->packageContexts->attach($ctx);
foreach ($this->requires as $node) {
$node->addPackageContext($ctx);
}
foreach ($this->extends as $node) {
$node->addPackageContext($ctx);
}
return $this;
}
/**
* Check if this node requires only one other node.
*
* @return boolean
*/
private function oneRequire(): bool
{
return \count($this->requires) === 1 && empty($this->extends);
}
/**
* Check if this node extends only one other node.
*
* @return boolean
*/
private function oneExtend(): bool
{
return \count($this->extends) === 1;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Phabel\PluginGraph;
/**
* List of packages associated with plugin.
*/
class PackageContext
{
/**
* Package list.
*
* @var array<string, null>
*/
private array $packages = [];
/**
* Add package.
*
* @param string $package Package
*
* @return void
*/
public function addPackage(string $package): void
{
$this->packages[$package] = null;
}
/**
* Merge two contexts
*
* @param self $other Other context
*
* @return self New context
*/
public function merge(self $other): self
{
$this->packages += $other->packages;
return $this;
}
/**
* Get package list
*
* @return array
*/
public function getPackages(): array
{
return array_values($this->packages);
}
}

View File

@ -2,8 +2,6 @@
namespace Phabel;
use PhpParser\NodeVisitor;
interface PluginInterface
{
/**
@ -14,40 +12,67 @@ interface PluginInterface
*
* When possible, use the extends method to reduce complexity.
*
* @return array|string Plugin name(s)
* @return array Plugin name(s)
*
* @psalm-return array<class-string<Plugin|NodeVisitor>>|class-string<Plugin|NodeVisitor>
* @psalm-return class-string<PluginInterface>[]|array<class-string<PluginInterface>, array>
*/
public function needs();
public static function needs(): array;
/**
* Specify which plugins does this plugin extends.
* Specify which plugins does this plugin extend.
*
* At each depth level, the traverser will first execute the enter|leave methods of the specified plugins, then immediately execute the enter|leave methods of the current plugin.
*
* This is preferred, and allows only a single traversal of the AST.
*
* @return array|string Plugin name(s)
* @return array Plugin name(s)
*
* @psalm-return array<class-string<Plugin|NodeVisitor>>|class-string<Plugin|NodeVisitor>
* @psalm-return class-string<PluginInterface>[]|array<class-string<PluginInterface>, array>
*/
public function extends();
public static function extends(): array;
/**
* Get configuration key
* Specify a list of composer dependencies.
*
* @return array
*/
public static function composerRequires(): array;
/**
* Get configuration key.
*
* @param string $key Key
* @param mixed $default Default value, if key is not present
*
* @param string $key Key
*
* @return mixed
*/
public function getConfig(string $key);
public function getConfig(string $key, $default);
/**
* Set configuration key
* Set configuration key.
*
* @param string $key Key
* @param mixed $value Value
*
*
* @return void
*/
public function setConfig(string $key, $value): void;
/**
* Merge multiple configurations into one (or more).
*
* @param array ...$configs Configurations
*
* @return array[]
*/
public static function mergeConfigs(array ...$configs): array;
/**
* Split configuration.
*
* For example, if you have a configuration that enables feature A, B and C, return three configuration arrays each enabling ONLY A, only B and only C.
* This is used for optimizing the AST traversing process during resolution of the plugin graph.
*
* @param array $config Configuration
*
* @return array[]
*/
public static function splitConfig(array $config): array;
}

View File

@ -1,28 +0,0 @@
<?php
namespace Spatie\Php7to5\NodeVisitors;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
class MethodCallReplacer extends NodeVisitorAbstract
{
/**
* {@inheritdoc}
*/
public function leaveNode(Node $node)
{
if (!$node instanceof Node\Expr\MethodCall) {
return;
}
$value = &$node->var;
if (!$value instanceof Node\Expr\Clone_ &&
!$value instanceof Node\Expr\Yield_ &&
!$value instanceof Node\Expr\Closure
) {
return;
}
$value = new Node\Expr\FuncCall(new Node\Name('\\returnMe'), [$value]);
return $node;
}
}

View File

@ -7,6 +7,7 @@ use PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\Isset_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Yield_;
/**
@ -37,7 +38,7 @@ class CompoundAccess extends Plugin
* Fix yield array access.
*
* @param ArrayDimFetch $node Node
*
*
* @return void
*/
public function enterArrayYield(ArrayDimFetch $node): void
@ -48,10 +49,10 @@ class CompoundAccess extends Plugin
$node->var = self::callPoly('returnMe', $node->var);
}
/**
* Fix yield array access
* Fix yield array access.
*
* @param Yield_ $node Yield
*
*
* @return void
*/
public function enterYield(Yield_ $node): void
@ -61,10 +62,28 @@ class CompoundAccess extends Plugin
return;
}
if ($value instanceof Node\Expr\FuncCall ||
$value instanceof Node\Expr\MethodCall ||
$value instanceof Node\Expr\StaticCall ||
$value instanceof Node\Scalar
) {
$value instanceof Node\Expr\MethodCall ||
$value instanceof Node\Expr\StaticCall ||
$value instanceof Node\Scalar
) {
return;
}
$value = self::callPoly('returnMe', $value);
}
/**
* Replace method call on yielded|cloned|closure object.
*
* @param MethodCall $node Method call
*
* @return void
*/
public function enterMethodCall(MethodCall $node): void
{
$value = &$node->var;
if (!$value instanceof Node\Expr\Clone_ &&
!$value instanceof Node\Expr\Yield_ &&
!$value instanceof Node\Expr\Closure
) {
return;
}
$value = self::callPoly('returnMe', $value);

View File

@ -10,10 +10,10 @@ use PhpParser\Node\Expr\StaticCall;
class NullCoalesceReplacer extends Plugin
{
/**
* Replace null coalesce
* Replace null coalesce.
*
* @param Coalesce $node Coalesce
*
*
* @return StaticCall
*/
public function enter(Coalesce $node): StaticCall
@ -33,6 +33,6 @@ class NullCoalesceReplacer extends Plugin
*/
public static function coalesce($ifNotNull, $then)
{
return isset($ifNotNull) ?: $then;
return isset($ifNotNull) ? $ifNotNull : $then;
}
}

View File

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

View File

@ -78,7 +78,7 @@ class ThrowableReplacer extends Plugin
*
* @return array
*/
public function extends(): array
public static function extends(): array
{
return [
TypeHintStripper::class => [

View File

@ -3,10 +3,7 @@
namespace Phabel\Target\Php71;
use Phabel\Plugin;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\NodeVisitor\NameResolver;
/**
* Removes the class constant visibility modifiers (PHP 7.1).
@ -17,19 +14,11 @@ class ClassConstantVisibilityModifiersRemover extends Plugin
* Makes public private and protected class constants.
*
* @param ClassConst $node Constant
*
*
* @return void
*/
public function enter(ClassConst $node): void
{
$node->flags = 0; // Remove constant modifier
}
/**
* {@inheritDoc}
*/
public function needs(): string
{
return NameResolver::class;
}
}

View File

@ -110,8 +110,8 @@ class ListKey extends Plugin
/**
* {@inheritDoc}
*/
public function needs(): string
public static function needs(): array
{
return ArrayList::class;
return [ArrayList::class];
}
}

View File

@ -42,8 +42,8 @@ class MultipleCatchReplacer extends Plugin
*
* @psalm-return class-string
*/
public function extends(): string
public static function extends(): string
{
return ThrowableReplacer::class;
return [ThrowableReplacer::class];
}
}

View File

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

View File

@ -4,5 +4,126 @@ namespace Phabel;
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
}
}

99
src/TraverserConfig.php Normal file
View File

@ -0,0 +1,99 @@
<?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--;
}
}
}
}