From b998d1e9b5cb2e8b1bde91bdcff9d7cffdb2bc86 Mon Sep 17 00:00:00 2001 From: Nikita Popov <nikita.ppv@googlemail.com> Date: Sat, 3 Mar 2018 15:40:51 +0100 Subject: [PATCH] Add funcCall(), methodCall() and staticCall() builders --- CHANGELOG.md | 10 +++- doc/component/AST_builders.markdown | 6 ++ lib/PhpParser/BuilderFactory.php | 75 +++++++++++++++++++++++- lib/PhpParser/BuilderHelpers.php | 36 +++++++++++- lib/PhpParser/Node/Expr/FuncCall.php | 4 +- test/PhpParser/Builder/ClassTest.php | 2 +- test/PhpParser/BuilderFactoryTest.php | 82 +++++++++++++++++++++++++++ 7 files changed, 207 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2961f6ce..f20c9fa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ Version 4.0.1-dev ----------------- -Nothing yet. +### Changed + +* Added checks to node traverser to prevent replacing a statement with an expression or vice versa. + This should prevent common mistakes in the implementation of node visitors. + +### Added + +* Added `funcCall()`, `methodCall()` and `staticCall()` methods to `BuilderFactory`, to simplify + creation of call nodes. Version 4.0.0 (2018-02-28) -------------------------- diff --git a/doc/component/AST_builders.markdown b/doc/component/AST_builders.markdown index 0e32873d..9158818e 100644 --- a/doc/component/AST_builders.markdown +++ b/doc/component/AST_builders.markdown @@ -100,6 +100,12 @@ nodes. The following methods are currently available: * `val($value)`: Creates an AST node for a literal value like `42` or `[1, 2, 3]`. * `args(array $args)`: Creates an array of function/method arguments, including the required `Arg` wrappers. Also converts literals to AST nodes. + * `funcCall($name, array $args = [])`: Create a function call node. Converts `$name` to a `Name` + node and normalizes arguments. + * `methodCall(Expr $var, $name, array $args = [])`: Create a method call node. Converts `$name` to + an `Identifier` node and normalizes arguments. + * `staticCall($class, $name, array $args = [])`: Create a static method call node. Converts + `$class` to a `Name` node, `$name` to an `Identifier` node and normalizes arguments. * `concat(...$exprs)`: Create a tree of `BinaryOp\Concat` nodes for the given expressions. These methods may be expanded on an as-needed basis. Please open an issue or PR if a common diff --git a/lib/PhpParser/BuilderFactory.php b/lib/PhpParser/BuilderFactory.php index 85c0acc7..03187534 100644 --- a/lib/PhpParser/BuilderFactory.php +++ b/lib/PhpParser/BuilderFactory.php @@ -5,6 +5,8 @@ namespace PhpParser; use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Concat; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name; use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Use_; @@ -141,6 +143,55 @@ class BuilderFactory return $normalizedArgs; } + /** + * Creates a function call node. + * + * @param string|Name|Expr $name Function name + * @param array $args Function arguments + * + * @return Expr\FuncCall + */ + public function funcCall($name, array $args = []) : Expr\FuncCall { + return new Expr\FuncCall( + BuilderHelpers::normalizeNameOrExpr($name), + $this->args($args) + ); + } + + /** + * Creates a method call node. + * + * @param Expr $var Variable the method is called on + * @param string|Identifier|Expr $name Method name + * @param array $args Method arguments + * + * @return Expr\MethodCall + */ + public function methodCall(Expr $var, $name, array $args = []) : Expr\MethodCall { + return new Expr\MethodCall( + $var, + $this->normalizeIdentifierOrExpr($name), + $this->args($args) + ); + } + + /** + * Creates a static method call node. + * + * @param string|Name|Expr $class Class name + * @param string|Identifier|Expr $name Method name + * @param array $args Method arguments + * + * @return Expr\StaticCall + */ + public function staticCall($class, $name, array $args = []) : Expr\StaticCall { + return new Expr\StaticCall( + BuilderHelpers::normalizeNameOrExpr($class), + $this->normalizeIdentifierOrExpr($name), + $this->args($args) + ); + } + /** * Creates nested Concat nodes from a list of expressions. * @@ -161,15 +212,35 @@ class BuilderFactory return $lastConcat; } - private function normalizeStringExpr($expr) { + /** + * @param string|Expr $expr + * @return Expr + */ + private function normalizeStringExpr($expr) : Expr { if ($expr instanceof Expr) { return $expr; } - if (is_string($expr)) { + if (\is_string($expr)) { return new String_($expr); } throw new \LogicException('Expected string or Expr'); } + + /** + * @param string|Identifier|Expr $name + * @return Identifier|Expr + */ + private function normalizeIdentifierOrExpr($name) { + if ($name instanceof Identifier || $name instanceof Expr) { + return $name; + } + + if (\is_string($name)) { + return new Identifier($name); + } + + throw new \LogicException('Expected string or instance of Node\Identifier or Node\Expr'); + } } diff --git a/lib/PhpParser/BuilderHelpers.php b/lib/PhpParser/BuilderHelpers.php index 024afb96..d3ced902 100644 --- a/lib/PhpParser/BuilderHelpers.php +++ b/lib/PhpParser/BuilderHelpers.php @@ -56,13 +56,36 @@ final class BuilderHelpers } /** - * Normalizes a name: Converts plain string names to PhpParser\Node\Name. + * Normalizes a name: Converts string names to Name nodes. * * @param Name|string $name The name to normalize * * @return Name The normalized name */ public static function normalizeName($name) : Name { + return self::normalizeNameCommon($name, false); + } + + /** + * Normalizes a name: Converts string names to Name nodes, while also allowing expressions. + * + * @param Expr|Name|string $name The name to normalize + * + * @return Name|Expr The normalized name or expression + */ + public static function normalizeNameOrExpr($name) { + return self::normalizeNameCommon($name, true); + } + + /** + * Normalizes a name: Converts string names to Name nodes, optionally allowing expressions. + * + * @param Expr|Name|string $name The name to normalize + * @param bool $allowExpr Whether to also allow expressions + * + * @return Name|Expr The normalized name, or expression (if allowed) + */ + private static function normalizeNameCommon($name, bool $allowExpr) { if ($name instanceof Name) { return $name; } elseif (is_string($name)) { @@ -79,7 +102,16 @@ final class BuilderHelpers } } - throw new \LogicException('Name must be a string or an instance of PhpParser\Node\Name'); + if ($allowExpr) { + if ($name instanceof Expr) { + return $name; + } + throw new \LogicException( + 'Name must be a string or an instance of Node\Name or Node\Expr' + ); + } else { + throw new \LogicException('Name must be a string or an instance of Node\Name'); + } } /** diff --git a/lib/PhpParser/Node/Expr/FuncCall.php b/lib/PhpParser/Node/Expr/FuncCall.php index b424f28d..79457670 100644 --- a/lib/PhpParser/Node/Expr/FuncCall.php +++ b/lib/PhpParser/Node/Expr/FuncCall.php @@ -16,8 +16,8 @@ class FuncCall extends Expr * Constructs a function call node. * * @param Node\Name|Expr $name Function name - * @param Node\Arg[] $args Arguments - * @param array $attributes Additional attributes + * @param Node\Arg[] $args Arguments + * @param array $attributes Additional attributes */ public function __construct($name, array $args = [], array $attributes = []) { parent::__construct($attributes); diff --git a/test/PhpParser/Builder/ClassTest.php b/test/PhpParser/Builder/ClassTest.php index 13a453bb..fe324d76 100644 --- a/test/PhpParser/Builder/ClassTest.php +++ b/test/PhpParser/Builder/ClassTest.php @@ -153,7 +153,7 @@ DOC; /** * @expectedException \LogicException - * @expectedExceptionMessage Name must be a string or an instance of PhpParser\Node\Name + * @expectedExceptionMessage Name must be a string or an instance of Node\Name */ public function testInvalidName() { $this->createClassBuilder('Test') diff --git a/test/PhpParser/BuilderFactoryTest.php b/test/PhpParser/BuilderFactoryTest.php index bdebadd9..1bbfec48 100644 --- a/test/PhpParser/BuilderFactoryTest.php +++ b/test/PhpParser/BuilderFactoryTest.php @@ -6,8 +6,12 @@ use PhpParser\Builder; use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Concat; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\LNumber; use PhpParser\Node\Scalar\String_; use PHPUnit\Framework\TestCase; +use Symfony\Component\Yaml\Tests\A; class BuilderFactoryTest extends TestCase { @@ -92,6 +96,84 @@ class BuilderFactoryTest extends TestCase ); } + public function testCalls() { + $factory = new BuilderFactory(); + + // Simple function call + $this->assertEquals( + new Expr\FuncCall( + new Name('var_dump'), + [new Arg(new String_('str'))] + ), + $factory->funcCall('var_dump', ['str']) + ); + // Dynamic function call + $this->assertEquals( + new Expr\FuncCall(new Expr\Variable('fn')), + $factory->funcCall(new Expr\Variable('fn')) + ); + + // Simple method call + $this->assertEquals( + new Expr\MethodCall( + new Expr\Variable('obj'), + new Identifier('method'), + [new Arg(new LNumber(42))] + ), + $factory->methodCall(new Expr\Variable('obj'), 'method', [42]) + ); + // Explicitly pass Identifier node + $this->assertEquals( + new Expr\MethodCall( + new Expr\Variable('obj'), + new Identifier('method') + ), + $factory->methodCall(new Expr\Variable('obj'), new Identifier('method')) + ); + // Dynamic method call + $this->assertEquals( + new Expr\MethodCall( + new Expr\Variable('obj'), + new Expr\Variable('method') + ), + $factory->methodCall(new Expr\Variable('obj'), new Expr\Variable('method')) + ); + + // Simple static method call + $this->assertEquals( + new Expr\StaticCall( + new Name\FullyQualified('Foo'), + new Identifier('bar'), + [new Arg(new Expr\Variable('baz'))] + ), + $factory->staticCall('\Foo', 'bar', [new Expr\Variable('baz')]) + ); + // Dynamic static method call + $this->assertEquals( + new Expr\StaticCall( + new Expr\Variable('foo'), + new Expr\Variable('bar') + ), + $factory->staticCall(new Expr\Variable('foo'), new Expr\Variable('bar')) + ); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage Expected string or instance of Node\Identifier or Node\Expr + */ + public function testInvalidIdentifier() { + (new BuilderFactory())->staticCall('Foo', new Name('bar')); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage Name must be a string or an instance of Node\Name or Node\Expr + */ + public function testInvalidNameOrExpr() { + (new BuilderFactory())->funcCall(new Node\Stmt\Return_()); + } + public function testIntegration() { $factory = new BuilderFactory; $node = $factory->namespace('Name\Space')