1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +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:
orklah 2023-04-16 21:14:21 +02:00 committed by GitHub
commit 24dc5d49b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 295 additions and 137 deletions

View File

@ -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>

View File

@ -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,

View File

@ -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,
$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,
/**
* @var array<TKeyedArray> $intersection_types
* @var TKeyedArray $first_type
* @var TKeyedArray $last_type
*/
return self::getTypeFromKeyedArrays(
$codebase,
$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[$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;
}
$keyed_intersection_types = self::extractKeyedIntersectionTypes(
$codebase,
$intersection_types,
);
$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,
);
}
}

View File

@ -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;
}

View File

@ -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;
}
/**

View File

@ -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(

View File

@ -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(
'&',

View File

@ -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' => [],
],
];
}