src/Phan/Language/Type/GenericArrayType.php
<?php
declare(strict_types=1);
namespace Phan\Language\Type;
use ast\Node;
use Closure;
use Generator;
use InvalidArgumentException;
use Phan\AST\ASTReverter;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Config;
use Phan\Debug\Frame;
use Phan\Exception\RecursionDepthException;
use Phan\Issue;
use Phan\Language\Context;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\Type;
use Phan\Language\UnionType;
use Phan\Language\UnionTypeBuilder;
/**
* Phan's representation for the types `array<string,MyClass>` and `MyClass[]`
* @see ArrayShapeType for representations of `array{key:MyClass}`
* @see ArrayType for the representation of `array`
* @phan-pure
*/
class GenericArrayType extends ArrayType implements GenericArrayInterface
{
/** @phan-override */
public const NAME = 'array';
// In PHP, array keys can be integers or strings. These constants describe all possible combinations of those key types.
/**
* No array keys.
* Array types with this key type Similar to KEY_MIXED, but adding a key type will change the array to the new key
* instead of staying as KEY_MIXED.
*/
public const KEY_EMPTY = 0; // No way to create this type yet.
/** array keys are integers */
public const KEY_INT = 1;
/** array keys are strings */
public const KEY_STRING = 2;
/** array keys are integers or strings. */
public const KEY_MIXED = 3; // i.e. KEY_INT|KEY_STRING
public const KEY_NAMES = [
self::KEY_EMPTY => 'empty',
self::KEY_INT => 'int',
self::KEY_STRING => 'string',
self::KEY_MIXED => 'mixed', // treated the same way as int|string
];
/**
* @var Type
* The type of every value in this array
*/
protected $element_type;
/**
* @var int
* Enum representing the type of every key in this array
*/
protected $key_type;
/**
* @param Type $type
* The type of every element in this array
*
* @param bool $is_nullable
* Set to true if the type should be nullable, else pass
* false
*
* @param int $key_type
* Corresponds to the type of the array keys. Set this to a GenericArrayType::KEY_* constant.
*
* @throws InvalidArgumentException if $key_type is an invalid constant
*/
protected function __construct(Type $type, bool $is_nullable, int $key_type)
{
if ($key_type & ~3) {
throw new InvalidArgumentException("Invalid key_type $key_type");
}
parent::__construct('\\', self::NAME, [], $is_nullable);
$this->element_type = $type;
$this->key_type = $key_type;
}
/**
* Returns the key type of this generic array.
* e.g. for `int[]`, returns self::KEY_MIXED, for `array<string,mixed>`, returns self::KEY_STRING.
*/
public function getKeyType(): int
{
return $this->key_type;
}
/**
* @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.
*/
public function withIsNullable(bool $is_nullable): Type
{
if ($is_nullable === $this->is_nullable) {
return $this;
}
return static::fromElementType(
$this->element_type,
$is_nullable,
$this->key_type
);
}
/**
* @return bool
* True if this Type can be cast to the given Type
* cleanly
*/
protected function canCastToNonNullableType(Type $type): bool
{
if ($type instanceof ArrayType) {
if ($type instanceof GenericArrayType) {
if (!$this->element_type->canCastToType($type->element_type)) {
return false;
}
if ((($this->key_type ?: self::KEY_MIXED) & ($type->key_type ?: self::KEY_MIXED)) === 0) {
// Attempting to cast an int key to a string key (or vice versa) is normally invalid.
// However, the scalar_array_key_cast config would make any cast of array keys a valid cast.
return Config::getValue('scalar_array_key_cast');
}
return true;
} elseif ($type instanceof ArrayShapeType) {
if ((($this->key_type ?: self::KEY_MIXED) & $type->getKeyType()) === 0 && !Config::getValue('scalar_array_key_cast')) {
// Attempting to cast an int key to a string key (or vice versa) is normally invalid.
// However, the scalar_array_key_cast config would make any cast of array keys a valid cast.
return false;
}
return $this->genericArrayElementUnionType()->canCastToUnionType($type->genericArrayElementUnionType());
}
return true;
}
if (\get_class($type) === IterableType::class) {
// can cast to Iterable but not Traversable
return true;
}
if ($type instanceof GenericIterableType) {
return $this->canCastToGenericIterableType($type);
}
$d = \strtolower($type->__toString());
if ($d[0] === '\\') {
$d = \substr($d, 1);
}
if ($d === 'callable') {
return $this->key_type !== self::KEY_STRING;
}
return parent::canCastToNonNullableType($type);
}
protected function canCastToNonNullableTypeWithoutConfig(Type $type): bool
{
if ($type instanceof ArrayType) {
if ($type instanceof GenericArrayType) {
if (!$this->element_type->canCastToTypeWithoutConfig($type->element_type)) {
return false;
}
if ((($this->key_type ?: self::KEY_MIXED) & ($type->key_type ?: self::KEY_MIXED)) === 0) {
// Attempting to cast an int key to a string key (or vice versa) is normally invalid.
return false;
}
return true;
} elseif ($type instanceof ArrayShapeType) {
if ((($this->key_type ?: self::KEY_MIXED) & $type->getKeyType()) === 0) {
// Attempting to cast an int key to a string key (or vice versa) is normally invalid.
return false;
}
return $this->genericArrayElementUnionType()->canCastToUnionTypeWithoutConfig($type->genericArrayElementUnionType());
}
return true;
}
if (\get_class($type) === IterableType::class) {
// can cast to Iterable but not Traversable
return true;
}
if ($type instanceof GenericIterableType) {
return $this->canCastToGenericIterableType($type);
}
$d = \strtolower($type->__toString());
if ($d[0] === '\\') {
$d = \substr($d, 1);
}
if ($d === 'callable') {
return $this->key_type !== self::KEY_STRING;
}
return parent::canCastToNonNullableTypeWithoutConfig($type);
}
private function canCastToGenericIterableType(
GenericIterableType $iterable_type
): bool {
if (!$this->element_type->asPHPDocUnionType()->canCastToUnionType($iterable_type->getElementUnionType())) {
return false;
}
// TODO: Account for scalar key casting config
$key_union_type = self::unionTypeForKeyType($this->key_type);
if (!$key_union_type->canCastToUnionType($iterable_type->getKeyUnionType())) {
return false;
}
return true;
}
/**
* @param Type $type
* The element type for an array.
*
* @param bool $is_nullable
* Set to true if the this is a nullable array(e.g. `?($type[])`),
* else pass false
*
* @param int $key_type
* Corresponds to the type of the array keys. Set this to a GenericArrayType::KEY_* constant.
*
* @return GenericArrayType
* Get a type representing an array of the given type
*/
public static function fromElementType(
Type $type,
bool $is_nullable,
int $key_type
): GenericArrayType {
// Make sure we only ever create exactly one
// object for any unique type
static $canonical_object_maps = null;
if ($canonical_object_maps === null) {
$canonical_object_maps = [];
for ($i = 0; $i < 8; $i++) {
$canonical_object_maps[] = new \SplObjectStorage();
}
}
$map_index = $key_type * 2 + ($is_nullable ? 1 : 0);
$map = $canonical_object_maps[$map_index];
if (!$map->contains($type)) {
$map->attach(
$type,
new GenericArrayType($type, $is_nullable, $key_type)
);
}
return $map->offsetGet($type);
}
public function isGenericArray(): bool
{
return true;
}
/**
* @return Type
* A variation of this type that is not generic.
* i.e. 'int[]' becomes 'int'.
*/
public function genericArrayElementType(): Type
{
return $this->element_type;
}
/**
* @unused-param $code_base
* @return UnionType returns the array value's union type
* @phan-override
*/
public function iterableValueUnionType(CodeBase $code_base): UnionType
{
return $this->element_type->asPHPDocUnionType();
}
/**
* @unused-param $code_base
* @return UnionType the array key's union type
* @phan-override
*/
public function iterableKeyUnionType(CodeBase $code_base): UnionType
{
return self::unionTypeForKeyType($this->key_type);
}
/**
* @return UnionType
* A variation of this type that is not generic.
* i.e. 'int[]' becomes 'int'.
*/
public function genericArrayElementUnionType(): UnionType
{
return $this->element_type->asPHPDocUnionType();
}
public function __toString(): string
{
$string = $this->element_type->__toString();
if ($this->key_type === self::KEY_MIXED) {
// Disambiguation is needed for ?T[] and (?T)[] but not array<int,?T>
if ($string[0] === '?' || $this->element_type instanceof FunctionLikeDeclarationType) {
$string = '(' . $string . ')';
}
$string = "{$string}[]";
} else {
$string = 'array<' . self::KEY_NAMES[$this->key_type] . ',' . $string . '>';
}
if ($this->is_nullable) {
if ($string[0] === '?') {
$string = "?($string)";
} else {
$string = '?' . $string;
}
}
return $string;
}
/**
* @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.
* @override
*/
public function asExpandedTypes(
CodeBase $code_base,
int $recursion_depth = 0
): UnionType {
// 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());
}
return $this->memoize(__METHOD__, function () use ($code_base, $recursion_depth): UnionType {
$union_type = $this->asPHPDocUnionType();
$element_type = $this->element_type;
if (!$element_type->isObjectWithKnownFQSEN()) {
return $union_type;
}
$class_fqsen = FullyQualifiedClassName::fromType($element_type);
if (!$code_base->hasClassWithFQSEN($class_fqsen)) {
return $union_type;
}
$clazz = $code_base->getClassByFQSEN($class_fqsen);
$class_union_type = $clazz->getUnionType();
$additional_union_type = $clazz->getAdditionalTypes();
if ($additional_union_type !== null) {
$class_union_type = $class_union_type->withUnionType($additional_union_type);
}
// TODO: Use helpers for list, non-empty-array, etc.
foreach ($class_union_type->getTypeSet() as $type) {
$union_type = $union_type->withType(static::fromElementType($type, $this->is_nullable, $this->key_type));
}
// Recurse up the tree to include all types
$recursive_union_type_builder = new UnionTypeBuilder();
$representation = $this->__toString();
try {
foreach ($union_type->getTypeSet() as $generic_array_type) {
if ($generic_array_type->__toString() !== $representation) {
$recursive_union_type_builder->addUnionType(
$generic_array_type->asExpandedTypes(
$code_base,
$recursion_depth + 1
)
);
} else {
$recursive_union_type_builder->addType($generic_array_type);
}
}
} catch (RecursionDepthException $_) {
return ArrayType::instance($this->is_nullable)->asPHPDocUnionType();
}
// Add in aliases
// (If enable_class_alias_support is false, this will do nothing)
if (Config::getValue('enable_class_alias_support')) {
$this->addClassAliases($code_base, $recursive_union_type_builder, $class_fqsen);
}
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.
* @override
*/
public function asExpandedTypesPreservingTemplate(
CodeBase $code_base,
int $recursion_depth = 0
): UnionType {
// 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());
}
return $this->memoize(__METHOD__, function () use ($code_base, $recursion_depth): UnionType {
$union_type = $this->asPHPDocUnionType();
$class_fqsen = FullyQualifiedClassName::fromType($this->element_type);
if (!$code_base->hasClassWithFQSEN($class_fqsen)) {
return $union_type;
}
$clazz = $code_base->getClassByFQSEN($class_fqsen);
$class_union_type = $clazz->getUnionType();
$additional_union_type = $clazz->getAdditionalTypes();
if ($additional_union_type !== null) {
$class_union_type = $class_union_type->withUnionType($additional_union_type);
}
$union_type = $union_type->withUnionType(
$class_union_type->asGenericArrayTypes($this->key_type)
);
// Recurse up the tree to include all types
$recursive_union_type_builder = new UnionTypeBuilder();
$representation = $this->__toString();
try {
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);
}
}
} catch (RecursionDepthException $_) {
return ArrayType::instance($this->is_nullable)->asPHPDocUnionType();
}
// Add in aliases
// (If enable_class_alias_support is false, this will do nothing)
if (Config::getValue('enable_class_alias_support')) {
$this->addClassAliases($code_base, $recursive_union_type_builder, $class_fqsen);
}
return $recursive_union_type_builder->getPHPDocUnionType();
});
}
// (If enable_class_alias_support is false, this will not be called)
private function addClassAliases(
CodeBase $code_base,
UnionTypeBuilder $union_type_builder,
FullyQualifiedClassName $class_fqsen
): void {
$fqsen_aliases = $code_base->getClassAliasesByFQSEN($class_fqsen);
foreach ($fqsen_aliases as $alias_fqsen_record) {
$alias_fqsen = $alias_fqsen_record->alias_fqsen;
$union_type_builder->addType(
$alias_fqsen->asType()->asGenericArrayType($this->key_type)
);
}
}
/**
* Returns the key type for the keys of the real type set of this union type,
* strictly
* E.g. for `array<string,\stdClass>`, returns self::KEY_STRING
* for `array<string,\stdClass>|array`, returns self::KEY_MIXED
* @param list<Type> $type_set
*/
public static function keyUnionTypeFromTypeSetStrict(array $type_set): int
{
$key_types = self::KEY_EMPTY;
foreach ($type_set as $type) {
if ($type instanceof GenericArrayType) {
$key_types |= $type->key_type;
} elseif ($type instanceof ArrayShapeType) {
if ($type->isNotEmptyArrayShape()) {
$key_types |= $type->getKeyType();
}
} else {
return self::KEY_MIXED;
}
// Treating ArrayType as mixed or excluding ArrayType would both cause false positives. Ignore ArrayType.
}
// int|string corresponds to KEY_MIXED (KEY_INT|KEY_STRING)
// And if we're unable to find any types, return KEY_MIXED.
return $key_types ?: self::KEY_MIXED;
}
/**
* Returns the key type for the keys of this union type.
* E.g. for `array<string,\stdClass>`, returns self::KEY_STRING
*/
public static function keyTypeFromUnionTypeKeys(UnionType $union_type): int
{
$key_types = self::KEY_EMPTY;
foreach ($union_type->getTypeSet() as $type) {
if ($type instanceof GenericArrayType) {
$key_types |= $type->key_type;
} elseif ($type instanceof ArrayShapeType) {
if ($type->isNotEmptyArrayShape()) {
$key_types |= $type->getKeyType();
}
}
// Treating ArrayType as mixed or excluding ArrayType would both cause false positives. Ignore ArrayType.
}
// int|string corresponds to KEY_MIXED (KEY_INT|KEY_STRING)
// And if we're unable to find any types, return KEY_MIXED.
return $key_types ?: self::KEY_MIXED;
}
/** @suppress PhanUnreferencedPublicClassConstant */
public const CONVERT_KEY_MIXED_TO_EMPTY_UNION_TYPE = 0;
public const CONVERT_KEY_MIXED_TO_INT_OR_STRING_UNION_TYPE = 1;
/**
* @return UnionType a union type corresponding to $key_type
*/
public static function unionTypeForKeyType(int $key_type, int $behavior = self::CONVERT_KEY_MIXED_TO_INT_OR_STRING_UNION_TYPE): UnionType
{
static $int_union_type = null;
static $string_union_type = null;
static $int_or_string_union_type = null;
if ($int_union_type === null) {
$int_union_type = UnionType::fromFullyQualifiedPHPDocString('int');
$string_union_type = UnionType::fromFullyQualifiedPHPDocString('string');
$int_or_string_union_type = UnionType::fromFullyQualifiedPHPDocString('int|string');
}
switch ($key_type) {
case self::KEY_INT:
return $int_union_type;
case self::KEY_STRING:
return $string_union_type;
default:
if ($behavior === self::CONVERT_KEY_MIXED_TO_INT_OR_STRING_UNION_TYPE) {
return $int_or_string_union_type;
}
return UnionType::empty();
}
}
/**
* Returns `self::KEY_*` corresponding to the provided union type.
* E.g. for `string`, returns `self::KEY_STRING`.
*/
public static function keyTypeFromUnionTypeValues(UnionType $union_type): int
{
$key_types = self::KEY_EMPTY;
foreach ($union_type->getTypeSet() as $type) {
if ($type instanceof StringType) {
$key_types |= self::KEY_STRING;
} elseif ($type instanceof IntType) {
$key_types |= self::KEY_INT;
} elseif ($type instanceof MixedType) {
// Anything including a mixed type is a mixed type.
return self::KEY_MIXED;
} // skip invalid types.
}
// int|string corresponds to KEY_MIXED (KEY_INT|KEY_STRING)
// And if we're unable to find any types, return KEY_MIXED.
return $key_types ?: self::KEY_MIXED;
}
/**
* @param array<int|string,mixed> $array - The array keys are used for the final result.
*
* @return int
* Corresponds to the type of the array keys of $array. This is a GenericArrayType::KEY_* constant (KEY_INT, KEY_STRING, or KEY_MIXED).
*/
public static function getKeyTypeForArrayLiteral(array $array): int
{
$key_type = GenericArrayType::KEY_EMPTY;
foreach ($array as $key => $_) {
$key_type |= (\is_string($key) ? GenericArrayType::KEY_STRING : GenericArrayType::KEY_INT);
}
return $key_type ?: GenericArrayType::KEY_MIXED;
}
/**
* @return int
* Corresponds to the type of the array keys of the array represented by $node.
* This is a GenericArrayType::KEY_* constant (KEY_INT, KEY_STRING, or KEY_MIXED).
*/
public static function getKeyTypeOfArrayNode(CodeBase $code_base, Context $context, Node $node, bool $should_catch_issue_exception = true): int
{
$key_type_enum = GenericArrayType::KEY_EMPTY;
// Check the all elements for key types.
foreach ($node->children as $child) {
if (!($child instanceof Node)) {
continue;
}
if ($child->kind === \ast\AST_UNPACK) {
// PHP 7.4's array spread operator adds integer keys, e.g. `[...$array, 'other' => 'value']`
$key_type_enum |= GenericArrayType::KEY_INT;
continue;
}
// Don't bother recursing more than one level to iterate over possible types.
$key_node = $child->children['key'];
if ($key_node instanceof Node) {
$key_type = UnionTypeVisitor::unionTypeFromNode(
$code_base,
$context,
$key_node,
$should_catch_issue_exception
);
if ($key_type->isVoidType()) {
Issue::maybeEmit(
$code_base,
$context,
Issue::TypeVoidExpression,
$node->lineno,
ASTReverter::toShortString($key_node)
);
}
$key_type_enum |= self::keyTypeFromUnionTypeValues($key_type);
} elseif ($key_node !== null) {
if (\is_string($key_node)) {
$key_type_enum |= GenericArrayType::KEY_STRING;
} elseif (\is_scalar($key_node)) {
$key_type_enum |= GenericArrayType::KEY_INT;
}
} else {
$key_type_enum |= GenericArrayType::KEY_INT;
}
// If we already think it's mixed, return immediately.
if ($key_type_enum === GenericArrayType::KEY_MIXED) {
return GenericArrayType::KEY_MIXED;
}
}
return $key_type_enum ?: GenericArrayType::KEY_MIXED;
}
public function hasArrayShapeOrLiteralTypeInstances(): bool
{
return $this->element_type->hasArrayShapeOrLiteralTypeInstances();
}
public function hasArrayShapeTypeInstances(): bool
{
return $this->element_type->hasArrayShapeTypeInstances();
}
/**
* @return list<Type>
* @override
*/
public function withFlattenedArrayShapeOrLiteralTypeInstances(): array
{
// TODO: Any point in caching this?
$type_instances = $this->element_type->withFlattenedArrayShapeOrLiteralTypeInstances();
if (\count($type_instances) === 1 && $type_instances[0] === $this->element_type) {
return [$this];
}
$results = [];
foreach ($type_instances as $type) {
$results[] = static::fromElementType($type, $this->is_nullable, $this->key_type);
}
return $results;
}
/**
* @override
*/
public function shouldBeReplacedBySpecificTypes(): bool
{
return false;
}
/**
* Returns true if this contains a type that is definitely nullable or a non-object.
* e.g. returns true false, array, int
* returns false for callable, object, iterable, T, etc.
*/
public function isDefiniteNonCallableType(): bool
{
return $this->key_type === self::KEY_STRING;
}
/**
* Returns true for `T` and `T[]` and `\MyClass<T>`, but not `\MyClass<\OtherClass>` or `false`
*/
public function hasTemplateTypeRecursive(): bool
{
return $this->genericArrayElementUnionType()->hasTemplateTypeRecursive();
}
/**
* @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 {
$element_type = $this->genericArrayElementUnionType();
$new_element_type = $element_type->withTemplateParameterTypeMap($template_parameter_type_map);
if ($element_type === $new_element_type) {
return $this->asPHPDocUnionType();
}
// TODO: Override in array shape subclass
return $new_element_type->asGenericArrayTypes($this->getKeyType());
}
/**
* @unused-param $type
* Precondition: Callers should check isObjectWithKnownFQSEN
*/
public function hasSameNamespaceAndName(Type $type): bool
{
return false;
}
/**
* If this generic array type in a parameter declaration has template types, get the closure to extract the real types for that template type from argument union types
*
* @param CodeBase $code_base
* @return ?Closure(UnionType,Context):UnionType
*/
public function getTemplateTypeExtractorClosure(CodeBase $code_base, TemplateType $template_type): ?Closure
{
$closure = $this->element_type->getTemplateTypeExtractorClosure($code_base, $template_type);
if (!$closure) {
return null;
}
// If a function expects T[], then T is the generic array element type of the passed in union type
return static function (UnionType $array_type, Context $context) use ($closure): UnionType {
return $closure($array_type->genericArrayElementTypes(), $context);
};
}
/**
* @return Generator<void,Type> (void => $inner_type)
*/
public function getReferencedClasses(): Generator
{
return $this->element_type->getReferencedClasses();
}
/**
* Returns the corresponding type that would be used in a signature
* @override
*/
public function asSignatureType(): Type
{
return ArrayType::instance($this->is_nullable);
}
/**
* Given a type such as array<int,static>, return array<int,SomeClass>
* @override
*/
public function withStaticResolvedInContext(Context $context): Type
{
$resolved_element_type = $this->element_type->withStaticResolvedInContext($context);
if ($this->element_type === $resolved_element_type) {
return $this;
}
return static::fromElementType(
$resolved_element_type,
$this->is_nullable,
$this->key_type
);
}
/**
* Returns a type where all referenced union types (e.g. in generic arrays) have real type sets removed.
* @phan-real-return static
*/
public function withErasedUnionTypes(): Type
{
return $this->memoize(__METHOD__, function (): GenericArrayType {
$erased_element_type = $this->element_type->withErasedUnionTypes();
if ($erased_element_type === $this->element_type) {
return $this;
}
return static::fromElementType(
$erased_element_type,
$this->is_nullable,
$this->key_type
);
});
}
public function asCallableType(): ?Type
{
if ($this->key_type === self::KEY_INT) {
return null;
}
return CallableArrayType::instance(false);
}
public function asNonFalseyType(): Type
{
return NonEmptyGenericArrayType::fromElementType(
$this->element_type,
false,
$this->key_type
);
}
/**
* Do not use this. Use ArrayType::instance or static::fromElementType
* @unused-param $is_nullable
* @internal
* @deprecated
*/
public static function instance(bool $is_nullable)
{
throw new \AssertionError(static::class . '::' . __FUNCTION__ . ' should not be used');
}
/**
* Overridden in subclasses for non-empty-array and non-empty-list.
*/
public function isDefinitelyNonEmptyArray(): bool
{
return false;
}
/**
* @unused-param $can_reduce_size
* Returns the equivalent (possibly nullable) associative array type for this type.
*/
public function asAssociativeArrayType(bool $can_reduce_size): ArrayType
{
return AssociativeArrayType::fromElementType(
$this->element_type,
$this->is_nullable,
$this->key_type
);
}
/**
* Convert ArrayTypes with integer-only keys to ListType.
*/
public function convertIntegerKeyArrayToList(): ArrayType
{
if ($this->key_type !== GenericArrayType::KEY_INT) {
return $this;
}
if ($this->isDefinitelyNonEmptyArray()) {
return NonEmptyListType::fromElementType($this->element_type, $this->is_nullable, $this->key_type);
}
return ListType::fromElementType($this->element_type, $this->is_nullable, $this->key_type);
}
public function isSubtypeOf(Type $type): bool
{
// TODO more specific
if (!$this->canCastToType($type)) {
return false;
}
// TODO: Also account for iterables
if ($type instanceof GenericArrayType) {
if (!$this->element_type->isSubtypeOf($type->element_type)) {
return false;
}
} elseif ($type instanceof GenericIterableType) {
if (!$this->element_type->asPHPDocUnionType()->hasSubtypeOf($type->getElementUnionType())) {
return false;
}
}
return true;
}
public function getTypesRecursively(): Generator
{
yield $this;
yield from $this->element_type->getTypesRecursively();
}
}