diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index 644f2e5ee..8fffa2c68 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -400,7 +400,8 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ &$invalid_method_call_types, &$existent_method_ids, &$non_existent_class_method_ids, - &$non_existent_interface_method_ids + &$non_existent_interface_method_ids, + bool &$check_visibility = true ) { $config = $codebase->config; @@ -520,8 +521,6 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ $fq_class_name = $lhs_type_part->value; - $intersection_types = $lhs_type_part->getIntersectionTypes(); - $is_mock = ExpressionAnalyzer::isMock($fq_class_name); $has_mock = $has_mock || $is_mock; @@ -568,6 +567,54 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ return false; } + $class_storage = $codebase->classlike_storage_provider->get($fq_class_name); + + $check_visibility = $check_visibility && !$class_storage->override_method_visibility; + + $intersection_types = $lhs_type_part->getIntersectionTypes(); + + $all_intersection_return_type = null; + $all_intersection_existent_method_ids = []; + + if ($intersection_types) { + foreach ($intersection_types as $intersection_type) { + $i_non_existent_class_method_ids = []; + $i_non_existent_interface_method_ids = []; + + $intersection_return_type = null; + + self::analyzeAtomicCall( + $statements_analyzer, + $stmt, + $codebase, + $context, + $intersection_type, + $lhs_var_id, + $intersection_return_type, + $returns_by_ref, + $has_mock, + $has_valid_method_call_type, + $has_mixed_method_call, + $invalid_method_call_types, + $all_intersection_existent_method_ids, + $i_non_existent_class_method_ids, + $i_non_existent_interface_method_ids, + $check_visibility + ); + + if ($intersection_return_type) { + if (!$all_intersection_return_type || $all_intersection_return_type->isMixed()) { + $all_intersection_return_type = $intersection_return_type; + } else { + $all_intersection_return_type = Type::intersectUnionTypes( + $all_intersection_return_type, + $intersection_return_type + ); + } + } + } + } + if (!$stmt->name instanceof PhpParser\Node\Identifier) { if (!$context->ignore_variable_method) { $codebase->analyzer->addMixedMemberName( @@ -603,8 +650,6 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ $statements_analyzer->getSource() ) ) { - $class_storage = $codebase->classlike_storage_provider->get($fq_class_name); - $interface_has_method = false; if ($class_storage->abstract && $class_storage->class_implements) { @@ -668,6 +713,13 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ $fq_class_name ); + if ($all_intersection_return_type) { + $return_type_candidate = Type::intersectUnionTypes( + $all_intersection_return_type, + $return_type_candidate + ); + } + if (!$return_type) { $return_type = $return_type_candidate; } else { @@ -733,80 +785,12 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ $fq_class_name = $context->self; } - $check_visibility = true; - - if ($intersection_types) { - foreach ($intersection_types as $intersection_type) { - if ($intersection_type instanceof TNamedObject - && $codebase->interfaceExists($intersection_type->value) - ) { - $interface_storage = $codebase->classlike_storage_provider->get($intersection_type->value); - - $check_visibility = $check_visibility && !$interface_storage->override_method_visibility; - } - } - } - $is_interface = false; if ($codebase->interfaceExists($fq_class_name)) { $is_interface = true; } - if ($intersection_types && !$codebase->methodExists($method_id)) { - if ($is_interface) { - $interface_storage = $codebase->classlike_storage_provider->get($fq_class_name); - - $check_visibility = $check_visibility && !$interface_storage->override_method_visibility; - } - - foreach ($intersection_types as $intersection_type) { - if ($intersection_type instanceof Type\Atomic\TTemplateParam) { - if (!$intersection_type->as->isMixed() - && !$intersection_type->as->hasObject() - ) { - $intersection_type = array_values( - $intersection_type->as->getTypes() - )[0]; - - if (!$intersection_type instanceof TNamedObject) { - throw new \UnexpectedValueException( - 'Shouldn’t get a non-object generic param here' - ); - } - - $intersection_type->from_docblock = true; - } else { - continue; - } - } - - $method_id = $intersection_type->value . '::' . $method_name_lc; - $fq_class_name = $intersection_type->value; - - $does_class_exist = ClassLikeAnalyzer::checkFullyQualifiedClassLikeName( - $statements_analyzer, - $fq_class_name, - new CodeLocation($source, $stmt->var), - $statements_analyzer->getSuppressedIssues(), - false, - false, - true, - $intersection_type->from_docblock - ); - - if (!$does_class_exist) { - return false; - } - - if ($codebase->methodExists($method_id)) { - $is_interface = $codebase->interfaceExists($fq_class_name); - - break; - } - } - } - $source_method_id = $source instanceof FunctionLikeAnalyzer ? $source->getMethodId() : null; @@ -854,6 +838,13 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ if ($pseudo_method_storage->return_type) { $return_type_candidate = clone $pseudo_method_storage->return_type; + if ($all_intersection_return_type) { + $return_type_candidate = Type::intersectUnionTypes( + $all_intersection_return_type, + $return_type_candidate + ); + } + if (!$return_type) { $return_type = $return_type_candidate; } else { @@ -869,6 +860,18 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ } } + if ($all_intersection_return_type && $all_intersection_existent_method_ids) { + $existent_method_ids = array_merge($existent_method_ids, $all_intersection_existent_method_ids); + + if (!$return_type) { + $return_type = $all_intersection_return_type; + } else { + $return_type = Type::combineUnionTypes($all_intersection_return_type, $return_type); + } + + return; + } + if ($is_interface) { $non_existent_interface_method_ids[] = $intersection_method_id ?: $method_id; } else { @@ -1193,11 +1196,24 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ } if ($return_type_candidate) { + if ($all_intersection_return_type) { + $return_type_candidate = Type::intersectUnionTypes( + $all_intersection_return_type, + $return_type_candidate + ); + } + if (!$return_type) { $return_type = $return_type_candidate; } else { $return_type = Type::combineUnionTypes($return_type_candidate, $return_type); } + } elseif ($all_intersection_return_type) { + if (!$return_type) { + $return_type = $all_intersection_return_type; + } else { + $return_type = Type::combineUnionTypes($all_intersection_return_type, $return_type); + } } else { $return_type = Type::getMixed(); } diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 2c5b85b7f..99020d336 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -1306,6 +1306,107 @@ abstract class Type return $combined_type; } + /** + * Combines two union types into one via an intersection + * + * @param Union $type_1 + * @param Union $type_2 + * + * @return Union + */ + public static function intersectUnionTypes( + Union $type_1, + Union $type_2 + ) { + if ($type_1->isMixed() && $type_2->isMixed()) { + $combined_type = Type::getMixed(); + } else { + $both_failed_reconciliation = false; + + if ($type_1->failed_reconciliation) { + if ($type_2->failed_reconciliation) { + $both_failed_reconciliation = true; + } else { + return $type_2; + } + } elseif ($type_2->failed_reconciliation) { + return $type_1; + } + + if ($type_1->isMixed() && !$type_2->isMixed()) { + $combined_type = clone $type_2; + } elseif (!$type_1->isMixed() && $type_2->isMixed()) { + $combined_type = clone $type_1; + } else { + $combined_type = clone $type_1; + + foreach ($combined_type->getTypes() as $type_1_atomic) { + 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_2_atomic instanceof TIterable + || $type_2_atomic instanceof TNamedObject + || $type_2_atomic instanceof TTemplateParam) + ) { + if (!$type_1_atomic->extra_types) { + $type_1_atomic->extra_types = []; + } + + $type_2_atomic_clone = clone $type_2_atomic; + + $type_2_atomic_clone->extra_types = []; + + $type_1_atomic->extra_types[] = $type_2_atomic_clone; + + $type_2_atomic_intersection_types = $type_2_atomic->getIntersectionTypes(); + + if ($type_2_atomic_intersection_types) { + foreach ($type_2_atomic_intersection_types as $type_2_intersection_type) { + $type_1_atomic->extra_types[] = clone $type_2_intersection_type; + } + } + } + } + } + } + + if (!$type_1->initialized && !$type_2->initialized) { + $combined_type->initialized = false; + } + + if ($type_1->possibly_undefined_from_try && $type_2->possibly_undefined_from_try) { + $combined_type->possibly_undefined_from_try = true; + } + + if ($type_1->from_docblock && $type_2->from_docblock) { + $combined_type->from_docblock = true; + } + + if ($type_1->from_calculation && $type_2->from_calculation) { + $combined_type->from_calculation = true; + } + + if ($type_1->ignore_nullable_issues && $type_2->ignore_nullable_issues) { + $combined_type->ignore_nullable_issues = true; + } + + if ($type_1->ignore_falsable_issues && $type_2->ignore_falsable_issues) { + $combined_type->ignore_falsable_issues = true; + } + + if ($both_failed_reconciliation) { + $combined_type->failed_reconciliation = true; + } + } + + if ($type_1->possibly_undefined && $type_2->possibly_undefined) { + $combined_type->possibly_undefined = true; + } + + return $combined_type; + } + public static function clearCache() : void { self::$memoized_tokens = []; diff --git a/tests/InterfaceTest.php b/tests/InterfaceTest.php index 5ec06e580..af13f6e78 100644 --- a/tests/InterfaceTest.php +++ b/tests/InterfaceTest.php @@ -504,6 +504,12 @@ class InterfaceTest extends TestCase if ($i instanceof A) { $i->foo(); } + } + + function takeA(A $a) : void { + if ($a instanceof I) { + $a->foo(); + } }', ], 'docblockParamInheritance' => [ @@ -555,6 +561,26 @@ class InterfaceTest extends TestCase $c->current(); }' ], + 'intersectMixedTypes' => [ + 'foo(); + } + + /** @param IBar&IFoo $i */ + function iBarFirst($i) : string { + return $i->foo(); + }', + ], ]; } diff --git a/tests/Template/TemplateTest.php b/tests/Template/TemplateTest.php index 43c9cecd6..89093ebfe 100644 --- a/tests/Template/TemplateTest.php +++ b/tests/Template/TemplateTest.php @@ -2429,6 +2429,58 @@ class TemplateTest extends TestCase getObject(new C())->sayHello();' ], + 'SKIPPED-templatedInterfaceIntersectionFirst' => [ + '&IChild */ + function makeConcrete() : IChild { + return new class() implements IChild { + public function foo() { + return new C(); + } + }; + } + + $a = makeConcrete()->foo();', + [ + '$a' => 'C', + ] + ], + 'templatedInterfaceIntersectionSecond' => [ + ' */ + function makeConcrete() : IChild { + return new class() implements IChild { + public function foo() { + return new C(); + } + }; + } + + $a = makeConcrete()->foo();', + [ + '$a' => 'C', + ] + ], ]; }