diff --git a/src/DiffType.php b/src/DiffType.php new file mode 100644 index 0000000..f2bd088 --- /dev/null +++ b/src/DiffType.php @@ -0,0 +1,33 @@ +a . ', ' . $this->b . '>'; + } + + public function isSupertypeOf(TypeInterface $other): bool + { + return $this->a->isSupertypeOf($other) && !$this->b->isSupertypeOf($other); + } + + /** + * @param list $typeParameters + */ + public static function withTypeParameters(array $typeParameters): static + { + return new self($typeParameters[0], $typeParameters[1]); + } +} diff --git a/src/DiffableInterface.php b/src/DiffableInterface.php new file mode 100644 index 0000000..33a54b4 --- /dev/null +++ b/src/DiffableInterface.php @@ -0,0 +1,13 @@ +getText(); assert($typeName !== null); + if ($typeName === 'diff') { + return TypeOperations::difference(...$typeArguments); + } return $resolve($typeName, $typeArguments); } diff --git a/src/Scope.php b/src/Scope.php index 6de41ca..bc53707 100644 --- a/src/Scope.php +++ b/src/Scope.php @@ -47,6 +47,7 @@ private static function global(): self $global->register('iterable', IterableType::class); $global->register('list', ListType::class); $global->register('array', ArrayType::class); + $global->register('diff', DiffType::class); self::$global = $global; } return self::$global; diff --git a/src/StructType.php b/src/StructType.php index b3ca8ef..93bdad6 100644 --- a/src/StructType.php +++ b/src/StructType.php @@ -11,7 +11,7 @@ /** * @psalm-immutable */ -class StructType implements TypeInterface, KeyValueTypeInterface +class StructType implements TypeInterface, KeyValueTypeInterface, DiffableInterface { /** * @param array $fields @@ -21,6 +21,7 @@ private function __construct(private array $fields) } /** + * @psalm-pure * @param array $fields */ public static function create(array $fields): self @@ -81,4 +82,24 @@ public function getValueType(): TypeInterface } return UnionType::create($valueTypes); } + + public function difference(TypeInterface $other): ?TypeInterface + { + if (!$other instanceof self) { + return null; + } + $newFields = []; + foreach ($this->fields as $key => $fieldDefinition) { + $otherFieldDefinition = $other->fields[$key] ?? null; + if ($otherFieldDefinition === null) { + $newFields[$key] = $fieldDefinition; + continue; + } + $newFields[$key] = [ + 'type' => TypeOperations::difference($fieldDefinition['type'], $otherFieldDefinition['type']), + 'optional' => $fieldDefinition['optional'], + ]; + } + return self::create($newFields); + } } diff --git a/src/TypeOperations.php b/src/TypeOperations.php new file mode 100644 index 0000000..eb3de44 --- /dev/null +++ b/src/TypeOperations.php @@ -0,0 +1,29 @@ +difference($b); + if ($diff !== null) { + return $diff; + } + } + if (!$a->isSupertypeOf($b)) { + return $a; + } + return new DiffType($a, $b); + } + + private function __construct() + { + } +} diff --git a/src/UnionType.php b/src/UnionType.php index ce53ff9..8c3cbac 100644 --- a/src/UnionType.php +++ b/src/UnionType.php @@ -12,7 +12,7 @@ /** * @psalm-immutable */ -final class UnionType implements TypeInterface +final class UnionType implements TypeInterface, DiffableInterface { /** * @param list $alternatives @@ -116,4 +116,19 @@ public function allAreSubtypesOf(TypeInterface $supertype): bool } return true; } + + public function difference(TypeInterface $other): ?TypeInterface + { + $newAlternatives = []; + foreach ($this->alternatives as $alternative) { + if ($other->isSupertypeOf($alternative)) { + continue; + } + $newAlternatives[] = $alternative; + } + if (count($newAlternatives) === count($this->alternatives)) { + return $this; + } + return self::create($newAlternatives); + } } diff --git a/tests/Functional/CompatibleTypes.md b/tests/Functional/CompatibleTypes.md index 5faa4b5..088bb14 100644 --- a/tests/Functional/CompatibleTypes.md +++ b/tests/Functional/CompatibleTypes.md @@ -1,4 +1,5 @@ # Basic types + - `string` accepts `string` - `int` accepts `int` - `float` accepts `float` @@ -22,6 +23,7 @@ - `non-empty-string` accepts `non-empty-string` # Unions + - `string|int` accepts `string` - `string|int` accepts `int` - `string|float|int` accepts `float` @@ -32,6 +34,7 @@ - `string|null` accepts `null` # Callables + - `callable(string|int): void` accepts `callable(string): void` - `callable(): string|int` accepts `callable(): string` - `callable(): void` accepts `callable(): string` @@ -41,11 +44,13 @@ - `callable(): (int|string)` accepts `callable(): string` # Tuples + - `array{string, int}` accepts `array{string, int}` - `array{string}` accepts `array{string, int}` - `array{string, int|bool}` accepts `array{string, int}` # Structs + - `array{foo: string}` accepts `array{foo: string}` - `array{foo: string|int}` accepts `array{foo: string}` - `array{foo: string, bar: int}` accepts `array{foo: string, bar: int}` @@ -56,6 +61,7 @@ - `array{foo?: string}` accepts `array{foo?: string}` # String Literals + - `"test"` accepts `"test"` - `'test'` accepts `'test'` - `"test"` accepts `'test'` @@ -66,6 +72,7 @@ - `mixed` accepts `"test"` # Int Literals + - `0` accepts `0` - `int` accepts `0` - `1` accepts `1` @@ -78,6 +85,7 @@ - `int` accepts `-69` # Collections + - `list` accepts `list` - `array` accepts `list` - `iterable` accepts `array` @@ -96,9 +104,20 @@ - `list` accepts `list` # Classes + - `Foo` accepts `Foo` - `FooInterface` accepts `Foo` # Parens + - `(callable(): string)|string` accepts `string` - `(callable(): string)|string` accepts `callable(): string` + +# Difference + +- `diff` accepts `string` +- `diff` accepts `'foo'|'bar'` +- `diff` accepts `int` +- `diff` accepts `string` +- `diff` accepts `array` +- `diff` accepts `diff` diff --git a/tests/Functional/IncompatibleTypes.md b/tests/Functional/IncompatibleTypes.md index eb9aaf9..5733863 100644 --- a/tests/Functional/IncompatibleTypes.md +++ b/tests/Functional/IncompatibleTypes.md @@ -1,4 +1,5 @@ # Simple + - `int` doesn't accept `string` - `bool` doesn't accept `mixed` - `int` doesn't accept `string|int` @@ -12,22 +13,26 @@ - `non-empty-string` doesn't accept `''` # Union + - `int|string` doesn't accept `string|int|bool` - `string|float` doesn't accept `string|int` - `string|float` doesn't accept `bool` # Callable + - `callable(float): void` doesn't accept `callable(): void` - `callable(): string` doesn't accept `callable(): int` - `callable(float): void` doesn't accept `callable(string): void` - `callable(): void` doesn't accept `string` # Tuple + - `array{string, float}` doesn't accept `array{string}` - `array{string|int, float}` doesn't accept `array{float, float}` - `array{int, string}` doesn't accept `int` # Struct + - `array{foo: int}` doesn't accept `array{foo: string}` - `array{foo: string, bar: int}` doesn't accept `array{foo: string}` - `array{foo: string}` doesn't accept `array{foo?: string}` @@ -35,9 +40,11 @@ - `array{foo?: string, bar: int}` doesn't accept `array{bar: string}` # Intersection + - `array{foo: string}&array{bar: int}` doesn't accept `bool` # Literal + - `'test'` doesn't accept `string` - `"test"` doesn't accept `string` - `"bar"` doesn't accept `"foo"` @@ -45,6 +52,7 @@ - `27` doesn't accept `int` # Collections + - `array` doesn't accept `list` - `array` doesn't accept `iterable` - `iterable` doesn't accept `string` @@ -57,6 +65,13 @@ - `array` doesn't accept `array{foo: int}` # Classes + - `Foo` doesn't accept `FooInterface` - `FooInterface` doesn't accept `Popo` - `Foo` doesn't accept `string` + +# Difference + +- `diff` doesn't accept `'foo'` +- `diff` doesn't accept `int` +- `diff` doesn't accept `string` diff --git a/tests/Functional/ParseAndToString.md b/tests/Functional/ParseAndToString.md index 4d24918..7054a8f 100644 --- a/tests/Functional/ParseAndToString.md +++ b/tests/Functional/ParseAndToString.md @@ -88,3 +88,12 @@ - `iterable` -> `iterable` - `iterable` - `Foo` + +# Difference + +- `diff<'foo'|'bar'|'baz', 'bar'>` -> `'foo'|'baz'` +- `diff<23|'foo'|42|'bar', string>` -> `23|42` +- `diff` -> `array{type: 'b', name: string}` +- `diff` -> `array{name: diff}` +- `diff` -> `int` +- `diff` -> `string`