This commit is contained in:
Daniil Gentili 2020-08-14 20:40:01 +02:00
parent 9b35d45048
commit 653f76862d
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
6 changed files with 259 additions and 55 deletions

View File

@ -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);
}
}

View File

@ -1,9 +0,0 @@
<?php
namespace Phabel\Context;
use Phabel\Plugin;
class UniqueId extends Plugin
{
}

View File

@ -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}

View File

@ -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];
}
}

View File

@ -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;
}

View File

@ -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 {