332 lines
10 KiB
PHP
332 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Phabel;
|
|
|
|
use PhpParser\Node;
|
|
use PhpParser\Node\Arg;
|
|
use PhpParser\Node\Expr;
|
|
use PhpParser\Node\Expr\ArrayDimFetch;
|
|
use PhpParser\Node\Expr\Assign;
|
|
use PhpParser\Node\Expr\AssignOp;
|
|
use PhpParser\Node\Expr\AssignRef;
|
|
use PhpParser\Node\Expr\Cast\String_;
|
|
use PhpParser\Node\Expr\Clone_;
|
|
use PhpParser\Node\Expr\Eval_;
|
|
use PhpParser\Node\Expr\FuncCall;
|
|
use PhpParser\Node\Expr\Include_;
|
|
use PhpParser\Node\Expr\List_;
|
|
use PhpParser\Node\Expr\MethodCall;
|
|
use PhpParser\Node\Expr\New_;
|
|
use PhpParser\Node\Expr\NullsafeMethodCall;
|
|
use PhpParser\Node\Expr\NullsafePropertyFetch;
|
|
use PhpParser\Node\Expr\PostDec;
|
|
use PhpParser\Node\Expr\PostInc;
|
|
use PhpParser\Node\Expr\PreDec;
|
|
use PhpParser\Node\Expr\PreInc;
|
|
use PhpParser\Node\Expr\PropertyFetch;
|
|
use PhpParser\Node\Expr\StaticCall;
|
|
use PhpParser\Node\Expr\Variable;
|
|
use PhpParser\Node\Expr\Yield_;
|
|
use PhpParser\Node\Expr\YieldFrom;
|
|
use PhpParser\Node\Name;
|
|
use PhpParser\Node\Stmt\Expression;
|
|
use PhpParser\ParserFactory;
|
|
use ReflectionClass;
|
|
|
|
/**
|
|
* Various tools.
|
|
*
|
|
* @author Daniil Gentili <daniil@daniil.it>
|
|
* @license MIT
|
|
*/
|
|
abstract class Tools
|
|
{
|
|
/**
|
|
* Replace node of one type with another.
|
|
*
|
|
* @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<Node> $class Class of new node
|
|
* @psalm-param array<string, string> $propertyMap Property map between old and new objects
|
|
*
|
|
* @return Node
|
|
*/
|
|
public static function replaceType(Node $node, string $class, array $propertyMap = []): Node
|
|
{
|
|
if ($propertyMap) {
|
|
$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<Node> $class Class of new node
|
|
* @psalm-param array<string, string> $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);
|
|
}
|
|
/**
|
|
* Create variable assignment.
|
|
*
|
|
* @param Variable $name Variable
|
|
* @param Expr $expression Expression
|
|
*
|
|
* @return Expression
|
|
*/
|
|
public static function assign(Variable $name, Expr $expression): Expression
|
|
{
|
|
return new Expression(
|
|
new Assign(
|
|
$name,
|
|
$expression
|
|
)
|
|
);
|
|
}
|
|
/**
|
|
* Call function.
|
|
*
|
|
* @param array{0: class-string, 1: string}|callable-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 method of object.
|
|
*
|
|
* @param Expr $name Object name
|
|
* @param string $method Method
|
|
* @param Expr|Arg ...$parameters Parameters
|
|
*
|
|
* @return MethodCall
|
|
*/
|
|
public static function callMethod(Expr $name, string $method, ...$parameters): MethodCall
|
|
{
|
|
$parameters = \array_map(fn ($data) => $data instanceof Arg ? $data : new Arg($data), $parameters);
|
|
return new MethodCall($name, $method, $parameters);
|
|
}
|
|
/**
|
|
* Convert array, int or other literal to node.
|
|
*
|
|
* @param mixed $data Data to convert
|
|
*
|
|
* @return Node
|
|
*/
|
|
public static function toLiteral($data): Node
|
|
{
|
|
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('Invalid code was provided!');
|
|
}
|
|
return $res[0]->expr;
|
|
}
|
|
|
|
/**
|
|
* Check if this node or any child node have any side effects (like calling other methods, or assigning variables).
|
|
*
|
|
* @param ?Expr $node Node
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function hasSideEffects(?Expr $node): bool
|
|
{
|
|
if (!$node) {
|
|
return false;
|
|
}
|
|
if ($node->hasAttribute('hasSideEffects')
|
|
|| $node instanceof String_ // __toString
|
|
|| $node instanceof ArrayDimFetch // offsetSet/offsetGet
|
|
|| $node instanceof Assign
|
|
|| $node instanceof AssignOp
|
|
|| $node instanceof AssignRef
|
|
|| $node instanceof Clone_ // __clone
|
|
|| $node instanceof Eval_
|
|
|| $node instanceof FuncCall
|
|
|| $node instanceof Include_
|
|
|| $node instanceof List_ // offsetGet/offsetSet
|
|
|| $node instanceof New_
|
|
|| $node instanceof NullsafeMethodCall
|
|
|| $node instanceof NullsafePropertyFetch
|
|
|| $node instanceof PostDec
|
|
|| $node instanceof PostInc
|
|
|| $node instanceof PreDec
|
|
|| $node instanceof PreInc
|
|
|| $node instanceof PropertyFetch
|
|
|| $node instanceof StaticCall
|
|
|| $node instanceof Yield_
|
|
|| $node instanceof YieldFrom
|
|
) {
|
|
$node->setAttribute('hasSideEffects', true);
|
|
return true;
|
|
}
|
|
foreach ($node->getAttributes() as $name) {
|
|
if ($node->{$name} instanceof Expr) {
|
|
if (self::hasSideEffects($node->{$name})) {
|
|
$node->setAttribute('hasSideEffects', true);
|
|
return true;
|
|
}
|
|
} elseif (\is_array($node->{$name})) {
|
|
foreach ($node->{$name} as $var) {
|
|
if ($var instanceof Expr && self::hasSideEffects($var)) {
|
|
$node->setAttribute('hasSideEffects', true);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Create a new object extended from this object, with the specified additional trait + interface
|
|
*
|
|
* @param object $obj
|
|
* @param string $trait
|
|
* @param string $interface
|
|
*
|
|
* @return object
|
|
*/
|
|
public static function cloneWithTrait(object $obj, string $trait, string $interface): object
|
|
{
|
|
$reflect = new ReflectionClass($obj);
|
|
|
|
|
|
$r = $reflect;
|
|
while ($r && $r->isAnonymous()) {
|
|
$r = $r->getParentClass();
|
|
}
|
|
|
|
$extend = "extends \\".$r->getName();
|
|
$eval = "\$newObj = new class $extend implements \\$interface {
|
|
use \\$trait;
|
|
|
|
public function __construct() {}
|
|
};";
|
|
eval($eval);
|
|
|
|
$reflectNew = new ReflectionClass($newObj);
|
|
|
|
do {
|
|
if ($tmp = $reflectNew->getParentClass()) {
|
|
$reflectNew = $tmp;
|
|
}
|
|
foreach ($reflect->getProperties() as $prop) {
|
|
if ($reflectNew->hasProperty($prop->getName())) {
|
|
$propNew = $reflectNew->getProperty($prop->getName());
|
|
$propNew->setAccessible(true);
|
|
$prop->setAccessible(true);
|
|
$propNew->setValue($newObj, $prop->getValue($obj));
|
|
}
|
|
}
|
|
} while ($reflect = $reflect->getParentClass());
|
|
|
|
return $newObj;
|
|
}
|
|
|
|
/**
|
|
* Checks private property exists in an object.
|
|
*
|
|
* @param object $obj Object
|
|
* @param string $var Attribute name
|
|
*
|
|
* @psalm-suppress InvalidScope
|
|
*
|
|
* @return bool
|
|
* @access public
|
|
*/
|
|
public static function hasVar($obj, string $var): bool
|
|
{
|
|
return \Closure::bind(
|
|
function () use ($var) {
|
|
return isset($this->{$var});
|
|
},
|
|
$obj,
|
|
\get_class($obj)
|
|
)->__invoke();
|
|
}
|
|
/**
|
|
* Accesses a private variable from an object.
|
|
*
|
|
* @param object $obj Object
|
|
* @param string $var Attribute name
|
|
*
|
|
* @psalm-suppress InvalidScope
|
|
*
|
|
* @return mixed
|
|
* @access public
|
|
*/
|
|
public static function &getVar($obj, string $var)
|
|
{
|
|
return \Closure::bind(
|
|
function & () use ($var) {
|
|
return $this->{$var};
|
|
},
|
|
$obj,
|
|
\get_class($obj)
|
|
)->__invoke();
|
|
}
|
|
/**
|
|
* Sets a private variable in an object.
|
|
*
|
|
* @param object $obj Object
|
|
* @param string $var Attribute name
|
|
* @param mixed $val Attribute value
|
|
*
|
|
* @psalm-suppress InvalidScope
|
|
*
|
|
* @return void
|
|
*
|
|
* @access public
|
|
*/
|
|
public static function setVar($obj, string $var, &$val): void
|
|
{
|
|
\Closure::bind(
|
|
function () use ($var, &$val) {
|
|
$this->{$var} =& $val;
|
|
},
|
|
$obj,
|
|
\get_class($obj)
|
|
)->__invoke();
|
|
}
|
|
}
|