diff --git a/src/Plugin.php b/src/Plugin.php index 7d6b9d9..0ebe534 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -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 []; } diff --git a/src/Target/Php56/YieldFromReplacer.php b/src/Plugin/Amp/YieldFromReplacer.php similarity index 100% rename from src/Target/Php56/YieldFromReplacer.php rename to src/Plugin/Amp/YieldFromReplacer.php diff --git a/src/Target/Php56/YieldReturnDetector.php b/src/Plugin/Amp/YieldReturnDetector.php similarity index 100% rename from src/Target/Php56/YieldReturnDetector.php rename to src/Plugin/Amp/YieldReturnDetector.php diff --git a/src/Target/Php56/YieldReturnReplacer.php b/src/Plugin/Amp/YieldReturnReplacer.php similarity index 100% rename from src/Target/Php56/YieldReturnReplacer.php rename to src/Plugin/Amp/YieldReturnReplacer.php diff --git a/src/Plugin/TypeHintStripper.php b/src/Plugin/TypeHintStripper.php index a882f38..d574556 100644 --- a/src/Plugin/TypeHintStripper.php +++ b/src/Plugin/TypeHintStripper.php @@ -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; } } diff --git a/src/PluginGraph/Graph.php b/src/PluginGraph/Graph.php new file mode 100644 index 0000000..2956fab --- /dev/null +++ b/src/PluginGraph/Graph.php @@ -0,0 +1,75 @@ +, array> + */ + 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 $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 $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); + } +} diff --git a/src/PluginGraph/Node.php b/src/PluginGraph/Node.php new file mode 100644 index 0000000..e370e8e --- /dev/null +++ b/src/PluginGraph/Node.php @@ -0,0 +1,159 @@ +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 $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, array>|class-string[]) $requirements Requirements + * + * @return array, 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; + } +} diff --git a/src/PluginGraph/PackageContext.php b/src/PluginGraph/PackageContext.php new file mode 100644 index 0000000..afb1709 --- /dev/null +++ b/src/PluginGraph/PackageContext.php @@ -0,0 +1,49 @@ + + */ + 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); + } +} diff --git a/src/PluginInterface.php b/src/PluginInterface.php index ef2aeb9..e11d812 100644 --- a/src/PluginInterface.php +++ b/src/PluginInterface.php @@ -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 + * @psalm-return class-string[]|array, 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 + * @psalm-return class-string[]|array, 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; } diff --git a/src/Target/Php56/MethodCallReplacer.php b/src/Target/Php56/MethodCallReplacer.php deleted file mode 100644 index 6edb9f3..0000000 --- a/src/Target/Php56/MethodCallReplacer.php +++ /dev/null @@ -1,28 +0,0 @@ -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; - } -} diff --git a/src/Target/Php70/CompoundAccess.php b/src/Target/Php70/CompoundAccess.php index 6a56ef0..ac044c8 100644 --- a/src/Target/Php70/CompoundAccess.php +++ b/src/Target/Php70/CompoundAccess.php @@ -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); diff --git a/src/Target/Php70/NullCoalesceReplacer.php b/src/Target/Php70/NullCoalesceReplacer.php index c8b70e8..f4aa4a2 100644 --- a/src/Target/Php70/NullCoalesceReplacer.php +++ b/src/Target/Php70/NullCoalesceReplacer.php @@ -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; } } diff --git a/src/Target/Php70/ScalarTypeHintsRemover.php b/src/Target/Php70/ScalarTypeHintsRemover.php index 2d4e216..5ddaa8c 100644 --- a/src/Target/Php70/ScalarTypeHintsRemover.php +++ b/src/Target/Php70/ScalarTypeHintsRemover.php @@ -12,7 +12,7 @@ class ScalarTypeHintsRemover extends Plugin * * @return array */ - public function needs(): array + public static function needs(): array { return [ TypeHintStripper::class => [ diff --git a/src/Target/Php70/ThrowableReplacer.php b/src/Target/Php70/ThrowableReplacer.php index bc9cce1..d62c844 100644 --- a/src/Target/Php70/ThrowableReplacer.php +++ b/src/Target/Php70/ThrowableReplacer.php @@ -78,7 +78,7 @@ class ThrowableReplacer extends Plugin * * @return array */ - public function extends(): array + public static function extends(): array { return [ TypeHintStripper::class => [ diff --git a/src/Target/Php71/ClassConstantVisibilityModifiersRemover.php b/src/Target/Php71/ClassConstantVisibilityModifiersRemover.php index 5848f83..ee72c61 100644 --- a/src/Target/Php71/ClassConstantVisibilityModifiersRemover.php +++ b/src/Target/Php71/ClassConstantVisibilityModifiersRemover.php @@ -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; - } } diff --git a/src/Target/Php71/ListKey.php b/src/Target/Php71/ListKey.php index 353aea1..730a622 100644 --- a/src/Target/Php71/ListKey.php +++ b/src/Target/Php71/ListKey.php @@ -110,8 +110,8 @@ class ListKey extends Plugin /** * {@inheritDoc} */ - public function needs(): string + public static function needs(): array { - return ArrayList::class; + return [ArrayList::class]; } } diff --git a/src/Target/Php71/MultipleCatchReplacer.php b/src/Target/Php71/MultipleCatchReplacer.php index e5a4730..68ff85a 100644 --- a/src/Target/Php71/MultipleCatchReplacer.php +++ b/src/Target/Php71/MultipleCatchReplacer.php @@ -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]; } } diff --git a/src/Target/Php71/NullableTypeRemover.php b/src/Target/Php71/NullableTypeRemover.php index a0705a2..bd06bea 100644 --- a/src/Target/Php71/NullableTypeRemover.php +++ b/src/Target/Php71/NullableTypeRemover.php @@ -12,7 +12,7 @@ class NullableTypeRemover extends Plugin * * @return array */ - public function needs(): array + public static function needs(): array { return [ TypeHintStripper::class => [ diff --git a/src/Traverser.php b/src/Traverser.php index c5b6434..34b7854 100644 --- a/src/Traverser.php +++ b/src/Traverser.php @@ -4,5 +4,126 @@ namespace Phabel; class Traverser { - -} \ No newline at end of file + /** + * Plugins by level. + * + * @var array, array>> + */ + private array $plugins = []; + /** + * Excluded plugins by level. + * + * @var array, bool>> + */ + private array $excludedPlugins = []; + /** + * Files indexed by level. + * + * @var array + */ + private array $files = []; + /** + * Add plugin at a certain dependency level. + * + * @param class-string $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 $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 $class Class to resolve + * + * @return array, array> + */ + private static function simpleNeeds(string $class): array + { + /** + * @var array, 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 $class Class to resolve + * + * @return array, array> + */ + private static function simpleExtends(string $class): array + { + /** + * @var array, array>[] + */ + static $cache = []; + if (isset($cache[$class])) { + return $cache[$class]; + } + $needs = $class::extends(); + return $cache[$class] = isset($needs[0]) ? array_fill_keys($needs, []) : $needs + } +} diff --git a/src/TraverserConfig.php b/src/TraverserConfig.php new file mode 100644 index 0000000..3093442 --- /dev/null +++ b/src/TraverserConfig.php @@ -0,0 +1,99 @@ +, array<0: bool, 1: array>[]>> + */ + private array $plugins = []; + /** + * Excluded plugins by level. + * + * @var array, array[]>> + */ + private array $excludedPlugins = []; + /** + * Files indexed by level. + * + * @var array + */ + private array $files = []; + /** + * Add plugin at a certain dependency level. + * + * @param class-string $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 $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>> + */ + 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--; + } + } + } +}