<?php

declare(strict_types=1);

namespace Phan\Language;

use Closure;
use Exception;
use Generator;
use InvalidArgumentException;
use Phan\CodeBase;
use Phan\Config;
use Phan\Debug\Frame;
use Phan\Exception\CodeBaseException;
use Phan\Exception\IssueException;
use Phan\Exception\RecursionDepthException;
use Phan\Issue;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\FQSEN\FullyQualifiedFunctionName;
use Phan\Language\FQSEN\FullyQualifiedMethodName;
use Phan\Language\Type\ArrayShapeType;
use Phan\Language\Type\ArrayType;
use Phan\Language\Type\AssociativeArrayType;
use Phan\Language\Type\BoolType;
use Phan\Language\Type\CallableStringType;
use Phan\Language\Type\CallableType;
use Phan\Language\Type\ClassStringType;
use Phan\Language\Type\FalseType;
use Phan\Language\Type\FloatType;
use Phan\Language\Type\GenericArrayInterface;
use Phan\Language\Type\GenericArrayType;
use Phan\Language\Type\IntType;
use Phan\Language\Type\IterableType;
use Phan\Language\Type\ListType;
use Phan\Language\Type\LiteralFloatType;
use Phan\Language\Type\LiteralIntType;
use Phan\Language\Type\LiteralStringType;
use Phan\Language\Type\LiteralTypeInterface;
use Phan\Language\Type\MixedType;
use Phan\Language\Type\MultiType;
use Phan\Language\Type\NonEmptyArrayInterface;
use Phan\Language\Type\NonEmptyListType;
use Phan\Language\Type\NonEmptyMixedType;
use Phan\Language\Type\NonEmptyStringType;
use Phan\Language\Type\NullType;
use Phan\Language\Type\ObjectType;
use Phan\Language\Type\ScalarType;
use Phan\Language\Type\SelfType;
use Phan\Language\Type\StaticType;
use Phan\Language\Type\StringType;
use Phan\Language\Type\TemplateType;
use Phan\Language\Type\TrueType;
use Phan\Language\Type\VoidType;
use Serializable;
use Stringable;

use function is_int;
use function substr;

/**
 * Phan's internal representation of union types, and methods for working with union types.
 *
 * This representation is immutable.
 * Phan represents union types as a list of unique `Type`s
 * (This was the most efficient representation, since most union types have 0, 1, or 2 unique types in practice)
 * To add/remove a type to a UnionType, you replace it with a UnionType that had that type added.
 *
 * @see AnnotatedUnionType for the way Phan represents extra information about types
 * @see https://github.com/phan/phan/wiki/About-Union-Types
 *
 * > Union types can be any native type such as int, string, bool, or array, any class such as DateTime,
 * > arrays of types such as string[], DateTime[],
 * > or a union of any other types such as string|int|null|DateTime|DateTime[],
 * > and many other types
 *
 * @phan-file-suppress PhanPluginDescriptionlessCommentOnPublicMethod TODO: Document the public methods
 * @phan-pure types/union types are immutable, but technically not pure (some methods cause issues to be emitted with Issue::maybeEmit()).
 *            However, it's useful to treat them as if they were pure, to warn about not using return values.
 * @suppress PhanRedefinedInheritedInterface this uses a polyfill for Stringable
 */
class UnionType implements Serializable, Stringable
{
    /**
     * @var string
     * A list of one or more types delimited by the '|'
     * character (e.g. 'int|DateTime|string[]')
     */
    // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName
    public const union_type_regex =
        Type::type_regex
        . '(\s*[|&]\s*' . Type::type_regex . ')*';

    /**
     * @var string
     * A list of one or more types delimited by the '|'
     * character (e.g. 'int|DateTime|string[]' or 'null|$this')
     * This may be used for return types.
     *
     * TODO: Equivalent variants with no capturing? (May not improve performance much)
     */
    // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName
    public const union_type_regex_or_this =
        Type::type_regex_or_this
        . '(\s*[|&]\s*' . Type::type_regex_or_this . ')*';

    /**
     * @var list<Type> This is an immutable list of unique types.
     */
    private $type_set;

    /**
     * @var list<Type> * This is an immutable list of unique types.
     */
    private $real_type_set;

    /**
     * @param list<Type> $type_list
     * An optional list of types represented by this union
     * @param bool $is_unique - Whether or not this is already unique. Only set to true within UnionType code.
     * @param list<Type> $real_type_set
     * @see UnionType::of() for a more memory efficient equivalent.
     * @phan-pure
     */
    public function __construct(array $type_list, bool $is_unique = false, array $real_type_set = [])
    {
        $this->type_set = ($is_unique || \count($type_list) <= 1) ? $type_list : self::getUniqueTypes($type_list);
        $this->real_type_set = ($is_unique || \count($real_type_set) <= 1) ? $real_type_set : self::getUniqueTypes($real_type_set);
    }

    /**
     * @param Type[] $type_list
     * @param Type[] $real_type_set
     * @phan-pure
     */
    public static function of(array $type_list, array $real_type_set = []): UnionType
    {
        $n = \count($type_list);
        if ($n === 0) {
            if ($real_type_set) {
                if (\count($real_type_set) === 1) {
                    // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
                    return \reset($real_type_set)->asRealUnionType();
                }
                return new self($real_type_set, false, $real_type_set);
            }
            return self::$empty_instance;
        }
        if ($n === 1 && \count($real_type_set) <= 1) {
            if (!$real_type_set) {
                // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
                return \reset($type_list)->asPHPDocUnionType();
            } elseif ($real_type_set === $type_list) {
                // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
                return \reset($type_list)->asRealUnionType();
            }
            return new self($type_list, true, $real_type_set);
        } else {
            return new self($type_list, false, $real_type_set);
        }
    }

    /**
     * @param Type[] $type_list
     * @param Type[] $real_type_set
     * @phan-pure
     */
    private static function ofUnique(array $type_list, array $real_type_set = []): UnionType
    {
        $n = \count($type_list);
        if ($n === 0) {
            if ($real_type_set) {
                if (\count($real_type_set) === 1) {
                    // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
                    return \reset($real_type_set)->asRealUnionType();
                }
                return new self($real_type_set, true, $real_type_set);
            }
            return self::$empty_instance;
        }
        if ($n === 1 && \count($real_type_set) <= 1) {
            if (!$real_type_set) {
                // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
                return \reset($type_list)->asPHPDocUnionType();
            } elseif ($real_type_set === $type_list) {
                // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
                return \reset($type_list)->asRealUnionType();
            }
        }
        return new self($type_list, true, $real_type_set);
    }

    /** @var EmptyUnionType an empty union type - Cached here for quick access. */
    private static $empty_instance;

    /**
     * @return EmptyUnionType (Real return type omitted for performance)
     */
    public static function empty(): EmptyUnionType
    {
        return self::$empty_instance;
    }

    /**
     * @internal
     */
    public static function init(): void
    {
        if (\is_null(self::$empty_instance)) {
            self::$empty_instance = EmptyUnionType::instance();
        }
    }

    /**
     * @suppress PhanAccessReadOnlyProperty
     */
    public function __clone()
    {
        $this->with_erased_real_type_set = null;
        $this->as_normalized = null;
    }

    // __clone of $this->type_set would be a no-op due to copy on write semantics.
    // And clone isn't necessary anymore now that type_set is immutable

    /**
     * @param string $fully_qualified_string
     * A '|' delimited string representing a type in the form
     * 'int|string|null|ClassName'.
     * @throws InvalidArgumentException if any type name in the union type was invalid
     *
     * @see self::fromFullyQualifiedRealString() if you are absolutely sure this is the real type of the expression.
     * @phan-pure
     */
    public static function fromFullyQualifiedPHPDocString(
        string $fully_qualified_string
    ): UnionType {
        if ($fully_qualified_string === '') {
            return self::$empty_instance;
        }

        /** @var array<string,UnionType> annotation not read by phan */
        static $memoize_map = [];
        $union_type = $memoize_map[$fully_qualified_string] ?? null;

        if (\is_null($union_type)) {
            /** Convert the `|` separated types in the union type to a list of types */
            $types = \array_map(static function (string $type_name): Type {
                // @phan-suppress-next-line PhanThrowTypeAbsentForCall FIXME: Standardize on InvalidArgumentException
                return Type::fromFullyQualifiedString($type_name);
            }, self::extractTypeParts($fully_qualified_string));

            $unique_types = self::getUniqueTypes(self::normalizeMultiTypes($types));
            if (\count($unique_types) === 1) {
                // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
                $union_type = \reset($unique_types)->asPHPDocUnionType();
            } else {
                // TODO: Support template types within <> and test?
                $union_type = new UnionType(
                    $unique_types,
                    true
                );
            }
            $memoize_map[$fully_qualified_string] = $union_type;
        }

        return $union_type;
    }

    /**
     * @return list<Type> the corresponding set of types for this string.
     * @throws InvalidArgumentException if any type name in the union type was invalid
     */
    public static function typeSetFromString(string $fully_qualified_string): array
    {
        return self::fromFullyQualifiedPHPDocString($fully_qualified_string)->type_set;
    }

    /**
     * Returns a type with the following phpdoc types and real types.
     * Use this if they are both non-empty but different.
     * The caller should pass in phpdoc types that are subtypes of the real types
     * (e.g. phpdoc='int|float' real='int')
     * @phan-pure
     */
    public static function fromFullyQualifiedPHPDocAndRealString(
        string $fully_qualified_phpdoc_string,
        string $fully_qualified_real_string
    ): UnionType {
        if ($fully_qualified_real_string === $fully_qualified_phpdoc_string) {
            return self::fromFullyQualifiedRealString($fully_qualified_real_string);
        }
        static $memoize_map = [];
        $key = "$fully_qualified_real_string@$fully_qualified_phpdoc_string";
        $union_type = $memoize_map[$key] ?? null;

        if (\is_null($union_type)) {
            $phpdoc = UnionType::fromFullyQualifiedPHPDocString($fully_qualified_phpdoc_string);
            $real = UnionType::fromFullyQualifiedPHPDocString($fully_qualified_real_string);
            $union_type = UnionType::ofUnique($phpdoc->getTypeSet(), $real->getTypeSet());
            $memoize_map[$key] = $union_type;
        }
        return $union_type;
    }

    /**
     * @param string $fully_qualified_string
     * A '|' delimited string representing a type in the form
     * 'int|string|null|ClassName'.
     * @throws InvalidArgumentException if any type name in the union type was invalid
     * @phan-pure
     */
    public static function fromFullyQualifiedRealString(
        string $fully_qualified_string
    ): UnionType {
        if ($fully_qualified_string === '') {
            return self::$empty_instance;
        }

        /** @var array<string,UnionType> annotation not read by phan */
        static $memoize_map = [];
        $union_type = $memoize_map[$fully_qualified_string] ?? null;

        if (\is_null($union_type)) {
            /** Convert the `|` separated types in the union type to a list of types */
            $types = \array_map(static function (string $type_name): Type {
                // @phan-suppress-next-line PhanThrowTypeAbsentForCall FIXME: Standardize on InvalidArgumentException
                return Type::fromFullyQualifiedString($type_name);
            }, self::extractTypeParts($fully_qualified_string));

            $unique_types = self::getUniqueTypes(self::normalizeMultiTypes($types));
            if (\count($unique_types) === 1) {
                // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
                $union_type = \reset($unique_types)->asRealUnionType();
            } else {
                // TODO: Support template types within <> and test?
                $union_type = new UnionType(
                    $unique_types,
                    true,
                    $unique_types
                );
            }
            $memoize_map[$fully_qualified_string] = $union_type;
        }

        return $union_type;
    }

    /**
     * Returns a list of unique types in the provided type list.
     *
     * @param list<Type> $type_list
     * @return list<Type>
     * @phan-pure
     */
    public static function getUniqueTypes(array $type_list): array
    {
        $new_type_list = [];
        if (\count($type_list) >= 8) {
            // This approach is faster, but only when there are 8 or more types (tested in php 7.3)
            // See https://github.com/phan/phan/pull/3475#issuecomment-550570579
            foreach ($type_list as $type) {
                $new_type_list[\spl_object_id($type)] = $type;
            }
            return \array_values($new_type_list);
        }
        foreach ($type_list as $type) {
            if (!\in_array($type, $new_type_list, true)) {
                $new_type_list[] = $type;
            }
        }
        return $new_type_list;
    }

    /**
     * @param list<Type> $type_list a list of unique types
     * @param list<Type> $other_type_list a list of unique types
     * @return list<Type> a list of the unique types from both lists
     * @phan-pure
     */
    private static function mergeUniqueTypes(array $type_list, array $other_type_list): array
    {
        if (\count($other_type_list) <= 4) {
            // NOTE: implementing it this way takes advantage of copy-on-write for small arrays.
            // If no new types were added, the original array $type_list will be reused.
            foreach ($other_type_list as $type) {
                if (!\in_array($type, $type_list, true)) {
                    $type_list[] = $type;
                }
            }
            return $type_list;
        }
        $new_type_list = [];
        // Avoid worst-case quadratic runtime
        foreach ($type_list as $type) {
            $new_type_list[\spl_object_id($type)] = $type;
        }
        foreach ($other_type_list as $type) {
            $new_type_list[\spl_object_id($type)] = $type;
        }
        return \array_values($new_type_list);
    }

    /**
     * @param string $type_string
     * A '|' delimited string representing a type in the form
     * 'int|string|null|ClassName'.
     *
     * @param Context $context
     * The context in which the type string was
     * found
     *
     * @param int $source one of the constants in Type::FROM_*
     *
     * @param ?CodeBase $code_base
     * May be provided to resolve 'parent' in the context
     * (e.g. if parsing complex phpdoc).
     * Unnecessary in most use cases.
     * @phan-pure
     */
    public static function fromStringInContext(
        string $type_string,
        Context $context,
        int $source,
        CodeBase $code_base = null
    ): UnionType {
        if ($type_string === '') {
            // NOTE: '0' is a valid LiteralIntType
            return self::$empty_instance;
        }

        $types = [];
        foreach (self::extractTypePartsForStringInContext($type_string) as $type_name) {
            $types[] = Type::fromStringInContext(
                $type_name,
                $context,
                $source,
                $code_base
            );
        }
        return UnionType::of(self::normalizeMultiTypes($types));
    }

    /**
     * @return list<string>
     */
    private static function extractTypePartsForStringInContext(string $type_string): array
    {
        static $cache = [];
        $parts = $cache[$type_string] ?? null;
        if (\is_array($parts)) {
            return $parts;
        }
        $parts = [];
        foreach (self::extractTypeParts($type_string) as $type_name) {
            // Exclude empty type names
            // Exclude namespaces without type names (e.g. `\`, `\NS\`)
            if ($type_name === '' || \preg_match('@\\\\[\[\]]*$@D', $type_name)) {
                $parts[] = $type_name;
                continue;
            }
            if (substr($type_name, -1) === ')') {
                if (substr($type_name, 0, 1) === '(') {
                    // @phan-suppress-next-line PhanPossiblyFalseTypeArgument
                    foreach (self::extractTypePartsForStringInContext(substr($type_name, 1, -1)) as $inner_type_name) {
                        $parts[] = $inner_type_name;
                    }
                    continue;
                } elseif (substr($type_name, 0, 2) === '?(') {
                    // @phan-suppress-next-line PhanPossiblyFalseTypeArgument
                    foreach (self::extractTypePartsForStringInContext(substr($type_name, 2, -1)) as $inner_type_name) {
                        if (substr($inner_type_name, 0, 1) === '?') {
                            $parts[] = $inner_type_name;
                        } else {
                            $parts[] = '?' . $inner_type_name;
                        }
                    }
                    continue;
                }
            }
            $parts[] = $type_name;
        }
        $cache[$type_string] = $parts;
        return $parts;
    }

    /**
     * @return list<string>
     */
    private static function extractTypeParts(string $type_string): array
    {
        $parts = [];
        foreach (\preg_split('@[|&]@', $type_string) as $part) {
            $parts[] = \trim($part);
        }

        if (\count($parts) <= 1) {
            return $parts;
        }
        if (!\preg_match('/[<({]/', $type_string)) {
            return $parts;
        }
        return self::mergeTypeParts($parts);
    }

    /**
     * Expands any GenericMultiArrayType and ScalarRawType instances in $types if necessary.
     *
     * @param list<Type> $types
     * @return array<int,Type>
     */
    public static function normalizeMultiTypes(array $types): array
    {
        foreach ($types as $i => $type) {
            if ($type instanceof MultiType) {
                foreach ($type->asIndividualTypeInstances() as $new_type) {
                    $types[] = $new_type;
                }
                unset($types[$i]);
            }
        }
        return $types;
    }

    /**
     * @param string[] $parts (already trimmed)
     * @return string[]
     * @see Type::extractTemplateParameterTypeNameList() (Similar method)
     */
    private static function mergeTypeParts(array $parts): array
    {
        $prev_parts = [];
        $delta = 0;
        $results = [];
        foreach ($parts as $part) {
            if (\count($prev_parts) > 0) {
                $prev_parts[] = $part;
                $delta += \substr_count($part, '<') + \substr_count($part, '(') + \substr_count($part, '{') - \substr_count($part, '>') - \substr_count($part, ')') - \substr_count($part, '}');
                if ($delta <= 0) {
                    if ($delta === 0) {
                        $results[] = \implode('|', $prev_parts);
                    }  // ignore unparsable data such as "<T,T2>>"
                    $prev_parts = [];
                    $delta = 0;
                    continue;
                }
                continue;
            }
            $bracket_count = \substr_count($part, '<') + \substr_count($part, '(') + \substr_count($part, '{');
            if ($bracket_count === 0) {
                $results[] = $part;
                continue;
            }
            $delta = $bracket_count - \substr_count($part, '>') - \substr_count($part, ')') - \substr_count($part, '}');
            if ($delta === 0) {
                $results[] = $part;
            } elseif ($delta > 0) {
                $prev_parts[] = $part;
            }  // otherwise ignore unparsable data such as ">" (should be impossible)
        }
        return $results;
    }

    /**
     * @param ?\ReflectionType $reflection_type
     *
     * @return UnionType
     * A UnionType with 0 or more nullable/non-nullable Types
     * (limited to at most 1 in php 7, unlimited in php 8)
     */
    public static function fromReflectionType(?\ReflectionType $reflection_type): UnionType
    {
        if ($reflection_type !== null) {
            return self::fromStringInContext(
                Type::stringFromReflectionType($reflection_type),
                new Context(),
                Type::FROM_TYPE
            );
        }
        return self::$empty_instance;
    }

    /**
     * @return array<string,string>
     * Get a map from property name to its type for the given
     * class name.
     */
    public static function internalPropertyMapForClassName(
        string $class_name
    ): array {
        $map = self::internalPropertyMap();

        $canonical_class_name = \strtolower($class_name);

        return $map[$canonical_class_name] ?? [];
    }

    /**
     * @return array<string,array<string,string>>
     * A map from builtin class properties to type information
     *
     * @see \Phan\Language\Internal\PropertyMap
     */
    public static function internalPropertyMap(): array
    {
        static $map = [];

        if (!$map) {
            $map_raw = require(__DIR__ . '/Internal/PropertyMap.php');
            foreach ($map_raw as $key => $value) {
                $map[\strtolower($key)] = $value;
            }

            // Merge in an empty type for dynamic properties on any
            // classes listed as supporting them.
            foreach (require(__DIR__ . '/Internal/DynamicPropertyMap.php') as $class_name) {
                $map[\strtolower($class_name)]['*'] = '';
            }
        }

        return $map;
    }

    /**
     * A list of types for parameters associated with the
     * given builtin function with the given name
     *
     * @param FullyQualifiedMethodName|FullyQualifiedFunctionName $function_fqsen
     *
     * @return list<array{return_type:?UnionType,parameter_name_type_map:array<string,UnionType>}>
     *
     * @see internal_varargs_check
     * Formerly `function internal_varargs_check`
     */
    public static function internalFunctionSignatureMapForFQSEN(
        $function_fqsen
    ): array {
        $map = self::internalFunctionSignatureMap(Config::get_closest_target_php_version_id());

        if ($function_fqsen instanceof FullyQualifiedMethodName) {
            $class_fqsen =
                $function_fqsen->getFullyQualifiedClassName();
            $class_name = $class_fqsen->getNamespacedName();
            $function_name =
                $class_name . '::' . $function_fqsen->getName();
        } else {
            $function_name = $function_fqsen->getNamespacedName();
        }

        $function_name = \strtolower($function_name);

        $function_name_original = $function_name;
        $alternate_id = 0;

        /**
         * @param string|null $type_name
         * @return UnionType|null
         */
        $get_for_global_context = static function (?string $type_name): ?UnionType {
            if (!\is_string($type_name) || $type_name === '') {
                return null;
            }

            static $internal_fn_cache = [];


            $result = $internal_fn_cache[$type_name] ?? null;
            if ($result === null) {
                $context = new Context();
                $result = UnionType::fromStringInContext($type_name, $context, Type::FROM_PHPDOC);
                $internal_fn_cache[$type_name] = $result;
            }
            return $result;
        };

        $configurations = [];
        while (isset($map[$function_name])) {
            // Get some static data about the function
            $type_name_struct = $map[$function_name];

            // Figure out the return type
            $return_type_name = $type_name_struct[0];
            $return_type = $get_for_global_context($return_type_name);

            $parameter_name_type_map = [];

            foreach ($type_name_struct as $name => $type_name) {
                if (\is_int($name)) {
                    // Integer key names are reserved for metadata in the future.
                    continue;
                }
                $parameter_name_type_map[$name] = $get_for_global_context($type_name) ?? self::$empty_instance;
            }

            $configurations[] = [
                'return_type' => $return_type,
                'parameter_name_type_map' => $parameter_name_type_map,
            ];

            $function_name =
                $function_name_original . '\'' . (++$alternate_id);
        }

        return $configurations;
    }

