Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/DiffType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace PhpTypes;

/**
* @psalm-immutable
*/
final class DiffType implements TypeInterface, GenericTypeInterface
{
public function __construct(private TypeInterface $a, private TypeInterface $b)
{
}

public function __toString(): string
{
return 'diff<' . $this->a . ', ' . $this->b . '>';
}

public function isSupertypeOf(TypeInterface $other): bool
{
return $this->a->isSupertypeOf($other) && !$this->b->isSupertypeOf($other);
}

/**
* @param list<TypeInterface> $typeParameters
*/
public static function withTypeParameters(array $typeParameters): static
{
return new self($typeParameters[0], $typeParameters[1]);
}
}
13 changes: 13 additions & 0 deletions src/DiffableInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace PhpTypes;

interface DiffableInterface
{
/**
* @psalm-pure
*/
public function difference(TypeInterface $other): ?TypeInterface;
}
3 changes: 3 additions & 0 deletions src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ private static function fromGeneric(SimpleExprContext $context, callable $resolv
assert($identifier !== null);
$typeName = $identifier->getText();
assert($typeName !== null);
if ($typeName === 'diff') {
return TypeOperations::difference(...$typeArguments);
}
return $resolve($typeName, $typeArguments);
}

Expand Down
1 change: 1 addition & 0 deletions src/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
23 changes: 22 additions & 1 deletion src/StructType.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/**
* @psalm-immutable
*/
class StructType implements TypeInterface, KeyValueTypeInterface
class StructType implements TypeInterface, KeyValueTypeInterface, DiffableInterface
{
/**
* @param array<string, array{type: TypeInterface, optional: bool}> $fields
Expand All @@ -21,6 +21,7 @@ private function __construct(private array $fields)
}

/**
* @psalm-pure
* @param array<string, array{type: TypeInterface, optional: bool}> $fields
*/
public static function create(array $fields): self
Expand Down Expand Up @@ -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);
}
}
29 changes: 29 additions & 0 deletions src/TypeOperations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace PhpTypes;

final class TypeOperations
{
/**
* @psalm-pure
*/
public static function difference(TypeInterface $a, TypeInterface $b): TypeInterface
{
if ($a instanceof DiffableInterface) {
$diff = $a->difference($b);
if ($diff !== null) {
return $diff;
}
}
if (!$a->isSupertypeOf($b)) {
return $a;
}
return new DiffType($a, $b);
}

private function __construct()
{
}
}
17 changes: 16 additions & 1 deletion src/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
/**
* @psalm-immutable
*/
final class UnionType implements TypeInterface
final class UnionType implements TypeInterface, DiffableInterface
{
/**
* @param list<TypeInterface> $alternatives
Expand Down Expand Up @@ -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);
}
}
19 changes: 19 additions & 0 deletions tests/Functional/CompatibleTypes.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Basic types

- `string` accepts `string`
- `int` accepts `int`
- `float` accepts `float`
Expand All @@ -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`
Expand All @@ -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`
Expand All @@ -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}`
Expand All @@ -56,6 +61,7 @@
- `array{foo?: string}` accepts `array{foo?: string}`

# String Literals

- `"test"` accepts `"test"`
- `'test'` accepts `'test'`
- `"test"` accepts `'test'`
Expand All @@ -66,6 +72,7 @@
- `mixed` accepts `"test"`

# Int Literals

- `0` accepts `0`
- `int` accepts `0`
- `1` accepts `1`
Expand All @@ -78,6 +85,7 @@
- `int` accepts `-69`

# Collections

- `list<int>` accepts `list<int>`
- `array<int, string>` accepts `list<string>`
- `iterable<string>` accepts `array<string>`
Expand All @@ -96,9 +104,20 @@
- `list` accepts `list<mixed>`

# Classes

- `Foo` accepts `Foo`
- `FooInterface` accepts `Foo`

# Parens

- `(callable(): string)|string` accepts `string`
- `(callable(): string)|string` accepts `callable(): string`

# Difference

- `diff<string, 'foo'>` accepts `string`
- `diff<string, 'foo'>` accepts `'foo'|'bar'`
- `diff<mixed, float>` accepts `int`
- `diff<mixed, float>` accepts `string`
- `diff<mixed, float>` accepts `array<string, string>`
- `diff<string|int, 'foo'|'bar'>` accepts `diff<string|int, 'foo'|'bar'>`
15 changes: 15 additions & 0 deletions tests/Functional/IncompatibleTypes.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Simple

- `int` doesn't accept `string`
- `bool` doesn't accept `mixed`
- `int` doesn't accept `string|int`
Expand All @@ -12,39 +13,46 @@
- `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}`
- `array{foo: string}` doesn't accept `bool`
- `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"`
- `27` doesn't accept `23`
- `27` doesn't accept `int`

# Collections

- `array<string, string>` doesn't accept `list<string>`
- `array` doesn't accept `iterable`
- `iterable` doesn't accept `string`
Expand All @@ -57,6 +65,13 @@
- `array<string, string>` doesn't accept `array{foo: int}`

# Classes

- `Foo` doesn't accept `FooInterface`
- `FooInterface` doesn't accept `Popo`
- `Foo` doesn't accept `string`

# Difference

- `diff<string, 'foo'>` doesn't accept `'foo'`
- `diff<string, 'foo'>` doesn't accept `int`
- `diff<mixed, string>` doesn't accept `string`
9 changes: 9 additions & 0 deletions tests/Functional/ParseAndToString.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,12 @@
- `iterable<string>` -> `iterable<mixed, string>`
- `iterable<string, int>`
- `Foo`

# Difference

- `diff<'foo'|'bar'|'baz', 'bar'>` -> `'foo'|'baz'`
- `diff<23|'foo'|42|'bar', string>` -> `23|42`
- `diff<array{type: 'a', age: int}|array{type: 'b', name: string}, array{type: 'a'}>` -> `array{type: 'b', name: string}`
- `diff<array{name: string}, array{name: 'John'}>` -> `array{name: diff<string, 'John'>}`
- `diff<int|(callable(): int), callable>` -> `int`
- `diff<string, 42>` -> `string`