Update
This commit is contained in:
parent
9b35d45048
commit
653f76862d
|
@ -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<Node>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Phabel\Context;
|
||||
|
||||
use Phabel\Plugin;
|
||||
|
||||
class UniqueId extends Plugin
|
||||
{
|
||||
}
|
|
@ -177,17 +177,24 @@ abstract class Plugin implements PluginInterface
|
|||
*/
|
||||
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('<?php '.$data);
|
||||
return self::toNode(\var_export($data, true));
|
||||
}
|
||||
/**
|
||||
* Convert code to node.
|
||||
*
|
||||
* @param string $code Code
|
||||
*
|
||||
* @memoize $code
|
||||
*
|
||||
* @return Node
|
||||
*/
|
||||
public static function toNode(string $code): Node
|
||||
{
|
||||
$res = (new ParserFactory)->create(ParserFactory::PREFER_PHP7)->parse('<?php '.$code);
|
||||
if ($res === null || empty($res) || !$res[0] instanceof Expression || !isset($res[0]->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}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue