diff --git a/src/Context.php b/src/Context.php index ea856d9..6b5a244 100644 --- a/src/Context.php +++ b/src/Context.php @@ -2,6 +2,7 @@ namespace Phabel; +use PhpParser\Node; use SplStack; /** @@ -11,8 +12,8 @@ use SplStack; class Context { /** - * Parent nodes stack - * + * Parent nodes stack. + * * @var SplStack */ public SplStack $parents; @@ -20,4 +21,31 @@ class Context { $this->parents = new SplStack; } + /** + * Insert nodes before node. + * + * @param Node $node + * @param Node ...$nodes + * @return void + */ + public function insertBefore(Node $node, Node ...$nodes): void + { + $subNode = $node->getAttribute('currentNode'); + $subNodeIndex = $node->getAttribute('currentNodeIndex'); + \array_splice($node->{$subNode}, $subNodeIndex, 0, $nodes); + $node->setAttribute('currentNodeIndex', $subNodeIndex+\count($nodes)); + } + /** + * Insert nodes after node. + * + * @param Node $node + * @param Node ...$nodes + * @return void + */ + public function insertAfter(Node $node, Node ...$nodes): void + { + $subNode = $node->getAttribute('currentNode'); + $subNodeIndex = $node->getAttribute('currentNodeIndex'); + \array_splice($node->{$subNode}, $subNodeIndex+1, 0, $nodes); + } } diff --git a/src/Context/UniqueId.php b/src/Context/UniqueId.php deleted file mode 100644 index 622f483..0000000 --- a/src/Context/UniqueId.php +++ /dev/null @@ -1,9 +0,0 @@ -create(ParserFactory::PREFER_PHP7)->parse('create(ParserFactory::PREFER_PHP7)->parse('expr)) { - throw new \RuntimeException('An invalid literal was provided!'); + throw new \RuntimeException('Invalid code was provided!'); } - return $cache[$data] = $res[0]->expr; + return $res[0]->expr; } /** * {@inheritDoc} diff --git a/src/Plugin/TypeHintStripper.php b/src/Plugin/TypeHintStripper.php index a35f0ff..e29fd24 100644 --- a/src/Plugin/TypeHintStripper.php +++ b/src/Plugin/TypeHintStripper.php @@ -2,12 +2,38 @@ namespace Phabel\Plugin; +use Phabel\Context; use Phabel\Plugin; use PhpParser\Node; +use PhpParser\Node\Expr\ArrowFunction; +use PhpParser\Node\Expr\Assign; +use PhpParser\Node\Expr\AssignRef; +use PhpParser\Node\Expr\BinaryOp\BooleanOr; +use PhpParser\Node\Expr\BinaryOp\Concat; +use PhpParser\Node\Expr\BinaryOp\Plus; +use PhpParser\Node\Expr\BooleanNot; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Expr\Closure; +use PhpParser\Node\Expr\Instanceof_; +use PhpParser\Node\Expr\New_; +use PhpParser\Node\Expr\Variable; use PhpParser\Node\FunctionLike; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\NullableType; use PhpParser\Node\Param; +use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Scalar\MagicConst\Function_ as MagicConstFunction_; +use PhpParser\Node\Scalar\MagicConst\Method; +use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Foreach_; +use PhpParser\Node\Stmt\If_; +use PhpParser\Node\Stmt\Return_; +use PhpParser\Node\Stmt\Throw_; use PhpParser\Node\UnionType; +use SplStack; /** * Replace all usages of a certain type in typehints. @@ -16,46 +42,204 @@ use PhpParser\Node\UnionType; */ class TypeHintStripper extends Plugin { + private SplStack $stack; + public function __construct() + { + $this->stack = $stack; + } + private function toClosure(FunctionLike &$func): void + { + if ($func instanceof ArrowFunction) { + $nodes = []; + foreach ($func->getSubNodeNames() as $node) { + $nodes[$node] = $func->{$nodes}; + } + $func = new Closure($nodes, $func->getAttributes()); + } + } + private function generateConditions(Variable $var, array $types, bool $fromNullable = false): array + { + $noOopTypes = true; + $typeNames = []; + $stringType = ''; + foreach ($types as &$type) { + $typeNames []= $type->toString(); + + if ($type instanceof Identifier) { + $typeName = $type->toLowerString(); + switch ($typeName) { + case 'callable': + case 'array': + case 'bool': + case 'float': + case 'int': + case 'object': + case 'string': + case 'null': + $stringType = new String_($typeName === 'callable' ? + $typeName : + ($typeName === 'object' ? 'an object' : "of type $typeName")); + $type = Plugin::call("is_$typeName", $var); + break; + case 'iterable': + $stringType = new String_('iterable'); + $type = new BooleanOr( + Plugin::call("is_array", $var), + new Instanceof_($var, new Name(\Traversable::class)) + ); + break; + default: + $noOopTypes = false; + $stringType = $type->isSpecialClassName() ? + new Concat(new String_("an instance of "), new ClassConstFetch($type, new Identifier('class'))) : + new String_("an instance of ".$type->toString()); + $type = new Instanceof_($var, $type); + } + } else { + $noOopTypes = false; + $stringType = new String_("an instance of ".$type->toString()); + $type = new Instanceof_($var, $type); + } + } + if (\count($typeNames) > 1) { + $stringType = new String_(\implode("|", $typeNames)); + } + $condition = new BooleanNot(\count($types) === 1 ? $types[0] : \array_reduce($types, fn (Node $a, Node $b): BooleanOr => new BooleanOr($a, $b))); + return [$noOopTypes, $stringType, $condition]; + } /** * Strip typehint. * - * @param null|Identifier|Name|NullableType|UnionType &$type Type + * @param Variable $var Variable + * @param null|Identifier|Name|NullableType|UnionType $type Type * - * @return void + * @return array{0: bool, 1: Node, 2: BooleanNot} Conditions for if */ - private function strip(?Node &$type): void + private function strip(Variable $var, ?Node $type): array { - if (!$type || $type instanceof UnionType) { - return; + if (!$type) { + return []; + } + if ($type instanceof UnionType && $this->getConfig('union', false)) { + return $this->generateConditions($var, $type->types); } if ($type instanceof NullableType && $this->getConfig('nullable', false)) { - $type = null; - return; + return $this->generateConditions($var, [$type->type], true); } - $throwableType = $type instanceof NullableType ? $type->type : $type; - if (\in_array($throwableType->toString(), $this->getConfig('types', []))) { - $type = null; + $subType = $type instanceof NullableType ? $type->type : $type; + if (\in_array($subType->toString(), $this->getConfig('types', []))) { + return $this->generateConditions($var, [$subType], $type instanceof NullableType); } + return null; } /** - * Remove param. - * - * @param Param $node Parameter - * @return void - */ - public function enterParam(Param $node): void - { - $this->strip($node->type); - } - /** - * Strip return throwable type. + * Strip type hints from function. * * @param FunctionLike $func Function * * @return void */ - public function enterFuncReturn(FunctionLike $func): void + public function enterFunction(FunctionLike $func, Context $ctx): void { - $this->strip($func->getReturnType()); + $functionName = new Method(); + if ($func instanceof ClassMethod) { + /** @var ClassLike */ + $parent = $ctx->parents->top(); + if (!$parent->name) { + $functionName = new Concat(new String_('class@anonymous:'), new MagicConstFunction_()); + } + } + $stmts = []; + foreach ($func->getParams() as $index => $param) { + if (!$condition = $this->strip($param->variadic ? new Variable('phabelVariadic') : $param->var, $param->type)) { + break; + } + $param->type = null; + [$noOop, $string, $condition] = $condition; + $start = $param->variadic ? new Concat(new String_("Argument "), new Plus(new LNumber($index), new Variable('phabelVariadicIndex'))) : new String_("Argument $index"); + $start = new Concat($start, new String_(" passed to ")); + $start = new Concat($start, $functionName); + $start = new Concat($start, new String_(" must be ")); + $start = new Concat($start, $string); + $start = new Concat($start, new String_(", ")); + $start = new Concat($start, $noOop ? self::call('gettype', $param->var) : self::callPoly('gettype', $param->var)); + $start = new Concat($start, new String_(" given, called in ")); + $start = new Concat($start, self::callPoly('trace', 0)); + + $if = new If_($condition, [new Throw_(new New_(new FullyQualified(\TypeError::class), [$start]))]); + if ($param->variadic) { + $stmts []= new Foreach_($param->var, new Variable('phabelVariadic'), ['keyVar' => new Variable('phabelVariadicIndex'), 'stmts' => [$if]]); + } else { + $stmts []= $if; + } + } + if ($stmts) { + $this->toClosure($func); + $func->stmts = \array_merge($stmts, $func->getStmts()); + } + + if ($this->getConfig('void', false) && $func->getReturnType() instanceof Identifier && $func->getReturnType()->toLowerString() === 'void') { + $this->toClosure($func); + $this->stack->push([true]); + } + $var = new Variable('phabelReturn'); + if (!$condition = $this->strip($var, $func->getReturnType(), false)) { + $this->stack->push([null]); + return; + } + $this->toClosure($func); + $this->stack->push([false, $functionName, $func->returnsByRef(), ...$condition]); + return $func; + } + public function enterReturn(Return_ $return, Context $ctx): ?Node + { + $current = $this->stack->top(); + if ($current[0] === null) { + return; + } + if ($current[0] === true) { + if ($return->expr !== null) { + // This should be a transpilation error, wait for better stack traces before throwing here + return new Throw_(new New_(new FullyQualified(\ParseError::class), [new String_("A void function must not return a value")])); + } + return; + } + [, $functionName, $byRef, $noOop, $string, $condition] = $current; + + $var = new Variable('phabelReturn'); + $assign = $byRef ? new AssignRef($var, $return->expr ?? new Name('null')) : new Assign($var, $return->expr ?? new Name('null')); + + $start = new String_("Return value of"); + $start = new Concat($start, $functionName); + $start = new Concat($start, new String_(" must be ")); + $start = new Concat($start, $string); + $start = new Concat($start, new String_(", ")); + $start = new Concat($start, $noOop ? self::call('gettype', $var) : self::callPoly('gettype', $var)); + $start = new Concat($start, new String_(" returned in ")); + $start = new Concat($start, self::callPoly('trace', 0)); + + $if = new If_($condition, [new Throw_(new New_(new FullyQualified(\TypeError::class), [$start]))]); + + $return->expr = $var; + + $ctx->insertBefore($ctx->parents->top(), $assign, $if); + } + public static function trace($index): string + { + $trace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[$index]; + return ($trace['file'] ?? '').' on line '.($trace['line'] ?? ''); + } + public static function gettype($object) + { + if (\is_object($object)) { + $type = \get_class($object); + return str_starts_with($type, 'class@anonymous') ? 'instance of class@anonymous' : "instance of $type"; + } + return \gettype($object); + } + + public static function runWithAfter(): array + { + return [StringConcatOptimizer::class]; } } diff --git a/src/Target/Php70/AnonymousClassReplacer.php b/src/Target/Php70/AnonymousClassReplacer.php index 9d2bf75..e31d67a 100644 --- a/src/Target/Php70/AnonymousClassReplacer.php +++ b/src/Target/Php70/AnonymousClassReplacer.php @@ -53,17 +53,8 @@ class AnonymousClassReplacer extends Plugin $prevNode = $node; foreach ($ctx->parents as $node) { if ($node instanceof Namespace_ || $node instanceof RootNode) { - $foundIndex = -1; - foreach ($node->stmts as $index => $curNode) { - if ($curNode === $prevNode) { - $foundIndex = $index; - break; - } - } - if ($foundIndex >= 0) { - \array_splice($node->stmts, $foundIndex, 0, [$classNode]); - return; - } + $ctx->insertBefore($node, $classNode); + return; } $prevNode = $node; } diff --git a/src/Traverser.php b/src/Traverser.php index 897fa91..fd35580 100644 --- a/src/Traverser.php +++ b/src/Traverser.php @@ -130,9 +130,12 @@ class Traverser } $context->parents->push($node); foreach ($node->getSubNodeNames() as $name) { + $node->setAttribute('currentNode', $name); + $subNode = &$node->{$name}; if (\is_array($subNode)) { - foreach ($subNode as &$subNodeNode) { + foreach ($subNode as $index => &$subNodeNode) { + $node->setAttribute('currentNodeIndex', $index); $this->traverseNode($subNodeNode, $plugins, $context); } } else {