mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Merge pull request #9638 from boesing/bugfix/intersection-type-type-alias
Allow to intersect type alias with non-type-aliases
This commit is contained in:
commit
24dc5d49b2
@ -410,7 +410,6 @@
|
||||
<PossiblyUndefinedIntArrayOffset>
|
||||
<code>$const_name</code>
|
||||
<code>$const_name</code>
|
||||
<code>$intersection_types[0]</code>
|
||||
<code><![CDATA[$parse_tree->children[0]]]></code>
|
||||
<code><![CDATA[$parse_tree->condition->children[0]]]></code>
|
||||
<code>array_keys($offset_template_data)[0]</code>
|
||||
|
@ -324,6 +324,7 @@ class TypeExpander
|
||||
];
|
||||
}
|
||||
|
||||
/** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */
|
||||
foreach ($return_type->extra_types ?? [] as $alias) {
|
||||
$more_recursively_fleshed_out_types = self::expandAtomic(
|
||||
$codebase,
|
||||
|
@ -71,6 +71,7 @@ use function array_key_exists;
|
||||
use function array_key_first;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_pop;
|
||||
use function array_shift;
|
||||
use function array_unique;
|
||||
@ -1108,6 +1109,10 @@ class TypeParser
|
||||
$intersection_types[$name] = $atomic_type;
|
||||
}
|
||||
|
||||
if ($intersection_types === []) {
|
||||
return new TMixed();
|
||||
}
|
||||
|
||||
$first_type = reset($intersection_types);
|
||||
$last_type = end($intersection_types);
|
||||
|
||||
@ -1127,133 +1132,24 @@ class TypeParser
|
||||
}
|
||||
|
||||
if ($onlyTKeyedArray) {
|
||||
/** @var non-empty-array<string|int, Union> */
|
||||
$properties = [];
|
||||
|
||||
if ($first_type instanceof TArray) {
|
||||
array_shift($intersection_types);
|
||||
} elseif ($last_type instanceof TArray) {
|
||||
array_pop($intersection_types);
|
||||
}
|
||||
|
||||
$all_sealed = true;
|
||||
|
||||
/** @var TKeyedArray $intersection_type */
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
foreach ($intersection_type->properties as $property => $property_type) {
|
||||
if ($intersection_type->fallback_params !== null) {
|
||||
$all_sealed = false;
|
||||
}
|
||||
|
||||
if (!array_key_exists($property, $properties)) {
|
||||
$properties[$property] = $property_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
$new_type = Type::intersectUnionTypes(
|
||||
$properties[$property],
|
||||
$property_type,
|
||||
/**
|
||||
* @var array<TKeyedArray> $intersection_types
|
||||
* @var TKeyedArray $first_type
|
||||
* @var TKeyedArray $last_type
|
||||
*/
|
||||
return self::getTypeFromKeyedArrays(
|
||||
$codebase,
|
||||
);
|
||||
|
||||
if ($new_type === null) {
|
||||
throw new TypeParseTreeException(
|
||||
'Incompatible intersection types for "' . $property . '", '
|
||||
. $properties[$property] . ' and ' . $property_type
|
||||
. ' provided',
|
||||
);
|
||||
}
|
||||
$properties[$property] = $new_type;
|
||||
}
|
||||
}
|
||||
|
||||
$first_or_last_type = $first_type instanceof TArray
|
||||
? $first_type
|
||||
: ($last_type instanceof TArray ? $last_type : null);
|
||||
|
||||
$fallback_params = null;
|
||||
|
||||
if ($first_or_last_type !== null) {
|
||||
$fallback_params = [
|
||||
$first_or_last_type->type_params[0],
|
||||
$first_or_last_type->type_params[1],
|
||||
];
|
||||
} elseif (!$all_sealed) {
|
||||
$fallback_params = [Type::getArrayKey(), Type::getMixed()];
|
||||
}
|
||||
|
||||
return new TKeyedArray(
|
||||
$properties,
|
||||
null,
|
||||
$fallback_params,
|
||||
false,
|
||||
$intersection_types,
|
||||
$first_type,
|
||||
$last_type,
|
||||
$from_docblock,
|
||||
);
|
||||
}
|
||||
|
||||
$keyed_intersection_types = [];
|
||||
|
||||
if ($intersection_types[0] instanceof TTypeAlias) {
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
if (!$intersection_type instanceof TTypeAlias) {
|
||||
throw new TypeParseTreeException(
|
||||
'Intersection types with a type alias can only be comprised of other type aliases, '
|
||||
. get_class($intersection_type) . ' provided',
|
||||
$keyed_intersection_types = self::extractKeyedIntersectionTypes(
|
||||
$codebase,
|
||||
$intersection_types,
|
||||
);
|
||||
}
|
||||
|
||||
$keyed_intersection_types[$intersection_type->getKey()] = $intersection_type;
|
||||
}
|
||||
|
||||
$first_type = array_shift($keyed_intersection_types);
|
||||
|
||||
if ($keyed_intersection_types) {
|
||||
return $first_type->setIntersectionTypes($keyed_intersection_types);
|
||||
}
|
||||
|
||||
return $first_type;
|
||||
}
|
||||
|
||||
$callable_intersection = null;
|
||||
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
if ($intersection_type instanceof TIterable
|
||||
|| $intersection_type instanceof TNamedObject
|
||||
|| $intersection_type instanceof TTemplateParam
|
||||
|| $intersection_type instanceof TObjectWithProperties
|
||||
) {
|
||||
$keyed_intersection_types[self::extractIntersectionKey($intersection_type)] = $intersection_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (get_class($intersection_type) === TObject::class) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($intersection_type instanceof TCallable) {
|
||||
if ($callable_intersection !== null) {
|
||||
throw new TypeParseTreeException(
|
||||
'The intersection type must not contain more than one callable type!',
|
||||
);
|
||||
}
|
||||
$callable_intersection = $intersection_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new TypeParseTreeException(
|
||||
'Intersection types must be all objects, '
|
||||
. get_class($intersection_type) . ' provided',
|
||||
);
|
||||
}
|
||||
|
||||
if ($callable_intersection !== null) {
|
||||
$callable_object_type = new TCallableObject(
|
||||
$callable_intersection->from_docblock,
|
||||
$callable_intersection,
|
||||
);
|
||||
|
||||
$keyed_intersection_types[self::extractIntersectionKey($callable_object_type)] = $callable_object_type;
|
||||
}
|
||||
|
||||
$intersect_static = false;
|
||||
|
||||
@ -1262,12 +1158,29 @@ class TypeParser
|
||||
$intersect_static = true;
|
||||
}
|
||||
|
||||
if (!$keyed_intersection_types && $intersect_static) {
|
||||
if ($keyed_intersection_types === [] && $intersect_static) {
|
||||
return new TNamedObject('static', false, false, [], $from_docblock);
|
||||
}
|
||||
|
||||
$first_type = array_shift($keyed_intersection_types);
|
||||
|
||||
// Keyed array intersection are merged together and are not combinable with object-types
|
||||
if ($first_type instanceof TKeyedArray) {
|
||||
// assume all types are keyed arrays
|
||||
array_unshift($keyed_intersection_types, $first_type);
|
||||
/** @var TKeyedArray $last_type */
|
||||
$last_type = end($keyed_intersection_types);
|
||||
|
||||
/** @var array<TKeyedArray> $keyed_intersection_types */
|
||||
return self::getTypeFromKeyedArrays(
|
||||
$codebase,
|
||||
$keyed_intersection_types,
|
||||
$first_type,
|
||||
$last_type,
|
||||
$from_docblock,
|
||||
);
|
||||
}
|
||||
|
||||
if ($intersect_static
|
||||
&& $first_type instanceof TNamedObject
|
||||
) {
|
||||
@ -1275,6 +1188,7 @@ class TypeParser
|
||||
}
|
||||
|
||||
if ($keyed_intersection_types) {
|
||||
/** @var non-empty-array<string,TIterable|TNamedObject|TCallableObject|TTemplateParam|TObjectWithProperties> $keyed_intersection_types */
|
||||
return $first_type->setIntersectionTypes($keyed_intersection_types);
|
||||
}
|
||||
|
||||
@ -1570,12 +1484,212 @@ class TypeParser
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TNamedObject|TObjectWithProperties|TCallableObject|TIterable|TTemplateParam $intersection_type
|
||||
* @param TNamedObject|TObjectWithProperties|TCallableObject|TIterable|TTemplateParam|TKeyedArray $intersection_type
|
||||
*/
|
||||
private static function extractIntersectionKey(Atomic $intersection_type): string
|
||||
{
|
||||
return $intersection_type instanceof TIterable
|
||||
return $intersection_type instanceof TIterable || $intersection_type instanceof TKeyedArray
|
||||
? $intersection_type->getId()
|
||||
: $intersection_type->getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-array<Atomic> $intersection_types
|
||||
* @return non-empty-array<string,TIterable|TNamedObject|TCallableObject|TTemplateParam|TObjectWithProperties|TKeyedArray>
|
||||
*/
|
||||
private static function extractKeyedIntersectionTypes(
|
||||
Codebase $codebase,
|
||||
array $intersection_types
|
||||
): array {
|
||||
$keyed_intersection_types = [];
|
||||
$callable_intersection = null;
|
||||
$any_object_type_found = $any_array_found = false;
|
||||
|
||||
$normalized_intersection_types = self::resolveTypeAliases(
|
||||
$codebase,
|
||||
$intersection_types,
|
||||
);
|
||||
|
||||
foreach ($normalized_intersection_types as $intersection_type) {
|
||||
if ($intersection_type instanceof TKeyedArray
|
||||
&& !$intersection_type instanceof TCallableKeyedArray
|
||||
) {
|
||||
$any_array_found = true;
|
||||
|
||||
if ($any_object_type_found) {
|
||||
throw new TypeParseTreeException(
|
||||
'The intersection type must not mix array and object types!',
|
||||
);
|
||||
}
|
||||
|
||||
$keyed_intersection_types[self::extractIntersectionKey($intersection_type)] = $intersection_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
$any_object_type_found = true;
|
||||
|
||||
if ($intersection_type instanceof TIterable
|
||||
|| $intersection_type instanceof TNamedObject
|
||||
|| $intersection_type instanceof TTemplateParam
|
||||
|| $intersection_type instanceof TObjectWithProperties
|
||||
) {
|
||||
$keyed_intersection_types[self::extractIntersectionKey($intersection_type)] = $intersection_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (get_class($intersection_type) === TObject::class) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($intersection_type instanceof TCallable) {
|
||||
if ($callable_intersection !== null) {
|
||||
throw new TypeParseTreeException(
|
||||
'The intersection type must not contain more than one callable type!',
|
||||
);
|
||||
}
|
||||
$callable_intersection = $intersection_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new TypeParseTreeException(
|
||||
'Intersection types must be all objects, '
|
||||
. get_class($intersection_type) . ' provided',
|
||||
);
|
||||
}
|
||||
|
||||
if ($callable_intersection !== null) {
|
||||
$callable_object_type = new TCallableObject(
|
||||
$callable_intersection->from_docblock,
|
||||
$callable_intersection,
|
||||
);
|
||||
|
||||
$keyed_intersection_types[self::extractIntersectionKey($callable_object_type)] = $callable_object_type;
|
||||
}
|
||||
|
||||
if ($any_object_type_found && $any_array_found) {
|
||||
throw new TypeParseTreeException(
|
||||
'Intersection types must be all objects or all keyed array.',
|
||||
);
|
||||
}
|
||||
|
||||
assert($keyed_intersection_types !== []);
|
||||
|
||||
return $keyed_intersection_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Atomic> $intersection_types
|
||||
* @return array<Atomic>
|
||||
*/
|
||||
private static function resolveTypeAliases(Codebase $codebase, array $intersection_types): array
|
||||
{
|
||||
$normalized_intersection_types = [];
|
||||
$modified = false;
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
if (!$intersection_type instanceof TTypeAlias) {
|
||||
$normalized_intersection_types[] = [$intersection_type];
|
||||
continue;
|
||||
}
|
||||
|
||||
$modified = true;
|
||||
|
||||
$normalized_intersection_types[] = TypeExpander::expandAtomic(
|
||||
$codebase,
|
||||
$intersection_type,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if ($modified === false) {
|
||||
return $intersection_types;
|
||||
}
|
||||
|
||||
return self::resolveTypeAliases(
|
||||
$codebase,
|
||||
array_merge(...$normalized_intersection_types),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<TKeyedArray> $intersection_types
|
||||
* @param TKeyedArray|TArray $first_type
|
||||
* @param TKeyedArray|TArray $last_type
|
||||
*/
|
||||
private static function getTypeFromKeyedArrays(
|
||||
Codebase $codebase,
|
||||
array $intersection_types,
|
||||
Atomic $first_type,
|
||||
Atomic $last_type,
|
||||
bool $from_docblock
|
||||
): Atomic {
|
||||
/** @var non-empty-array<string|int, Union> */
|
||||
$properties = [];
|
||||
|
||||
if ($first_type instanceof TArray) {
|
||||
array_shift($intersection_types);
|
||||
} elseif ($last_type instanceof TArray) {
|
||||
array_pop($intersection_types);
|
||||
}
|
||||
|
||||
$all_sealed = true;
|
||||
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
foreach ($intersection_type->properties as $property => $property_type) {
|
||||
if ($intersection_type->fallback_params !== null) {
|
||||
$all_sealed = false;
|
||||
}
|
||||
|
||||
if (!array_key_exists($property, $properties)) {
|
||||
$properties[$property] = $property_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
$new_type = Type::intersectUnionTypes(
|
||||
$properties[$property],
|
||||
$property_type,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
if ($new_type === null) {
|
||||
throw new TypeParseTreeException(
|
||||
'Incompatible intersection types for "' . $property . '", '
|
||||
. $properties[$property] . ' and ' . $property_type
|
||||
. ' provided',
|
||||
);
|
||||
}
|
||||
$properties[$property] = $new_type;
|
||||
}
|
||||
}
|
||||
|
||||
$first_or_last_type = $first_type instanceof TArray
|
||||
? $first_type
|
||||
: ($last_type instanceof TArray ? $last_type : null);
|
||||
|
||||
$fallback_params = null;
|
||||
|
||||
if ($first_or_last_type !== null) {
|
||||
$fallback_params = [
|
||||
$first_or_last_type->type_params[0],
|
||||
$first_or_last_type->type_params[1],
|
||||
];
|
||||
} elseif (!$all_sealed) {
|
||||
$fallback_params = [Type::getArrayKey(), Type::getMixed()];
|
||||
}
|
||||
|
||||
return new TKeyedArray(
|
||||
$properties,
|
||||
null,
|
||||
$fallback_params,
|
||||
false,
|
||||
$from_docblock,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -78,11 +78,10 @@ trait CallableTrait
|
||||
$cloned->is_pure = $is_pure;
|
||||
return $cloned;
|
||||
}
|
||||
public function getKey(bool $include_extra = true): string
|
||||
|
||||
public function getParamString(): string
|
||||
{
|
||||
$param_string = '';
|
||||
$return_type_string = '';
|
||||
|
||||
if ($this->params !== null) {
|
||||
$param_string .= '(';
|
||||
foreach ($this->params as $i => $param) {
|
||||
@ -96,12 +95,27 @@ trait CallableTrait
|
||||
$param_string .= ')';
|
||||
}
|
||||
|
||||
return $param_string;
|
||||
}
|
||||
|
||||
public function getReturnTypeString(): string
|
||||
{
|
||||
$return_type_string = '';
|
||||
|
||||
if ($this->return_type !== null) {
|
||||
$return_type_multiple = count($this->return_type->getAtomicTypes()) > 1;
|
||||
$return_type_string = ':' . ($return_type_multiple ? '(' : '')
|
||||
. $this->return_type->getId() . ($return_type_multiple ? ')' : '');
|
||||
}
|
||||
|
||||
return $return_type_string;
|
||||
}
|
||||
|
||||
public function getKey(bool $include_extra = true): string
|
||||
{
|
||||
$param_string = $this->getParamString();
|
||||
$return_type_string = $this->getReturnTypeString();
|
||||
|
||||
return ($this->is_pure ? 'pure-' : ($this->is_pure === null ? '' : 'impure-'))
|
||||
. $this->value . $param_string . $return_type_string;
|
||||
}
|
||||
|
@ -21,7 +21,12 @@ final class TCallableObject extends TObject
|
||||
|
||||
public function getKey(bool $include_extra = true): string
|
||||
{
|
||||
return 'callable-object';
|
||||
$key = 'callable-object';
|
||||
if ($this->callable !== null) {
|
||||
$key .= $this->callable->getParamString() . $this->callable->getReturnTypeString();
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,10 +10,6 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer;
|
||||
use Psalm\Internal\Type\TypeCombiner;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TLiteralClassString;
|
||||
use Psalm\Type\Atomic\TLiteralInt;
|
||||
use Psalm\Type\Atomic\TNonEmptyArray;
|
||||
use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
|
||||
@ -49,7 +45,7 @@ class TKeyedArray extends Atomic
|
||||
/**
|
||||
* If the shape has fallback params then they are here
|
||||
*
|
||||
* @var ?list{Union, Union}
|
||||
* @var array{Union, Union}|null
|
||||
*/
|
||||
public $fallback_params;
|
||||
|
||||
@ -67,7 +63,7 @@ class TKeyedArray extends Atomic
|
||||
* Constructs a new instance of a generic type
|
||||
*
|
||||
* @param non-empty-array<string|int, Union> $properties
|
||||
* @param ?list{Union, Union} $fallback_params
|
||||
* @param array{Union, Union}|null $fallback_params
|
||||
* @param array<string, bool> $class_strings
|
||||
*/
|
||||
public function __construct(
|
||||
|
@ -14,6 +14,9 @@ final class TTypeAlias extends Atomic
|
||||
{
|
||||
/**
|
||||
* @var array<string, TTypeAlias>|null
|
||||
* @deprecated type aliases are resolved within {@see TypeParser::resolveTypeAliases()} and therefore the
|
||||
* referencing type(s) are part of other intersection types. The intersection types are not set anymore
|
||||
* and with v6 this property along with its related methods will get removed.
|
||||
*/
|
||||
public $extra_types;
|
||||
|
||||
@ -30,14 +33,19 @@ final class TTypeAlias extends Atomic
|
||||
{
|
||||
$this->declaring_fq_classlike_name = $declaring_fq_classlike_name;
|
||||
$this->alias_name = $alias_name;
|
||||
/** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */
|
||||
$this->extra_types = $extra_types;
|
||||
parent::__construct(true);
|
||||
}
|
||||
/**
|
||||
* @param array<string, TTypeAlias>|null $extra_types
|
||||
* @deprecated type aliases are resolved within {@see TypeParser::resolveTypeAliases()} and therefore the
|
||||
* referencing type(s) are part of other intersection types. This method will get removed with v6.
|
||||
* @psalm-suppress PossiblyUnusedMethod For backwards compatibility, we have to keep this here.
|
||||
*/
|
||||
public function setIntersectionTypes(?array $extra_types): self
|
||||
{
|
||||
/** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */
|
||||
if ($extra_types === $this->extra_types) {
|
||||
return $this;
|
||||
}
|
||||
@ -55,6 +63,7 @@ final class TTypeAlias extends Atomic
|
||||
|
||||
public function getId(bool $exact = true, bool $nested = false): string
|
||||
{
|
||||
/** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */
|
||||
if ($this->extra_types) {
|
||||
return $this->getKey() . '&' . implode(
|
||||
'&',
|
||||
|
@ -114,6 +114,26 @@ final class IntersectionTypeTest extends TestCase
|
||||
'assertions' => [],
|
||||
'ignored_issues' => ['UnsafeInstantiation', 'MixedMethodCall'],
|
||||
],
|
||||
'classStringOfImportedCallableTypeIntersection' => [
|
||||
'code' => '<?php
|
||||
/** @psalm-type CallableType = callable */
|
||||
class Bar
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/** @psalm-import-type CallableType from Bar */
|
||||
class Foo
|
||||
{
|
||||
/**
|
||||
* @param class-string<object&CallableType> $className
|
||||
*/
|
||||
function takesCallableObject(string $className): void {}
|
||||
}
|
||||
',
|
||||
'assertions' => [],
|
||||
'ignored_issues' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user