commit c43eac2b19c95f4d1c5dbd918d69b76097abedd2 Author: Daniil Gentili Date: Mon Aug 3 21:31:32 2020 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a9875b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b7531f4 --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "name": "phabel/phabel", + "description": "Write and deploy modern PHP 8 code, today.", + "type": "project", + "require": { + "nikic/php-parser": "^4.7" + }, + "license": "MIT", + "authors": [ + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" + } + ] +} diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..3240886 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/src/Context.php b/src/Context.php new file mode 100644 index 0000000..8abf4dc --- /dev/null +++ b/src/Context.php @@ -0,0 +1,8 @@ + $class Class of new node + * @psalm-param array $propertyMap Property map between old and new objects + * + * @return Node + */ + public static function replaceType(Node $node, string $class, array $propertyMap = []): Node + { + if ($propertyMap) { + /** @var Node */ + $nodeNew = (new ReflectionClass($class))->newInstanceWithoutConstructor(); + foreach ($propertyMap as $old => $new) { + $nodeNew->{$new} = $node->{$old}; + } + $nodeNew->setAttributes($node->getAttributes()); + return $nodeNew; + } + return new $class( + ...\array_map(fn (string $name) => $node->{$name}, $node->getSubNodeNames()), + $node->getAttributes() + ); + } + /** + * Replace type in-place. + * + * @param Node &$node Original node + * @param string $class Class of new node + * @param array $propertyMap Property map between old and new objects + * + * @psalm-param class-string $class Class of new node + * @psalm-param array $propertyMap Property map between old and new objects + * + * @param-out Node &$node + * + * @return void + */ + public static function replaceTypeInPlace(Node &$node, string $class, array $propertyMap = []): void + { + $node = self::replaceType($node, $class, $propertyMap); + } + /** + * Call function. + * + * @param class-string|array{0: class-string, 1: string} $name Function name + * @param Expr|Arg ...$parameters Parameters + * + * @return FuncCall|StaticCall + */ + public static function call($name, ...$parameters) + { + $parameters = \array_map(fn ($data) => $data instanceof Arg ? $data : new Arg($data), $parameters); + return \is_array($name) ? new StaticCall(new Name($name[0]), $name[1], $parameters) : new FuncCall(new Name($name), $parameters); + } + /** + * Call polyfill function from current plugin. + * + * @param string $name Function name + * @param Expr|Arg ...$parameters Parameters + * + * @return StaticCall + */ + protected static function callPoly(string $name, ...$parameters): StaticCall + { + return self::call([static::class, $name], ...$parameters); + } + /** + * Convert array, int or other literal to node. + * + * @param mixed $data Data to convert + * + * @return Node + */ + public static function toLiteral($data): Node + { + /** @var Node[] */ + static $cache = []; + $data = \var_export($data, true); + if (isset($cache[$data])) { + return $cache[$data]; + } + $res = (new ParserFactory)->create(ParserFactory::PREFER_PHP7)->parse('expr)) { + throw new \RuntimeException('An invalid literal was provided!'); + } + return $cache[$data] = $res[0]->expr; + } + /** + * {@inheritDoc} + */ + public function getConfig(string $key) + { + return $this->config[$key]; + } + /** + * {@inheritDoc} + */ + public function setConfig(string $key, $value): void + { + $this->config[$key] = $value; + } + /** + * {@inheritDoc} + */ + public function needs() + { + return []; + } + /** + * {@inheritDoc} + */ + public function extends() + { + return []; + } +} diff --git a/src/Plugin/TypeHintStripper.php b/src/Plugin/TypeHintStripper.php new file mode 100644 index 0000000..a882f38 --- /dev/null +++ b/src/Plugin/TypeHintStripper.php @@ -0,0 +1,60 @@ +getConfig('nullable')) { + $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'))) { + $type = null; + } + } + /** + * Remove param. + * + * @param Param $node Parameter + * @return void + */ + public function enterParam(Param $node): void + { + $this->strip($node->type); + } + /** + * Strip return throwable type. + * + * @param FunctionLike $func Function + * + * @return void + */ + public function enterFuncReturn(FunctionLike $func): void + { + $this->strip($func->getReturnType()); + } +} diff --git a/src/PluginInterface.php b/src/PluginInterface.php new file mode 100644 index 0000000..ef2aeb9 --- /dev/null +++ b/src/PluginInterface.php @@ -0,0 +1,53 @@ +>|class-string + */ + public function needs(); + /** + * Specify which plugins does this plugin extends. + * + * 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) + * + * @psalm-return array>|class-string + */ + public function extends(); + + /** + * Get configuration key + * + * @param string $key Key + * + * @return mixed + */ + public function getConfig(string $key); + + /** + * Set configuration key + * + * @param string $key Key + * @param mixed $value Value + * + * @return void + */ + public function setConfig(string $key, $value): void; +} diff --git a/src/Target/Php56/MethodCallReplacer.php b/src/Target/Php56/MethodCallReplacer.php new file mode 100644 index 0000000..6edb9f3 --- /dev/null +++ b/src/Target/Php56/MethodCallReplacer.php @@ -0,0 +1,28 @@ +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/Php56/YieldFromReplacer.php b/src/Target/Php56/YieldFromReplacer.php new file mode 100644 index 0000000..fc3f65a --- /dev/null +++ b/src/Target/Php56/YieldFromReplacer.php @@ -0,0 +1,29 @@ +expr; + + return new Node\Expr\Yield_($generator); + } +} diff --git a/src/Target/Php56/YieldReturnDetector.php b/src/Target/Php56/YieldReturnDetector.php new file mode 100644 index 0000000..05d3f49 --- /dev/null +++ b/src/Target/Php56/YieldReturnDetector.php @@ -0,0 +1,38 @@ +hasYield []= $node; + } + if ($node instanceof Node\Expr\Yield_ || + $node instanceof Node\Expr\YieldFrom + ) { + end($this->hasYield)->hasYield = true; + } + } + public function leaveNode(Node $node) + { + if ($node instanceof Node\FunctionLike) { + array_pop($this->hasYield); + } + } +} diff --git a/src/Target/Php56/YieldReturnReplacer.php b/src/Target/Php56/YieldReturnReplacer.php new file mode 100644 index 0000000..e79c9a3 --- /dev/null +++ b/src/Target/Php56/YieldReturnReplacer.php @@ -0,0 +1,55 @@ +functions[] = $node; + } + } + + /** + * {@inheritdoc} + */ + public function leaveNode(Node $node) + { + if ($node instanceof Node\FunctionLike) { + array_pop($this->functions); + return; + } + if (!$node instanceof Node\Stmt\Return_) { + return; + } + if ($node->expr === null) { + return new Node\Stmt\Return_(); + } + + if (!(end($this->functions)->hasYield ?? false)) { + return; + } + + $value = $node->expr; + + $newReturn = new Node\Expr\Yield_( + new Node\Expr\New_( + new Node\Expr\ConstFetch( + new Node\Name('\YieldReturnValue') + ), + [$value] + ) + ); + + $stmts = [$newReturn, new Node\Stmt\Return_()]; + return $stmts; + } +} diff --git a/src/Target/Php70/AnonymousClassReplacer.php b/src/Target/Php70/AnonymousClassReplacer.php new file mode 100644 index 0000000..8d2c885 --- /dev/null +++ b/src/Target/Php70/AnonymousClassReplacer.php @@ -0,0 +1,128 @@ +class; + if (!$classNode instanceof Node\Stmt\Class_) { + return; + } + + $newClassName = 'AnonymousClass'.(self::$count++); + + $classNode->name = $newClassName; + + $this->anonymousClassNodes[] = $classNode; + + // Generate new code that instantiate our new class + $newNode = new Node\Expr\New_( + new Node\Expr\ConstFetch( + new Node\Name($newClassName) + ), + $node->args + ); + + return $newNode; + } + + /** + * {@inheritdoc} + */ + public function afterTraverse(array $nodes) + { + if (count($this->anonymousClassNodes) === 0) { + return $nodes; + } + + $anonymousClassStatements = $this->anonymousClassNodes; + + $anonymousClassStatements = $this->convertToPhp5Statements($anonymousClassStatements); + + $hookIndex = $this->getAnonymousClassHookIndex($nodes); + + $nodes = $this->moveAnonymousClassesToHook($nodes, $hookIndex, $anonymousClassStatements); + + return $nodes; + } + + /** + * Find the index of the first statement that is not a declare, use or namespace statement. + * + * @param array $statements + * + * @return int + * + * @throws \Spatie\Php7to5\Exceptions\InvalidPhpCode + */ + protected function getAnonymousClassHookIndex(array $statements) + { + $hookIndex = false; + + foreach ($statements as $index => $statement) { + if (!$statement instanceof Declare_ && + !$statement instanceof Use_ && + !$statement instanceof Namespace_) { + $hookIndex = $index; + } + } + + if ($hookIndex === false) { + return 1; + //throw InvalidPhpCode::noValidLocationFoundToInsertClasses(); + } + + return $hookIndex; + } + + /** + * @param array $nodes + * @param $hookIndex + * @param $anonymousClassStatements + * + * @return array + */ + protected function moveAnonymousClassesToHook(array $nodes, $hookIndex, $anonymousClassStatements) + { + $preStatements = array_slice($nodes, 0, $hookIndex); + $postStatements = array_slice($nodes, $hookIndex); + + return array_merge($preStatements, $anonymousClassStatements, $postStatements); + } + + /** + * @param array $php7statements + * + * @return \PhpParser\Node[] + */ + public function convertToPhp5Statements(array $php7statements) + { + $converter = Converter::getTraverser($php7statements); + + $php5Statements = $converter->traverse($php7statements); + + return $php5Statements; + } +} diff --git a/src/Target/Php70/ClosureCallReplacer.php b/src/Target/Php70/ClosureCallReplacer.php new file mode 100644 index 0000000..1a13c4f --- /dev/null +++ b/src/Target/Php70/ClosureCallReplacer.php @@ -0,0 +1,43 @@ +name; + if ($name instanceof Name || $name instanceof Variable) { + return null; + } + \array_unshift($node->args, new Arg($name)); + return self::callPoly('callMe', ...$node->args); + } + + /** + * Call provided argument. + * + * @param callable $callable Callable + * @param mixed ...$arguments Arguments + * + * @return mixed + */ + public static function callMe(callable $callable, ...$arguments) + { + return $callable($arguments); + } +} diff --git a/src/Target/Php70/CompoundAccess.php b/src/Target/Php70/CompoundAccess.php new file mode 100644 index 0000000..6a56ef0 --- /dev/null +++ b/src/Target/Php70/CompoundAccess.php @@ -0,0 +1,89 @@ +vars as &$var) { + if (!$var instanceof ArrayDimFetch) { + continue; + } + if (!$var->var instanceof ClassConstFetch) { + continue; + } + $var->var = self::callPoly('returnMe', $var->var); + } + } + /** + * Fix yield array access. + * + * @param ArrayDimFetch $node Node + * + * @return void + */ + public function enterArrayYield(ArrayDimFetch $node): void + { + if (!$node->var instanceof Node\Expr\Yield_) { + return; + } + $node->var = self::callPoly('returnMe', $node->var); + } + /** + * Fix yield array access + * + * @param Yield_ $node Yield + * + * @return void + */ + public function enterYield(Yield_ $node): void + { + $value = &$node->value; + if ($value instanceof Node\Expr\Variable && $value->name !== "this") { + return; + } + if ($value instanceof Node\Expr\FuncCall || + $value instanceof Node\Expr\MethodCall || + $value instanceof Node\Expr\StaticCall || + $value instanceof Node\Scalar + ) { + return; + } + $value = self::callPoly('returnMe', $value); + } + /** + * Returns the data provided. + * + * @param mixed $data Data + * + * @return mixed + * + * @template T + * + * @psalm-param T $data data + * + * @psalm-return T + */ + public static function returnMe($data) + { + return $data; + } +} diff --git a/src/Target/Php70/DefineArrayReplacer.php b/src/Target/Php70/DefineArrayReplacer.php new file mode 100644 index 0000000..5155f6e --- /dev/null +++ b/src/Target/Php70/DefineArrayReplacer.php @@ -0,0 +1,40 @@ +name instanceof Name || $node->name->toString() != 'define') { + return null; + } + + $nameNode = $node->args[0]->value; + $valueNode = $node->args[1]->value; + + if (!$valueNode instanceof Node\Expr\Array_) { + return null; + } + + $constNode = new Node\Const_($nameNode->value, $valueNode); + + return new Node\Stmt\Const_([$constNode]); + } +} diff --git a/src/Target/Php70/GroupUseReplacer.php b/src/Target/Php70/GroupUseReplacer.php new file mode 100644 index 0000000..63ee680 --- /dev/null +++ b/src/Target/Php70/GroupUseReplacer.php @@ -0,0 +1,43 @@ +prefix->parts; + + return \array_map(fn (UseUse $useNode) => $this->createUseNode($nodePrefixParts, $useNode), $node->uses); + } + + /** + * Create separate use node. + * + * @param string[] $nodePrefixParts Use prefix + * @param UseUse $useNode Current use node + * + * @return Use_ New use node + */ + protected function createUseNode(array $nodePrefixParts, UseUse $useNode): Use_ + { + $nodePrefixParts []= $useNode->name; + + $nameNode = new Node\Name($nodePrefixParts); + + return new Use_([new UseUse($nameNode, $useNode->alias)], $useNode->type); + } +} diff --git a/src/Target/Php70/NullCoalesceReplacer.php b/src/Target/Php70/NullCoalesceReplacer.php new file mode 100644 index 0000000..c8b70e8 --- /dev/null +++ b/src/Target/Php70/NullCoalesceReplacer.php @@ -0,0 +1,38 @@ +left instanceof Node\Expr\ErrorSuppress)) { + $node->left = new Node\Expr\ErrorSuppress($node->left); + } + return self::callPoly('coalesce', $node->left, $node->right); + } + /** + * Coalesce. + * + * @param null|mixed $ifNotNull If not null, return this + * @param mixed $then Else this + * + * @return mixed + */ + public static function coalesce($ifNotNull, $then) + { + return isset($ifNotNull) ?: $then; + } +} diff --git a/src/Target/Php70/ReservedNameReplacer.php b/src/Target/Php70/ReservedNameReplacer.php new file mode 100644 index 0000000..412be50 --- /dev/null +++ b/src/Target/Php70/ReservedNameReplacer.php @@ -0,0 +1,29 @@ +name; + if (!\is_string($name) || !\in_array(\strtolower($name), ['continue', 'empty', 'use', 'default', 'echo'])) { + return; + } + $name .= '_'; + } +} diff --git a/src/Target/Php70/ScalarTypeHintsRemover.php b/src/Target/Php70/ScalarTypeHintsRemover.php new file mode 100644 index 0000000..2d4e216 --- /dev/null +++ b/src/Target/Php70/ScalarTypeHintsRemover.php @@ -0,0 +1,23 @@ + [ + 'types' => ['int', 'integer', 'float', 'string', 'bool', 'boolean'] + ] + ]; + } +} diff --git a/src/Target/Php70/SpaceshipOperatorReplacer.php b/src/Target/Php70/SpaceshipOperatorReplacer.php new file mode 100644 index 0000000..9587adb --- /dev/null +++ b/src/Target/Php70/SpaceshipOperatorReplacer.php @@ -0,0 +1,37 @@ +left, $node->right); + } + /** + * Spacesip operator. + * + * @param integer|string|float $a A + * @param integer|string|float $b B + * + * @return integer + */ + public static function spaceship($a, $b): int + { + return $a < $b ? -1 : ($a === $b ? 0 : 1); + } +} diff --git a/src/Target/Php70/StrictTypesDeclareStatementRemover.php b/src/Target/Php70/StrictTypesDeclareStatementRemover.php new file mode 100644 index 0000000..d282775 --- /dev/null +++ b/src/Target/Php70/StrictTypesDeclareStatementRemover.php @@ -0,0 +1,24 @@ +declares = \array_filter($node->declares, fn (DeclareDeclare $declare) => $declare->key->name !== 'strict_types'); + + if (empty($node->declares)) { + return NodeTraverser::REMOVE_NODE; + } + return null; + } +} diff --git a/src/Target/Php70/ThrowableReplacer.php b/src/Target/Php70/ThrowableReplacer.php new file mode 100644 index 0000000..bc9cce1 --- /dev/null +++ b/src/Target/Php70/ThrowableReplacer.php @@ -0,0 +1,92 @@ +isThrowable($node->class)) { + return null; + } + return new BooleanOr( + new Instanceof_($node->expr, new FullyQualified('Exception')), + new Instanceof_($node->expr, new FullyQualified('Error')) + ); + } + /** + * Substitute try-catch. + * + * @param TryCatch $node TryCatch node + * + * @return void + */ + public function enterTryCatch(TryCatch $node): void + { + foreach ($node->catches as $catch) { + $alreadyHasError = false; + $needs = false; + foreach ($catch->types as &$type) { + if ($type instanceof FullyQualified && + $type->getLast() === "Error") { + $alreadyHasError = true; + } + if ($this->isThrowable($type)) { + $needs = true; + $type = new FullyQualified('Exception'); + } + } + if ($needs && !$alreadyHasError) { + $catch->types[] = new FullyQualified('Error'); + } + } + } + + /** + * Other transforms. + * + * @return array + */ + public function extends(): array + { + return [ + TypeHintStripper::class => [ + 'type' => [ + \Throwable::class, + 'Throwable' + ] + ] + ]; + } +} diff --git a/src/Target/Php71/ArrayList.php b/src/Target/Php71/ArrayList.php new file mode 100644 index 0000000..3def2bc --- /dev/null +++ b/src/Target/Php71/ArrayList.php @@ -0,0 +1,57 @@ +valueVar instanceof Array_) { + self::replaceTypeInPlace($node->valueVar, List_::class); + } + } + /** + * Called when entering assignment node. + * + * @param Assign $node Node + * + * @return void + */ + public function enterAssign(Assign $node): void + { + if ($node->var instanceof Array_) { + self::replaceTypeInPlace($node->var, List_::class); + } + } + /** + * Called when entering list for nested lists. + * + * @param List_ $node Node + * + * @return void + */ + public function enterList(List_ $node): void + { + foreach ($node->items as $item) { + if ($item->value instanceof Array_) { + self::replaceTypeInPlace($item->value, List_::class); + } + } + } +} diff --git a/src/Target/Php71/ClassConstantVisibilityModifiersRemover.php b/src/Target/Php71/ClassConstantVisibilityModifiersRemover.php new file mode 100644 index 0000000..5848f83 --- /dev/null +++ b/src/Target/Php71/ClassConstantVisibilityModifiersRemover.php @@ -0,0 +1,35 @@ +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 new file mode 100644 index 0000000..353aea1 --- /dev/null +++ b/src/Target/Php71/ListKey.php @@ -0,0 +1,117 @@ +valueVar instanceof List_ || !$this->shouldSplit($node->valueVar)) { + return; + } + [$node->valueVar, $array] = $this->splitList($node->valueVar); + $node->expr = self::callPoly('destructure', $array, $node->expr); + } + /** + * Parse list assignment with custom keys. + * + * @param Assign $node List assignment + * + * @return void + */ + public function enterAssign(Assign $node): void + { + if (!$node->var instanceof List_ || !$this->shouldSplit($node->var)) { + return; + } + [$node->var, $array] = $this->splitList($node->var); + $node->expr = self::callPoly('destructure', $array, $node->expr); + } + /** + * Whether this is a keyed list. + * + * @param List_ $list List + * + * @return boolean + */ + private function shouldSplit(List_ $list): bool + { + return isset($list->items[0]->key); + } + /** + * Split keyed list into new list assignment and array to pass to destructure function. + * + * @param List_ $list Keyed list + * + * @return array{0: List_, 1: Array_} + */ + private function splitList(List_ $list): array + { + $newList = []; + $keys = []; + $key = 0; // Technically list assignment does not support mixed keys, but we need this for nested assignments + foreach ($list->items as $item) { + if ($item) { + $curKey = $item->key ?? $key++; + $item->key = null; + if ($item->value instanceof List_) { + [$item->value, $keys[$curKey]] = $this->splitList($list); + } else { + $keys[$curKey] = null; + } + } else { + $newList []= null; + $keys[$key++] = null; + } + } + /** @var Array_ */ + $keys = self::toLiteral($keys); + return [new List_($newList), $keys]; + } + /** + * Destructure array. + * + * @param array $keys Custom keys + * @param array $array Array + * + * @psalm-param array $keys Custom keys + * + * @return array + */ + public static function destructure(array $keys, array $array): array + { + $res = []; + foreach ($keys as $key => $type) { + if ($type === null) { + $res[] = $array[$key]; + } else { + $res[] = self::destructure($type, $array[$key]); + } + } + return $res; + } + /** + * {@inheritDoc} + */ + public function needs(): string + { + return ArrayList::class; + } +} diff --git a/src/Target/Php71/MultipleCatchReplacer.php b/src/Target/Php71/MultipleCatchReplacer.php new file mode 100644 index 0000000..e5a4730 --- /dev/null +++ b/src/Target/Php71/MultipleCatchReplacer.php @@ -0,0 +1,49 @@ +catches as $catch) { + if (\count($catch->types) === 1) { + $catches []= $catch; + } else { + foreach ($catch->types as $type) { + $ncatch = clone $catch; + $ncatch->types = [$type]; + $catches []= $ncatch; + } + } + } + $node->catches = $catches; + } + /** + * Extends throwable replacer + * + * @return string + * + * @psalm-return class-string + */ + public function extends(): string + { + return ThrowableReplacer::class; + } +} diff --git a/src/Target/Php71/NullableTypeRemover.php b/src/Target/Php71/NullableTypeRemover.php new file mode 100644 index 0000000..a0705a2 --- /dev/null +++ b/src/Target/Php71/NullableTypeRemover.php @@ -0,0 +1,23 @@ + [ + 'nulable' => true + ] + ]; + } +} diff --git a/src/Traverser.php b/src/Traverser.php new file mode 100644 index 0000000..c5b6434 --- /dev/null +++ b/src/Traverser.php @@ -0,0 +1,8 @@ +