src/Phan/Language/Type.php

Summary

Maintainability
F
1 mo
Test Coverage
<?php

declare(strict_types=1);

namespace Phan\Language;

use AssertionError;
use ast\flags;
use Closure;
use Error;
use Generator;
use InvalidArgumentException;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Config;
use Phan\Debug\Frame;
use Phan\Exception\EmptyFQSENException;
use Phan\Exception\FQSENException;
use Phan\Exception\InvalidFQSENException;
use Phan\Exception\IssueException;
use Phan\Exception\RecursionDepthException;
use Phan\Issue;
use Phan\Language\Element\Comment\Builder;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\Type\ArrayShapeType;
use Phan\Language\Type\ArrayType;
use Phan\Language\Type\AssociativeArrayType;
use Phan\Language\Type\BoolType;
use Phan\Language\Type\CallableArrayType;
use Phan\Language\Type\CallableDeclarationType;
use Phan\Language\Type\CallableObjectType;
use Phan\Language\Type\CallableStringType;
use Phan\Language\Type\CallableType;
use Phan\Language\Type\ClassStringType;
use Phan\Language\Type\ClosureDeclarationParameter;
use Phan\Language\Type\ClosureDeclarationType;
use Phan\Language\Type\ClosureType;
use Phan\Language\Type\FalseType;
use Phan\Language\Type\FloatType;
use Phan\Language\Type\FunctionLikeDeclarationType;
use Phan\Language\Type\GenericArrayTemplateKeyType;
use Phan\Language\Type\GenericArrayType;
use Phan\Language\Type\GenericIterableType;
use Phan\Language\Type\GenericMultiArrayType;
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\MixedType;
use Phan\Language\Type\NativeType;
use Phan\Language\Type\NonEmptyAssociativeArrayType;
use Phan\Language\Type\NonEmptyGenericArrayType;
use Phan\Language\Type\NonEmptyListType;
use Phan\Language\Type\NonEmptyMixedType;
use Phan\Language\Type\NonEmptyStringType;
use Phan\Language\Type\NonNullMixedType;
use Phan\Language\Type\NonZeroIntType;
use Phan\Language\Type\NullType;
use Phan\Language\Type\ObjectType;
use Phan\Language\Type\ResourceType;
use Phan\Language\Type\ScalarRawType;
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 Phan\Library\StringUtil;
use Phan\Library\Tuple5;

use function count;
use function explode;
use function in_array;
use function ltrim;
use function preg_match;
use function strcasecmp;
use function stripos;
use function strtolower;
use function substr;
use function trim;

/**
 * The base class for all of Phan's types.
 * A plain Type represents a class instance.
 * Separate subclasses exist for NativeType, ArrayType, ScalarType, TemplateType, etc.
 *
 *
 * @phan-file-suppress PhanPartialTypeMismatchArgumentInternal
 * @phan-file-suppress PhanSuspiciousTruthyString
 * phpcs:disable Generic.NamingConventions.UpperCaseConstantName
 * @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 types.
 */
class Type
{
    use \Phan\Memoize;

    /**
     * @var string
     * A legal type identifier (e.g. 'int' or 'DateTime')
     */
    public const simple_type_regex =
        '(\??)(?:callable-(?:string|object|array)|associative-array|class-string|lowercase-string|non-(?:zero-int|null-mixed|empty-(?:associative-array|array|list|string|lowercase-string|mixed))|\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*)';

    public const simple_noncapturing_type_regex =
        '\\\\?(?:callable-(?:string|object|array)|associative-array|class-string|lowercase-string|non-(?:zero-int|null-mixed|empty-(?:associative-array|array|list|string|lowercase-string|mixed))|[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*)';

    /**
     * @var string
     * A legal type identifier (e.g. 'int' or 'DateTime')
     */
    public const simple_type_regex_or_this =
        '(\??)(callable-(?:string|object|array)|associative-array|class-string|lowercase-string|non-(?:zero-int|null-mixed|empty-(?:associative-array|array|list|string|lowercase-string|mixed))|\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*|\$this)';

    public const shape_key_regex =
        '(?:[-.\/^;$%*+_a-zA-Z0-9\x7f-\xff]|\\\\(?:[nrt\\\\]|x[0-9a-fA-F]{2}))+\??';

    /**
     * A literal integer or string.
     *
     * Note that string literals can only contain a whitelist of characters.
     * NOTE: The / is escaped
     */
    public const noncapturing_literal_regex =
        '\??(?:-?(?:(?:0|[1-9][0-9]*)(?:\.[0-9]+)?)|\'(?:[- ,.\/?:;"!#$%^&*_+=a-zA-Z0-9_\x80-\xff]|\\\\(?:[\'\\\\]|x[0-9a-fA-F]{2}))*\')';
        // '\??(?:-?(?:0|[1-9][0-9]*)|\'(?:[a-zA-Z0-9_])*\')';

    /**
     * @var string
     * A legal array entry in an array shape (e.g. 'field:string[]')
     *
     * @suppress PhanUnreferencedPublicClassConstant
     */
    public const array_shape_entry_regex_noncapturing =
        '(?:' . self::shape_key_regex . '\s*:)?\s*(?:' . self::simple_noncapturing_type_regex . '=?)';

    /**
     * @var string
     * A legal type identifier matching a type optionally with a []
     * indicating that it's a generic typed array (e.g. 'int[]',
     * 'string' or 'Set<DateTime>' or 'array{field:string}')
     *
     * https://www.debuggex.com/ is useful for a visual description of these regexes
     */
    public const type_regex =
        '('
        . '(?:\??\((?-1)(?:[|&](?-1))*\)|'  // Recursion: "?(T)" or "(T)" with brackets. Also allow parsing (a|b) within brackets.
        . '(?:'
          . '\??(?:\\\\?Closure|callable)(\((?:[^()]|(?-1))*\))'  // `Closure(...)` can have matching pairs of () inside `...`, recursively
          . '(?:\s*:\s*'  // optional return type, can be ":T" or ":(T1|T2)" or ": ?(T1|T2)"
            . '(?:'
              . self::simple_noncapturing_type_regex . '|'  // Forbid ambiguity in `Closure():int[]` by disallowing complex return types without '()'. Always parse that as `(Closure():int)[]`.
              . '\((?-2)(?:\s*[|&]\s*(?-2))*\)'
            . ')'
          . ')?'
        . ')|'
        . self::noncapturing_literal_regex . '|'
        . '(' . self::simple_type_regex . ')'  // ?T or T.
        . '(?:'
          . '<'
            . '('
              . '(?-5)(?:[|&](?-5))*'
              . '(?:\s*,\s*'
                . '(?-5)(?:[|&](?-5))*'
              . ')*'
            . ')'
          . '>'
          . '|'
          . '\{('  // Expect either '{' or '<', after a word token.
            . '(?:' . self::shape_key_regex . '\s*:)?\s*(?-6)(?:[|&](?-6))*=?'  // {shape_key_regex:<type_regex>}
            . '(?:,(?:\s*' . self::shape_key_regex . '\s*:)?\s*(?-6)(?:[|&](?-6))*=?)*'  // {shape_key_regex:<type_regex>}
          . ')?\})?'
        . ')'
        . '(?:\[\])*'
      . ')';

    /**
     * @var string
     * A legal type identifier matching a type optionally with a []
     * indicating that it's a generic typed array (e.g. 'int[]' or '$this[]',
     * 'string' or 'Set<DateTime>' or 'array<int>' or 'array<int|string>')
     *
     * https://www.debuggex.com/ is useful for a visual description of these regexes
     */
    public const type_regex_or_this =
        '('
        . '('
          . '(?:'
            . '\??\((?-1)(?:[|&](?-1))*\)|'  // Recursion: "?(T)" or "(T)" with brackets. Also allow parsing (a|b) within brackets.
            . '(?:'
              . '\??(?:\\\\?Closure|callable)(\((?:[^()]|(?-1))*\))'  // `Closure(...)` can have matching pairs of () inside `...`, recursively
              . '(?:\s*:\s*'  // optional return type, can be ":T" or ":(T1|T2)"
                . '(?:'
                  . self::simple_noncapturing_type_regex . '|'  // Forbid ambiguity in `Closure():int[]` by disallowing complex return types without '()'. Always parse that as `(Closure():int)[]`.
                  . '\((?-2)(?:\s*[|&]\s*(?-2))*\)'  // Complicated return types can be placed within ().
                . ')'
              . ')?'
            . ')|'
            . self::noncapturing_literal_regex . '|'
            . '(' . self::simple_type_regex_or_this . ')'  // 3 patterns
            . '(?:<'
              . '('
                . '(?-6)(?:[|&](?-6))*'  // We use relative references instead of named references so that more than one one type_regex can be used in a regex.
                . '(?:\s*,\s*'
                  . '(?-6)(?:[|&](?-6))*'
                . ')*'
              . ')'
              . '>'
              . '|'
              . '(\{)('  // Expect either '{' or '<', after a word token. Match '{' to disambiguate 'array{}'
                . '(?:' . self::shape_key_regex . '\s*:)?\s*(?-8)(?:[|&](?-8))*=?'  // {shape_key_regex:<type_regex>}
                . '(?:,(?:\s*' . self::shape_key_regex . '\s*:)?\s*(?-8)(?:[|&](?-8))*=?)*'  // {shape_key_regex:<type_regex>}
              . ')?\})?'
            . ')'
          . '(?:\[\])*'
        . ')'
       . ')';

    /**
     * @var array<string,bool> - For checking if a string is an internal type. This is used for case-insensitive lookup.
     */
    public const _internal_type_set = [
        'associative-array' => true,
        'array'           => true,
        'bool'            => true,
        'callable'        => true,
        'callable-array'  => true,
        'callable-object' => true,
        'callable-string' => true,
        'class-string'    => true,
        'false'           => true,
        'float'           => true,
        'int'             => true,
        'iterable'        => true,
        'list'            => true,
        'lowercase-string' => true,
        'mixed'           => true,
        'non-empty-array' => true,
        'non-empty-associative-array' => true,
        'non-empty-mixed' => true,
        'non-null-mixed' => true,
        'non-empty-list'  => true,
        'non-empty-string' => true,
        'non-empty-lowercase-string' => true,
        'non-zero-int'    => true,
        'null'            => true,
        'object'          => true,
        'resource'        => true,
        'scalar'          => true,
        'static'          => true,
        'string'          => true,
        'true'            => true,
        'void'            => true,
    ];

    /**
     * These can currently be used in phpdoc but not real types.
     * This is a subset of self::_internal_type_set
     *
     * https://secure.php.net/manual/en/reserved.other-reserved-words.php
     * > The following list of words have had soft reservations placed on them.
     * > Whilst they may still be used as class, interface, and trait names (as well as in namespaces),
     * > usage of them is highly discouraged since they may be used in future versions of PHP.
     *
     * (numeric not supported yet)
     */
    public const _soft_internal_type_set = [
        'resource'  => true,
        'scalar'    => true,
        'true'      => true,
    ];

    // Distinguish between multiple ways types can be created.
    // e.g. integer and resource are phpdoc types, but they aren't actual types.

    /** For types created from a type in an AST node, e.g. `int $x` */
    public const FROM_NODE = 0;

    /** For types copied from another type, e.g. `$x = $y` gets types from $y */
    public const FROM_TYPE = 1;

    /** For types copied from phpdoc, e.g. `(at)param integer $x` */
    public const FROM_PHPDOC = 2;

    /**
     * To distinguish NativeType subclasses and classes with the same name.
     * Overridden in some subclasses but not others.
     */
    public const KEY_PREFIX = '';

    /** To normalize combinations of union types */
    public const _bit_false    = (1 << 0);
    public const _bit_true     = (1 << 1);
    public const _bit_bool_combination = self::_bit_false | self::_bit_true;
    public const _bit_nullable = (1 << 2);

    /**
     * @var string
     * The namespace of this type such as '' (for internal types such as 'int')
     * or '\' or '\Phan\Language'
     */
    protected $namespace = null;

    /**
     * @var string
     * The name of this type such as 'int' or 'MyClass'
     */
    protected $name = '';

    /**
     * @var list<UnionType>
     * A possibly empty list of concrete types that
     * act as parameters to this type if it is a templated
     * type.
     */
    protected $template_parameter_type_list = [];

    /**
     * @var bool
     * True if this type is nullable, else false
     */
    protected $is_nullable = false;

    /**
     * @var array<string,Type> - Maps a key to a Type or subclass of Type
     */
    private static $canonical_object_map = [];

    /**
     * @var ?string the progress state of the app. Used to clear memoizations for Type instances not in canonical_object_map.
     * TODO: Look into WeakMap and garbage collection in php 8, if it is supported?
     */
    protected static $current_progress_state = null;

    /**
     * @param string $namespace
     * The (optional) namespace of the type such as '\'
     * or '\Phan\Language'.
     * This should not be the empty string.
     *
     * @param string $name
     * The name of the type such as 'int' or 'MyClass'
     *
     * @param list<UnionType> $template_parameter_type_list
     * A (possibly empty) list of template parameter types
     *
     * @param bool $is_nullable
     * True if this type can be null, false if it cannot
     * be null.
     */
    protected function __construct(
        string $namespace,
        string $name,
        $template_parameter_type_list,
        bool $is_nullable
    ) {
        $this->namespace = $namespace;
        $this->name = $name;
        $this->template_parameter_type_list = $template_parameter_type_list;
        $this->is_nullable = $is_nullable;
    }

    // Override two magic methods to ensure that Type isn't being cloned accidentally.
    // (It has previously been accidentally cloned in unit tests by phpunit (global_state helper),
    //  which saves and restores some static properties)

    /**
     * @throws Error this should not be called accidentally
     * @suppress PhanPluginRemoveDebugCall deliberate output before uncatchable Error
     */
    public function __wakeup()
    {
        \debug_print_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
        throw new Error("Cannot unserialize Type '$this'");
    }

    /** @throws Error this should not be called accidentally */
    public function __clone()
    {
        throw new Error("Cannot clone Type '$this'");
    }

