forked from laminas/laminas-code
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rewrote
CompositeType
and TypeGenerator
to leverage the new union…
…/intersection type abstractions Signed-off-by: Marco Pivetta <ocramius@gmail.com>
- Loading branch information
Showing
4 changed files
with
40 additions
and
164 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,159 +1,63 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Laminas\Code\Generator\TypeGenerator; | ||
|
||
use Laminas\Code\Generator\Exception\InvalidArgumentException; | ||
|
||
use function array_diff_key; | ||
use function array_filter; | ||
use function array_flip; | ||
use function array_map; | ||
use function assert; | ||
use function explode; | ||
use function implode; | ||
use function preg_match; | ||
use function sprintf; | ||
use function str_contains; | ||
use function str_ends_with; | ||
use function str_starts_with; | ||
use function substr; | ||
use function usort; | ||
|
||
/** | ||
* Represents a union/intersection type, as supported by PHP. | ||
* This means that this object can be composed of {@see AtomicType} or other {@see CompositeType} objects. | ||
* | ||
* @internal the {@see CompositeType} is an implementation detail of the type generator, | ||
* | ||
* @psalm-immutable | ||
* @final | ||
*/ | ||
final class CompositeType implements TypeInterface | ||
abstract class CompositeType | ||
{ | ||
public const UNION_SEPARATOR = '|'; | ||
public const INTERSECTION_SEPARATOR = '&'; | ||
|
||
/** | ||
* @param non-empty-list<TypeInterface> $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<IntersectionType|AtomicType> $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<AtomicType> $typesInIntersection */ | ||
$typesInIntersection = array_map(self::fromString(...), explode(self::INTERSECTION_SEPARATOR, $type)); | ||
|
||
return new self($types, $isIntersection); | ||
} | ||
|
||
/** | ||
* @return non-empty-list<TypeInterface> | ||
*/ | ||
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); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters