Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subtype check improvements #19

Merged
merged 5 commits into from
Aug 7, 2023
Merged
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
192 changes: 144 additions & 48 deletions src/Compiler/Type/PhpDocTypeUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
use ReflectionNamedType;
use ReflectionType;
use ReflectionUnionType;
use ShipMonk\InputMapper\Runtime\Optional;
use ShipMonk\InputMapper\Runtime\OptionalNone;
use ShipMonk\InputMapper\Runtime\OptionalSome;
use Traversable;
use function array_map;
use function constant;
Expand All @@ -49,6 +52,7 @@
use function max;
use function method_exists;
use function str_contains;
use function strcasecmp;
use function strtolower;

class PhpDocTypeUtils
Expand Down Expand Up @@ -266,6 +270,11 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool
$a = self::normalizeType($a);
$b = self::normalizeType($b);

// universal subtype
if ($a instanceof IdentifierTypeNode && $a->name === 'never') {
return true;
}

// expand complex types
if ($a instanceof UnionTypeNode) {
return Arrays::every($a->types, static fn(TypeNode $inner) => self::isSubTypeOf($inner, $b));
Expand All @@ -285,21 +294,25 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool

if ($b instanceof IdentifierTypeNode) {
if (!self::isKeyword($b)) {
return $a instanceof IdentifierTypeNode && is_a($a->name, $b->name, true);
return match (true) {
$a instanceof IdentifierTypeNode => is_a($a->name, $b->name, true),
$a instanceof GenericTypeNode => is_a($a->type->name, $b->name, true),
default => false,
};
}

return match (strtolower($b->name)) {
return match ($b->name) {
'array' => match (true) {
$a instanceof ArrayTypeNode => true,
$a instanceof ArrayShapeNode => true,
$a instanceof IdentifierTypeNode => in_array(strtolower($a->name), ['array', 'list'], true),
$a instanceof GenericTypeNode => in_array(strtolower($a->type->name), ['array', 'list'], true),
$a instanceof IdentifierTypeNode => in_array($a->name, ['array', 'list'], true),
$a instanceof GenericTypeNode => in_array($a->type->name, ['array', 'list'], true),
default => false,
},

'callable' => match (true) {
$a instanceof CallableTypeNode => true,
$a instanceof IdentifierTypeNode => self::isKeyword($a) ? strtolower($a->name) === 'callable' : method_exists($a->name, '__invoke'),
$a instanceof IdentifierTypeNode => self::isKeyword($a) ? $a->name === 'callable' : method_exists($a->name, '__invoke'),
$a instanceof ConstTypeNode => match (true) {
$a->constExpr instanceof ConstExprStringNode => is_callable($a->constExpr->value),
$a->constExpr instanceof ConstFetchNode => is_callable(constant((string) $a->constExpr)),
Expand All @@ -309,7 +322,7 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool
},

'false' => match (true) {
$a instanceof IdentifierTypeNode => strtolower($a->name) === 'false',
$a instanceof IdentifierTypeNode => $a->name === 'false',
$a instanceof ConstTypeNode => match (true) {
$a->constExpr instanceof ConstExprFalseNode => true,
$a->constExpr instanceof ConstFetchNode => constant((string) $a->constExpr) === false,
Expand All @@ -319,7 +332,7 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool
},

'float' => match (true) {
$a instanceof IdentifierTypeNode => strtolower($a->name) === 'float',
$a instanceof IdentifierTypeNode => $a->name === 'float',
$a instanceof ConstTypeNode => match (true) {
$a->constExpr instanceof ConstExprFloatNode => true,
$a->constExpr instanceof ConstFetchNode => is_float(constant((string) $a->constExpr)),
Expand All @@ -329,7 +342,7 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool
},

'int' => match (true) {
$a instanceof IdentifierTypeNode => strtolower($a->name) === 'int',
$a instanceof IdentifierTypeNode => $a->name === 'int',
$a instanceof ConstTypeNode => match (true) {
$a->constExpr instanceof ConstExprIntegerNode => true,
$a->constExpr instanceof ConstFetchNode => is_int(constant((string) $a->constExpr)),
Expand All @@ -340,17 +353,17 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool

'list' => match (true) {
$a instanceof ArrayShapeNode => Arrays::every($a->items, static fn(ArrayShapeItemNode $item, int $idx) => self::getArrayShapeKey($item) === (string) $idx),
$a instanceof IdentifierTypeNode => strtolower($a->name) === 'list',
$a instanceof GenericTypeNode => strtolower($a->type->name) === 'list',
$a instanceof IdentifierTypeNode => $a->name === 'list',
$a instanceof GenericTypeNode => $a->type->name === 'list',
default => false,
},

'mixed' => true,

'never' => $a instanceof IdentifierTypeNode && strtolower($a->name) === 'never',
'never' => $a instanceof IdentifierTypeNode && $a->name === 'never',

'null' => match (true) {
$a instanceof IdentifierTypeNode => strtolower($a->name) === 'null',
$a instanceof IdentifierTypeNode => $a->name === 'null',
$a instanceof ConstTypeNode => match (true) {
$a->constExpr instanceof ConstExprNullNode => true,
$a->constExpr instanceof ConstFetchNode => constant((string) $a->constExpr) === null,
Expand All @@ -361,15 +374,15 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool

'object' => match (true) {
$a instanceof ObjectShapeNode => true,
$a instanceof IdentifierTypeNode => strtolower($a->name) === 'object' || !self::isKeyword($a),
$a instanceof IdentifierTypeNode => $a->name === 'object' || !self::isKeyword($a),
$a instanceof GenericTypeNode => !self::isKeyword($a->type),
default => false,
},

'resource' => $a instanceof IdentifierTypeNode && strtolower($a->name) === 'resource',
'resource' => $a instanceof IdentifierTypeNode && $a->name === 'resource',

'string' => match (true) {
$a instanceof IdentifierTypeNode => strtolower($a->name) === 'string',
$a instanceof IdentifierTypeNode => $a->name === 'string',
$a instanceof ConstTypeNode => match (true) {
$a->constExpr instanceof ConstExprStringNode => true,
$a->constExpr instanceof ConstFetchNode => is_string(constant((string) $a->constExpr)),
Expand All @@ -379,7 +392,7 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool
},

'true' => match (true) {
$a instanceof IdentifierTypeNode => strtolower($a->name) === 'true',
$a instanceof IdentifierTypeNode => $a->name === 'true',
$a instanceof ConstTypeNode => match (true) {
$a->constExpr instanceof ConstExprTrueNode => true,
$a->constExpr instanceof ConstFetchNode => constant((string) $a->constExpr) === true,
Expand All @@ -388,41 +401,17 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool
default => false,
},

'void' => $a instanceof IdentifierTypeNode && strtolower($a->name) === 'void',
'void' => $a instanceof IdentifierTypeNode && $a->name === 'void',

default => false,
};
}

if ($b instanceof GenericTypeNode) {
return match (strtolower($b->type->name)) {
'array' => match (true) {
$a instanceof ArrayTypeNode => self::isSubTypeOf($a->type, $b->genericTypes[1]),
$a instanceof ArrayShapeNode => Arrays::every(
$a->items,
static fn(ArrayShapeItemNode $item) => (
self::isSubTypeOf(self::getArrayShapeKeyType($item), $b->genericTypes[0]) && self::isSubTypeOf($item->valueType, $b->genericTypes[1])
),
),
$a instanceof GenericTypeNode => match ($a->type->name) {
'array' => self::isSubTypeOf($a->genericTypes[0], $b->genericTypes[0]) && self::isSubTypeOf($a->genericTypes[1], $b->genericTypes[1]),
'list' => self::isSubTypeOf(new IdentifierTypeNode('int'), $b->genericTypes[0]) && self::isSubTypeOf($a->genericTypes[0], $b->genericTypes[1]),
default => false,
},
default => false,
},

'list' => match (true) {
$a instanceof ArrayShapeNode => Arrays::every($a->items, static fn(ArrayShapeItemNode $item, int $idx) => (
self::getArrayShapeKey($item) === (string) $idx && self::isSubTypeOf($item->valueType, $b->genericTypes[0])
)),
$a instanceof GenericTypeNode => match ($a->type->name) {
'list' => self::isSubTypeOf($a->genericTypes[0], $b->genericTypes[0]),
default => false,
},
default => false,
},

return match (true) {
$a instanceof GenericTypeNode => self::isSubTypeOfGeneric($a, $b),
$a instanceof IdentifierTypeNode => self::isSubTypeOfGeneric(new GenericTypeNode($a, []), $b),
$a instanceof ArrayShapeNode => self::isSubTypeOfGeneric(self::convertArrayShapeToGenericType($a), $b),
default => false,
};
}
Expand Down Expand Up @@ -459,6 +448,103 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool
return false;
}

private static function isSubTypeOfGeneric(GenericTypeNode $a, GenericTypeNode $b): bool
{
$typeDef = self::getGenericTypeDefinition($a);

if (strcasecmp($a->type->name, $b->type->name) === 0) {
return Arrays::every($typeDef['parameters'] ?? [], static function (array $parameter, int $idx) use ($a, $b): bool {
$genericTypeA = $a->genericTypes[$idx] ?? $parameter['bound'] ?? new IdentifierTypeNode('mixed');
$genericTypeB = $b->genericTypes[$idx] ?? $parameter['bound'] ?? new IdentifierTypeNode('mixed');

return match ($parameter['variance']) {
'in' => self::isSubTypeOf($genericTypeB, $genericTypeA),
'out' => self::isSubTypeOf($genericTypeA, $genericTypeB),
'inout' => self::isSubTypeOf($genericTypeA, $genericTypeB) && self::isSubTypeOf($genericTypeB, $genericTypeA),
default => throw new LogicException("Invalid variance {$parameter['variance']}"),
};
});
}

$superTypes = isset($typeDef['superTypes']) ? $typeDef['superTypes']($a->genericTypes) : [];
return Arrays::some($superTypes, static function (TypeNode $superType) use ($b): bool {
return self::isSubTypeOf($superType, $b);
});
}

/**
* @return array{
* superTypes?: callable(array<TypeNode>): list<TypeNode>,
* parameters?: list<array{variance: 'in' | 'out' | 'inout', bound?: TypeNode}>,
* }
*/
private static function getGenericTypeDefinition(GenericTypeNode $type): array
{
return match ($type->type->name) {
'array' => [
'superTypes' => static fn (array $types): array => [
new GenericTypeNode(new IdentifierTypeNode('iterable'), [
$types[0] ?? new IdentifierTypeNode('mixed'),
$types[1] ?? new IdentifierTypeNode('mixed'),
]),
],
'parameters' => [
['variance' => 'out'],
['variance' => 'out'],
],
],

'list' => [
'superTypes' => static fn (array $types): array => [
new GenericTypeNode(new IdentifierTypeNode('array'), [
new IdentifierTypeNode('int'),
$types[0] ?? new IdentifierTypeNode('mixed'),
]),
],
'parameters' => [
['variance' => 'out'],
],
],

Optional::class => [
'parameters' => [
['variance' => 'out'],
],
],

OptionalSome::class => [
'superTypes' => static fn (array $types): array => [
new GenericTypeNode(new IdentifierTypeNode(Optional::class), [
$types[0] ?? new IdentifierTypeNode('mixed'),
]),
],
'parameters' => [
['variance' => 'out'],
],
],

OptionalNone::class => [
'superTypes' => static fn (): array => [
new GenericTypeNode(new IdentifierTypeNode(Optional::class), [new IdentifierTypeNode('never')]),
],
],

default => [],
};
}

private static function convertArrayShapeToGenericType(ArrayShapeNode $type): GenericTypeNode
{
$valueType = new UnionTypeNode(Arrays::map($type->items, static fn(ArrayShapeItemNode $item) => $item->valueType));

if (Arrays::every($type->items, static fn(ArrayShapeItemNode $item, int $idx) => self::getArrayShapeKey($item) === (string) $idx)) {
return new GenericTypeNode(new IdentifierTypeNode('list'), [$valueType]);
}

$keyType = new UnionTypeNode(Arrays::map($type->items, self::getArrayShapeKeyType(...)));
return new GenericTypeNode(new IdentifierTypeNode('array'), [$keyType, $valueType]);
}

private static function getArrayShapeKey(ArrayShapeItemNode $item): string
{
if ($item->keyName instanceof ConstExprStringNode || $item->keyName instanceof ConstExprIntegerNode) {
Expand Down Expand Up @@ -499,7 +585,7 @@ private static function normalizeType(TypeNode $type): TypeNode
new IdentifierTypeNode('string'),
new IdentifierTypeNode('bool'),
]),
default => $type,
default => self::isKeyword($type) ? new IdentifierTypeNode(strtolower($type->name)) : $type,
};
}

Expand Down Expand Up @@ -550,8 +636,18 @@ private static function normalizeType(TypeNode $type): TypeNode
return new ArrayShapeNode($newItems, $type->sealed, $type->kind);
}

if ($type instanceof GenericTypeNode && $type->type->name === 'array' && count($type->genericTypes) === 1) {
return new GenericTypeNode(new IdentifierTypeNode('array'), [new IdentifierTypeNode('mixed'), self::normalizeType($type->genericTypes[0])]);
if ($type instanceof GenericTypeNode) {
if (strtolower($type->type->name) === 'array' && count($type->genericTypes) === 1) {
return new GenericTypeNode(new IdentifierTypeNode('array'), [new IdentifierTypeNode('mixed'), self::normalizeType($type->genericTypes[0])]);
}

if (strtolower($type->type->name) === 'iterable' && count($type->genericTypes) === 1) {
return new GenericTypeNode(new IdentifierTypeNode('iterable'), [new IdentifierTypeNode('mixed'), self::normalizeType($type->genericTypes[0])]);
}

if (self::isKeyword($type->type)) {
return new GenericTypeNode(new IdentifierTypeNode(strtolower($type->type->name)), $type->genericTypes);
}
}

return $type;
Expand Down
Loading
Loading