From 0246f600f4185388287f1467d1ddceb3affb163a Mon Sep 17 00:00:00 2001 From: Brown Date: Wed, 19 Jun 2019 12:00:07 -0400 Subject: [PATCH] Fix #1813 - convert object&Foo into Foo after template resolution --- .../Statements/Block/ForeachAnalyzer.php | 4 +- .../Statements/Expression/CallAnalyzer.php | 5 +- src/Psalm/Internal/Analyzer/TypeAnalyzer.php | 4 ++ src/Psalm/Internal/Type/TypeCombination.php | 8 ++- src/Psalm/Type.php | 11 +-- src/Psalm/Type/Atomic.php | 1 + .../Type/Atomic/HasIntersectionTrait.php | 22 +++--- .../Type/Atomic/TObjectWithProperties.php | 18 ++++- src/Psalm/Type/Union.php | 12 +++- tests/Template/TemplateTest.php | 72 +++++++++++++++++++ 10 files changed, 132 insertions(+), 25 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index aa8fc5819..088c6b313 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -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'); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 955225d72..7c172486e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -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; } } diff --git a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php index 1e47de7ad..cbb4bd18a 100644 --- a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php @@ -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; diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index 1c159e87d..40a6eca40 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -99,7 +99,7 @@ class TypeCombination private $floats = []; /** - * @var array|null + * @var array|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 ?: [], diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 63cb5bf1e..2d3796be4 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -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 = []; diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index bdc22291f..99d41cd04 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -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) { diff --git a/src/Psalm/Type/Atomic/HasIntersectionTrait.php b/src/Psalm/Type/Atomic/HasIntersectionTrait.php index 2a25bd6b3..376b38b13 100644 --- a/src/Psalm/Type/Atomic/HasIntersectionTrait.php +++ b/src/Psalm/Type/Atomic/HasIntersectionTrait.php @@ -8,21 +8,19 @@ use Psalm\Codebase; trait HasIntersectionTrait { /** - * @var array|null + * @var array|null */ public $extra_types; /** * @param array $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|null + * @return array|null */ - public function getIntersectionTypes() + public function getIntersectionTypes() : ?array { return $this->extra_types; } /** * @param array> $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; diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index cdfb164d3..f796481c7 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -6,6 +6,8 @@ use Psalm\Type\Atomic; class TObjectWithProperties extends TObject { + use HasIntersectionTrait; + /** * @var array */ @@ -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; } /** diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 9a78a4ff9..bdbbee8e3 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -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); } } } diff --git a/tests/Template/TemplateTest.php b/tests/Template/TemplateTest.php index d1a604fa7..ca0d64c3b 100644 --- a/tests/Template/TemplateTest.php +++ b/tests/Template/TemplateTest.php @@ -2704,6 +2704,78 @@ class TemplateTest extends TestCase return new TestPromise(true); }', ], + 'allowTemplatedIntersectionFirst' => [ + ' $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' => [ + ' $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();' + ], ]; }