Skip to content

Commit 83b6a1a

Browse files
committed
Add support for ListTag type casting and PHPStan generics
1 parent 53db374 commit 83b6a1a

File tree

2 files changed

+67
-11
lines changed

2 files changed

+67
-11
lines changed

src/tag/CompoundTag.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,26 @@ public function getTag(string $name) : ?Tag{
8888
/**
8989
* Returns the ListTag with the specified name, or null if it does not exist. Triggers an exception if a tag exists
9090
* with that name and the tag is not a ListTag.
91+
*
92+
* @phpstan-template TValue of Tag
93+
* @phpstan-param class-string<TValue> $tagClass
94+
* @phpstan-return ListTag<TValue>|null
95+
*
96+
* @throws UnexpectedTagTypeException
9197
*/
92-
public function getListTag(string $name) : ?ListTag{
98+
public function getListTag(string $name, string $tagClass = Tag::class) : ?ListTag{
9399
$tag = $this->getTag($name);
94-
if($tag !== null && !($tag instanceof ListTag)){
95-
throw new UnexpectedTagTypeException("Expected a tag of type " . ListTag::class . ", got " . get_class($tag));
100+
if($tag !== null){
101+
if(!$tag instanceof ListTag){
102+
throw new UnexpectedTagTypeException("Expected a tag of type " . ListTag::class . ", got " . get_class($tag));
103+
}
104+
$casted = $tag->cast($tagClass);
105+
if($casted === null){
106+
throw new UnexpectedTagTypeException("Unable to cast list to ListTag<$tagClass>");
107+
}
108+
return $casted;
96109
}
97-
return $tag;
110+
return null;
98111
}
99112

100113
/**

src/tag/ListTag.php

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
use function str_repeat;
3535

3636
/**
37-
* @phpstan-implements \IteratorAggregate<int, Tag>
37+
* @phpstan-template TValue of Tag = Tag
38+
* @phpstan-implements \IteratorAggregate<int, TValue>
3839
*/
3940
final class ListTag extends Tag implements \Countable, \IteratorAggregate{
4041
use NoDynamicFieldsTrait;
@@ -43,12 +44,13 @@ final class ListTag extends Tag implements \Countable, \IteratorAggregate{
4344
private $tagType;
4445
/**
4546
* @var \SplDoublyLinkedList|Tag[]
46-
* @phpstan-var \SplDoublyLinkedList<Tag>
47+
* @phpstan-var \SplDoublyLinkedList<TValue>
4748
*/
4849
private $value;
4950

5051
/**
5152
* @param Tag[] $value
53+
* @phpstan-param TValue[] $value
5254
*/
5355
public function __construct(array $value = [], int $tagType = NBT::TAG_End){
5456
self::restrictArgCount(__METHOD__, func_num_args(), 2);
@@ -61,7 +63,7 @@ public function __construct(array $value = [], int $tagType = NBT::TAG_End){
6163

6264
/**
6365
* @return Tag[]
64-
* @phpstan-return list<Tag>
66+
* @phpstan-return list<TValue>
6567
*/
6668
public function getValue() : array{
6769
$value = [];
@@ -86,6 +88,34 @@ public function getAllValues() : array{
8688
return $result;
8789
}
8890

91+
/**
92+
* @phpstan-template TTarget of Tag
93+
* @phpstan-param class-string<TTarget> $tagClass
94+
* @phpstan-this-out self<TTarget> $this
95+
*/
96+
private function checkTagClass(string $tagClass) : bool{
97+
return $this->value->isEmpty() ?
98+
$this->tagType === NBT::TAG_End :
99+
$this->first() instanceof $tagClass;
100+
}
101+
102+
/**
103+
* Returns $this if the tag values are of type $tagClass, null otherwise.
104+
* The returned value will have the proper PHPStan generic types set if it matches.
105+
*
106+
* If the list is empty and is of TAG_End type, the cast will always succeed, since TAG_End lists
107+
* will have their type updated as soon as a value is inserted. However, casting any other type of
108+
* empty list will fail if the type is incorrect.
109+
*
110+
* @phpstan-template TTarget of Tag
111+
* @phpstan-param class-string<TTarget> $tagClass
112+
*
113+
* @phpstan-return self<TTarget>|null
114+
*/
115+
public function cast(string $tagClass) : ?self{
116+
return $this->checkTagClass($tagClass) ? $this : null;
117+
}
118+
89119
public function count() : int{
90120
return $this->value->count();
91121
}
@@ -96,6 +126,8 @@ public function getCount() : int{
96126

97127
/**
98128
* Appends the specified tag to the end of the list.
129+
*
130+
* @phpstan-param TValue $tag
99131
*/
100132
public function push(Tag $tag) : void{
101133
$this->checkTagType($tag);
@@ -104,13 +136,16 @@ public function push(Tag $tag) : void{
104136

105137
/**
106138
* Removes the last tag from the list and returns it.
139+
* @phpstan-return TValue
107140
*/
108141
public function pop() : Tag{
109142
return $this->value->pop();
110143
}
111144

112145
/**
113146
* Adds the specified tag to the start of the list.
147+
*
148+
* @phpstan-param TValue $tag
114149
*/
115150
public function unshift(Tag $tag) : void{
116151
$this->checkTagType($tag);
@@ -119,6 +154,7 @@ public function unshift(Tag $tag) : void{
119154

120155
/**
121156
* Removes the first tag from the list and returns it.
157+
* @phpstan-return TValue
122158
*/
123159
public function shift() : Tag{
124160
return $this->value->shift();
@@ -128,6 +164,8 @@ public function shift() : Tag{
128164
* Inserts a tag into the list between existing tags, at the specified offset. Later values in the list are moved up
129165
* by 1 position.
130166
*
167+
* @phpstan-param TValue $tag
168+
*
131169
* @return void
132170
* @throws \OutOfRangeException if the offset is not within the bounds of the list
133171
*/
@@ -146,6 +184,8 @@ public function remove(int $offset) : void{
146184
/**
147185
* Returns the tag at the specified offset.
148186
*
187+
* @phpstan-return TValue
188+
*
149189
* @throws \OutOfRangeException if the offset is not within the bounds of the list
150190
*/
151191
public function get(int $offset) : Tag{
@@ -157,13 +197,15 @@ public function get(int $offset) : Tag{
157197

158198
/**
159199
* Returns the element in the first position of the list, without removing it.
200+
* @phpstan-return TValue
160201
*/
161202
public function first() : Tag{
162203
return $this->value->bottom();
163204
}
164205

165206
/**
166207
* Returns the element in the last position in the list (the end), without removing it.
208+
* @phpstan-return TValue
167209
*/
168210
public function last() : Tag{
169211
return $this->value->top();
@@ -172,6 +214,8 @@ public function last() : Tag{
172214
/**
173215
* Overwrites the tag at the specified offset.
174216
*
217+
* @phpstan-param TValue $tag
218+
*
175219
* @throws \OutOfRangeException if the offset is not within the bounds of the list
176220
*/
177221
public function set(int $offset, Tag $tag) : void{
@@ -209,6 +253,7 @@ public function getTagType() : int{
209253
}
210254

211255
/**
256+
* @deprecated
212257
* Sets the type of tag that can be added to this list. If TAG_End is used, the type will be auto-detected from the
213258
* first tag added to the list.
214259
*
@@ -263,23 +308,21 @@ public static function read(NbtStreamReader $reader, ReaderTracker $tracker) : s
263308
public function write(NbtStreamWriter $writer) : void{
264309
$writer->writeByte($this->tagType);
265310
$writer->writeInt($this->value->count());
266-
/** @var Tag $tag */
267311
foreach($this->value as $tag){
268312
$tag->write($writer);
269313
}
270314
}
271315

272316
protected function stringifyValue(int $indentation) : string{
273317
$str = "{\n";
274-
/** @var Tag $tag */
275318
foreach($this->value as $tag){
276319
$str .= str_repeat(" ", $indentation + 1) . $tag->toString($indentation + 1) . "\n";
277320
}
278321
return $str . str_repeat(" ", $indentation) . "}";
279322
}
280323

281324
public function __clone(){
282-
/** @phpstan-var \SplDoublyLinkedList<Tag> $new */
325+
/** @phpstan-var \SplDoublyLinkedList<TValue> $new */
283326
$new = new \SplDoublyLinkedList();
284327

285328
foreach($this->value as $tag){
@@ -295,7 +338,7 @@ protected function makeCopy(){
295338

296339
/**
297340
* @return \Generator|Tag[]
298-
* @phpstan-return \Generator<int, Tag, void, void>
341+
* @phpstan-return \Generator<int, TValue, void, void>
299342
*/
300343
public function getIterator() : \Generator{
301344
//we technically don't need iterator_to_array() here, but I don't feel comfortable relying on "yield from" to

0 commit comments

Comments
 (0)