    /**
     * @return list<Type>
     * The list of simple types associated with this
     * union type. Keys are consecutive.
     */
    public function getTypeSet(): array
    {
        return $this->type_set;
    }

    /**
     * @return bool true if this has a non-empty real type set.
     */
    public function hasRealTypeSet(): bool
    {
        return (bool)$this->real_type_set;
    }

    /**
     * @return list<Type>
     * The list of real simple types associated with this
     * union type. Keys are consecutive.
     *
     * If this is empty, the real union type is unknown
     */
    public function getRealTypeSet(): array
    {
        return $this->real_type_set;
    }

    /**
     * Add a type name to the list of types
     * @phan-pure
     */
    public function withType(Type $type): UnionType
    {
        // TODO: Figure out a better way to specify if involved types are real
        $type_set = $this->type_set;
        if (\count($type_set) === 0) {
            return $type->withErasedUnionTypes()->asPHPDocUnionType();
        }
        if (\in_array($type, $type_set, true)) {
            return $this->eraseRealTypeSetRecursively();
        }
        // 2 or more types in type_set
        $type_set[] = $type;
        return new UnionType($type_set, true, []);
    }

    /**
     * Returns a new union type
     * which removes this type from the list of types,
     * keeping the keys in a consecutive order.
     *
     * Each type in $this->type_set occurs exactly once.
     * @phan-pure
     */
    public function withoutType(Type $type): UnionType
    {
        // Copy the array $this->type_set
        $type_set = $this->type_set;
        foreach ($type_set as $key => $other_type) {
            if ($type === $other_type) {
                // Remove the only instance of $type from the copy.
                // TODO: Make this work for removing from the real type set
                unset($type_set[$key]);
                return UnionType::ofUnique(\array_values($type_set), []);
            }
        }
        // We did not find $type in type_set. The resulting union type is unchanged.
        return $this->eraseRealTypeSetRecursively();
    }

    /**
     * @return bool
     * True if this union type contains the given named
     * type.
     */
    public function hasType(Type $type): bool
    {
        return \in_array($type, $this->type_set, true);
    }

    /**
     * Returns a union type with an empty real type set
     * @phan-pure
     * @suppress PhanUnreferencedPublicMethod
     */
    public function eraseRealTypeSet(): UnionType
    {
        if ($this->real_type_set) {
            if (\count($this->type_set) === 1) {
                // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
                return \reset($this->type_set)->asPHPDocUnionType();
            }
            return new UnionType($this->type_set, true, []);
        }
        return $this;
    }

    /**
     * @var ?UnionType|?true this union type, with the real type set erased.
     *
     * True if this union type already has all real types erased, to make it possible to garbage collect this with cycle detection disabled.
     */
    private $with_erased_real_type_set = null;

    /**
     * Returns a union type with an empty real type set (including in elements of generic arrays, etc.)
     * @suppress PhanAccessReadOnlyProperty
     */
    public function eraseRealTypeSetRecursively(): UnionType
    {
        $with_erased_real_type_set = $this->with_erased_real_type_set;
        if (\is_null($with_erased_real_type_set)) {
            $this->with_erased_real_type_set = $with_erased_real_type_set = $this->computeEraseRealTypeSetRecursively();
        }
        if (\is_object($with_erased_real_type_set)) {
            return $with_erased_real_type_set;
        }
        return $this;
    }

    /** @return UnionType|true */
    private function computeEraseRealTypeSetRecursively()
    {
        $new_type_set = [];
        foreach ($this->type_set as $type) {
            $new_type_set[] = $type->withErasedUnionTypes();
        }
        if (!$this->real_type_set && $new_type_set === $this->type_set) {
            return $this;
        }
        return UnionType::of($new_type_set);
    }

    /**
     * Returns a union type which adds the given types to this type
     * @phan-pure
     */
    public function withUnionType(UnionType $union_type): UnionType
    {
        // Precondition: Both UnionTypes have lists of unique types.
        $type_set = $this->type_set;
        if (\count($type_set) === 0) {
            return $union_type->eraseRealTypeSetRecursively();
        }
        $other_type_set = $union_type->type_set;

        if (\count($other_type_set) === 0) {
            return $this->eraseRealTypeSetRecursively();
        }
        return UnionType::ofUnique(
            self::mergeUniqueTypes($type_set, $other_type_set),
            ($this->real_type_set && $union_type->real_type_set) ? self::mergeUniqueTypes($this->real_type_set, $union_type->real_type_set) : []
        );
    }

