Skip to content

Commit 0675d2c

Browse files
laurentjjaapio
authored andcommitted
Support of array expression of PSR-5
1 parent e7ead46 commit 0675d2c

File tree

4 files changed

+224
-7
lines changed

4 files changed

+224
-7
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ The TypeResolver can resolve:
3939
- a php primitive or pseudo-primitive such as a string or void (`@var string` or `@return void`).
4040
- a composite such as an array of string (`@var string[]`).
4141
- a compound such as a string or integer (`@var string|integer`).
42+
- an array expression (`@var (string|TypeResolver)[]`)
4243
- an object or interface such as the TypeResolver class (`@var TypeResolver`
4344
or `@var \phpDocumentor\Reflection\TypeResolver`)
4445

src/TypeResolver.php

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ final class TypeResolver
3333
/** @var integer the iterator parser is inside a nullable expression context */
3434
const PARSER_IN_NULLABLE = 1;
3535

36+
/** @var integer the iterator parser is inside an array expression context */
37+
const PARSER_IN_ARRAY_EXPRESSION = 2;
38+
3639
/** @var string[] List of recognized keywords and unto which Value Object they map */
3740
private $keywords = array(
3841
'string' => Types\String_::class,
@@ -109,9 +112,10 @@ public function resolve($type, Context $context = null)
109112
$context = new Context('');
110113
}
111114

112-
// split the type string into tokens `|`, `?` and type names
113-
$tokens = preg_split('/(\||\?)/', $type, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
115+
// split the type string into tokens `|`, `?`, `(`, `)[]` and type names
116+
$tokens = preg_split('/(\||\?|\(|\)(?:\[\])+)/', $type, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
114117
$tokenIterator = new \ArrayIterator($tokens);
118+
115119
return $this->parseTypes($tokenIterator, $context, self::PARSER_IN_COMPOUND);
116120
}
117121

@@ -131,19 +135,53 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
131135
$token = '';
132136
while ($tokens->valid()) {
133137
$token = $tokens->current();
134-
if ($parserContext === self::PARSER_IN_COMPOUND && $token == '|') {
138+
139+
if ($token == '|') {
135140
if (count($types) == 0) {
136141
throw new \RuntimeException(
137142
'A type is missing before a type separator'
138143
);
139144
}
145+
if ($parserContext !== self::PARSER_IN_COMPOUND
146+
&& $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION) {
147+
throw new \RuntimeException(
148+
'Unexpected type separator'
149+
);
150+
}
140151
$tokens->next();
141-
} else if ($parserContext === self::PARSER_IN_COMPOUND
142-
&& $token == '?'
143-
) {
152+
153+
} else if ($token == '?') {
154+
if ($parserContext !== self::PARSER_IN_COMPOUND
155+
&& $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION) {
156+
throw new \RuntimeException(
157+
'Unexpected nullable character'
158+
);
159+
}
160+
144161
$tokens->next();
145162
$type = $this->parseTypes($tokens, $context, self::PARSER_IN_NULLABLE);
146163
$types[] = new Nullable($type);
164+
165+
} else if ($token === '(') {
166+
$tokens->next();
167+
$type = $this->parseTypes($tokens, $context, self::PARSER_IN_ARRAY_EXPRESSION);
168+
169+
$resolvedType = new Array_($type);
170+
171+
// we generates arrays corresponding to the number of '[]'
172+
// after the ')'
173+
$numberOfArrays = (strlen($tokens->current()) -1) / 2;
174+
for ($i = 0; $i < $numberOfArrays - 1; $i++) {
175+
$resolvedType = new Array_($resolvedType);
176+
}
177+
$types[] = $resolvedType;
178+
$tokens->next();
179+
180+
} else if ($parserContext === self::PARSER_IN_ARRAY_EXPRESSION
181+
&& $token[0] === ')'
182+
) {
183+
break;
184+
147185
} else {
148186
$type = $this->resolveSingleType($token, $context);
149187
$tokens->next();
@@ -165,6 +203,11 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
165203
'A type is missing after a nullable character'
166204
);
167205
}
206+
if ($parserContext == self::PARSER_IN_ARRAY_EXPRESSION) {
207+
throw new \RuntimeException(
208+
'A type is missing in an array expression'
209+
);
210+
}
168211
throw new \RuntimeException(
169212
'No types in a compound list'
170213
);

src/Types/Array_.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ public function __toString()
8181
return 'array';
8282
}
8383

84+
if ($this->valueType instanceof Compound) {
85+
return '(' . $this->valueType . ')[]';
86+
}
87+
8488
return $this->valueType . '[]';
8589
}
8690
}

