diff --git a/src/PseudoTypes/IntegerRange.php b/src/PseudoTypes/IntegerRange.php new file mode 100644 index 0000000..c5a3bc5 --- /dev/null +++ b/src/PseudoTypes/IntegerRange.php @@ -0,0 +1,61 @@ +minValue = $minValue; + $this->maxValue = $maxValue; + } + + public function underlyingType(): Type + { + return new Integer(); + } + + public function getMinValue(): string + { + return $this->minValue; + } + + public function getMaxValue(): string + { + return $this->maxValue; + } + + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + */ + public function __toString(): string + { + return 'int<' . $this->minValue . ', ' . $this->maxValue . '>'; + } +} diff --git a/src/PseudoTypes/NegativeInteger.php b/src/PseudoTypes/NegativeInteger.php new file mode 100644 index 0000000..c51d3fe --- /dev/null +++ b/src/PseudoTypes/NegativeInteger.php @@ -0,0 +1,39 @@ + PseudoTypes\NonEmptyLowercaseString::class, 'non-empty-string' => PseudoTypes\NonEmptyString::class, 'numeric-string' => PseudoTypes\NumericString::class, + 'numeric' => PseudoTypes\Numeric_::class, 'trait-string' => PseudoTypes\TraitString::class, 'int' => Types\Integer::class, 'integer' => Types\Integer::class, 'positive-int' => PseudoTypes\PositiveInteger::class, + 'negative-int' => PseudoTypes\NegativeInteger::class, 'bool' => Types\Boolean::class, 'boolean' => Types\Boolean::class, 'real' => Types\Float_::class, @@ -257,6 +261,8 @@ private function parseTypes(ArrayIterator $tokens, Context $context, int $parser if ($classType !== null) { if ((string) $classType === 'class-string') { $types[] = $this->resolveClassString($tokens, $context); + } elseif ((string) $classType === 'int') { + $types[] = $this->resolveIntRange($tokens); } elseif ((string) $classType === 'interface-string') { $types[] = $this->resolveInterfaceString($tokens, $context); } else { @@ -479,6 +485,75 @@ private function resolveClassString(ArrayIterator $tokens, Context $context): Ty return new ClassString($classType->getFqsen()); } + /** + * Resolves integer ranges + * + * @param ArrayIterator $tokens + */ + private function resolveIntRange(ArrayIterator $tokens): Type + { + $tokens->next(); + + $token = ''; + $minValue = null; + $maxValue = null; + $commaFound = false; + $tokenCounter = 0; + while ($tokens->valid()) { + $tokenCounter++; + $token = $tokens->current(); + if ($token === null) { + throw new RuntimeException( + 'Unexpected nullable character' + ); + } + + $token = trim($token); + + if ($token === '>') { + break; + } + + if ($token === ',') { + $commaFound = true; + } + + if ($commaFound === false && $minValue === null) { + if (is_numeric($token) || $token === 'max' || $token === 'min') { + $minValue = $token; + } + } + + if ($commaFound === true && $maxValue === null) { + if (is_numeric($token) || $token === 'max' || $token === 'min') { + $maxValue = $token; + } + } + + $tokens->next(); + } + + if ($token !== '>') { + if (empty($token)) { + throw new RuntimeException( + 'interface-string: ">" is missing' + ); + } + + throw new RuntimeException( + 'Unexpected character "' . $token . '", ">" is missing' + ); + } + + if (!$minValue || !$maxValue || $tokenCounter > 4) { + throw new RuntimeException( + 'int has not the correct format' + ); + } + + return new IntegerRange($minValue, $maxValue); + } + /** * Resolves class string * diff --git a/tests/unit/CollectionResolverTest.php b/tests/unit/CollectionResolverTest.php index fcfd782..379aafe 100644 --- a/tests/unit/CollectionResolverTest.php +++ b/tests/unit/CollectionResolverTest.php @@ -231,10 +231,16 @@ public function testBadArrayCollectionKey(): void public function testGoodArrayCollectionKey(): void { $fixture = new TypeResolver(); - $fixture->resolve('array', new Context('')); + $resolvedType = $fixture->resolve('array', new Context('')); + + $this->assertInstanceOf(Array_::class, $resolvedType); + $this->assertSame('array', (string) $resolvedType); $fixture = new TypeResolver(); - $fixture->resolve('array', new Context('')); + $resolvedType = $fixture->resolve('array', new Context('')); + + $this->assertInstanceOf(Array_::class, $resolvedType); + $this->assertSame('array', (string) $resolvedType); } /** diff --git a/tests/unit/IntegerRangeResolverTest.php b/tests/unit/IntegerRangeResolverTest.php new file mode 100644 index 0000000..c6dc757 --- /dev/null +++ b/tests/unit/IntegerRangeResolverTest.php @@ -0,0 +1,165 @@ + + * @coversDefaultClass \phpDocumentor\Reflection\TypeResolver + */ +class IntegerRangeResolverTest extends TestCase +{ + /** + * @uses \phpDocumentor\Reflection\Types\Context + * @uses \phpDocumentor\Reflection\Types\Compound + * @uses \phpDocumentor\Reflection\Types\Collection + * @uses \phpDocumentor\Reflection\Types\String_ + * + * @covers ::__construct + * @covers ::resolve + */ + public function testResolvingIntRange(): void + { + $fixture = new TypeResolver(); + + $resolvedType = $fixture->resolve('int<-5, 5>', new Context('')); + + $this->assertInstanceOf(IntegerRange::class, $resolvedType); + $this->assertSame('int<-5, 5>', (string) $resolvedType); + + $minValue = $resolvedType->getMinValue(); + $maxValue = $resolvedType->getMaxValue(); + + $this->assertSame('-5', $minValue); + $this->assertSame('5', $maxValue); + } + + /** + * @uses \phpDocumentor\Reflection\Types\Context + * @uses \phpDocumentor\Reflection\Types\Compound + * @uses \phpDocumentor\Reflection\Types\Collection + * @uses \phpDocumentor\Reflection\Types\String_ + * + * @covers ::__construct + * @covers ::resolve + */ + public function testResolvingIntRangeWithKeywords(): void + { + $fixture = new TypeResolver(); + + $resolvedType = $fixture->resolve('int', new Context('')); + + $this->assertInstanceOf(IntegerRange::class, $resolvedType); + $this->assertSame('int', (string) $resolvedType); + + $minValue = $resolvedType->getMinValue(); + $maxValue = $resolvedType->getMaxValue(); + + $this->assertSame('min', $minValue); + $this->assertSame('max', $maxValue); + } + + /** + * @uses \phpDocumentor\Reflection\Types\Context + * @uses \phpDocumentor\Reflection\Types\Compound + * @uses \phpDocumentor\Reflection\Types\Collection + * @uses \phpDocumentor\Reflection\Types\String_ + * + * @covers ::__construct + * @covers ::resolve + */ + public function testResolvingIntRangeErrorMisingMaxValue(): void + { + $this->expectException('RuntimeException'); + $this->expectExceptionMessage('int has not the correct format'); + + $fixture = new TypeResolver(); + $resolvedType = $fixture->resolve('int', new Context('')); + } + + /** + * @uses \phpDocumentor\Reflection\Types\Context + * @uses \phpDocumentor\Reflection\Types\Compound + * @uses \phpDocumentor\Reflection\Types\Collection + * @uses \phpDocumentor\Reflection\Types\String_ + * + * @covers ::__construct + * @covers ::resolve + */ + public function testResolvingIntRangeErrorMisingMinValue(): void + { + $this->expectException('RuntimeException'); + $this->expectExceptionMessage('int has not the correct format'); + + $fixture = new TypeResolver(); + $resolvedType = $fixture->resolve('int<,max>', new Context('')); + } + + /** + * @uses \phpDocumentor\Reflection\Types\Context + * @uses \phpDocumentor\Reflection\Types\Compound + * @uses \phpDocumentor\Reflection\Types\Collection + * @uses \phpDocumentor\Reflection\Types\String_ + * + * @covers ::__construct + * @covers ::resolve + */ + public function testResolvingIntRangeErrorMisingComma(): void + { + $this->expectException('RuntimeException'); + $this->expectExceptionMessage('int has not the correct format'); + + $fixture = new TypeResolver(); + $resolvedType = $fixture->resolve('int', new Context('')); + } + + /** + * @uses \phpDocumentor\Reflection\Types\Context + * @uses \phpDocumentor\Reflection\Types\Compound + * @uses \phpDocumentor\Reflection\Types\Collection + * @uses \phpDocumentor\Reflection\Types\String_ + * + * @covers ::__construct + * @covers ::resolve + */ + public function testResolvingIntRangeErrorMissingEnd(): void + { + $this->expectException('RuntimeException'); + $this->expectExceptionMessage('Unexpected character "max", ">" is missing'); + + $fixture = new TypeResolver(); + $resolvedType = $fixture->resolve('intexpectException('RuntimeException'); + $this->expectExceptionMessage('int has not the correct format'); + + $fixture = new TypeResolver(); + $resolvedType = $fixture->resolve('int', new Context('')); + } +} diff --git a/tests/unit/NumericResolverTest.php b/tests/unit/NumericResolverTest.php new file mode 100644 index 0000000..d21532c --- /dev/null +++ b/tests/unit/NumericResolverTest.php @@ -0,0 +1,48 @@ + + * @coversDefaultClass \phpDocumentor\Reflection\TypeResolver + */ +class NumericResolverTest extends TestCase +{ + /** + * @uses \phpDocumentor\Reflection\Types\Context + * @uses \phpDocumentor\Reflection\Types\Compound + * @uses \phpDocumentor\Reflection\Types\Collection + * @uses \phpDocumentor\Reflection\Types\String_ + * + * @covers ::__construct + * @covers ::resolve + */ + public function testResolvingIntRange(): void + { + $fixture = new TypeResolver(); + + $resolvedType = $fixture->resolve('numeric', new Context('')); + + $this->assertInstanceOf(Numeric_::class, $resolvedType); + $this->assertSame('numeric', (string) $resolvedType); + $this->assertSame(false, $resolvedType->underlyingType()->contains(new String_())); + $this->assertSame(true, $resolvedType->underlyingType()->contains(new NumericString())); + } +} diff --git a/tests/unit/PseudoTypes/IntRangeTest.php b/tests/unit/PseudoTypes/IntRangeTest.php new file mode 100644 index 0000000..ca9bb06 --- /dev/null +++ b/tests/unit/PseudoTypes/IntRangeTest.php @@ -0,0 +1,43 @@ +assertSame($expectedString, (string) $array); + } + + /** + * @return mixed[] + */ + public function provideArrays(): array + { + return [ + 'simple int range' => [new IntegerRange('-5', '5'), 'int<-5, 5>'], + 'mixed int range' => [new IntegerRange('min', '5'), 'int'], + 'keyword int range' => [new IntegerRange('min', 'max'), 'int'], + ]; + } +} diff --git a/tests/unit/TypeResolverTest.php b/tests/unit/TypeResolverTest.php index 7a8f062..a18cb30 100644 --- a/tests/unit/TypeResolverTest.php +++ b/tests/unit/TypeResolverTest.php @@ -729,10 +729,12 @@ public function provideKeywords(): array ['non-empty-lowercase-string', PseudoTypes\NonEmptyLowercaseString::class], ['non-empty-string', PseudoTypes\NonEmptyString::class], ['numeric-string', PseudoTypes\NumericString::class], + ['numeric', PseudoTypes\Numeric_::class], ['trait-string', PseudoTypes\TraitString::class], ['int', Types\Integer::class], ['integer', Types\Integer::class], ['positive-int', PseudoTypes\PositiveInteger::class], + ['negative-int', PseudoTypes\NegativeInteger::class], ['float', Types\Float_::class], ['double', Types\Float_::class], ['bool', Types\Boolean::class],