Initial commit

This commit is contained in:
Daniil Gentili 2020-08-03 21:31:32 +02:00
commit c43eac2b19
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
29 changed files with 1329 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/vendor/
composer.lock

15
composer.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "phabel/phabel",
"description": "Write and deploy modern PHP 8 code, today.",
"type": "project",
"require": {
"nikic/php-parser": "^4.7"
},
"license": "MIT",
"authors": [
{
"name": "Daniil Gentili",
"email": "daniil@daniil.it"
}
]
}

15
psalm.xml Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<psalm
errorLevel="1"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>

8
src/Context.php Normal file
View File

@ -0,0 +1,8 @@
<?php
namespace Phabel;
class Context
{
}

10
src/Context/UniqueId.php Normal file
View File

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

141
src/Plugin.php Normal file
View File

@ -0,0 +1,141 @@
<?php
namespace Phabel;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Expression;
use PhpParser\ParserFactory;
use ReflectionClass;
abstract class Plugin implements PluginInterface
{
/**
* Configuration array.
*/
private array $config = [];
/**
* 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) {
/** @var Node */
$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);
}
/**
* Call function.
*
* @param class-string|array{0: class-string, 1: 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 polyfill function from current plugin.
*
* @param string $name Function name
* @param Expr|Arg ...$parameters Parameters
*
* @return StaticCall
*/
protected static function callPoly(string $name, ...$parameters): StaticCall
{
return self::call([static::class, $name], ...$parameters);
}
/**
* Convert array, int or other literal to node.
*
* @param mixed $data Data to convert
*
* @return Node
*/
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);
if ($res === null || empty($res) || !$res[0] instanceof Expression || !isset($res[0]->expr)) {
throw new \RuntimeException('An invalid literal was provided!');
}
return $cache[$data] = $res[0]->expr;
}
/**
* {@inheritDoc}
*/
public function getConfig(string $key)
{
return $this->config[$key];
}
/**
* {@inheritDoc}
*/
public function setConfig(string $key, $value): void
{
$this->config[$key] = $value;
}
/**
* {@inheritDoc}
*/
public function needs()
{
return [];
}
/**
* {@inheritDoc}
*/
public function extends()
{
return [];
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Phabel\Plugin;
use Phabel\Plugin;
use PhpParser\Node;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\NullableType;
use PhpParser\Node\Param;
use PhpParser\Node\UnionType;
/**
* Replace all usages of a certain type in typehints.
*/
class TypeHintStripper extends Plugin
{
/**
* Strip typehint.
*
* @param null|Identifier|Name|NullableType|UnionType &$type Type
*
* @return void
*/
private function strip(?Node &$type): void
{
if (!$type || $type instanceof UnionType) {
return;
}
if ($type instanceof NullableType && $this->getConfig('nullable')) {
$type = null;
return;
}
$throwableType = $type instanceof NullableType ? $type->type : $type;
// Make this less ugly when we implement a namespace context
if (\in_array($throwableType->toString(), $this->getConfig('types'))) {
$type = null;
}
}
/**
* Remove param.
*
* @param Param $node Parameter
* @return void
*/
public function enterParam(Param $node): void
{
$this->strip($node->type);
}
/**
* Strip return throwable type.
*
* @param FunctionLike $func Function
*
* @return void
*/
public function enterFuncReturn(FunctionLike $func): void
{
$this->strip($func->getReturnType());
}
}

53
src/PluginInterface.php Normal file
View File

@ -0,0 +1,53 @@
<?php
namespace Phabel;
use PhpParser\NodeVisitor;
interface PluginInterface
{
/**
* Specify which plugins does this plugin require.
*
* At each level, the traverser will execute the enter and leave methods of the specified plugins, completing a full AST traversal before starting a new AST traversal for the current plugin.
* Of course, this increases complexity as it forces an additional traversal of the AST.
*
* When possible, use the extends method to reduce complexity.
*
* @return array|string Plugin name(s)
*
* @psalm-return array<class-string<Plugin|NodeVisitor>>|class-string<Plugin|NodeVisitor>
*/
public function needs();
/**
* Specify which plugins does this plugin extends.
*
* At each depth level, the traverser will first execute the enter|leave methods of the specified plugins, then immediately execute the enter|leave methods of the current plugin.
*
* This is preferred, and allows only a single traversal of the AST.
*
* @return array|string Plugin name(s)
*
* @psalm-return array<class-string<Plugin|NodeVisitor>>|class-string<Plugin|NodeVisitor>
*/
public function extends();
/**
* Get configuration key
*
* @param string $key Key
*
* @return mixed
*/
public function getConfig(string $key);
/**
* Set configuration key
*
* @param string $key Key
* @param mixed $value Value
*
* @return void
*/
public function setConfig(string $key, $value): void;
}

View File

@ -0,0 +1,28 @@
<?php
namespace Spatie\Php7to5\NodeVisitors;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
class MethodCallReplacer extends NodeVisitorAbstract
{
/**
* {@inheritdoc}
*/
public function leaveNode(Node $node)
{
if (!$node instanceof Node\Expr\MethodCall) {
return;
}
$value = &$node->var;
if (!$value instanceof Node\Expr\Clone_ &&
!$value instanceof Node\Expr\Yield_ &&
!$value instanceof Node\Expr\Closure
) {
return;
}
$value = new Node\Expr\FuncCall(new Node\Name('\\returnMe'), [$value]);
return $node;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Spatie\Php7to5\NodeVisitors;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PhpParser\ParserFactory;
class YieldFromReplacer extends NodeVisitorAbstract
{
/**
* @var array
*/
protected $foreachYield;
/**
* {@inheritdoc}
*/
public function leaveNode(Node $node)
{
if (!$node instanceof Node\Expr\YieldFrom) {
return;
}
$generator = $node->expr;
return new Node\Expr\Yield_($generator);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Spatie\Php7to5\NodeVisitors;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\Node\Stmt\Declare_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\NodeVisitorAbstract;
use Spatie\Php7to5\Converter;
use Spatie\Php7to5\Exceptions\InvalidPhpCode;
class YieldReturnDetector extends NodeVisitorAbstract
{
protected $hasYield = [];
/**
* {@inheritdoc}
*/
public function enterNode(Node $node)
{
if ($node instanceof Node\FunctionLike) {
$this->hasYield []= $node;
}
if ($node instanceof Node\Expr\Yield_ ||
$node instanceof Node\Expr\YieldFrom
) {
end($this->hasYield)->hasYield = true;
}
}
public function leaveNode(Node $node)
{
if ($node instanceof Node\FunctionLike) {
array_pop($this->hasYield);
}
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Spatie\Php7to5\NodeVisitors;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
class YieldReturnReplacer extends NodeVisitorAbstract
{
protected $functions = [];
/**
* {@inheritdoc}
*/
public function enterNode(Node $node)
{
if ($node instanceof Node\FunctionLike) {
$this->functions[] = $node;
}
}
/**
* {@inheritdoc}
*/
public function leaveNode(Node $node)
{
if ($node instanceof Node\FunctionLike) {
array_pop($this->functions);
return;
}
if (!$node instanceof Node\Stmt\Return_) {
return;
}
if ($node->expr === null) {
return new Node\Stmt\Return_();
}
if (!(end($this->functions)->hasYield ?? false)) {
return;
}
$value = $node->expr;
$newReturn = new Node\Expr\Yield_(
new Node\Expr\New_(
new Node\Expr\ConstFetch(
new Node\Name('\YieldReturnValue')
),
[$value]
)
);
$stmts = [$newReturn, new Node\Stmt\Return_()];
return $stmts;
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace Spatie\Php7to5\NodeVisitors;
use PhpParser\Node;
use PhpParser\Node\Stmt\Declare_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\NodeVisitorAbstract;
use Spatie\Php7to5\Converter;
use Spatie\Php7to5\Exceptions\InvalidPhpCode;
class AnonymousClassReplacer extends NodeVisitorAbstract
{
/**
* @var array
*/
protected $anonymousClassNodes = [];
public static $count = 0;
/**
* {@inheritdoc}
*/
public function leaveNode(Node $node)
{
if (!$node instanceof Node\Expr\New_) {
return;
}
$classNode = $node->class;
if (!$classNode instanceof Node\Stmt\Class_) {
return;
}
$newClassName = 'AnonymousClass'.(self::$count++);
$classNode->name = $newClassName;
$this->anonymousClassNodes[] = $classNode;
// Generate new code that instantiate our new class
$newNode = new Node\Expr\New_(
new Node\Expr\ConstFetch(
new Node\Name($newClassName)
),
$node->args
);
return $newNode;
}
/**
* {@inheritdoc}
*/
public function afterTraverse(array $nodes)
{
if (count($this->anonymousClassNodes) === 0) {
return $nodes;
}
$anonymousClassStatements = $this->anonymousClassNodes;
$anonymousClassStatements = $this->convertToPhp5Statements($anonymousClassStatements);
$hookIndex = $this->getAnonymousClassHookIndex($nodes);
$nodes = $this->moveAnonymousClassesToHook($nodes, $hookIndex, $anonymousClassStatements);
return $nodes;
}
/**
* Find the index of the first statement that is not a declare, use or namespace statement.
*
* @param array $statements
*
* @return int
*
* @throws \Spatie\Php7to5\Exceptions\InvalidPhpCode
*/
protected function getAnonymousClassHookIndex(array $statements)
{
$hookIndex = false;
foreach ($statements as $index => $statement) {
if (!$statement instanceof Declare_ &&
!$statement instanceof Use_ &&
!$statement instanceof Namespace_) {
$hookIndex = $index;
}
}
if ($hookIndex === false) {
return 1;
//throw InvalidPhpCode::noValidLocationFoundToInsertClasses();
}
return $hookIndex;
}
/**
* @param array $nodes
* @param $hookIndex
* @param $anonymousClassStatements
*
* @return array
*/
protected function moveAnonymousClassesToHook(array $nodes, $hookIndex, $anonymousClassStatements)
{
$preStatements = array_slice($nodes, 0, $hookIndex);
$postStatements = array_slice($nodes, $hookIndex);
return array_merge($preStatements, $anonymousClassStatements, $postStatements);
}
/**
* @param array $php7statements
*
* @return \PhpParser\Node[]
*/
public function convertToPhp5Statements(array $php7statements)
{
$converter = Converter::getTraverser($php7statements);
$php5Statements = $converter->traverse($php7statements);
return $php5Statements;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Phabel\Target\Php70;
use Phabel\Plugin;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
class ClosureCallReplacer extends Plugin
{
/**
* Replace composite function calls
*
* @param FuncCall $node Function call
*
* @return StaticCall|null
*/
public function enter(FuncCall $node): ?StaticCall
{
$name = $node->name;
if ($name instanceof Name || $name instanceof Variable) {
return null;
}
\array_unshift($node->args, new Arg($name));
return self::callPoly('callMe', ...$node->args);
}
/**
* Call provided argument.
*
* @param callable $callable Callable
* @param mixed ...$arguments Arguments
*
* @return mixed
*/
public static function callMe(callable $callable, ...$arguments)
{
return $callable($arguments);
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace Phabel\Target\Php70;
use Phabel\Plugin;
use PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\Isset_;
use PhpParser\Node\Expr\Yield_;
/**
* Fix certain compound access statements.
*/
class CompoundAccess extends Plugin
{
/**
* Replace non-simple isset calls.
*
* @param Isset_ $node Isset node
*
* @return void
*/
public function enterIsset(Isset_ $node): void
{
foreach ($node->vars as &$var) {
if (!$var instanceof ArrayDimFetch) {
continue;
}
if (!$var->var instanceof ClassConstFetch) {
continue;
}
$var->var = self::callPoly('returnMe', $var->var);
}
}
/**
* Fix yield array access.
*
* @param ArrayDimFetch $node Node
*
* @return void
*/
public function enterArrayYield(ArrayDimFetch $node): void
{
if (!$node->var instanceof Node\Expr\Yield_) {
return;
}
$node->var = self::callPoly('returnMe', $node->var);
}
/**
* Fix yield array access
*
* @param Yield_ $node Yield
*
* @return void
*/
public function enterYield(Yield_ $node): void
{
$value = &$node->value;
if ($value instanceof Node\Expr\Variable && $value->name !== "this") {
return;
}
if ($value instanceof Node\Expr\FuncCall ||
$value instanceof Node\Expr\MethodCall ||
$value instanceof Node\Expr\StaticCall ||
$value instanceof Node\Scalar
) {
return;
}
$value = self::callPoly('returnMe', $value);
}
/**
* Returns the data provided.
*
* @param mixed $data Data
*
* @return mixed
*
* @template T
*
* @psalm-param T $data data
*
* @psalm-return T
*/
public static function returnMe($data)
{
return $data;
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Phabel\Target\Php70;
use Phabel\Plugin;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Const_;
/*
* Converts define() arrays into const arrays
*/
class DefineArrayReplacer extends Plugin
{
/**
* Convert define() arrays into const arrays
*
* @param FuncCall $node Node
*
* @return Const_|null
*/
public function enter(FuncCall $node): ?Const_
{
if (!$node->name instanceof Name || $node->name->toString() != 'define') {
return null;
}
$nameNode = $node->args[0]->value;
$valueNode = $node->args[1]->value;
if (!$valueNode instanceof Node\Expr\Array_) {
return null;
}
$constNode = new Node\Const_($nameNode->value, $valueNode);
return new Node\Stmt\Const_([$constNode]);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Phabel\Target\Php70;
use Phabel\Plugin;
use PhpParser\Node;
use PhpParser\Node\Stmt\GroupUse;
use PhpParser\Node\Stmt\Use_;
use PhpParser\Node\Stmt\UseUse;
class GroupUseReplacer extends Plugin
{
/**
* Replace group use with multiple use statements
*
* @param GroupUse $node Group use statement
*
* @return Use_[]
*/
public function leave(GroupUse $node): array
{
$nodePrefixParts = $node->prefix->parts;
return \array_map(fn (UseUse $useNode) => $this->createUseNode($nodePrefixParts, $useNode), $node->uses);
}
/**
* Create separate use node.
*
* @param string[] $nodePrefixParts Use prefix
* @param UseUse $useNode Current use node
*
* @return Use_ New use node
*/
protected function createUseNode(array $nodePrefixParts, UseUse $useNode): Use_
{
$nodePrefixParts []= $useNode->name;
$nameNode = new Node\Name($nodePrefixParts);
return new Use_([new UseUse($nameNode, $useNode->alias)], $useNode->type);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Phabel\Target\Php70;
use Phabel\Plugin;
use PhpParser\Node;
use PhpParser\Node\Expr\BinaryOp\Coalesce;
use PhpParser\Node\Expr\StaticCall;
class NullCoalesceReplacer extends Plugin
{
/**
* Replace null coalesce
*
* @param Coalesce $node Coalesce
*
* @return StaticCall
*/
public function enter(Coalesce $node): StaticCall
{
if (!($node->left instanceof Node\Expr\ErrorSuppress)) {
$node->left = new Node\Expr\ErrorSuppress($node->left);
}
return self::callPoly('coalesce', $node->left, $node->right);
}
/**
* Coalesce.
*
* @param null|mixed $ifNotNull If not null, return this
* @param mixed $then Else this
*
* @return mixed
*/
public static function coalesce($ifNotNull, $then)
{
return isset($ifNotNull) ?: $then;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Phabel\Target\Php70;
use Phabel\Plugin;
use PhpParser\Node;
class ReservedNameReplacer extends Plugin
{
/**
* {@inheritdoc}
*/
public function leaveNode(Node $node): void
{
if (!$node instanceof Node\Expr\MethodCall &&
!$node instanceof Node\Expr\StaticCall &&
!$node instanceof Node\Stmt\ClassMethod &&
!$node instanceof Node\Expr\ClassConstFetch &&
!$node instanceof Node\Const_
) {
return;
}
$name = &$node->name;
if (!\is_string($name) || !\in_array(\strtolower($name), ['continue', 'empty', 'use', 'default', 'echo'])) {
return;
}
$name .= '_';
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Phabel\Target\Php70;
use Phabel\Plugin;
use Phabel\Plugin\TypeHintStripper;
class ScalarTypeHintsRemover extends Plugin
{
/**
* Alias.
*
* @return array
*/
public function needs(): array
{
return [
TypeHintStripper::class => [
'types' => ['int', 'integer', 'float', 'string', 'bool', 'boolean']
]
];
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Phabel\Target\Php70;
use Phabel\Plugin;
use PhpParser\Node\Expr\BinaryOp\Spaceship;
use PhpParser\Node\Expr\StaticCall;
/**
* Polyfill spaceship operator.
*/
class SpaceshipOperatorReplacer extends Plugin
{
/**
* Replace spaceship operator.
*
* @param Spaceship $node Node
*
* @return StaticCall
*/
public function enter(Spaceship $node): StaticCall
{
return self::callPoly('spaceship', $node->left, $node->right);
}
/**
* Spacesip operator.
*
* @param integer|string|float $a A
* @param integer|string|float $b B
*
* @return integer
*/
public static function spaceship($a, $b): int
{
return $a < $b ? -1 : ($a === $b ? 0 : 1);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Phabel\Target\Php70;
use PhpParser\Node\Stmt\Declare_;
use PhpParser\Node\Stmt\DeclareDeclare;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
class StrictTypesDeclareStatementRemover extends NodeVisitorAbstract
{
/**
* {@inheritdoc}
*/
public function leave(Declare_ $node): ?int
{
$node->declares = \array_filter($node->declares, fn (DeclareDeclare $declare) => $declare->key->name !== 'strict_types');
if (empty($node->declares)) {
return NodeTraverser::REMOVE_NODE;
}
return null;
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace Phabel\Target\Php70;
use Phabel\Plugin;
use Phabel\Plugin\TypeHintStripper;
use PhpParser\Node;
use PhpParser\Node\Expr\BinaryOp\BooleanOr;
use PhpParser\Node\Expr\Instanceof_;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\TryCatch;
/**
* Replace \Throwable usages.
*/
class ThrowableReplacer extends Plugin
{
/**
* Check if type string is \Throwable or Throwable.
*
* @param string $type Type string
*
* @return boolean
*/
private function isThrowable(string $type): bool
{
// Make this less ugly when we implement a namespace context
return $type === \Throwable::class || $type === 'Throwable';
}
/**
* Split instance of \Throwable.
*
* @param Instanceof_ $node
*
* @return ?BooleanOr
*/
public function enterInstanceOf(Instanceof_ $node): ?BooleanOr
{
if (!$this->isThrowable($node->class)) {
return null;
}
return new BooleanOr(
new Instanceof_($node->expr, new FullyQualified('Exception')),
new Instanceof_($node->expr, new FullyQualified('Error'))
);
}
/**
* Substitute try-catch.
*
* @param TryCatch $node TryCatch node
*
* @return void
*/
public function enterTryCatch(TryCatch $node): void
{
foreach ($node->catches as $catch) {
$alreadyHasError = false;
$needs = false;
foreach ($catch->types as &$type) {
if ($type instanceof FullyQualified &&
$type->getLast() === "Error") {
$alreadyHasError = true;
}
if ($this->isThrowable($type)) {
$needs = true;
$type = new FullyQualified('Exception');
}
}
if ($needs && !$alreadyHasError) {
$catch->types[] = new FullyQualified('Error');
}
}
}
/**
* Other transforms.
*
* @return array
*/
public function extends(): array
{
return [
TypeHintStripper::class => [
'type' => [
\Throwable::class,
'Throwable'
]
]
];
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Phabel\Target\Php71;
use Phabel\Plugin;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\List_;
use PhpParser\Node\Stmt\Foreach_;
/**
* Replaces [] array list syntax.
*/
class ArrayList extends Plugin
{
/**
* Caled when entering Foreach_ node.
*
* @param Foreach_ $node Node
*
* @return void
*/
public function enterForeach(Foreach_ $node): void
{
if ($node->valueVar instanceof Array_) {
self::replaceTypeInPlace($node->valueVar, List_::class);
}
}
/**
* Called when entering assignment node.
*
* @param Assign $node Node
*
* @return void
*/
public function enterAssign(Assign $node): void
{
if ($node->var instanceof Array_) {
self::replaceTypeInPlace($node->var, List_::class);
}
}
/**
* Called when entering list for nested lists.
*
* @param List_ $node Node
*
* @return void
*/
public function enterList(List_ $node): void
{
foreach ($node->items as $item) {
if ($item->value instanceof Array_) {
self::replaceTypeInPlace($item->value, List_::class);
}
}
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Phabel\Target\Php71;
use Phabel\Plugin;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\NodeVisitor\NameResolver;
/**
* Removes the class constant visibility modifiers (PHP 7.1).
*/
class ClassConstantVisibilityModifiersRemover extends Plugin
{
/**
* Makes public private and protected class constants.
*
* @param ClassConst $node Constant
*
* @return void
*/
public function enter(ClassConst $node): void
{
$node->flags = 0; // Remove constant modifier
}
/**
* {@inheritDoc}
*/
public function needs(): string
{
return NameResolver::class;
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace Phabel\Target\Php71;
use Phabel\Plugin;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\List_;
use PhpParser\Node\Stmt\Foreach_;
/**
* Polyfills keyed list assignment.
*/
class ListKey extends Plugin
{
/**
* Parse list foreach with custom keys.
*
* @param Foreach_ $node Foreach
*
* @return void
*/
public function enterForeach(Foreach_ $node): void
{
if (!$node->valueVar instanceof List_ || !$this->shouldSplit($node->valueVar)) {
return;
}
[$node->valueVar, $array] = $this->splitList($node->valueVar);
$node->expr = self::callPoly('destructure', $array, $node->expr);
}
/**
* Parse list assignment with custom keys.
*
* @param Assign $node List assignment
*
* @return void
*/
public function enterAssign(Assign $node): void
{
if (!$node->var instanceof List_ || !$this->shouldSplit($node->var)) {
return;
}
[$node->var, $array] = $this->splitList($node->var);
$node->expr = self::callPoly('destructure', $array, $node->expr);
}
/**
* Whether this is a keyed list.
*
* @param List_ $list List
*
* @return boolean
*/
private function shouldSplit(List_ $list): bool
{
return isset($list->items[0]->key);
}
/**
* Split keyed list into new list assignment and array to pass to destructure function.
*
* @param List_ $list Keyed list
*
* @return array{0: List_, 1: Array_}
*/
private function splitList(List_ $list): array
{
$newList = [];
$keys = [];
$key = 0; // Technically list assignment does not support mixed keys, but we need this for nested assignments
foreach ($list->items as $item) {
if ($item) {
$curKey = $item->key ?? $key++;
$item->key = null;
if ($item->value instanceof List_) {
[$item->value, $keys[$curKey]] = $this->splitList($list);
} else {
$keys[$curKey] = null;
}
} else {
$newList []= null;
$keys[$key++] = null;
}
}
/** @var Array_ */
$keys = self::toLiteral($keys);
return [new List_($newList), $keys];
}
/**
* Destructure array.
*
* @param array $keys Custom keys
* @param array $array Array
*
* @psalm-param array<string, null|array> $keys Custom keys
*
* @return array
*/
public static function destructure(array $keys, array $array): array
{
$res = [];
foreach ($keys as $key => $type) {
if ($type === null) {
$res[] = $array[$key];
} else {
$res[] = self::destructure($type, $array[$key]);
}
}
return $res;
}
/**
* {@inheritDoc}
*/
public function needs(): string
{
return ArrayList::class;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Phabel\Target\Php71;
use Phabel\Plugin;
use PhpParser\Node\Stmt\TryCatch;
/**
* Replace compound catches.
*/
class MultipleCatchReplacer extends Plugin
{
/**
* Replace compound catches.
*
* Do this while leaving to avoid re-iterating uselessly on duplicated code.
*
* @param TryCatch $node Catch stmt
*
* @return void
*/
public function leave(TryCatch $node): void
{
$catches = [];
foreach ($node->catches as $catch) {
if (\count($catch->types) === 1) {
$catches []= $catch;
} else {
foreach ($catch->types as $type) {
$ncatch = clone $catch;
$ncatch->types = [$type];
$catches []= $ncatch;
}
}
}
$node->catches = $catches;
}
/**
* Extends throwable replacer
*
* @return string
*
* @psalm-return class-string
*/
public function extends(): string
{
return ThrowableReplacer::class;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Spatie\Php7to5\NodeVisitors;
use Phabel\Plugin;
use Phabel\Plugin\TypeHintStripper;
class NullableTypeRemover extends Plugin
{
/**
* Remove nullable typehint
*
* @return array
*/
public function needs(): array
{
return [
TypeHintStripper::class => [
'nulable' => true
]
];
}
}

8
src/Traverser.php Normal file
View File

@ -0,0 +1,8 @@
<?php
namespace Phabel;
class Traverser
{
}