diff --git a/composer.json b/composer.json index 9936c87..3a60bcb 100644 --- a/composer.json +++ b/composer.json @@ -11,11 +11,6 @@ "email": "niza@cego.dk" } ], - "autoload": { - "psr-4": { - "Cego\\phpstan\\": "src/" - } - }, "autoload-dev": { "psr-4": { "Test\\": "test/" diff --git a/extension.neon b/extension.neon index 33b46ee..11536cc 100644 --- a/extension.neon +++ b/extension.neon @@ -1,25 +1,6 @@ includes: - ./../../nunomaduro/larastan/extension.neon -services: - - - class: Cego\phpstan\SpatieLaravelData\Collectors\ConstructorCollector - tags: - - phpstan.collector - - - - class: Cego\phpstan\SpatieLaravelData\Collectors\FromCollector - tags: - - phpstan.collector - - - - class: Cego\phpstan\SpatieLaravelData\Collectors\CastCollector - tags: - - phpstan.collector - -rules: - - Cego\phpstan\SpatieLaravelData\Rules\ValidTypeRule - parameters: level: 8 reportUnmatchedIgnoredErrors: false diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/SpatieLaravelData/Collectors/CastCollector.php b/src/SpatieLaravelData/Collectors/CastCollector.php deleted file mode 100644 index e9cc4ab..0000000 --- a/src/SpatieLaravelData/Collectors/CastCollector.php +++ /dev/null @@ -1,97 +0,0 @@ -> - */ -class CastCollector implements Collector -{ - /** - * Returns the node type, this collector operates on - * - * @phpstan-return class-string - */ - public function getNodeType(): string - { - return InClassMethodNode::class; - } - - /** - * Process the nodes and stores value in the collector instance - * - * @phpstan-param StaticCall $node - * - * @throws ShouldNotHappenException - * - * @return string|null Collected data - */ - public function processNode(Node $node, Scope $scope): ?string - { - // Skip wrong nodes - if ( ! $node instanceof InClassMethodNode) { - return null; - } - - // Skip wrong methods - if ($this->isNotCastMethod($node)) { - return null; - } - - $variant = ParametersAcceptorSelector::selectSingle($node->getMethodReflection()->getVariants()); - $returnType = $variant->getReturnType(); - - return Str::of($returnType->describe(VerbosityLevel::typeOnly())) - // Get individual union types - ->explode('|') - // Get individual intersection types - ->map(fn (string $type) => Str::of($type)->explode('&')) - // For each intersection type (which might be an intersection of 1 item) - // Only keep cast information for classes / interfaces - ->map(function (Collection $intersectionTypes) { - $classTypes = $intersectionTypes - // We only care about classes / interfaces - ->filter(fn (string $type) => class_exists($type) || interface_exists($type)) - // We do not care for the uncastable class - ->reject(fn (string $type) => is_a($type, Uncastable::class, true)); - - // We only support intersection types of explicit classes / interfaces. - if ($intersectionTypes->count() !== $classTypes->count()) { - return []; - } - - return $classTypes->all(); - }) - // Remove any intersection types we have deemed unfit - ->reject(fn (array $collection) => empty($collection)) - ->pipe(UnionType::fromRaw(...)) - ->toString(); - } - - /** - * Returns true if the given node is not the cast method of a Cast class - * - * @param InClassMethodNode $node - * - * @return bool - */ - private function isNotCastMethod(InClassMethodNode $node): bool - { - return $node->getMethodReflection()->getName() !== 'cast' - || ! $node->getMethodReflection()->getDeclaringClass()->implementsInterface(Cast::class); - } -} diff --git a/src/SpatieLaravelData/Collectors/ConstructorCollector.php b/src/SpatieLaravelData/Collectors/ConstructorCollector.php deleted file mode 100644 index 37120b5..0000000 --- a/src/SpatieLaravelData/Collectors/ConstructorCollector.php +++ /dev/null @@ -1,183 +0,0 @@ ->> - */ -class ConstructorCollector implements Collector -{ - /** - * Returns the node type, this collector operates on - * - * @phpstan-return class-string - */ - public function getNodeType(): string - { - return InClassMethodNode::class; - } - - /** - * Process the nodes and stores value in the collector instance - * - * @phpstan-param InClassMethodNode $node - * - * @return string|null Collected data - */ - public function processNode(Node $node, Scope $scope): ?string - { - if ( ! $node instanceof InClassMethodNode) { - return null; - } - - if ($this->isNotSpatieLaravelDataConstructor($node)) { - return null; - } - - return serialize(new Constructor( - $node->getMethodReflection()->getDeclaringClass()->getName(), - collect($node->getOriginalNode()->getParams())->map($this->getParameterTypes(...))->all() - )); - } - - /** - * Returns a key-value mapping of the parameter name and its allowed types - * - * @param Param $parameter - * - * @return KeyTypePair - */ - private function getParameterTypes(Param $parameter): KeyTypePair - { - return new KeyTypePair( - $this->getParameterName($parameter), - UnionType::fromRaw($this->parseType($parameter->type)), - ); - } - - /** - * @param null|Identifier|Name|ComplexType $type - * - * @return array> - */ - private function parseType($type): array - { - // If no type is defined, then return mixed. - if ($type === null) { - return [['mixed']]; - } - - // Simple type (int, string, bool) - if ($type instanceof Identifier) { - return [[$type->name]]; - } - - // Class types - if ($type instanceof Name) { - // We do not support special type checking (self, parent, static) - // since we are unlikely to use this feature, - // and implementing it is currently not straight forward. - if ($type->isSpecialClassName()) { - return [['mixed']]; - } - - return [[$type->toCodeString()]]; - } - - // Complex types - if ($type instanceof Node\ComplexType) { - if ($type instanceof Node\NullableType) { - return [ - ...$this->parseType($type->type), - ['null'], - ]; - } - - if ($type instanceof Node\UnionType) { - return collect($type->types) - ->map(fn ($unionType) => $this->parseType($unionType)) - ->flatten(1) - ->all(); - } - - if ($type instanceof Node\IntersectionType) { - return [ - collect($type->types) - ->map(fn ($intersectionType) => $this->parseType($intersectionType)) - ->flatten(2) - ->all(), - ]; - } - } - - return [['mixed']]; - } - - /** - * Returns the name of the given parameter - * - * @param Param $parameter - * - * @return string - */ - private function getParameterName(Param $parameter): string - { - if ( ! is_string($parameter->var->name)) { - throw new RuntimeException('A constructor property name cannot be an expression'); - } - - return $parameter->var->name; - } - - /** - * Returns true if the given node is not a laravel data constructor - * - * @param InClassMethodNode $node - * - * @return bool - */ - private function isNotSpatieLaravelDataConstructor(InClassMethodNode $node): bool - { - return $this->isNotConstructor($node) - || $this->isNotSpatieLaravelDataClass($node->getMethodReflection()->getDeclaringClass()); - } - - /** - * Returns true if the given node is not a constructor class - * - * @param InClassMethodNode $node - * - * @return bool - */ - private function isNotConstructor(InClassMethodNode $node): bool - { - return $node->getMethodReflection()->getName() !== '__construct'; - } - - /** - * Returns true if the given class is not a laravel data class - * - * @param ClassReflection $class - * - * @return bool - */ - private function isNotSpatieLaravelDataClass(ClassReflection $class): bool - { - return ! in_array(Data::class, $class->getParentClassesNames(), true); - } -} diff --git a/src/SpatieLaravelData/Collectors/FromCollector.php b/src/SpatieLaravelData/Collectors/FromCollector.php deleted file mode 100644 index 6163c53..0000000 --- a/src/SpatieLaravelData/Collectors/FromCollector.php +++ /dev/null @@ -1,123 +0,0 @@ -> - */ -class FromCollector implements Collector -{ - /** - * Returns the node type, this collector operates on - * - * @phpstan-return class-string - */ - public function getNodeType(): string - { - return StaticCall::class; - } - - /** - * Process the nodes and stores value in the collector instance - * - * @phpstan-param StaticCall $node - * - * @return string|null Collected data - */ - public function processNode(Node $node, Scope $scope): ?string - { - if ( ! $node instanceof StaticCall) { - return null; - } - - if ($this->isNotSpatieLaravelDataFromCall($node, $scope)) { - return null; - } - - $types = []; - - foreach ($node->args as $arg) { - if ($arg instanceof Node\VariadicPlaceholder) { - continue; - } - - if ( ! $arg->value instanceof Array_) { - continue; - } - - $argData = []; - - foreach ($arg->value->items as $item) { - if ( ! $item->key instanceof Node\Scalar\String_) { - continue; - } - - $type = $scope->getType($item->value); - - if ($type instanceof ConstantScalarType) { - $argData[] = new KeyTypePair($item->key->value, UnionType::fromString(get_debug_type($type->getValue()))); - } else { - $argData[] = new KeyTypePair($item->key->value, UnionType::fromString($scope->getType($item->value)->describe(VerbosityLevel::typeOnly()))); - } - } - - $types[] = $argData; - } - - return serialize(new Call( - $this->getTargetClass($node, $scope), - $types, - new Method( - $scope->getFile(), - $node->getLine(), - ) - )); - } - - /** - * Returns true if the given node is not a laravel data class static ::From call - * - * @param StaticCall $node - * @param Scope $scope - * - * @return bool - */ - private function isNotSpatieLaravelDataFromCall(StaticCall $node, Scope $scope): bool - { - if (strtolower($node->name->name) !== 'from') { - return true; - } - - return ! is_a($this->getTargetClass($node, $scope), Data::class, true); - } - - /** - * Returns the target / result class of the given static call - * - * @param StaticCall $node - * @param Scope $scope - * - * @return string - */ - private function getTargetClass(StaticCall $node, Scope $scope) - { - if ($node->class instanceof Node\Expr) { - return $scope->getType($node->class)->getReferencedClasses()[0]; - } - - return $scope->resolveName($node->class); - } -} diff --git a/src/SpatieLaravelData/Data/Call.php b/src/SpatieLaravelData/Data/Call.php deleted file mode 100644 index a7a6f26..0000000 --- a/src/SpatieLaravelData/Data/Call.php +++ /dev/null @@ -1,48 +0,0 @@ -> $arrayArguments - * @param Method $method - */ - public function __construct( - public readonly string $target, - public readonly array $arrayArguments, - public readonly Method $method, - ) { - } - - /** - * Returns array containing all the necessary state of the object. - */ - public function __serialize(): array - { - return [ - 'target' => $this->target, - 'method' => serialize($this->method), - 'arrayArguments' => serialize($this->arrayArguments), - ]; - } - - /** - * Restores the object state from the given data array. - * - * @param array $data - */ - public function __unserialize(array $data): void - { - $this->target = $data['target']; - $this->method = Method::unserialize($data['method']); - $this->arrayArguments = unserialize($data['arrayArguments'], ['allowed_classes' => [KeyTypePair::class]]); - } -} diff --git a/src/SpatieLaravelData/Data/Constructor.php b/src/SpatieLaravelData/Data/Constructor.php deleted file mode 100644 index 6f6d79f..0000000 --- a/src/SpatieLaravelData/Data/Constructor.php +++ /dev/null @@ -1,53 +0,0 @@ - $properties - */ - public function __construct( - public readonly string $class, - array $properties - ) { - $this->properties = collect($properties)->keyBy('key')->all(); - } - - /** - * Returns array containing all the necessary state of the object. - * - * @since 7.4 - * @link https://wiki.php.net/rfc/custom_object_serialization - */ - public function __serialize(): array - { - return [ - 'class' => $this->class, - 'properties' => serialize($this->properties), - ]; - } - - /** - * Restores the object state from the given data array. - * - * @param array $data - * - * @since 7.4 - * @link https://wiki.php.net/rfc/custom_object_serialization - */ - public function __unserialize(array $data): void - { - $this->class = $data['class']; - $this->properties = unserialize($data['properties'], ['allowed_classes' => [KeyTypePair::class]]); - } -} diff --git a/src/SpatieLaravelData/Data/KeyTypePair.php b/src/SpatieLaravelData/Data/KeyTypePair.php deleted file mode 100644 index ceab129..0000000 --- a/src/SpatieLaravelData/Data/KeyTypePair.php +++ /dev/null @@ -1,45 +0,0 @@ - $this->key, - 'type' => serialize($this->type), - ]; - } - - /** - * Restores the object state from the given data array. - * - * @param array $data - */ - public function __unserialize(array $data): void - { - $this->key = $data['key']; - $this->type = UnionType::unserialize($data['type']); - } -} diff --git a/src/SpatieLaravelData/Data/Method.php b/src/SpatieLaravelData/Data/Method.php deleted file mode 100644 index 3320471..0000000 --- a/src/SpatieLaravelData/Data/Method.php +++ /dev/null @@ -1,44 +0,0 @@ - $this->file, - 'line' => $this->line, - ]; - } - - /** - * Restores the object state from the given data array. - * - * @param array $data - */ - public function __unserialize(array $data): void - { - $this->file = $data['file']; - $this->line = $data['line']; - } -} diff --git a/src/SpatieLaravelData/Rules/ValidTypeRule.php b/src/SpatieLaravelData/Rules/ValidTypeRule.php deleted file mode 100644 index 3c213e4..0000000 --- a/src/SpatieLaravelData/Rules/ValidTypeRule.php +++ /dev/null @@ -1,196 +0,0 @@ - - */ - public function getNodeType(): string - { - return CollectedDataNode::class; - } - - /** - * Processes the given node - * - * @phpstan-param TNodeType $node - * - * @return (string|RuleError)[] errors - */ - public function processNode(Node $node, Scope $scope): array - { - if ( ! $node instanceof CollectedDataNode) { - return []; - } - - $castCollector = collect($node->get(CastCollector::class)) - ->flatten() - ->map(UnionType::fromString(...)) - ->reject(fn (UnionType $unionType) => $unionType->isMixed()) - ->values() - ->all(); - - $classCollector = collect($node->get(ConstructorCollector::class)) - ->flatten(1) - ->map(Constructor::unserialize(...)) - ->keyBy('class') - ->all(); - - return collect($node->get(FromCollector::class)) - // Flatten from a list of calls pr. file, to just a list of calls. - ->flatten(1) - ->map(Call::unserialize(...)) - // Make sure the rule does not crash in case a specific data class has not been analyzed - This can happen with data classes in the vendor folder - ->filter(fn (Call $call) => isset($classCollector[$call->target])) - // Check each call for errors - ->map(fn (Call $call) => $this->compareTypes($call, $castCollector, $classCollector[$call->target])) - // Flatten from a list of errors pr. call, to just a list of errors. - ->flatten() - // To array, so PhpStan can serialize the data. - ->all(); - } - - /** - * Compares the types of the specific call, with the constructor which would be expected - * - * @param Call $call - * @param list $casts - * @param Constructor $constructor - * - * @throws ShouldNotHappenException - * - * @return array - */ - private function compareTypes(Call $call, array $casts, Constructor $constructor): array - { - $errors = []; - - foreach ($call->arrayArguments as $arrayList) { - foreach ($arrayList as $type) { - // Ignore any additional data, since it does not matter - if ( ! isset($constructor->properties[$type->key])) { - continue; - } - - $error = $this->checkType($call, $type->key, $type->type, $casts, $constructor->properties[$type->key]->type); - - if ($error !== null) { - $errors[] = $error; - } - } - } - - return $errors; - } - - /** - * Checks the specific type for a single key, with the expected types of that key. - * - * @param Call $call - * @param string $key - * @param UnionType $actualType - * @param list $casts - * @param UnionType $expectedType - * - * @throws ShouldNotHappenException - * - * @return RuleError|null - */ - private function checkType(Call $call, string $key, UnionType $actualType, array $casts, UnionType $expectedType): ?RuleError - { - // Casters cannot cast nullable values, so quick test for type error - // is simply to check if the expected type accepts null values or not. - if ($actualType->isNullable() && $expectedType->isNotNullable()) { - return $this->buildError($call, $key, $expectedType, $actualType); - } - - // Otherwise, ignore cases where there exists a cast - since we cannot analyse them in dept. - if ($this->expectedTypesMatchesExactlyCast($casts, $expectedType)) { - return null; - } - - // Run full type inspection and return any errors found. - if ( ! TypeSystem::isSubtypeOf($actualType, $expectedType)) { - return $this->buildError($call, $key, $expectedType, $actualType); - } - - return null; - } - - /** - * Builds a RuleError instance - * - * @param Call $call - * @param string $key - * @param string $expectedType - * @param string $actualType - * - * @throws ShouldNotHappenException - * - * @return RuleError - */ - private function buildError(Call $call, string $key, string $expectedType, string $actualType): RuleError - { - return RuleErrorBuilder::message(self::getErrorMessage($key, $call->target, $expectedType, $actualType)) - ->line($call->method->line) - ->file($call->method->file) - ->tip('This is a custom CEGO rule, if you found a bug fix it in the cego/phpstan project') - ->build(); - } - - /** - * Returns the error message to give the developer on errors - * - * @param string $property - * @param string $class - * @param string $expectedType - * @param string $actualType - * - * @return string - */ - public static function getErrorMessage(string $property, string $class, string $expectedType, string $actualType): string - { - return sprintf('Argument $%s for %s::__construct() expects type [%s] but [%s] was given', $property, $class, $expectedType, $actualType); - } - - /** - * Returns true if a cast exists exactly for the expected types. - * - * @param list $casts - * @param UnionType $expectedTypes - * - * @return bool - */ - private function expectedTypesMatchesExactlyCast(array $casts, UnionType $expectedTypes): bool - { - $systemCasts = [UnionType::fromString(DataCollection::class)]; - - foreach ([...$casts, ...$systemCasts] as $castType) { - if (TypeSystem::isSubtypeOf($expectedTypes, $castType)) { - return true; - } - } - - return false; - } -} diff --git a/src/SpatieLaravelData/Traits/UnserializesSelf.php b/src/SpatieLaravelData/Traits/UnserializesSelf.php deleted file mode 100644 index efb9df7..0000000 --- a/src/SpatieLaravelData/Traits/UnserializesSelf.php +++ /dev/null @@ -1,11 +0,0 @@ - [__CLASS__]]); - } -} diff --git a/src/TypeSystem/IntersectionType.php b/src/TypeSystem/IntersectionType.php deleted file mode 100644 index 4c8bc8e..0000000 --- a/src/TypeSystem/IntersectionType.php +++ /dev/null @@ -1,113 +0,0 @@ - - */ - public array $types = []; - - /** - * Constructor - * - * @param Type[] $types - */ - public function __construct(array $types) - { - $this->types = $types; - } - - /** - * Constructor from array representation - * - * @param array $intersectionType - * - * @return static - */ - public static function fromRaw(array $intersectionType): self - { - return new self(collect($intersectionType)->mapInto(Type::class)->all()); - } - - /** - * Returns the types composing the intersection type - * - * @return array - */ - public function getTypes(): array - { - return $this->types; - } - - /** - * Returns true if this is an intersection of a single type. - * Meaning it is not actually an intersection type. - * - * @return bool - */ - public function isIntersectionOfOne(): bool - { - return count($this->types) === 1; - } - - /** - * Returns the type in its string representation - * - * @return string - */ - public function toString(): string - { - return $this->__toString(); - } - - /** - * Allows a class to decide how it will react when it is treated like a string. - * - * @return string - */ - public function __toString(): string - { - return collect($this->types) - ->map(fn (Type $type) => $type->toString()) - ->sort() - ->implode('&'); - } - - /** - * Returns true if the type is considered to accept "null" - * - * @return bool - */ - public function isNullable(): bool - { - // An intersection type is mixed, if any of the types are considered nullable. - foreach ($this->types as $type) { - if ($type->isNull()) { - return true; - } - } - - return false; - } - - /** - * Returns true if the type is considered to accept "mixed" - * - * @return bool - */ - public function isMixed(): bool - { - // An intersection type is mixed, if any of the types are considered mixed. - foreach ($this->types as $type) { - if ($type->isMixed()) { - return true; - } - } - - return false; - } -} diff --git a/src/TypeSystem/Type.php b/src/TypeSystem/Type.php deleted file mode 100644 index 3a6712f..0000000 --- a/src/TypeSystem/Type.php +++ /dev/null @@ -1,159 +0,0 @@ -type = 'mixed'; - } else { - $this->type = Str::of($type)->replaceMatches('/<.*>/', '')->toString(); - } - } - - /** - * Returns true if the type is not real, meaning a variable cannot have this type. - * - * @return bool - */ - public function isNotReal(): bool - { - return in_array(strtolower($this->type), ['void', 'never'], true); - } - - /** - * Returns true if the type is null - * - * @return bool - */ - public function isNull(): bool - { - return strtolower($this->type) === 'null'; - } - - /** - * Returns true if the type is of mixed - * - * @return bool - */ - public function isMixed(): bool - { - return strtolower($this->type) === 'mixed'; - } - - /** - * Returns true if the type is a number - * - * @return bool - */ - public function isNumber(): bool - { - return in_array(strtolower($this->type), ['int', 'float'], true); - } - - /** - * Returns true if the type is a float - * - * @return bool - */ - public function isFloat(): bool - { - return strtolower($this->type) === 'float'; - } - - /** - * Returns true if it is a class type - * - * @return bool - */ - public function isClass(): bool - { - return class_exists($this->type); - } - - /** - * Returns true if the a interface type - * - * @return bool - */ - public function isInterface(): bool - { - return interface_exists($this->type); - } - - /** - * Returns true if the given type exactly matches this type - * - * @param Type $type - * - * @return bool - */ - public function equals(Type $type): bool - { - return strtolower($this->type) === strtolower($type->type); - } - - /** - * Returns true if this type is exactly the given type, or direct subset (for classes and interfaces) - * - * @param Type $type - * - * @return bool - */ - public function isA(Type $type): bool - { - return strtolower($this->type) === strtolower($type->type) - || is_a($this->type, $type->type, true); - } - - /** - * Returns true if the type is class or interface type - * - * @return bool - */ - public function isClassOrInterface(): bool - { - return $this->isClass() || $this->isInterface(); - } - - /** - * Returns the string in its string representation - * - * @return string - */ - public function toString(): string - { - return $this->__toString(); - } - - /** - * Allows a class to decide how it will react when it is treated like a string. - * - * @return string - */ - public function __toString(): string - { - if ($this->isClassOrInterface()) { - return ltrim($this->type, '\\'); - } - - // Handles when empty string == mixed - if ($this->isMixed()) { - return 'mixed'; - } - - return strtolower($this->type); - } -} diff --git a/src/TypeSystem/TypeSystem.php b/src/TypeSystem/TypeSystem.php deleted file mode 100644 index 1dcbc92..0000000 --- a/src/TypeSystem/TypeSystem.php +++ /dev/null @@ -1,106 +0,0 @@ -getIntersectionTypes() as $intersectionType) { - if ( ! self::isIntersectionTypeSubsetOfUnionType($intersectionType, $parentType)) { - return false; - } - } - - return true; - } - - /** - * Returns true if the given intersection type is a subset of the given parent type - * - * @param IntersectionType $intersectionType - * @param UnionType $parentType - * - * @return bool - */ - private static function isIntersectionTypeSubsetOfUnionType(IntersectionType $intersectionType, UnionType $parentType): bool - { - // For an intersection type to be a subset of a union type, then at least one of the underlying types - // for the intersection type has to be a subset of the type options for the union type - foreach ($intersectionType->getTypes() as $type) { - if (self::isTypeSubsetOfUnionType($type, $parentType)) { - return true; - } - } - - return false; - } - - /** - * Returns true if the given type, is a subset of the given union type - * - * @param Type $type - * @param UnionType $parentUnionType - * - * @return bool - */ - private static function isTypeSubsetOfUnionType(Type $type, UnionType $parentUnionType): bool - { - // For a specific type to be a subset of a union type, then the type - // has to be a subset of only one of the types of the underlying intersection types. - foreach ($parentUnionType->getIntersectionTypes() as $parentIntersectionType) { - foreach ($parentIntersectionType->getTypes() as $parentType) { - if (self::isTypeSubsetOfType($type, $parentType)) { - return true; - } - } - } - - return false; - } - - /** - * Returns true if the given type is a subset of the given parent type - * - * @param Type $type - * @param Type $parentType - * - * @return bool - */ - private static function isTypeSubsetOfType(Type $type, Type $parentType): bool - { - // A non-real type cannot exist as a variable, and therefor is never a subtype. - if ($type->isNotReal()) { - return false; - } - - // Everything is a subset of mixed - if ($parentType->isMixed()) { - return true; - } - - // All numbers are a subset of float - if ($type->isNumber() && $parentType->isFloat()) { - return true; - } - - // If a type equals or is a instance of the parent type, then its a subset. - if ($type->isA($parentType)) { - return true; - } - - // Otherwise it is not. - return false; - } -} diff --git a/src/TypeSystem/UnionType.php b/src/TypeSystem/UnionType.php deleted file mode 100644 index e1aaf7e..0000000 --- a/src/TypeSystem/UnionType.php +++ /dev/null @@ -1,155 +0,0 @@ - - */ - private array $intersectionTypes; - - /** - * @param IntersectionType[] $intersectionTypes - */ - public function __construct(array $intersectionTypes) - { - $this->intersectionTypes = $intersectionTypes; - } - - public static function fromRaw(array|Arrayable $unionType): self - { - if ($unionType instanceof Arrayable) { - $unionType = $unionType->toArray(); - } - - return new self(collect($unionType)->map(IntersectionType::fromRaw(...))->all()); - } - - public static function fromString(string $type): self - { - return self::fromRaw( - Str::of($type) - ->explode('|') - ->map(fn (string $type) => explode('&', $type)) - ->all() - ); - } - - /** - * Returns true if the type is nullable - * - * @return bool - */ - public function isNullable(): bool - { - // A union type is nullable, if just one of the types are nullable - foreach ($this->intersectionTypes as $type) { - if ($type->isNullable()) { - return true; - } - } - - return false; - } - - /** - * Returns true if the type is not nullable - * - * @return bool - */ - public function isNotNullable(): bool - { - return ! $this->isNullable(); - } - - public function isMixed(): bool - { - // A union type is mixed, if just one of the types are considered mixed - foreach ($this->intersectionTypes as $type) { - if ($type->isMixed()) { - return true; - } - } - - return false; - } - - /** - * @return array - */ - public function getIntersectionTypes(): array - { - return $this->intersectionTypes; - } - - public function isUnionOfOne(): bool - { - return count($this->intersectionTypes) === 1; - } - - public function toString(): string - { - return $this->__toString(); - } - - /** - * Magic method {@see https://www.php.net/manual/en/language.oop5.magic.php#object.tostring} - * allows a class to decide how it will react when it is treated like a string. - * - * @return string Returns string representation of the object that - * implements this interface (and/or "__toString" magic method). - */ - public function __toString(): string - { - $type = collect($this->intersectionTypes) - ->map(function (IntersectionType $intersectionType) { - if ($intersectionType->isIntersectionOfOne()) { - return $intersectionType->toString(); - } - - return sprintf('(%s)', $intersectionType->toString()); - }) - ->sort() - ->implode('|'); - - // No need to add parentheses from intersection, unless there are more types. - if ($this->isUnionOfOne()) { - return trim($type, '()'); - } - - return $type; - } - - /** - * Returns array containing all the necessary state of the object. - * - * @since 7.4 - * @link https://wiki.php.net/rfc/custom_object_serialization - */ - public function __serialize(): array - { - return [ - 'type' => $this->toString(), - ]; - } - - /** - * Restores the object state from the given data array. - * - * @param array $data - * - * @since 7.4 - * @link https://wiki.php.net/rfc/custom_object_serialization - */ - public function __unserialize(array $data): void - { - $this->intersectionTypes = self::fromString($data['type'])->intersectionTypes; - } -}