    /**
     * @param string $namespace
     * The (optional) namespace of the type such as '\'
     * or '\Phan\Language'.
     *
     * @param string $type_name
     * The name of the type such as 'int' or 'MyClass'
     *
     * @param list<UnionType> $template_parameter_type_list
     * A (possibly empty) list of template parameter types
     *
     * @param bool $is_nullable
     * True if this type can be null, false if it cannot
     * be null.
     *
     * @param int $source
     * Type::FROM_NODE, Type::FROM_TYPE, or Type::FROM_PHPDOC
     * (E.g. outside of phpdoc, "integer" would be a class name)
     *
     * @return Type
     * A single canonical instance of the given type.
     *
     * @suppress PhanThrowTypeAbsent
     *
     * Overridden in some subclasses but not others.
     */
    protected static function make(
        string $namespace,
        string $type_name,
        array $template_parameter_type_list,
        bool $is_nullable,
        int $source
    ): Type {

        $namespace = \trim($namespace);

        if ('\\' === $namespace && $source) {
            $type_name = self::canonicalNameFromName($type_name);
        }

        // If this looks like a generic type string, explicitly
        // make it as such
        $pos = \strrpos($type_name, '[]');
        if ($pos > 0) {
            return GenericArrayType::fromElementType(Type::make(
                $namespace,
                \substr($type_name, 0, $pos),
                $template_parameter_type_list,
                false,
                $source
            ), $is_nullable, GenericArrayType::KEY_MIXED);
        }

        if ($namespace === '') {
            throw new InvalidFQSENException("Namespace cannot be empty", $type_name);
        }

        if ('\\' !== $namespace[0]) {
            throw new InvalidFQSENException("Namespace must be fully qualified", "$namespace\\$type_name");
        }

        if ($type_name === '') {
            throw new EmptyFQSENException("Type name cannot be empty", \rtrim($namespace, "\\") . "\\");
        }

        if (\strpos($type_name, '|') !== false) {
            throw new InvalidFQSENException("Type name '$type_name' may not contain a pipe", $type_name);
        }

        // Create a canonical representation of the
        // namespace and name
        if ('\\' === $namespace && $source === Type::FROM_PHPDOC) {
            $type_name = self::canonicalNameFromName($type_name);
        }

        // Make sure we only ever create exactly one
        // object for any unique type
        $key = ($is_nullable ? '?' : '') . static::KEY_PREFIX . $namespace . '\\' . $type_name;

        if ($template_parameter_type_list) {
            $key .= '<' . \implode(',', \array_map(static function (UnionType $union_type): string {
                return $union_type->__toString();
            }, $template_parameter_type_list)) . '>';
        }

        $key = strtolower($key);

        $value = self::$canonical_object_map[$key] ?? null;
        if (!$value) {
            if ($namespace === '\\') {
                switch (strtolower($type_name)) {
                    case 'closure':
                        $value = new ClosureType(
                            '\\',
                            'Closure',
                            $template_parameter_type_list,
                            $is_nullable
                        );
                        break;
                    case 'callable':
                        $value = new CallableType(
                            '\\',
                            'callable',
                            $template_parameter_type_list,
                            $is_nullable
                        );
                        break;
                    case 'callable-object':
                        $value = new CallableObjectType(
                            $is_nullable
                        );
                        break;
                    case 'callable-string':
                        $value = new CallableStringType(
                            $is_nullable
                        );
                        break;
                    case 'callable-array':
                        $value = new CallableArrayType(
                            '\\',
                            'callable-array',
                            $template_parameter_type_list,
                            $is_nullable
                        );
                        break;
                    case 'lowercase-string':
                        $value = StringType::instance($is_nullable);
                        break;
                    case 'class-string':
                        $value = new ClassStringType(
                            '\\',
                            'class-string',
                            $template_parameter_type_list,
                            $is_nullable
                        );
                        break;
                    case 'list':
                        $value = self::parseListTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, false);
                        break;
                    case 'associative-array':
                        $value = self::parseGenericArrayTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, false, true);
                        break;
                    case 'non-empty-associative-array':
                        $value = self::parseGenericArrayTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, true, true);
                        break;
                    case 'non-empty-array':
                        $value = self::parseGenericArrayTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, true, false);
                        break;
                    case 'non-empty-list':
                        $value = self::parseListTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, true);
                        break;
                    case 'non-empty-string':
                    case 'non-empty-lowercase-string':
                        $value = new NonEmptyStringType($is_nullable);
                        break;
                    case 'non-zero-int':
                        $value = new NonZeroIntType($is_nullable);
                        break;
                }
            }
            if (!$value) {
                $value = new static(
                    $namespace,
                    $type_name,
                    $template_parameter_type_list,
                    $is_nullable
                );
            }
            self::$canonical_object_map[$key] = $value;
        }
        return $value;
    }

    /**
     * Call this before forking and analysis phase, when in daemon mode.
     * This may hurt performance.
     *
     * It's important to clear asExpandedTypes(),
     * as the parent classes may have changed since the last parse attempt.
     *
     * This gets called immediately after the parse phase but before the analysis phase.
     */
    public static function clearAllMemoizations(): void
    {
        // Clear anything that has memoized state
        foreach (self::$canonical_object_map as $type) {
            $type->memoizeFlushAll();
        }
    }

    /**
     * Handle the current analysis state changing to parse, analyze, method, etc.
     * Clear any memoizations of expanded types.
     */
    public static function handleChangeCurrentProgressState(?string $state): void
    {
        self::$current_progress_state = $state;
    }

    /**
     * Memoize the result of $fn(), saving the result
     * with key $key.
     *
     * @template T
     *
     * @param string $key
     * The key to use for storing the result of the
     * computation.
     *
     * @param Closure():T $fn
     * A function to compute only once for the given
     * $key.
     *
     * @return T
     * The result of the given computation is returned.
     *
     * This replaces Memoize::memoize.
     * @suppress PhanPartialTypeMismatchReturn
     */
    public function memoize(string $key, Closure $fn)
    {
        if (($this->memoized_data['current_progress_state'] ?? null) !== self::$current_progress_state) {
            $this->memoized_data = [
                'current_progress_state' => self::$current_progress_state,
                $key => $fn(),
            ];
        } elseif (!\array_key_exists($key, $this->memoized_data)) {
            $this->memoized_data[$key] = $fn();
        }

        return $this->memoized_data[$key];
    }

    /**
     * Constructs a type based on the input type and the provided mapping
     * from template type identifiers to concrete union types.
     *
     * @param Type $type
     * The base type of this generic type referencing a
     * generic class
     *
     * @param list<UnionType> $template_parameter_type_list
     * A map from a template type identifier to a
     * concrete union type
     * @phan-side-effect-free
     * @suppress PhanThrowTypeAbsentForCall
     */
    public static function fromType(
        Type $type,
        array $template_parameter_type_list
    ): Type {
        return self::make(
            $type->getNamespace(),
            $type->getName(),
            $template_parameter_type_list,
            $type->is_nullable,
            Type::FROM_TYPE
        );
    }

    /**
     * @param mixed $object
     * @return Type
     * Get a type for the given object. Equivalent to Type::fromObject($object)->asNonLiteralType()
     * @phan-side-effect-free
     */
    public static function nonLiteralFromObject($object): Type
    {
        static $type_map = null;
        if ($type_map === null) {
            $type_map = [
                'integer' => IntType::instance(false),
                'boolean' => BoolType::instance(false),
                'double'  => FloatType::instance(false),
                'string'  => StringType::instance(false),
                'object'  => ObjectType::instance(false),
                'NULL'    => NullType::instance(false),
                'array'   => ArrayType::instance(false),
                'resource' => ResourceType::instance(false),  // For inferring the type of constants STDIN, etc.
            ];
        }
        // gettype(2) doesn't return 'int', it returns 'integer', so use FROM_PHPDOC
        return $type_map[\gettype($object)];
    }

    /**
     * Get a type for the given object
     * @param mixed $object
     * @throws AssertionError if the type was unexpected
     * @phan-side-effect-free
     */
    public static function fromObject($object): Type
    {
        switch (\gettype($object)) {
            case 'integer':
                return LiteralIntType::instanceForValue($object, false);
            case 'string':
                return LiteralStringType::instanceForValue($object, false);
            case 'NULL':
                return NullType::instance(false);
            case 'double':
                return LiteralFloatType::instanceForValue($object, false);
            case 'object':
                // @phan-suppress-next-line PhanThrowTypeMismatchForCall
                return Type::fromFullyQualifiedString('\\' . \get_class($object));
            case 'boolean':
                return $object ? TrueType::instance(false) : FalseType::instance(false);
            case 'array':
                return ArrayType::instance(false);
            case 'resource':
                return ResourceType::instance(false);  // For inferring the type of constants STDIN, etc.
            default:
                throw new \AssertionError("Unknown type " . \gettype($object));
        }
    }

    /**
     * Get a type for the given object.
     * If $object is an array, return an ArrayShapeType (with nested fields) instead of an ArrayType
     * @param mixed $object
     * @throws AssertionError if the type was unexpected
     * @phan-side-effect-free
     */
    public static function fromObjectExtended($object): Type
    {
        if (\is_array($object)) {
            return self::fromArray($object);
        }
        return self::fromObject($object);
    }

    /**
     * Get a type for the given array as an array shape, recursively.
     * @param array<mixed,mixed> $array
     * @throws AssertionError if the type was unexpected
     * @phan-side-effect-free
     */
    public static function fromArray(array $array): ArrayShapeType
    {
        return ArrayShapeType::fromFieldTypes(
            \array_map(
                /**
                 * @param mixed $value
                 */
                static function ($value): UnionType {
                    // TODO: Look into how this is used and add real equivalent?
                    return self::fromObjectExtended($value)->asPHPDocUnionType();
                },
                $array
            ),
            false
        );
    }

    /**
     * @param string $type_name
     * The name of the internal type such as 'int'
     *
     * @param bool $is_nullable
     * Set to true if the type should be nullable, else pass
     * false
     *
     * @param int $source Type::FROM_*
     *
     * @param list<UnionType> $template_parameter_type_list used for static, etc. @phan-mandatory-param
     *
     * @return Type
     * Get a type for the given type name
     *
     * @throws AssertionError if the type was unexpected
     * @phan-side-effect-free
     */
    public static function fromInternalTypeName(
        string $type_name,
        bool $is_nullable,
        int $source,
        array $template_parameter_type_list = []
    ): Type {

        // If this is a generic type (like int[]), return
        // a generic of internal types.
        //
        // When there's a nullability operator such as in
        // `?int[]`, it applies to the array rather than
        // the int
        if (false !== ($pos = \strrpos($type_name, '[]'))) {
            return GenericArrayType::fromElementType(
                self::fromInternalTypeName(
                    \substr($type_name, 0, $pos),
                    false,
                    $source,
                    $template_parameter_type_list
                ),
                $is_nullable,
                GenericArrayType::KEY_MIXED
            );
        }

        $type_name =
            self::canonicalNameFromName($type_name);

        // TODO: Is this worth optimizing into a lookup table?
        switch (strtolower($type_name)) {
            case 'array':
                return ArrayType::instance($is_nullable);
            case 'associative-array':
                return AssociativeArrayType::fromElementType(MixedType::instance(false), $is_nullable, GenericArrayType::KEY_MIXED);
            case 'bool':
                return BoolType::instance($is_nullable);
            case 'callable':
                return CallableType::instance($is_nullable);
            case 'callable-array':
                return CallableArrayType::instance($is_nullable);
            case 'callable-object':
                return CallableObjectType::instance($is_nullable);
            case 'callable-string':
                return CallableStringType::instance($is_nullable);
            case 'class-string':
                return ClassStringType::instance($is_nullable);
            case 'closure':
                return ClosureType::instance($is_nullable);
            case 'false':
                return FalseType::instance($is_nullable);
            case 'float':
                return FloatType::instance($is_nullable);
            case 'int':
                return IntType::instance($is_nullable);
            case 'list':
                return ListType::fromElementType(MixedType::instance(false), $is_nullable);
            case 'mixed':
                return MixedType::instance($is_nullable);
            case 'non-empty-mixed':
                return NonEmptyMixedType::instance($is_nullable);
            case 'non-null-mixed':
                return NonNullMixedType::instance($is_nullable);
            case 'non-empty-array':
                return NonEmptyGenericArrayType::fromElementType(MixedType::instance(false), $is_nullable, GenericArrayType::KEY_MIXED);
            case 'non-empty-associative-array':
                return NonEmptyAssociativeArrayType::fromElementType(MixedType::instance(false), $is_nullable, GenericArrayType::KEY_MIXED);
            case 'non-empty-list':
                return NonEmptyListType::fromElementType(MixedType::instance(false), $is_nullable);
            case 'non-empty-lowercase-string':
            case 'non-empty-string':
                return NonEmptyStringType::instance($is_nullable);
            case 'non-zero-int':
                return NonZeroIntType::instance($is_nullable);
            case 'null':
                return NullType::instance($is_nullable);
            case 'object':
                return ObjectType::instance($is_nullable);
            case 'resource':
                return ResourceType::instance($is_nullable);
            case 'scalar':
                return ScalarRawType::instance($is_nullable);
            case 'string':
            case 'lowercase-string':
                return StringType::instance($is_nullable);
            case 'true':
                return TrueType::instance($is_nullable);
            case 'void':
                return VoidType::instance(false);
            case 'iterable':
                return IterableType::instance($is_nullable);
            case 'static':
            case '$this':
                return StaticType::instanceWithTemplateTypeList($is_nullable, $template_parameter_type_list);
        }

        if (\substr($type_name, 0, 1) === '?') {
            // @phan-suppress-next-line PhanPossiblyFalseTypeArgument
            return self::fromInternalTypeName(\substr($type_name, 1), true, $source, $template_parameter_type_list);
        }
        throw new AssertionError("No internal type with name $type_name");
    }

    /**
     * @param string $namespace
     * A fully qualified namespace
     *
     * @param string $type_name
     * The name of the type
     *
     * @return Type
     * A type representing the given namespace and type
     * name.
     *
     * @param bool $is_nullable
     * True if this type can be null, false if it cannot
     * be null.
     * @phan-side-effect-free
     *
     * @suppress PhanThrowTypeAbsentForCall
     */
    public static function fromNamespaceAndName(
        string $namespace,
        string $type_name,
        bool $is_nullable
    ): Type {
        return self::make($namespace, $type_name, [], $is_nullable, Type::FROM_NODE);
    }

    /**
     * Converts the reflection type to a string that Phan can understand
     * @phan-side-effect-free
     */
    public static function stringFromReflectionType(
        ?\ReflectionType $reflection_type
    ): string {
        if (!$reflection_type) {
            return '';
        }
        if ($reflection_type instanceof \ReflectionNamedType) {
            $reflection_type_string = $reflection_type->getName();
            if ($reflection_type->allowsNull()) {
                if (\PHP_VERSION_ID >= 80000 && $reflection_type_string === 'mixed') {
                    return 'mixed';
                }
                return "?" . $reflection_type_string;
            }
            return $reflection_type_string;
        }
        // Unreachable in php 7.1-7.4, but reachable and revertsed deprecation in php 8.0+?
        return (string)$reflection_type;
    }

    /**
     * @param string $fully_qualified_string
     * A fully qualified type name
     *
     *
     * @return Type
     * The type with that fully qualified type name (cached for efficiency)
     *
     * @throws InvalidArgumentException if type name was invalid
     *
     * @throws FQSENException
     * @phan-side-effect-free
     */
    public static function fromFullyQualifiedString(
        string $fully_qualified_string
    ): Type {
        static $type_cache = [];
        return $type_cache[$fully_qualified_string] ?? ($type_cache[$fully_qualified_string] = self::fromFullyQualifiedStringInner($fully_qualified_string));
    }

    /**
     * Extracts the parts of this Type from the passed in fully qualified type name.
     * Callers should ensure that the type regex accepts $fully_qualified_string
     *
     * @throws InvalidArgumentException if namespace is missing from something that should have a namespace
     * @suppress PhanPossiblyFalseTypeArgument, PhanPossiblyFalseTypeArgumentInternal
     *
     * @throws FQSENException
     */
    protected static function fromFullyQualifiedStringInner(
        string $fully_qualified_string
    ): Type {
        if ($fully_qualified_string === '') {
            throw new InvalidArgumentException("Type cannot be empty");
        }
        while (\substr($fully_qualified_string, -1) === ')') {
            if ($fully_qualified_string[0] === '?') {
                $fully_qualified_string = '?' . \substr($fully_qualified_string, 2, -1);
            } else {
                $fully_qualified_string = \substr($fully_qualified_string, 1, -1);
            }
        }
        if (\substr($fully_qualified_string, -2) === '[]') {
            if ($fully_qualified_string[0] === '?') {
                $is_nullable = true;
                $fully_qualified_substring = \substr($fully_qualified_string, 1, -2);
            } else {
                $is_nullable = false;
                $fully_qualified_substring = \substr($fully_qualified_string, 0, -2);
            }
            return GenericArrayType::fromElementType(
                Type::fromFullyQualifiedString($fully_qualified_substring),
                $is_nullable,
                GenericArrayType::KEY_MIXED
            );
        }

        $tuple = self::typeStringComponents($fully_qualified_string);

        $namespace = $tuple->_0;
        $type_name = $tuple->_1;
        $template_parameter_type_name_list = $tuple->_2;
        $is_nullable = $tuple->_3;
        $shape_components = $tuple->_4;
        if (\preg_match('/^(' . self::noncapturing_literal_regex . ')$/D', $type_name)) {
            return self::fromEscapedLiteralScalar($type_name);
        }
        if (\is_array($shape_components)) {
            if (\strcasecmp($type_name, 'array') === 0) {
                return ArrayShapeType::fromFieldTypes(
                    self::shapeComponentStringsToTypes($shape_components, new Context(), Type::FROM_NODE),
                    $is_nullable
                );
            }
            if ($type_name === 'Closure' || $type_name === 'callable') {
                return self::fromFullyQualifiedFunctionLike($type_name === 'Closure', $shape_components, $is_nullable);
            }
        }

        // Map the names of the types to actual types in the
        // template parameter type list
        $template_parameter_type_list = self::createTemplateParameterTypeList($template_parameter_type_name_list);

        if (!$namespace) {
            if (count($template_parameter_type_name_list) > 0) {
                $type_name = \strtolower($type_name);
                switch ($type_name) {
                    case 'array':
                    case 'non-empty-array':
                        return self::parseGenericArrayTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, $type_name === 'non-empty-array', false);
                    case 'associative-array':
                    case 'non-empty-associative-array':
                        return self::parseGenericArrayTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, $type_name === 'non-empty-array', true);
                    case 'list':
                    case 'non-empty-list':
                        return self::parseListTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, $type_name === 'non-empty-list');
                    case 'iterable':
                        return self::parseGenericIterableTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable);
                }
            }
            return self::fromInternalTypeName(
                $fully_qualified_string,
                $is_nullable,
                Type::FROM_NODE,
                $template_parameter_type_list
            );
        }

        if (0 !== \strpos($namespace, '\\')) {
            $namespace = '\\' . $namespace;
        }

        if ($type_name === '') {
            throw new EmptyFQSENException("Type was not fully qualified", $fully_qualified_string);
        }
        if ($namespace === '') {
            throw new InvalidFQSENException("Type was not fully qualified", $fully_qualified_string);
        }

        return self::make(
            $namespace,
            $type_name,
            $template_parameter_type_list,
            $is_nullable,
            Type::FROM_NODE
        );
    }

    private static function fromEscapedLiteralScalar(string $escaped_literal): ScalarType
    {
        $is_nullable = $escaped_literal[0] === '?';
        if ($is_nullable) {
            $escaped_literal = \substr($escaped_literal, 1);
        }
        if ($escaped_literal[0] === "'") {
            // @phan-suppress-next-line PhanPossiblyFalseTypeArgument
            return LiteralStringType::fromEscapedString($escaped_literal, $is_nullable);
        }
        $value = \filter_var($escaped_literal, \FILTER_VALIDATE_INT);
        if (\is_int($value)) {
            return LiteralIntType::instanceForValue($value, $is_nullable);
        }
        $value = \filter_var($escaped_literal, \FILTER_VALIDATE_FLOAT);
        if (\is_float($value)) {
            return LiteralFloatType::instanceForValue($value, $is_nullable);
        }
        return FloatType::instance($is_nullable);
    }

    /**
     * @param list<string> $template_parameter_type_name_list
     * @return list<UnionType>
     */
    private static function createTemplateParameterTypeList(array $template_parameter_type_name_list): array
    {
        return \array_map(static function (string $type_name): UnionType {
            return UnionType::fromFullyQualifiedPHPDocString($type_name);
        }, $template_parameter_type_name_list);
    }

    /**
     * @param bool $is_closure_type
     * @param list<string> $shape_components
     * @param bool $is_nullable
     * @throws AssertionError if creating a closure/callable from the arguments failed
     * @suppress PhanPossiblyFalseTypeArgument, PhanPossiblyFalseTypeArgumentInternal
     */
    private static function fromFullyQualifiedFunctionLike(
        bool $is_closure_type,
        array $shape_components,
        bool $is_nullable
    ): FunctionLikeDeclarationType {
        if (!$shape_components) {
            // The literal int '0' is a valid union type, but it's falsey, so check the count instead.
            // shouldn't happen
            throw new AssertionError("Expected at least one component of a closure phpdoc type");
        }
        $return_type = \array_pop($shape_components);
        if ($return_type[0] === '(' && \substr($return_type, -1) === ')') {
            // TODO: Maybe catch that in UnionType parsing instead
            $return_type = \substr($return_type, 1, -1);
        }
        $params = self::closureParamComponentStringsToParams($shape_components, new Context(), Type::FROM_NODE);
        $return_type = UnionType::fromStringInContext($return_type, new Context(), Type::FROM_NODE);

        if ($is_closure_type) {
            return new ClosureDeclarationType(new Context(), $params, $return_type, false, $is_nullable);
        } else {
            return new CallableDeclarationType(new Context(), $params, $return_type, false, $is_nullable);
        }
    }
    /**
     * @param list<UnionType> $template_parameter_type_list
     * @param bool $is_nullable
     */
    private static function parseGenericArrayTypeFromTemplateParameterList(
        array $template_parameter_type_list,
        bool $is_nullable,
        bool $always_has_elements,
        bool $is_associative
    ): ArrayType {
        $make = static function (Type $element_type, int $key_type) use ($is_nullable, $always_has_elements, $is_associative): ArrayType {
            if ($always_has_elements) {
                if ($is_associative) {
                    return NonEmptyAssociativeArrayType::fromElementType($element_type, $is_nullable, $key_type);
                }
                return NonEmptyGenericArrayType::fromElementType($element_type, $is_nullable, $key_type);
            }
            if ($is_associative) {
                return AssociativeArrayType::fromElementType($element_type, $is_nullable, $key_type);
            }
            return GenericArrayType::fromElementType($element_type, $is_nullable, $key_type);
        };
        $template_count = count($template_parameter_type_list);
        if ($template_count > 2) {
            if ($always_has_elements || $is_associative) {
                return $make(MixedType::instance(false), GenericArrayType::KEY_MIXED);
            }
            return ArrayType::instance($is_nullable);
        }
        // array<T> or array<key, T>
        // TODO support associative-array?
        $types = $template_parameter_type_list[$template_count - 1]->getTypeSet();
        if ($template_count === 2) {
            if (count($types) === 1 && $template_parameter_type_list[0]->hasTemplateType()) {
                return GenericArrayTemplateKeyType::fromTemplateAndElementType(
                    // @phan-suppress-next-line PhanPossiblyFalseTypeArgument
                    \reset($types),
                    $is_nullable,
                    $template_parameter_type_list[0]
                );
            }
            $key_type = GenericArrayType::keyTypeFromUnionTypeValues($template_parameter_type_list[0]);
        } else {
            $key_type = GenericArrayType::KEY_MIXED;
        }

        // TODO: Infer non-empty-array<int,Type> from conditions such as count($types) == 1
        if (count($types) === 1) {
            // @phan-suppress-next-line PhanPossiblyFalseTypeArgument
            return $make(\reset($types), $key_type);
        } elseif (count($types) > 1) {
            return new GenericMultiArrayType(
                $types,
                $is_nullable,
                $key_type,
                $always_has_elements,
                false,
                $is_associative
            );
        }
        if ($always_has_elements || $is_associative) {
            return $make(MixedType::instance(false), $key_type);
        }
        return ArrayType::instance($is_nullable);
    }

    /**
     * @param list<UnionType> $template_parameter_type_list
     * @param bool $is_nullable
     */
    private static function parseListTypeFromTemplateParameterList(
        array $template_parameter_type_list,
        bool $is_nullable,
        bool $always_has_elements
    ): ArrayType {
        $template_count = count($template_parameter_type_list);
        if ($template_count !== 1) {
            if ($always_has_elements) {
                return NonEmptyListType::fromElementType(
                    MixedType::instance(false),
                    $is_nullable
                );
            }
            return ListType::fromElementType(
                MixedType::instance(false),
                $is_nullable
            );
        }
        // array<T> or array<key, T>
        $types = $template_parameter_type_list[$template_count - 1]->getTypeSet();
        if (count($types) === 1) {
            if ($always_has_elements) {
                return NonEmptyListType::fromElementType(
                    // @phan-suppress-next-line PhanPossiblyFalseTypeArgument
                    \reset($types),
                    $is_nullable
                );
            }
            return ListType::fromElementType(
                // @phan-suppress-next-line PhanPossiblyFalseTypeArgument
                \reset($types),
                $is_nullable
            );
        } elseif (count($types) > 1) {
            return new GenericMultiArrayType(
                $types,
                $is_nullable,
                GenericArrayType::KEY_INT,
                $always_has_elements,
                true
            );
        }
        if ($always_has_elements) {
            return NonEmptyListType::fromElementType(
                MixedType::instance(false),
                $is_nullable
            );
        }
        return ListType::fromElementType(
            MixedType::instance(false),
            $is_nullable
        );
    }

    /**
     * @param list<UnionType> $template_parameter_type_list
     * @param bool $is_nullable
     */
    private static function parseGenericIterableTypeFromTemplateParameterList(
        array $template_parameter_type_list,
        bool $is_nullable
    ): Type {
        $template_count = count($template_parameter_type_list);
        if ($template_count <= 2) {  // iterable<T> or iterable<key, T>
            // TODO: Warn about unparseable type or throw if more arguments are seen?
            $key_union_type = ($template_count === 2)
                ? $template_parameter_type_list[0]
                : UnionType::empty();
            $value_union_type = $template_parameter_type_list[$template_count - 1];
            return GenericIterableType::fromKeyAndValueTypes($key_union_type, $value_union_type, $is_nullable);
        }
        return IterableType::instance($is_nullable);
    }

    /**
     * @param list<UnionType> $template_parameter_type_list
     * @param bool $is_nullable
     */
    private static function parseClassStringTypeFromTemplateParameterList(
        array $template_parameter_type_list,
        bool $is_nullable
    ): Type {
        $template_count = count($template_parameter_type_list);
        if ($template_count === 1) {
            return new ClassStringType(
                '\\',
                'class-string',
                $template_parameter_type_list,
                $is_nullable
            );
        }
        return ClassStringType::instance($is_nullable);
    }

    /**
     * @param string $string
     * A string representing a type
     *
     * @param Context $context
     * The context in which the type string was
     * found
     *
     * @param int $source
     * Type::FROM_NODE, Type::FROM_TYPE, or Type::FROM_PHPDOC
     *
     * @param ?CodeBase $code_base
     * May be provided to resolve 'parent' in the context
     * (e.g. if parsing complex phpdoc).
     * Unnecessary in most use cases.
     *
     * @return Type
     * Parse a type from the given string
     *
     * @suppress PhanPossiblyFalseTypeArgument, PhanPossiblyFalseTypeArgumentInternal, PhanThrowTypeAbsent, PhanThrowTypeAbsentForCall
     * @phan-side-effect-free
     */
    public static function fromStringInContext(
        string $string,
        Context $context,
        int $source,
        CodeBase $code_base = null
    ): Type {
        if ($string === '') {
            throw new EmptyFQSENException("Type cannot be empty", '');
        }
        while (\substr($string, -1) === ')') {
            if ($string[0] === '?') {
                if ($string[1] !== '(') {
                    // Account for the Closure(params...):return syntax
                    break;
                }
                $string = '?' . \substr($string, 2, -1);
            } else {
                if ($string[0] !== '(') {
                    break;
                }
                $string = \substr($string, 1, -1);
            }
        }

        if (\substr($string, -2) === '[]') {
            if ($string[0] === '?') {
                $is_nullable = true;
                $substring = \substr($string, 1, -2);
            } else {
                $is_nullable = false;
                $substring = \substr($string, 0, -2);
            }
            if ($substring === '') {
                return ArrayType::instance($is_nullable);
            }
            $types = UnionType::fromStringInContext(
                $substring,
                $context,
                $source,
                $code_base
            );

            $type_set = $types->getTypeSet();
            if (count($type_set) === 1) {
                return GenericArrayType::fromElementType(
                    \reset($type_set),
                    $is_nullable,
                    GenericArrayType::KEY_MIXED
                );
            } else {
                return new GenericMultiArrayType(
                    $type_set,
                    $is_nullable,
                    GenericArrayType::KEY_MIXED
                );
            }
        }

        // If our scope has a generic type identifier defined on it
        // that matches the type string, return that type.
        if ($source === Type::FROM_PHPDOC && $context->getScope()->hasTemplateType(ltrim($string, '?'))) {
            return $context->getScope()->getTemplateType(ltrim($string, '?'))->withIsNullable(substr($string, 0, 1) === '?');
        }

        // Extract the namespace, type and parameter type name list
        $tuple = self::typeStringComponents($string);

        $namespace = $tuple->_0;
        $type_name = $tuple->_1;
        $template_parameter_type_name_list = $tuple->_2;
        $is_nullable = $tuple->_3;
        $shape_components = $tuple->_4;


        if (\preg_match('/^(' . self::noncapturing_literal_regex . ')$/D', $type_name)) {
            return self::fromEscapedLiteralScalar($type_name);
        }

        if (\is_array($shape_components)) {
            if (\strcasecmp($type_name, 'array') === 0) {
                return ArrayShapeType::fromFieldTypes(
                    self::shapeComponentStringsToTypes($shape_components, $context, $source, $code_base),
                    $is_nullable
                );
            }
            if ($type_name === 'Closure' || $type_name === 'callable') {
                if ($type_name === 'Closure' && $code_base !== null) {
                    self::checkClosureString($code_base, $context, $string);
                }
                return self::fromFunctionLikeInContext($type_name === 'Closure', $shape_components, $context, $source, $is_nullable);
            }
        }

        // Map the names of the types to actual types in the
        // template parameter type list
        $template_parameter_type_list =
            \array_map(static function (string $type_name) use ($code_base, $context, $source): UnionType {
                return UnionType::fromStringInContext($type_name, $context, $source, $code_base);
            }, $template_parameter_type_name_list);

        // @var bool
        // True if this type name if of the form 'C[]'
        $is_generic_array_type =
            self::isGenericArrayString($type_name);

        // If this is a generic array type, get the name of
        // the type of each element
        $non_generic_array_type_name = $type_name;
        if ($is_generic_array_type
           && false !== ($pos = \strrpos($type_name, '[]'))
        ) {
            $non_generic_array_type_name =
                \substr($type_name, 0, $pos);
        }

        // Check to see if the type name is mapped via
        // a using clause.
        //
        // Gotta check this before checking for native types
        // because there are monsters out there that will
        // remap the names via things like `use \Foo\String`.
        $non_generic_partially_qualified_array_type_name =
            $non_generic_array_type_name;
        if ($namespace) {
            $non_generic_partially_qualified_array_type_name =
                $namespace . '\\' . $non_generic_partially_qualified_array_type_name;
        }

        if ($is_generic_array_type && false !== \strrpos($non_generic_array_type_name, '[]')) {
            return GenericArrayType::fromElementType(
                Type::fromStringInContext($non_generic_partially_qualified_array_type_name, $context, $source),
                $is_nullable,
                GenericArrayType::KEY_MIXED
            );
        }
        if (\substr($non_generic_partially_qualified_array_type_name, 0, 1) !== '\\' && $context->hasNamespaceMapFor(
            \ast\flags\USE_NORMAL,
            $non_generic_partially_qualified_array_type_name
        )) {
            $fqsen =
                $context->getNamespaceMapFor(
                    \ast\flags\USE_NORMAL,
                    $non_generic_partially_qualified_array_type_name
                );

            if ($is_generic_array_type) {
                return GenericArrayType::fromElementType(
                    Type::make(
                        $fqsen->getNamespace(),
                        $fqsen->getName(),
                        $template_parameter_type_list,
                        false,
                        $source
                    ),
                    $is_nullable,
                    GenericArrayType::KEY_MIXED
                );
            }

            return Type::make(
                $fqsen->getNamespace(),
                $fqsen->getName(),
                $template_parameter_type_list,
                $is_nullable,
                $source
            );
        }

        // If this was a fully qualified type, we're all
        // set
        if ($namespace && $namespace[0] === '\\') {
            return self::make(
                $namespace,
                $type_name,
                $template_parameter_type_list,
                $is_nullable,
                $source
            );
        }

        if (self::isInternalTypeString($type_name, $source)) {
            if (count($template_parameter_type_list) > 0) {
                switch (\strtolower($type_name)) {
                    case 'array':
                        return self::parseGenericArrayTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, false, false);
                    case 'associative-array':
                        return self::parseGenericArrayTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, false, true);
                    case 'non-empty-array':
                        return self::parseGenericArrayTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, true, false);
                    case 'non-empty-associative-array':
                        return self::parseGenericArrayTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, true, true);
                    case 'iterable':
                        return self::parseGenericIterableTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable);
                    case 'class-string':
                        return self::parseClassStringTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable);
                    case 'list':
                        return self::parseListTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, false);
                    case 'non-empty-list':
                        return self::parseListTypeFromTemplateParameterList($template_parameter_type_list, $is_nullable, true);
                }
                // TODO: Warn about unrecognized types.
            }
            return self::fromInternalTypeName($type_name, $is_nullable, $source, $template_parameter_type_list);
        }

        // Things like `self[]` or `$this[]`
        if ($is_generic_array_type
            && self::isSelfTypeString($non_generic_array_type_name)
            && $context->isInClassScope()
        ) {
            // The element type can be nullable.
            // Independently, the array of elements can also be nullable.
            if (stripos($non_generic_array_type_name, 'parent') !== false) {
                // Will throw if $code_base is null or there is no parent type
                $element_type = self::maybeFindParentType($non_generic_array_type_name[0] === '?', $context, $code_base);
            } else {
                // Equivalent to getClassFQSEN()->asType() but slightly faster (this is frequently used)
                // @phan-suppress-next-line PhanThrowTypeAbsentForCall
                $element_type = self::fromFullyQualifiedString(
                    $context->getClassFQSEN()->__toString()
                );
            }

            return GenericArrayType::fromElementType(
                $element_type,
                $is_nullable,
                GenericArrayType::KEY_MIXED
            );
        }

        // If this is a type referencing the current class
        // in scope such as 'self' or 'static', return that whether or not this is in a class
        // (but not NS\self which is an invalid class name)
        if (self::isSelfTypeString($type_name) && !$namespace) {
            if (stripos($type_name, 'parent') !== false) {
                // Will throw if $code_base is null or there is no parent type
                return self::maybeFindParentType($is_nullable, $context, $code_base);
            }
            if ($source === self::FROM_PHPDOC && $context->getScope()->isInTraitScope()) {
                return SelfType::instanceWithTemplateTypeList($is_nullable, $template_parameter_type_list);
            }
            if (!$context->isInClassScope()) {
                if (strcasecmp($type_name, 'static') === 0) {
                    return StaticType::instance($is_nullable);
                }
                return SelfType::instance($is_nullable);
            }
            // Equivalent to getClassFQSEN()->asType()->withIsNullable but slightly faster (this is frequently used)
            // @phan-suppress-next-line PhanThrowTypeAbsentForCall
            $fqsen = $context->getClassFQSEN();
            return self::make(
                $fqsen->getNamespace(),
                $fqsen->getName(),
                $template_parameter_type_list,
                $is_nullable,
                $source
            );
        }

        // Merge the current namespace with the given relative
        // namespace
        $context_namespace = $context->getNamespace();
        if ($context_namespace) {
            if ($namespace) {
                $namespace = \rtrim($context_namespace, '\\') . '\\' . $namespace;
            } else {
                $namespace = $context_namespace;
            }
        } else {
            $namespace = '\\' . $namespace;
        }

        // Attach the context's namespace to the type name
        return self::make(
            $namespace,
            $type_name,
            $template_parameter_type_list,
            $is_nullable,
            $source
        );
    }


    private static function checkClosureString(
        CodeBase $code_base,
        Context $context,
        string $string
    ): void {
        // Note: Because of the regex, the namespace should be either empty or '\\'
        if (preg_match('/^\??\\\\/', $string) > 0) {
            // This is fully qualified
            return;
        }
        // This check is probably redundant, we can't parse
        if ($context->hasNamespaceMapFor(
            \ast\flags\USE_NORMAL,
            'Closure'
        )) {
            $fqsen = $context->getNamespaceMapFor(
                \ast\flags\USE_NORMAL,
                'Closure'
            );
            $namespace = $fqsen->getNamespace();
        } else {
            $namespace = $context->getNamespace();
        }
        if (($namespace ?: '\\') !== '\\') {
            Issue::maybeEmit(
                $code_base,
                $context,
                Issue::CommentAmbiguousClosure,
                $context->getLineNumberStart(),
                $string,
                $namespace . '\\Closure'
            );
        }
    }
    /**
     * @throws IssueException (TODO: Catch, emit, and proceed?
     */
    private static function maybeFindParentType(bool $is_nullable, Context $context, CodeBase $code_base = null): Type
    {
        if ($code_base === null) {
            return NonNullMixedType::instance($is_nullable);
        }
        $parent_type = UnionTypeVisitor::findParentType($context, $code_base);
        if (!$parent_type) {
            return NonNullMixedType::instance($is_nullable);
        }

        return $parent_type->withIsNullable($is_nullable);
    }

    /**
     * @param bool $is_closure_type
     * @param list<string> $shape_components
     * @param Context $context
     * @param int $source
     * @param bool $is_nullable
     * @throws AssertionError if the components were somehow invalid
     * @suppress PhanPossiblyFalseTypeArgument
     */
    private static function fromFunctionLikeInContext(
        bool $is_closure_type,
        array $shape_components,
        Context $context,
        int $source,
        bool $is_nullable
    ): FunctionLikeDeclarationType {
        $return_type = \array_pop($shape_components);
        if (!StringUtil::isNonZeroLengthString($return_type)) {
            throw new AssertionError("Expected a return type");
        }
        if ($return_type[0] === '(' && \substr($return_type, -1) === ')') {
            $return_type = \substr($return_type, 1, -1);
        }
        $params = self::closureParamComponentStringsToParams($shape_components, $context, $source);
        $return = UnionType::fromStringInContext($return_type, $context, $source);
        if ($is_closure_type) {
            return new ClosureDeclarationType($context, $params, $return, false, $is_nullable);
        } else {
            return new CallableDeclarationType($context, $params, $return, false, $is_nullable);
        }
    }

    /**
     * @param array<string|int,string> $shape_components Maps field keys (integers or strings) to the corresponding type representations
     * @param Context $context
     * @param int $source
     * @param ?CodeBase $code_base for resolving 'parent'
     * @return array<string|int,UnionType> The types for the representations of types, in the given $context
     */
    private static function shapeComponentStringsToTypes(array $shape_components, Context $context, int $source, CodeBase $code_base = null): array
    {
        $result = [];
        foreach ($shape_components as $key => $component_string) {
            if (\is_string($key) && \strpos($key, '\\') !== false) {
                $key = ArrayShapeType::unescapeKey($key);
            }
            if (\is_string($key) && \substr($key, -1) === '?') {
                if (\substr($component_string, -1) === '=') {
                    $component_string = \substr($component_string, 0, -1);
                }
                $key = \substr($key, 0, -1);
                $result[$key] = UnionType::fromStringInContext($component_string, $context, $source, $code_base)->withIsPossiblyUndefined(true);
            } elseif (\substr($component_string, -1) === '=') {
                $component_string = \substr($component_string, 0, -1);
                $result[$key] = UnionType::fromStringInContext($component_string, $context, $source, $code_base)->withIsPossiblyUndefined(true);
            } else {
                $result[$key] = UnionType::fromStringInContext($component_string, $context, $source, $code_base);
            }
        }
        return $result;
    }

    /**
     * @param list<string> $param_components Maps field keys (integers or strings) to the corresponding type representations
     * @param Context $context
     * @param int $source
     * @return list<ClosureDeclarationParameter> The representations of parameters of the closure, in the given $context
     *
     * @see Comment::magicParamFromMagicMethodParamString() - This is similar but has minor differences, such as references
     * @suppress PhanAccessClassConstantInternal
     */
    private static function closureParamComponentStringsToParams(array $param_components, Context $context, int $source): array
    {
        $result = [];
        foreach ($param_components as $param_string) {
            if ($param_string === '') {
                // TODO: warn
                continue;
            }
            if (preg_match('/^(' . UnionType::union_type_regex . ')?\s*(&\s*)?(?:(\.\.\.)\s*)?(?:\$' . Builder::WORD_REGEX . ')?((?:\s*=.*)?)$/', $param_string, $param_match)) {
                // Note: a closure declaration can be by reference, unlike (at)method
                $union_type_string = $param_match[1] ?: 'mixed';
                $union_type = UnionType::fromStringInContext(
                    $union_type_string,
                    $context,
                    $source
                );
                $is_reference = $param_match[15] !== '';
                $is_variadic = $param_match[16] === '...';
                $default_str = $param_match[18];
                $has_default_value = $default_str !== '';
                if ($has_default_value) {
                    $default_value_repr = trim(explode('=', $default_str, 2)[1]);
                    if (strcasecmp($default_value_repr, 'null') === 0) {
                        $union_type = $union_type->nullableClone();
                    }
                }
                $var_name = $param_match[17];
                $result[] = new ClosureDeclarationParameter($union_type, $is_variadic, $is_reference, $has_default_value, $var_name);
            }  // TODO: Otherwise, warn
        }
        return $result;
    }

    /**
     * @var ?UnionType of [$this]
     */
    protected $singleton_union_type;

    /**
     * @var ?UnionType of [$this] with an identical real type set
     */
    protected $singleton_real_union_type;

    /**
     * @return UnionType
     * A UnionType representing this and only this type (from phpdoc or real types)
     * @see asRealUnionType() if you are certain this is the real type of the expression.
     * @suppress PhanAccessReadOnlyProperty
     */
    public function asPHPDocUnionType(): UnionType
    {
        // return new UnionType([$this]);
        // Memoize the set of types. The constructed UnionType object can be modified later, so it isn't memoized.
        return $this->singleton_union_type ?? ($this->singleton_union_type = new UnionType([$this], true, []));
    }

    /**
     * @return UnionType
     * A UnionType representing this and only this type
     * @suppress PhanAccessReadOnlyProperty
     */
    public function asRealUnionType(): UnionType
    {
        // return new UnionType([$this]);
        // Memoize the set of types. The constructed UnionType object can be modified later, so it isn't memoized.
        return $this->singleton_real_union_type ?? ($this->singleton_real_union_type = $this->computeRealUnionType());
    }

    private function computeRealUnionType(): UnionType
    {
        $type_set = [$this];
        return new UnionType($type_set, true, $type_set);
    }

    /**
     * @return FQSEN
     * A fully-qualified structural element name derived
     * from this type
     *
     * @see FullyQualifiedClassName::fromType() for a method that always returns FullyQualifiedClassName
     */
    public function asFQSEN(): FQSEN
    {
        // Note: some subclasses, such as CallableType, return different subtypes of FQSEN
        return FullyQualifiedClassName::fromType($this);
    }

    /**
     * @return string
     * The name associated with this type
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @return string
     * The namespace associated with this type
     */
    public function getNamespace(): string
    {
        return $this->namespace;
    }

    /**
     * Is this nullable?
     *
     * E.g. returns true for `?array`, `null`, `mixed`, etc.
     */
    public function isNullable(): bool
    {
        return $this->is_nullable;
    }

    /**
     * Is this nullable in a way that Phan would emit warnings about nullable?
     *
     * E.g. returns true for `?array`, `null`, `?mixed` (but not `mixed`), etc.
     *
     * Currently, the only difference between this and isNullable() is for `?mixed` vs `mixed`
     */
    public function isNullableLabeled(): bool
    {
        return $this->is_nullable;
    }

    /**
     * Returns true if this has some possibly falsey values
     */
    public function isPossiblyFalsey(): bool
    {
        return $this->is_nullable;
    }

    /**
     * Returns true if this is guaranteed to be falsey
     */
    public function isAlwaysFalsey(): bool
    {
        return false;  // overridden in FalseType and NullType, as well as literal scalar types
    }

    /**
     * Returns true if this is possibly truthy.
     */
    public function isPossiblyTruthy(): bool
    {
        return true;  // overridden in various types. This base class (Type) is implicitly the type of an object, which is always truthy.
    }

    /**
     * Returns true if this is guaranteed to be truthy.
     *
     * Overridden in various types.
     *
     * This base class (Type) is type of an object with a known FQSEN,
     * which is always truthy.
     */
    public function isAlwaysTruthy(): bool
    {
        return !$this->is_nullable;
    }

    /**
     * Returns true for types such as `mixed`, `bool`, `false`
     */
    public function isPossiblyFalse(): bool
    {
        return false;
    }

    /**
     * Returns true for non-nullable `FalseType`
     */
    public function isAlwaysFalse(): bool
    {
        return false;  // overridden in FalseType
    }

    /**
     * Returns true if this could include the type `true`
     * (e.g. for `mixed`, `bool`, etc.)
     */
    public function isPossiblyTrue(): bool
    {
        return false;
    }

    /**
     * Returns true for non-nullable `TrueType`
     */
    public function isAlwaysTrue(): bool
    {
        return false;
    }

    /**
     * Returns true for FalseType, TrueType, and BoolType
     */
    public function isInBoolFamily(): bool
    {
        return false;
    }

    /**
     * Returns true if this type may satisfy `is_numeric()`
     */
    public function isPossiblyNumeric(): bool
    {
        return false;
    }

    /**
     * @param bool $is_nullable
     * Set to true if the type should be nullable, else pass
     * false
     *
     * @return Type
     * A new type that is a copy of this type but with the
     * given nullability value.
     *
     * @suppress PhanThrowTypeAbsentForCall should not happen provided a valid type
     */
    public function withIsNullable(bool $is_nullable): Type
    {
        if ($is_nullable === $this->is_nullable) {
            return $this;
        }
        return static::make(
            $this->namespace,
            $this->name,
            $this->template_parameter_type_list,
            $is_nullable,
            Type::FROM_TYPE
        );
    }

    /**
     * Returns this type with any falsey types (e.g. false, null, 0, '') removed.
     *
     * Overridden by BoolType, etc.
     * @see self::isAlwaysFalsey()
     */
    public function asNonFalseyType(): Type
    {
        // Overridden by BoolType subclass to return TrueType
        return $this->withIsNullable(false);
    }

    /**
     * Returns this type with any truthy types removed.
     *
     * Overridden by BoolType, etc.
     * @see self::isAlwaysTruthy()
     */
    public function asNonTruthyType(): Type
    {
        // Overridden by ScalarType, BoolType, etc.
        return NullType::instance(false);
    }

    /**
     * Returns this type with the type `false` removed.
     *
     * Overridden by BoolType, etc.
     * @see self::isAlwaysFalse()
     */
    public function asNonFalseType(): Type
    {
        return $this;
    }

    /**
     * Returns this type with the type `true` removed.
     *
     * Overridden by BoolType, etc.
     * @see self::isAlwaysTrue()
     */
    public function asNonTrueType(): Type
    {
        return $this;
    }

    /**
     * @return bool
     * True if this is a native type (like int, string, etc.)
     */
    public function isNativeType(): bool
    {
        return false;
    }

    /**
     * @return bool
     * True if this is a native type or an array of native types
     * (like int, string, bool[], etc.),
     */
    private static function isInternalTypeString(string $original_type_name, int $source): bool
    {
        $type_name = \str_replace('[]', '', strtolower($original_type_name));
        if ($source === Type::FROM_PHPDOC) {
            $type_name = self::canonicalNameFromName($type_name);  // Have to convert boolean[] to bool
        }
        if (!\array_key_exists($type_name, self::_internal_type_set)) {
            return $original_type_name === '$this';  // This is the only case-sensitive check.
        }
        // All values of $type_name exist as a valid phpdoc type, but some don't exist as real types.
        if ($source === Type::FROM_NODE && \array_key_exists($type_name, self::_soft_internal_type_set)) {
            return false;
        }
        return true;
    }

    /**
     * @return bool
     * True if this type is a type referencing the
     * class context in which it exists such as 'static'
     * or 'self'.
     */
    public function isSelfType(): bool
    {
        // TODO: Ensure that this is always a SelfType instance
        return $this->namespace === '\\' && self::isSelfTypeString($this->name);
    }

    /**
     * @return bool
     * True if this type is a type referencing the
     * class context 'static'.
     * Overridden in the subclass StaticType
     */
    public function isStaticType(): bool
    {
        return false;
    }

    /**
     * Returns true if this has any instance of `static` or `self`.
     * This is overridden in subclasses such as `SelfType` and `IterableType`
     * @unused-param $code_base
     */
    public function hasStaticOrSelfTypesRecursive(CodeBase $code_base): bool
    {
        // TODO: Check template types?
        return false;
    }

    /**
     * @param string $type_string
     * A string defining a type such as 'self' or 'int'.
     *
     * @return bool
     * True if the given type references the class context
     * in which it exists such as 'self' or 'parent'
     *
     * @phan-side-effect-free
     */
    public static function isSelfTypeString(
        string $type_string
    ): bool {
        // Note: While 'self' and 'parent' are case-insensitive, '$this' is case-sensitive
        // Not sure if that should extend to phpdoc.
        return \preg_match('/^\\\\?([sS][eE][lL][fF]|[pP][aA][rR][eE][nN][tT]|\$this)$/D', $type_string) > 0;
    }

    /**
     * @param string $type_string
     * A string defining a type such as 'static' or 'int'.
     *
     * @return bool
     * True if the given type references the class context
     * in which it exists is '$this' or 'static'
     *
     * @phan-side-effect-free
     */
    public static function isStaticTypeString(
        string $type_string
    ): bool {
        // Note: While 'self' and 'parent' are case-insensitive, '$this' is case-sensitive
        // Not sure if that should extend to phpdoc.
        return \preg_match('/^\\\\?([sS][tT][aA][tT][iI][cC]|\\$this)$/D', $type_string) > 0;
    }

    /**
     * @return bool
     * True if this type is scalar.
     */
    public function isScalar(): bool
    {
        return false;  // Overridden in subclass ScalarType
    }

    /**
     * @return bool
     * True if this type is a printable scalar.
     * @internal
     */
    public function isPrintableScalar(): bool
    {
        return false;  // Overridden in subclass ScalarType
    }

    /**
     * @return bool
     * True if this type is a valid operand for a bitwise operator ('|', '&', or '^').
     * @internal
     */
    public function isValidBitwiseOperand(): bool
    {
        return false;  // Overridden in subclasses
    }

    /**
     * @return bool
     * True if this type is a callable or a Closure.
     */
    public function isCallable(): bool
    {
        return false;  // Overridden in subclass CallableType, ClosureType, FunctionLikeDeclarationType
    }

    /**
     * @return bool
     * True if this type is an object (or the phpdoc `object`)
     */
    public function isObject(): bool
    {
        return true;  // Overridden in various subclasses
    }

    /**
     * Returns this type (or a subtype) converted to a type of an expression satisfying is_object(expr)
     * Returns null if Phan cannot cast this type to an object type.
     */
    public function asObjectType(): ?Type
    {
        return $this->withIsNullable(false);
    }


    /**
     * @return bool
     * True if this type is an object (and not the phpdoc `object` or a template)
     */
    public function isObjectWithKnownFQSEN(): bool
    {
        return true;  // Overridden in various subclasses
    }

    /**
     * @return bool
     * True if this type is possibly an object (or the phpdoc `object`)
     * This is the same as isObject(), except that it returns true for the exact class of IterableType.
     */
    public function isPossiblyObject(): bool
    {
        return true;  // Overridden in various subclasses
    }

    /**
     * Check if this type can possibly cast to the declared type, ignoring nullability of this type
     *
     * Precondition: This is either non-nullable or the type NullType/VoidType
     * @unused-param $context
     */
    public function canCastToDeclaredType(CodeBase $code_base, Context $context, Type $other): bool
    {
        if ($other->isPossiblyObject() && $this->canPossiblyCastToClass($code_base, $other->withIsNullable(false))) {
            return true;
        }
        return $this->canCastToType($other);
    }
    /**
     * Check if there is any way this type or a subclass could cast to $other.
     * (does not check for mixed)
     */
    public function canPossiblyCastToClass(CodeBase $code_base, Type $other): bool
    {
        if (!$this->isPossiblyObject()) {
            return false;
        }
        // Check if either side is something we don't know about, e.g. `object`, `iterable`, etc.
        if (!$this->isObjectWithKnownFQSEN()) {
            return true;
        }
        if (!$other->isObjectWithKnownFQSEN()) {
            return true;
        }

        if ($other->asExpandedTypes($code_base)->hasType($this) || $this->asExpandedTypes($code_base)->hasType($other)) {
            // This is a subtype of $other, or vice-versa
            return true;
        }
        $this_fqsen = FullyQualifiedClassName::fromType($this);
        if (!$code_base->hasClassWithFQSEN($this_fqsen)) {
            return true;
        }
        $this_class = $code_base->getClassByFQSEN($this_fqsen);

        $other_fqsen = FullyQualifiedClassName::fromType($other);
        if (!$code_base->hasClassWithFQSEN($other_fqsen)) {
            return true;
        }
        $other_class = $code_base->getClassByFQSEN($other_fqsen);

        if ($this_class->isFinal() || $other_class->isFinal()) {
            // If at least one class is final (and the other is a trait/interface), we already confirmed there's nothing in common.
            return false;
        }
        if (($this_class->isClass() || $this->isThrowableInterface()) &&
            ($other_class->isClass() || $other->isThrowableInterface())) {
            // So now we have two classes or interfaces that are only implemented by a sealed set of classes.
            // We already know that their expanded types don't overlap from checking asExpandedTypes, so there's no possible common subtype.
            return false;
        }

        return true;
    }

    /**
     * @return bool
     * True if this type is iterable. Does not check ancestor types.
     */
    public function isIterable(): bool
    {
        return false;  // Overridden in subclass IterableType (with subclass ArrayType)
    }

    /**
     * Convert this to a subtype that satisfies is_iterable(), or returns null
     * @see UnionType::iterableTypesStrictCast
     */
    public function asIterable(CodeBase $code_base): ?Type
    {
        if ($this->asExpandedTypes($code_base)->hasIterable()) {
            return $this->withIsNullable(false);
        }
        return null;
    }

    /**
     * @return bool
     * True if this type is array-like (is of type array, is
     * a generic array, or implements ArrayAccess).
     */
    public function isArrayLike(): bool
    {
        // includes both nullable and non-nullable ArrayAccess/array
        // (Overridden by ArrayType)
        return $this->isArrayAccess();
    }

    /**
     * @return bool
     * True if this is a generic type such as 'int[]' or 'string[]'.
     * Currently, this is the same as `$type instanceof GenericArrayInterface`
     * @suppress PhanUnreferencedPublicMethod
     */
    public function isGenericArray(): bool
    {
        return false;  // Overridden in GenericArrayType and ArrayShapeType
    }

    /**
     * @return bool - Returns true if this is `\ArrayAccess` (nullable or not)
     */
    public function isArrayAccess(): bool
    {
        return (\strcasecmp($this->getName(), 'ArrayAccess') === 0
            && $this->getNamespace() === '\\');
    }

    /**
     * Returns true if is_countable() is satisfied for the non-null version of this type.
     * e.g. returns true for `?ArrayObject`, `int[]`, '\Countable', and `array`.
     * Returns false for `iterable`, `mixed`, `\BaseClass`, etc.
     */
    public function isCountable(CodeBase $code_base): bool
    {
        if (!$this->isObjectWithKnownFQSEN()) {
            return false;
        }
        foreach ($this->asExpandedTypes($code_base)->getTypeSet() as $type) {
            if ($type->name === 'Countable' && $type->namespace === '\\') {
                return true;
            }
        }
        return false;
    }

    /**
     * Is this an array or ArrayAccess, or a subtype of those?
     * E.g. returns true for `\ArrayObject`, `array<int,string>`, etc.
     */
    public function isArrayOrArrayAccessSubType(CodeBase $code_base): bool
    {
        return $this->asExpandedTypes($code_base)->hasArrayAccess();
    }

    /**
     * @return bool - Returns true if this is \Traversable (nullable or not)
     */
    public function isTraversable(): bool
    {
        return (\strcasecmp($this->getName(), 'Traversable') === 0
            && $this->getNamespace() === '\\');
    }

    /**
     * @return bool - Returns true if this is \Generator (nullable or not)
     * @suppress PhanUnreferencedPublicMethod
     */
    public function isGenerator(): bool
    {
        return (\strcasecmp($this->getName(), 'Generator') === 0
            && $this->getNamespace() === '\\');
    }

    /**
     * @param string $type_name
     * A non-namespaced type name like 'int[]'
     *
     * @return bool
     * True if this is a generic type such as 'int[]' or
     * 'string[]'.
     */
    private static function isGenericArrayString(string $type_name): bool
    {
        if (\strrpos($type_name, '[]') !== false) {
            return $type_name !== '[]';
        }
        return false;
    }

    /**
     * @return ?UnionType returns the iterable key's union type, if this is a subtype of iterable. null otherwise.
     */
    public function iterableKeyUnionType(CodeBase $code_base): ?UnionType
    {
        if ($this->namespace === '\\') {
            $name = strtolower($this->name);
            if ($name === 'traversable' || $name === 'iterator') {
                return $this->keyTypeOfTraversable();
            }
            // TODO: Abstract this out for all internal classes
            if ($name === 'generator') {
                return $this->keyTypeOfGenerator();
            }
            // TODO: If this is a subclass of iterator, look up the signature of MyClass->key()
        }

        $fqsen = FullyQualifiedClassName::fromType($this);
        if (!$code_base->hasClassWithFQSEN($fqsen)) {
            return null;
        }
        static $iterator_fqsen;
        static $iterator_aggregate_fqsen;
        if ($iterator_fqsen === null) {
            // @phan-suppress-next-line PhanThrowTypeAbsentForCall
            $iterator_fqsen = Type::fromFullyQualifiedString('\Iterator');
            // @phan-suppress-next-line PhanThrowTypeAbsentForCall
            $iterator_aggregate_fqsen = Type::fromFullyQualifiedString('\IteratorAggregate');
        }
        $expanded_types = $this->asExpandedTypes($code_base);
        $iterator_type = $this;
        // Given an IteratorAggregate implementation with getIterator, determine the class of the Iterator if possible.
        if ($expanded_types->hasTypeWithFQSEN($iterator_aggregate_fqsen)) {
            $class = $code_base->getClassByFQSEN($fqsen);
            if (!$class->hasMethodWithName($code_base, 'getIterator', true)) {
                // Should be impossible
                return null;
            }
            // Find the class of the iterator
            $method = $class->getMethodByName($code_base, 'getIterator');
            $new_expanded_types = null;
            foreach ($method->getUnionType()->getTypeSet() as $iterator_type) {
                if ($iterator_type->isObjectWithKnownFQSEN()) {
                    $new_fqsen = FullyQualifiedClassName::fromType($iterator_type);
                    if (!$code_base->hasClassWithFQSEN($new_fqsen)) {
                        continue;
                    }
                    $new_expanded_types = $iterator_type->asExpandedTypes($code_base);
                    $fqsen = $new_fqsen;
                    break;
                }
            }
            if (!$new_expanded_types) {
                return null;
            }
            $expanded_types = $new_expanded_types;
        }
        // Given an Iterator, return the type of the key
        if ($expanded_types->hasTypeWithFQSEN($iterator_fqsen)) {
            $class = $code_base->getClassByFQSEN($fqsen);
            if (!$class->hasMethodWithName($code_base, 'key', true)) {
                // Should be impossible
                return null;
            }
            $method = $class->getMethodByName($code_base, 'key');
            $result = $method->getUnionType();
            if ($result->hasTemplateTypeRecursive()) {
                $result = $result->withTemplateParameterTypeMap(
                    $iterator_type->getTemplateParameterTypeMap($code_base)
                )->withoutTemplateTypeRecursive();
            }
            return $result;
        }
        return StringType::instance(false)->asPHPDocUnionType();
    }

    /**
     * @return ?UnionType returns the iterable value's union type if this is a subtype of iterable, null otherwise.
     *
     * This is overridden by the array subclasses
     */
    public function iterableValueUnionType(CodeBase $code_base): ?UnionType
    {
        if ($this->namespace === '\\') {
            $name = strtolower($this->name);
            if ($name === 'traversable' || $name === 'iterator') {
                return $this->valueTypeOfTraversable();
            }
            // TODO: Abstract this out for all internal classes
            if ($name === 'generator') {
                return $this->valueTypeOfGenerator();
            }
            // TODO: If this is a subclass of iterator, look up the signature of MyClass->current()
        }
        $fqsen = FullyQualifiedClassName::fromType($this);
        if (!$code_base->hasClassWithFQSEN($fqsen)) {
            return null;
        }
        static $iterator_fqsen;
        static $iterator_aggregate_fqsen;
        if ($iterator_fqsen === null) {
            // @phan-suppress-next-line PhanThrowTypeAbsentForCall
            $iterator_fqsen = Type::fromFullyQualifiedString('\Iterator');
            // @phan-suppress-next-line PhanThrowTypeAbsentForCall
            $iterator_aggregate_fqsen = Type::fromFullyQualifiedString('\IteratorAggregate');
        }
        $expanded_types = $this->asExpandedTypes($code_base);
        $iterator_type = $this;
        // Given an IteratorAggregate implementation with getIterator, determine the class of the Iterator if possible.
        if ($expanded_types->hasTypeWithFQSEN($iterator_aggregate_fqsen)) {
            $class = $code_base->getClassByFQSEN($fqsen);
            if (!$class->hasMethodWithName($code_base, 'getIterator', true)) {
                // Should be impossible
                return null;
            }
            // Find the class of the iterator
            $method = $class->getMethodByName($code_base, 'getIterator');
            $new_expanded_types = null;
            foreach ($method->getUnionType()->getTypeSet() as $iterator_type) {
                if ($iterator_type->isObjectWithKnownFQSEN()) {
                    $new_fqsen = FullyQualifiedClassName::fromType($iterator_type);
                    if (!$code_base->hasClassWithFQSEN($new_fqsen)) {
                        continue;
                    }
                    $new_expanded_types = $iterator_type->asExpandedTypes($code_base);
                    $fqsen = $new_fqsen;
                    break;
                }
            }
            if (!$new_expanded_types) {
                return null;
            }
            $expanded_types = $new_expanded_types;
        }
        // Given an Iterator, return the type of the value (from ->current())
        if ($expanded_types->hasTypeWithFQSEN($iterator_fqsen)) {
            $class = $code_base->getClassByFQSEN($fqsen);
            if (!$class->hasMethodWithName($code_base, 'current', true)) {
                // Should be impossible
                return null;
            }
            $method = $class->getMethodByName($code_base, 'current');
            $result = $method->getUnionType();
            if ($result->hasTemplateTypeRecursive()) {
                $result = $result->withTemplateParameterTypeMap(
                    $iterator_type->getTemplateParameterTypeMap($code_base)
                )->withoutTemplateTypeRecursive();
            }
            return $result;
        }
        return null;
    }

    // TODO: Use a template-based abstraction so that this boilerplate can be removed
    private function keyTypeOfTraversable(): ?UnionType
    {
        $template_type_list = $this->template_parameter_type_list;
        if (count($template_type_list) === 2) {
            return $template_type_list[0];
        }
        return null;
    }

    private function valueTypeOfTraversable(): ?UnionType
    {
        $template_type_list = $this->template_parameter_type_list;
        $count = count($template_type_list);
        if ($count >= 1 && $count <= 2) {
            return $template_type_list[$count - 1];
        }
        return null;
    }


    private function keyTypeOfGenerator(): ?UnionType
    {
        $template_type_list = $this->template_parameter_type_list;
        if (count($template_type_list) >= 2 && count($template_type_list) <= 4) {
            return $template_type_list[0];
        }
        return null;
    }

    private function valueTypeOfGenerator(): ?UnionType
    {
        $template_type_list = $this->template_parameter_type_list;
        if (count($template_type_list) >= 2 && count($template_type_list) <= 4) {
            return $template_type_list[1];
        } elseif (count($template_type_list) === 1) {
            return $template_type_list[0];
        }
        return null;
    }

    /**
     * @param int $key_type
     * Corresponds to the type of the array keys. Set this to a GenericArrayType::KEY_* constant.
     *
     * @return Type
     * Get a new type which is the generic array version of
     * this type. For instance, 'int' will produce 'int[]'.
     *
     * As a special case to reduce false positives, 'array' (with no known types) will produce 'array'
     *
     * Overridden in subclasses
     */
    public function asGenericArrayType(int $key_type): Type
    {
        return GenericArrayType::fromElementType($this, false, $key_type);
    }

    /**
     * @return bool
     * True if this type has any template parameter types
     * @suppress PhanUnreferencedPublicMethod potentially used in the future
     *           TODO: Would need to override this in ArrayShapeType, GenericArrayType
     */
    public function hasTemplateParameterTypes(): bool
    {
        return count($this->template_parameter_type_list) > 0;
    }

    /**
     * @return list<UnionType>
     * The set of types filling in template parameter types defined
     * on the class specified by this type.
     */
    public function getTemplateParameterTypeList(): array
    {
        return $this->template_parameter_type_list;
    }

    /**
     * @param CodeBase $code_base
     * The code base to look up classes against
     *
     * @return array<string,UnionType>
     * A map from template type identifier to a concrete type
     */
    public function getTemplateParameterTypeMap(CodeBase $code_base): array
    {
        return $this->memoize(__METHOD__, /** @return array<string,UnionType> */ function () use ($code_base): array {
            $fqsen = FullyQualifiedClassName::fromType($this);

            if (!$code_base->hasClassWithFQSEN($fqsen)) {
                return [];
            }

            $class = $code_base->getClassByFQSEN($fqsen);

            $template_parameter_type_list =
                $this->template_parameter_type_list;

            $map = [];
            foreach (\array_keys($class->getTemplateTypeMap()) as $i => $identifier) {
                if (isset($template_parameter_type_list[$i])) {
                    $map[$identifier] = $template_parameter_type_list[$i];
                }
            }

            return $map;
        });
    }

    /**
     * @param CodeBase $code_base
     * The code base to use in order to find super classes, etc.
     *
     * @param int $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 class types to all inherited classes returning
     * a superset of this type.
     *
     * TODO: Add equivalent to preserve the real type
     *
     * @suppress PhanPartialTypeMismatchReturn
     */
    public function asExpandedTypes(
        CodeBase $code_base,
        int $recursion_depth = 0
    ): UnionType {
        if (($this->memoized_data['current_progress_state'] ?? null) === self::$current_progress_state) {
            $memoized = $this->memoized_data['expanded_types'] ?? null;
            if (\is_object($memoized)) {
                return $memoized;
            }
        } else {
            $this->memoized_data = ['current_progress_state' => self::$current_progress_state];
        }
        // We're going to assume that if the type hierarchy
        // is taller than some value we probably messed up
        // and should bail out.
        if ($recursion_depth >= 20) {
            throw new RecursionDepthException("Recursion has gotten out of hand: " . Frame::getExpandedTypesDetails());
        }
        // @phan-suppress-next-line PhanAccessReadOnlyProperty
        return $this->memoized_data['expanded_types'] = $this->computeExpandedTypes($code_base, $recursion_depth);
    }

    private function computeExpandedTypes(CodeBase $code_base, int $recursion_depth): UnionType
    {
        $union_type = $this->asPHPDocUnionType();

        $class_fqsen = $this->asFQSEN();

        if (!($class_fqsen instanceof FullyQualifiedClassName)) {
            return $union_type;
        }

        if (!$code_base->hasClassWithFQSEN($class_fqsen)) {
            return $union_type;
        }
        // Guard against recursion
        // @phan-suppress-next-line PhanAccessReadOnlyProperty
        $this->memoized_data['expanded_types'] = $union_type;

        $clazz = $code_base->getClassByFQSEN($class_fqsen);

        $union_type = $union_type->withUnionType(
            $clazz->getUnionType()->withIsNullable($this->is_nullable)
        );
        $additional_union_type = $clazz->getAdditionalTypes();
        if ($additional_union_type !== null) {
            $union_type = $union_type->withUnionType($additional_union_type->withIsNullable($this->is_nullable));
        }

        // Recurse up the tree to include all types
        $representation = $this->__toString();
        $recursive_union_type_builder = new UnionTypeBuilder();
        foreach ($union_type->getTypeSet() as $clazz_type) {
            if ($clazz_type->__toString() !== $representation) {
                $recursive_union_type_builder->addUnionType(
                    $clazz_type->asExpandedTypes(
                        $code_base,
                        $recursion_depth + 1
                    )
                );
            } else {
                $recursive_union_type_builder->addType($clazz_type);
            }
        }
        if (count($this->template_parameter_type_list) > 0) {
            $recursive_union_type_builder->addUnionType(
                $clazz->resolveParentTemplateType($this->getTemplateParameterTypeMap($code_base))
            );
        }

        // Add in aliases
        // (If enable_class_alias_support is false, this will do nothing)
        $fqsen_aliases = $code_base->getClassAliasesByFQSEN($class_fqsen);
        foreach ($fqsen_aliases as $alias_fqsen_record) {
            $alias_fqsen = $alias_fqsen_record->alias_fqsen;
            $recursive_union_type_builder->addType(
                $alias_fqsen->asType()->withIsNullable($this->is_nullable)
            );
        }

        return $recursive_union_type_builder->getPHPDocUnionType();
    }

    /**
     * @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 class types to all inherited classes returning
     * a superset of this type.
     *
     * @suppress PhanPartialTypeMismatchReturn
     */
    public function asExpandedTypesPreservingTemplate(
        CodeBase $code_base,
        int $recursion_depth = 0
    ): UnionType {
        if (($this->memoized_data['current_progress_state'] ?? null) === self::$current_progress_state) {
            $memoized = $this->memoized_data['expanded_types_preserving_template'] ?? null;
            if (\is_object($memoized)) {
                return $memoized;
            }
        } else {
            $this->memoized_data = ['current_progress_state' => self::$current_progress_state];
        }
        // We're going to assume that if the type hierarchy
        // is taller than some value we probably messed up
        // and should bail out.
        if ($recursion_depth >= 20) {
            throw new RecursionDepthException("Recursion has gotten out of hand: " . Frame::getExpandedTypesDetails());
        }
        // @phan-suppress-next-line PhanAccessReadOnlyProperty
        return $this->memoized_data['expanded_types_preserving_template'] = $this->computeExpandedTypesPreservingTemplate($code_base, $recursion_depth);
    }

    private function computeExpandedTypesPreservingTemplate(CodeBase $code_base, int $recursion_depth): UnionType
    {
        $union_type = $this->asPHPDocUnionType();

        $class_fqsen = $this->asFQSEN();

        if (!($class_fqsen instanceof FullyQualifiedClassName)) {
            return $union_type;
        }

        if (!$code_base->hasClassWithFQSEN($class_fqsen)) {
            return $union_type;
        }
        // Guard against recursion such as `class X extends Y{} class Y extends X{}`.
        // @phan-suppress-next-line PhanAccessReadOnlyProperty
        $this->memoized_data['expanded_types_preserving_template'] = $union_type;

        $clazz = $code_base->getClassByFQSEN($class_fqsen);

        $union_type = $union_type->withUnionType(
            $clazz->getUnionType()->withIsNullable($this->is_nullable)
        );

        if (count($this->template_parameter_type_list) > 0) {
            $template_union_type = $clazz->resolveParentTemplateType($this->getTemplateParameterTypeMap($code_base))->asExpandedTypesPreservingTemplate($code_base, $recursion_depth + 1);
            $template_union_type = $template_union_type->withType($this);
        } else {
            $template_union_type = UnionType::empty();
        }

        $additional_union_type = $clazz->getAdditionalTypes();
        if ($additional_union_type !== null) {
            $union_type = $union_type->withUnionType($additional_union_type->withIsNullable($this->is_nullable));
        }

        $representation = $this->__toString();
        $recursive_union_type_builder = new UnionTypeBuilder();
        // Recurse up the tree to include all types
        if (count($this->template_parameter_type_list) > 0) {
            $recursive_union_type_builder->addUnionType(
                $template_union_type
            );
        }

        foreach ($union_type->getTypeSet() as $clazz_type) {
            if ($clazz_type->__toString() !== $representation) {
                $recursive_union_type_builder->addUnionType(
                    $clazz_type->asExpandedTypesPreservingTemplate(
                        $code_base,
                        $recursion_depth + 1
                    )
                );
            } else {
                $recursive_union_type_builder->addType($clazz_type);
            }
        }

        // Add in aliases
        // (If enable_class_alias_support is false, this will do nothing)
        $fqsen_aliases = $code_base->getClassAliasesByFQSEN($class_fqsen);
        foreach ($fqsen_aliases as $alias_fqsen_record) {
            $alias_fqsen = $alias_fqsen_record->alias_fqsen;
            $recursive_union_type_builder->addType(
                $alias_fqsen->asType()->withIsNullable($this->is_nullable)
            );
        }

        $result = $recursive_union_type_builder->getPHPDocUnionType();
        if (!$template_union_type->isEmpty()) {
            return $result->replaceWithTemplateTypes($template_union_type);
        }
        return $result;
    }

    /**
     * @param Type[] $target_type_set 1 or more types
     * @return bool
     * True if this Type can be cast to the given Type cleanly.
     * This is overridden by ArrayShapeType to allow array{a:string,b:stdClass} to cast to string[]|stdClass[]
     */
    public function canCastToAnyTypeInSet(array $target_type_set): bool
    {
        foreach ($target_type_set as $target_type) {
            if ($this->canCastToType($target_type)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param Type[] $target_type_set 1 or more types
     * @return bool
     * True if this Type can be cast to the given Type cleanly, ignoring permissive config settings.
     * This is overridden by ArrayShapeType to allow array{a:string,b:stdClass} to cast to string[]|stdClass[]
     */
    public function canCastToAnyTypeInSetWithoutConfig(array $target_type_set): bool
    {
        foreach ($target_type_set as $target_type) {
            if ($this->canCastToTypeWithoutConfig($target_type)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param Type[] $target_type_set 1 or more types
     * @return bool
     * True if this Type can be cast to the given set of types cleanly.
     * This is overridden by ArrayShapeType to allow array{a:string,b:stdClass} to cast to string[]|stdClass[]
     */
    public function isSubtypeOfAnyTypeInSet(array $target_type_set): bool
    {
        foreach ($target_type_set as $target_type) {
            if ($this->isSubtypeOf($target_type)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param Type[] $target_type_set 1 or more types
     * @return bool
     * True if this Type can be cast to the given set of Types cleanly (accounting for templates)
     * TODO: Override this in ArrayShapeType to allow array{a:string,b:stdClass} to cast to string[]|stdClass[]
     */
    public function canCastToAnyTypeInSetHandlingTemplates(array $target_type_set, CodeBase $code_base): bool
    {
        foreach ($target_type_set as $target_type) {
            if ($this->canCastToTypeHandlingTemplates($target_type, $code_base)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return bool
     * True if this Type can be cast to the given Type
     * cleanly
     */
    public function canCastToType(Type $type): bool
    {
        // Check to see if we have an exact object match
        if ($this === $type) {
            return true;
        }

        if ($type instanceof MixedType) {
            // This is not NullType; it has to be truthy to cast to non-empty-mixed.
            return \get_class($type) !== NonEmptyMixedType::class || $this->isPossiblyTruthy();
        }

        if ($this->is_nullable) {
            // A nullable type cannot cast to a non-nullable type (Except when null_casts_as_any_type is true)
            if (Config::get_null_casts_as_any_type()) {
                return true;
            } elseif (Config::get_null_casts_as_array() && $type->isArrayLike()) {
                return true;
            } elseif ($type->isScalar() && (
                    Config::getValue('scalar_implicit_cast') ||
                    in_array($type->getName(), Config::getValue('scalar_implicit_partial')['null'] ?? [], true))) {
                // e.g. allow casting ?string to string if scalar_implicit_cast or 'null' => ['string'] is in scalar_implicit_partial.
                return true;
            }

            if (!$type->isNullable()) {
                return false;
            }
        }

        // Get a non-null version of the type we're comparing
        // against.
        if ($type->is_nullable) {
            $type = $type->withIsNullable(false);

            // Check one more time to see if the types are equal
            if ($this === $type) {
                return true;
            }
        }

        // Test to see if we can cast to the non-nullable version
        // of the target type.
        return $this->canCastToNonNullableType($type);
    }

    /**
     * @return bool
     * True if this Type can be cast to the given Type
     * cleanly (accounting for templates)
     */
    public function canCastToTypeHandlingTemplates(Type $type, CodeBase $code_base): bool
    {
        // Check to see if we have an exact object match
        if ($this === $type) {
            return true;
        }

        if ($type instanceof MixedType) {
            // This is not NullType; it has to be truthy to cast to non-empty-mixed.
            return \get_class($type) !== NonEmptyMixedType::class || $this->isPossiblyTruthy();
        }

        // A nullable type cannot cast to a non-nullable type
        if ($this->is_nullable && !$type->is_nullable) {
            // If this is nullable, but that isn't, and we've
            // configured nulls to cast as anything (or as arrays), ignore
            // the nullable part.
            if (Config::get_null_casts_as_any_type()) {
                return $this->withIsNullable(false)->canCastToType($type);
            } elseif (Config::get_null_casts_as_array() && $type->isArrayLike()) {
                return $this->withIsNullable(false)->canCastToType($type);
            }

            return false;
        }

        // Get a non-null version of the type we're comparing
        // against.
        if ($type->is_nullable) {
            $type = $type->withIsNullable(false);

            // Check one more time to see if the types are equal
            if ($this === $type) {
                return true;
            }
        }

        // Test to see if we can cast to the non-nullable version
        // of the target type.
        return $this->canCastToNonNullableTypeHandlingTemplates($type, $code_base);
    }

    /**
     * @return bool
     * True if this Type can be cast to the given Type
     * cleanly without config settings.
     */
    public function canCastToTypeWithoutConfig(Type $type): bool
    {
        // Check to see if we have an exact object match
        if ($this === $type) {
            return true;
        }

        if ($type instanceof MixedType) {
            // This is not NullType; it has to be truthy to cast to non-empty-mixed.
            return \get_class($type) !== NonEmptyMixedType::class || $this->isPossiblyTruthy();
        }

        if ($this->is_nullable) {
            // A nullable type cannot cast to a non-nullable type.
            if (!$type->isNullable()) {
                return false;
            }
        }

        // Get a non-null version of the type we're comparing
        // against.
        if ($type->is_nullable) {
            $type = $type->withIsNullable(false);

            // Check one more time to see if the types are equal
            if ($this === $type) {
                return true;
            }
        }

        // Test to see if we can cast to the non-nullable version
        // of the target type.
        return $this->canCastToNonNullableTypeWithoutConfig($type);
    }

    /**
     * @param Type $type
     * A Type which is not nullable. This constraint is not
     * enforced, so be careful.
     *
     * @return bool
     * True if this not nullable Type can be cast to the given Type
     * cleanly
     */
    protected function canCastToNonNullableType(Type $type): bool
    {
        // can't cast native types (includes iterable or array) to object. ObjectType overrides this function.
        if ($type instanceof ObjectType
            && !$this->isNativeType()
        ) {
            return true;
        }

        if (!($type instanceof NativeType)) {
            return false;
        }

        if ($type instanceof MixedType) {
            // This is not NullType; it has to be truthy to cast to non-empty-mixed.
            return \get_class($type) !== NonEmptyMixedType::class || $this->isPossiblyTruthy();
        }

        // Check for allowable type conversions from object types to native types
        if ($type::NAME === 'iterable') {
            if ($this->namespace === '\\' && in_array($this->name, ['Generator', 'Traversable', 'Iterator'], true)) {
                if (count($this->template_parameter_type_list) === 0 || !($type instanceof GenericIterableType)) {
                    return true;
                }
                return $this->canCastTraversableToIterable($type);
            }
        } elseif (\get_class($type) === CallableType::class) {
            return $this->namespace === '\\' && $this->name === 'Closure';
        }
        return false;
    }

    /**
     * @param Type $type
     * A Type which is not nullable. This constraint is not
     * enforced, so be careful.
     *
     * @return bool
     * True if this Type can be cast to the given Type
     * cleanly, ignoring permissive config casting rules
     */
    protected function canCastToNonNullableTypeWithoutConfig(Type $type): bool
    {
        // can't cast native types (includes iterable or array) to object. ObjectType overrides this function.
        if ($type instanceof ObjectType
            && !$this->isNativeType()
        ) {
            return true;
        }

        if (!($type instanceof NativeType)) {
            return false;
        }

        if ($type instanceof MixedType) {
            // This is not NullType; it has to be truthy to cast to non-empty-mixed.
            return \get_class($type) !== NonEmptyMixedType::class || $this->isPossiblyTruthy();
        }

        // Check for allowable type conversions from object types to native types
        if ($type::NAME === 'iterable') {
            if ($this->namespace === '\\' && in_array($this->name, ['Generator', 'Traversable', 'Iterator'], true)) {
                if (count($this->template_parameter_type_list) === 0 || !($type instanceof GenericIterableType)) {
                    return true;
                }
                return $this->canCastTraversableToIterable($type);
            }
        } elseif (\get_class($type) === CallableType::class) {
            return $this->namespace === '\\' && $this->name === 'Closure';
        }
        return false;
    }

    /**
     * @param Type $type
     * A Type which is not nullable. This constraint is not
     * enforced, so be careful.
     *
     * @return bool
     * True if this Type can be cast to the given Type
     * cleanly, accounting for template types.
     */
    protected function canCastToNonNullableTypeHandlingTemplates(Type $type, CodeBase $code_base): bool
    {
        if ($this->canCastToNonNullableType($type)) {
            return true;
        }
        if ($this->isObjectWithKnownFQSEN() && $type->isObjectWithKnownFQSEN()) {
            if ($this->name === $type->name && $this->namespace === $type->namespace) {
                return $this->canTemplateTypesCast($type->template_parameter_type_list, $code_base);
            }
        }
        return false;
    }

    /**
     * @return bool
     * True if this Type is a subtype of the other type.
     */
    public function isSubtypeOf(Type $type): bool
    {
        // Check to see if we have an exact object match
        if ($this === $type) {
            return true;
        }

        $other_is_nullable = $type->isNullable();
        // A nullable type is not a subtype of a non-nullable type
        if ($this->isNullable() && !$other_is_nullable) {
            return false;
        }

        if ($type instanceof MixedType) {
            // e.g. ?int is a subtype of mixed, but ?int is not a subtype of non-empty-mixed/non-null-mixed
            // (check isNullable first)
            // This is not NullType; it has to be truthy to cast to non-empty-mixed.
            return \get_class($type) !== NonEmptyMixedType::class || $this->isPossiblyTruthy();
        }

        // Get a non-null version of the type we're comparing
        // against.
        if ($other_is_nullable) {
            $type = $type->withIsNullable(false);

            // Check one more time to see if the types are equal
            if ($this === $type) {
                return true;
            }
        }

        // Test to see if we are a subtype of the non-nullable version
        // of the target type.
        return $this->isSubtypeOfNonNullableType($type);
    }

    /**
     * Returns true if this can cast to the non-nullable version of the target type
     *
     * This is overridden in subclasses such as MixedType.
     * (All types are sub-types of mixed, but mixed isn't a subtype of those types)
     *
     * TODO: Override everywhere else
     */
    protected function isSubtypeOfNonNullableType(Type $type): bool
    {
        return $this->canCastToNonNullableType($type);
    }

    /**
     * @param list<UnionType> $other_template_parameter_type_list
     */
    private function canTemplateTypesCast(array $other_template_parameter_type_list, CodeBase $code_base): bool
    {
        foreach ($this->template_parameter_type_list as $i => $param) {
            $other_param = $other_template_parameter_type_list[$i] ?? null;
            if ($other_param !== null) {
                if (!$param->asExpandedTypes($code_base)->canCastToUnionType($other_param)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Precondition: $this represents \Traversable, \Iterator, or \Generator
     */
    private function canCastTraversableToIterable(GenericIterableType $type): bool
    {
        $template_types = $this->template_parameter_type_list;
        $count = count($template_types);
        $name = $this->name;
        if ($name === 'Traversable' || $name === 'Iterator') {
            // Phan supports Traversable<TValue> and Traversable<TKey, TValue>
            if ($count > 2 || $count < 1) {
                // No idea what this means, assume it passes.
                return true;
            }
            if (!$this->template_parameter_type_list[$count - 1]->canCastToUnionType($type->getElementUnionType())) {
                return false;
            }
            if ($count === 2) {
                if (!$this->template_parameter_type_list[0]->canCastToUnionType($type->getKeyUnionType())) {
                    return false;
                }
            }
            return true;
        } elseif ($name === 'Generator') {
            // Phan partially supports the following syntaxes for PHP doc comments
            // 1. Generator<TValue>
            // 2. Generator<TKey, TValue>
            // 3. Generator<TKey, TValue, TYield>
            // 4. Generator<TKey, TValue, TYield, TReturn> (PHP generators can return a final value, but HHVM cannot)

            // TODO: Handle casting Generator to a Generator with a different number of template parameters
            if ($count > 4 || $count < 1) {
                // No idea what this means, assume it passes
                return true;
            }

            if (!$this->template_parameter_type_list[\min(1, $count - 1)]->canCastToUnionType($type->getElementUnionType())) {
                return false;
            }
            if ($count >= 2) {
                if (!$this->template_parameter_type_list[0]->canCastToUnionType($type->getKeyUnionType())) {
                    return false;
                }
            }
            return true;
        }
        // TODO: Check for template parameters, cast those
        return true;
    }

    /**
     * @param UnionType $union_type
     * A union type to compare against. Resolve it before checking.
     *
     * @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.
     *
     * @see StaticType::isExclusivelyNarrowedFormOrEquivalentTo() for how it resolves static.
     * TODO: Refactor.
     *
     * @see UnionType::isExclusivelyNarrowedFormOrEquivalentTo() for a check on union types as a whole.
     */
    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()) {
            return true;
        }

        // Check to see if the other union type contains this
        if ($union_type->hasType($this)) {
            return true;
        }
        if ($this->isNullable() && !$union_type->containsNullable()) {
            // e.g. can't cast ?int to int, mixed to non-null-mixed, etc.
            return false;
        }
        $this_resolved = $this->withStaticResolvedInContext($context);
        // TODO: Allow casting MyClass<TemplateType> to MyClass (Without the template?


        // TODO: Need to resolve expanded union types (parents, interfaces) of classes *before* this is called.

        // Test to see if this (or any ancestor types) can cast to the given union type.
        $expanded_types = $this_resolved->asExpandedTypes($code_base);
        foreach ($expanded_types->getTypeSet() as $type) {
            if ($type->isSubtypeOfAnyTypeInSet($union_type->getTypeSet())) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return Type
     * Either this or 'static' resolved in the given context.
     */
    public function withStaticResolvedInContext(
        Context $context
    ): Type {
        if ($this->template_parameter_type_list) {
            return $this->withStaticResolvedInContextTemplate($context);
        }
        return $this;
    }

    private function withStaticResolvedInContextTemplate(
        Context $context
    ): Type {
        $new_template_parameter_type_list = [];
        foreach ($this->template_parameter_type_list as $t) {
            $new_template_parameter_type_list[] = $t->withStaticResolvedInContext($context);
        }
        if ($new_template_parameter_type_list === $this->template_parameter_type_list) {
            return $this;
        }
        return self::fromType($this, $new_template_parameter_type_list);
    }

    /**
     * @return string
     * A string representation of this type in FQSEN form.
     */
    public function asFQSENString(): string
    {
        $namespace = $this->namespace;
        if (!$namespace) {
            return $this->name;
        }

        if ('\\' === $namespace) {
            return '\\' . $this->name;
        }

        return "{$namespace}\\{$this->name}";
    }

    /**
     * @return string
     * A human readable representation of this type
     * (This is frequently called, so prefer efficient operations)
     */
    public function __toString()
    {
        return $this->memoize(__METHOD__, function (): string {
            $string = $this->asFQSENString();

            if (count($this->template_parameter_type_list) > 0) {
                $string .= $this->templateParameterTypeListAsString();
            }

            if ($this->is_nullable) {
                $string = '?' . $string;
            }

            return $string;
        });
    }

    /**
     * Gets the part of the Type string for the template parameters.
     * Precondition: $this->template_parameter_string is not null.
     */
    final protected function templateParameterTypeListAsString(): string
    {
        return '<' .
            \implode(',', \array_map(static function (UnionType $type): string {
                return $type->__toString();
            }, $this->template_parameter_type_list)) . '>';
    }

    private const CANONICAL_NAME_MAP = [
        'boolean'  => 'bool',
        'callback' => 'callable',
        'closure'  => 'Closure',
        'double'   => 'float',
        'integer'  => 'int',
    ];

    /**
     * @param string $name
     * Any type name
     *
     * @return string
     * A canonical name for the given type name
     *
     * @phan-side-effect-free
     */
    public static function canonicalNameFromName(
        string $name
    ): string {
        return self::CANONICAL_NAME_MAP[strtolower($name)] ?? $name;
    }

    /**
     * @param string $type_string
     * Any type string such as 'int' or 'Set<int>'
     *
     * @return Tuple5<string,string,list<string>,bool,?array<string|int,string>>
     * A 5-tuple with the following types:
     * 0: the namespace
     * 1: the type name.
     * 2: The template parameters, if any
     * 3: Whether or not the type is nullable
     * 4: The shape components, if any. Null unless this is an array shape type string such as 'array{field:int}'
     *
     * NOTE: callers must check for the generic array symbol in the type name or for type names beginning with 'array{' (case-insensitive)
     *
     * NOTE: callers must not mutate the result.
     */
    private static function typeStringComponents(
        string $type_string
    ): Tuple5 {
        // This doesn't depend on any configs; the result can be safely cached.
        static $cache = [];
        return $cache[$type_string] ?? ($cache[$type_string] = self::typeStringComponentsInner($type_string));
    }

    /**
     * @return Tuple5<string,string,list<string>,bool,?array<string|int,string>>
     * A 5-tuple with the following types (for a type string that may contain array shape, closure, or template uses):
     * 0: the namespace
     * 1: the type name.
     * 2: The template parameters, if any
     * 3: Whether or not the type is nullable
     * 4: The shape components, if any. Null unless this is an array shape type string such as 'array{field:int}'
     * @suppress PhanPossiblyFalseTypeArgument
     */
    private static function typeStringComponentsInner(
        string $type_string
    ): Tuple5 {
        // Check to see if we have template parameter types
        $template_parameter_type_name_list = [];
        $shape_components = null;

        $match = [];
        $is_nullable = false;
        if (\preg_match('/^' . self::type_regex_or_this . '$/', $type_string, $match)) {
            $closure_components = $match[3] ?? '';
            if ($closure_components !== '') {
                return self::closureTypeStringComponents($type_string, $closure_components);
            }
            if (!isset($match[2])) {
                // Parse '(X)' as 'X'
                return self::typeStringComponents(\substr($match[1], 1, -1));
            } elseif (!isset($match[4])) {
                if (\substr($type_string, -1) === ')') {
                    // Parse '?(X[]) as '?X[]'
                    return self::typeStringComponents('?' . \substr($match[2], 2, -1));
                } else {
                    return new Tuple5(
                        '',
                        $match[0],
                        [],
                        false,
                        null
                    );
                }
            }
            $type_string = $match[4];

            // Rip out the nullability indicator if it
            // exists and note its nullability
            $is_nullable = ($match[5] ?? '') === '?';
            if ($is_nullable) {
                $type_string = \substr($type_string, 1);
            }

            if (($match[8] ?? '') !== '') {
                $shape_components = self::extractShapeComponents($match[9] ?? '');  // will be empty array for 'array{}'
            } else {
                // Recursively parse this
                $template_parameter_type_name_list = ($match[7] ?? '') !== ''
                    ? self::extractNameList($match[7])
                    : [];
            }
        }

        // Determine if the type name is fully qualified
        // (as specified by a leading backslash).
        // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal
        $is_fully_qualified = (0 === \strpos($type_string, '\\'));

        // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal
        $fq_class_name_elements = \array_filter(\explode('\\', $type_string));

        $class_name =
            (string)\array_pop($fq_class_name_elements);

        $namespace = ($is_fully_qualified ? '\\' : '')
            . \implode('\\', \array_filter(
                $fq_class_name_elements
            ));

        return new Tuple5(
            $namespace,
            $class_name,
            $template_parameter_type_name_list,
            $is_nullable,
            $shape_components
        );
    }

    /**
     * @return Tuple5<string,string,list<string>,bool,?array<string|int,string>>
     * A 5-tuple with the following types (for a type string of a callable/closure):
     * 0: the namespace
     * 1: the type name.
     * 2: The template parameters, if any
     * 3: Whether or not the type is nullable
     * 4: The shape components, if any. Null unless this is an array shape type string such as 'array{field:int}'
     */
    private static function closureTypeStringComponents(string $type_string, string $inner): Tuple5
    {
        // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal
        $parts = self::closureParams(\trim(\substr($inner, 1, -1)));
        // TODO: parse params, same as @method

        // Parse the optional return type for this closure
        $i = \strpos($type_string, $inner) + \strlen($inner);
        $colon_index = \strpos($type_string, ':', $i);

        if ($colon_index !== false) {
            // @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal
            $return_type_string = \ltrim(\substr($type_string, $colon_index + 1));
        } else {
            $return_type_string = 'void';
        }
        $parts[] = $return_type_string;

        return new Tuple5(
            '\\',
            preg_match('/^\??callable/i', $type_string) > 0 ? 'callable' : 'Closure',
            [],
            $type_string[0] === '?',
            $parts
        );
    }

    /**
     * @return list<string>
     */
    private static function closureParams(string $arg_list): array
    {
        // Special check if param list has 0 params.
        if ($arg_list === '') {
            return [];
        }
        // TODO: Would need to use a different approach if templates were ever supported
        //       e.g. The magic method parsing doesn't support commas?
        return \array_map('trim', self::extractNameList($arg_list));
    }

    /**
     * @return array<string|int,string> maps field name to field type.
     */
    private static function extractShapeComponents(string $shape_component_string): array
    {
        $result = [];
        foreach (self::extractNameList($shape_component_string) as $shape_component) {
            // Because these can be nested, there may be more than one ':'. Only consider the first.
            if (preg_match('/^(' . self::shape_key_regex . ')\s*:\s*(.*)$/D', $shape_component, $parts)) {
                $field_name = $parts[1];
                $field_value = \trim($parts[2]);
                if ($field_value === '') {
                    continue;
                }
                $result[$field_name] = $field_value;
            } else {
                $shape_component = \trim($shape_component);
                if ($shape_component === '') {
                    continue;
                }
                $result[] = $shape_component;
            }
        }
        return $result;
    }

    /**
     * Extracts the inner parts of a template name list (i.e. within <>) or a shape component list (i.e. within {})
     * @return list<string>
     */
    private static function extractNameList(string $list_string): array
    {
        $results = [];
        $prev_parts = [];
        $delta = 0;
        foreach (\explode(',', $list_string) as $result) {
            $result = \trim($result);
            $open_bracket_count = \substr_count($result, '<') + \substr_count($result, '{') + \substr_count($result, '(');
            $close_bracket_count = \substr_count($result, '>') + \substr_count($result, '}') + \substr_count($result, ')');
            if (count($prev_parts) > 0) {
                $prev_parts[] = $result;
                $delta += $open_bracket_count - $close_bracket_count;
                if ($delta <= 0) {
                    if ($delta === 0) {
                        $results[] = \implode(',', $prev_parts);
                    }  // ignore unparsable data such as "<T,T2>>" or "T, T2{}}"
                    $prev_parts = [];
                    $delta = 0;
                }
                continue;
            }
            if ($open_bracket_count === 0) {
                $results[] = $result;
                continue;
            }
            $delta = $open_bracket_count - $close_bracket_count;
            if ($delta === 0) {
                $results[] = $result;
            } elseif ($delta > 0) {
                $prev_parts[] = $result;
            }  // otherwise ignore unparsable data such as ">" (should be impossible)

            // e.g. we're breaking up T1<T2<X,Y>> into "T1<T2<X" and "Y>>"
        }
        if (\strpos($list_string, "'") !== false) {
            return self::joinQuotedStrings($results);
        }
        return $results;
    }

    /**
     * Heuristic to handle literal commas in single quoted strings inside of templates/array shapes.
     *
     * @param list<string> $results
     * @return list<string>
     */
    private static function joinQuotedStrings(array $results): array
    {
        // Preserve the original count: This will change if results are combined.
        $N = count($results);
        // Iterate by offset (manually) to avoid unexpected behavior of unset of subsequent elements in foreach
        for ($i = 0; $i < $N;) {
            $part = $results[$i];
            if (\substr_count($part, "'") % 2 === 0) {
                $i++;
                continue;
            }
            // This has an odd number of single quotes. Combine it with other parts until the total number of single quotes is even, or throw.
            for ($j = $i + 1; $j < $N; $j++) {
                $other = $results[$j];
                unset($results[$j]);
                $results[$i] .= ",$other";
                if (\substr_count($other, "'") % 2 !== 0) {
                    $i = $j + 1;
                    continue 2;
                }
            }
            throw new InvalidArgumentException("Unmatched \"'\" of $part in " . \implode(',', $results));
        }
        if ($N !== count($results)) {
            return \array_values($results);
        }
        // @phan-suppress-next-line PhanPartialTypeMismatchReturn this is already a list (ensured by above check).
        return $results;
    }

    /**
     * Helper function for internal use by UnionType.
     * Overridden by subclasses.
     */
    public function getNormalizationFlags(): int
    {
        return $this->is_nullable ? self::_bit_nullable : 0;
    }

    /**
     * Returns true if this contains any array shape type instances
     * or literal type instances that could be normalized to
     * regular generic array types or scalar types.
     */
    public function hasArrayShapeOrLiteralTypeInstances(): bool
    {
        return false;
    }

    /**
     * Returns true if this contains any array shape type instances
     * that could be normalized to regular generic array types.
     */
    public function hasArrayShapeTypeInstances(): bool
    {
        return false;
    }

    /**
     * Used to check if this type can be replaced by more specific types, for non-quick mode
     *
     * @internal
     */
    public function shouldBeReplacedBySpecificTypes(): bool
    {
        // Could check for final classes such as stdClass here, but not much of a reason to.
        return true;
    }

    /**
     * Converts this type to one where array shapes are flattened to generic arrays, and literal scalars are converted to the general type for that scalar.
     *
     * E.g. converts the type `array{0:2}` to `array<int,int>`
     *
     * This is overridden by subclasses.
     *
     * @return Type[]
     */
    public function withFlattenedArrayShapeOrLiteralTypeInstances(): array
    {
        return [$this];
    }

    /**
     * Converts this type to one where array shapes are flattened to generic arrays, and literal scalars are converted to the general type for that scalar.
     *
     * E.g. converts the type `array{0:array{key:float}>}` to `array<int,array{key:float}>`
     *
     * This is overridden by subclasses.
     *
     * @return Type[]
     * @suppress PhanUnreferencedPublicMethod added for convenience (the only override is ArrayShapeType)
     */
    public function withFlattenedTopLevelArrayShapeTypeInstances(): array
    {
        return [$this];
    }

    /**
     * Overridden in subclasses such as LiteralIntType
     */
    public function asNonLiteralType(): Type
    {
        return $this;
    }

    /**
     * Returns true if this is a potentially valid operand for a numeric operator.
     * Callers should also check if this is nullable.
     */
    public function isValidNumericOperand(): bool
    {
        return false;
    }

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

    /**
     * Returns true if this contains a type that is definitely non-callable
     * e.g. returns true for false, array, int
     *      returns false for callable, array, object, iterable, T, etc.
     */
    public function isDefiniteNonCallableType(): bool
    {
        // Any non-final class could be extended with a callable type.
        // TODO: Check if final
        return false;
    }

    /**
     * Check if this type can satisfy a comparison (<, <=, >, >=)
     * @param int|string|float|bool|null $scalar
     * @param int $flags (e.g. \ast\flags\BINARY_IS_SMALLER)
     * @internal
     * @suppress PhanUnusedPublicMethodParameter
     */
    public function canSatisfyComparison($scalar, int $flags): bool
    {
        return true;
    }

    /**
     * Perform the binary operation corresponding to $flags on $a OP $b
     * @param array|int|string|float|bool|null $a
     * @param int|string|float|bool|null $b
     * @param int $flags
     * @internal
     * @phan-side-effect-free
     */
    public static function performComparison($a, $b, int $flags): bool
    {
        switch ($flags) {
            case flags\BINARY_IS_GREATER:
                return $a > $b;
            case flags\BINARY_IS_GREATER_OR_EQUAL:
                return $a >= $b;
            case flags\BINARY_IS_SMALLER:
                return $a < $b;
            case flags\BINARY_IS_SMALLER_OR_EQUAL:
                return $a <= $b;
        }
        throw new AssertionError("Impossible flag $flags");
    }

    /**
     * Returns the type after an expression such as `++$x`
     */
    public function getTypeAfterIncOrDec(): UnionType
    {
        if ($this->is_nullable) {
            // ++null is 1
            return UnionType::of([$this->withIsNullable(false), IntType::instance(false)], []);
        }
        // ++$obj; doesn't change the object.
        return $this->asPHPDocUnionType();
    }

    /**
     * Returns the Type for \Traversable
     *
     * @suppress PhanThrowTypeAbsentForCall
     * @phan-side-effect-free
     */
    public static function traversableInstance(): Type
    {
        static $instance = null;
        return $instance ?? ($instance = Type::fromFullyQualifiedString('\Traversable'));
    }

    /**
     * Returns the Type for \Throwable
     *
     * @suppress PhanThrowTypeAbsentForCall
     * @phan-side-effect-free
     */
    public static function throwableInstance(): Type
    {
        static $instance = null;
        return $instance ?? ($instance = Type::fromFullyQualifiedString('\Throwable'));
    }

    /**
     * Returns the Type for \Countable
     *
     * @suppress PhanThrowTypeAbsentForCall
     * @phan-side-effect-free
     */
    public static function countableInstance(): Type
    {
        static $instance = null;
        return $instance ?? ($instance = Type::fromFullyQualifiedString('\Countable'));
    }

    /**
     * Returns true if this is `MyNs\MyClass<T..>` when $type is `MyNs\MyClass`
     */
    public function isTemplateSubtypeOf(Type $type): bool
    {
        if ($this->name !== $type->name || $this->namespace !== $type->namespace) {
            return false;
        }
        return \count($this->template_parameter_type_list) > 0;
    }

    /**
     * Returns true for `T` and `T[]` and `\MyClass<T>`, but not `\MyClass<\OtherClass>`
     *
     * Overridden in subclasses.
     */
    public function hasTemplateTypeRecursive(): bool
    {
        foreach ($this->template_parameter_type_list as $type) {
            if ($type->hasTemplateTypeRecursive()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Convert `\MyClass<T>` and `\MyClass<\OtherClass>` to just `\MyClass`.
     *
     * TODO: Override in subclasses such as generic arrays, generic iterables, and array shapes.
     */
    public function eraseTemplatesRecursive(): Type
    {
        if (!$this->template_parameter_type_list) {
            return $this;
        }
        return static::fromType($this, []);
    }

    /**
     * @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.
     *
     * Overridden in subclasses
     */
    public function withTemplateParameterTypeMap(
        array $template_parameter_type_map
    ): UnionType {
        if (!$this->template_parameter_type_list) {
            return $this->asPHPDocUnionType();
        }
        $new_type_list = [];
        foreach ($this->template_parameter_type_list as $type) {
            $new_type_list[] = $type->withTemplateParameterTypeMap($template_parameter_type_map);
        }
        if ($new_type_list === $this->template_parameter_type_list) {
            return $this->asPHPDocUnionType();
        }
        return self::fromType($this, $new_type_list)->asPHPDocUnionType();
    }

    /**
     * Precondition: Callers should check isObjectWithKnownFQSEN
     */
    public function hasSameNamespaceAndName(Type $type): bool
    {
        return $this->name === $type->name && $this->namespace === $type->namespace;
    }

    /**
     * @param CodeBase $code_base may be used for resolving inheritance
     * @param TemplateType $template_type the template type that this union type is being searched for
     *
     * @return ?Closure(UnionType, Context):UnionType a closure to determine the union type(s) that are in the same position(s) as the template type.
     * This is overridden in subclasses.
     */
    public function getTemplateTypeExtractorClosure(CodeBase $code_base, TemplateType $template_type): ?Closure
    {
        if (!$this->template_parameter_type_list) {
            return null;
        }
        if (!$this->isObjectWithKnownFQSEN()) {
            return null;
        }
        $closure = null;
        foreach ($this->template_parameter_type_list as $i => $actual_template_union_type) {
            $inner_extractor_closure = $actual_template_union_type->getTemplateTypeExtractorClosure($code_base, $template_type);
            if (!$inner_extractor_closure) {
                continue;
            }
            $closure = TemplateType::combineParameterClosures(
                $closure,
                static function (UnionType $type, Context $context) use ($inner_extractor_closure, $i): UnionType {
                    $result = UnionType::empty();
                    foreach ($type->getTypeSet() as $inner_type) {
                        $replacement_type = $inner_type->template_parameter_type_list[$i] ?? null;
                        if ($replacement_type) {
                            $result = $result->withUnionType($inner_extractor_closure($replacement_type, $context));
                        }
                    }
                    return $result;
                }
            );
        }

        return $closure;
    }

    /**
     * Returns the function interface that would be used if this type were a callable, or null.
     *
     * @param CodeBase $code_base the code base in which the function interface is found
     * @param Context $context the context where the function interface is referenced (for emitting issues) @phan-unused-param
     */
    public function asFunctionInterfaceOrNull(CodeBase $code_base, Context $context): ?FunctionInterface
    {
        if (static::class !== self::class) {
            // Overridden in other subclasses
            return null;
        }
        $fqsen = FullyQualifiedClassName::fromType($this);
        if (!$code_base->hasClassWithFQSEN($fqsen)) {
            return null;
        }
        $class = $code_base->getClassByFQSEN($fqsen);
        if (!$class->hasMethodWithName($code_base, '__invoke', true)) {
            Issue::maybeEmit(
                $code_base,
                $context,
                Issue::UndeclaredInvokeInCallable,
                $context->getLineNumberStart(),
                '__invoke',
                $fqsen
            );
            return null;
        }
        return $class->getMethodByName($code_base, '__invoke');
    }

    /**
     * Gets the inner Types in this Type (or subclass of Type) that can refer to classes.
     *
     * For a regular Type, that's the Type.
     *
     * @return Generator<mixed,Type>
     *
     * TODO: Also support template types
     */
    public function getReferencedClasses(): Generator
    {
        yield $this;
    }

    /**
     * Returns true if this type or a parent type can be used in a signature.
     * Returns false for template types, resources, object, etc.
     */
    public function canUseInRealSignature(): bool
    {
        return true;
    }

    /**
     * Returns the corresponding type that would be used in a signature
     */
    public function asSignatureType(): Type
    {
        if ($this->template_parameter_type_list) {
            return self::fromType($this, []);
        }
        return $this;
    }

    /**
     * Convert this to a subtype that satisfies is_callable(), or return null
     */
    public function asCallableType(): ?Type
    {
        if ($this->isCallable()) {
            return $this->withIsNullable(false);
        }
        return null;
    }

    /**
     * Convert this to a subtype that satisfies is_array(), or returns null
     * @see UnionType::arrayTypesStrictCast
     */
    public function asArrayType(): ?Type
    {
        return null;
    }

    /**
     * Convert this to a subtype that satisfies is_scalar(), or returns null
     */
    public function asScalarType(): ?Type
    {
        return null;
    }

    /**
     * callers should use UnionType->hasAnyTypeOverlap() and UnionType->hasAnyWeakTypeOverlap() instead.
     * Overridden in subclasses
     * @internal
     */
    public function weaklyOverlaps(Type $other): bool
    {
        // TODO: Finish implementing, check if types are compatible when both are non-null, check for object vs non-object
        return $this->isPossiblyFalsey() && $other->isPossiblyFalsey();
    }

    /**
     * Returns a type where all referenced union types (e.g. in generic arrays) have real type sets removed.
     * Overridden in subclasses
     * @phan-pure
     */
    public function withErasedUnionTypes(): Type
    {
        return $this;
    }

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

    /**
     * Returns true if this is referring to the throwable interface exactly
     */
    public function isThrowableInterface(): bool
    {
        return $this->name === 'Throwable' && $this->namespace === '\\';
    }
}