1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Fix #1674 - treat intersections more equally regardless of order

This commit is contained in:
Matthew Brown 2019-05-24 12:48:37 -04:00
parent a43e4d879b
commit 3e2b7163ca
4 changed files with 268 additions and 73 deletions

View File

@ -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(
'Shouldnt 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();
}

View File

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

View File

@ -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' => [
'<?php
interface IFoo {
function foo();
}
interface IBar {
function foo() : string;
}
/** @param IFoo&IBar $i */
function iFooFirst($i) : string {
return $i->foo();
}
/** @param IBar&IFoo $i */
function iBarFirst($i) : string {
return $i->foo();
}',
],
];
}

View File

@ -2429,6 +2429,58 @@ class TemplateTest extends TestCase
getObject(new C())->sayHello();'
],
'SKIPPED-templatedInterfaceIntersectionFirst' => [
'<?php
/** @psalm-template T */
interface IParent {
/** @psalm-return T */
function foo();
}
interface IChild extends IParent {}
class C {}
/** @psalm-return IParent<C>&IChild */
function makeConcrete() : IChild {
return new class() implements IChild {
public function foo() {
return new C();
}
};
}
$a = makeConcrete()->foo();',
[
'$a' => 'C',
]
],
'templatedInterfaceIntersectionSecond' => [
'<?php
/** @psalm-template T */
interface IParent {
/** @psalm-return T */
function foo();
}
interface IChild extends IParent {}
class C {}
/** @psalm-return IChild&IParent<C> */
function makeConcrete() : IChild {
return new class() implements IChild {
public function foo() {
return new C();
}
};
}
$a = makeConcrete()->foo();',
[
'$a' => 'C',
]
],
];
}