From aa62c5f9c00ec1eac9f7d569f20162178f79d2d2 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Thu, 8 Dec 2022 01:32:11 +0100 Subject: [PATCH] Rewrote `CompositeType` and `TypeGenerator` to leverage the new union/intersection type abstractions Signed-off-by: Marco Pivetta --- src/Generator/TypeGenerator.php | 7 +- src/Generator/TypeGenerator/CompositeType.php | 162 ++++-------------- src/Generator/TypeGenerator/TypeInterface.php | 32 ---- .../TypeGenerator/CompositeTypeTest.php | 3 + 4 files changed, 40 insertions(+), 164 deletions(-) delete mode 100644 src/Generator/TypeGenerator/TypeInterface.php diff --git a/src/Generator/TypeGenerator.php b/src/Generator/TypeGenerator.php index e75cb2a6..14b8ed0a 100644 --- a/src/Generator/TypeGenerator.php +++ b/src/Generator/TypeGenerator.php @@ -5,7 +5,8 @@ use Laminas\Code\Generator\Exception\InvalidArgumentException; use Laminas\Code\Generator\TypeGenerator\AtomicType; use Laminas\Code\Generator\TypeGenerator\CompositeType; -use Laminas\Code\Generator\TypeGenerator\TypeInterface; +use Laminas\Code\Generator\TypeGenerator\IntersectionType; +use Laminas\Code\Generator\TypeGenerator\UnionType; use ReflectionClass; use ReflectionIntersectionType; use ReflectionNamedType; @@ -132,7 +133,7 @@ public static function fromTypeString(string $type): self return new self(CompositeType::fromString($trimmedNullable)); } - private function __construct(private readonly TypeInterface $type, private readonly bool $nullable = false) + private function __construct(private readonly UnionType|IntersectionType|AtomicType $type, private readonly bool $nullable = false) { if ($nullable && $type instanceof AtomicType) { $type->assertCanBeStandaloneNullable(); @@ -165,7 +166,7 @@ public function equals(TypeGenerator $otherType): bool */ public function __toString(): string { - return $this->type->__toString(); + return $this->type->toString(); } /** diff --git a/src/Generator/TypeGenerator/CompositeType.php b/src/Generator/TypeGenerator/CompositeType.php index b48e824a..deb2d502 100644 --- a/src/Generator/TypeGenerator/CompositeType.php +++ b/src/Generator/TypeGenerator/CompositeType.php @@ -1,159 +1,63 @@ $types - */ - private function __construct(protected readonly array $types, private readonly bool $isIntersection) - { - } - /** @psalm-pure */ - public static function fromString(string $type): self + public static function fromString(string $type): UnionType|IntersectionType|AtomicType { - $types = []; - $isIntersection = false; - $separator = self::UNION_SEPARATOR; - - if (! str_contains($type, $separator)) { - $isIntersection = true; - $separator = self::INTERSECTION_SEPARATOR; - } - - foreach (explode($separator, $type) as $typeString) { - if (str_contains($typeString, self::INTERSECTION_SEPARATOR)) { - if (! str_starts_with($typeString, '(')) { - throw new InvalidArgumentException(sprintf( - 'Invalid intersection type "%s": missing opening parenthesis', - $typeString - )); - } - - if (! str_ends_with($typeString, ')')) { - throw new InvalidArgumentException(sprintf( - 'Invalid intersection type "%s": missing closing parenthesis', - $typeString - )); - } - - $types[] = self::fromString(substr($typeString, 1, -1)); - } else { - $types[] = AtomicType::fromString($typeString); + if (str_contains($type, self::UNION_SEPARATOR)) { + // This horrible regular expression verifies that union delimiters `|` are never contained + // in parentheses, and that all intersection `&` are contained in parentheses. It's simplistic, + // and it will crash with very large broken types, but that's sufficient for our **current** + // use-case. + // If this becomes more problematic, an actual parser is a better (although slower) alternative. + if (1 !== preg_match('/^(([|]|[^()&]+)+|(\(([&]|[^|()]+)\))+)+$/', $type)) { + throw new InvalidArgumentException(sprintf( + 'Invalid type syntax "%s": intersections in a union must be surrounded by "(" and ")"', + $type + )); } - } - - usort( - $types, - static function (TypeInterface $left, TypeInterface $right): int { - if ($left instanceof AtomicType && $right instanceof AtomicType) { - return [$left->sortIndex, $left->type] <=> [$right->sortIndex, $right->type]; - } - - return [$right instanceof self] <=> [$left instanceof self]; - } - ); - - foreach ($types as $index => $typeItem) { - if (! $typeItem instanceof AtomicType) { - continue; - } - - $otherTypes = array_diff_key($types, array_flip([$index])); - - assert([] !== $otherTypes, 'There are always 2 or more types in a union type'); - $otherTypes = array_filter($otherTypes, static fn (TypeInterface $type) => ! $type instanceof self); - - if ([] === $otherTypes) { - continue; - } + /** @var non-empty-list $typesInUnion */ + $typesInUnion = array_map( + self::fromString(...), + array_map( + static fn (string $type): string => trim($type, '()'), + explode(self::UNION_SEPARATOR, $type) + ) + ); - if ($isIntersection) { - $typeItem->assertCanIntersectWith($otherTypes); - } else { - $typeItem->assertCanUnionWith($otherTypes); - } + return new UnionType($typesInUnion); } + + if (str_contains($type, self::INTERSECTION_SEPARATOR)) { + /** @var non-empty-list $typesInIntersection */ + $typesInIntersection = array_map(self::fromString(...), explode(self::INTERSECTION_SEPARATOR, $type)); - return new self($types, $isIntersection); - } - - /** - * @return non-empty-list - */ - public function getTypes(): array - { - return $this->types; - } - - public function isIntersection(): bool - { - return $this->isIntersection; - } - - /** @return self::INTERSECTION_SEPARATOR|self::UNION_SEPARATOR */ - public function getSeparator(): string - { - return $this->isIntersection ? self::INTERSECTION_SEPARATOR : self::UNION_SEPARATOR; - } - - /** @return non-empty-string */ - public function __toString(): string - { - $typesAsStrings = array_map( - static function (TypeInterface $type): string { - $typeString = $type->__toString(); - - return $type instanceof self && $type->isIntersection() ? sprintf('(%s)', $typeString) : $typeString; - }, - $this->types - ); - - return implode($this->getSeparator(), $typesAsStrings); - } - - /** @return non-empty-string */ - public function fullyQualifiedName(): string - { - $typesAsStrings = array_map( - static function (TypeInterface $type): string { - $typeString = $type->fullyQualifiedName(); - - return $type instanceof self && $type->isIntersection() ? sprintf('(%s)', $typeString) : $typeString; - }, - $this->types - ); + return new IntersectionType($typesInIntersection); + } - return implode($this->getSeparator(), $typesAsStrings); + return AtomicType::fromString($type); } } diff --git a/src/Generator/TypeGenerator/TypeInterface.php b/src/Generator/TypeGenerator/TypeInterface.php deleted file mode 100644 index f91eb18c..00000000 --- a/src/Generator/TypeGenerator/TypeInterface.php +++ /dev/null @@ -1,32 +0,0 @@ -