mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 13:51:54 +01:00
Fix #4038 - don’t remove null types unnecessarily in mixed union
This commit is contained in:
parent
a466feda18
commit
a0a9c9b7bc
@ -238,15 +238,12 @@ class ReturnTypeAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
$inferred_return_type = TypeAnalyzer::simplifyUnionType(
|
||||
$inferred_return_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
|
||||
$codebase,
|
||||
\Psalm\Internal\Type\TypeExpander::expandUnion(
|
||||
$codebase,
|
||||
$inferred_return_type,
|
||||
$source->getFQCLN(),
|
||||
$source->getFQCLN(),
|
||||
$source->getParentFQCLN()
|
||||
)
|
||||
$inferred_return_type,
|
||||
$source->getFQCLN(),
|
||||
$source->getFQCLN(),
|
||||
$source->getParentFQCLN()
|
||||
);
|
||||
|
||||
// hack until we have proper yield type collection
|
||||
@ -552,8 +549,9 @@ class ReturnTypeAnalyzer
|
||||
|
||||
return null;
|
||||
}
|
||||
} elseif ((!$inferred_return_type->isNullable() && $declared_return_type->isNullable())
|
||||
|| (!$inferred_return_type->isFalsable() && $declared_return_type->isFalsable())
|
||||
} elseif (!$inferred_return_type->hasMixed()
|
||||
&& ((!$inferred_return_type->isNullable() && $declared_return_type->isNullable())
|
||||
|| (!$inferred_return_type->isFalsable() && $declared_return_type->isFalsable()))
|
||||
) {
|
||||
if ($function instanceof Function_
|
||||
|| $function instanceof Closure
|
||||
|
@ -255,7 +255,7 @@ class ReturnAnalyzer
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($stmt_type->isMixed()) {
|
||||
if ($stmt_type->hasMixed()) {
|
||||
if ($local_return_type->isVoid() || $local_return_type->isNever()) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidReturnStatement(
|
||||
@ -276,17 +276,29 @@ class ReturnAnalyzer
|
||||
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
|
||||
}
|
||||
|
||||
if ($stmt_type->isMixed()) {
|
||||
if (IssueBuffer::accepts(
|
||||
new MixedReturnStatement(
|
||||
'Could not infer a return type',
|
||||
new CodeLocation($source, $stmt->expr)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new MixedReturnStatement(
|
||||
'Could not infer a return type',
|
||||
'Possibly-mixed return value',
|
||||
new CodeLocation($source, $stmt->expr)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
// fall through
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($local_return_type->isMixed()) {
|
||||
|
@ -120,87 +120,4 @@ class TypeAnalyzer
|
||||
|
||||
return $result_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Type\Union
|
||||
*/
|
||||
public static function simplifyUnionType(Codebase $codebase, Type\Union $union)
|
||||
{
|
||||
$union_type_count = count($union->getAtomicTypes());
|
||||
|
||||
if ($union_type_count === 1 || ($union_type_count === 2 && $union->isNullable())) {
|
||||
return $union;
|
||||
}
|
||||
|
||||
$from_docblock = $union->from_docblock;
|
||||
$ignore_nullable_issues = $union->ignore_nullable_issues;
|
||||
$ignore_falsable_issues = $union->ignore_falsable_issues;
|
||||
$possibly_undefined = $union->possibly_undefined;
|
||||
|
||||
$unique_types = [];
|
||||
|
||||
$inverse_contains = [];
|
||||
|
||||
foreach ($union->getAtomicTypes() as $type_part) {
|
||||
$is_contained_by_other = false;
|
||||
|
||||
// don't try to simplify intersection types
|
||||
if (($type_part instanceof TNamedObject
|
||||
|| $type_part instanceof TTemplateParam
|
||||
|| $type_part instanceof TIterable)
|
||||
&& $type_part->extra_types
|
||||
) {
|
||||
return $union;
|
||||
}
|
||||
|
||||
foreach ($union->getAtomicTypes() as $container_type_part) {
|
||||
$string_container_part = $container_type_part->getId();
|
||||
$string_input_part = $type_part->getId();
|
||||
|
||||
$atomic_comparison_result = new TypeComparisonResult();
|
||||
|
||||
if ($type_part !== $container_type_part &&
|
||||
!(
|
||||
$container_type_part instanceof TInt
|
||||
|| $container_type_part instanceof TFloat
|
||||
|| $container_type_part instanceof TCallable
|
||||
|| ($container_type_part instanceof TString && $type_part instanceof TCallable)
|
||||
|| ($container_type_part instanceof TArray && $type_part instanceof TCallable)
|
||||
) &&
|
||||
!isset($inverse_contains[$string_input_part][$string_container_part]) &&
|
||||
AtomicTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$type_part,
|
||||
$container_type_part,
|
||||
false,
|
||||
false,
|
||||
$atomic_comparison_result
|
||||
) &&
|
||||
!$atomic_comparison_result->to_string_cast
|
||||
) {
|
||||
$inverse_contains[$string_container_part][$string_input_part] = true;
|
||||
|
||||
$is_contained_by_other = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$is_contained_by_other) {
|
||||
$unique_types[] = $type_part;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$unique_types) {
|
||||
throw new \UnexpectedValueException('There must be more than one unique type');
|
||||
}
|
||||
|
||||
$unique_type = new Type\Union($unique_types);
|
||||
|
||||
$unique_type->from_docblock = $from_docblock;
|
||||
$unique_type->ignore_nullable_issues = $ignore_nullable_issues;
|
||||
$unique_type->ignore_falsable_issues = $ignore_falsable_issues;
|
||||
$unique_type->possibly_undefined = $possibly_undefined;
|
||||
|
||||
return $unique_type;
|
||||
}
|
||||
}
|
||||
|
@ -1717,6 +1717,9 @@ class ArrayAssignmentTest extends TestCase
|
||||
$cache[$locale] = 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress MixedReturnStatement
|
||||
*/
|
||||
return $cache[$locale];
|
||||
}',
|
||||
'error_message' => 'InvalidReturnStatement',
|
||||
@ -1733,6 +1736,7 @@ class ArrayAssignmentTest extends TestCase
|
||||
|
||||
/**
|
||||
* @psalm-suppress MixedArrayAccess
|
||||
* @psalm-suppress MixedReturnStatement
|
||||
*/
|
||||
return $cache[$locale][$locale];
|
||||
}',
|
||||
|
@ -1337,6 +1337,52 @@ class FunctionTemplateTest extends TestCase
|
||||
[],
|
||||
'7.4'
|
||||
],
|
||||
'mixedDoesntSwallowNull' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template E
|
||||
* @param E $e
|
||||
* @param mixed $d
|
||||
* @return ?E
|
||||
* @psalm-suppress MixedInferredReturnType
|
||||
*/
|
||||
function reduce_values($e, $d) {
|
||||
if (rand(0, 1)) {
|
||||
$c = $e;
|
||||
} elseif (rand(0, 1)) {
|
||||
/** @psalm-suppress MixedAssignment */
|
||||
$c = $d;
|
||||
} else {
|
||||
$c = null;
|
||||
}
|
||||
|
||||
/** @psalm-suppress MixedReturnStatement */
|
||||
return $c;
|
||||
}'
|
||||
],
|
||||
'mixedDoesntSwallowNullProgressive' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template E
|
||||
* @param E $e
|
||||
* @param mixed $d
|
||||
* @return ?E
|
||||
* @psalm-suppress MixedInferredReturnType
|
||||
*/
|
||||
function reduce_values($e, $d)
|
||||
{
|
||||
if (rand(0, 1)) {
|
||||
$d = $e;
|
||||
}
|
||||
|
||||
if (rand(0, 1)) {
|
||||
/** @psalm-suppress MixedReturnStatement */
|
||||
return $d;
|
||||
}
|
||||
|
||||
return null;
|
||||
}'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user