246 lines
9.6 KiB
PHP
246 lines
9.6 KiB
PHP
<?php
|
|
|
|
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.
|
|
*
|
|
* @author Daniil Gentili <daniil@daniil.it>
|
|
*/
|
|
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 Variable $var Variable
|
|
* @param null|Identifier|Name|NullableType|UnionType $type Type
|
|
*
|
|
* @return array{0: bool, 1: Node, 2: BooleanNot} Conditions for if
|
|
*/
|
|
private function strip(Variable $var, ?Node $type): array
|
|
{
|
|
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)) {
|
|
return $this->generateConditions($var, [$type->type], true);
|
|
}
|
|
$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;
|
|
}
|
|
/**
|
|
* Strip type hints from function.
|
|
*
|
|
* @param FunctionLike $func Function
|
|
*
|
|
* @return void
|
|
*/
|
|
public function enterFunction(FunctionLike $func, Context $ctx): void
|
|
{
|
|
$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];
|
|
}
|
|
}
|