tests/unit/TypeResolverTest.php

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use phpDocumentor\Reflection\Types\Iterable_;
2020
use phpDocumentor\Reflection\Types\Nullable;
2121
use phpDocumentor\Reflection\Types\Object_;
22+
use phpDocumentor\Reflection\Types\Boolean;
2223
use Mockery\MockInterface;
2324
use phpDocumentor\Reflection\Types\String_;
2425

@@ -251,7 +252,7 @@ public function testResolvingCompoundTypedArrayTypes()
251252
$this->assertInstanceOf(Compound::class, $resolvedType);
252253
$this->assertSame('\stdClass[]|\phpDocumentor\Reflection\DocBlock[]', (string)$resolvedType);
253254

254-
/** @var Array_ $secondType */
255+
/** @var Array_ $firstType */
255256
$firstType = $resolvedType->get(0);
256257

257258
/** @var Array_ $secondType */
@@ -263,6 +264,174 @@ public function testResolvingCompoundTypedArrayTypes()
263264
$this->assertInstanceOf(Object_::class, $secondType->getValueType());
264265
}
265266

267+
268+
/**
269+
* @covers ::__construct
270+
* @covers ::resolve
271+
* @covers ::<private>
272+
*
273+
* @uses \phpDocumentor\Reflection\Types\Context
274+
* @uses \phpDocumentor\Reflection\Types\Compound
275+
* @uses \phpDocumentor\Reflection\Types\Array_
276+
* @uses \phpDocumentor\Reflection\Types\Object_
277+
* @uses \phpDocumentor\Reflection\Fqsen
278+
* @uses \phpDocumentor\Reflection\FqsenResolver
279+
*/
280+
public function testResolvingArrayExpressionObjectsTypes()
281+
{
282+
$fixture = new TypeResolver();
283+
284+
/** @var Array_ $resolvedType */
285+
$resolvedType = $fixture->resolve('(\stdClass|Reflection\DocBlock)[]', new Context('phpDocumentor'));
286+
287+
$this->assertInstanceOf(Array_::class, $resolvedType);
288+
$this->assertSame('(\stdClass|\phpDocumentor\Reflection\DocBlock)[]', (string)$resolvedType);
289+
290+
/** @var Compound $valueType */
291+
$valueType = $resolvedType->getValueType();
292+
293+
$this->assertInstanceOf(Compound::class, $valueType);
294+
295+
/** @var Object_ $firstType */
296+
$firstType = $valueType->get(0);
297+
298+
/** @var Object_ $secondType */
299+
$secondType = $valueType->get(1);
300+
301+
$this->assertInstanceOf(Object_::class, $firstType);
302+
$this->assertInstanceOf(Object_::class, $secondType);
303+
}
304+
305+
/**
306+
* @covers ::__construct
307+
* @covers ::resolve
308+
* @covers ::<private>
309+
*
310+
* @uses \phpDocumentor\Reflection\Types\Context
311+
* @uses \phpDocumentor\Reflection\Types\Compound
312+
* @uses \phpDocumentor\Reflection\Types\Array_
313+
* @uses \phpDocumentor\Reflection\Types\Object_
314+
* @uses \phpDocumentor\Reflection\Fqsen
315+
* @uses \phpDocumentor\Reflection\FqsenResolver
316+
*/
317+
public function testResolvingArrayExpressionSimpleTypes()
318+
{
319+
$fixture = new TypeResolver();
320+
321+
/** @var Array_ $resolvedType */
322+
$resolvedType = $fixture->resolve('(string|\stdClass|boolean)[]', new Context(''));
323+
324+
$this->assertInstanceOf(Array_::class, $resolvedType);
325+
$this->assertSame('(string|\stdClass|bool)[]', (string)$resolvedType);
326+
327+
/** @var Compound $valueType */
328+
$valueType = $resolvedType->getValueType();
329+
330+
$this->assertInstanceOf(Compound::class, $valueType);
331+
332+
/** @var String_ $firstType */
333+
$firstType = $valueType->get(0);
334+
335+
/** @var Object_ $secondType */
336+
$secondType = $valueType->get(1);
337+
338+
/** @var Boolean $thirdType */
339+
$thirdType = $valueType->get(2);
340+
341+
$this->assertInstanceOf(String_::class, $firstType);
342+
$this->assertInstanceOf(Object_::class, $secondType);
343+
$this->assertInstanceOf(Boolean::class, $thirdType);
344+
}
345+
346+
/**
347+
* @covers ::__construct
348+
* @covers ::resolve
349+
* @covers ::<private>
350+
*
351+
* @uses \phpDocumentor\Reflection\Types\Context
352+
* @uses \phpDocumentor\Reflection\Types\Compound
353+
* @uses \phpDocumentor\Reflection\Types\Array_
354+
* @uses \phpDocumentor\Reflection\Types\Object_
355+
* @uses \phpDocumentor\Reflection\Fqsen
356+
* @uses \phpDocumentor\Reflection\FqsenResolver
357+
*/
358+
public function testResolvingArrayOfArrayExpressionTypes()
359+
{
360+
$fixture = new TypeResolver();
361+
362+
/** @var Array_ $resolvedType */
363+
$resolvedType = $fixture->resolve('(string|\stdClass)[][]', new Context(''));
364+
365+
$this->assertInstanceOf(Array_::class, $resolvedType);
366+
$this->assertSame('(string|\stdClass)[][]', (string)$resolvedType);
367+
368+
/** @var Array_ $parentArrayType */
369+
$parentArrayType = $resolvedType->getValueType();
370+
$this->assertInstanceOf(Array_::class, $parentArrayType);
371+
372+
/** @var Compound $valueType */
373+
$valueType = $parentArrayType->getValueType();
374+
$this->assertInstanceOf(Compound::class, $valueType);
375+
376+
/** @var String_ $firstType */
377+
$firstType = $valueType->get(0);
378+
379+
/** @var Object_ $secondType */
380+
$secondType = $valueType->get(1);
381+
382+
$this->assertInstanceOf(String_::class, $firstType);
383+
$this->assertInstanceOf(Object_::class, $secondType);
384+
}
385+
386+
387+
/**
388+
* @covers ::__construct
389+
* @covers ::resolve
390+
* @covers ::<private>
391+
*
392+
* @uses \phpDocumentor\Reflection\Types\Context
393+
* @uses \phpDocumentor\Reflection\Types\Compound
394+
* @uses \phpDocumentor\Reflection\Types\Array_
395+
* @uses \phpDocumentor\Reflection\Types\Object_
396+
* @uses \phpDocumentor\Reflection\Fqsen
397+
* @uses \phpDocumentor\Reflection\FqsenResolver
398+
*/
399+
public function testResolvingArrayExpressionOrCompoundTypes()
400+
{
401+
$fixture = new TypeResolver();
402+
403+
/** @var Compound $resolvedType */
404+
$resolvedType = $fixture->resolve('\stdClass|(string|\stdClass)[]|bool', new Context(''));
405+
406+
$this->assertInstanceOf(Compound::class, $resolvedType);
407+
$this->assertSame('\stdClass|(string|\stdClass)[]|bool', (string)$resolvedType);
408+
409+
/** @var Object_ $firstType */
410+
$firstType = $resolvedType->get(0);
411+
$this->assertInstanceOf(Object_::class, $firstType);
412+
413+
/** @var Array_ $secondType */
414+
$secondType = $resolvedType->get(1);
415+
$this->assertInstanceOf(Array_::class, $secondType);
416+
417+
/** @var Array_ $thirdType */
418+
$thirdType = $resolvedType->get(2);
419+
$this->assertInstanceOf(Boolean::class, $thirdType);
420+
421+
/** @var Compound $valueType */
422+
$valueType = $secondType->getValueType();
423+
$this->assertInstanceOf(Compound::class, $valueType);
424+
425+
/** @var String_ $firstArrayType */
426+
$firstArrayType = $valueType->get(0);
427+
428+
/** @var Object_ $secondArrayType */
429+
$secondArrayType = $valueType->get(1);
430+
431+
$this->assertInstanceOf(String_::class, $firstArrayType);
432+
$this->assertInstanceOf(Object_::class, $secondArrayType);
433+
}
434+
266435
/**
267436
* This test asserts that the parameter order is correct.
268437
*

0 commit comments

Comments
 (0)