From 0265c28e6e8cb5b180105b4afe0e89b728bfc7f9 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Sun, 12 Jul 2015 23:55:57 +0200 Subject: [PATCH] Switch NameResolver resolve methods to be immutable To make it theoretically possible to expose this as API. Add slice() method to Name to allow this. Deprecate existing mutable Name APIs. --- lib/PhpParser/Node/Name.php | 49 +++++++++++++++++++--- lib/PhpParser/NodeVisitor/NameResolver.php | 44 +++++++++++-------- test/PhpParser/Node/NameTest.php | 32 ++++++++++++++ 3 files changed, 101 insertions(+), 24 deletions(-) diff --git a/lib/PhpParser/Node/Name.php b/lib/PhpParser/Node/Name.php index 1303564..ba6a20c 100644 --- a/lib/PhpParser/Node/Name.php +++ b/lib/PhpParser/Node/Name.php @@ -106,6 +106,8 @@ class Name extends NodeAbstract /** * Sets the whole name. * + * @deprecated Create a new Name instead, or manually modify the $parts property + * * @param string|array|self $name The name to set the whole name to */ public function set($name) { @@ -115,6 +117,8 @@ class Name extends NodeAbstract /** * Prepends a name to this name. * + * @deprecated Use Name::concat($name1, $name2) instead + * * @param string|array|self $name Name to prepend */ public function prepend($name) { @@ -124,6 +128,8 @@ class Name extends NodeAbstract /** * Appends a name to this name. * + * @deprecated Use Name::concat($name1, $name2) instead + * * @param string|array|self $name Name to append */ public function append($name) { @@ -133,6 +139,8 @@ class Name extends NodeAbstract /** * Sets the first part of the name. * + * @deprecated Use concat($first, $name->slice(1)) instead + * * @param string|array|self $name The name to set the first part to */ public function setFirst($name) { @@ -149,14 +157,43 @@ class Name extends NodeAbstract } /** - * Concatenate two names, yielding a new Name instance + * Gets a slice of a name (similar to array_slice). * - * @param string|array|self The first name - * @param string|array|self The second name - * @return Name Concatenated name + * This method returns a new instance of the same type as the original and with the same + * attributes. + * + * If the slice is empty, a Name with an empty parts array is returned. While this is + * meaningless in itself, it works correctly in conjunction with concat(). + * + * @param int $offset Offset to start the slice at + * + * @return static Sliced name */ - public static function concat($name1, $name2) { - return new Name(array_merge(self::prepareName($name1), self::prepareName($name2))); + public function slice($offset) { + // TODO negative offset and length + if ($offset < 0 || $offset > count($this->parts)) { + throw new \OutOfBoundsException(sprintf('Offset %d is out of bounds', $offset)); + } + + return new static(array_slice($this->parts, $offset), $this->attributes); + } + + /** + * Concatenate two names, yielding a new Name instance. + * + * The type of the generated instance depends on which class this method is called on, for + * example Name\FullyQualified::concat() will yield a Name\FullyQualified instance. + * + * @param string|array|self $name1 The first name + * @param string|array|self $name2 The second name + * @param array $attributes Attributes to assign to concatenated name + * + * @return static Concatenated name + */ + public static function concat($name1, $name2, array $attributes = []) { + return new static( + array_merge(self::prepareName($name1), self::prepareName($name2)), $attributes + ); } /** diff --git a/lib/PhpParser/NodeVisitor/NameResolver.php b/lib/PhpParser/NodeVisitor/NameResolver.php index 162ea31..b381d9f 100644 --- a/lib/PhpParser/NodeVisitor/NameResolver.php +++ b/lib/PhpParser/NodeVisitor/NameResolver.php @@ -6,6 +6,7 @@ use PhpParser\NodeVisitorAbstract; use PhpParser\Error; use PhpParser\Node; use PhpParser\Node\Name; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Expr; use PhpParser\Node\Stmt; @@ -174,13 +175,16 @@ class NameResolver extends NodeVisitorAbstract $aliasName = strtolower($name->getFirst()); if (!$name->isRelative() && isset($this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName])) { // resolve aliases (for non-relative names) - $name->setFirst($this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName]); - } elseif (null !== $this->namespace) { - // if no alias exists prepend current namespace - $name->prepend($this->namespace); + $alias = $this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName]; + return FullyQualified::concat($alias, $name->slice(1), $name->getAttributes()); } - return new Name\FullyQualified($name->parts, $name->getAttributes()); + if (null !== $this->namespace) { + // if no alias exists prepend current namespace + return FullyQualified::concat($this->namespace, $name, $name->getAttributes()); + } + + return new FullyQualified($name->parts, $name->getAttributes()); } protected function resolveOtherName(Name $name, $type) { @@ -192,34 +196,38 @@ class NameResolver extends NodeVisitorAbstract // resolve aliases for qualified names $aliasName = strtolower($name->getFirst()); if ($name->isQualified() && isset($this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName])) { - $name->setFirst($this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName]); - } elseif ($name->isUnqualified()) { + $alias = $this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName]; + return FullyQualified::concat($alias, $name->slice(1), $name->getAttributes()); + } + + if ($name->isUnqualified()) { if ($type === Stmt\Use_::TYPE_CONSTANT) { // constant aliases are case-sensitive, function aliases case-insensitive $aliasName = $name->getFirst(); } - if (isset($this->aliases[$type][$aliasName])) { - // resolve unqualified aliases - $name->set($this->aliases[$type][$aliasName]); - } else { + if (!isset($this->aliases[$type][$aliasName])) { // unqualified, unaliased names cannot be resolved at compile-time return $name; } - } elseif (null !== $this->namespace) { - // if no alias exists prepend current namespace - $name->prepend($this->namespace); + + // resolve unqualified aliases + return new FullyQualified($this->aliases[$type][$aliasName], $name->getAttributes()); } - return new Name\FullyQualified($name->parts, $name->getAttributes()); + if (null !== $this->namespace) { + // if no alias exists prepend current namespace + return FullyQualified::concat($this->namespace, $name, $name->getAttributes()); + } + + return new FullyQualified($name->parts, $name->getAttributes()); } protected function addNamespacedName(Node $node) { if (null !== $this->namespace) { - $node->namespacedName = clone $this->namespace; - $node->namespacedName->append($node->name); + $node->namespacedName = Name::concat($this->namespace, $node->name); } else { - $node->namespacedName = new Name($node->name, $node->getAttributes()); + $node->namespacedName = new Name($node->name); } } } diff --git a/test/PhpParser/Node/NameTest.php b/test/PhpParser/Node/NameTest.php index 1b20ae3..3378551 100644 --- a/test/PhpParser/Node/NameTest.php +++ b/test/PhpParser/Node/NameTest.php @@ -95,6 +95,38 @@ class NameTest extends \PHPUnit_Framework_TestCase $this->assertSame('foo\bar\bar\foo', $name->toString()); } + public function testSlice() { + $name = new Name('foo\bar'); + $this->assertEquals(new Name('foo\bar'), $name->slice(0)); + $this->assertEquals(new Name('bar'), $name->slice(1)); + $this->assertEquals(new Name([]), $name->slice(2)); + } + + /** + * @expectedException \OutOfBoundsException + * @expectedExceptionMessage Offset 4 is out of bounds + */ + public function testSliceException() { + (new Name('foo\bar\baz'))->slice(4); + } + + public function testConcat() { + $this->assertEquals(new Name('foo\bar\baz'), Name::concat('foo', 'bar\baz')); + $this->assertEquals( + new Name\FullyQualified('foo\bar'), + Name\FullyQualified::concat(['foo'], new Name('bar')) + ); + + $attributes = ['foo' => 'bar']; + $this->assertEquals( + new Name\Relative('foo\bar\baz', $attributes), + Name\Relative::concat(new Name\FullyQualified('foo\bar'), 'baz', $attributes) + ); + + $this->assertEquals(new Name('foo'), Name::concat([], 'foo')); + $this->assertEquals(new Name([]), Name::concat([], [])); + } + public function testIs() { $name = new Name('foo'); $this->assertTrue ($name->isUnqualified());