1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

bugfix: do not extend the type - only narrow down

Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com>
This commit is contained in:
Maximilian Bösing 2022-06-12 19:20:38 +02:00
parent ed1bb8a9a6
commit 7e033d8051
No known key found for this signature in database
GPG Key ID: 9A8988C93CEC81A3
2 changed files with 51 additions and 13 deletions

View File

@ -796,6 +796,7 @@ class CallAnalyzer
} elseif (isset($context->vars_in_scope[$assertion_var_id])) {
$other_type = $context->vars_in_scope[$assertion_var_id];
if (self::isNewTypeNarrowingDownOldType($other_type, $union)) {
$union = self::createUnionIntersectionFromOldType($union, $other_type);
foreach ($union->getAtomicTypes() as $atomic_type) {
if ($assertion_type instanceof TTemplateParam
&& $assertion_type->as->getId() === $atomic_type->getId()
@ -1154,25 +1155,49 @@ class CallAnalyzer
return false;
}
$old_atomic_type = $old_type->getSingleAtomic();
foreach ($new_type->getAtomicTypes() as $new_atomic_type) {
if ($new_atomic_type->equals($old_atomic_type, false)) {
// Old type is one of the new types and thus, the old type must not be modified
return false;
}
// Do not hassle around with single literals as they supposed to be more accurate than any new type assertion
if ($old_type->isSingleFloatLiteral()
|| $old_type->isSingleIntLiteral()
|| $old_type->isSingleStringLiteral()
) {
return false;
}
// Literals should always replace non-literals
if (!$old_type->containsAnyLiteral() &&
(
($old_type->isString() && $new_type->allStringLiterals())
|| ($old_type->isInt() && $new_type->allIntLiterals())
|| ($old_type->isFloat() && $new_type->allFloatLiterals())
)
if (($old_type->isString() && $new_type->allStringLiterals())
|| ($old_type->isInt() && $new_type->allIntLiterals())
|| ($old_type->isFloat() && $new_type->allFloatLiterals())
) {
return true;
}
return false;
}
/**
* This method should kick all literals within `new_type` which are not part of the already known `old_type`.
* So lets say we already know that the old type is one of "a", "b" or "c".
* If another assertion takes place to determine if the value is either "a", "c" or "d", we can kick "d" as that
* won't be possible.
*/
private static function createUnionIntersectionFromOldType(Union $new_type, Union $old_type): Union
{
if (!$new_type->allLiterals() || !$old_type->allLiterals()) {
return $new_type;
}
$equal_atomic_types = [];
foreach ($new_type->getAtomicTypes() as $new_atomic_type) {
foreach ($old_type->getAtomicTypes() as $old_atomic_type) {
if (!$new_atomic_type->equals($old_atomic_type, false)) {
continue;
}
$equal_atomic_types[] = $new_atomic_type;
}
}
return new Union($equal_atomic_types);
}
}

View File

@ -90,7 +90,6 @@ class AssertAnnotationTest extends TestCase
$this->analyzeFile('somefile.php', new Context());
}
/**
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>}>
*/
@ -2082,6 +2081,9 @@ class AssertAnnotationTest extends TestCase
/** @var string $anotherString */
$anotherString;
/** @var null|string $nullableString */
$nullableString;
/** @var mixed $maybeInt */
$maybeInt;
/** @var mixed $maybeFloat */
@ -2093,11 +2095,22 @@ class AssertAnnotationTest extends TestCase
assertOneOf($anotherString, ["a", "b", "c"]);
consumeLiteralStringValue($anotherString);
assertOneOf($nullableString, ["a", "b", "c"]);
assertOneOf($nullableString, ["a", "c"]);
assertOneOf($maybeInt, [1, 2, 3]);
consumeAnyIntegerValue($maybeInt);
assertOneOf($maybeFloat, [1.5, 2.5, 3.5]);
consumeAnyFloatValue($maybeFloat);
/** @var "a"|"b"|"c" $abc */
$abc;
/** @param "a"|"b" $aOrB */
function consumeAOrB(string $aOrB): void {}
assertOneOf($abc, ["a", "b"]);
consumeAOrB($abc);
'
],
];