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')