From c5aec57035946e73a2d8c8156b5cb37ef8852406 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 18 Jan 2022 19:56:09 +0100 Subject: [PATCH 1/4] Add the diff type --- src/DiffType.php | 25 +++++++++++++++++++++++++ src/DiffableInterface.php | 8 ++++++++ src/Parser.php | 3 +++ src/Scope.php | 1 + src/SetOperations.php | 18 ++++++++++++++++++ src/StructType.php | 22 +++++++++++++++++++++- src/UnionType.php | 17 ++++++++++++++++- tests/Functional/CompatibleTypes.md | 19 +++++++++++++++++++ tests/Functional/IncompatibleTypes.md | 15 +++++++++++++++ tests/Functional/ParseAndToString.md | 7 +++++++ 10 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 src/DiffType.php create mode 100644 src/DiffableInterface.php create mode 100644 src/SetOperations.php diff --git a/src/DiffType.php b/src/DiffType.php new file mode 100644 index 0000000..657dd35 --- /dev/null +++ b/src/DiffType.php @@ -0,0 +1,25 @@ +a . ', ' . $this->b . '>'; + } + + public function isSupertypeOf(TypeInterface $other): bool + { + return $this->a->isSupertypeOf($other) && !$this->b->isSupertypeOf($other); + } + + 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..434b148 --- /dev/null +++ b/src/DiffableInterface.php @@ -0,0 +1,8 @@ +getText(); assert($typeName !== null); + if ($typeName === 'diff') { + return SetOperations::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/SetOperations.php b/src/SetOperations.php new file mode 100644 index 0000000..c80d000 --- /dev/null +++ b/src/SetOperations.php @@ -0,0 +1,18 @@ +difference($b); + } + return new DiffType($a, $b); + } + + private function __construct() + { + } +} diff --git a/src/StructType.php b/src/StructType.php index b3ca8ef..47de6f6 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 @@ -81,4 +81,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]; + if ($otherFieldDefinition === null) { + $newFields[$key] = $fieldDefinition; + continue; + } + $newFields[$key] = [ + 'type' => SetOperations::difference($fieldDefinition['type'], $otherFieldDefinition['type']), + 'optional' => $fieldDefinition['optional'], + ]; + } + return self::create($newFields); + } } 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..ba9953b 100644 --- a/tests/Functional/ParseAndToString.md +++ b/tests/Functional/ParseAndToString.md @@ -88,3 +88,10 @@ - `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}` From ca86a02571372d2a648344ebda1359c5b480cb16 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 18 Jan 2022 20:00:42 +0100 Subject: [PATCH 2/4] Add a test case --- tests/Functional/ParseAndToString.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Functional/ParseAndToString.md b/tests/Functional/ParseAndToString.md index ba9953b..3e4809e 100644 --- a/tests/Functional/ParseAndToString.md +++ b/tests/Functional/ParseAndToString.md @@ -95,3 +95,4 @@ - `diff<23|'foo'|42|'bar', string>` -> `23|42` - `diff` -> `array{type: 'b', name: string}` - `diff` -> `array{name: diff}` +- `diff` -> `int` From 5ea95b8f49408a897bb7bb0bba279ae6cca86e4b Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 18 Jan 2022 20:10:55 +0100 Subject: [PATCH 3/4] Fix some code style and static analysis issues --- src/DiffType.php | 10 +++++++++- src/DiffableInterface.php | 5 +++++ src/SetOperations.php | 10 +++++++++- src/StructType.php | 3 ++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/DiffType.php b/src/DiffType.php index 657dd35..f2bd088 100644 --- a/src/DiffType.php +++ b/src/DiffType.php @@ -1,14 +1,19 @@ a . ', ' . $this->b . '>'; } @@ -18,6 +23,9 @@ 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 index 434b148..33a54b4 100644 --- a/src/DiffableInterface.php +++ b/src/DiffableInterface.php @@ -1,8 +1,13 @@ difference($b); + $diff = $a->difference($b); + if ($diff !== null) { + return $diff; + } } return new DiffType($a, $b); } diff --git a/src/StructType.php b/src/StructType.php index 47de6f6..caa3348 100644 --- a/src/StructType.php +++ b/src/StructType.php @@ -21,6 +21,7 @@ private function __construct(private array $fields) } /** + * @psalm-pure * @param array $fields */ public static function create(array $fields): self @@ -89,7 +90,7 @@ public function difference(TypeInterface $other): ?TypeInterface } $newFields = []; foreach ($this->fields as $key => $fieldDefinition) { - $otherFieldDefinition = $other->fields[$key]; + $otherFieldDefinition = $other->fields[$key] ?? null; if ($otherFieldDefinition === null) { $newFields[$key] = $fieldDefinition; continue; From a736ebbb66d92114e399c1546e8c99db8f5b5060 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 19 Jan 2022 18:28:26 +0100 Subject: [PATCH 4/4] diff -> string --- src/Parser.php | 2 +- src/StructType.php | 2 +- src/{SetOperations.php => TypeOperations.php} | 5 ++++- tests/Functional/ParseAndToString.md | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) rename src/{SetOperations.php => TypeOperations.php} (82%) diff --git a/src/Parser.php b/src/Parser.php index ea46232..3afbef1 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -111,7 +111,7 @@ private static function fromGeneric(SimpleExprContext $context, callable $resolv $typeName = $identifier->getText(); assert($typeName !== null); if ($typeName === 'diff') { - return SetOperations::difference(...$typeArguments); + return TypeOperations::difference(...$typeArguments); } return $resolve($typeName, $typeArguments); } diff --git a/src/StructType.php b/src/StructType.php index caa3348..93bdad6 100644 --- a/src/StructType.php +++ b/src/StructType.php @@ -96,7 +96,7 @@ public function difference(TypeInterface $other): ?TypeInterface continue; } $newFields[$key] = [ - 'type' => SetOperations::difference($fieldDefinition['type'], $otherFieldDefinition['type']), + 'type' => TypeOperations::difference($fieldDefinition['type'], $otherFieldDefinition['type']), 'optional' => $fieldDefinition['optional'], ]; } diff --git a/src/SetOperations.php b/src/TypeOperations.php similarity index 82% rename from src/SetOperations.php rename to src/TypeOperations.php index 9628400..eb3de44 100644 --- a/src/SetOperations.php +++ b/src/TypeOperations.php @@ -4,7 +4,7 @@ namespace PhpTypes; -final class SetOperations +final class TypeOperations { /** * @psalm-pure @@ -17,6 +17,9 @@ public static function difference(TypeInterface $a, TypeInterface $b): TypeInter return $diff; } } + if (!$a->isSupertypeOf($b)) { + return $a; + } return new DiffType($a, $b); } diff --git a/tests/Functional/ParseAndToString.md b/tests/Functional/ParseAndToString.md index 3e4809e..7054a8f 100644 --- a/tests/Functional/ParseAndToString.md +++ b/tests/Functional/ParseAndToString.md @@ -96,3 +96,4 @@ - `diff` -> `array{type: 'b', name: string}` - `diff` -> `array{name: diff}` - `diff` -> `int` +- `diff` -> `string`