diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml index 5eb7dce..c01fb1f 100644 --- a/.github/workflows/infection.yml +++ b/.github/workflows/infection.yml @@ -1,6 +1,6 @@ name: Infection -on: [ pull_request, push ] +on: [ pull_request ] jobs: build: @@ -52,3 +52,10 @@ jobs: --min-msi=100 \ --min-covered-msi=100 \ --ignore-msi-with-no-mutations + + - name: Archive Infection log + uses: actions/upload-artifact@v3 + if: always() + with: + name: infection-log + path: infection.log diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml index 2ff0e1b..bedf0c7 100644 --- a/.github/workflows/phpcs.yml +++ b/.github/workflows/phpcs.yml @@ -1,6 +1,6 @@ name: PHPCS -on: [ pull_request, push ] +on: [ pull_request ] jobs: build: diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index c273ebc..c95af5d 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -1,6 +1,6 @@ name: PHPStan -on: [ pull_request, push ] +on: [ pull_request ] jobs: build: diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index d141c3a..20f81aa 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -1,6 +1,6 @@ name: PHPUnit -on: [ pull_request, push ] +on: [ pull_request ] jobs: build: @@ -11,6 +11,7 @@ jobs: matrix: php: [ '8.1' ] prefer-lowest: [ '--prefer-lowest', '' ] + fail-fast: false name: PHPUnit on PHP ${{ matrix.php }} diff --git a/composer.json b/composer.json index 3133f95..be22bbe 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "require-dev": { "infection/infection": "^0.26.13", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", + "phpstan/phpstan": "^1.9", "phpstan/phpstan-strict-rules": "^1.3", "phpunit/phpunit": "^9.5", "slevomat/coding-standard": "^8.1", @@ -21,7 +21,8 @@ }, "autoload-dev": { "psr-4": { - "PhpTypes\\Types\\Tests\\Functional\\": "tests/functional" + "PhpTypes\\Types\\Tests\\Functional\\": "tests/functional", + "PhpTypes\\Types\\Tests\\Unit\\": "tests/unit" } }, "config": { diff --git a/infection.json b/infection.json index e2ab411..0466e3b 100644 --- a/infection.json +++ b/infection.json @@ -9,6 +9,10 @@ "text": "infection.log" }, "mutators": { - "@default": true - } -} \ No newline at end of file + "@default": true, + "global-ignoreSourceCodeByRegex": [ + "assert\\(.+\\);" + ] + }, + "timeout": 1 +} diff --git a/phpstan.dist.neon b/phpstan.dist.neon index b4851a3..1494f51 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -3,10 +3,3 @@ parameters: paths: - src - tests - scanFiles: - - generated/PhpTypes/Ast/Generated/PhpTypesBaseListener.php - - generated/PhpTypes/Ast/Generated/PhpTypesBaseVisitor.php - - generated/PhpTypes/Ast/Generated/PhpTypesLexer.php - - generated/PhpTypes/Ast/Generated/PhpTypesListener.php - - generated/PhpTypes/Ast/Generated/PhpTypesParser.php - - generated/PhpTypes/Ast/Generated/PhpTypesVisitor.php diff --git a/phpunit.xml b/phpunit.xml index 00e8e9d..9f975db 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,6 +7,9 @@ ./tests/functional + + ./tests/unit + diff --git a/src/BoolType.php b/src/BoolType.php index 4256dca..7cc9e54 100644 --- a/src/BoolType.php +++ b/src/BoolType.php @@ -13,17 +13,14 @@ public function __construct(public readonly ?bool $value = null) { } - public function __toString(): string - { - return match ($this->value) { - null => 'bool', - true => 'true', - false => 'false', - }; - } - public function toNode(): NodeInterface { - return new IdentifierNode((string)$this); + return new IdentifierNode( + match ($this->value) { + null => 'bool', + true => 'true', + false => 'false', + } + ); } } diff --git a/src/ClassLikeType.php b/src/ClassLikeType.php index 8a066ac..4385edb 100644 --- a/src/ClassLikeType.php +++ b/src/ClassLikeType.php @@ -12,9 +12,13 @@ final class ClassLikeType extends AbstractType /** * @param non-empty-string $name * @param list $typeParameters + * @param list $parents */ - public function __construct(public readonly string $name, public readonly array $typeParameters = []) - { + public function __construct( + public readonly string $name, + public readonly array $typeParameters = [], + public readonly array $parents = [], + ) { } public function toNode(): NodeInterface diff --git a/src/Compatibility.php b/src/Compatibility.php new file mode 100644 index 0000000..41075d1 --- /dev/null +++ b/src/Compatibility.php @@ -0,0 +1,315 @@ + self::checkBool($super, $sub), + $super instanceof CallableType => self::checkCallable($super, $sub), + $super instanceof ClassLikeType => self::checkClassLike($super, $sub), + $super instanceof ClassStringType => self::checkClassString($super, $sub), + $super instanceof FloatType => self::checkFloat($sub), + $super instanceof IntLiteralType => self::checkIntLiteral($super, $sub), + $super instanceof IntersectionType => self::checkIntersection($super, $sub), + $super instanceof IntType => self::checkInt($super, $sub), + $super instanceof IterableType => self::checkIterable($super, $sub), + $super instanceof ListType => self::checkList($super, $sub), + $super instanceof MapType => self::checkMap($super, $sub), + $super instanceof MixedType => true, + $super instanceof NeverType => false, + $super instanceof NullType => $sub instanceof NullType, + $super instanceof ScalarType => self::checkScalar($sub), + $super instanceof StringLiteralType => self::checkStringLiteral($super, $sub), + $super instanceof StringType => self::checkString($super, $sub), + $super instanceof StructType => self::checkStruct($super, $sub), + $super instanceof TupleType => self::checkTuple($super, $sub), + $super instanceof UnionType => self::checkUnion($super, $sub), + default => throw new LogicException(sprintf('Unsupported type "%s"', get_class($super))), + }; + } + + private static function checkClassString(ClassStringType $super, AbstractType $sub): bool + { + if (!$sub instanceof ClassStringType) { + return false; + } + if ($super->class === null) { + return true; + } + if ($sub->class === null) { + return false; + } + return self::check($super->class, $sub->class); + } + + private static function checkClassLike(ClassLikeType $super, AbstractType $sub): bool + { + if (!$sub instanceof ClassLikeType) { + return false; + } + if ($super->name === $sub->name) { + return true; + } + foreach ($sub->parents as $parent) { + if (self::check($super, $parent)) { + return true; + } + } + return false; + } + + private static function checkString(StringType $super, AbstractType $sub): bool + { + if ($sub instanceof StringLiteralType) { + if ($super->numeric) { + return is_numeric($sub->value); + } + if ($super->nonEmpty) { + return $sub->value !== ''; + } + return true; + } + if ($sub instanceof ClassStringType) { + return !$super->numeric; + } + if (!$sub instanceof StringType) { + return false; + } + if ($super->numeric) { + return $sub->numeric; + } + if ($super->nonEmpty) { + return $sub->nonEmpty; + } + return true; + } + + private static function checkBool(BoolType $super, AbstractType $sub): bool + { + if (!$sub instanceof BoolType) { + return false; + } + if ($super->value === null) { + return true; + } + return $super->value === $sub->value; + } + + private static function checkInt(IntType $super, AbstractType $sub): bool + { + if ($sub instanceof IntLiteralType) { + return ($super->min ?? PHP_INT_MIN) <= $sub->value && $sub->value <= ($super->max ?? PHP_INT_MAX); + } + if (!$sub instanceof IntType) { + return false; + } + [$superMin, $superMax, $subMin, $subMax] = [ + $super->min ?? PHP_INT_MIN, + $super->max ?? PHP_INT_MAX, + $sub->min ?? PHP_INT_MIN, + $sub->max ?? PHP_INT_MAX, + ]; + return $superMin <= $subMin && $superMax >= $subMax; + } + + private static function checkIntLiteral(IntLiteralType $super, AbstractType $sub): bool + { + if ($sub instanceof IntLiteralType) { + return $super->value === $sub->value; + } + if ($sub instanceof IntType) { + return $sub->min === $super->value && $sub->max === $super->value; + } + return false; + } + + private static function checkFloat(AbstractType $sub): bool + { + return $sub instanceof FloatType + || $sub instanceof IntLiteralType + || $sub instanceof IntType; + } + + private static function checkUnion(UnionType $super, AbstractType $sub): bool + { + return self::check($super->left, $sub) || self::check($super->right, $sub); + } + + private static function checkScalar(AbstractType $sub): bool + { + return $sub instanceof ScalarType + || $sub instanceof IntLiteralType + || $sub instanceof IntType + || $sub instanceof FloatType + || $sub instanceof BoolType + || $sub instanceof StringType + || $sub instanceof StringLiteralType + || $sub instanceof ClassStringType; + } + + private static function checkList(ListType $super, AbstractType $sub): bool + { + if ($sub instanceof TupleType) { + if ($super->nonEmpty && $sub->elements === []) { + return false; + } + foreach ($sub->elements as $element) { + if (self::check($super->type, $element)) { + continue; + } + return false; + } + return true; + } + if (!$sub instanceof ListType) { + return false; + } + if ($super->nonEmpty && !$sub->nonEmpty) { + return false; + } + return self::check($super->type, $sub->type); + } + + private static function checkMap(MapType $super, AbstractType $sub): bool + { + if ($sub instanceof ToMapInterface) { + $sub = $sub->toMap(); + } + if (!$sub instanceof MapType) { + return false; + } + if ($super->nonEmpty && !$sub->nonEmpty) { + return false; + } + return self::check($super->keyType, $sub->keyType) && self::check($super->valueType, $sub->valueType); + } + + private static function checkTuple(TupleType $super, AbstractType $sub): bool + { + if ($super->elements === []) { + return $sub instanceof ListType + || $sub instanceof TupleType + || $sub instanceof MapType + || $sub instanceof StructType; + } + if (!$sub instanceof TupleType) { + return false; + } + if (count($super->elements) > count($sub->elements)) { + return false; + } + foreach ($super->elements as $i => $element) { + if (self::check($element, $sub->elements[$i])) { + continue; + } + return false; + } + return true; + } + + private static function checkStruct(StructType $super, AbstractType $sub): bool + { + if (!$sub instanceof StructType) { + return false; + } + foreach ($super->members as $name => $member) { + $subMember = array_key_exists($name, $sub->members) ? $sub->members[$name] : null; + if ($subMember === null) { + if ($member->optional) { + continue; + } + return false; + } + if (!self::check($member->type, $subMember->type)) { + return false; + } + if ($subMember->optional && !$member->optional) { + return false; + } + } + return true; + } + + private static function checkStringLiteral(StringLiteralType $super, AbstractType $sub): bool + { + return $sub instanceof StringLiteralType && $super->value === $sub->value; + } + + private static function checkCallable(CallableType $super, AbstractType $sub): bool + { + if (!$sub instanceof CallableType) { + return false; + } + if (!$super->returnType instanceof VoidType && !self::check($super->returnType, $sub->returnType)) { + return false; + } + if (count($sub->parameters) > count($super->parameters)) { + return false; + } + foreach ($sub->parameters as $i => $parameter) { + $superParameter = $super->parameters[$i]; + if (!$parameter->optional && $superParameter->optional) { + return false; + } + if (self::check($parameter->type, $superParameter->type)) { + continue; + } + return false; + } + return true; + } + + private static function checkIterable(IterableType $super, AbstractType $sub): bool + { + if ($sub instanceof ToIterableInterface) { + $sub = $sub->toIterable(); + } + if (!$sub instanceof IterableType) { + return false; + } + return self::check($super->keyType, $sub->keyType) + && self::check($super->valueType, $sub->valueType); + } + + private static function checkSubUnion(AbstractType $super, UnionType $sub): bool + { + return self::check($super, $sub->left) && self::check($super, $sub->right); + } + + private static function checkIntersection(IntersectionType $super, AbstractType $sub): bool + { + return self::check($super->left, $sub) && self::check($super->right, $sub); + } + + private static function checkSubIntersection(AbstractType $super, IntersectionType $sub): bool + { + return self::check($super, $sub->left) && self::check($super, $sub->right); + } +} diff --git a/src/Conversion/ToIterableInterface.php b/src/Conversion/ToIterableInterface.php new file mode 100644 index 0000000..039139e --- /dev/null +++ b/src/Conversion/ToIterableInterface.php @@ -0,0 +1,13 @@ +type->toNode()) : StructMemberNode::required($this->type->toNode()); } + + public function intersect(self $other): self + { + return new self(IntersectionType::create($this->type, $other->type), $this->optional && $other->optional); + } } diff --git a/src/IntType.php b/src/IntType.php index 07995f2..5d801d2 100644 --- a/src/IntType.php +++ b/src/IntType.php @@ -14,11 +14,6 @@ public function __construct(public readonly int|null $min = null, public readonl { } - public static function minMax(int $min, int $max): self - { - return new self($min, $max); - } - public static function min(int $min): self { return new self($min, null); diff --git a/src/IntersectionType.php b/src/IntersectionType.php index 48cb49a..af3ca1a 100644 --- a/src/IntersectionType.php +++ b/src/IntersectionType.php @@ -7,14 +7,62 @@ use PhpTypes\Ast\Node\IntersectionNode; use PhpTypes\Ast\Node\NodeInterface; +use function array_shift; +use function assert; +use function count; + final class IntersectionType extends AbstractType { - public function __construct(public readonly AbstractType $left, public readonly AbstractType $right) + private function __construct(public readonly AbstractType $left, public readonly AbstractType $right) + { + } + + public static function create(AbstractType $left, AbstractType $right): AbstractType + { + $parts = array_merge( + $left instanceof self ? $left->flatten() : [$left], + $right instanceof self ? $right->flatten() : [$right], + ); + $structs = []; + foreach ($parts as $index => $part) { + if (!$part instanceof StructType) { + continue; + } + $structs[] = $part; + unset($parts[$index]); + } + if ($structs !== []) { + $parts[] = StructType::merge($structs); + } + assert($parts !== []); + return self::unflatten($parts); + } + + /** + * @param non-empty-array $types + */ + private static function unflatten(array $types): AbstractType { + $first = array_shift($types); + return match (count($types)) { + 0 => $first, + 1 => new self($first, array_shift($types)), + default => new self($first, self::unflatten($types)), + }; } public function toNode(): NodeInterface { return new IntersectionNode($this->left->toNode(), $this->right->toNode()); } + + /** + * @return non-empty-list + */ + private function flatten(): array + { + $leftParts = $this->left instanceof self ? $this->left->flatten() : [$this->left]; + $rightParts = $this->right instanceof self ? $this->right->flatten() : [$this->right]; + return array_merge($leftParts, $rightParts); + } } diff --git a/src/ListType.php b/src/ListType.php index e590048..83745f9 100644 --- a/src/ListType.php +++ b/src/ListType.php @@ -6,20 +6,27 @@ use PhpTypes\Ast\Node\IdentifierNode; use PhpTypes\Ast\Node\NodeInterface; +use PhpTypes\Types\Conversion\ToIterableInterface; +use PhpTypes\Types\Conversion\ToMapInterface; -final class ListType extends AbstractType +final class ListType extends AbstractType implements ToIterableInterface, ToMapInterface { public function __construct(public readonly AbstractType $type, public readonly bool $nonEmpty = false) { } - public function nonEmpty(AbstractType $type): AbstractType + public function toNode(): NodeInterface { - return new ListType($type, true); + return new IdentifierNode($this->nonEmpty ? 'non-empty-list' : 'list', [$this->type->toNode()]); } - public function toNode(): NodeInterface + public function toMap(): MapType { - return new IdentifierNode($this->nonEmpty ? 'non-empty-list' : 'list', [$this->type->toNode()]); + return new MapType(new IntType(), $this->type, $this->nonEmpty); + } + + public function toIterable(): IterableType + { + return new IterableType(new IntType(), $this->type); } } diff --git a/src/MapType.php b/src/MapType.php index 81ac46b..0396a65 100644 --- a/src/MapType.php +++ b/src/MapType.php @@ -6,19 +6,28 @@ use PhpTypes\Ast\Node\IdentifierNode; use PhpTypes\Ast\Node\NodeInterface; +use PhpTypes\Types\Conversion\ToIterableInterface; +use RuntimeException; use function in_array; +use function sprintf; -final class MapType extends AbstractType +final class MapType extends AbstractType implements ToIterableInterface { public function __construct( public readonly AbstractType $keyType, public readonly AbstractType $valueType, public readonly bool $nonEmpty = false, ) { + if (Compatibility::check(new UnionType(new StringType(), new IntType()), $keyType)) { + return; + } + throw new RuntimeException( + sprintf('Can\'t use %s as array key. Only strings and integers are allowed.', $keyType), + ); } - public function nonEmpty(AbstractType $keyType, AbstractType $valueType): AbstractType + public static function nonEmpty(AbstractType $keyType, AbstractType $valueType): self { return new self($keyType, $valueType, true); } @@ -42,4 +51,9 @@ public function toNode(): NodeInterface [$keyNode, $this->valueType->toNode()] ); } + + public function toIterable(): IterableType + { + return new IterableType($this->keyType, $this->valueType); + } } diff --git a/src/ScalarType.php b/src/ScalarType.php index c5b1573..2c138a2 100644 --- a/src/ScalarType.php +++ b/src/ScalarType.php @@ -1,5 +1,7 @@ register('bool', new BoolType()); $scope->register('false', new BoolType(false)); $scope->register('float', new FloatType()); - $scope->register('int', self::int(...)); $scope->register('iterable', $scope->iterable(...)); $scope->register('list', self::list(...)); $scope->register('mixed', new MixedType()); @@ -45,58 +43,6 @@ public static function global(): self return $scope; } - /** - * @param list $typeParameters - */ - private static function int(array $typeParameters): IntType - { - switch (count($typeParameters)) { - case 0: - return new IntType(); - case 2: - if ( - $typeParameters[0] instanceof IntLiteralType - && $typeParameters[1] instanceof IntLiteralType - ) { - return IntType::minMax( - $typeParameters[0]->value, - $typeParameters[1]->value, - ); - } - if ( - $typeParameters[0] instanceof IntLiteralType - && $typeParameters[1] instanceof IdentifierNode - && $typeParameters[1]->name === 'max' - ) { - return IntType::min($typeParameters[0]->value); - } - if ( - $typeParameters[0] instanceof IdentifierNode - && $typeParameters[0]->name === 'min' - && $typeParameters[1] instanceof IntLiteralType - ) { - return IntType::max($typeParameters[1]->value); - } - throw new RuntimeException( - sprintf( - 'Integer types must take one of the following forms: ' . - 'int, `int<23, 42>`, `int`, `int<23, max>`. ' . - 'Got: int<%s>', - implode(', ', $typeParameters), - ) - ); - default: - throw new RuntimeException( - sprintf( - 'Integer types must take one of the following forms: ' . - 'int, `int<23, 42>`, `int`, `int<23, max>`. ' . - 'Got: int<%s>', - implode(', ', $typeParameters), - ) - ); - } - } - /** * @param list $types */ @@ -130,7 +76,8 @@ public function getType(string $name, array $typeParameters = []): AbstractType { $type = $this->types[$name] ?? null; if ($type === null) { - throw new RuntimeException(sprintf('Unknown type %s', $name)); + $typeString = $name . ($typeParameters !== [] ? '<' . implode(', ', $typeParameters) . '>' : ''); + throw new RuntimeException(sprintf('Unknown type %s', $typeString)); } return $type instanceof AbstractType ? $type : $type($typeParameters); } @@ -155,7 +102,8 @@ private function map(array $typeParameters, bool $nonEmpty = false): MapType 2 => [$typeParameters[0], $typeParameters[1]], default => throw new RuntimeException( 'Array types must take one of the following forms: ' . - 'array, array, array', + 'array, array, array. ' . + 'Got array<' . implode(', ', $typeParameters) . '>', ), }; return new MapType($keyType, $valueType, $nonEmpty); diff --git a/src/StringType.php b/src/StringType.php index a530a5c..cc09eba 100644 --- a/src/StringType.php +++ b/src/StringType.php @@ -23,14 +23,6 @@ public static function numeric(): AbstractType return new StringType(true, true); } - public function __toString(): string - { - if ($this->numeric) { - return 'numeric-string'; - } - return $this->nonEmpty ? 'non-empty-string' : 'string'; - } - public function toNode(): NodeInterface { if ($this->numeric) { diff --git a/src/StructType.php b/src/StructType.php index e2f9471..aa84d30 100644 --- a/src/StructType.php +++ b/src/StructType.php @@ -6,17 +6,37 @@ use PhpTypes\Ast\Node\NodeInterface; use PhpTypes\Ast\Node\StructNode; +use PhpTypes\Types\Conversion\ToIterableInterface; +use PhpTypes\Types\Conversion\ToMapInterface; use PhpTypes\Types\Dto\StructMember; -final class StructType extends AbstractType +final class StructType extends AbstractType implements ToIterableInterface, ToMapInterface { /** - * @param array $members + * @param non-empty-array $members */ public function __construct(public readonly array $members) { } + /** + * @param non-empty-array $structs + */ + public static function merge(iterable $structs): self + { + $members = []; + foreach ($structs as $struct) { + foreach ($struct->members as $name => $member) { + if (!isset($members[$name])) { + $members[$name] = $member; + continue; + } + $members[$name] = $members[$name]->intersect($member); + } + } + return new self($members); + } + public function toNode(): NodeInterface { $members = []; @@ -25,4 +45,34 @@ public function toNode(): NodeInterface } return new StructNode($members); } + + public function toMap(): MapType + { + $types = $this->keyAndValueType(); + return MapType::nonEmpty($types[0], $types[1]); + } + + public function toIterable(): IterableType + { + $types = $this->keyAndValueType(); + return new IterableType($types[0], $types[1]); + } + + /** + * @return array{AbstractType, AbstractType} + */ + private function keyAndValueType(): array + { + /** @var array{AbstractType, AbstractType}|null $types */ + $types = null; + foreach ($this->members as $name => $member) { + if ($types === null) { + $types = [new StringLiteralType($name), $member->type]; + continue; + } + $types[0] = new UnionType($types[0], new StringLiteralType($name)); + $types[1] = new UnionType($types[1], $member->type); + } + return $types; + } } diff --git a/src/TupleType.php b/src/TupleType.php index cecca99..118e9c0 100644 --- a/src/TupleType.php +++ b/src/TupleType.php @@ -6,8 +6,10 @@ use PhpTypes\Ast\Node\NodeInterface; use PhpTypes\Ast\Node\TupleNode; +use PhpTypes\Types\Conversion\ToIterableInterface; +use PhpTypes\Types\Conversion\ToMapInterface; -final class TupleType extends AbstractType +final class TupleType extends AbstractType implements ToIterableInterface, ToMapInterface { /** * @param list $elements @@ -24,4 +26,31 @@ public function toNode(): NodeInterface } return new TupleNode($elementNodes); } + + public function toMap(): MapType + { + return new MapType(new IntType(), $this->valueType()); + } + + public function toIterable(): IterableType + { + return new IterableType(new IntType(), $this->valueType()); + } + + private function valueType(): AbstractType + { + $valueType = null; + foreach ($this->elements as $element) { + if ($valueType === null) { + $valueType = $element; + continue; + } + // @infection-ignore-all This isn't really required, it's just an optimization. + if (Compatibility::check($valueType, $element)) { + continue; + } + $valueType = new UnionType($valueType, $element); + } + return $valueType ?? new NeverType(); + } } diff --git a/src/Type.php b/src/Type.php index 5d03eef..727e228 100644 --- a/src/Type.php +++ b/src/Type.php @@ -20,6 +20,8 @@ use RuntimeException; use function count; +use function implode; +use function sprintf; final class Type { @@ -44,6 +46,9 @@ private static function fromNode(NodeInterface $node, Scope $scope): AbstractTyp $node instanceof StructNode => self::fromStruct($node->members, $scope), $node instanceof TupleNode => self::fromTuple($node, $scope), $node instanceof UnionNode => self::fromUnion($node, $scope), + default => throw new RuntimeException( + sprintf('Unsupported node type: %s (%s)', get_class($node), $node) + ), }; } @@ -61,10 +66,18 @@ private static function fromIdentifier(IdentifierNode $node, Scope $scope): Abst private static function fromUnion(UnionNode $node, Scope $scope): AbstractType { - return new UnionType( - self::fromNode($node->left, $scope), - self::fromNode($node->right, $scope), - ); + $left = self::fromNode($node->left, $scope); + $right = self::fromNode($node->right, $scope); + if (Compatibility::check($left, $right)) { + return $left; + } + if (Compatibility::check($right, $left)) { + return $right; + } + if ($left instanceof BoolType && $right instanceof BoolType) { + return new BoolType($left->value === $right->value ? $left->value : null); + } + return new UnionType($left, $right); } private static function fromTuple(TupleNode $node, Scope $scope): TupleType @@ -77,10 +90,13 @@ private static function fromTuple(TupleNode $node, Scope $scope): TupleType } /** - * @param list $members + * @param array $members */ - private static function fromStruct(array $members, Scope $scope): StructType + private static function fromStruct(array $members, Scope $scope): StructType|TupleType { + if (count($members) === 0) { + return new TupleType([]); + } $typeMembers = []; foreach ($members as $name => $member) { $typeMembers[$name] = $member->optional @@ -106,7 +122,7 @@ private static function fromCallable(CallableNode $node, Scope $scope): Callable private static function fromIntersection(IntersectionNode $node, Scope $scope): AbstractType { - return new IntersectionType( + return IntersectionType::create( self::fromNode($node->left, $scope), self::fromNode($node->right, $scope), ); @@ -119,7 +135,13 @@ private static function fromInt(IdentifierNode $node): IntType return new IntType(); } if ($numberOfParams !== 2) { - throw new RuntimeException('Invalid number of type parameters'); + throw new RuntimeException( + sprintf( + 'The int type takes exactly zero or two type parameters, %d (%s) given', + $numberOfParams, + implode(', ', $node->typeParameters), + ), + ); } $min = (static function () use ($node) { if ($node->typeParameters[0] instanceof IdentifierNode && $node->typeParameters[0]->name === 'min') { @@ -128,7 +150,12 @@ private static function fromInt(IdentifierNode $node): IntType if ($node->typeParameters[0] instanceof IntLiteralNode) { return $node->typeParameters[0]->value; } - throw new RuntimeException('Invalid int type'); + throw new RuntimeException( + sprintf( + "Invalid minimum value for int type: %s. Must be an integer or \"min\".", + $node->typeParameters[0], + ) + ); })(); $max = (static function () use ($node) { if ($node->typeParameters[1] instanceof IdentifierNode && $node->typeParameters[1]->name === 'max') { @@ -137,7 +164,12 @@ private static function fromInt(IdentifierNode $node): IntType if ($node->typeParameters[1] instanceof IntLiteralNode) { return $node->typeParameters[1]->value; } - throw new RuntimeException('Invalid int type'); + throw new RuntimeException( + sprintf( + "Invalid maximum value for int type: %s. Must be an integer or \"max\".", + $node->typeParameters[1], + ) + ); })(); return new IntType($min, $max); } diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php new file mode 100644 index 0000000..75fe69c --- /dev/null +++ b/tests/functional/CompatibilityTest.php @@ -0,0 +1,246 @@ + */ + private static array $cache = []; + + private static Scope $scope; + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + self::$scope = Scope::global(); + $fooInterface = new ClassLikeType('FooInterface'); + self::$scope->register('FooInterface', $fooInterface); + self::$scope->register('Foo', new ClassLikeType('Foo', parents: [$fooInterface])); + $runnable = new ClassLikeType('Runnable'); + self::$scope->register('Runnable', $runnable); + $loggable = new ClassLikeType('Loggable'); + self::$scope->register('Loggable', $loggable); + $runnableAndLoggable = new ClassLikeType('RunnableAndLoggable', parents: [$runnable, $loggable]); + self::$scope->register('RunnableAndLoggable', $runnableAndLoggable); + } + + /** + * @return list + */ + private static function compatibleTypes(): array + { + $types = []; + foreach (self::filesInDirectory(__DIR__ . '/compatible-types/') as $file) { + foreach (explode("\n", \Safe\file_get_contents($file)) as $line) { + $isMatch = \Safe\preg_match('/- `(?.+)` is a subtype of `(?.+)`/', $line, $matches); + if ($isMatch === 0) { + continue; + } + $types[] = [$matches['super'], $matches['sub']]; + } + } + return $types; + } + + /** + * @return iterable + */ + private static function filesInDirectory(string $directory): iterable + { + foreach (new DirectoryIterator($directory) as $file) { + if ($file->isDot()) { + continue; + } + + yield $file->getPathname(); + } + } + + /** + * @return iterable + */ + private static function types(): iterable + { + foreach (self::typeFiles() as $file) { + foreach (explode("\n", \Safe\file_get_contents($file)) as $line) { + if ($line === '') { + continue; + } + yield $line; + } + } + } + + /** + * @return iterable + */ + private static function typeFiles(): iterable + { + return self::filesInDirectory(__DIR__ . '/types/'); + } + + /** + * @return list + */ + private static function aliases(): array + { + $aliases = []; + foreach (explode("\n", \Safe\file_get_contents(__DIR__ . '/aliases.md')) as $line) { + $isMatch = \Safe\preg_match('/^- `(?.+)` is an alias of `(?.+)`/', $line, $matches); + if ($isMatch === 0) { + continue; + } + $tuple = [$matches['a'], $matches['b']]; + sort($tuple); + $aliases[] = $tuple; + } + return $aliases; + } + + private static function fromString(string $typeString, Scope $scope): AbstractType + { + $type = self::$cache[$typeString] ?? null; + if ($type === null) { + $type = Type::fromString($typeString, $scope); + self::$cache[$typeString] = $type; + } + return $type; + } + + /** + * @dataProvider compatibilityCases + */ + public function testCompatibility(string $super, string $sub, bool $expected): void + { + $superType = self::fromString($super, self::$scope); + $subType = self::fromString($sub, self::$scope); + + $message = $expected + ? sprintf('Expected "%s" to be a subtype of "%s", but it is not', $sub, $super) + : sprintf('Expected "%s" not to be a subtype of "%s", but it is', $sub, $super); + self::assertSame($expected, Compatibility::check($superType, $subType), $message); + } + + /** + * @return iterable + */ + public function compatibilityCases(): iterable + { + $compatibleTypes = self::compatibleTypes(); + foreach (self::types() as $super) { + foreach (self::types() as $sub) { + $compatibleTypesKey = array_search([$super, $sub], $compatibleTypes, true); + $expected = $compatibleTypesKey !== false; + $name = $expected + ? sprintf('%s is a subtype of %s', $sub, $super) + : sprintf('%s is not a subtype of %s', $sub, $super); + yield $name => [$super, $sub, $expected]; + if ($compatibleTypesKey === false) { + continue; + } + unset($compatibleTypes[$compatibleTypesKey]); + } + } + if ($compatibleTypes === []) { + return; + } + throw new LogicException( + sprintf( + "There are %s unchecked compatibility declarations:\n%s", + count($compatibleTypes), + implode( + "\n", + array_map(static function (array $compatibleType): string { + return sprintf('- `%s` is a subtype of `%s`', $compatibleType[1], $compatibleType[0]); + }, $compatibleTypes), + ), + ) + ); + } + + /** + * @dataProvider allTypes + */ + public function testEveryTypeIsCompatibleWithMixed(string $type): void + { + self::assertTrue( + Compatibility::check(new MixedType(), self::fromString($type, self::$scope)), + sprintf('Expected "%s" to be a subtype of "mixed", but it is not', $type), + ); + } + + /** + * @return iterable + */ + public function allTypes(): iterable + { + foreach (self::types() as $type) { + yield $type => [$type]; + } + } + + /** + * @dataProvider aliasCases + */ + public function testAliases(string $a, string $b, bool $expected): void + { + $aType = self::fromString($a, self::$scope); + $bType = self::fromString($b, self::$scope); + + $aContainsB = Compatibility::check($aType, $bType); + $bContainsA = Compatibility::check($bType, $aType); + $isAlias = $aContainsB && $bContainsA; + + $message = $expected + ? sprintf('Expected "%s" to be an alias of "%s", but it is not', $b, $a) + : sprintf('Expected "%s" not to be an alias of "%s", but it is', $b, $a); + self::assertSame($expected, $isAlias, $message); + } + + /** + * @return iterable + */ + public function aliasCases(): iterable + { + $aliases = self::aliases(); + $seen = []; + foreach (self::types() as $a) { + foreach (self::types() as $b) { + if ($a === $b) { + continue; + } + $tuple = [$a, $b]; + sort($tuple); + if (in_array($tuple, $seen, true)) { + continue; + } + $expected = array_search($tuple, $aliases, true) !== false; + $name = $expected + ? sprintf('%s is an alias of %s', $b, $a) + : sprintf('%s is not an alias of %s', $b, $a); + yield $name => [$a, $b, $expected]; + $seen[] = $tuple; + } + } + } +} diff --git a/tests/functional/InvalidTypesTest.php b/tests/functional/InvalidTypesTest.php new file mode 100644 index 0000000..89f65d6 --- /dev/null +++ b/tests/functional/InvalidTypesTest.php @@ -0,0 +1,83 @@ +register('Foo', new ClassLikeType('Foo')); + $scope->register('Bar', new ClassLikeType('Bar')); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage($expectedMessage); + + Type::fromString($typeString, $scope); + } + + /** + * @return iterable + */ + public function invalidTypes(): iterable + { + yield 'Boolean array key' => [ + 'array', + 'Can\'t use bool as array key. Only strings and integers are allowed.', + ]; + yield 'Iterable with three type parameters' => [ + 'iterable', + 'Iterable types must take one of the following forms: ' . + 'iterable, iterable, iterable', + ]; + yield 'Class string with two type parameters' => [ + 'class-string', + 'class-string takes zero or one type parameters', + ]; + yield 'List with two type parameters' => [ + 'list', + 'The list type takes exactly one type parameter, 2 (string, int) given', + ]; + yield 'Unknown identifier' => [ + 'my-imaginary-type', + 'Unknown type my-imaginary-type', + ]; + yield 'Unknown identifier with type parameters' => [ + 'my-imaginary-type', + 'Unknown type my-imaginary-type', + ]; + yield 'Map with three type parameters' => [ + 'array', + 'Array types must take one of the following forms: ' + . 'array, array, array. ' + . 'Got array', + ]; + yield 'Int with invalid minimum' => [ + 'int', + 'Invalid minimum value for int type: Foo. Must be an integer or "min".', + ]; + yield 'Int with invalid maximum' => [ + 'int<10, Foo>', + 'Invalid maximum value for int type: Foo. Must be an integer or "max".', + ]; + yield 'Int with a single type parameter' => [ + 'int<10>', + 'The int type takes exactly zero or two type parameters, 1 (10) given', + ]; + yield 'Int with three type parameters' => [ + 'int<10, 20, 30>', + 'The int type takes exactly zero or two type parameters, 3 (10, 20, 30) given', + ]; + } +} diff --git a/tests/functional/aliases.md b/tests/functional/aliases.md new file mode 100644 index 0000000..3130e40 --- /dev/null +++ b/tests/functional/aliases.md @@ -0,0 +1,37 @@ +- `array` is an alias of `array` +- `array` is an alias of `array` +- `array` is an alias of `array` +- `array` is an alias of `array` +- `array{name: string}` is an alias of `array{name: string} & array{name?: string}` +- `array{name: string, age: int}` is an alias of `array{name: string} & array{age: int}` +- `array{name: string, age?: int}` is an alias of `array{age?: int, name: string}` +- `int<23, 23>` is an alias of `23` +- `int<1, max>` is an alias of `positive-int` +- `int` is an alias of `negative-int` +- `iterable` is an alias of `iterable` +- `iterable` is an alias of `iterable` +- `iterable` is an alias of `iterable` +- `iterable` is an alias of `iterable` +- `string | int` is an alias of `int | string` +- `true | false` is an alias of `bool` +- `false | true` is an alias of `bool` +- `false | true` is an alias of `true | false` +- `bool | true` is an alias of `bool` +- `bool | true` is an alias of `true | false` +- `bool | true` is an alias of `false | true` +- `string | 'foo'` is an alias of `string` +- `string | list` is an alias of `list | string` + +- `array{age?: int, name: string}` is an alias of `array{name: string} & array{name?: string}` +- `array{name: string, age?: int}` is an alias of `array{name: string} & array{name?: string}` +- `array{name: string}` is an alias of `array{age?: int, name: string}` +- `array{name: string, age?: int}` is an alias of `array{name: string}` + +- `array{}` is an alias of `array` +- `array{}` is an alias of `array` +- `array{}` is an alias of `array` +- `array{}` is an alias of `list` +- `array{}` is an alias of `list` +- `array{}` is an alias of `list` +- `array{}` is an alias of `list` +- `array{}` is an alias of `list | list` diff --git a/tests/functional/compatible-types/bool.md b/tests/functional/compatible-types/bool.md new file mode 100644 index 0000000..4ad68cd --- /dev/null +++ b/tests/functional/compatible-types/bool.md @@ -0,0 +1,23 @@ +- `bool` is a subtype of `bool` +- `bool` is a subtype of `bool | true` +- `bool` is a subtype of `false | true` +- `bool` is a subtype of `scalar` +- `bool` is a subtype of `string | int | bool` +- `bool` is a subtype of `true | false` + +- `false` is a subtype of `bool` +- `false` is a subtype of `bool | true` +- `false` is a subtype of `false` +- `false` is a subtype of `false | list` +- `false` is a subtype of `false | true` +- `false` is a subtype of `scalar` +- `false` is a subtype of `string | int | bool` +- `false` is a subtype of `true | false` + +- `true` is a subtype of `bool` +- `true` is a subtype of `bool | true` +- `true` is a subtype of `false | true` +- `true` is a subtype of `true` +- `true` is a subtype of `scalar` +- `true` is a subtype of `string | int | bool` +- `true` is a subtype of `true | false` diff --git a/tests/functional/compatible-types/callable.md b/tests/functional/compatible-types/callable.md new file mode 100644 index 0000000..8e96806 --- /dev/null +++ b/tests/functional/compatible-types/callable.md @@ -0,0 +1,27 @@ +- `callable(): string` is a subtype of `callable(): string` +- `callable(): string` is a subtype of `callable(): void` + +- `callable(): void` is a subtype of `callable(): void` + +- `callable(string): float` is a subtype of `callable(string): float` + +- `callable(string): int` is a subtype of `callable(string): float` +- `callable(string): int` is a subtype of `callable(string): int` +- `callable(string): int` is a subtype of `callable(string, bool): int` +- `callable(string): int` is a subtype of `callable(string, int): int` + +- `callable(string=): int` is a subtype of `callable(string): float` +- `callable(string=): int` is a subtype of `callable(string): int` +- `callable(string=): int` is a subtype of `callable(string=): int` +- `callable(string=): int` is a subtype of `callable(string, bool): int` +- `callable(string=): int` is a subtype of `callable(string, int): int` + +- `callable(string, bool): int` is a subtype of `callable(string, bool): int` + +- `callable(string, int): int` is a subtype of `callable(string, int): int` + +- `callable(string | int): int` is a subtype of `callable(string): float` +- `callable(string | int): int` is a subtype of `callable(string): int` +- `callable(string | int): int` is a subtype of `callable(string, bool): int` +- `callable(string | int): int` is a subtype of `callable(string, int): int` +- `callable(string | int): int` is a subtype of `callable(string | int): int` diff --git a/tests/functional/compatible-types/class-like.md b/tests/functional/compatible-types/class-like.md new file mode 100644 index 0000000..ebdb95c --- /dev/null +++ b/tests/functional/compatible-types/class-like.md @@ -0,0 +1,2 @@ +- `RunnableAndLoggable` is a subtype of `RunnableAndLoggable` +- `RunnableAndLoggable` is a subtype of `Runnable & Loggable` diff --git a/tests/functional/compatible-types/int-literal.md b/tests/functional/compatible-types/int-literal.md new file mode 100644 index 0000000..ae6ed4f --- /dev/null +++ b/tests/functional/compatible-types/int-literal.md @@ -0,0 +1,60 @@ +- `-42` is a subtype of `-42` +- `-42` is a subtype of `float` +- `-42` is a subtype of `int` +- `-42` is a subtype of `int | string` +- `-42` is a subtype of `int<-123, 321>` +- `-42` is a subtype of `int` +- `-42` is a subtype of `int` +- `-42` is a subtype of `negative-int` +- `-42` is a subtype of `scalar` +- `-42` is a subtype of `string | int` +- `-42` is a subtype of `string | int | bool` + +- `-1` is a subtype of `-1` +- `-1` is a subtype of `float` +- `-1` is a subtype of `int` +- `-1` is a subtype of `int | string` +- `-1` is a subtype of `int<-123, 321>` +- `-1` is a subtype of `int` +- `-1` is a subtype of `int` +- `-1` is a subtype of `negative-int` +- `-1` is a subtype of `scalar` +- `-1` is a subtype of `string | int` +- `-1` is a subtype of `string | int | bool` + +- `0` is a subtype of `0` +- `0` is a subtype of `float` +- `0` is a subtype of `int` +- `0` is a subtype of `int | string` +- `0` is a subtype of `int<-123, 321>` +- `0` is a subtype of `int` +- `0` is a subtype of `scalar` +- `0` is a subtype of `string | int` +- `0` is a subtype of `string | int | bool` + +- `1` is a subtype of `1` +- `1` is a subtype of `float` +- `1` is a subtype of `int` +- `1` is a subtype of `int | string` +- `1` is a subtype of `int<-123, 321>` +- `1` is a subtype of `int<1, max>` +- `1` is a subtype of `int` +- `1` is a subtype of `positive-int` +- `1` is a subtype of `scalar` +- `1` is a subtype of `string | int` +- `1` is a subtype of `string | int | bool` + +- `23` is a subtype of `23` +- `23` is a subtype of `float` +- `23` is a subtype of `int` +- `23` is a subtype of `int | string` +- `23` is a subtype of `int<-123, 321>` +- `23` is a subtype of `int<1, max>` +- `23` is a subtype of `int<23, 23>` +- `23` is a subtype of `int<23, max>` +- `23` is a subtype of `int<23, 42>` +- `23` is a subtype of `int` +- `23` is a subtype of `positive-int` +- `23` is a subtype of `scalar` +- `23` is a subtype of `string | int` +- `23` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/int.md b/tests/functional/compatible-types/int.md new file mode 100644 index 0000000..1e0613a --- /dev/null +++ b/tests/functional/compatible-types/int.md @@ -0,0 +1,98 @@ +- `int` is a subtype of `float` +- `int` is a subtype of `int` +- `int` is a subtype of `int | string` +- `int` is a subtype of `scalar` +- `int` is a subtype of `string | int` +- `int` is a subtype of `string | int | bool` + +- `int<-123, 321>` is a subtype of `float` +- `int<-123, 321>` is a subtype of `int` +- `int<-123, 321>` is a subtype of `int | string` +- `int<-123, 321>` is a subtype of `int<-123, 321>` +- `int<-123, 321>` is a subtype of `scalar` +- `int<-123, 321>` is a subtype of `string | int` +- `int<-123, 321>` is a subtype of `string | int | bool` + +- `int<1, max>` is a subtype of `float` +- `int<1, max>` is a subtype of `int` +- `int<1, max>` is a subtype of `int | string` +- `int<1, max>` is a subtype of `int<1, max>` +- `int<1, max>` is a subtype of `positive-int` +- `int<1, max>` is a subtype of `scalar` +- `int<1, max>` is a subtype of `string | int` +- `int<1, max>` is a subtype of `string | int | bool` + +- `int<23, 23>` is a subtype of `23` +- `int<23, 23>` is a subtype of `float` +- `int<23, 23>` is a subtype of `int` +- `int<23, 23>` is a subtype of `int | string` +- `int<23, 23>` is a subtype of `int<-123, 321>` +- `int<23, 23>` is a subtype of `int<1, max>` +- `int<23, 23>` is a subtype of `int<23, 23>` +- `int<23, 23>` is a subtype of `int<23, 42>` +- `int<23, 23>` is a subtype of `int<23, max>` +- `int<23, 23>` is a subtype of `int` +- `int<23, 23>` is a subtype of `positive-int` +- `int<23, 23>` is a subtype of `scalar` +- `int<23, 23>` is a subtype of `string | int` +- `int<23, 23>` is a subtype of `string | int | bool` + +- `int<23, 42>` is a subtype of `float` +- `int<23, 42>` is a subtype of `int` +- `int<23, 42>` is a subtype of `int | string` +- `int<23, 42>` is a subtype of `int<-123, 321>` +- `int<23, 42>` is a subtype of `int<1, max>` +- `int<23, 42>` is a subtype of `int<23, 42>` +- `int<23, 42>` is a subtype of `int<23, max>` +- `int<23, 42>` is a subtype of `int` +- `int<23, 42>` is a subtype of `positive-int` +- `int<23, 42>` is a subtype of `scalar` +- `int<23, 42>` is a subtype of `string | int` +- `int<23, 42>` is a subtype of `string | int | bool` + +- `int<23, max>` is a subtype of `float` +- `int<23, max>` is a subtype of `int` +- `int<23, max>` is a subtype of `int | string` +- `int<23, max>` is a subtype of `int<1, max>` +- `int<23, max>` is a subtype of `int<23, max>` +- `int<23, max>` is a subtype of `positive-int` +- `int<23, max>` is a subtype of `scalar` +- `int<23, max>` is a subtype of `string | int` +- `int<23, max>` is a subtype of `string | int | bool` + +- `int` is a subtype of `float` +- `int` is a subtype of `int` +- `int` is a subtype of `int | string` +- `int` is a subtype of `int` +- `int` is a subtype of `int` +- `int` is a subtype of `negative-int` +- `int` is a subtype of `scalar` +- `int` is a subtype of `string | int` +- `int` is a subtype of `string | int | bool` + +- `int` is a subtype of `float` +- `int` is a subtype of `int` +- `int` is a subtype of `int | string` +- `int` is a subtype of `int` +- `int` is a subtype of `scalar` +- `int` is a subtype of `string | int` +- `int` is a subtype of `string | int | bool` + +- `negative-int` is a subtype of `float` +- `negative-int` is a subtype of `int` +- `negative-int` is a subtype of `int | string` +- `negative-int` is a subtype of `int` +- `negative-int` is a subtype of `int` +- `negative-int` is a subtype of `negative-int` +- `negative-int` is a subtype of `scalar` +- `negative-int` is a subtype of `string | int` +- `negative-int` is a subtype of `string | int | bool` + +- `positive-int` is a subtype of `float` +- `positive-int` is a subtype of `int` +- `positive-int` is a subtype of `int | string` +- `positive-int` is a subtype of `int<1, max>` +- `positive-int` is a subtype of `positive-int` +- `positive-int` is a subtype of `scalar` +- `positive-int` is a subtype of `string | int` +- `positive-int` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/intersection.md b/tests/functional/compatible-types/intersection.md new file mode 100644 index 0000000..decf5a8 --- /dev/null +++ b/tests/functional/compatible-types/intersection.md @@ -0,0 +1,30 @@ +- `array{name: string} & array{age: int}` is a subtype of `array` +- `array{name: string} & array{age: int}` is a subtype of `array` +- `array{name: string} & array{age: int}` is a subtype of `array` +- `array{name: string} & array{age: int}` is a subtype of `array<'name' | 'age', string | int>` +- `array{name: string} & array{age: int}` is a subtype of `array{}` +- `array{name: string} & array{age: int}` is a subtype of `array{age?: int, name: string}` +- `array{name: string} & array{age: int}` is a subtype of `array{name: string, age: int}` +- `array{name: string} & array{age: int}` is a subtype of `array{name: string, age?: int}` +- `array{name: string} & array{age: int}` is a subtype of `array{name: string}` +- `array{name: string} & array{age: int}` is a subtype of `array{name: string} & array{age: int}` +- `array{name: string} & array{age: int}` is a subtype of `array{name: string} & array{name?: string}` +- `array{name: string} & array{age: int}` is a subtype of `array` +- `array{name: string} & array{age: int}` is a subtype of `iterable` +- `array{name: string} & array{age: int}` is a subtype of `iterable` +- `array{name: string} & array{age: int}` is a subtype of `iterable` + +- `array{name: string} & array{name?: string}` is a subtype of `array` +- `array{name: string} & array{name?: string}` is a subtype of `array` +- `array{name: string} & array{name?: string}` is a subtype of `array` +- `array{name: string} & array{name?: string}` is a subtype of `array<'name' | 'age', string | int>` +- `array{name: string} & array{name?: string}` is a subtype of `array{}` +- `array{name: string} & array{name?: string}` is a subtype of `array{age?: int, name: string}` +- `array{name: string} & array{name?: string}` is a subtype of `array{name: string, age?: int}` +- `array{name: string} & array{name?: string}` is a subtype of `array{name: string}` +- `array{name: string} & array{name?: string}` is a subtype of `array{name: string} & array{name?: string}` +- `array{name: string} & array{name?: string}` is a subtype of `array` +- `array{name: string} & array{name?: string}` is a subtype of `iterable` +- `array{name: string} & array{name?: string}` is a subtype of `iterable` +- `array{name: string} & array{name?: string}` is a subtype of `iterable` +- `array{name: string} & array{name?: string}` is a subtype of `iterable` diff --git a/tests/functional/compatible-types/iterable.md b/tests/functional/compatible-types/iterable.md new file mode 100644 index 0000000..f0943f3 --- /dev/null +++ b/tests/functional/compatible-types/iterable.md @@ -0,0 +1,52 @@ +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md new file mode 100644 index 0000000..68f53b7 --- /dev/null +++ b/tests/functional/compatible-types/list.md @@ -0,0 +1,69 @@ +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array{}` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `list` +- `list` is a subtype of `list` +- `list` is a subtype of `list` +- `list` is a subtype of `list | list` + +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array{}` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `list` + +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array{}` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `list` + +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array{}` +- `list` is a subtype of `false | list` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `list` +- `list` is a subtype of `list` +- `list` is a subtype of `list | list` +- `list` is a subtype of `list | string` +- `list` is a subtype of `string | list` + +- `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `array{}` +- `non-empty-list` is a subtype of `false | list` +- `non-empty-list` is a subtype of `iterable` +- `non-empty-list` is a subtype of `iterable` +- `non-empty-list` is a subtype of `iterable` +- `non-empty-list` is a subtype of `iterable` +- `non-empty-list` is a subtype of `list` +- `non-empty-list` is a subtype of `list` +- `non-empty-list` is a subtype of `list | list` +- `non-empty-list` is a subtype of `list | string` +- `non-empty-list` is a subtype of `string | list` +- `non-empty-list` is a subtype of `non-empty-array` +- `non-empty-list` is a subtype of `non-empty-list` diff --git a/tests/functional/compatible-types/map.md b/tests/functional/compatible-types/map.md new file mode 100644 index 0000000..f1cca0b --- /dev/null +++ b/tests/functional/compatible-types/map.md @@ -0,0 +1,96 @@ +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array{}` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` + +- `array<'name' | 'age', string | int>` is a subtype of `array` +- `array<'name' | 'age', string | int>` is a subtype of `array<'name' | 'age', string | int>` +- `array<'name' | 'age', string | int>` is a subtype of `array` +- `array<'name' | 'age', string | int>` is a subtype of `array` +- `array<'name' | 'age', string | int>` is a subtype of `array` +- `array<'name' | 'age', string | int>` is a subtype of `array{}` +- `array<'name' | 'age', string | int>` is a subtype of `iterable` +- `array<'name' | 'age', string | int>` is a subtype of `iterable` +- `array<'name' | 'age', string | int>` is a subtype of `iterable` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array{}` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array{}` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array{}` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array{}` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array{}` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array{}` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array{}` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` + +- `non-empty-array` is a subtype of `array` +- `non-empty-array` is a subtype of `array` +- `non-empty-array` is a subtype of `array` +- `non-empty-array` is a subtype of `array` +- `non-empty-array` is a subtype of `array` +- `non-empty-array` is a subtype of `array{}` +- `non-empty-array` is a subtype of `iterable` +- `non-empty-array` is a subtype of `iterable` +- `non-empty-array` is a subtype of `iterable` +- `non-empty-array` is a subtype of `iterable` +- `non-empty-array` is a subtype of `non-empty-array` diff --git a/tests/functional/compatible-types/misc.md b/tests/functional/compatible-types/misc.md new file mode 100644 index 0000000..a0edafc --- /dev/null +++ b/tests/functional/compatible-types/misc.md @@ -0,0 +1,14 @@ +- `array{}` is a subtype of `array` +- `array{}` is a subtype of `array{}` +- `array{}` is a subtype of `array` +- `array{}` is a subtype of `array` +- `array{}` is a subtype of `iterable` +- `array{}` is a subtype of `iterable` +- `array{}` is a subtype of `iterable` + +- `float` is a subtype of `float` +- `float` is a subtype of `scalar` + +- `null` is a subtype of `null` + +- `scalar` is a subtype of `scalar` diff --git a/tests/functional/compatible-types/string-literal.md b/tests/functional/compatible-types/string-literal.md new file mode 100644 index 0000000..4ecb704 --- /dev/null +++ b/tests/functional/compatible-types/string-literal.md @@ -0,0 +1,45 @@ +- `''` is a subtype of `''` +- `''` is a subtype of `int | string` +- `''` is a subtype of `list | string` +- `''` is a subtype of `scalar` +- `''` is a subtype of `string` +- `''` is a subtype of `string | 'foo'` +- `''` is a subtype of `string | int` +- `''` is a subtype of `string | int | bool` +- `''` is a subtype of `string | list` + +- `'42'` is a subtype of `'42'` +- `'42'` is a subtype of `int | string` +- `'42'` is a subtype of `list | string` +- `'42'` is a subtype of `non-empty-string` +- `'42'` is a subtype of `numeric-string` +- `'42'` is a subtype of `scalar` +- `'42'` is a subtype of `string` +- `'42'` is a subtype of `string | 'foo'` +- `'42'` is a subtype of `string | int` +- `'42'` is a subtype of `string | int | bool` +- `'42'` is a subtype of `string | list` + +- `'foo'` is a subtype of `'foo'` +- `'foo'` is a subtype of `'foo' | 'bar'` +- `'foo'` is a subtype of `int | string` +- `'foo'` is a subtype of `list | string` +- `'foo'` is a subtype of `non-empty-string` +- `'foo'` is a subtype of `scalar` +- `'foo'` is a subtype of `string` +- `'foo'` is a subtype of `string | 'foo'` +- `'foo'` is a subtype of `string | int` +- `'foo'` is a subtype of `string | int | bool` +- `'foo'` is a subtype of `string | list` + +- `'bar'` is a subtype of `'bar'` +- `'bar'` is a subtype of `'foo' | 'bar'` +- `'bar'` is a subtype of `int | string` +- `'bar'` is a subtype of `list | string` +- `'bar'` is a subtype of `non-empty-string` +- `'bar'` is a subtype of `scalar` +- `'bar'` is a subtype of `string` +- `'bar'` is a subtype of `string | 'foo'` +- `'bar'` is a subtype of `string | int` +- `'bar'` is a subtype of `string | int | bool` +- `'bar'` is a subtype of `string | list` diff --git a/tests/functional/compatible-types/string.md b/tests/functional/compatible-types/string.md new file mode 100644 index 0000000..88e6725 --- /dev/null +++ b/tests/functional/compatible-types/string.md @@ -0,0 +1,65 @@ +- `class-string` is a subtype of `class-string` +- `class-string` is a subtype of `int | string` +- `class-string` is a subtype of `list | string` +- `class-string` is a subtype of `non-empty-string` +- `class-string` is a subtype of `scalar` +- `class-string` is a subtype of `string` +- `class-string` is a subtype of `string | 'foo'` +- `class-string` is a subtype of `string | int` +- `class-string` is a subtype of `string | int | bool` +- `class-string` is a subtype of `string | list` + +- `class-string` is a subtype of `class-string` +- `class-string` is a subtype of `class-string` +- `class-string` is a subtype of `class-string` +- `class-string` is a subtype of `int | string` +- `class-string` is a subtype of `list | string` +- `class-string` is a subtype of `non-empty-string` +- `class-string` is a subtype of `scalar` +- `class-string` is a subtype of `string` +- `class-string` is a subtype of `string | 'foo'` +- `class-string` is a subtype of `string | int` +- `class-string` is a subtype of `string | int | bool` +- `class-string` is a subtype of `string | list` + +- `class-string` is a subtype of `class-string` +- `class-string` is a subtype of `class-string` +- `class-string` is a subtype of `int | string` +- `class-string` is a subtype of `list | string` +- `class-string` is a subtype of `non-empty-string` +- `class-string` is a subtype of `scalar` +- `class-string` is a subtype of `string` +- `class-string` is a subtype of `string | 'foo'` +- `class-string` is a subtype of `string | int` +- `class-string` is a subtype of `string | int | bool` +- `class-string` is a subtype of `string | list` + +- `non-empty-string` is a subtype of `int | string` +- `non-empty-string` is a subtype of `list | string` +- `non-empty-string` is a subtype of `non-empty-string` +- `non-empty-string` is a subtype of `scalar` +- `non-empty-string` is a subtype of `string` +- `non-empty-string` is a subtype of `string | 'foo'` +- `non-empty-string` is a subtype of `string | int` +- `non-empty-string` is a subtype of `string | int | bool` +- `non-empty-string` is a subtype of `string | list` + +- `numeric-string` is a subtype of `int | string` +- `numeric-string` is a subtype of `list | string` +- `numeric-string` is a subtype of `non-empty-string` +- `numeric-string` is a subtype of `numeric-string` +- `numeric-string` is a subtype of `scalar` +- `numeric-string` is a subtype of `string` +- `numeric-string` is a subtype of `string | 'foo'` +- `numeric-string` is a subtype of `string | int` +- `numeric-string` is a subtype of `string | int | bool` +- `numeric-string` is a subtype of `string | list` + +- `string` is a subtype of `int | string` +- `string` is a subtype of `list | string` +- `string` is a subtype of `scalar` +- `string` is a subtype of `string` +- `string` is a subtype of `string | 'foo'` +- `string` is a subtype of `string | int` +- `string` is a subtype of `string | int | bool` +- `string` is a subtype of `string | list` diff --git a/tests/functional/compatible-types/struct.md b/tests/functional/compatible-types/struct.md new file mode 100644 index 0000000..a994566 --- /dev/null +++ b/tests/functional/compatible-types/struct.md @@ -0,0 +1,80 @@ +- `array{age?: int, name: string}` is a subtype of `array` +- `array{age?: int, name: string}` is a subtype of `array<'name' | 'age', string | int>` +- `array{age?: int, name: string}` is a subtype of `array` +- `array{age?: int, name: string}` is a subtype of `array` +- `array{age?: int, name: string}` is a subtype of `array` +- `array{age?: int, name: string}` is a subtype of `array{}` +- `array{age?: int, name: string}` is a subtype of `array{age?: int, name: string}` +- `array{age?: int, name: string}` is a subtype of `array{name: string}` +- `array{age?: int, name: string}` is a subtype of `array{name: string, age?: int}` +- `array{age?: int, name: string}` is a subtype of `array{name: string} & array{name?: string}` +- `array{age?: int, name: string}` is a subtype of `iterable` +- `array{age?: int, name: string}` is a subtype of `iterable` +- `array{age?: int, name: string}` is a subtype of `iterable` + +- `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array<'name' | 'age', string | int>` +- `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array{}` +- `array{name: int}` is a subtype of `array{name: int}` +- `array{name: int}` is a subtype of `iterable` +- `array{name: int}` is a subtype of `iterable` +- `array{name: int}` is a subtype of `iterable` + +- `array{name: string}` is a subtype of `array` +- `array{name: string}` is a subtype of `array<'name' | 'age', string | int>` +- `array{name: string}` is a subtype of `array` +- `array{name: string}` is a subtype of `array` +- `array{name: string}` is a subtype of `array` +- `array{name: string}` is a subtype of `array{}` +- `array{name: string}` is a subtype of `array{age?: int, name: string}` +- `array{name: string}` is a subtype of `array{name: string}` +- `array{name: string}` is a subtype of `array{name: string} & array{name?: string}` +- `array{name: string}` is a subtype of `array{name: string, age?: int}` +- `array{name: string}` is a subtype of `iterable` +- `array{name: string}` is a subtype of `iterable` +- `array{name: string}` is a subtype of `iterable` +- `array{name: string}` is a subtype of `iterable` + +- `array{name: string, age: int}` is a subtype of `array` +- `array{name: string, age: int}` is a subtype of `array<'name' | 'age', string | int>` +- `array{name: string, age: int}` is a subtype of `array` +- `array{name: string, age: int}` is a subtype of `array` +- `array{name: string, age: int}` is a subtype of `array` +- `array{name: string, age: int}` is a subtype of `array{}` +- `array{name: string, age: int}` is a subtype of `array{age?: int, name: string}` +- `array{name: string, age: int}` is a subtype of `array{name: string}` +- `array{name: string, age: int}` is a subtype of `array{name: string} & array{age: int}` +- `array{name: string, age: int}` is a subtype of `array{name: string} & array{name?: string}` +- `array{name: string, age: int}` is a subtype of `array{name: string, age: int}` +- `array{name: string, age: int}` is a subtype of `array{name: string, age?: int}` +- `array{name: string, age: int}` is a subtype of `iterable` +- `array{name: string, age: int}` is a subtype of `iterable` +- `array{name: string, age: int}` is a subtype of `iterable` + +- `array{name: string, age?: int}` is a subtype of `array` +- `array{name: string, age?: int}` is a subtype of `array<'name' | 'age', string | int>` +- `array{name: string, age?: int}` is a subtype of `array` +- `array{name: string, age?: int}` is a subtype of `array` +- `array{name: string, age?: int}` is a subtype of `array` +- `array{name: string, age?: int}` is a subtype of `array{}` +- `array{name: string, age?: int}` is a subtype of `array{age?: int, name: string}` +- `array{name: string, age?: int}` is a subtype of `array{name: string}` +- `array{name: string, age?: int}` is a subtype of `array{name: string, age?: int}` +- `array{name: string, age?: int}` is a subtype of `array{name: string} & array{name?: string}` +- `array{name: string, age?: int}` is a subtype of `iterable` +- `array{name: string, age?: int}` is a subtype of `iterable` +- `array{name: string, age?: int}` is a subtype of `iterable` + +- `array{obj: Foo}` is a subtype of `array` +- `array{obj: Foo}` is a subtype of `array` +- `array{obj: Foo}` is a subtype of `array` +- `array{obj: Foo}` is a subtype of `array{}` +- `array{obj: Foo}` is a subtype of `array{obj: Foo}` +- `array{obj: Foo}` is a subtype of `iterable` +- `array{obj: Foo}` is a subtype of `iterable` +- `array{obj: Foo}` is a subtype of `iterable` diff --git a/tests/functional/compatible-types/tuple.md b/tests/functional/compatible-types/tuple.md new file mode 100644 index 0000000..96acc93 --- /dev/null +++ b/tests/functional/compatible-types/tuple.md @@ -0,0 +1,61 @@ +- `array{}` is a subtype of `false | list` +- `array{}` is a subtype of `list` +- `array{}` is a subtype of `list` +- `array{}` is a subtype of `list` +- `array{}` is a subtype of `list` +- `array{}` is a subtype of `list | list` +- `array{}` is a subtype of `list | string` +- `array{}` is a subtype of `string | list` + +- `array{int, string}` is a subtype of `array` +- `array{int, string}` is a subtype of `array` +- `array{int, string}` is a subtype of `array` +- `array{int, string}` is a subtype of `array` +- `array{int, string}` is a subtype of `array{}` +- `array{int, string}` is a subtype of `array{int, string}` +- `array{int, string}` is a subtype of `iterable` +- `array{int, string}` is a subtype of `iterable` +- `array{int, string}` is a subtype of `iterable` +- `array{int, string}` is a subtype of `list` + +- `array{string, int}` is a subtype of `array` +- `array{string, int}` is a subtype of `array` +- `array{string, int}` is a subtype of `array` +- `array{string, int}` is a subtype of `array` +- `array{string, int}` is a subtype of `array{}` +- `array{string, int}` is a subtype of `array{string, int}` +- `array{string, int}` is a subtype of `iterable` +- `array{string, int}` is a subtype of `iterable` +- `array{string, int}` is a subtype of `iterable` +- `array{string, int}` is a subtype of `list` + +- `array{string, int, string}` is a subtype of `array` +- `array{string, int, string}` is a subtype of `array` +- `array{string, int, string}` is a subtype of `array` +- `array{string, int, string}` is a subtype of `array` +- `array{string, int, string}` is a subtype of `array{}` +- `array{string, int, string}` is a subtype of `array{string, int}` +- `array{string, int, string}` is a subtype of `array{string, int, string}` +- `array{string, int, string}` is a subtype of `iterable` +- `array{string, int, string}` is a subtype of `iterable` +- `array{string, int, string}` is a subtype of `iterable` +- `array{string, int, string}` is a subtype of `list` + +- `array{string, string}` is a subtype of `array` +- `array{string, string}` is a subtype of `array` +- `array{string, string}` is a subtype of `array` +- `array{string, string}` is a subtype of `array` +- `array{string, string}` is a subtype of `array` +- `array{string, string}` is a subtype of `array{}` +- `array{string, string}` is a subtype of `array{string, string}` +- `array{string, string}` is a subtype of `false | list` +- `array{string, string}` is a subtype of `iterable` +- `array{string, string}` is a subtype of `iterable` +- `array{string, string}` is a subtype of `iterable` +- `array{string, string}` is a subtype of `iterable` +- `array{string, string}` is a subtype of `list` +- `array{string, string}` is a subtype of `list` +- `array{string, string}` is a subtype of `list | list` +- `array{string, string}` is a subtype of `list | string` +- `array{string, string}` is a subtype of `non-empty-list` +- `array{string, string}` is a subtype of `string | list` diff --git a/tests/functional/compatible-types/union.md b/tests/functional/compatible-types/union.md new file mode 100644 index 0000000..f8181b0 --- /dev/null +++ b/tests/functional/compatible-types/union.md @@ -0,0 +1,72 @@ +- `'foo' | 'bar'` is a subtype of `'foo' | 'bar'` +- `'foo' | 'bar'` is a subtype of `int | string` +- `'foo' | 'bar'` is a subtype of `list | string` +- `'foo' | 'bar'` is a subtype of `non-empty-string` +- `'foo' | 'bar'` is a subtype of `scalar` +- `'foo' | 'bar'` is a subtype of `string` +- `'foo' | 'bar'` is a subtype of `string | 'foo'` +- `'foo' | 'bar'` is a subtype of `string | int` +- `'foo' | 'bar'` is a subtype of `string | int | bool` +- `'foo' | 'bar'` is a subtype of `string | list` + +- `bool | true` is a subtype of `bool` +- `bool | true` is a subtype of `bool | true` +- `bool | true` is a subtype of `false | true` +- `bool | true` is a subtype of `scalar` +- `bool | true` is a subtype of `string | int | bool` +- `bool | true` is a subtype of `true | false` + +- `false | list` is a subtype of `false | list` + +- `false | true` is a subtype of `bool` +- `false | true` is a subtype of `bool | true` +- `false | true` is a subtype of `false | true` +- `false | true` is a subtype of `scalar` +- `false | true` is a subtype of `string | int | bool` +- `false | true` is a subtype of `true | false` + +- `int | string` is a subtype of `int | string` +- `int | string` is a subtype of `scalar` +- `int | string` is a subtype of `string | int` +- `int | string` is a subtype of `string | int | bool` + +- `list | list` is a subtype of `array` +- `list | list` is a subtype of `array` +- `list | list` is a subtype of `array` +- `list | list` is a subtype of `array` +- `list | list` is a subtype of `array{}` +- `list | list` is a subtype of `list` +- `list | list` is a subtype of `list | list` +- `list | list` is a subtype of `iterable` +- `list | list` is a subtype of `iterable` +- `list | list` is a subtype of `iterable` + +- `list | string` is a subtype of `list | string` +- `list | string` is a subtype of `string | list` + +- `string | 'foo'` is a subtype of `int | string` +- `string | 'foo'` is a subtype of `list | string` +- `string | 'foo'` is a subtype of `scalar` +- `string | 'foo'` is a subtype of `string` +- `string | 'foo'` is a subtype of `string | 'foo'` +- `string | 'foo'` is a subtype of `string | int` +- `string | 'foo'` is a subtype of `string | int | bool` +- `string | 'foo'` is a subtype of `string | list` + +- `string | int` is a subtype of `int | string` +- `string | int` is a subtype of `scalar` +- `string | int` is a subtype of `string | int` +- `string | int` is a subtype of `string | int | bool` + +- `string | int | bool` is a subtype of `scalar` +- `string | int | bool` is a subtype of `string | int | bool` + +- `string | list` is a subtype of `list | string` +- `string | list` is a subtype of `string | list` + +- `true | false` is a subtype of `bool` +- `true | false` is a subtype of `bool | true` +- `true | false` is a subtype of `false | true` +- `true | false` is a subtype of `scalar` +- `true | false` is a subtype of `string | int | bool` +- `true | false` is a subtype of `true | false` diff --git a/tests/functional/types/bool.txt b/tests/functional/types/bool.txt new file mode 100644 index 0000000..f5399db --- /dev/null +++ b/tests/functional/types/bool.txt @@ -0,0 +1,3 @@ +bool +false +true diff --git a/tests/functional/types/callable.txt b/tests/functional/types/callable.txt new file mode 100644 index 0000000..362fe63 --- /dev/null +++ b/tests/functional/types/callable.txt @@ -0,0 +1,8 @@ +callable(): void +callable(): string +callable(string): float +callable(string): int +callable(string=): int +callable(string, bool): int +callable(string, int): int +callable(string | int): int diff --git a/tests/functional/types/class-like.txt b/tests/functional/types/class-like.txt new file mode 100644 index 0000000..d2c2c2a --- /dev/null +++ b/tests/functional/types/class-like.txt @@ -0,0 +1 @@ +RunnableAndLoggable diff --git a/tests/functional/types/int-literal.txt b/tests/functional/types/int-literal.txt new file mode 100644 index 0000000..4e45a34 --- /dev/null +++ b/tests/functional/types/int-literal.txt @@ -0,0 +1,5 @@ +-42 +-1 +0 +1 +23 diff --git a/tests/functional/types/int.txt b/tests/functional/types/int.txt new file mode 100644 index 0000000..a08a415 --- /dev/null +++ b/tests/functional/types/int.txt @@ -0,0 +1,10 @@ +int +int<-123, 321> +int<1, max> +int<23, 23> +int<23, 42> +int<23, max> +int +int +negative-int +positive-int diff --git a/tests/functional/types/intersection.txt b/tests/functional/types/intersection.txt new file mode 100644 index 0000000..9b61472 --- /dev/null +++ b/tests/functional/types/intersection.txt @@ -0,0 +1,3 @@ +array{name: string} & array{age: int} +array{name: string} & array{name?: string} +Runnable & Loggable diff --git a/tests/functional/types/iterable.txt b/tests/functional/types/iterable.txt new file mode 100644 index 0000000..bd74d86 --- /dev/null +++ b/tests/functional/types/iterable.txt @@ -0,0 +1,10 @@ +iterable +iterable +iterable +iterable +iterable +iterable +iterable +iterable +iterable +iterable diff --git a/tests/functional/types/list.txt b/tests/functional/types/list.txt new file mode 100644 index 0000000..ec1cdd2 --- /dev/null +++ b/tests/functional/types/list.txt @@ -0,0 +1,5 @@ +list +list +list +list +non-empty-list diff --git a/tests/functional/types/map.txt b/tests/functional/types/map.txt new file mode 100644 index 0000000..139d212 --- /dev/null +++ b/tests/functional/types/map.txt @@ -0,0 +1,10 @@ +array +array +array +array +array +array +array +array +array<'name' | 'age', string | int> +non-empty-array diff --git a/tests/functional/types/misc.txt b/tests/functional/types/misc.txt new file mode 100644 index 0000000..6922243 --- /dev/null +++ b/tests/functional/types/misc.txt @@ -0,0 +1,5 @@ +array{} +float +null +scalar +never diff --git a/tests/functional/types/string-literal.txt b/tests/functional/types/string-literal.txt new file mode 100644 index 0000000..85c8ff7 --- /dev/null +++ b/tests/functional/types/string-literal.txt @@ -0,0 +1,4 @@ +'' +'42' +'foo' +'bar' diff --git a/tests/functional/types/string.txt b/tests/functional/types/string.txt new file mode 100644 index 0000000..0a896e8 --- /dev/null +++ b/tests/functional/types/string.txt @@ -0,0 +1,6 @@ +class-string +class-string +class-string +non-empty-string +numeric-string +string diff --git a/tests/functional/types/struct.txt b/tests/functional/types/struct.txt new file mode 100644 index 0000000..6be54f1 --- /dev/null +++ b/tests/functional/types/struct.txt @@ -0,0 +1,6 @@ +array{age?: int, name: string} +array{name: int} +array{name: string} +array{name: string, age: int} +array{name: string, age?: int} +array{obj: Foo} diff --git a/tests/functional/types/tuple.txt b/tests/functional/types/tuple.txt new file mode 100644 index 0000000..5d6d4ee --- /dev/null +++ b/tests/functional/types/tuple.txt @@ -0,0 +1,4 @@ +array{int, string} +array{string, int} +array{string, int, string} +array{string, string} diff --git a/tests/functional/types/union.txt b/tests/functional/types/union.txt new file mode 100644 index 0000000..3db2d25 --- /dev/null +++ b/tests/functional/types/union.txt @@ -0,0 +1,12 @@ +int | string +string | int +string | int | bool +list | list +true | false +false | true +false | list +'foo' | 'bar' +bool | true +string | 'foo' +list | string +string | list diff --git a/tests/unit/CompatibilityTest.php b/tests/unit/CompatibilityTest.php new file mode 100644 index 0000000..b796961 --- /dev/null +++ b/tests/unit/CompatibilityTest.php @@ -0,0 +1,30 @@ +expectException(LogicException::class); + + Compatibility::check($super, new IntType()); + } +}