mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
Fix #1813 - convert object&Foo into Foo after template resolution
This commit is contained in:
parent
7155ed4e06
commit
0246f600f4
@ -580,7 +580,9 @@ class ForeachAnalyzer
|
||||
}
|
||||
|
||||
foreach ($iterator_atomic_types as $iterator_atomic_type) {
|
||||
if ($iterator_atomic_type instanceof Type\Atomic\TTemplateParam) {
|
||||
if ($iterator_atomic_type instanceof Type\Atomic\TTemplateParam
|
||||
|| $iterator_atomic_type instanceof Type\Atomic\TObjectWithProperties
|
||||
) {
|
||||
throw new \UnexpectedValueException('Shouldn’t get a generic param here');
|
||||
}
|
||||
|
||||
|
@ -2823,9 +2823,12 @@ class CallAnalyzer
|
||||
|
||||
if ($type_part->extra_types) {
|
||||
foreach ($type_part->extra_types as $extra_type) {
|
||||
if ($extra_type instanceof Type\Atomic\TTemplateParam) {
|
||||
if ($extra_type instanceof Type\Atomic\TTemplateParam
|
||||
|| $extra_type instanceof Type\Atomic\TObjectWithProperties
|
||||
) {
|
||||
throw new \UnexpectedValueException('Shouldn’t get a generic param here');
|
||||
}
|
||||
|
||||
$method_id .= '&' . $extra_type->value . '::' . $method_name_arg->value;
|
||||
}
|
||||
}
|
||||
|
@ -452,6 +452,8 @@ class TypeAnalyzer
|
||||
foreach ($intersection_container_types as $intersection_container_type) {
|
||||
if ($intersection_container_type instanceof TIterable) {
|
||||
$intersection_container_type_lower = 'iterable';
|
||||
} elseif ($intersection_container_type instanceof TObjectWithProperties) {
|
||||
$intersection_container_type_lower = 'object';
|
||||
} elseif ($intersection_container_type instanceof TTemplateParam) {
|
||||
if ($intersection_container_type->as->isMixed()) {
|
||||
continue;
|
||||
@ -485,6 +487,8 @@ class TypeAnalyzer
|
||||
foreach ($intersection_input_types as $intersection_input_type) {
|
||||
if ($intersection_input_type instanceof TIterable) {
|
||||
$intersection_input_type_lower = 'iterable';
|
||||
} elseif ($intersection_input_type instanceof TObjectWithProperties) {
|
||||
$intersection_input_type_lower = 'object';
|
||||
} elseif ($intersection_input_type instanceof TTemplateParam) {
|
||||
if ($intersection_input_type->as->isMixed()) {
|
||||
continue;
|
||||
|
@ -99,7 +99,7 @@ class TypeCombination
|
||||
private $floats = [];
|
||||
|
||||
/**
|
||||
* @var array<string, TNamedObject|TTemplateParam|TIterable>|null
|
||||
* @var array<string, TNamedObject|TTemplateParam|TIterable|TObject>|null
|
||||
*/
|
||||
private $extra_types;
|
||||
|
||||
@ -654,7 +654,11 @@ class TypeCombination
|
||||
}
|
||||
}
|
||||
|
||||
if ($type instanceof TNamedObject || $type instanceof TTemplateParam || $type instanceof TIterable) {
|
||||
if ($type instanceof TNamedObject
|
||||
|| $type instanceof TTemplateParam
|
||||
|| $type instanceof TIterable
|
||||
|| $type instanceof Type\Atomic\TObjectWithProperties
|
||||
) {
|
||||
if ($type->extra_types) {
|
||||
$combination->extra_types = array_merge(
|
||||
$combination->extra_types ?: [],
|
||||
|
@ -395,9 +395,10 @@ abstract class Type
|
||||
$keyed_intersection_types = [];
|
||||
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
if (!$intersection_type instanceof TNamedObject
|
||||
if (!$intersection_type instanceof TIterable
|
||||
&& !$intersection_type instanceof TNamedObject
|
||||
&& !$intersection_type instanceof TTemplateParam
|
||||
&& !$intersection_type instanceof TIterable
|
||||
&& !$intersection_type instanceof TObjectWithProperties
|
||||
) {
|
||||
throw new TypeParseTreeException(
|
||||
'Intersection types must all be objects, ' . get_class($intersection_type) . ' provided'
|
||||
@ -1435,10 +1436,12 @@ abstract class Type
|
||||
foreach ($type_2->getTypes() as $type_2_atomic) {
|
||||
if (($type_1_atomic instanceof TIterable
|
||||
|| $type_1_atomic instanceof TNamedObject
|
||||
|| $type_1_atomic instanceof TTemplateParam)
|
||||
|| $type_1_atomic instanceof TTemplateParam
|
||||
|| $type_1_atomic instanceof TObjectWithProperties)
|
||||
&& ($type_2_atomic instanceof TIterable
|
||||
|| $type_2_atomic instanceof TNamedObject
|
||||
|| $type_2_atomic instanceof TTemplateParam)
|
||||
|| $type_2_atomic instanceof TTemplateParam
|
||||
|| $type_2_atomic instanceof TObjectWithProperties)
|
||||
) {
|
||||
if (!$type_1_atomic->extra_types) {
|
||||
$type_1_atomic->extra_types = [];
|
||||
|
@ -930,6 +930,7 @@ abstract class Atomic
|
||||
if ($this instanceof TNamedObject
|
||||
|| $this instanceof TTemplateParam
|
||||
|| $this instanceof TIterable
|
||||
|| $this instanceof Type\Atomic\TObjectWithProperties
|
||||
) {
|
||||
if ($this->extra_types) {
|
||||
foreach ($this->extra_types as &$type) {
|
||||
|
@ -8,21 +8,19 @@ use Psalm\Codebase;
|
||||
trait HasIntersectionTrait
|
||||
{
|
||||
/**
|
||||
* @var array<string, TNamedObject|TTemplateParam|TIterable>|null
|
||||
* @var array<string, TNamedObject|TTemplateParam|TIterable|TObjectWithProperties>|null
|
||||
*/
|
||||
public $extra_types;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $aliased_classes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getNamespacedIntersectionTypes(
|
||||
?string $namespace,
|
||||
array $aliased_classes,
|
||||
?string $this_class,
|
||||
bool $use_phpdoc_format
|
||||
) {
|
||||
) : string {
|
||||
if (!$this->extra_types) {
|
||||
return '';
|
||||
}
|
||||
@ -31,7 +29,7 @@ trait HasIntersectionTrait
|
||||
'&',
|
||||
array_map(
|
||||
/**
|
||||
* @param TNamedObject|TTemplateParam|TIterable $extra_type
|
||||
* @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties $extra_type
|
||||
* @return string
|
||||
*/
|
||||
function (Atomic $extra_type) use (
|
||||
@ -53,29 +51,25 @@ trait HasIntersectionTrait
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TNamedObject $type
|
||||
*
|
||||
* @return void
|
||||
* @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties $type
|
||||
*/
|
||||
public function addIntersectionType(TNamedObject $type)
|
||||
public function addIntersectionType(Type\Atomic $type) : void
|
||||
{
|
||||
$this->extra_types[$type->getKey()] = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, TNamedObject|TTemplateParam|TIterable>|null
|
||||
* @return array<string, TNamedObject|TTemplateParam|TIterable|TObjectWithProperties>|null
|
||||
*/
|
||||
public function getIntersectionTypes()
|
||||
public function getIntersectionTypes() : ?array
|
||||
{
|
||||
return $this->extra_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function replaceIntersectionTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase)
|
||||
public function replaceIntersectionTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase) : void
|
||||
{
|
||||
if (!$this->extra_types) {
|
||||
return;
|
||||
|
@ -6,6 +6,8 @@ use Psalm\Type\Atomic;
|
||||
|
||||
class TObjectWithProperties extends TObject
|
||||
{
|
||||
use HasIntersectionTrait;
|
||||
|
||||
/**
|
||||
* @var array<string|int, Union>
|
||||
*/
|
||||
@ -23,6 +25,12 @@ class TObjectWithProperties extends TObject
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
$extra_types = '';
|
||||
|
||||
if ($this->extra_types) {
|
||||
$extra_types = '&' . implode('&', $this->extra_types);
|
||||
}
|
||||
|
||||
return 'object{' .
|
||||
implode(
|
||||
', ',
|
||||
@ -40,11 +48,17 @@ class TObjectWithProperties extends TObject
|
||||
$this->properties
|
||||
)
|
||||
) .
|
||||
'}';
|
||||
'}' . $extra_types;
|
||||
}
|
||||
|
||||
public function getId()
|
||||
{
|
||||
$extra_types = '';
|
||||
|
||||
if ($this->extra_types) {
|
||||
$extra_types = '&' . implode('&', $this->extra_types);
|
||||
}
|
||||
|
||||
return 'object{' .
|
||||
implode(
|
||||
', ',
|
||||
@ -62,7 +76,7 @@ class TObjectWithProperties extends TObject
|
||||
$this->properties
|
||||
)
|
||||
) .
|
||||
'}';
|
||||
'}' . $extra_types;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1333,12 +1333,22 @@ class Union
|
||||
}
|
||||
|
||||
if ($atomic_type->extra_types) {
|
||||
foreach ($template_type->getTypes() as $atomic_template_type) {
|
||||
foreach ($template_type->getTypes() as $template_type_key => $atomic_template_type) {
|
||||
if ($atomic_template_type instanceof TNamedObject
|
||||
|| $atomic_template_type instanceof TTemplateParam
|
||||
|| $atomic_template_type instanceof TIterable
|
||||
|| $atomic_template_type instanceof Type\Atomic\TObjectWithProperties
|
||||
) {
|
||||
$atomic_template_type->extra_types = $atomic_type->extra_types;
|
||||
} elseif ($atomic_template_type instanceof Type\Atomic\TObject) {
|
||||
$first_atomic_type = array_shift($atomic_type->extra_types);
|
||||
|
||||
if ($atomic_type->extra_types) {
|
||||
$first_atomic_type->extra_types = $atomic_type->extra_types;
|
||||
}
|
||||
|
||||
$template_type->removeType($template_type_key);
|
||||
$template_type->addType($first_atomic_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2704,6 +2704,78 @@ class TemplateTest extends TestCase
|
||||
return new TestPromise(true);
|
||||
}',
|
||||
],
|
||||
'allowTemplatedIntersectionFirst' => [
|
||||
'<?php
|
||||
class MockObject
|
||||
{
|
||||
public function checkExpectations() : void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-template RequestedType
|
||||
* @psalm-param class-string<RequestedType> $className
|
||||
* @psalm-return RequestedType&MockObject
|
||||
* @psalm-suppress MixedInferredReturnType
|
||||
* @psalm-suppress MixedReturnStatement
|
||||
*/
|
||||
function mock(string $className)
|
||||
{
|
||||
eval(\'"there be dragons"\');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
class A {
|
||||
public function foo() : void {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
*/
|
||||
function useMock(string $className) : void {
|
||||
mock($className)->checkExpectations();
|
||||
}
|
||||
|
||||
mock(A::class)->foo();'
|
||||
],
|
||||
'allowTemplatedIntersectionSecond' => [
|
||||
'<?php
|
||||
class MockObject
|
||||
{
|
||||
public function checkExpectations() : void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-template RequestedType
|
||||
* @psalm-param class-string<RequestedType> $className
|
||||
* @psalm-return MockObject&RequestedType
|
||||
* @psalm-suppress MixedInferredReturnType
|
||||
* @psalm-suppress MixedReturnStatement
|
||||
*/
|
||||
function mock(string $className)
|
||||
{
|
||||
eval(\'"there be dragons"\');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
class A {
|
||||
public function foo() : void {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
*/
|
||||
function useMock(string $className) : void {
|
||||
mock($className)->checkExpectations();
|
||||
}
|
||||
|
||||
mock(A::class)->foo();'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user