src/Phan/Language/Type/GenericIterableType.php
<?php
declare(strict_types=1);
namespace Phan\Language\Type;
use Closure;
use Generator;
use Phan\CodeBase;
use Phan\Config;
use Phan\Debug\Frame;
use Phan\Exception\RecursionDepthException;
use Phan\Language\Context;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\Type;
use Phan\Language\UnionType;
use Phan\Language\UnionTypeBuilder;
use function count;
use function json_encode;
/**
* Phan's representation of the type `iterable<KeyType,ValueType>`
* @phan-pure
*/
final class GenericIterableType extends IterableType
{
/** @phan-override */
public const NAME = 'iterable';
/**
* @var UnionType the union type of the keys of this iterable.
*/
private $key_union_type;
/**
* @var UnionType the union type of the elements of this iterable.
*/
private $element_union_type;
protected function __construct(UnionType $key_union_type, UnionType $element_union_type, bool $is_nullable)
{
parent::__construct('\\', self::NAME, [], $is_nullable);
$this->key_union_type = $key_union_type;
$this->element_union_type = $element_union_type;
}
/**
* @return UnionType returns the iterable key's union type, because this is a subtype of iterable.
* Other classes in the `Type` type hierarchy may return null.
*/
public function getKeyUnionType(): UnionType
{
return $this->key_union_type;
}
/**
* Returns `GenericArrayType::KEY_*` for the union type of this iterable's keys.
* e.g. for `iterable<string, stdClass>`, returns KEY_STRING
*/
public function getKeyType(): int
{
return $this->memoize(__METHOD__, function (): int {
return GenericArrayType::keyTypeFromUnionTypeValues($this->key_union_type);
});
}
/**
* @return UnionType returns the union type of possible element types.
*/
public function getElementUnionType(): UnionType
{
return $this->element_union_type;
}
public function genericArrayElementUnionType(): UnionType
{
return $this->element_union_type;
}
/**
* @unused-param $code_base
* @return UnionType returns the iterable key's union type
* @phan-override
*
* @see self::getKeyUnionType()
*/
public function iterableKeyUnionType(CodeBase $code_base): UnionType
{
return $this->key_union_type;
}
/**
* @unused-param $code_base
* @return UnionType returns the iterable value's union type
* @phan-override
*
* @see self::getElementUnionType()
*/
public function iterableValueUnionType(CodeBase $code_base): UnionType
{
return $this->element_union_type;
}
/**
* Returns a nullable/non-nullable GenericIterableType
* representing `iterable<$key_union_type, $element_union_type>`
*/
public static function fromKeyAndValueTypes(UnionType $key_union_type, UnionType $element_union_type, bool $is_nullable): GenericIterableType
{
static $cache = [];
$key = ($is_nullable ? '?' : '') . json_encode($key_union_type->generateUniqueId()) . ':' . json_encode($element_union_type->generateUniqueId());
return $cache[$key] ?? ($cache[$key] = new self($key_union_type, $element_union_type, $is_nullable));
}
public function canCastToNonNullableType(Type $type): bool
{
if ($type instanceof GenericIterableType) {
// TODO: Account for scalar key casting config?
if (!$this->key_union_type->canCastToUnionType($type->key_union_type)) {
return false;
}
if (!$this->element_union_type->canCastToUnionType($type->element_union_type)) {
return false;
}
return true;
}
return parent::canCastToNonNullableType($type);
}
public function canCastToNonNullableTypeWithoutConfig(Type $type): bool
{
if ($type instanceof GenericIterableType) {
if (!$this->key_union_type->canCastToUnionTypeWithoutConfig($type->key_union_type)) {
return false;
}
if (!$this->element_union_type->canCastToUnionTypeWithoutConfig($type->element_union_type)) {
return false;
}
return true;
}
return parent::canCastToNonNullableTypeWithoutConfig($type);
}
/**
* Returns true for `T` and `T[]` and `\MyClass<T>`, but not `\MyClass<\OtherClass>` or `false`
*/
public function hasTemplateTypeRecursive(): bool
{
return $this->key_union_type->hasTemplateTypeRecursive() || $this->element_union_type->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 {
$new_key_type = $this->key_union_type->withTemplateParameterTypeMap($template_parameter_type_map);
$new_element_type = $this->element_union_type->withTemplateParameterTypeMap($template_parameter_type_map);
if ($new_element_type === $this->element_union_type &&
$new_key_type === $this->key_union_type) {
return $this->asPHPDocUnionType();
}
return self::fromKeyAndValueTypes($new_key_type, $new_element_type, $this->is_nullable)->asPHPDocUnionType();
}
public function __toString(): string
{
$string = $this->element_union_type->__toString();
if (!$this->key_union_type->isEmpty()) {
$string = $this->key_union_type->__toString() . ',' . $string;
}
$string = "iterable<$string>";
if ($this->is_nullable) {
$string = '?' . $string;
}
return $string;
}
/**
* 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_union_type->getTemplateTypeExtractorClosure($code_base, $template_type);
if ($closure) {
// If a function expects T[], then T is the generic array element type of the passed in union type
$element_closure = static function (UnionType $type, Context $context) use ($code_base, $closure): UnionType {
return $closure($type->iterableValueUnionType($code_base), $context);
};
} else {
$element_closure = null;
}
$closure = $this->key_union_type->getTemplateTypeExtractorClosure($code_base, $template_type);
if ($closure) {
$key_closure = static function (UnionType $type, Context $context) use ($code_base, $closure): UnionType {
return $closure($type->iterableKeyUnionType($code_base), $context);
};
} else {
$key_closure = null;
}
return TemplateType::combineParameterClosures($key_closure, $element_closure);
}
/**
* Returns the corresponding type that would be used in a signature
* @override
*/
public function asSignatureType(): Type
{
return IterableType::instance($this->is_nullable);
}
public function asArrayType(): ?Type
{
$key_type = GenericArrayType::keyTypeFromUnionTypeValues($this->key_union_type);
if ($this->element_union_type->typeCount() === 1) {
$element_type = $this->element_union_type->getTypeSet()[0];
} else {
if ($key_type === GenericArrayType::KEY_MIXED) {
return ArrayType::instance(false);
}
$element_type = MixedType::instance(false);
}
return GenericArrayType::fromElementType($element_type, false, $key_type);
}
/**
* Returns a type where all referenced union types (e.g. in generic arrays) have real type sets removed.
*/
public function withErasedUnionTypes(): Type
{
return $this->memoize(__METHOD__, function (): Type {
$erased_element_union_type = $this->element_union_type->eraseRealTypeSetRecursively();
$erased_key_union_type = $this->key_union_type->eraseRealTypeSetRecursively();
if ($erased_key_union_type === $this->key_union_type && $erased_element_union_type === $this->element_union_type) {
return $this;
}
return self::fromKeyAndValueTypes($this->key_union_type, $erased_element_union_type, $this->is_nullable);
});
}
/**
* @override
*/
public function withIsNullable(bool $is_nullable): Type
{
if ($is_nullable === $this->is_nullable) {
return $this;
}
return self::fromKeyAndValueTypes(
$this->key_union_type,
$this->element_union_type,
$is_nullable
);
}
public function getTypesRecursively(): Generator
{
yield $this;
yield from $this->key_union_type->getTypesRecursively();
yield from $this->element_union_type->getTypesRecursively();
}
/**
* @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.
*
* TODO: Support expanding key types. Support better checks for casting from Traversable/array.
* Copy those fixes to asExpandedTypesPreservingTemplate().
* @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 {
$element_types = $this->element_union_type->getTypeSet();
if (count($element_types) >= 2) {
$union_type_builder = new UnionTypeBuilder();
foreach ($element_types as $element_type) {
$new_type = self::fromKeyAndValueTypes($this->key_union_type, $element_type->asPHPDocUnionType(), $this->is_nullable);
$union_type_builder->addUnionType($new_type->asExpandedTypes($code_base, $recursion_depth + 1));
}
return $union_type_builder->getPHPDocUnionType();
}
$element_type = \reset($element_types);
$union_type = $this->asPHPDocUnionType();
if (!$element_type instanceof Type) {
return $union_type;
}
$union_type = $this->asPHPDocUnionType();
$recursive_union_type_builder = new UnionTypeBuilder();
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(self::fromKeyAndValueTypes($this->key_union_type, $type->asPHPDocUnionType(), $this->is_nullable));
}
// Recurse up the tree to include all types
$representation = $this->__toString();
try {
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);
}
}
} catch (RecursionDepthException $_) {
return GenericIterableType::fromKeyAndValueTypes($this->key_union_type, UnionType::fromFullyQualifiedPHPDocString('mixed'), $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 {
$element_types = $this->element_union_type->getTypeSet();
if (count($element_types) >= 2) {
$union_type_builder = new UnionTypeBuilder();
foreach ($element_types as $element_type) {
$new_type = self::fromKeyAndValueTypes($this->key_union_type, $element_type->asPHPDocUnionType(), $this->is_nullable);
$union_type_builder->addUnionType($new_type->asExpandedTypesPreservingTemplate($code_base, $recursion_depth + 1));
}
return $union_type_builder->getPHPDocUnionType();
}
$element_type = \reset($element_types);
$union_type = $this->asPHPDocUnionType();
if (!$element_type instanceof Type) {
return $union_type;
}
$union_type = $this->asPHPDocUnionType();
$recursive_union_type_builder = new UnionTypeBuilder();
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(self::fromKeyAndValueTypes($this->key_union_type, $type->asPHPDocUnionType(), $this->is_nullable));
}
// Recurse up the tree to include all types
$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 GenericIterableType::fromKeyAndValueTypes($this->key_union_type, UnionType::fromFullyQualifiedPHPDocString('mixed'), $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(
GenericIterableType::fromKeyAndValueTypes($this->key_union_type, $alias_fqsen->asType()->asPHPDocUnionType(), $this->is_nullable)
);
}
}
public function getReferencedClasses(): Generator
{
yield from $this->key_union_type->getReferencedClasses();
yield from $this->element_union_type->getReferencedClasses();
}
}