Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

### Changed

- Make `NodeList` an actual list

## v15.1.0

### Added
Expand Down
68 changes: 28 additions & 40 deletions src/Language/AST/NodeList.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,20 @@
class NodeList implements \IteratorAggregate, \Countable
{
/**
* @var array<Node>
* @var list<Node|array<string, mixed>>
*
* @phpstan-var list<T>
* @phpstan-var list<T|array<string, mixed>> $nodes
*/
private array $nodes = [];
protected array $nodes;

/**
* @param iterable<Node|array> $nodes
* @param list<Node|array<string, mixed>> $nodes
*
* @phpstan-param iterable<T|array<string, mixed>> $nodes
* @phpstan-param list<T|array<string, mixed>> $nodes
*/
public function __construct(iterable $nodes)
public function __construct(array $nodes)
{
foreach ($nodes as $node) {
$this->nodes[] = $this->process($node);
}
$this->nodes = $nodes;
}

public function has(int $offset): bool
Expand All @@ -40,7 +38,12 @@ public function has(int $offset): bool
*/
public function get(int $offset): Node
{
return $this->nodes[$offset];
$node = $this->nodes[$offset];

// @phpstan-ignore-next-line not really possible to express the correctness of this in PHP
return \is_array($node)
? ($this->nodes[$offset] = AST::fromArray($node))
: $node;
}

/**
Expand All @@ -61,37 +64,17 @@ public function add(Node $value): void
$this->nodes[] = $value;
}

/**
* @param Node|array<string, mixed> $value
*
* @phpstan-param T|array<string, mixed> $value
*
* @phpstan-return T
*/
private function process($value): Node
public function unset(int $offset): void
{
if (\is_array($value)) {
/** @phpstan-var T $value */
$value = AST::fromArray($value);
}

return $value;
}

public function remove(Node $node): void
{
$foundKey = \array_search($node, $this->nodes, true);
if ($foundKey === false) {
throw new \InvalidArgumentException('Node not found in NodeList');
}

unset($this->nodes[$foundKey]);
unset($this->nodes[$offset]);
$this->nodes = \array_values($this->nodes);
}

public function getIterator(): \Traversable
{
yield from $this->nodes;
foreach ($this->nodes as $key => $_) {
yield $key => $this->get($key);
}
}

public function count(): int
Expand All @@ -102,15 +85,17 @@ public function count(): int
/**
* Remove a portion of the NodeList and replace it with something else.
*
* @param T|array<T>|null $replacement
* @param Node|array<Node>|null $replacement
* @phpstan-param T|array<T>|null $replacement
*
* @phpstan-return NodeList<T> the NodeList with the extracted elements
*/
public function splice(int $offset, int $length, $replacement = null): NodeList
{
return new NodeList(
\array_splice($this->nodes, $offset, $length, $replacement)
);
$spliced = \array_splice($this->nodes, $offset, $length, $replacement);

// @phpstan-ignore-next-line generic type mismatch
return new NodeList($spliced);
}

/**
Expand All @@ -124,7 +109,10 @@ public function merge(iterable $list): NodeList
$list = \iterator_to_array($list);
}

return new NodeList(\array_merge($this->nodes, $list));
$merged = \array_merge($this->nodes, $list);

// @phpstan-ignore-next-line generic type mismatch
return new NodeList($merged);
}

/**
Expand Down
42 changes: 19 additions & 23 deletions src/Language/Visitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,13 @@ public static function visit(object $root, array $visitor, ?array $keyMap = null

/**
* @var list<array{
* inArray: bool,
* inList: bool,
* index: int,
* keys: Node|NodeList|mixed,
* edits: array<int, array{mixed, mixed}>,
* }> $stack */
$stack = [];
$inArray = $root instanceof NodeList;
$inList = $root instanceof NodeList;
$keys = [$root];
$index = -1;
$edits = [];
Expand Down Expand Up @@ -210,12 +210,12 @@ public static function visit(object $root, array $visitor, ?array $keyMap = null

$editOffset = 0;
foreach ($edits as [$editKey, $editValue]) {
if ($inArray) {
if ($inList) {
$editKey -= $editOffset;
}

if ($inArray && $editValue === null) {
assert($node instanceof NodeList, 'Follows from $inArray');
if ($inList && $editValue === null) {
assert($node instanceof NodeList, 'Follows from $inList');
$node->splice($editKey, 1);
++$editOffset;
} elseif ($node instanceof NodeList) {
Expand All @@ -236,23 +236,19 @@ public static function visit(object $root, array $visitor, ?array $keyMap = null
'index' => $index,
'keys' => $keys,
'edits' => $edits,
'inArray' => $inArray,
'inList' => $inList,
] = \array_pop($stack);
} else {
$key = $parent !== null
? (
$inArray
? $index
: $keys[$index]
)
: null;
$node = $parent !== null
? (
$parent instanceof NodeList
? $parent->get($key)
: $parent->{$key}
)
: $newRoot;
if ($parent === null) {
$node = $newRoot;
} else {
$key = $inList
? $index
: $keys[$index];
$node = $parent instanceof NodeList
? $parent->get($key)
: $parent->{$key};
}
if ($node === null) {
continue;
}
Expand Down Expand Up @@ -310,14 +306,14 @@ public static function visit(object $root, array $visitor, ?array $keyMap = null
\array_pop($path);
} else {
$stack[] = [
'inArray' => $inArray,
'inList' => $inList,
'index' => $index,
'keys' => $keys,
'edits' => $edits,
];
$inArray = $node instanceof NodeList;
$inList = $node instanceof NodeList;

$keys = ($inArray ? $node : $visitorKeys[$node->kind]) ?? [];
$keys = ($inList ? $node : $visitorKeys[$node->kind]) ?? [];
$index = -1;
$edits = [];
if ($parent !== null) {
Expand Down
11 changes: 6 additions & 5 deletions tests/Language/NodeListTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ private static function assertNotSameButEquals(object $node, object $clone): voi

public function testThrowsOnInvalidArrays(): void
{
$this->expectException(InvariantViolation::class);

// @phpstan-ignore-next-line Wrong on purpose
new NodeList([['not a valid array representation of an AST node']]);
$nodeList = new NodeList([['not a valid array representation of an AST node']]);

$this->expectException(InvariantViolation::class);
iterator_to_array($nodeList);
}

public function testAddNodes(): void
Expand All @@ -60,13 +61,13 @@ public function testAddNodes(): void
self::assertCount(2, $nodeList);
}

public function testRemoveDoesNotBreakList(): void
public function testUnsetDoesNotBreakList(): void
{
$foo = new NameNode(['value' => 'foo']);
$bar = new NameNode(['value' => 'bar']);

$nodeList = new NodeList([$foo, $bar]);
$nodeList->remove($foo);
$nodeList->unset(0);

self::assertTrue($nodeList->has(0));
self::assertSame($bar, $nodeList->get(0));
Expand Down