    /**
     * @return bool
     * True if this type has a type referencing the
     * class context in which it exists such as 'self'
     * or '$this'
     */
    public function hasSelfType(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->isSelfType()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return bool
     * True if this union type has any types that are bool/false/true types
     */
    public function hasTypeInBoolFamily(): bool
    {
        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type->isInBoolFamily();
        });
    }

    /**
     * Returns the types for which is_bool($x) would be true.
     *
     * @return UnionType
     * A UnionType with known bool types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     * @suppress PhanUnreferencedPublicMethod this is referenced dynamically in RedundantConditionCallPlugin
     */
    public function getTypesInBoolFamily(): UnionType
    {
        return $this->makeFromFilter(static function (Type $type): bool {
            return $type->isInBoolFamily();
        });
    }

    /**
     * Returns this union type after asserting is_bool($x)
     * @suppress PhanUnreferencedPublicMethod this is referenced dynamically in ConditionVisitor
     */
    public function boolTypes(): UnionType
    {
        return UnionType::of(
            self::filterTypesInBoolFamily($this->type_set),
            self::filterTypesInBoolFamily($this->real_type_set)
        )->asNormalizedTypes();
    }

    /**
     * @param Type[] $type_list
     * @return list<Type>
     */
    private static function filterTypesInBoolFamily(array $type_list): array
    {
        $result = [];
        foreach ($type_list as $type) {
            if ($type->isInBoolFamily()) {
                $result[] = $type->withIsNullable(false);
            } elseif ($type instanceof MixedType) {
                if ($type instanceof NonEmptyMixedType) {
                    $result[] = TrueType::instance(false);
                } else {
                    return [BoolType::instance(false)];
                }
            }
        }
        return $result ?: [BoolType::instance(false)];
    }

    /**
     * @param CodeBase $code_base
     * The code base to look up classes against
     *
     * TODO: Defer resolving the template parameters until parse ends. Low priority.
     *
     * @return array<string,UnionType>
     * A map from template type identifiers to the UnionType
     * to replace it with
     */
    public function getTemplateParameterTypeMap(
        CodeBase $code_base
    ): array {
        if ($this->isEmpty()) {
            return [];
        }

        return \array_reduce(
            $this->type_set,
            /**
             * @param array<string,UnionType> $map
             * @return array<string,UnionType>
             */
            static function (array $map, Type $type) use ($code_base): array {
                return \array_merge(
                    $type->getTemplateParameterTypeMap($code_base),
                    $map
                );
            },
            []
        );
    }


    /**
     * @param array<string,UnionType> $template_parameter_type_map
     * A map from template type identifiers to concrete types
     *
     * @return UnionType
     * This UnionType with any template types contained herein
     * mapped to concrete types defined in the given map.
     */
    public function withTemplateParameterTypeMap(
        array $template_parameter_type_map
    ): UnionType {
        $has_template = false;
        $concrete_type_list = [];
        foreach ($this->type_set as $type) {
            $new_union_type = $type->withTemplateParameterTypeMap($template_parameter_type_map);
            if ($new_union_type->isType($type)) {
                $concrete_type_list[] = $type;
            } else {
                $has_template = true;
                foreach ($new_union_type->getTypeSet() as $new_type) {
                    $concrete_type_list[] = $new_type;
                }
            }
        }

        return $has_template ? UnionType::of($concrete_type_list, $this->real_type_set) : $this;
    }

    /**
     * @return bool
     * True if this union type has any types that are template types
     * (e.g. true for the template type T, false for MyClass<T>)
     */
    public function hasTemplateType(): bool
    {
        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return ($type instanceof TemplateType);
        });
    }

    /**
     * @return bool
     * True if this union type has any types that are template types
     * (e.g. true for the template type T, true for MyClass<T>, true for T[], false for \MyClass<\stdClass>)
     */
    public function hasTemplateTypeRecursive(): bool
    {
        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type->hasTemplateTypeRecursive();
        });
    }

    /**
     * @return UnionType
     * Removes template types from this union type, e.g. converts T|\stdClass to \stdClass.
     * @suppress PhanUnreferencedPublicMethod
     */
    public function withoutTemplateTypeRecursive(): UnionType
    {
        return $this->makeFromFilter(static function (Type $type): bool {
            return !$type->hasTemplateTypeRecursive();
        });
    }

    /**
     * Convert `\MyClass<T>` and `\MyClass<\OtherClass>` to just `\MyClass`.
     */
    public function eraseTemplatesRecursive(): UnionType
    {
        return $this->asMappedUnionType(static function (Type $type): Type {
            return $type->eraseTemplatesRecursive();
        });
    }

    /**
     * @return bool
     * True if this union type has any types that have generic
     * types
     */
    public function hasTemplateParameterTypes(): bool
    {
        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type->hasTemplateParameterTypes();
        });
    }

    /**
     * @return bool
     * True if this type has a type referencing the
     * class context 'static' at the top level.
     */
    public function hasStaticType(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type instanceof StaticType) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return UnionType
     * A new UnionType with any references to 'static' resolved
     * in the given context.
     */
    public function withStaticResolvedInContext(
        Context $context
    ): UnionType {

        // If the context isn't in a class scope,
        // there's nothing we can do
        if (!$context->isInClassScope()) {
            return $this;
        }
        return $this->asMappedUnionType(static function (Type $type) use ($context): Type {
            return $type->withStaticResolvedInContext($context);
        });
    }

    /**
     * @return UnionType
     * A new UnionType with any references to 'static' resolved
     * in the given function or method's context.
     */
    public function withStaticResolvedInFunctionLike(
        FunctionInterface $function
    ): UnionType {
        $context = $function->getContext();
        if ($function instanceof Method) {
            // Gets the context of the method *after* inheritance
            $context = $context->withScope(new class ($function->getClassFQSEN()) extends Scope {
                /** @var FullyQualifiedClassName the name being resolved */
                private $class_fqsen;
                public function __construct(FullyQualifiedClassName $fqsen)
                {
                    $this->class_fqsen = $fqsen;
                }

                /**
                 * @override
                 */
                public function isInClassScope(): bool
                {
                    return true;
                }

                /**
                 * @override
                 */
                public function getClassFQSEN(): FullyQualifiedClassName
                {
                    return $this->class_fqsen;
                }
            });
        }
        return $this->withStaticResolvedInContext($context);
    }

    /**
     * @return UnionType
     * A new UnionType with any references to 'self' (but not 'static') resolved
     * in the given context.
     */
    public function withSelfResolvedInContext(
        Context $context
    ): UnionType {
        // If the context isn't in a class scope, or if it doesn't have 'self',
        // there's nothing we can do
        if (!$context->isInClassScope() || !$this->hasSelfType()) {
            return $this;
        }

        return $this->asMappedUnionType(static function (Type $type) use ($context): Type {
            if ($type instanceof SelfType) {
                return $context->getClassFQSEN()->asType()->withIsNullable($type->isNullable());
            }
            return $type;
        });
    }

    /**
     * @return UnionType
     * A new UnionType *plus* any references to 'self' (but not 'static') resolved
     * in the given context.
     */
    public function withAddedClassForResolvedSelf(
        Context $context
    ): UnionType {
        // If the context isn't in a class scope, or if it doesn't have 'self',
        // there's nothing we can do
        if (!$context->isInClassScope() || !$this->hasSelfType()) {
            return $this;
        }

        $is_nullable = $this->containsNullable();
        $resolved_type = $context->getClassFQSEN()->asType()->withIsNullable($is_nullable);
        return UnionType::of(
            \array_merge($this->type_set, [$resolved_type]),
            $this->real_type_set
        );
    }

    /**
     * @return bool
     * True if and only if this UnionType contains
     * the given type and no others.
     */
    public function isType(Type $type): bool
    {
        $type_set = $this->type_set;
        if (\count($type_set) !== 1) {
            return false;
        }

        return \reset($type_set) === $type;
    }

    /**
     * @return bool
     * True if this UnionType is exclusively native
     * types
     */
    public function isNativeType(): bool
    {
        if ($this->isEmpty()) {
            return false;
        }

        return $this->allTypesMatchCallback(static function (Type $type): bool {
            return $type->isNativeType();
        });
    }

    /**
     * @return bool
     * True iff this union contains the exact set of types
     * represented in the given union type.
     *
     * This does not check real types.
     */
    public function isEqualTo(UnionType $union_type): bool
    {
        if ($this === $union_type) {
            // true about half the time.
            return true;
        }
        $type_set = $this->type_set;
        $other_type_set = $union_type->type_set;
        if (\count($type_set) !== \count($other_type_set)) {
            return false;
        }
        foreach ($type_set as $type) {
            if (!\in_array($type, $other_type_set, true)) {
                return false;
            }
        }
        return !$union_type->isPossiblyUndefined();
    }

    /**
     * @return bool
     * True iff this union contains the exact set of types
     * represented in the given union type.
     *
     * This checks real types.
     */
    public function isIdenticalTo(UnionType $union_type): bool
    {
        if ($this === $union_type) {
            // true about half the time.
            return true;
        }
        $type_set = $this->type_set;
        $other_type_set = $union_type->type_set;
        if (\count($type_set) !== \count($other_type_set)) {
            return false;
        }
        foreach ($type_set as $type) {
            if (!\in_array($type, $other_type_set, true)) {
                return false;
            }
        }
        $real_type_set = $this->real_type_set;
        $other_real_type_set = $union_type->real_type_set;
        if (\count($real_type_set) !== \count($other_real_type_set)) {
            return false;
        }
        foreach ($real_type_set as $type) {
            if (!\in_array($type, $other_real_type_set, true)) {
                return false;
            }
        }
        return !$union_type->isPossiblyUndefined();
    }

    /**
     * @return bool
     * True iff this union contains a type that's also in
     * the other union type.
     *
     * This does not check real types.
     */
    public function hasCommonType(UnionType $union_type): bool
    {
        $other_type_set = $union_type->type_set;
        if (\count($other_type_set) > 4 && \count($this->type_set) > 4) {
            return $this->hasCommonTypeSetCheck($other_type_set);
        }
        foreach ($this->type_set as $type) {
            if (\in_array($type, $other_type_set, true)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check if $this->type_set and $other_type_list have any types in common.
     * For two union types with 10 types, this takes at most 20 operations instead of 100 operations.
     *
     * @param list<Type> $other_type_list
     */
    private function hasCommonTypeSetCheck(array $other_type_list): bool
    {
        $type_set = [];
        // Avoid worst-case quadratic runtime
        foreach ($this->type_set as $type) {
            $type_set[\spl_object_id($type)] = true;
        }
        foreach ($other_type_list as $type) {
            if (\array_key_exists(\spl_object_id($type), $type_set)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return UnionType this union type without the subclasses/sub-types of $object_type
     */
    public function withoutSubclassesOf(CodeBase $code_base, Type $object_type): UnionType
    {
        return UnionType::of(
            self::typesWithoutSubclassesOf($code_base, $this->type_set, $object_type),
            self::typesWithoutSubclassesOf($code_base, $this->real_type_set, $object_type)
        );
    }

    /**
     * @param list<Type> $type_set
     * @return list<Type> the subset of types in $type_set excluding the subclasses/sub-types of $object_type
     */
    public static function typesWithoutSubclassesOf(CodeBase $code_base, array $type_set, Type $object_type): array
    {
        $is_nullable = false;
        $new_type_set = [];

        foreach ($type_set as $type) {
            if ($type->isNullable()) {
                $is_nullable = true;
            }
            if ($type->withIsNullable(false)->asExpandedTypes($code_base)->hasType($object_type)) {
                continue;
            }
            $new_type_set[] = $type;
        }
        if (!$is_nullable) {
            return $new_type_set;
        }
        if (!$new_type_set) {
            // There was a null somewhere in the old union type.
            return [NullType::instance(false)];
        }
        foreach ($new_type_set as $i => $type) {
            $new_type_set[$i] = $type->withIsNullable(true);
        }
        return $new_type_set;
    }

    /**
     * @return bool - True if not empty and at least one type is NullType or nullable.
     */
    public function containsNullable(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->isNullable()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return bool - True if empty or at least one type is NullType or nullable.
     * e.g. true for `?int`, `int|null`, or ``
     */
    public function containsNullableOrIsEmpty(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->isNullable()) {
                return true;
            }
        }
        return \count($this->type_set) === 0;
    }

    /**
     * @return bool - True if not empty and at least one type is NullType or mixed.
     */
    public function containsNullableOrMixed(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->isNullable() || $type instanceof MixedType) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return bool - True if not empty, not possibly undefined, and at least one type is NullType or nullable.
     */
    public function containsNullableOrUndefined(): bool
    {
        return $this->containsNullable();
    }

    /**
     * Returns true if is_null(expr) is unconditionally true for this type
     */
    public function isNull(): bool
    {
        foreach ($this->type_set as $type) {
            if (!($type instanceof NullType) && !($type instanceof VoidType)) {
                return false;
            }
        }
        return \count($this->type_set) !== 0;
    }

    /**
     * @return UnionType a clone of this that does not include null,
     *                   and has the non-null equivalents of any nullable types in this UnionType
     */
    public function nonNullableClone(): UnionType
    {
        return self::of(
            self::toNonNullableTypeList($this->type_set),
            self::toNonNullableTypeList($this->real_type_set)
        );
    }

    /**
     * @param Type[] $type_list
     * @return list<Type> a list of non-nullable types that may contain duplicates.
     */
    private static function toNonNullableTypeList(array $type_list): array
    {
        $result = [];
        foreach ($type_list as $type) {
            if (!$type->isNullable()) {
                $result[] = $type;
                continue;
            }
            if ($type instanceof NullType || $type instanceof VoidType) {
                continue;
            }

            $result[] = $type->withIsNullable(false);
        }
        return $result;
    }


    /**
     * @return UnionType a clone of this that has the nullable equivalents of any types in this UnionType
     * (e.g. returns '?T|?false' for 'T|false')
     */
    public function nullableClone(): UnionType
    {
        return self::of(
            self::toNullableTypeList($this->type_set),
            self::toNullableTypeList($this->real_type_set)
        );
    }

    /**
     * This can be used to handle the fact that the real types might be nullable to avoid false positives in redundant condition detection (e.g. accessing an array offset)
     */
    public function withNullableRealTypes(): UnionType
    {
        if (!$this->real_type_set) {
            return $this;
        }
        $real_type_set = self::toNullableTypeList($this->real_type_set);
        if ($real_type_set === $this->real_type_set) {
            return $this;
        }
        return self::of($this->type_set, $real_type_set);
    }

    /**
     * @param Type[] $type_list
     * @return list<Type> a list of nullable types that may contain duplicates.
     */
    private static function toNullableTypeList(array $type_list): array
    {
        $result = [];
        foreach ($type_list as $type) {
            if ($type->isNullable()) {
                $result[] = $type;
            } else {
                $result[] = $type->withIsNullable(true);
            }
        }
        return $result;
    }

    /**
     * Analogous to Type->withIsNullable()
     * @suppress PhanUnreferencedPublicMethod
     * @see self::nullableClone()
     * @see self::nonNullableClone()
     */
    public function withIsNullable(bool $is_nullable): UnionType
    {
        return $is_nullable ? $this->nullableClone() : $this->nonNullableClone();
    }

    /**
     * @return bool - True if type set is not empty and at least one type is NullType or nullable or FalseType or BoolType.
     * (I.e. the type is always falsey, or both sometimes falsey with a non-falsey type it can be narrowed down to)
     */
    public function containsFalsey(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->isPossiblyFalsey()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return UnionType a clone of this with any falsey types (null, false, falsey int/string literals, etc.) removed.
     */
    public function nonFalseyClone(): UnionType
    {
        return UnionType::of(
            self::toNonFalseyTypeSet($this->type_set),
            self::toNonFalseyTypeSet($this->real_type_set)
        );
    }

    /**
     * @param list<Type> $type_set
     * @return list<Type> which may contain duplicates
     */
    private static function toNonFalseyTypeSet(array $type_set): array
    {
        $result = [];
        foreach ($type_set as $type) {
            if (!$type->isPossiblyFalsey()) {
                $result[] = $type;
                continue;
            }
            if ($type->isAlwaysFalsey()) {
                // don't add null/false to the resulting type
                continue;
            }

            // add non-nullable equivalents, and replace BoolType with non-nullable TrueType
            $result[] = $type->asNonFalseyType();
        }
        // TODO: Preserve real types
        if ($result) {
            return $result;
        }
        static $non_empty_mixed = null;
        return $non_empty_mixed ?? ($non_empty_mixed = UnionType::typeSetFromString('non-empty-mixed'));
    }

    /**
     * @return bool - True if type set is not empty and at least one type is possibly truthy.
     */
    public function containsTruthy(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->isPossiblyTruthy()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if this contains at least one IntType or LiteralIntType
     */
    public function hasIntType(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type instanceof IntType) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if this contains at least one non-null IntType or LiteralIntType
     */
    public function hasNonNullIntType(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type instanceof IntType && !$type->isNullable()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if this type's real type set is exclusively non-null float types and is non-empty
     */
    public function isExclusivelyRealFloatTypes(): bool
    {
        foreach ($this->real_type_set as $type) {
            if (!($type instanceof FloatType) || $type->isNullable()) {
                return false;
            }
        }
        return \count($this->real_type_set) > 0;
    }

    /**
     * Returns true if this is exclusively non-null IntType or LiteralIntType
     */
    public function isNonNullIntType(): bool
    {
        if (\count($this->type_set) === 0) {
            return false;
        }
        foreach ($this->type_set as $type) {
            if (!($type instanceof IntType) || $type->isNullable()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if this is exclusively non-null IntType or LiteralIntType or FloatType or LiteralFloatType
     */
    public function isNonNullIntOrFloatType(): bool
    {
        if (\count($this->type_set) === 0) {
            return false;
        }
        foreach ($this->type_set as $type) {
            if (!($type instanceof IntType || $type instanceof FloatType) || $type->isNullable()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if this is exclusively non-null IntType or FloatType or subclasses
     */
    public function isNonNullNumberType(): bool
    {
        if (\count($this->type_set) === 0) {
            return false;
        }
        foreach ($this->type_set as $type) {
            if (!($type instanceof IntType || $type instanceof FloatType) || $type->isNullable()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if this contains at least one StringType or LiteralStringType
     */
    public function hasStringType(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type instanceof StringType) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if this contains at least one non-null StringType or LiteralStringType
     */
    public function hasNonNullStringType(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type instanceof StringType && !$type->isNullable()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if this is exclusively non-null StringType or LiteralStringType
     */
    public function isNonNullStringType(): bool
    {
        if (\count($this->type_set) === 0) {
            return false;
        }
        foreach ($this->type_set as $type) {
            if (!($type instanceof StringType) || $type->isNullable()) {
                return false;
            }
        }
        return true;
    }

    /**
     * @return bool if this contains at least one literal int/string type (e.g. `'myString'|false`)
     */
    public function hasLiterals(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type instanceof LiteralTypeInterface) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return UnionType result of converting literal int/string types to the non-literal equivalents
     * (e.g. converts `'myString'|false` to `string|false`)
     */
    public function asNonLiteralType(): UnionType
    {
        if (!$this->hasLiterals()) {
            return $this;
        }
        $new_types = [];
        foreach ($this->type_set as $type) {
            $new_types[] = $type->asNonLiteralType();
        }
        return UnionType::of($new_types);
    }

    /**
     * @return UnionType result of removing truthy types from this value
     * (e.g. converts `0|1|bool|\stdClass` to `0|false`)
     * (e.g. converts `?\stdClass` to `null`)
     */
    public function nonTruthyClone(): UnionType
    {
        return UnionType::of(
            self::toNonTruthyTypeSet($this->type_set),
            self::toNonTruthyTypeSet($this->real_type_set)
        );
    }

    /**
     * @param list<Type> $type_set
     * @return list<Type>
     */
    private static function toNonTruthyTypeSet(array $type_set): array
    {
        $result = [];
        foreach ($type_set as $type) {
            if (!$type->isPossiblyTruthy()) {
                $result[] = $type;
                continue;
            }
            if ($type->isAlwaysTruthy()) {
                // don't add null/false to the resulting type
                continue;
            }

            // add non-nullable equivalents, and replace BoolType with non-nullable TrueType
            $result[] = $type->asNonTruthyType();
        }
        return $result;
    }

    /**
     * @return bool - True if type set is not empty and at least one type is BoolType or FalseType
     */
    public function containsFalse(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->isPossiblyFalse()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return bool - True if type set is not empty and at least one type is BoolType or TrueType
     */
    public function containsTrue(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->isPossiblyTrue()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return UnionType result of removing false from this value
     * (e.g. converts `0|1|bool|\stdClass` to `0|1|true|\stdClass`)
     */
    public function nonFalseClone(): UnionType
    {
        $builder = new UnionTypeBuilder();
        $did_change = false;
        foreach ($this->type_set as $type) {
            if (!$type->isPossiblyFalse()) {
                $builder->addType($type);
                continue;
            }
            $did_change = true;
            if ($type->isAlwaysFalse()) {
                // don't add null/false to the resulting type
                continue;
            }

            // add non-nullable equivalents, and replace BoolType with non-nullable TrueType
            $builder->addType($type->asNonFalseType());
        }
        return $did_change ? $builder->getPHPDocUnionType() : $this;
    }

    /**
     * @return UnionType result of removing true from this value
     * (e.g. converts `0|1|bool|\stdClass` to `0|1|false|\stdClass`)
     */
    public function nonTrueClone(): UnionType
    {
        $builder = new UnionTypeBuilder();
        $did_change = false;
        foreach ($this->type_set as $type) {
            if (!$type->isPossiblyTrue()) {
                $builder->addType($type);
                continue;
            }
            $did_change = true;
            if ($type->isAlwaysTrue()) {
                // don't add null/false to the resulting type
                continue;
            }

            // add non-nullable equivalents, and replace BoolType with non-nullable TrueType
            $builder->addType($type->asNonTrueType());
        }
        return $did_change ? $builder->getPHPDocUnionType() : $this;
    }

    /**
     * @param UnionType $union_type
     * A union type to compare against
     *
     * @param Context $context
     * The context in which this type exists.
     *
     * @param CodeBase $code_base
     * The code base in which both this and the given union
     * types exist.
     *
     * @return bool
     * True if each type within this union type can cast
     * to the given union type.
     */
    // Currently unused and buggy, commenting this out.
    /**
    public function isExclusivelyNarrowedFormOrEquivalentTo(
        UnionType $union_type,
        Context $context,
        CodeBase $code_base
    ) : bool {

        // Special rule: anything can cast to nothing
        // and nothing can cast to anything
        if ($union_type->isEmpty() || $this->isEmpty()) {
            return true;
        }

        // Check to see if the types are equivalent
        if ($this->isEqualTo($union_type)) {
            return true;
        }
        // TODO: Allow casting MyClass<TemplateType> to MyClass (Without the template?)

        // Resolve 'static' for the given context to
        // determine what's actually being referred
        // to in concrete terms.
        $other_resolved_type =
            $union_type->withStaticResolvedInContext($context);
        $other_resolved_type_set = $other_resolved_type->type_set;

        // Convert this type to a set of resolved types to iterate over.
        $this_resolved_type_set =
            $this->withStaticResolvedInContext($context)->type_set;

        // Convert this type to an array of resolved
        // types.
        $type_set =
            $this->withStaticResolvedInContext($context)
            ->getTypeSet();
        // TODO: Need to resolve expanded union types (parents, interfaces) of classes *before* this is called.

        // Test to see if every single type in this union
        // type can cast to the given union type.
        foreach ($this_resolved_type_set as $type) {
            // First check if this contains the type as an optimization.
            if ($other_resolved_type_set->contains($type)) {
                continue;
            }
            $expanded_types = $type->asExpandedTypes($code_base);
            if ($other_resolved_type->canCastToUnionType(
                $expanded_types
            )) {
                continue;
            }
        }
        return true;
    }
     */

    // TODO: Callers should call withStaticResolvedInContext?
    public function isExclusivelyNarrowedFormOf(CodeBase $code_base, UnionType $other): bool
    {
        if ($other->isEmpty()) {
            return true;
        }
        if ($this->isEmpty() || $this->hasMixedType()) {
            return false;
        }
        foreach ($this->type_set as $type) {
            if (!$type->asPHPDocUnionType()->canStrictCastToUnionType($code_base, $other)) {
                return false;
            }
        }
        return true;
    }

    /**
     * @param Type[] $type_list
     * A list of types
     *
     * @return bool
     * True if this union type contains any of the given
     * named types
     */
    public function hasAnyType(array $type_list): bool
    {
        $type_set = $this->type_set;
        if (\count($type_set) === 0) {
            return false;
        }
        foreach ($type_list as $type) {
            if (\in_array($type, $type_set, true)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return bool
     * True if this type has any subtype of the `iterable` type (e.g. `Traversable`, `Array`).
     */
    public function hasIterable(): bool
    {
        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type->isIterable();
        });
    }

    /**
     * Returns this union type after asserting is_iterable($x).
     *
     * @return UnionType
     * A UnionType with known iterable types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     * @suppress PhanUnreferencedPublicMethod
     */
    public function iterableTypesStrictCast(CodeBase $code_base): UnionType
    {
        return UnionType::of(
            self::castToIterableTypesStrict($code_base, $this->type_set, false),
            self::castToIterableTypesStrict($code_base, $this->real_type_set, false)
        );
    }

    /**
     * Similar to iterableTypesStrictCast, but also
     * casts callable-object and object to Traversable.
     * TODO: Could check if classes are final.
     */
    public function iterableTypesStrictCastAssumeTraversable(CodeBase $code_base): UnionType
    {
        return UnionType::of(
            self::castToIterableTypesStrict($code_base, $this->type_set, true),
            self::castToIterableTypesStrict($code_base, $this->real_type_set, true)
        );
    }

    /**
     * Casts the type list to non-null iterables and sub-types of iterable.
     * Preserve classes implementing Traversable.
     * @param Type[] $type_list
     * @return list<Type>
     */
    private static function castToIterableTypesStrict(CodeBase $code_base, array $type_list, bool $object_to_traversable): array
    {
        $result = [];
        foreach ($type_list as $type) {
            $new_type = $type->asIterable($code_base);
            if ($new_type) {
                $result[] = $new_type;
            } elseif ($object_to_traversable && $type->isPossiblyObject()) {
                $result[] = Type::traversableInstance();
            }
        }
        return $result ?: [IterableType::instance(false)];
    }

    /**
     * @return int
     * The number of types in this union type
     */
    public function typeCount(): int
    {
        return \count($this->type_set);
    }

    /**
     * @return bool
     * True if this Union has no types
     */
    public function isEmpty(): bool
    {
        return \count($this->type_set) === 0;
    }

    /**
     * @return bool
     * True if this Union has no types or is the mixed type
     */
    public function isEmptyOrMixed(): bool
    {
        foreach ($this->type_set as $type) {
            if (!$type instanceof MixedType) {
                return false;
            }
        }
        return true;
    }

    /**
     * @param UnionType $target
     * The type we'd like to see if this type can cast
     * to
     *
     * @param CodeBase $code_base
     * The code base used to expand types
     *
     * @return bool
     * Test to see if this type can be cast to the
     * given type after expanding both union types
     * to include all ancestor types
     *
     * TODO: ensure that this is only called after the parse phase is over.
     */
    public function canCastToExpandedUnionType(
        UnionType $target,
        CodeBase $code_base
    ): bool {

        $this_expanded =
            $this->asExpandedTypes($code_base);

        $target_expanded =
            $target->asExpandedTypes($code_base);

        return
            $this_expanded->canCastToUnionType(
                $target_expanded
            );
    }

    /**
     * Check if a class with templates can be cast to another class with templates
     * At least one of the source or target is expected to have templates when this is called.
     *
     * This allows casting Some<\MyClass> to cast to Option<\MyClass>, but not Option<\UnrelatedClass>
     */
    public function canCastToUnionTypeHandlingTemplates(
        UnionType $target,
        CodeBase $code_base
    ): bool {
        // Fast-track most common cases first
        $type_set = $this->type_set;
        // If either type is unknown, we can't call it
        // a success
        if (\count($type_set) === 0) {
            return true;
        }
        $target_type_set = $target->type_set;
        if (\count($target_type_set) === 0) {
            return true;
        }
        $target = $target->asNormalizedTypes();

        // T overlaps with T, a future call to Type->canCastToType will pass.
        if ($this->hasCommonType($target)) {
            return true;
        }
        static $float_type;
        static $int_type;
        static $mixed_type;
        static $null_type;
        if ($null_type === null) {
            $int_type   = IntType::instance(false);
            $float_type = FloatType::instance(false);
            $mixed_type = MixedType::instance(false);
            $null_type  = NullType::instance(false);
        }

        if (Config::get_null_casts_as_any_type()) {
            // null <-> null
            if ($this->isType($null_type)
                || $target->isType($null_type)
            ) {
                return true;
            }
        } else {
            // If null_casts_as_any_type isn't set, then try the other two fallbacks.
            if (Config::get_null_casts_as_array() && $this->isType($null_type) && $target->hasArrayLike()) {
                return true;
            } elseif (Config::get_array_casts_as_null() && $target->isType($null_type) && $this->hasArrayLike()) {
                return true;
            }
        }

        // mixed <-> mixed
        if (\in_array($mixed_type, $type_set, true)
            || \in_array($mixed_type, $target_type_set, true)
        ) {
            return true;
        }

        // int -> float
        if (\in_array($int_type, $type_set, true)
            && \in_array($float_type, $target_type_set, true)
        ) {
            return true;
        }

        // Check conversion on the cross product of all
        // type combinations and see if any can cast to
        // any.
        foreach ($type_set as $source_type) {
            if ($source_type->canCastToAnyTypeInSetHandlingTemplates($target_type_set, $code_base)) {
                return true;
            }
        }

        // Allow casting ?T to T|null for any type T. Check if null is part of this type first.
        if (\in_array($null_type, $target_type_set, true)) {
            foreach ($type_set as $source_type) {
                // Only redo this check for the nullable types, we already failed the checks for non-nullable types.
                if ($source_type->isNullable()) {
                    // TODO: Add unit tests of nullable templates
                    return $source_type->withIsNullable(false)->canCastToAnyTypeInSetHandlingTemplates($target_type_set, $code_base);
                }
            }
        }

        // Only if no source types can be cast to any target
        // types do we say that we cannot perform the cast
        return false;
    }

    /**
     * @param UnionType $target
     * A type to check to see if this can cast to it
     *
     * @return bool
     * True if this type is allowed to cast to the given type
     * i.e. int->float is allowed  while float->int is not.
     */
    public function canCastToUnionType(
        UnionType $target
    ): bool {
        // Fast-track most common cases first
        $type_set = $this->type_set;
        // If either type is unknown, we can't call it
        // a success
        if (\count($type_set) === 0) {
            return true;
        }
        $target_type_set = $target->type_set;
        if (\count($target_type_set) === 0) {
            return true;
        }

        // T overlaps with T, a future call to Type->canCastToType will pass.
        $target = $target->asNormalizedTypes();
        if ($this->hasCommonType($target)) {
            return true;
        }

        static $float_type;
        static $int_type;
        static $mixed_type;
        static $null_type;
        if ($null_type === null) {
            $int_type   = IntType::instance(false);
            $float_type = FloatType::instance(false);
            $mixed_type = MixedType::instance(false);
            $null_type  = NullType::instance(false);
        }

        if (Config::get_null_casts_as_any_type()) {
            // null <-> null
            // (this fork has weaker type casting rules than phan/phan, using hasType instead of isType)
            if ($this->hasType(NullType::instance(false))
                || $target->isType(NullType::instance(false))
            ) {
                return true;
            }
        } elseif (Config::get_null_casts_as_array() && $this->hasType(NullType::instance(false)) && $target->hasArrayLike()) {
            // null->array
            return true;
        } elseif (Config::get_array_casts_as_null() && $target->isType(NullType::instance(false)) && $this->hasArrayLike()) {
            // array -> null
            return true;
        }

        // mixed <-> mixed
        if (\in_array($mixed_type, $type_set, true)
            || \in_array($mixed_type, $target_type_set, true)
        ) {
            return true;
        }

        // int -> float
        if (\in_array($int_type, $type_set, true)
            && \in_array($float_type, $target_type_set, true)
        ) {
            return true;
        }

        // Check conversion on the cross product of all
        // type combinations and see if any can cast to
        // any.
        foreach ($type_set as $source_type) {
            if ($source_type->canCastToAnyTypeInSet($target_type_set)) {
                return true;
            }
        }

        // Allow casting ?T to T|null for any type T. Check if null is part of this type first.
        if (\in_array($null_type, $target_type_set, true)) {
            foreach ($type_set as $source_type) {
                // Only redo this check for the nullable types, we already failed the checks for non-nullable types.
                if ($source_type->isNullable()) {
                    return $source_type->withIsNullable(false)->canCastToAnyTypeInSet($target_type_set);
                }
            }
        }

        // Only if no source types can be cast to any target
        // types do we say that we cannot perform the cast
        return false;
    }

    /**
     * @param UnionType $target
     * A type to check to see if this can cast to it
     *
     * @return bool
     * True if this type is allowed to cast to the given type
     * (intended to ignore any permissive config settings, such as null_casts_as_any_type)
     * i.e. int->float is allowed  while float->int is not.
     */
    public function canCastToUnionTypeWithoutConfig(
        UnionType $target
    ): bool {
        // Fast-track most common cases first
        $type_set = $this->type_set;
        // If either type is unknown, we can't call it
        // a success
        if (\count($type_set) === 0) {
            return true;
        }
        $target_type_set = $target->type_set;
        if (\count($target_type_set) === 0) {
            return true;
        }

        // T overlaps with T, a future call to Type->canCastToType will pass.
        $target = $target->asNormalizedTypes();
        if ($this->hasCommonType($target)) {
            return true;
        }

        static $float_type;
        static $int_type;
        static $mixed_type;
        static $null_type;
        if ($null_type === null) {
            $int_type   = IntType::instance(false);
            $float_type = FloatType::instance(false);
            $mixed_type = MixedType::instance(false);
            $null_type  = NullType::instance(false);
        }

        // mixed <-> mixed
        if (\in_array($mixed_type, $type_set, true)
            || \in_array($mixed_type, $target_type_set, true)
        ) {
            return true;
        }

        // int -> float
        if (\in_array($int_type, $type_set, true)
            && \in_array($float_type, $target_type_set, true)
        ) {
            return true;
        }

        // Check conversion on the cross product of all
        // type combinations and see if any can cast to
        // any.
        foreach ($type_set as $source_type) {
            if ($source_type->canCastToAnyTypeInSetWithoutConfig($target_type_set)) {
                return true;
            }
        }

        // Allow casting ?T to T|null for any type T. Check if null is part of this type first.
        if (\in_array($null_type, $target_type_set, true)) {
            foreach ($type_set as $source_type) {
                // Only redo this check for the nullable types, we already failed the checks for non-nullable types.
                if ($source_type->isNullable()) {
                    return $source_type->withIsNullable(false)->canCastToAnyTypeInSetWithoutConfig($target_type_set);
                }
            }
        }

        // Only if no source types can be cast to any target
        // types do we say that we cannot perform the cast
        return false;
    }

    /**
     * Precondition: $this->canCastToUnionType() is false.
     *
     * This tells us if it would have succeeded if the source type was not nullable.
     *
     * @internal
     */
    public function canCastToUnionTypeIfNonNull(UnionType $target): bool
    {
        $non_null = $this->nonNullableClone();
        if ($non_null === $this) {
            // This wasn't nullable in the first place
            return false;
        }
        if ($non_null->isEmpty()) {
            // This was exclusively null - It should be a full TypeMismatch
            return false;
        }
        return $non_null->canCastToUnionType($target);
    }

    /**
     * Checks if any type in this union type can strictly cast to the other type
     *
     * E.g. allows ?T|Other -> T, null|Other -> ?T, ?T -> ?Other
     * Does not allow ?T -> Other, etc.
     *
     * @param bool $allow_casting
     * If true, allow non-identical types to cast to other types if that would be permitted by casting rules for param/return types
     * (e.g. int -> float)
     */
    public function canAnyTypeStrictCastToUnionType(CodeBase $code_base, UnionType $target, bool $allow_casting = true): bool
    {
        foreach ($this->type_set as $type) {
            if ($type instanceof IntType && !$allow_casting) {
                if (!$target->hasTypeMatchingCallback(static function (Type $type): bool {
                    return $type instanceof IntType || $type instanceof MixedType;
                })
                ) {
                    continue;
                }
            }
            if ($type->asPHPDocUnionType()->canStrictCastToUnionType($code_base, $target)) {
                return true;
            }
        }
        return \count($this->type_set) === 0 || ($this->containsNullable() && $target->containsNullable());
    }

    /**
     * @param UnionType $target
     * A type to check to see if this has subtypes of.
     *
     * @return bool
     * True if this type contains subtypes of the other type
     *
     * i.e. array -> iterable is allowed, but iterable -> array is not
     * i.e. MyClass -> mixed is allowed, but mixed -> MyClass is not
     */
    public function hasSubtypeOf(
        UnionType $target
    ): bool {
        // Fast-track most common cases first
        $type_set = $this->type_set;
        // If either type is unknown, we can't call it
        // a success
        if (\count($type_set) === 0) {
            return true;
        }
        $target_type_set = $target->type_set;
        if (\count($target_type_set) === 0) {
            return true;
        }

        // T overlaps with T, a future call to Type->canCastToType will pass.
        $target = $target->asNormalizedTypes();
        if ($this->hasCommonType($target)) {
            return true;
        }

        static $float_type;
        static $int_type;
        static $mixed_type;
        static $null_type;
        if ($null_type === null) {
            $int_type   = IntType::instance(false);
            $float_type = FloatType::instance(false);
            $mixed_type = MixedType::instance(false);
            $null_type  = NullType::instance(false);
        }

        // any -> mixed
        if (\in_array($mixed_type, $target_type_set, true)) {
            return true;
        }

        // int -> float
        if (\in_array($int_type, $type_set, true)
            && \in_array($float_type, $target_type_set, true)
        ) {
            return true;
        }

        // Check conversion on the cross product of all
        // type combinations and see if any can cast to
        // any.
        foreach ($type_set as $source_type) {
            if ($source_type->isSubtypeOfAnyTypeInSet($target_type_set)) {
                return true;
            }
        }

        // Allow casting ?T to T|null for any type T. Check if null is part of this type first.
        if (\in_array($null_type, $target_type_set, true)) {
            foreach ($type_set as $source_type) {
                // Only redo this check for the nullable types, we already failed the checks for non-nullable types.
                if ($source_type->isNullable()) {
                    if ($source_type->withIsNullable(false)->isSubtypeOfAnyTypeInSet($target_type_set)) {
                        return true;
                    }
                }
            }
        }

        // Only if no source types can be cast to any target
        // types do we say that we cannot perform the cast
        return false;
    }

    /**
     * Checks if any type in this union type weakly overlaps with other types
     *
     * E.g. allows `1 <= 2`, `null == false`, ?T -> ?Other, etc.
     * Does not allow ?T -> Other, etc.
     *
     * NOTE: callers should check that
     */
    protected function canAnyTypeWeakOverlapUnionType(UnionType $target): bool
    {
        foreach ($this->type_set as $type) {
            foreach ($target->type_set as $other_type) {
                if ($type->weaklyOverlaps($other_type)) {
                    return true;
                }
            }
        }
        return \count($this->type_set) === 0 || ($this->containsNullable() && $target->containsNullable());
    }

    /**
     * @param UnionType $target
     * A type to check to see if this can cast to it.
     *
     * Every single type in this type must be able to cast to a type in $target (Empty types can cast to empty)
     *
     * @return bool
     * True if every type in this union type is allowed to cast to the given union type.
     * i.e. int->float is allowed while int|object->float is not.
     *
     * @suppress PhanUnreferencedPublicMethod may be used elsewhere in the future
     */
    public function canStrictCastToUnionType(CodeBase $code_base, UnionType $target): bool
    {
        // Fast-track most common cases first
        $type_set = $this->type_set;
        // If either type is unknown, we can't call it
        // a success
        if (\count($type_set) === 0) {
            return true;
        }
        $target_type_set = $target->type_set;
        if (\count($target_type_set) === 0) {
            return true;
        }

        // every single type in T overlaps with T, a future call to Type->canCastToType will pass.
        $matches = true;
        foreach ($type_set as $type) {
            if (!\in_array($type, $target_type_set, true)) {
                $matches = false;
                break;
            }
        }
        if ($matches) {
            return true;
        }
        static $null_type;
        if ($null_type === null) {
            $null_type  = NullType::instance(false);
        }

        // Check conversion on the cross product of all
        // type combinations and see if any can cast to
        // any.
        $matches = true;
        foreach ($type_set as $source_type) {
            if (!$source_type->asExpandedTypes($code_base)->canCastToUnionTypeWithoutConfig($target)) {
                $matches = false;
                break;
            }
        }
        if ($matches) {
            return true;
        }

        // Allow casting ?T to T|null for any type T. Check if null is part of this type first.
        if (\in_array($null_type, $target_type_set, true)) {
            foreach ($type_set as $source_type) {
                // Only redo this check for the nullable types, we already failed the checks for non-nullable types.
                if (!$source_type->withIsNullable(false)->asExpandedTypes($code_base)->canCastToUnionTypeWithoutConfig($target)) {
                    return false;
                }
            }
            return true;
        }

        // Only if no source types can be cast to any target
        // types do we say that we cannot perform the cast
        return false;
    }

    /**
     * @param UnionType $target
     * A type to check to see if this is a strict subtype of this.
     *
     * Every single type in this type must be a strict subtype of a type in $target (Empty types can cast to empty)
     *
     * @return bool
     * True if every single type in this type is a strict subtype of a type in $target (Empty types can cast to empty)
     * i.e. int->float is allowed  while float->int is not.
     *
     * @suppress PhanUnreferencedPublicMethod may be used elsewhere in the future
     */
    public function isStrictSubtypeOf(CodeBase $code_base, UnionType $target): bool
    {
        // Fast-track most common cases first
        $type_set = $this->type_set;
        // If either type is unknown, we can't call it
        // a success
        if (\count($type_set) === 0) {
            return true;
        }
        $target_type_set = $target->type_set;
        if (\count($target_type_set) === 0) {
            return true;
        }

        // every single type in this overlaps with the target, so the strict cast will succeed.
        $matches = true;
        foreach ($type_set as $type) {
            if (!\in_array($type, $target_type_set, true)) {
                $matches = false;
                break;
            }
        }
        if ($matches) {
            return true;
        }
        static $null_type;
        if ($null_type === null) {
            $null_type  = NullType::instance(false);
        }

        // Check conversion on the cross product of all
        // type combinations and see if any can cast to
        // any.
        $matches = true;
        foreach ($type_set as $source_type) {
            if (!$source_type->asExpandedTypes($code_base)->hasSubtypeOf($target)) {
                $matches = false;
                break;
            }
        }
        if ($matches) {
            return true;
        }

        // Allow casting ?T to T|null for any type T. Check if null is part of this type first.
        if (\in_array($null_type, $target_type_set, true)) {
            foreach ($type_set as $source_type) {
                // Only redo this check for the nullable types, we already failed the checks for non-nullable types.
                if (!$source_type->withIsNullable(false)->asExpandedTypes($code_base)->hasSubtypeOf($target)) {
                    return false;
                }
            }
            return true;
        }

        // Only if no source types can be cast to any target
        // types do we say that we cannot perform the cast
        return false;
    }

    /**
     * Check if these types have any possible types in common.
     * (e.g. mixed <-> string, etc.)
     *
     * TODO: Make this work for callable <-> string, etc.
     * @suppress PhanUnreferencedPublicMethod
     */
    public function hasAnyTypeOverlap(CodeBase $code_base, UnionType $other): bool
    {
        return $this->canAnyTypeStrictCastToUnionType($code_base, $other, false) ||
            $other->canAnyTypeStrictCastToUnionType($code_base, $this, false);
    }

    /**
     * Check if these types have any possible similar types for weak type comparisons.
     * (e.g. allows false == null checks)
     *
     * @suppress PhanUnreferencedPublicMethod
     */
    public function hasAnyWeakTypeOverlap(UnionType $other): bool
    {
        return $this->canAnyTypeWeakOverlapUnionType($other) || $other->canAnyTypeWeakOverlapUnionType($this);
    }

    /**
     * Used for deciding whether to emit PhanTypeMismatchReturnReal.
     * Precondition: The source and target types are non-empty.
     *
     * - Doesn't allow null or void to cast to $other even if with null casting allowed in config,
     *   because that would always throw at runtime
     * - The strictness (e.g. allowing casts from string to bool) depends on the strict_types setting of the Context from which the source type was found.
     */
    public function canCastToDeclaredType(CodeBase $code_base, Context $context, UnionType $other): bool
    {
        if ($this->isNull()) {
            return $other->containsNullableOrMixed();
        }
        $other_type_set = $other->getTypeSet();
        if (!$other_type_set) {
            return true;
        }
        $type_set = $this->type_set;
        if (!$type_set) {
            return true;
        }
        foreach ($type_set as $type) {
            $type = $type->withIsNullable(false);
            foreach ($other_type_set as $other) {
                if ($type->canCastToDeclaredType($code_base, $context, $other)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * @return bool
     * True if all types in this union are definitely scalars
     * @see scalarTypes
     * @see scalarTypesStrict
     * @suppress PhanUnreferencedPublicMethod
     */
    public function isScalar(): bool
    {
        if ($this->isEmpty()) {
            return false;
        }

        return $this->allTypesMatchCallback(static function (Type $type): bool {
            return $type->isScalar();
        });
    }

    /**
     * @return bool
     * True if any types in this union are a printable scalar, or this is the empty union type
     */
    public function hasPrintableScalar(): bool
    {
        if ($this->isEmpty()) {
            return true;
        }

        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type->isPrintableScalar();
        });
    }

    /**
     * @return bool
     * True if any types in this union are a valid operand for a bitwise operator ('|', '&', or '^').
     */
    public function hasValidBitwiseOperand(): bool
    {
        if ($this->isEmpty()) {
            return true;
        }

        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type->isValidBitwiseOperand();
        });
    }

    /**
     * @return bool
     * True if this union has array-like types (is of type array, is
     * a generic array, or implements ArrayAccess).
     */
    public function hasArrayLike(): bool
    {
        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type->isArrayLike();
        });
    }

    /**
     * @return bool
     * True if this union has array-like types (is of type array,
     * or is a generic array)
     */
    public function hasArray(): bool
    {
        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type instanceof ArrayType;
        });
    }

    /**
     * @return bool
     * True if this union has array-like types (is of type array, is
     * a generic array, is an array shape, or implements ArrayAccess).
     */
    public function hasGenericArray(): bool
    {
        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type instanceof GenericArrayInterface;
        });
    }

    /**
     * @return bool
     * True if this union contains the ArrayAccess type.
     * (Call asExpandedTypes() first to check for subclasses of ArrayAccess)
     */
    public function hasArrayAccess(): bool
    {
        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type->isArrayAccess();
        });
    }

    /**
     * Returns the types of this union type that are arrays or are types implementing ArrayAccess.
     *
     * This is useful for code inferring the types of dimensions of a union type.
     */
    public function asArrayOrArrayAccessSubTypes(CodeBase $code_base): UnionType
    {
        $result = UnionType::empty();
        foreach ($this->type_set as $type) {
            if ($type->isArrayOrArrayAccessSubType($code_base)) {
                $result = $result->withType($type);
            }
        }
        return $result;
    }

    /**
     * @return bool
     * True if this union contains the Traversable type.
     * (Call asExpandedTypes() first to check for subclasses of Traversable)
     * @suppress PhanUnreferencedPublicMethod not used right now.
     */
    public function hasTraversable(): bool
    {
        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type->isTraversable();
        });
    }

    /**
     * @return bool
     * True if this union type represents types that are
     * array-like, and nothing else (e.g. can't be null).
     * If any of the array-like types are nullable, this returns false.
     */
    public function isExclusivelyArrayLike(): bool
    {
        if ($this->isEmpty()) {
            return false;
        }

        return $this->allTypesMatchCallback(static function (Type $type): bool {
            return $type->isArrayLike() && !$type->isNullable();
        });
    }

    /**
     * @return bool
     * True if this union type represents types that are arrays
     * or generic arrays, but nothing else.
     */
    public function isExclusivelyArray(): bool
    {
        if ($this->isEmpty()) {
            return false;
        }

        return $this->allTypesMatchCallback(static function (Type $type): bool {
            return $type instanceof ArrayType && !$type->isNullable();
        });
    }

    /**
     * @return UnionType
     * Get the subset of types which are not native
     */
    public function nonNativeTypes(): UnionType
    {
        return $this->makeFromFilter(static function (Type $type): bool {
            return !$type->isNativeType();
        });
    }

    /**
     * A memory efficient way to create a UnionType from a filter operation.
     * If this the filter preserves everything, returns $this instead
     */
    public function makeFromFilter(Closure $cb): UnionType
    {
        return UnionType::of(
            \array_filter($this->type_set, $cb),
            \array_filter($this->real_type_set, $cb)
        );
    }

    /**
     * Returns a list of class FQSENs representing the non-native types
     * associated with this UnionType.
     *
     * @param Context $context
     * The context in which we're resolving this union
     * type.
     *
     * @return Generator
     * @phan-return Generator<FullyQualifiedClassName>
     *
     * @throws CodeBaseException
     * An exception is thrown if a non-native type does not have
     * an associated class
     *
     * @throws IssueException
     * An exception is thrown if static is used as a type outside of an object
     * context
     *
     * TODO: Add a method to ContextNode to directly get FQSEN instead?
     */
    public function asClassFQSENList(
        Context $context
    ): Generator {
        // Iterate over each viable class type to see if any
        // have the constant we're looking for
        foreach ($this->type_set as $class_type) {
            if ($class_type->isNativeType()) {
                continue;
            }
            // Get the class FQSEN
            $class_fqsen = FullyQualifiedClassName::fromType($class_type);

            if ($class_type->isStaticType()) {
                if (!$context->isInClassScope()) {
                    throw new IssueException(
                        Issue::fromType(Issue::ContextNotObject)(
                            $context->getFile(),
                            $context->getLineNumberStart(),
                            [
                                $class_type->getName()
                            ]
                        )
                    );
                }
                yield $class_fqsen;
            } else {
                yield $class_fqsen;
            }
        }
    }

    /**
     * @param CodeBase $code_base
     * The code base in which to find classes
     *
     * @param Context $context
     * The context in which we're resolving this union
     * type.
     *
     * @return Generator<Clazz>
     *
     * A list of classes representing the non-native types
     * associated with this UnionType
     *
     * @throws CodeBaseException
     * An exception is thrown if a non-native type does not have
     * an associated class
     *
     * @throws IssueException
     * An exception is thrown if static is used as a type outside of an object
     * context
     */
    public function asClassList(
        CodeBase $code_base,
        Context $context
    ): Generator {
        // Iterate over each viable class type to see if any
        // have the constant we're looking for
        foreach ($this->type_set as $class_type) {
            if ($class_type->isNativeType()) {
                continue;
            }
            if ($class_type->isStaticType()) {
                if (!$context->isInClassScope()) {
                    throw new IssueException(
                        Issue::fromType(Issue::ContextNotObject)(
                            $context->getFile(),
                            $context->getLineNumberStart(),
                            [
                                $class_type->getName()
                            ]
                        )
                    );
                }
                yield $context->getClassInScope($code_base);
                continue;
            }

            if ($class_type->isSelfType()) {
                if (!$context->isInClassScope()) {
                    throw new IssueException(
                        Issue::fromType(Issue::ContextNotObject)(
                            $context->getFile(),
                            $context->getLineNumberStart(),
                            [
                                $class_type->getName()
                            ]
                        )
                    );
                }
                yield $context->getClassInScope($code_base);
                continue;
            }
            // Get the class FQSEN
            $class_fqsen = FullyQualifiedClassName::fromType($class_type);
            // See if the class exists
            if (!$code_base->hasClassWithFQSEN($class_fqsen)) {
                throw new CodeBaseException(
                    $class_fqsen,
                    "Cannot find class $class_fqsen"
                );
            }
            $class = $code_base->getClassByFQSEN($class_fqsen);
            if (!$class->isPHPInternal() && $code_base->hasClassWithFQSEN($class_fqsen->withAlternateId(1))) {
                self::emitRedefinedClassReferenceWarning($code_base, $context, $class_fqsen);
            }

            yield $class;
        }
    }

    /**
     * Warn about both "$class_fqsen" and the first alternate existing.
     *
     * TODO: Make callers such as ContextNode->asClassList() check if the reference is 'self' and don't emit this if it is?
     */
    public static function emitRedefinedClassReferenceWarning(
        CodeBase $code_base,
        Context $context,
        FullyQualifiedClassName $class_fqsen
    ): void {
        $class = $code_base->getClassByFQSENWithoutHydrating($class_fqsen);
        if ($class->isPHPInternal() || $class->hasSuppressIssue(Issue::RedefinedClassReference)) {
            // already checked if $class was internal.
            return;
        }

        $other_fqsen = $class_fqsen->withAlternateId($class_fqsen->getAlternateId() ? 0 : 1);
        if (!$code_base->hasClassWithFQSEN($other_fqsen)) {
            return;
        }
        $other_class = $code_base->getClassByFQSENWithoutHydrating($other_fqsen);

        if ($other_class->isPHPInternal() || $other_class->hasSuppressIssue(Issue::RedefinedClassReference)) {
            // already checked if $class was internal.
            return;
        }
        Issue::maybeEmit(
            $code_base,
            $context,
            Issue::RedefinedClassReference,
            $context->getLineNumberStart(),
            $class_fqsen->withAlternateId(0),
            $class->getContext()->getFile(),
            $class->getContext()->getLineNumberStart(),
            $other_class->getContext()->getFile(),
            $other_class->getContext()->getLineNumberStart()
        );
    }

    /**
     * Takes `a|b[]|c|d[]|e` and returns `a|c|e`
     *
     * @return UnionType
     * A UnionType with generic array types filtered out
     *
     * @suppress PhanUnreferencedPublicMethod
     */
    public function nonGenericArrayTypes(): UnionType
    {
        return $this->makeFromFilter(static function (Type $type): bool {
            return !($type instanceof GenericArrayInterface);
        });
    }

    /**
     * Takes `a|b[]|c|d[]|e` and returns `b[]|d[]`
     *
     * @return UnionType
     * A UnionType with generic array types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     */
    public function genericArrayTypes(): UnionType
    {
        return $this->makeFromFilter(static function (Type $type): bool {
            return $type instanceof GenericArrayInterface;
        });
    }

    /**
     * Takes `MyClass|int|array|?object` and returns `MyClass|?object`
     *
     * @return UnionType
     * A UnionType with known object types kept, other types filtered out.
     *
     * @see objectTypesStrict
     */
    public function objectTypes(): UnionType
    {
        return $this->makeFromFilter(static function (Type $type): bool {
            return $type->isObject();
        });
    }

    /**
     * Takes `MyClass|int|array|?object` and returns `MyClass|object`
     *
     * Takes `` and returns `object`
     *
     * Takes `callable|iterable` and returns `callable-object|Traversable`
     */
    public function objectTypesStrict(): UnionType
    {
        return UnionType::of(
            self::castToObjectTypesStrict($this->type_set) ?: [ObjectType::instance(false)],
            self::castToObjectTypesStrict($this->real_type_set) ?: [ObjectType::instance(false)]
        );
    }

    /**
     * Takes `MyClass|int|array|?object` and returns `MyClass|object`
     *
     * Takes `` and returns ``
     *
     * Takes `callable|iterable` and returns `callable-object|Traversable`
     * @suppress PhanUnreferencedPublicMethod called dynamically
     */
    public function objectTypesStrictAllowEmpty(): UnionType
    {
        return UnionType::of(
            self::castToObjectTypesStrict($this->type_set),
            self::castToObjectTypesStrict($this->real_type_set)
        );
    }

    /**
     * @param Type[] $type_list
     * @return list<Type> $type_list
     */
    private static function castToObjectTypesStrict(array $type_list): array
    {
        $result = [];
        foreach ($type_list as $type) {
            $type = $type->asObjectType();
            if ($type) {
                $result[] = $type;
            }
        }
        return $result;
    }

    /**
     * Takes `MyClass|int|array|?object` and returns `MyClass`
     *
     * @return UnionType
     * A UnionType with known object types with known FQSENs kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     */
    public function objectTypesWithKnownFQSENs(): UnionType
    {
        return $this->makeFromFilter(static function (Type $type): bool {
            return $type->isObjectWithKnownFQSEN();
        });
    }

    /**
     * Returns true if objectTypes would be non-empty.
     */
    public function hasObjectTypes(): bool
    {
        return $this->hasTypeMatchingCallback((static function (Type $type): bool {
            return $type->isObject();
        }));
    }

    /**
     * Returns true if at least one type could possibly be an object.
     * E.g. returns true for iterator.
     * NOTE: this returns false for `mixed`
     */
    public function hasPossiblyObjectTypes(): bool
    {
        return $this->hasTypeMatchingCallback((static function (Type $type): bool {
            return $type->isPossiblyObject();
        }));
    }

    /**
     * Returns true if this union type is empty, or if it's possible for any type (or sub-type) of this union type to be able to cast to $class_type
     */
    public function canPossiblyCastToClass(CodeBase $code_base, Type $class_type): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->withIsNullable(false)->canPossiblyCastToClass($code_base, $class_type)) {
                return true;
            }
        }
        return \count($this->type_set) === 0;
    }

    /**
     * Returns true if this union type is non-empty and all types inherit this class/trait/interface.
     */
    public function isExclusivelySubclassesOf(CodeBase $code_base, Type $class_type): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->isNullable() || !$type->asExpandedTypes($code_base)->hasType($class_type)) {
                return false;
            }
        }
        return \count($this->type_set) > 0;
    }
    /**
     * Returns the types for which is_scalar($x) would be true.
     * This means null/nullable is removed.
     * Takes `MyClass|int|?bool|array|?object` and returns `int|bool`
     * Takes `?MyClass` and returns an empty union type.
     *
     * @return UnionType
     * A UnionType with known scalar types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     */
    public function scalarTypes(): UnionType
    {
        return $this->nonNullableClone()->makeFromFilter(static function (Type $type): bool {
            return $type->isScalar();
        });
    }

    /**
     * A union type after asserting is_scalar($x)
     *
     */
    public function scalarTypesStrict(bool $allow_empty = false): UnionType
    {
        // NOTE: this is referenced dynamically in ConditionVisitor
        if ($allow_empty) {
            return UnionType::of(
                self::typeSetToScalarTypesStrict($this->type_set),
                self::typeSetToScalarTypesStrict($this->real_type_set)
            );
        } else {
            static $default;
            if ($default === null) {
                $default = [StringType::instance(false), IntType::instance(false), FloatType::instance(false), BoolType::instance(false)];
            }
            return UnionType::of(
                self::typeSetToScalarTypesStrict($this->type_set) ?: $default,
                self::typeSetToScalarTypesStrict($this->real_type_set) ?: $default
            );
        }
    }

    /**
     * @param Type[] $type_list
     * @return list<Type>
     */
    private static function typeSetToScalarTypesStrict(array $type_list): array
    {
        $result = [];
        foreach ($type_list as $type) {
            $type = $type->asScalarType();
            if ($type) {
                $result[] = $type;
            }
        }
        return $result;
    }

    /**
     * Returns the types for which is_callable($x) would be true.
     * TODO: Check for __invoke()?
     * Takes `Closure|false` and returns `Closure`
     * Takes `?MyClass` and returns an empty union type.
     *
     * @return UnionType
     * A UnionType with known callable types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     * @suppress PhanUnreferencedPublicMethod
     */
    public function callableTypes(): UnionType
    {
        return UnionType::of(
            self::castTypeListToCallable($this->type_set),
            self::castTypeListToCallable($this->real_type_set)
        );
    }

    /**
     * @param Type[] $type_list
     * @return list<Type> possibly containing duplicates
     */
    private static function castTypeListToCallable(array $type_list): array
    {
        $result = [];
        foreach ($type_list as $type) {
            $type = $type->asCallableType();
            if ($type) {
                $result[] = $type;
            }
        }
        return $result;
    }

    /**
     * Returns the types for which is_countable($x) would be true.
     * Takes `ArrayObject|false` and returns `ArrayObject`
     * Takes `?(int[])` and returns `int[]`
     * Takes `string` and returns an empty union type.
     *
     * @return UnionType
     * A UnionType with known countable types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     * @suppress PhanUnreferencedPublicMethod
     */
    public function countableTypesStrictCast(CodeBase $code_base): UnionType
    {
        static $default_types;
        if (\is_null($default_types)) {
            $default_types = [ArrayType::instance(false), Type::countableInstance()];
        }
        return UnionType::of(
            self::castTypeListToCountable($code_base, $this->type_set, false) ?: $default_types,
            self::castTypeListToCountable($code_base, $this->real_type_set, false) ?: $default_types
        );
    }

    /**
     * @param Type[] $type_list
     * @return list<Type> a list of countable subclasses and array types, possibly containing duplicates
     * @internal
     */
    public static function castTypeListToCountable(CodeBase $code_base, array $type_list, bool $assume_subclass_implements_countable): array
    {
        $result = [];
        foreach ($type_list as $type) {
            if ($type instanceof IterableType) {
                $result[] = $type->asArrayType();
                if ($assume_subclass_implements_countable && $type->isPossiblyObject()) {
                    $result[] = Type::countableInstance();
                }
                continue;
            } elseif ($type->isObjectWithKnownFQSEN()) {
                $type = $type->withIsNullable(false);
                $expanded_type = $type->asExpandedTypes($code_base);
                foreach ($expanded_type->getTypeSet() as $part_type) {
                    if ($part_type->getName() === 'Countable' && $part_type->getNamespace() === '\\') {
                        $result[] = $type;
                        continue 2;
                    }
                }
                if ($assume_subclass_implements_countable) {
                    try {
                        $fqsen = $type->asFQSEN();
                        if (!($fqsen instanceof FullyQualifiedClassName)) {
                            // This is a closure
                            continue;
                        }
                        if ($code_base->hasClassWithFQSEN($fqsen)) {
                            if ($code_base->getClassByFQSEN($fqsen)->isFinal()) {
                                // This is a final class and can't implement Countable
                                continue;
                            }
                        }
                    } catch (Exception $_) {
                        // ignore it
                    }
                    $result[] = Type::countableInstance();
                }
                continue;
            } else {
                if ($type->isPossiblyObject()) {
                    // e.g. object/mixed/callable-object can also be Countable
                    $result[] = Type::countableInstance();
                }
                if ($type instanceof MixedType) {
                    $result[] = ArrayType::instance(false);
                }
            }
        }
        return $result;
    }

    /**
     * Returns the types for which is_int($x) would be true.
     *
     * @return UnionType
     * A UnionType with known int types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     * @suppress PhanUnreferencedPublicMethod
     */
    public function intTypes(): UnionType
    {
        return UnionType::of(
            self::castTypeListToInt($this->type_set),
            self::castTypeListToInt($this->real_type_set)
        );
    }

    /**
     * @param Type[] $type_set
     * @return list<Type>
     */
    private static function castTypeListToInt(array $type_set): array
    {
        $result = [];
        foreach ($type_set as $type) {
            if ($type instanceof IntType) {
                $result[] = $type->withIsNullable(false);
            } elseif ($type instanceof MixedType) {
                $result[] = IntType::instance(false);
            }
        }
        return $result;
    }

    /**
     * Returns the types for which is_float($x) would be true.
     *
     * @return UnionType
     * A UnionType with known float types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     * @suppress PhanUnreferencedPublicMethod
     */
    public function floatTypes(): UnionType
    {
        return UnionType::of(
            self::castTypeListToFloat($this->type_set),
            self::castTypeListToFloat($this->real_type_set)
        );
    }

    /**
     * @param Type[] $type_set
     * @return list<Type>
     */
    private static function castTypeListToFloat(array $type_set): array
    {
        $result = [];
        foreach ($type_set as $type) {
            if ($type instanceof FloatType) {
                $result[] = $type->withIsNullable(false);
            } elseif ($type instanceof MixedType) {
                $result[] = FloatType::instance(false);
            }
        }
        return $result;
    }

    /**
     * Returns the types for which is_string($x) would be true.
     *
     * @return UnionType
     * A UnionType with known string types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     * @suppress PhanUnreferencedPublicMethod
     */
    public function stringTypes(): UnionType
    {
        return UnionType::of(
            self::castTypeListToString($this->type_set),
            self::castTypeListToString($this->real_type_set)
        );
    }

    /**
     * Returns true if this is non-empty and all types in this type set are strings.
     */
    public function isExclusivelyStringTypes(): bool
    {
        return $this->allTypesMatchCallback(static function (Type $type): bool {
            return $type instanceof StringType;
        });
    }

    /**
     * @param Type[] $type_set
     * @return list<Type>
     */
    private static function castTypeListToString(array $type_set): array
    {
        $result = [];
        foreach ($type_set as $type) {
            if ($type instanceof StringType) {
                $result[] = $type->withIsNullable(false);
            } elseif ($type instanceof CallableType) {
                $result[] = CallableStringType::instance(false);
            } elseif ($type instanceof MixedType) {
                $result[] = $type instanceof NonEmptyMixedType ? NonEmptyStringType::instance(false) : StringType::instance(false);
            }
        }
        return $result;
    }

    /**
     * Returns the types for which class_exists($x) would be true.
     *
     * @return UnionType
     * A UnionType with known class string types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     * @suppress PhanUnreferencedPublicMethod
     */
    public function classStringTypes(): UnionType
    {
        return UnionType::of(
            self::castTypeListToClassString($this->type_set),
            self::castTypeListToClassString($this->real_type_set)
        );
    }

    /**
     * @param Type[] $type_set
     * @return list<Type>
     */
    private static function castTypeListToClassString(array $type_set): array
    {
        $result = [];
        $is_possibly_string = false;
        foreach ($type_set as $type) {
            if ($type instanceof LiteralStringType) {
                if (!\preg_match(FullyQualifiedClassName::VALID_CLASS_REGEX, $type->getValue()) && !\preg_match('/^\\\\?oci-(lob|collection)$/iD', $type->getValue())) {
                    continue;
                }
                $result[] = $type->withIsNullable(false);
                continue;
            }
            if ($type instanceof StringType || $type instanceof MixedType) {
                $is_possibly_string = true;
            }
        }
        if ($is_possibly_string) {
            $result[] = ClassStringType::instance(false);
        }
        return $result;
    }

    /**
     * Returns the types for which method_exists($x, ...) could be true.
     *
     * @return UnionType
     * A UnionType with known class string types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     * @suppress PhanUnreferencedPublicMethod
     */
    public function classStringOrObjectTypes(): UnionType
    {
        return UnionType::of(
            self::castTypeListToClassStringOrObject($this->type_set),
            self::castTypeListToClassStringOrObject($this->real_type_set)
        );
    }

    /**
     * @param Type[] $type_set
     * @return list<Type>
     */
    private static function castTypeListToClassStringOrObject(array $type_set): array
    {
        return \array_merge(self::castTypeListToClassString($type_set), self::castToObjectTypesStrict($type_set));
    }

    /**
     * Returns the types for which is_numeric($x) is possibly true.
     *
     * @return UnionType
     * A UnionType with known numeric types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     * @suppress PhanUnreferencedPublicMethod
     */
    public function numericTypes(): UnionType
    {
        return $this->makeFromFilter(static function (Type $type): bool {
            // IntType and LiteralStringType
            return $type->isPossiblyNumeric();
        });
    }

    /**
     * Returns true if this has one or more callable types
     * TODO: Check for __invoke()?
     * Takes `Closure|false` and returns true
     * Takes `?MyClass` and returns false
     *
     * @return bool
     * A UnionType with known callable types kept, other types filtered out.
     *
     * @see self::callableTypes()
     *
     * @suppress PhanUnreferencedPublicMethod
     */
    public function hasCallableType(): bool
    {
        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type->isCallable();
        });
    }

    /**
     * Returns true if every type in this type is callable.
     * TODO: Check for __invoke()?
     *
     * Takes `callable` and returns true
     * Takes `callable|false` and returns false
     *
     * @return bool
     * A UnionType with known callable types kept, other types filtered out.
     *
     * @see nonGenericArrayTypes
     * @suppress PhanUnreferencedPublicMethod not used right now.
     */
    public function isExclusivelyCallable(): bool
    {
        return $this->allTypesMatchCallback(static function (Type $type): bool {
            return $type->isCallable();
        });
    }

    /**
     * @return bool is each of the types in this type bool, false, or true?
     * This also returns false if any type is nullable.
     */
    public function isExclusivelyBoolTypes(): bool
    {
        if ($this->isEmpty()) {
            return false;
        }
        foreach ($this->type_set as $type) {
            if (!$type->isInBoolFamily() || $type->isNullable()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Takes `a|b[]|c|d[]|e|array|ArrayAccess` and returns `a|c|e|ArrayAccess`
     *
     * @return UnionType
     * A UnionType with generic types(as well as the non-generic type `array`)
     * filtered out.
     *
     * @see nonGenericArrayTypes
     */
    public function nonArrayTypes(): UnionType
    {
        return $this->makeFromFilter(
            static function (Type $type): bool {
                return !($type instanceof ArrayType);
            }
        );
    }

    /**
     * Takes `a|b[]|c|d[]|e|f[]|ArrayAccess` and returns `b[]|f[]`
     *
     * @return UnionType
     * A UnionType with non-array types filtered out
     *
     * @see self::nonArrayTypes()
     */
    public function arrayTypes(): UnionType
    {
        return $this->makeFromFilter(
            static function (Type $type): bool {
                return $type instanceof ArrayType;
            }
        );
    }

    /**
     * This is the union type Phan infers from assert(is_array($x))
     * Converts iterable<key,value> to array<key, value>
     * Takes `A[]|ArrayAccess` and returns `A[]`
     * Takes `callable` and returns `callable-array`
     * Takes `` and returns `array`
     */
    public function arrayTypesStrictCast(): UnionType
    {
        return UnionType::of(
            self::castToArrayTypesStrict($this->type_set) ?: [ArrayType::instance(false)],
            self::castToArrayTypesStrict($this->real_type_set) ?: [ArrayType::instance(false)]
        );
    }

    /**
     * This is the union type Phan infers from `assert(is_array($x))`
     * Converts `iterable<key,value>` to `array<key, value>`
     * Takes `A[]|ArrayAccess` and returns `A[]`
     * Takes `callable` and returns `callable-array`
     * Takes `` and returns ``
     * @suppress PhanUnreferencedPublicMethod called dynamically
     */
    public function arrayTypesStrictCastAllowEmpty(): UnionType
    {
        return UnionType::of(
            self::castToArrayTypesStrict($this->type_set),
            self::castToArrayTypesStrict($this->real_type_set)
        );
    }

    /**
     * @param Type[] $type_list
     * @return list<Type>
     */
    private static function castToArrayTypesStrict(array $type_list): array
    {
        $result = [];
        foreach ($type_list as $type) {
            $type = $type->asArrayType();
            if ($type) {
                $result[] = $type;
            }
        }
        return $result;
    }

    /**
     * @return bool
     * True if this is exclusively generic types
     */
    public function isGenericArray(): bool
    {
        if ($this->isEmpty()) {
            return false;
        }

        return $this->allTypesMatchCallback(static function (Type $type): bool {
            return $type instanceof GenericArrayInterface;
        });
    }

    /**
     * @return bool
     * True if any of the types in this UnionType made $matcher_callback return true
     */
    public function hasTypeMatchingCallback(Closure $matcher_callback): bool
    {
        foreach ($this->type_set as $type) {
            if ($matcher_callback($type)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return bool
     * True if any of the types in the real type set of this UnionType made $matcher_callback return true.
     *
     * Callers should check hasRealTypeSet. This returns false if there are no real types.
     */
    public function hasRealTypeMatchingCallback(Closure $matcher_callback): bool
    {
        foreach ($this->real_type_set as $type) {
            if ($matcher_callback($type)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Equivalent to hasTypeMatchingCallback || hasRealTypeMatchingCallback
     */
    public function hasPhpdocOrRealTypeMatchingCallback(Closure $matcher_callback): bool
    {
        foreach ($this->type_set as $type) {
            if ($matcher_callback($type)) {
                return true;
            }
        }
        foreach ($this->real_type_set as $type) {
            if ($matcher_callback($type)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return bool
     * True if each of the types in this UnionType made $matcher_callback return true
     */
    public function allTypesMatchCallback(Closure $matcher_callback): bool
    {
        foreach ($this->type_set as $type) {
            if (!$matcher_callback($type)) {
                return false;
            }
        }
        return true;
    }

    /**
     * @return Type|false
     * Returns the first type in this UnionType made $matcher_callback return true
     * @suppress PhanUnreferencedPublicMethod
     */
    public function findTypeMatchingCallback(Closure $matcher_callback)
    {
        foreach ($this->type_set as $type) {
            if ($matcher_callback($type)) {
                return $type;
            }
        }
        return false;
    }

    /**
     * Takes `a|b[]|c|d[]|e|Traversable<f,g>` and returns `int|string|f`
     *
     * Takes `array{field:int,other:stdClass}` and returns `string`
     *
     * @param CodeBase $code_base (for detecting the iterable value types of `class MyIterator extends Iterator`)
     */
    public function iterableKeyUnionType(CodeBase $code_base): UnionType
    {
        // This is frequently called, and has been optimized
        $new_type_builder = new UnionTypeBuilder();
        foreach ($this->type_set as $type) {
            $key_union_type = $type->iterableKeyUnionType($code_base);
            if ($key_union_type === null) {
                // Does not have iterable values
                continue;
            }
            $new_type_builder->addUnionType($key_union_type);
        }
        $new_real_type_builder = new UnionTypeBuilder();
        foreach ($this->real_type_set as $type) {
            if ($type instanceof ScalarType) {
                // skip false, null, etc.
                continue;
            }
            $key_union_type = $type->iterableKeyUnionType($code_base);
            if ($key_union_type === null) {
                // Not certain of the real type. It could be a subclass of Traversable with unknown keys.
                $new_real_type_builder->clearTypeSet();
                break;
            }
            // TODO: Instead of coercing string to int here, update the real type when the array is assigned to,
            // if the string is a non-literal.
            //
            // Note that array shapes with 0 elements do not have types. (tests/files/src/0461_array_key_exists.php)
            if ($type instanceof ArrayType && $type->isPossiblyTruthy()) {
                if ($key_union_type->isEmpty()) {
                    $key_union_type = UnionType::fromFullyQualifiedPHPDocString('int|string');
                } else {
                    foreach ($key_union_type->getTypeSet() as $key_type) {
                        if ($key_type instanceof StringType && $key_type->isPossiblyNumeric()) {
                            // Numeric literals such as `'0'` cast to 0 when inserted as array keys.
                            $new_real_type_builder->addType(IntType::instance(false));
                            break;
                        }
                    }
                }
            }
            $new_real_type_builder->addUnionType($key_union_type);
        }
        $type_set = $new_type_builder->getTypeSet();
        $real_type_set = $new_real_type_builder->getTypeSet();
        if (!$type_set && $real_type_set) {
            $type_set = UnionType::typeSetFromString('mixed');
        }

        $result = UnionType::of($type_set, $real_type_set);
        return $result;
    }

    /**
     * Takes `a|b[]|c|d[]|e|Traversable<f,g>` and returns `b|d|g`
     *
     * Takes `array{field:int,other:string}` and returns `int|string`
     *
     * @param CodeBase $code_base (for detecting the iterable value types of `class MyIterator extends Iterator`)
     */
    public function iterableValueUnionType(CodeBase $code_base): UnionType
    {
        // This is frequently called, and has been optimized
        // TODO: Support real types if the type set is exclusively real iterable types
        $builder = new UnionTypeBuilder();
        $type_set = $this->type_set;
        foreach ($type_set as $type) {
            $element_type = $type->iterableValueUnionType($code_base);
            if ($element_type === null) {
                // Does not have iterable values
                continue;
            }
            $builder->addUnionType($element_type);
        }
        $real_builder = new UnionTypeBuilder();
        foreach ($this->real_type_set as $type) {
            if ($type instanceof ScalarType) {
                // skip false, null, etc.
                continue;
            }
            $element_type = $type->iterableValueUnionType($code_base);
            if ($element_type === null) {
                // Not certain of the real type. It could be a subclass of Traversable with unknown values.
                $real_builder->clearTypeSet();
                break;
            }
            $real_builder->addUnionType($element_type);
        }

        static $array_type_nonnull = null;
        static $array_type_nullable = null;
        static $mixed_type = null;
        static $null_type = null;
        if ($array_type_nonnull === null) {
            $array_type_nonnull = ArrayType::instance(false);
            $array_type_nullable = ArrayType::instance(true);
            $mixed_type = MixedType::instance(false);
            $null_type = NullType::instance(false);
        }

        // If array is in there, then it can be any type
        if (\in_array($array_type_nonnull, $type_set, true)) {
            $builder->addType($mixed_type);
        } elseif (\in_array($mixed_type, $type_set, true)
            || \in_array($array_type_nullable, $type_set, true)
        ) {
            // Same for mixed
            $builder->addType($mixed_type);
        }

        return UnionType::of($builder->getTypeSet(), $real_builder->getTypeSet());
    }

    /**
     * Takes `a|b[]|c|d[]|e` and returns `b|d`
     * Takes `array{field:int,other:string}` and returns `int|string`
     *
     * @param bool $add_real_types if true, this adds the real types that would be possible for `$x[$offset]`
     */
    public function genericArrayElementTypes(bool $add_real_types = false): UnionType
    {
        // This is frequently called, and has been optimized
        $result = [];
        $type_set = $this->type_set;
        foreach ($type_set as $type) {
            if ($type instanceof GenericArrayInterface) {
                if ($type instanceof GenericArrayType) {
                    $result[] = $type->genericArrayElementType();
                } else {
                    foreach ($type->genericArrayElementUnionType()->getTypeSet() as $inner_type) {
                        $result[] = $inner_type;
                    }
                }
            }
        }

        static $array_type_nonnull = null;
        static $array_type_nullable = null;
        static $mixed_type = null;
        static $null_type = null;
        if ($array_type_nonnull === null) {
            $array_type_nonnull = ArrayType::instance(false);
            $array_type_nullable = ArrayType::instance(true);
            $mixed_type = MixedType::instance(false);
            $null_type = NullType::instance(false);
        }

        if (\in_array($array_type_nullable, $type_set, true)) {
            // TODO: More consistency in what causes this check to infer null
            $result[] = $mixed_type;
            $result[] = $null_type;
        } elseif (\in_array($array_type_nonnull, $type_set, true) ||
            // If array is in there, then it can be any type
            \in_array($mixed_type, $type_set, true)
        ) {
            $result[] = $mixed_type;
        }
        if ($add_real_types && $result && $this->real_type_set) {
            return UnionType::of($result, self::computeRealElementTypesForDimAccess($this->real_type_set));
        }

        return UnionType::of($result);
    }

    /**
     * Returns the real types seen for an array dim access expression such as `$x = expr[offset]`
     * @param non-empty-list<Type> $real_type_set the set of types of expr
     * @return list<Type> possibly empty, possibly with duplicates. These types are nullable to indicate that array accesses can fail.
     * @internal
     */
    public static function computeRealElementTypesForDimAccess(array $real_type_set): array
    {
        $result = [];
        foreach ($real_type_set as $type) {
            if ($type instanceof StringType) {
                // Note that 'var'[9] will still be a string (the empty string) if the offset is invalid.
                // So will 'var'['not an int']
                $result[] = StringType::instance(false);
                continue;
            }
            if ($type->isPossiblyObject()) {
                // e.g. Mixed, \MyClass, iterable, etc.
                // We don't know some of the real types, so return the empty list as the set of real types.
                return [];
            }
            if (!$type->isArrayLike()) {
                continue;
            }
            if (!$type instanceof ArrayType) {
                return [];
            }
            $new_types = $type->genericArrayElementUnionType()->getTypeSet();
            if (!$new_types) {
                return [];
            }
            foreach ($new_types as $element_type) {
                if ($element_type instanceof MixedType) {
                    return [];
                }
                $result[] = $element_type->withIsNullable(true);
            }
        }
        return $result;
    }

    /**
     * Returns the real types seen for an array destructuring expression such as `[$x] = expr`
     * @param non-empty-list<Type> $real_type_set the set of types of expr
     * @return list<Type> possibly empty, possibly with duplicates. These types are nullable to indicate that array accesses can fail.
     * @internal
     */
    public static function computeRealElementTypesForDestructuringAccess(array $real_type_set): array
    {
        $result = [];
        foreach ($real_type_set as $type) {
            if ($type instanceof StringType) {
                // Note that 'var'[9] will still be a string (the empty string) if the offset is invalid.
                // So will 'var'['not an int']
                $result[] = NullType::instance(false);
                continue;
            }
            if ($type->isPossiblyObject()) {
                // e.g. Mixed, \MyClass, iterable, etc.
                // We don't know some of the real types, so return the empty list as the set of real types.
                return [];
            }
            if (!$type->isArrayLike()) {
                continue;
            }
            if (!$type instanceof ArrayType) {
                return [];
            }
            $new_types = $type->genericArrayElementUnionType()->getTypeSet();
            if (!$new_types) {
                return [];
            }
            foreach ($new_types as $element_type) {
                if ($element_type instanceof MixedType) {
                    return [];
                }
                $result[] = $element_type->withIsNullable(true);
            }
        }
        return $result;
    }

    /**
     * Takes `b|d[]` and returns `b[]|d[][]`
     *
     * @param int $key_type
     * Corresponds to the type of the array keys. Set this to a GenericArrayType::KEY_* constant.
     *
     * @return UnionType
     * The subset of types in this
     *
     * TODO: Add a variant that will convert mixed to array<int,mixed> instead of array?
     */
    public function elementTypesToGenericArray(int $key_type): UnionType
    {
        $parts = \array_map(static function (Type $type) use ($key_type): ArrayType {
            if ($type instanceof MixedType) {
                return ArrayType::instance(false);
            }
            return GenericArrayType::fromElementType($type, false, $key_type);
        }, $this->type_set);
        if (\count($parts) <= 1) {
            // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
            return \count($parts) === 1 ? \reset($parts)->asPHPDocUnionType() : self::$empty_instance;
        }
        return new UnionType($parts, false, []);
    }

    /**
     * @param Closure(Type):Type $closure
     * A closure mapping `Type` to `Type`
     *
     * @return UnionType
     * A new UnionType with each type mapped through the
     * given closure
     */
    public function asMappedUnionType(Closure $closure): UnionType
    {
        $new_type_set = \array_map($closure, $this->type_set);
        $new_real_type_set = \array_map($closure, $this->real_type_set);
        if ($new_type_set === $this->type_set && $new_real_type_set === $this->real_type_set) {
            return $this;
        }
        return UnionType::of(
            $new_type_set,
            $new_real_type_set
        );
    }

    /**
     * @param Closure(Type):(list<Type>) $closure
     * A closure mapping `Type` to a list of types for that type
     *
     * @return UnionType
     * A new UnionType with each type mapped to a list of types
     * through the given closure.
     */
    public function asMappedListUnionType(Closure $closure): UnionType
    {
        // In php 7.3, this could be replaced with https://www.php.net/array_push
        $new_type_set = [];
        foreach ($this->type_set as $type) {
            foreach ($closure($type) as $new_type) {
                $new_type_set[] = $new_type;
            }
        }
        $new_real_type_set = [];
        foreach ($this->real_type_set as $type) {
            foreach ($closure($type) as $new_type) {
                $new_real_type_set[] = $new_type;
            }
        }
        if ($new_type_set === $this->type_set && $new_real_type_set === $this->real_type_set) {
            return $this;
        }
        return UnionType::of(
            $new_type_set,
            $new_real_type_set
        );
    }

    /**
     * @param Closure(UnionType):UnionType $closure
     */
    public function withMappedElementTypes(Closure $closure): UnionType
    {
        return $this->asMappedUnionType(static function (Type $type) use ($closure): Type {
            if ($type instanceof ArrayShapeType) {
                $field_types = \array_map($closure, $type->getFieldTypes());
                $result = ArrayShapeType::fromFieldTypes($field_types, $type->isNullable());
                return $result;
            } elseif ($type instanceof GenericArrayType) {
                $element_types = $closure($type->genericArrayElementType()->asPHPDocUnionType());
                if ($element_types->typeCount() !== 1) {
                    $element_type = MixedType::instance(false);
                } else {
                    $element_type = $element_types->getTypeSet()[0];
                }
                return GenericArrayType::fromElementType($element_type, $type->isNullable(), $type->getKeyType());
            }
            return ArrayType::instance(false);
        });
    }

    /**
     * @param int $key_type
     * Corresponds to the type of the array keys. Set this to a GenericArrayType::KEY_* constant.
     *
     * @return UnionType
     * Get a new type for each type in this union which is
     * the generic array version of this type. For instance,
     * 'int|float' will produce 'int[]|float[]'.
     *
     * If $this is an empty UnionType, this method will produce an empty UnionType
     */
    public function asGenericArrayTypes(int $key_type): UnionType
    {
        return $this->asMappedUnionType(
            static function (Type $type) use ($key_type): Type {
                return $type->asGenericArrayType($key_type);
            }
        );
    }

    /**
     * Get a new type for each type in this union which is
     * the generic array version of this type. For instance,
     * 'int|float' will produce 'list<int>|list<float>'.
     *
     * If $this is an empty UnionType, this method will produce an empty UnionType
     */
    public function asListTypes(): UnionType
    {
        return $this->asMappedUnionType(
            // TODO: Fix https://github.com/phan/phan/issues/3755 and change return type to GenericArrayType
            static function (Type $type): Type {
                return ListType::fromElementType($type, false);
            }
        );
    }

    /**
     * @return UnionType
     * Get a new type for each type in this union which is
     * the generic array version of this type. For instance,
     * 'int|float' will produce 'int[]|float[]'.
     *
     * If $this is an empty UnionType, this method will produce 'array'
     */
    public function asNonEmptyGenericArrayTypes(int $key_type): UnionType
    {
        static $cache = [];
        $type = ($cache[$key_type] ?? ($cache[$key_type] = MixedType::instance(false)->asGenericArrayType($key_type)));
        if (\count($this->type_set) === 0) {
            return $type->asRealUnionType();
        }
        $result = $this->asMappedUnionType(
            static function (Type $type) use ($key_type): Type {
                return $type->asGenericArrayType($key_type);
            }
        );
        if (!$result->hasRealTypeSet()) {
            return $result->withRealType($type);
        }
        return $result;
    }

    /**
     * @return UnionType
     * Get a new type for each type in this union which is
     * the associative array version of this type. For instance,
     * 'int|float' will produce 'associative-array<int>|associative-array<float>'.
     *
     * If $this is an empty UnionType, this method will produce 'associative-array<mixed>'
     */
    public function asNonEmptyAssociativeArrayTypes(int $key_type): UnionType
    {
        static $cache = [];
        $type = ($cache[$key_type] ?? ($cache[$key_type] = AssociativeArrayType::fromElementType(MixedType::instance(false), false, $key_type)));
        if (\count($this->type_set) === 0) {
            return $type->asRealUnionType();
        }
        $result = $this->asMappedUnionType(
            static function (Type $type) use ($key_type): Type {
                return AssociativeArrayType::fromElementType($type, false, $key_type);
            }
        );
        if (!$result->hasRealTypeSet()) {
            return $result->withRealType($type);
        }
        return $result;
    }

    /**
     * @return UnionType
     * Get a new type for each type in this union which is
     * the generic array version of this type. For instance,
     * 'int|float' will produce 'list<int>|list<float>'.
     *
     * If $this is an empty UnionType, this method will produce 'list<mixed>'
     */
    public function asNonEmptyListTypes(): UnionType
    {
        static $type = null;
        if ($type === null) {
            $type = ListType::fromElementType(MixedType::instance(false), false);
        }
        if (\count($this->type_set) === 0) {
            return $type->asRealUnionType();
        }
        $result = $this->asMappedUnionType(
            static function (Type $type): Type {
                return ListType::fromElementType($type, false);
            }
        );
        if (!$result->hasRealTypeSet()) {
            return $result->withRealType($type);
        }
        return $result;
    }

    /**
     * @param CodeBase $code_base
     * The code base to use in order to find super classes, etc.
     *
     * @param $recursion_depth
     * This thing has a tendency to run-away on me. This tracks
     * how bad I messed up by seeing how far the expanded types
     * go
     *
     * @return UnionType
     * Expands all class types to all inherited classes returning
     * a superset of this type.
     */
    public function asExpandedTypes(
        CodeBase $code_base,
        int $recursion_depth = 0
    ): UnionType {
        // TODO: Preserve the original real types without expanding them?
        if ($recursion_depth >= 12) {
            throw new RecursionDepthException("Recursion has gotten out of hand: " . Frame::getExpandedTypesDetails());
        }

        $type_set = $this->type_set;
        if (\count($type_set) === 0) {
            return self::$empty_instance;
        } elseif (\count($type_set) === 1) {
            // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
            return \reset($type_set)->asExpandedTypes(
                $code_base,
                $recursion_depth + 1
            );
        }
        // 2 or more union types to merge

        $builder = new UnionTypeBuilder();
        foreach ($type_set as $type) {
            $builder->addUnionType(
                $type->asExpandedTypes(
                    $code_base,
                    $recursion_depth + 1
                )
            );
        }
        return UnionType::of($builder->getTypeSet(), $this->real_type_set);
    }

    /**
     * @param CodeBase $code_base
     * The code base to use in order to find super classes, etc.
     *
     * @param $recursion_depth
     * This thing has a tendency to run-away on me. This tracks
     * how bad I messed up by seeing how far the expanded types
     * go
     *
     * @return UnionType
     * Expands all class types to all inherited classes returning
     * a superset of this type, not removing template types
     */
    public function asExpandedTypesPreservingTemplate(
        CodeBase $code_base,
        int $recursion_depth = 0
    ): UnionType {
        if ($recursion_depth >= 12) {
            throw new RecursionDepthException("Recursion has gotten out of hand: " . Frame::getExpandedTypesDetails());
        }

        $type_set = $this->type_set;
        if (\count($type_set) === 0) {
            return self::$empty_instance;
        } elseif (\count($type_set) === 1) {
            // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
            return \reset($type_set)->asExpandedTypesPreservingTemplate(
                $code_base,
                $recursion_depth + 1
            );
        }
        // 2 or more union types to merge

        $builder = new UnionTypeBuilder();
        foreach ($type_set as $type) {
            $builder->addUnionType(
                $type->asExpandedTypesPreservingTemplate(
                    $code_base,
                    $recursion_depth + 1
                )
            );
        }
        return UnionType::of($builder->getTypeSet(), $this->real_type_set);
    }

    /**
     * Remove all types with the same FQSENs as $template_union_type with the types.
     * Then, return this with $template_union_type added.
     */
    public function replaceWithTemplateTypes(UnionType $template_union_type): UnionType
    {
        if ($template_union_type->isEmpty()) {
            return $this;
        }
        $new_type_set = $this->type_set;
        foreach ($this->type_set as $i => $type) {
            // TODO: Handle recursion
            if ($template_union_type->hasTypeWithFQSEN($type)) {
                unset($new_type_set[$i]);
                if ($type->isNullable()) {
                    // Preserve nullable
                    $template_union_type = $template_union_type->nullableClone();
                }
            }
        }
        $new_type_set = \array_merge($new_type_set, $template_union_type->getTypeSet());
        return UnionType::of($new_type_set, $this->real_type_set);
    }

    /**
     * Check if at least one type in this union type has the same FQSEN as $other
     * Returns false if $other is not an object type.
     */
    public function hasTypeWithFQSEN(Type $other): bool
    {
        if (!$other->isObjectWithKnownFQSEN()) {
            return false;
        }
        foreach ($this->type_set as $type) {
            if ($type->hasSameNamespaceAndName($other) && $type->isObjectWithKnownFQSEN()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Filters the types with the same FQSEN as $other
     * @suppress PhanUnreferencedPublicMethod
     */
    public function getTypesWithFQSEN(Type $other): UnionType
    {
        if (!$other->isObjectWithKnownFQSEN()) {
            return UnionType::empty();
        }
        $result = $this;
        foreach ($this->type_set as $type) {
            if ($type->hasSameNamespaceAndName($other) && $type->isObjectWithKnownFQSEN()) {
                $result = $result->withoutType($type);
            }
        }
        return $result;
    }

    /**
     * As per the Serializable interface
     *
     * @return string
     * A serialized representation of this type
     *
     * @see \Serializable
     */
    public function serialize(): string
    {
        if ($this->real_type_set) {
            return (string)$this . "\x00" . \implode('|', $this->real_type_set);
        }
        return (string)$this;
    }

    /**
     * As per the Serializable interface
     *
     * @param string $serialized
     * A serialized UnionType
     *
     * @see \Serializable
     * @suppress PhanAccessReadOnlyProperty this unserializes
     * @suppress PhanParamSignatureRealMismatchHasNoParamTypeInternal, PhanUnusedSuppression parameter type widening was allowed in php 7.2, signature changed in php 8
     */
    public function unserialize($serialized): void
    {
        $i = \strpos($serialized, "\x00");
        if ($i !== false) {
            $result = UnionType::fromFullyQualifiedPHPDocAndRealString(
                substr($serialized, 0, $i),
                (string)substr($serialized, $i + 1)
            );
            $this->type_set = $result->getTypeSet();
            $this->real_type_set = $result->getRealTypeSet();
            return;
        }
        $this->type_set = UnionType::fromFullyQualifiedPHPDocString($serialized)->getTypeSet();
    }

    /**
     * @return string
     * A human-readable string representation of this union
     * type
     */
    public function __toString(): string
    {
        // Create a new array containing the string
        // representations of each type
        $types = $this->type_set;
        $type_name_list =
            \array_map(static function (Type $type): string {
                return (string)$type;
            }, $types);

        // Sort the types so that we get a stable
        // representation
        \asort($type_name_list);

        // Join them with a pipe
        return \implode('|', $type_name_list);
    }

    /**
     * @return array<string,array<int|string,string>>
     * A map from builtin function name to type information
     *
     * @see \Phan\Language\Internal\FunctionSignatureMap
     */
    public static function internalFunctionSignatureMap(int $target_php_version): array
    {
        static $php73_map = [];

        if (!$php73_map) {
            $php73_map = self::computeLatestFunctionSignatureMap();
        }
        if ($target_php_version >= 70400) {
            static $php74_map = [];
            if (!$php74_map) {
                $php74_map = self::computePHP74FunctionSignatureMap($php73_map);
            }
            if ($target_php_version >= 80000) {
                static $php80_map = [];
                if (!$php80_map) {
                    $php80_map = self::computePHP80FunctionSignatureMap($php74_map);
                }
                return $php80_map;
            }
            return $php74_map;
        }
        if ($target_php_version >= 70300) {
            return $php73_map;
        }
        static $php72_map = [];
        if (!$php72_map) {
            $php72_map = self::computePHP72FunctionSignatureMap($php73_map);
        }
        if ($target_php_version >= 70200) {
            return $php72_map;
        }
        static $php71_map = [];
        if (!$php71_map) {
            $php71_map = self::computePHP71FunctionSignatureMap($php72_map);
        }
        if ($target_php_version >= 70100) {
            return $php71_map;
        }
        static $php70_map = [];
        if (!$php70_map) {
            $php70_map = self::computePHP70FunctionSignatureMap($php71_map);
        }
        if ($target_php_version >= 70000) {
            return $php70_map;
        }

        static $php56_map = [];
        if (!$php56_map) {
            $php56_map = self::computePHP56FunctionSignatureMap($php70_map);
        }
        return $php56_map;
    }

    /**
     * @return array<string,string[]>
     */
    private static function computeLatestFunctionSignatureMap(): array
    {
        $map = [];
        $map_raw = require(__DIR__ . '/Internal/FunctionSignatureMap.php');
        foreach ($map_raw as $key => $value) {
            $map[\strtolower($key)] = $value;
        }
        return $map;
    }

    /**
     * @return array<string,string> maps the lowercase function name to the return type
     * @internal the data format will change
     */
    public static function getLatestRealFunctionSignatureMap(int $target_php_version): array
    {
        if ($target_php_version >= 80000) {
            static $map_80;
            return $map_80 ?? ($map_80 = self::computeLatestRealFunctionSignatureMap(true));
        }
        static $map_73;
        return $map_73 ?? ($map_73 = self::computeLatestRealFunctionSignatureMap(false));
    }

    /**
     * @return array<string,string>
     */
    private static function computeLatestRealFunctionSignatureMap(bool $is_php8): array
    {
        $map = [];
        if ($is_php8) {
            $map_raw = require(__DIR__ . '/Internal/FunctionSignatureMapReal.php');
        } else {
            $map_raw = require(__DIR__ . '/Internal/FunctionSignatureMapReal_php73.php');
        }
        foreach ($map_raw as $key => $value) {
            $map[\strtolower($key)] = $value;
        }
        return $map;
    }

    /**
     * @param array<string,associative-array<int|string,string>> $php74_map
     * @return array<string,associative-array<int|string,string>>
     */
    private static function computePHP80FunctionSignatureMap(array $php74_map): array
    {
        $delta_raw = require(__DIR__ . '/Internal/FunctionSignatureMap_php80_delta.php');
        return self::applyDeltaToGetNewerSignatures($php74_map, $delta_raw);
    }

    /**
     * @param array<string,associative-array<int|string,string>> $php73_map
     * @return array<string,associative-array<int|string,string>>
     */
    private static function computePHP74FunctionSignatureMap(array $php73_map): array
    {
        $delta_raw = require(__DIR__ . '/Internal/FunctionSignatureMap_php74_delta.php');
        return self::applyDeltaToGetNewerSignatures($php73_map, $delta_raw);
    }

    /**
     * @param array<string,associative-array<int|string,string>> $php73_map
     * @return array<string,associative-array<int|string,string>>
     */
    private static function computePHP72FunctionSignatureMap(array $php73_map): array
    {
        $delta_raw = require(__DIR__ . '/Internal/FunctionSignatureMap_php73_delta.php');
        return self::applyDeltaToGetOlderSignatures($php73_map, $delta_raw);
    }

    /**
     * @param array<string,array<int|string,string>> $php72_map
     * @return array<string,array<int|string,string>>
     */
    private static function computePHP71FunctionSignatureMap(array $php72_map): array
    {
        $delta_raw = require(__DIR__ . '/Internal/FunctionSignatureMap_php72_delta.php');
        return self::applyDeltaToGetOlderSignatures($php72_map, $delta_raw);
    }

    /**
     * @param array<string,associative-array<int|string,string>> $php71_map
     * @return array<string,associative-array<int|string,string>>
     */
    private static function computePHP70FunctionSignatureMap(array $php71_map): array
    {
        $delta_raw = require(__DIR__ . '/Internal/FunctionSignatureMap_php71_delta.php');
        return self::applyDeltaToGetOlderSignatures($php71_map, $delta_raw);
    }

    /**
     * @param array<string,associative-array<int|string,string>> $php70_map
     * @return array<string,associative-array<int|string,string>>
     */
    private static function computePHP56FunctionSignatureMap(array $php70_map): array
    {
        $delta_raw = require(__DIR__ . '/Internal/FunctionSignatureMap_php70_delta.php');
        return self::applyDeltaToGetOlderSignatures($php70_map, $delta_raw);
    }

    /**
     * @param array<string,associative-array<int|string,string>> $older_map
     * @param array{new:array<string,associative-array<int|string,string>>,old:array<string,associative-array<int|string,string>>} $delta
     * @return array<string,associative-array<int|string,string>>
     *
     * @see applyDeltaToGetOlderSignatures - This is doing the exact same thing in reverse.
     * @suppress PhanUnreferencedPrivateMethod this will be used again when Phan supports the next PHP minor release
     */
    private static function applyDeltaToGetNewerSignatures(array $older_map, array $delta): array
    {
        return self::applyDeltaToGetOlderSignatures($older_map, [
            'old' => $delta['new'],
            'new' => $delta['old'],
        ]);
    }

    /**
     * @param array<string,associative-array<int|string,string>> $newer_map
     * @param array{new:array<string,associative-array<int|string,string>>,old:array<string,associative-array<int|string,string>>} $delta
     * @return array<string,associative-array<int|string,string>>
     */
    private static function applyDeltaToGetOlderSignatures(array $newer_map, array $delta): array
    {
        foreach ($delta['new'] as $key => $unused_signature) {
            // Would also unset alternates, but that step isn't necessary yet.
            unset($newer_map[\strtolower($key)]);
        }
        foreach ($delta['old'] as $key => $signature) {
            // Would also unset alternates, but that step isn't necessary yet.
            $newer_map[\strtolower($key)] = $signature;
        }

        // Return the newer map after modifying it to become the older map.
        return $newer_map;
    }

    /**
     * @var ?UnionType|?bool this type as the normalized version
     */
    private $as_normalized = null;

    /**
     * @return UnionType - A normalized version of this union type (May or may not be the same object, if no modifications were made)
     *
     * The following normalization rules apply
     *
     * 1. If one of the types is null or nullable, convert all types to nullable and remove "null" from the union type
     * 2. If both "true" and "false" (possibly nullable) coexist, or either coexists with "bool" (possibly nullable),
     *    then remove "true" and "false"
     *    @suppress PhanAccessReadOnlyProperty
     */
    public function asNormalizedTypes(): UnionType
    {
        if (\count($this->type_set) <= 1 && \count($this->real_type_set) <= 1) {
            // Optimization: can't simplify if there's only one type
            return $this;
        }
        $normalized = $this->as_normalized;
        if (\is_null($normalized)) {
            $this->as_normalized = $normalized = $this->asNormalizedTypesInner();
        }
        return \is_object($normalized) ? $normalized : $this;
    }

    private function asNormalizedTypesInner(): UnionType
    {
        $type_set = $this->type_set;
        $real_type_set = $this->real_type_set;
        $new_type_set = self::asNormalizedTypeSetInner($type_set);
        $new_real_type_set = self::asNormalizedTypeSetInner($real_type_set);
        if ($type_set === $new_type_set && $real_type_set === $new_real_type_set) {
            return $this;
        }
        return UnionType::of($new_type_set, $new_real_type_set);
    }

    /**
     * @param list<Type> $type_set
     * @return list<Type>
     */
    private static function asNormalizedTypeSetInner(array $type_set): array
    {
        if (\count($type_set) <= 1) {
            return $type_set;
        }
        $flags = 0;
        foreach ($type_set as $type) {
            $flags |= $type->getNormalizationFlags();
        }
        if ($flags === 0) {
            // Optimization: nothing to do if no types are null/nullable or booleans
            return $type_set;
        }
        $nullable = ($flags & Type::_bit_nullable) !== 0;
        if ($nullable) {
            $new_types = [];
            foreach ($type_set as $type) {
                if ($type instanceof NullType || $type instanceof VoidType) {
                    continue;
                }
                if ($type->isNullable()) {
                    $new_types[] = $type;
                } else {
                    $new_types[] = $type->withIsNullable(true);
                }
            }
            $builder = new UnionTypeBuilder(self::getUniqueTypes($new_types));
        } else {
            $builder = new UnionTypeBuilder($type_set);
        }

        // If this contains both true and false types, filter out both and add "bool" (or "?bool" for nullable)
        if (($flags & Type::_bit_bool_combination) === Type::_bit_bool_combination) {
            if ($nullable) {
                self::convertToTypeSetWithNormalizedNullableBools($builder);
            } else {
                self::convertToTypeSetWithNormalizedNonNullableBools($builder);
            }
        }
        $result_type_set = $builder->getTypeSet();
        if ($result_type_set === $type_set) {
            // Reuse the first array if possible, to keep memory usage down.
            return $type_set;
        }
        return $result_type_set;
    }

    /**
     * @param UnionType[] $union_types
     * @return UnionType union of these UnionTypes
     */
    public static function merge(array $union_types, bool $normalize_array_shapes = true): UnionType
    {
        $n = \count($union_types);
        if ($n < 2) {
            return \reset($union_types) ?: UnionType::$empty_instance;
        }
        $new_type_set = [];
        $array_shape_types = [];
        foreach ($union_types as $union_type) {
            $type_set = $union_type->type_set;
            if (\count($type_set) === 0) {
                continue;
            }
            if (\count($new_type_set) === 0) {
                // Take advantage of copy-on-write when possible (this avoids creating a new array if nothing else gets added)
                $new_type_set = $type_set;
                foreach ($type_set as $type) {
                    if ($type instanceof ArrayShapeType && $normalize_array_shapes) {
                        $array_shape_types[] = $type;
                    }
                }
                continue;
            }
            foreach ($type_set as $type) {
                $new_type_set[] = $type;
                if ($type instanceof ArrayShapeType && $normalize_array_shapes) {
                    $array_shape_types[] = $type;
                }
            }
        }
        if (\count($new_type_set) > 1) {
            $new_type_set = self::getUniqueTypes($new_type_set);
            $array_shape_types = self::getUniqueTypes($array_shape_types);
        }
        if ($array_shape_types) {
            // @phan-suppress-next-line PhanPartialTypeMismatchArgument phan can't infer new_type_set/union_types are non-empty
            $new_type_set = self::normalizeArrayShapes($new_type_set, $array_shape_types, $union_types, false);
        }
        $new_real_type_set = [];
        $array_shape_types = [];
        foreach ($union_types as $union_type) {
            $type_set = $union_type->real_type_set;
            if (\count($type_set) === 0) {
                $new_real_type_set = [];
                $array_shape_types = [];
                break;
            }
            if (\count($new_real_type_set) === 0) {
                $new_real_type_set = $type_set;
                foreach ($type_set as $type) {
                    if ($type instanceof ArrayShapeType && $normalize_array_shapes) {
                        $array_shape_types[] = $type;
                    }
                }
                continue;
            }
            foreach ($type_set as $type) {
                $new_real_type_set[] = $type;
                if ($type instanceof ArrayShapeType && $normalize_array_shapes) {
                    $array_shape_types[] = $type;
                    continue;
                }
            }
        }
        if (\count($new_real_type_set) > 1) {
            $new_real_type_set = self::getUniqueTypes($new_real_type_set);
            $array_shape_types = self::getUniqueTypes($array_shape_types);
        }
        if ($array_shape_types) {
            // @phan-suppress-next-line PhanPartialTypeMismatchArgument Phan can't count.
            $new_real_type_set = self::normalizeArrayShapes($new_real_type_set, $array_shape_types, $union_types, true);
        }
        // \Phan\Debug::debugLog("Before: " . \implode(' or ', \array_map(function (UnionType $type) : string { return $type->getDebugRepresentation(); }, $union_types)) . " array_shape_types=" . \implode(' or ', $array_shape_types) . "\n");
        return UnionType::of($new_type_set, $new_real_type_set);
        // \Phan\Debug::debugLog("After: " . $result->getDebugRepresentation() . "\n");
        // return $result;
    }

    /**
     * @param non-empty-list<Type> $type_set the elements of the type_set (both array shapes and non array shapes)
     * @param non-empty-list<ArrayShapeType> $array_shape_types the elements of the type_set that were ArrayShapeType instances
     * @param non-empty-list<UnionType> $union_types a list of two or more union types, at least one of which has array shapes
     * @param bool $from_real_type_set
     * @return non-empty-list<Type> $type_set a type set with all array shape types normalized
     */
    private static function normalizeArrayShapes(array $type_set, array $array_shape_types, array $union_types, bool $from_real_type_set): array
    {
        // If one of the union types had no array shape types, merge the array shape types of other types with the empty type
        $add_mixed = false;
        foreach ($union_types as $union_type) {
            foreach ($from_real_type_set ? $union_type->real_type_set : $union_type->type_set as $type) {
                if ($type instanceof ArrayShapeType) {
                    continue 2;
                }
            }
            $add_mixed = true;
            break;
        }
        if (!$add_mixed && \count($array_shape_types) === 1) {
            return $type_set;
        }
        // Replace the prior array shape types with the new array shape types.
        $array_shape_type = self::combineArrayShapeTypes($array_shape_types, $add_mixed);
        $new_type_set = [$array_shape_type];
        foreach ($type_set as $type) {
            if (!$type instanceof ArrayShapeType) {
                $new_type_set[] = $type;
            }
        }
        return $new_type_set;
    }

    /**
     * @param non-empty-list<ArrayShapeType> $types
     */
    private static function combineArrayShapeTypes(array $types, bool $add_mixed): ArrayShapeType
    {
        $is_nullable = false;
        $field_types_list = [];
        $common_field_types = [];
        foreach ($types as $type) {
            $is_nullable = $is_nullable || $type->isNullable();
            $field_types = $type->getFieldTypes();
            $common_field_types += $field_types;
            $field_types_list[] = $field_types;
        }
        foreach ($common_field_types as $key => $_) {
            $is_possibly_undefined = false;
            $new_element_union_types = [];
            foreach ($field_types_list as $field_types) {
                $element_union_type = $field_types[$key] ?? null;
                if (!$element_union_type) {
                    $is_possibly_undefined = true;
                    continue;
                }
                if ($add_mixed) {
                    $element_union_type = $element_union_type->eraseRealTypeSetRecursively();
                }
                $new_element_union_types[] = $element_union_type;
                $is_possibly_undefined = $is_possibly_undefined || $element_union_type->isPossiblyUndefined();
            }
            $new_element_union_type = UnionType::merge($new_element_union_types);
            if ($is_possibly_undefined) {
                $new_element_union_type = $new_element_union_type->withIsPossiblyUndefined(true);
            }
            $common_field_types[$key] = $new_element_union_type;
        }
        return ArrayShapeType::fromFieldTypes($common_field_types, $is_nullable);
    }

    /**
     * Must be called after converting nullable to non-nullable.
     * Removes false|true types and adds bool
     *
     * @param UnionTypeBuilder $builder (Containing only non-nullable values)
     */
    private static function convertToTypeSetWithNormalizedNonNullableBools(UnionTypeBuilder $builder): void
    {
        static $true_type = null;
        static $false_type = null;
        static $bool_type = null;
        if ($bool_type === null) {
            $true_type = TrueType::instance(false);
            $false_type = FalseType::instance(false);
            $bool_type = BoolType::instance(false);
        }
        if (!$builder->isEmpty()) {
            $builder->removeType($true_type);
            $builder->removeType($false_type);
        }

        $builder->addType($bool_type);
    }

    /**
     * Must be called after converting all types to null.
     * Removes ?false|?true types and adds ?bool
     *
     * @param UnionTypeBuilder $builder (Containing only non-nullable values)
     */
    private static function convertToTypeSetWithNormalizedNullableBools(UnionTypeBuilder $builder): void
    {
        static $true_type = null;
        static $false_type = null;
        static $bool_type = null;
        if ($bool_type === null) {
            $true_type = TrueType::instance(true);
            $false_type = FalseType::instance(true);
            $bool_type = BoolType::instance(true);
        }
        if (!$builder->isEmpty()) {
            $builder->removeType($true_type);
            $builder->removeType($false_type);
        }

        $builder->addType($bool_type);
    }

    /**
     * Generates a variable length string identifier that uniquely identifies the Type instances in this UnionType. (both phpdoc and real)
     * `int|string` will generate the same id as `string|int`.
     */
    public function generateUniqueId(): string
    {
        /** @var list<int> $ids */
        // Real types are given negative ids, and phpdoc types are given non-negative ids.
        $ids = [];
        foreach ($this->real_type_set as $type) {
            $ids[] = ~\spl_object_id($type);
        }
        foreach ($this->type_set as $type) {
            $ids[] = \spl_object_id($type);
        }
        // Sort the unique identifiers of Type instances so that int|string generates the same id as string|int
        \sort($ids);
        return \implode(',', $ids);
    }

    /**
     * Returns true if at least one of the types in this type set is a generic array shape
     * E.g. returns true for `array{}|false`, but not for `iterable<int,array{}>|false`
     */
    public function hasTopLevelArrayShapeTypeInstances(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type instanceof ArrayShapeType) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if at least one of the types in this type set is not a generic array shape
     * E.g. returns true for `array{}|false`, and false for `array{}`
     */
    public function hasTopLevelNonArrayShapeTypeInstances(): bool
    {
        foreach ($this->type_set as $type) {
            if (!($type instanceof ArrayShapeType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if at least one of the types in this type set contains an array shape.
     * TODO: Implement for all types that can contain other types.
     * @suppress PhanUnreferencedPublicMethod
     */
    public function hasArrayShapeTypeInstances(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->hasArrayShapeTypeInstances()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if at least one of the types in this type set contains an array shape or a literal type.
     * TODO: Implement for all types that can contain other types.
     *
     * e.g. returns true for `array<string,2>`, `2`, `array{key:int}`, etc.
     * @suppress PhanUnreferencedPublicMethod
     */
    public function hasArrayShapeOrLiteralTypeInstances(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->hasArrayShapeOrLiteralTypeInstances()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return bool true if at least one of the types in this type set is `mixed` or `?mixed`
     */
    public function hasMixedType(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type instanceof MixedType) {
                return true;
            }
        }
        return false;
    }

    /**
     * Flatten literals in keys and values of top-level array shapes into non-literal types (but not standalone literals)
     * E.g. convert array{2:array{key:'value'}} to array<int,array{key:'value'}>
     */
    public function withFlattenedTopLevelArrayShapeTypeInstances(): UnionType
    {
        if (!$this->hasArrayShapeTypeInstances()) {
            return $this;
        }
        return UnionType::of(
            self::withFlattenedTopLevelArrayShapeTypeInstancesForSet($this->type_set),
            self::withFlattenedTopLevelArrayShapeTypeInstancesForSet($this->real_type_set)
        );
    }

    /**
     * Convert non-empty-array, etc. to array.
     *
     * This reflects a change that removes elements from an array.
     * @see withFlattenedTopLevelArrayShapeTypeInstances
     */
    public function withPossiblyEmptyArrays(): UnionType
    {
         return $this->asMappedUnionType(static function (Type $type): Type {
            if ($type instanceof NonEmptyArrayInterface) {
                return $type->asPossiblyEmptyArrayType();
            }
             return $type;
         });
    }

    /**
     * @param list<Type> $type_set
     * @return list<Type>
     */
    private static function withFlattenedTopLevelArrayShapeTypeInstancesForSet(array $type_set): array
    {
        $result = [];
        $has_other_array_type = false;
        $empty_array_shape_type = null;
        foreach ($type_set as $type) {
            if ($type instanceof ArrayShapeType) {
                if (\count($type->getFieldTypes()) === 0) {
                    $empty_array_shape_type = $type;
                    continue;
                }
                $has_other_array_type = true;
                foreach ($type->withFlattenedTopLevelArrayShapeTypeInstances() as $type_part) {
                    $result[] = $type_part;
                }
            } else {
                $result[] = $type;
                if ($type instanceof ArrayType) {
                    $has_other_array_type = true;
                }
            }
        }
        if ($empty_array_shape_type) {
            $is_nullable = $empty_array_shape_type->isNullable();
            if (!$has_other_array_type) {
                $result[] = ArrayType::instance($is_nullable);
            } else {
                foreach ($result as $i => $type) {
                    if ($type instanceof NonEmptyListType) {
                        $type = $type->asPossiblyEmptyArrayType();
                        $result[$i] = $type;
                    }
                    if ($is_nullable) {
                        $result[$i] = $type->withIsNullable(true);
                    }
                }
            }
        }
        // @phan-suppress-next-line PhanPartialTypeMismatchReturn phan cannot infer that the assignments do not make the result associative-array.
        return $result;
    }

    /**
     * Flatten literals in keys and values into non-literal types (but not standalone literals)
     * E.g. convert array{2:3} to array<int,string>
     */
    public function withFlattenedArrayShapeTypeInstances(): UnionType
    {
        if (!$this->hasArrayShapeTypeInstances()) {
            return $this;
        }
        return UnionType::of(
            self::withFlattenedArrayShapeTypeInstancesForSet($this->type_set),
            self::withFlattenedArrayShapeTypeInstancesForSet($this->real_type_set)
        );
    }

    /**
     * @param list<Type> $type_set
     * @return list<Type>
     */
    private static function withFlattenedArrayShapeTypeInstancesForSet(array $type_set): array
    {
        $result = [];
        $has_other_array_type = false;
        $empty_array_shape_type = null;
        foreach ($type_set as $type) {
            if ($type->hasArrayShapeTypeInstances()) {
                if ($type instanceof ArrayShapeType) {
                    if (\count($type->getFieldTypes()) === 0) {
                        $empty_array_shape_type = $type;
                        continue;
                    }
                }
                $has_other_array_type = true;
                foreach ($type->withFlattenedArrayShapeOrLiteralTypeInstances() as $type_part) {
                    $result[] = $type_part;
                }
            } else {
                $result[] = $type;
                if ($type instanceof ArrayType) {
                    $has_other_array_type = true;
                }
            }
        }
        if ($empty_array_shape_type && !$has_other_array_type) {
            $result[] = ArrayType::instance($empty_array_shape_type->isNullable());
        }
        return $result;
    }

    /**
     * Flatten literals in keys and values into non-literal types (as well as standalone literals)
     * E.g. convert array{2:3} to array<int,string>, 'somestring' to string, etc.
     */
    public function withFlattenedArrayShapeOrLiteralTypeInstances(): UnionType
    {
        if (!$this->hasArrayShapeOrLiteralTypeInstances()) {
            return $this;
        }

        return UnionType::of(
            self::withFlattenedArrayShapeOrLiteralTypeInstancesForSet($this->type_set),
            self::withFlattenedArrayShapeOrLiteralTypeInstancesForSet($this->real_type_set)
        );
    }

    /**
     * @param list<Type> $type_set
     * @return list<Type>
     */
    private static function withFlattenedArrayShapeOrLiteralTypeInstancesForSet(array $type_set): array
    {
        $result = [];
        $has_other_array_type = false;
        $empty_array_shape_type = null;
        foreach ($type_set as $type) {
            if ($type->hasArrayShapeOrLiteralTypeInstances()) {
                if ($type instanceof ArrayShapeType) {
                    if (\count($type->getFieldTypes()) === 0) {
                        $empty_array_shape_type = $type;
                        continue;
                    }
                }
                foreach ($type->withFlattenedArrayShapeOrLiteralTypeInstances() as $type_part) {
                    $result[] = $type_part;
                }
            } else {
                $result[] = $type;
            }
            if ($type instanceof ArrayType) {
                $has_other_array_type = true;
            }
        }
        if ($empty_array_shape_type && !$has_other_array_type) {
            $result[] = ArrayType::instance($empty_array_shape_type->isNullable());
        }
        return $result;
    }

    /**
     * Returns this union type after an operation that converts arrays to associative arrays is applied.
     */
    public function withAssociativeArrays(bool $can_reduce_size): UnionType
    {
        return UnionType::of(
            self::withAssociativeArraysForSet($this->type_set, $can_reduce_size),
            self::withAssociativeArraysForSet($this->real_type_set, $can_reduce_size)
        );
    }

    /**
     * @param list<Type> $type_set
     * @return list<Type> with all array types as associative arrays or array shapes
     */
    private static function withAssociativeArraysForSet(array $type_set, bool $can_reduce_size): array
    {
        foreach ($type_set as $i => $type) {
            if (!$type instanceof ArrayType) {
                continue;
            }
            if ($type instanceof ArrayShapeType) {
                continue;
            }
            $type_set[$i] = $type->asAssociativeArrayType($can_reduce_size);
        }
        return $type_set;
    }

    /**
     * Returns this union type after an operation that converts arrays with integer keys to lists is applied.
     * (e.g. array_unshift, array_merge)
     */
    public function withIntegerKeyArraysAsLists(): UnionType
    {
        return UnionType::of(
            self::withIntegerKeyArraysAsListsForSet($this->type_set),
            self::withIntegerKeyArraysAsListsForSet($this->real_type_set)
        );
    }

    /**
     * @param list<Type> $type_set
     * @return list<Type> with all array types as lists, or as arrays with mixed or string keys
     */
    private static function withIntegerKeyArraysAsListsForSet(array $type_set): array
    {
        foreach ($type_set as $i => $type) {
            if (!$type instanceof ArrayType) {
                continue;
            }
            $type_set[$i] = $type->convertIntegerKeyArrayToList();
        }
        return $type_set;
    }

    /**
     * Used to check if any type in this param type should be replaced
     * by more specific types of arguments, in non-quick mode
     */
    public function shouldBeReplacedBySpecificTypes(): bool
    {
        if ($this->isEmpty()) {
            // We don't know anything about this type, this should be replaced by more specific argument types.
            return true;
        }
        return $this->hasTypeMatchingCallback(static function (Type $type): bool {
            return $type->shouldBeReplacedBySpecificTypes();
        });
    }

    /**
     * Removes $field_key from the fields of top-level array shapes of this union type.
     *
     * @param int|string|float|bool $field_key
     */
    public function withoutArrayShapeField($field_key): UnionType
    {
        return $this->asMappedUnionType(static function (Type $type) use ($field_key): Type {
            if ($type instanceof ArrayShapeType) {
                return $type->withoutField($field_key);
            }
            return $type;
        });
    }

    /**
     * Mark this union type as being possibly undefined.
     * This is used for union types of variables and for values of array shapes.
     *
     * @suppress PhanAccessReadOnlyProperty this is the only way to set is_possibly_undefined
     */
    public function withIsPossiblyUndefined(bool $is_possibly_undefined): UnionType
    {
        if (!$is_possibly_undefined) {
            return $this;
        }
        // TODO: Would using [NullType] make more sense here?
        $result = new AnnotatedUnionType($this->type_set, true, $this->real_type_set);
        $result->is_possibly_undefined = $is_possibly_undefined;
        return $result;
    }

    /**
     * Mark this union type as being possibly definitely undefined.
     * This is used for properties (of $this) and is planned for local variables.
     *
     * Base implementation. Overridden by AnnotatedUnionType.
     * @suppress PhanAccessReadOnlyProperty this is the only way to set is_possibly_undefined
     */
    public function withIsDefinitelyUndefined(): UnionType
    {
        $result = new AnnotatedUnionType($this->type_set, true, $this->real_type_set);
        $result->is_possibly_undefined = AnnotatedUnionType::DEFINITELY_UNDEFINED;
        return $result;
    }

    /**
     * Base implementation. Overridden by AnnotatedUnionType.
     * Used for fields of array shapes.
     *
     * This is distinct from null - The array shape offset potentially doesn't exist at all, which is different from existing and being null.
     */
    public function isPossiblyUndefined(): bool
    {
        return false;
    }

    /**
     * Base implementation. Overridden by AnnotatedUnionType.
     *
     * Used for properties(tracking unset of $this) and planned for variables.
     */
    public function isDefinitelyUndefined(): bool
    {
        return false;
    }

    /**
     * Returns true if at least one of the types in this union type is a class-like defining the method __toString()
     *
     * Callers should convert union types to the expanded union types first.
     * TODO: is that necessary?
     */
    public function hasClassWithToStringMethod(CodeBase $code_base, Context $context): bool
    {
        try {
            foreach ($this->asClassList($code_base, $context) as $clazz) {
                if ($clazz->hasMethodWithName($code_base, "__toString")) {
                    return true;
                }
            }
        } catch (CodeBaseException $_) {
            // Swallow "Cannot find class", go on to emit issue
        }
        return false;
    }

    /**
     * Returns true if the union type is exclusively generators
     */
    public function isExclusivelyGenerators(): bool
    {
        foreach ($this->type_set as $type) {
            if (!$type->isGenerator()) {
                return false;
            }
        }
        return \count($this->type_set) > 0;
    }

    /**
     * Gets the type of this converted to a generator.
     * E.g. converts the type of an iterable/array/Generator `$x`
     * to the type of a generator with the implementation `yield from $x`
     */
    public function asGeneratorTemplateType(): Type
    {
        $fallback_values = UnionType::empty();
        $fallback_keys = UnionType::empty();

        foreach ($this->type_set as $type) {
            if ($type->isGenerator()) {
                if ($type->hasTemplateParameterTypes()) {
                    return $type;
                }
            }
            // TODO: support Iterator<T> or Traversable<T> or iterable<T>
            if ($type instanceof GenericArrayType) {
                $fallback_values = $fallback_values->withType($type->genericArrayElementType());
                $key_type = $type->getKeyType();
                if ($key_type === GenericArrayType::KEY_INT) {
                    $fallback_keys = $fallback_keys->withType(IntType::instance(false));
                } elseif ($key_type === GenericArrayType::KEY_STRING) {
                    $fallback_keys = $fallback_keys->withType(StringType::instance(false));
                }
            } elseif ($type instanceof ArrayShapeType && $type->isNotEmptyArrayShape()) {
                $fallback_values = $fallback_values->withUnionType($type->genericArrayElementUnionType());
                $fallback_keys = $fallback_keys->withUnionType(GenericArrayType::unionTypeForKeyType($type->getKeyType()));
            }
        }

        // @phan-suppress-next-line PhanThrowTypeAbsentForCall
        $result = Type::fromFullyQualifiedString('\Generator');
        if ($fallback_keys->typeCount() > 0 || $fallback_values->typeCount() > 0) {
            $template_types = $fallback_keys->typeCount() > 0 ? [$fallback_keys, $fallback_values] : [$fallback_values];
            $result = $result->fromType($result, $template_types);
        }
        return $result;
    }

    /**
     * @return Generator<Type,Type> ($outer_type => $inner_type)
     *
     * This includes classes, StaticType (and "self"), and TemplateType.
     * This includes duplicate definitions
     * TODO: Warn about Closure Declarations with invalid parameters...
     *
     * TODO: Use different helper for GoToDefinitionRequest
     */
    public function getReferencedClasses(): Generator
    {
        foreach ($this->withFlattenedArrayShapeOrLiteralTypeInstances()->getTypeSet() as $outer_type) {
            foreach ($outer_type->getReferencedClasses() as $type) {
                yield $outer_type => $type;
            }
        }
    }

    /**
     * @return UnionType the union type of applying the minus operator to an expression with this union type
     */
    public function applyUnaryMinusOperator(): UnionType
    {
        /** @param int|float $value */
        return $this->applyNumericOperation(static function ($value): ScalarType {
            $result = -$value;
            if (\is_int($result)) {
                return LiteralIntType::instanceForValue($result, false);
            }
            // -INT_MIN is a float.
            return LiteralFloatType::instanceForValue($result, false);
        });
    }

    /**
     * @return UnionType the union type of applying the unary bitwise not operator to an expression with this union type
     */
    public function applyUnaryBitwiseNotOperator(): UnionType
    {
        return UnionType::of(
            self::applyUnaryBitwiseNotOperatorToList($this->type_set, false),
            self::applyUnaryBitwiseNotOperatorToList($this->real_type_set, true)
        );
    }

    /**
     * @param list<Type> $type_set
     * @return list<IntType|StringType>
     */
    private static function applyUnaryBitwiseNotOperatorToList(array $type_set, bool $is_real): array
    {
        if (!$type_set) {
            // Can be int|string
            return UnionType::typeSetFromString($is_real ? 'int|string' : 'int');
        }
        $result = [];
        foreach ($type_set as $type) {
            // ~null is an error, don't check isNullable()
            if ($type instanceof LiteralTypeInterface) {
                $value = ~$type->getValue();
                if (is_int($value)) {
                    $result[] = LiteralIntType::instanceForValue($value, false);
                } else {
                    $result[] = LiteralStringType::instanceForValue($value, false);
                }
                continue;
            }
            if ($type instanceof IntType || $type instanceof FloatType) {
                $result[] = IntType::instance(false);
            } elseif ($type instanceof StringType || $type instanceof CallableType) {
                $result[] = StringType::instance(false);
            } elseif ($type instanceof MixedType) {
                $result[] = StringType::instance(false);
                $result[] = IntType::instance(false);
            }
            // Not going to bother with objects for now
        }
        return $result ?: self::intOrStringTypeSet();
    }

    /**
     * Returns the boolean negation of this type.
     */
    public function applyUnaryNotOperator(): UnionType
    {
        return UnionType::of(
            self::applyUnaryNotOperatorToList($this->type_set),
            self::applyUnaryNotOperatorToList($this->real_type_set)
        );
    }

    /**
     * @param Type[] $type_set
     * @return list<Type>
     */
    private static function applyUnaryNotOperatorToList(array $type_set): array
    {
        $contains_falsey = false;
        $contains_truthy = false;
        foreach ($type_set as $type) {
            if ($type->isPossiblyFalsey()) {
                $contains_falsey = true;
            }
            if ($type->isPossiblyTruthy()) {
                $contains_truthy = true;
            }
            if ($contains_falsey && $contains_truthy) {
                return UnionType::typeSetFromString('bool');
            }
        }
        if ($contains_truthy) {
            return UnionType::typeSetFromString('false');
        } elseif ($contains_falsey) {
            return UnionType::typeSetFromString('true');
        }
        return UnionType::typeSetFromString('bool');
    }

    /**
     * Returns the boolean cast of this type
     */
    public function applyBoolCast(): UnionType
    {
        return UnionType::of(
            self::applyBoolCastToList($this->type_set),
            self::applyBoolCastToList($this->real_type_set)
        );
    }

    /**
     * @param Type[] $type_set
     * @return list<Type>
     */
    private static function applyBoolCastToList(array $type_set): array
    {
        $contains_falsey = false;
        $contains_truthy = false;
        foreach ($type_set as $type) {
            if ($type->isPossiblyFalsey()) {
                $contains_falsey = true;
            }
            if ($type->isPossiblyTruthy()) {
                $contains_truthy = true;
            }
            if ($contains_falsey && $contains_truthy) {
                return UnionType::typeSetFromString('bool');
            }
        }
        if ($contains_truthy) {
            return UnionType::typeSetFromString('true');
        } elseif ($contains_falsey) {
            return UnionType::typeSetFromString('false');
        }
        return UnionType::typeSetFromString('bool');
    }

    /** @return list<IntType|StringType> */
    private static function intOrStringTypeSet(): array
    {
        static $types;
        return $types ?? ($types = UnionType::typeSetFromString('int|string'));
    }

    /**
     * @return UnionType the union type of applying the unary plus operator to an expression with this union type
     */
    public function applyUnaryPlusOperator(): UnionType
    {
        /** @param int|float $value */
        return $this->applyNumericOperation(static function ($value): ScalarType {
            $result = +$value;
            if (\is_int($result)) {
                return LiteralIntType::instanceForValue($result, false);
            }
            return LiteralFloatType::instanceForValue($result, false);
        });
    }

    /**
     * @param Closure(int|float): ScalarType $operation
     */
    private function applyNumericOperation(Closure $operation): UnionType
    {
        return UnionType::of(
            self::applyNumericOperationToList($this->type_set, $operation),
            self::applyNumericOperationToList($this->real_type_set, $operation)
        );
    }

    /**
     * @param List<Type> $type_set
     * @param Closure(int|float): ScalarType $operation
     * @return list<ScalarType>
     */
    private static function applyNumericOperationToList(array $type_set, Closure $operation): array
    {
        $added_fallbacks = false;
        $result = [];
        foreach ($type_set as $type) {
            if ($type->isNullable()) {
                $result[] = LiteralIntType::instanceForValue(0, false);
            }
            if ($type instanceof LiteralIntType || $type instanceof LiteralFloatType) {
                $result[] = $operation($type->getValue());
            } else {
                if ($type instanceof LiteralStringType) {
                    if (\is_numeric($type->getValue())) {
                        $result[] = $operation(+$type->getValue());
                    } else {
                        // TODO: Could warn about non-numeric operand instead
                        $result[] = LiteralIntType::instanceForValue(0, false);
                    }
                    continue;
                }
                if ($added_fallbacks) {
                    continue;
                }
                if (!($type instanceof IntType)) {
                    $result[] = FloatType::instance(false);
                    if (!($type instanceof FloatType)) {
                        $result[] = IntType::instance(false);
                    }
                    $added_fallbacks = true;
                } else {
                    $result[] = IntType::instance(false);
                    // Keep added_fallbacks false in case this needs to add FloatType
                }
            }
        }
        if (!$result) {
            return UnionType::typeSetFromString('int|float');
        }
        return $result;
    }

    /**
     * @return ?string|?float|?int|bool|null
     * If this union type can be represented by a single scalar value,
     * then this returns that scalar value.
     *
     * Otherwise, this returns null.
     */
    public function asSingleScalarValueOrNull()
    {
        $type_set = $this->type_set;
        if (\count($type_set) !== 1) {
            return null;
        }
        $type = \reset($type_set);
        // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal TODO: Infer non-empty-array from count
        switch (\get_class($type)) {
            case LiteralIntType::class:
                return $type->isNullable() ? null : $type->getValue();
            case LiteralFloatType::class:
                return $type->isNullable() ? null : $type->getValue();
            case LiteralStringType::class:
                return $type->isNullable() ? null : $type->getValue();
            case FalseType::class:
                return false;
            case TrueType::class:
                return true;
            // case NullType::class:
            // case VoidType::class:
            default:
                return null;
        }
    }

    /**
     * @return ?string|?float|?int|bool|null|?UnionType
     * If this union type can be represented by a single scalar value or null,
     * then this returns that scalar value.
     *
     * Otherwise, this returns $this.
     */
    public function asSingleScalarValueOrNullOrSelf()
    {
        $type_set = $this->type_set;
        if (\count($type_set) !== 1) {
            return $this;
        }
        $type = \reset($type_set);
        // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
        if ($type->isNullable()) {
            return ($type instanceof NullType || $type instanceof VoidType) ? null : $this;
        }
        // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal
        switch (\get_class($type)) {
            case LiteralIntType::class:
            case LiteralStringType::class:
                return $type->getValue();
            case FalseType::class:
                return false;
            case TrueType::class:
                return true;
            default:
                return $this;
        }
    }

    /**
     * @return ?array|?string|?float|?int|bool|null|?UnionType
     * If this union type can be represented by a PHP value or null,
     * then this returns that PHP value.
     *
     * Otherwise, this returns $this.
     */
    public function asValueOrNullOrSelf()
    {
        $type_set = $this->type_set;
        if (\count($type_set) !== 1) {
            return $this;
        }
        $type = \reset($type_set);
        // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
        if ($type->isNullable()) {
            return ($type instanceof NullType || $type instanceof VoidType) ? null : $this;
        }
        // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal
        switch (\get_class($type)) {
            case ArrayShapeType::class:
                return $type->asArrayLiteralOrNull() ?? $this;
            case LiteralIntType::class:
                return $type->getValue();
            case LiteralStringType::class:
                return $type->getValue();
            case FalseType::class:
                return false;
            case TrueType::class:
                return true;
            case NullType::class:
                return null;
            default:
                return $this;
        }
    }

    /**
     * @return list<string>
     * Returns a list of known strings for LiteralStringType instances in this union type.
     * E.g. for `?'foo'|?'bar'|?false|?T`, returns `['foo', 'bar']`
     * @suppress PhanUnreferencedPublicMethod provided for plugins
     */
    public function asStringScalarValues(): array
    {
        $result = [];
        foreach ($this->type_set as $type) {
            if ($type instanceof LiteralStringType) {
                $result[] = $type->getValue();
            }
        }
        return $result;
    }

    /**
     * @return list<int>
     * Returns a list of known integers for LiteralIntType instances in this union type.
     * E.g. for `?11|?2|?false|?T`, returns `[11, 2]`
     * @suppress PhanUnreferencedPublicMethod provided for plugins
     */
    public function asIntScalarValues(): array
    {
        $result = [];
        foreach ($this->type_set as $type) {
            if ($type instanceof LiteralIntType) {
                $result[] = $type->getValue();
            }
        }
        return $result;
    }

    /**
     * @return ?list<?int|?bool|?string|?float>
     * Returns a list of known scalars that this union type could be.
     * E.g. for `?11|?2|?false|?'str'|?T`, returns `[11, 2, false, 'str', null]`
     *
     * If $strict is true, this will return null if there are non-scalar types.
     * @suppress PhanUnreferencedPublicMethod provided for plugins
     */
    public function asScalarValues(bool $strict = false): ?array
    {
        $result = [];
        $has_null = false;
        $has_false = false;
        $has_true = false;
        foreach ($this->type_set as $type) {
            if ($type->isNullable()) {
                $has_null = true;
            }
            switch (\get_class($type)) {
                case LiteralIntType::class:
                case LiteralFloatType::class:
                case LiteralStringType::class:
                    $result[] = $type->getValue();
                    break;
                case FalseType::class:
                    $has_false = true;
                    break;
                case TrueType::class:
                    $has_true = true;
                    break;
                case BoolType::class:
                    $has_true = true;
                    $has_false = true;
                    break;
                case NullType::class:
                case VoidType::class:
                    // already set $has_null = true;
                    break;
                default:
                    if ($strict) {
                        return null;
                    }
                    break;
            }
        }
        if ($has_null) {
            $result[] = null;
        }
        if ($has_false) {
            $result[] = false;
        }
        if ($has_true) {
            $result[] = true;
        }
        return $result;
    }

    /**
     * Returns true if this contains a type that is definitely nullable or a non-object.
     * e.g. returns true for ?T, T|false, T|array
     *      returns false for T|callable, object, T|iterable, etc.
     */
    public function containsDefiniteNonObjectType(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->isNullable() || $type->isDefiniteNonObjectType()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if this contains a type that is definitely nullable or a non-object/non-string.
     * e.g. returns true for ?T, T|false, T|array
     *      returns false for T|callable, object, T|iterable, etc.
     */
    public function containsDefiniteNonObjectAndNonClassType(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->isNullable() || ($type->isDefiniteNonObjectType() && !$type instanceof StringType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if at least one type in this union type definitely can't be cast to `callable`
     */
    public function containsDefiniteNonCallableType(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type->isNullable() || $type->isDefiniteNonCallableType()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if either (1) this is the empty type, or (2) at least one type in this union type can't be ruled out as being callable.
     */
    public function hasPossiblyCallableType(): bool
    {
        foreach ($this->type_set as $type) {
            if (!$type->isDefiniteNonCallableType()) {
                return true;
            }
        }
        return \count($this->type_set) === 0;
    }

    /**
     * Returns the union type resulting from applying the `++`/`--` operator to an expression with union type.
     * TODO: Compute the real type set
     */
    public function getTypeAfterIncOrDec(): UnionType
    {
        $result = UnionType::empty();
        foreach ($this->type_set as $type) {
            $result = $result->withUnionType($type->getTypeAfterIncOrDec());
        }
        return $result;
    }

    /**
     * @param TemplateType $template_type the template type that this union type is being searched for
     *
     * @return ?Closure(UnionType, Context):UnionType a closure to map types to the template type wherever it was in the original union type
     */
    public function getTemplateTypeExtractorClosure(CodeBase $code_base, TemplateType $template_type): ?Closure
    {
        $closure = null;
        foreach ($this->type_set as $type) {
            $closure = TemplateType::combineParameterClosures(
                $closure,
                $type->getTemplateTypeExtractorClosure($code_base, $template_type)
            );
        }
        return $closure;
    }

    /**
     * Returns true if this references $template_type in any way
     */
    public function usesTemplateType(TemplateType $template_type): bool
    {
        $new_union_type = $this->withTemplateParameterTypeMap([
            $template_type->getName() => UnionType::fromFullyQualifiedPHPDocString('mixed'),
        ]);
        return !$this->isEqualTo($new_union_type);
    }

    /**
     * @return bool
     * True if this is the void type
     */
    public function isVoidType(): bool
    {
        $type_set = $this->type_set;
        if (\count($type_set) !== 1) {
            return false;
        }
        return \reset($type_set) instanceof VoidType;
    }

    /**
     * Shorter version of `UnionType::of($this->getTypeSet(), [$type])`
     * @suppress PhanAccessReadOnlyProperty
     */
    public function withRealType(Type $type): UnionType
    {
        $real_type_set = [$type];
        if ($this->real_type_set === $real_type_set) {
            return $this;
        }
        if (!$this->type_set) {
            return $type->asRealUnionType();
        }
        $new_type = clone($this);
        $new_type->real_type_set = $real_type_set;
        return $new_type;
    }

    /**
     * Shorter version of `UnionType::of($this->type_set, $real_type_set)`
     * @param ?list<Type> $real_type_set
     * @suppress PhanAccessReadOnlyProperty
     */
    public function withRealTypeSet(?array $real_type_set): UnionType
    {
        if ($this->real_type_set === $real_type_set) {
            return $this;
        }
        if (!$real_type_set) {
            return $this->eraseRealTypeSetRecursively();
        }
        if (!$this->type_set) {
            return UnionType::of($real_type_set, $real_type_set);
        }
        $new_type = clone($this);
        $new_type->real_type_set = $real_type_set;
        return $new_type;
    }

    /**
     * Converts the real part of the union type to a standalone union type
     */
    public function getRealUnionType(): UnionType
    {
        $real_type_set = $this->real_type_set;
        if ($this->type_set === $real_type_set) {
            return $this;
        }
        if (!$real_type_set) {
            return UnionType::empty();
        } elseif (\count($real_type_set) === 1) {
            // @phan-suppress-next-line PhanPossiblyNonClassMethodCall
            return \reset($real_type_set)->asRealUnionType();
        }
        return new UnionType($real_type_set, true, $real_type_set);
    }

    /**
     * Converts a phpdoc type into the real union type equivalent.
     */
    public function asRealUnionType(): UnionType
    {
        return UnionType::ofUnique($this->type_set, $this->type_set);
    }

    /**
     * Check if this is exclusively real types.
     * TODO: Could check if real types are in a different order
     */
    public function isExclusivelyRealTypes(): bool
    {
        return \count($this->real_type_set) > 0 && $this->type_set === $this->real_type_set;
    }

    /**
     * Returns a detailed representation of this union type that can be used to debug issues when developing.
     * The representation may change - this should not be used for issue messages, etc.
     *
     * @suppress PhanUnreferencedPublicMethod
     */
    public function getDebugRepresentation(): string
    {
        $representation = $this->__toString();
        if ($this->real_type_set) {
            $representation .= "(real=" . $this->getRealUnionType()->__toString() . ")";
        }
        if ($representation === '') {
            return '(empty union type)';
        }
        return $representation;
    }

    /**
     * Returns true if this type has types for which `+expr` isn't an integer.
     */
    public function hasTypesCoercingToNonInt(): bool
    {
        foreach ($this->type_set as $type) {
            if ($type instanceof FloatType || $type instanceof StringType) {
                // TODO Could check for LiteralStringType
                return true;
            }
        }
        return \count($this->type_set) === 0;
    }

    /**
     * Returns true if this is the empty array shape (or the nullable version of it)
     */
    public function isEmptyArrayShape(): bool
    {
        foreach ($this->type_set as $type) {
            if (!($type instanceof ArrayShapeType) || $type->isNotEmptyArrayShape()) {
                return false;
            }
        }
        return \count($this->type_set) !== 0;
    }

    /**
     * Overridden in AnnotatedUnionType, which can be possibly nullable
     * @suppress PhanUnreferencedPublicMethod possibly useful for future code.
     */
    public function convertUndefinedToNullable(): UnionType
    {
        return $this;
    }

    /**
     * Returns a generator that yields all types and subtypes in the phpdoc type set.
     *
     * For example, for the union type `MyClass[]|false`, 3 types will be generated: `MyClass[]`, `MyClass`, and `false`.
     * This does not deduplicate types.
     *
     * @return Generator<Type>
     */
    public function getTypesRecursively(): Generator
    {
        foreach ($this->type_set as $type) {
            yield from $type->getTypesRecursively();
        }
    }
}

UnionType::init();
