diff --git a/src/PseudoTypes/Conditional.php b/src/PseudoTypes/Conditional.php new file mode 100644 index 0000000..dbccd89 --- /dev/null +++ b/src/PseudoTypes/Conditional.php @@ -0,0 +1,90 @@ +negated = $negated; + $this->subjectType = $subjectType; + $this->targetType = $targetType; + $this->if = $if; + $this->else = $else; + } + + public function isNegated(): bool + { + return $this->negated; + } + + public function getSubjectType(): Type + { + return $this->subjectType; + } + + public function getTargetType(): Type + { + return $this->targetType; + } + + public function getIf(): Type + { + return $this->if; + } + + public function getElse(): Type + { + return $this->else; + } + + public function underlyingType(): Type + { + return new Mixed_(); + } + + public function __toString(): string + { + return sprintf( + '(%s %s %s ? %s : %s)', + (string) $this->subjectType, + $this->negated ? 'is not' : 'is', + (string) $this->targetType, + (string) $this->if, + (string) $this->else + ); + } +} diff --git a/src/PseudoTypes/ConditionalForParameter.php b/src/PseudoTypes/ConditionalForParameter.php new file mode 100644 index 0000000..a9fccc5 --- /dev/null +++ b/src/PseudoTypes/ConditionalForParameter.php @@ -0,0 +1,90 @@ +negated = $negated; + $this->parameterName = $parameterName; + $this->targetType = $targetType; + $this->if = $if; + $this->else = $else; + } + + public function isNegated(): bool + { + return $this->negated; + } + + public function getParameterName(): string + { + return $this->parameterName; + } + + public function getTargetType(): Type + { + return $this->targetType; + } + + public function getIf(): Type + { + return $this->if; + } + + public function getElse(): Type + { + return $this->else; + } + + public function underlyingType(): Type + { + return new Mixed_(); + } + + public function __toString(): string + { + return sprintf( + '(%s %s %s ? %s : %s)', + '$' . $this->parameterName, + $this->negated ? 'is not' : 'is', + (string) $this->targetType, + (string) $this->if, + (string) $this->else + ); + } +} diff --git a/src/PseudoTypes/IntMask.php b/src/PseudoTypes/IntMask.php new file mode 100644 index 0000000..a4b50b9 --- /dev/null +++ b/src/PseudoTypes/IntMask.php @@ -0,0 +1,54 @@ +types = $types; + } + + /** + * @return Type[] + */ + public function getTypes(): array + { + return $this->types; + } + + public function underlyingType(): Type + { + return new Integer(); + } + + public function __toString(): string + { + return 'int-mask<' . implode(', ', $this->types) . '>'; + } +} diff --git a/src/PseudoTypes/IntMaskOf.php b/src/PseudoTypes/IntMaskOf.php new file mode 100644 index 0000000..80340e6 --- /dev/null +++ b/src/PseudoTypes/IntMaskOf.php @@ -0,0 +1,49 @@ +type = $type; + } + + public function getType(): Type + { + return $this->type; + } + + public function underlyingType(): Type + { + return new Integer(); + } + + public function __toString(): string + { + return 'int-mask-of<' . $this->type . '>'; + } +} diff --git a/src/PseudoTypes/KeyOf.php b/src/PseudoTypes/KeyOf.php new file mode 100644 index 0000000..6cf5e0a --- /dev/null +++ b/src/PseudoTypes/KeyOf.php @@ -0,0 +1,49 @@ +type = $type; + } + + public function getType(): Type + { + return $this->type; + } + + public function underlyingType(): Type + { + return new ArrayKey(); + } + + public function __toString(): string + { + return 'key-of<' . $this->type . '>'; + } +} diff --git a/src/PseudoTypes/OffsetAccess.php b/src/PseudoTypes/OffsetAccess.php new file mode 100644 index 0000000..22fa730 --- /dev/null +++ b/src/PseudoTypes/OffsetAccess.php @@ -0,0 +1,67 @@ +type = $type; + $this->offset = $offset; + } + + public function getType(): Type + { + return $this->type; + } + + public function getOffset(): Type + { + return $this->offset; + } + + public function underlyingType(): Type + { + return new Mixed_(); + } + + public function __toString(): string + { + if ( + $this->type instanceof Callable_ + || $this->type instanceof ConstExpression + || $this->type instanceof Nullable + ) { + return '(' . $this->type . ')[' . $this->offset . ']'; + } + + return $this->type . '[' . $this->offset . ']'; + } +} diff --git a/src/PseudoTypes/ValueOf.php b/src/PseudoTypes/ValueOf.php new file mode 100644 index 0000000..608b634 --- /dev/null +++ b/src/PseudoTypes/ValueOf.php @@ -0,0 +1,49 @@ +type = $type; + } + + public function getType(): Type + { + return $this->type; + } + + public function underlyingType(): Type + { + return new Mixed_(); + } + + public function __toString(): string + { + return 'value-of<' . $this->type . '>'; + } +} diff --git a/src/TypeResolver.php b/src/TypeResolver.php index 6ebca5e..6c46079 100644 --- a/src/TypeResolver.php +++ b/src/TypeResolver.php @@ -18,12 +18,17 @@ use phpDocumentor\Reflection\PseudoTypes\ArrayShape; use phpDocumentor\Reflection\PseudoTypes\ArrayShapeItem; use phpDocumentor\Reflection\PseudoTypes\CallableString; +use phpDocumentor\Reflection\PseudoTypes\Conditional; +use phpDocumentor\Reflection\PseudoTypes\ConditionalForParameter; use phpDocumentor\Reflection\PseudoTypes\ConstExpression; use phpDocumentor\Reflection\PseudoTypes\False_; use phpDocumentor\Reflection\PseudoTypes\FloatValue; use phpDocumentor\Reflection\PseudoTypes\HtmlEscapedString; use phpDocumentor\Reflection\PseudoTypes\IntegerRange; use phpDocumentor\Reflection\PseudoTypes\IntegerValue; +use phpDocumentor\Reflection\PseudoTypes\IntMask; +use phpDocumentor\Reflection\PseudoTypes\IntMaskOf; +use phpDocumentor\Reflection\PseudoTypes\KeyOf; use phpDocumentor\Reflection\PseudoTypes\List_; use phpDocumentor\Reflection\PseudoTypes\ListShape; use phpDocumentor\Reflection\PseudoTypes\ListShapeItem; @@ -38,10 +43,12 @@ use phpDocumentor\Reflection\PseudoTypes\NumericString; use phpDocumentor\Reflection\PseudoTypes\ObjectShape; use phpDocumentor\Reflection\PseudoTypes\ObjectShapeItem; +use phpDocumentor\Reflection\PseudoTypes\OffsetAccess; use phpDocumentor\Reflection\PseudoTypes\PositiveInteger; use phpDocumentor\Reflection\PseudoTypes\StringValue; use phpDocumentor\Reflection\PseudoTypes\TraitString; use phpDocumentor\Reflection\PseudoTypes\True_; +use phpDocumentor\Reflection\PseudoTypes\ValueOf; use phpDocumentor\Reflection\Types\AggregatedType; use phpDocumentor\Reflection\Types\Array_; use phpDocumentor\Reflection\Types\ArrayKey; @@ -112,6 +119,7 @@ use function sprintf; use function strpos; use function strtolower; +use function substr; use function trim; final class TypeResolver @@ -350,8 +358,29 @@ function (TypeNode $nestedType) use ($context): Type { return new This(); case ConditionalTypeNode::class: + return new Conditional( + $type->negated, + $this->createType($type->subjectType, $context), + $this->createType($type->targetType, $context), + $this->createType($type->if, $context), + $this->createType($type->else, $context), + ); + case ConditionalTypeForParameterNode::class: + return new ConditionalForParameter( + $type->negated, + substr($type->parameterName, 1), + $this->createType($type->targetType, $context), + $this->createType($type->if, $context), + $this->createType($type->else, $context), + ); + case OffsetAccessTypeNode::class: + return new OffsetAccess( + $this->createType($type->type, $context), + $this->createType($type->offset, $context) + ); + default: return new Mixed_(); } @@ -416,6 +445,25 @@ function (TypeNode $genericType) use ($context): Type { ) ); + case 'key-of': + return new KeyOf($this->createType($type->genericTypes[0], $context)); + + case 'value-of': + return new ValueOf($this->createType($type->genericTypes[0], $context)); + + case 'int-mask': + return new IntMask( + ...array_map( + function (TypeNode $genericType) use ($context): Type { + return $this->createType($genericType, $context); + }, + $type->genericTypes + ) + ); + + case 'int-mask-of': + return new IntMaskOf($this->createType($type->genericTypes[0], $context)); + default: $collectionType = $this->createType($type->type, $context); if ($collectionType instanceof Object_ === false) { diff --git a/src/Types/ArrayKey.php b/src/Types/ArrayKey.php index cf86df0..fc3641e 100644 --- a/src/Types/ArrayKey.php +++ b/src/Types/ArrayKey.php @@ -23,7 +23,7 @@ * * @psalm-immutable */ -final class ArrayKey extends AggregatedType implements PseudoType +class ArrayKey extends AggregatedType implements PseudoType { public function __construct() { diff --git a/src/Types/Mixed_.php b/src/Types/Mixed_.php index 56d1b6d..908f45e 100644 --- a/src/Types/Mixed_.php +++ b/src/Types/Mixed_.php @@ -20,7 +20,7 @@ * * @psalm-immutable */ -final class Mixed_ implements Type +class Mixed_ implements Type { /** * Returns a rendered output of the Type as it would be used in a DocBlock. diff --git a/tests/unit/PseudoTypes/ConditionalForParameterTest.php b/tests/unit/PseudoTypes/ConditionalForParameterTest.php new file mode 100644 index 0000000..7eec9c3 --- /dev/null +++ b/tests/unit/PseudoTypes/ConditionalForParameterTest.php @@ -0,0 +1,76 @@ +assertFalse($type->isNegated()); + $this->assertSame($parameterName, $type->getParameterName()); + $this->assertSame($targetType, $type->getTargetType()); + $this->assertSame($if, $type->getIf()); + $this->assertSame($else, $type->getElse()); + } + + /** + * @dataProvider provideToStringData + * @covers ::__toString + */ + public function testToString(string $expectedResult, ConditionalForParameter $type): void + { + $this->assertSame($expectedResult, (string) $type); + } + + /** + * @return array + */ + public static function provideToStringData(): array + { + return [ + 'basic' => [ + '($test is int ? static : static[])', + new ConditionalForParameter( + false, + 'test', + new Integer(), + new Static_(), + new Array_(new Static_()) + ), + ], + 'negated' => [ + '($test2 is not int ? static : static[])', + new ConditionalForParameter( + true, + 'test2', + new Integer(), + new Static_(), + new Array_(new Static_()) + ), + ], + ]; + } +} diff --git a/tests/unit/PseudoTypes/ConditionalTest.php b/tests/unit/PseudoTypes/ConditionalTest.php new file mode 100644 index 0000000..aff523d --- /dev/null +++ b/tests/unit/PseudoTypes/ConditionalTest.php @@ -0,0 +1,78 @@ +assertFalse($type->isNegated()); + $this->assertSame($subjectType, $type->getSubjectType()); + $this->assertSame($targetType, $type->getTargetType()); + $this->assertSame($if, $type->getIf()); + $this->assertSame($else, $type->getElse()); + } + + /** + * @dataProvider provideToStringData + * @covers ::__toString + */ + public function testToString(string $expectedResult, Conditional $type): void + { + $this->assertSame($expectedResult, (string) $type); + } + + /** + * @return array + */ + public static function provideToStringData(): array + { + return [ + 'basic' => [ + '(\\phpDocumentor\\T is int ? static : static[])', + new Conditional( + false, + new Object_(new Fqsen('\\phpDocumentor\\T')), + new Integer(), + new Static_(), + new Array_(new Static_()) + ), + ], + 'negated' => [ + '(\\phpDocumentor\\T is not int ? static : static[])', + new Conditional( + true, + new Object_(new Fqsen('\\phpDocumentor\\T')), + new Integer(), + new Static_(), + new Array_(new Static_()) + ), + ], + ]; + } +} diff --git a/tests/unit/PseudoTypes/IntMaskOfTest.php b/tests/unit/PseudoTypes/IntMaskOfTest.php new file mode 100644 index 0000000..14d8135 --- /dev/null +++ b/tests/unit/PseudoTypes/IntMaskOfTest.php @@ -0,0 +1,35 @@ +assertSame($childType, $type->getType()); + } + + /** + * @covers ::__toString + */ + public function testToString(): void + { + $type = new IntMask(new Compound([new IntegerValue(1), new IntegerValue(5), new IntegerValue(10)])); + + $this->assertSame('int-mask<1|5|10>', (string) $type); + } +} diff --git a/tests/unit/PseudoTypes/IntMaskTest.php b/tests/unit/PseudoTypes/IntMaskTest.php new file mode 100644 index 0000000..59fba2c --- /dev/null +++ b/tests/unit/PseudoTypes/IntMaskTest.php @@ -0,0 +1,33 @@ +assertSame($childTypes, $type->getTypes()); + } + + /** + * @covers ::__toString + */ + public function testToString(): void + { + $type = new IntMask(new IntegerValue(1), new IntegerValue(510), new IntegerValue(6000)); + $this->assertSame('int-mask<1, 510, 6000>', (string) $type); + } +} diff --git a/tests/unit/PseudoTypes/KeyOfTest.php b/tests/unit/PseudoTypes/KeyOfTest.php new file mode 100644 index 0000000..ced324a --- /dev/null +++ b/tests/unit/PseudoTypes/KeyOfTest.php @@ -0,0 +1,36 @@ +assertSame($childType, $type->getType()); + } + + /** + * @covers ::__toString + */ + public function testToString(): void + { + $type = new KeyOf(new ConstExpression(new Object_(new Fqsen('\\phpDocumentor\\Type')), 'ARRAY_CONST')); + + $this->assertSame('key-of<\\phpDocumentor\\Type::ARRAY_CONST>', (string) $type); + } +} diff --git a/tests/unit/PseudoTypes/OffsetAccessTest.php b/tests/unit/PseudoTypes/OffsetAccessTest.php new file mode 100644 index 0000000..34bd1c8 --- /dev/null +++ b/tests/unit/PseudoTypes/OffsetAccessTest.php @@ -0,0 +1,58 @@ +assertSame($mainType, $type->getType()); + $this->assertSame($offset, $type->getOffset()); + } + + /** + * @dataProvider provideToStringData + * @covers ::__toString + */ + public function testToString(string $expectedResult, OffsetAccess $type): void + { + $this->assertSame($expectedResult, (string) $type); + } + + /** + * @return array + */ + public static function provideToStringData(): array + { + return [ + 'basic' => [ + '\\phpDocumentor\\MyArray["bar"]', + new OffsetAccess(new Object_(new Fqsen('\\phpDocumentor\\MyArray')), new StringValue('bar')), + ], + 'with const expression' => [ + '(\\phpDocumentor\\Foo::SOME_ARRAY)["bar"]', + new OffsetAccess( + new ConstExpression(new Object_(new Fqsen('\\phpDocumentor\\Foo')), 'SOME_ARRAY'), + new StringValue('bar') + ), + ], + ]; + } +} diff --git a/tests/unit/PseudoTypes/ValueOfTest.php b/tests/unit/PseudoTypes/ValueOfTest.php new file mode 100644 index 0000000..e96e305 --- /dev/null +++ b/tests/unit/PseudoTypes/ValueOfTest.php @@ -0,0 +1,36 @@ +assertSame($childType, $type->getType()); + } + + /** + * @covers ::__toString + */ + public function testToString(): void + { + $type = new ValueOf(new ConstExpression(new Object_(new Fqsen('\\phpDocumentor\\Type')), 'ARRAY_CONST')); + + $this->assertSame('value-of<\\phpDocumentor\\Type::ARRAY_CONST>', (string) $type); + } +} diff --git a/tests/unit/TypeResolverTest.php b/tests/unit/TypeResolverTest.php index d77c34f..6aca0a2 100644 --- a/tests/unit/TypeResolverTest.php +++ b/tests/unit/TypeResolverTest.php @@ -18,12 +18,17 @@ use phpDocumentor\Reflection\PseudoTypes\ArrayShape; use phpDocumentor\Reflection\PseudoTypes\ArrayShapeItem; use phpDocumentor\Reflection\PseudoTypes\CallableString; +use phpDocumentor\Reflection\PseudoTypes\Conditional; +use phpDocumentor\Reflection\PseudoTypes\ConditionalForParameter; use phpDocumentor\Reflection\PseudoTypes\ConstExpression; use phpDocumentor\Reflection\PseudoTypes\False_; use phpDocumentor\Reflection\PseudoTypes\FloatValue; use phpDocumentor\Reflection\PseudoTypes\HtmlEscapedString; use phpDocumentor\Reflection\PseudoTypes\IntegerRange; use phpDocumentor\Reflection\PseudoTypes\IntegerValue; +use phpDocumentor\Reflection\PseudoTypes\IntMask; +use phpDocumentor\Reflection\PseudoTypes\IntMaskOf; +use phpDocumentor\Reflection\PseudoTypes\KeyOf; use phpDocumentor\Reflection\PseudoTypes\List_; use phpDocumentor\Reflection\PseudoTypes\ListShape; use phpDocumentor\Reflection\PseudoTypes\ListShapeItem; @@ -38,10 +43,12 @@ use phpDocumentor\Reflection\PseudoTypes\NumericString; use phpDocumentor\Reflection\PseudoTypes\ObjectShape; use phpDocumentor\Reflection\PseudoTypes\ObjectShapeItem; +use phpDocumentor\Reflection\PseudoTypes\OffsetAccess; use phpDocumentor\Reflection\PseudoTypes\PositiveInteger; use phpDocumentor\Reflection\PseudoTypes\StringValue; use phpDocumentor\Reflection\PseudoTypes\TraitString; use phpDocumentor\Reflection\PseudoTypes\True_; +use phpDocumentor\Reflection\PseudoTypes\ValueOf; use phpDocumentor\Reflection\Types\Array_; use phpDocumentor\Reflection\Types\ArrayKey; use phpDocumentor\Reflection\Types\Boolean; @@ -977,6 +984,53 @@ public function typeProvider(): array 'self', new Self_(), ], + [ + '($size is positive-int ? non-empty-array : array)', + new ConditionalForParameter( + false, + 'size', + new PositiveInteger(), + new NonEmptyArray(), + new Array_() + ), + ], + [ + '($size is not positive-int ? non-empty-array : int)', + new ConditionalForParameter( + true, + 'size', + new PositiveInteger(), + new NonEmptyArray(), + new Integer() + ), + ], + [ + '(T is int ? static : array)', + new Conditional( + false, + new Object_(new Fqsen('\\phpDocumentor\\T')), + new Integer(), + new Static_(), + new Array_(new Static_()) + ), + ], + [ + '(T is not int ? self : array)', + new Conditional( + true, + new Object_(new Fqsen('\\phpDocumentor\\T')), + new Integer(), + new Self_(), + new Array_(new Static_()) + ), + ], + [ + "MyArray['bar']", + new OffsetAccess( + new Object_(new Fqsen('\\phpDocumentor\\MyArray')), + new StringValue('bar') + ), + ], ]; } @@ -1035,6 +1089,26 @@ public function genericsProvider(): array 'int<1, 100>', new IntegerRange('1', '100'), ], + [ + 'key-of', + new KeyOf(new ConstExpression(new Object_(new Fqsen('\\phpDocumentor\\Type')), 'ARRAY_CONST')), + ], + [ + 'value-of', + new ValueOf(new ConstExpression(new Object_(new Fqsen('\\phpDocumentor\\Type')), 'ARRAY_CONST')), + ], + [ + 'int-mask<1, 2, 4>', + new IntMask(new IntegerValue(1), new IntegerValue(2), new IntegerValue(4)), + ], + [ + 'int-mask-of<1|2|4>', + new IntMaskOf(new Compound([new IntegerValue(1), new IntegerValue(2), new IntegerValue(4)])), + ], + [ + 'int-mask-of', + new IntMaskOf(new ConstExpression(new Object_(new Fqsen('\\phpDocumentor\\Foo')), 'INT_*')), + ], ]; }