*/ private string $arrayType, private string $symbol, ) {} public static function array(): self { return self::$array ??= new self(ArrayType::class, 'array'); } public static function nonEmptyArray(): self { return self::$nonEmptyArray ??= new self(NonEmptyArrayType::class, 'non-empty-array'); } public static function iterable(): self { return self::$iterable ??= new self(IterableType::class, 'iterable'); } public function traverse(TokenStream $stream): Type { if ($stream->done()) { return ($this->arrayType)::native(); } if ($stream->next() instanceof OpeningBracketToken) { $stream->forward(); return $this->arrayType($stream); } if ($this->arrayType === ArrayType::class && $stream->next() instanceof OpeningCurlyBracketToken) { return $this->shapedArrayType($stream); } return ($this->arrayType)::native(); } public function symbol(): string { return $this->symbol; } private function arrayType(TokenStream $stream): Type { if ($stream->done()) { throw new ArrayMissingSubType($this->symbol . '<'); } $type = $stream->read(); if ($stream->done()) { throw new ArrayExpectedCommaOrClosingBracket($this->symbol, $type); } $token = $stream->forward(); if ($token instanceof ClosingBracketToken) { return new ($this->arrayType)(ArrayKeyType::default(), $type); } if (! $token instanceof CommaToken) { throw new ArrayCommaMissing($this->symbol, $type); } $keyType = $type; if (! $keyType instanceof ArrayKeyType) { $keyTypes = $type instanceof UnionType ? $type->types() : [$type]; try { $keyType = ArrayKeyType::from($keyTypes); } catch (InvalidType $exception) { $subTypes = implode('|', array_map(static fn (Type $type) => $type->toString(), $keyTypes)); $keyType = new UnresolvableType($subTypes, $exception->getMessage()); } } else { $keyTypes = $keyType->types; } if ($stream->done()) { throw new ArrayMissingSubType($this->symbol . '<' . $keyType->toString() . ','); } $subType = $stream->read(); if ($keyType instanceof UnresolvableType) { return $keyType->forArrayType($this->symbol, $keyTypes, $subType); } $arrayType = new ($this->arrayType)($keyType, $subType); if ($stream->done() || ! $stream->forward() instanceof ClosingBracketToken) { throw new ArrayClosingBracketMissing($arrayType); } return $arrayType; } private function shapedArrayType(TokenStream $stream): Type { $stream->forward(); $elements = []; $index = 0; $isUnsealed = false; $unsealedType = null; while (! $stream->done()) { if ($stream->next() instanceof ClosingCurlyBracketToken) { $stream->forward(); break; } if (! empty($elements) && ! $stream->forward() instanceof CommaToken) { throw new ShapedArrayCommaMissing($elements); } if ($stream->done()) { throw new ShapedArrayClosingBracketMissing($elements); } if ($stream->next() instanceof ClosingCurlyBracketToken) { $stream->forward(); break; } $optional = false; if ($stream->next() instanceof TripleDotsToken) { $isUnsealed = true; $stream->forward(); } if ($stream->done()) { throw new ShapedArrayClosingBracketMissing($elements, unsealedType: false); } $keyToken = $stream->next(); if ($isUnsealed && ($keyToken instanceof ClosingCurlyBracketToken || $keyToken instanceof CommaToken)) { $stream->forward(); break; } else { $type = $stream->read(); } if ($isUnsealed) { $unsealedType = $type; if ($elements === []) { throw new ShapedArrayWithoutElementsWithSealedType($unsealedType); } if ($stream->done()) { throw new ShapedArrayClosingBracketMissing($elements, $unsealedType); } elseif (! $stream->next() instanceof ClosingCurlyBracketToken) { $unexpected = []; while (! $stream->done() && ! $stream->next() instanceof ClosingCurlyBracketToken) { $unexpected[] = $stream->forward(); } throw new ShapedArrayUnexpectedTokenAfterSealedType($elements, $unsealedType, $unexpected); } continue; } if ($stream->done()) { $elements[] = new ShapedArrayElement(new IntegerValueType($index), $type); throw new ShapedArrayClosingBracketMissing($elements); } if ($stream->next() instanceof NullableToken) { $stream->forward(); $optional = true; if ($stream->done()) { throw new ShapedArrayColonTokenMissing($elements, $type); } } if ($stream->next() instanceof ColonToken) { $stream->forward(); $key = $type; $type = null; if (! $key instanceof StringValueType && ! $key instanceof IntegerValueType) { $key = new StringValueType($keyToken->symbol()); } if ($key instanceof IntegerValueType) { $index++; } } else { if ($optional) { throw new ShapedArrayColonTokenMissing($elements, $type); } $key = new IntegerValueType($index++); } if (! $type) { if ($stream->done()) { throw new ShapedArrayElementTypeMissing($elements, $key, $optional); } $type = $stream->read(); } $elements[] = new ShapedArrayElement($key, $type, $optional); if ($stream->done()) { throw new ShapedArrayClosingBracketMissing($elements); } } return ShapedArrayType::from($elements, $isUnsealed, $unsealedType); } }