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

Merge pull request #9487 from vimeo/compare-list-generic-params

This commit is contained in:
Bruce Weirdan 2023-03-11 05:56:56 -04:00 committed by GitHub
commit 20a2e6653f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 128 additions and 5 deletions

View File

@ -15,6 +15,7 @@ use UnexpectedValueException;
use function assert;
use function count;
use function implode;
use function is_string;
use function preg_replace;
use function strpos;
use function strtolower;
@ -244,7 +245,7 @@ class NamespaceAnalyzer extends SourceAnalyzer
while (($pos = strpos($identifier, "\\")) !== false) {
if ($pos > 0) {
$part = substr($identifier, 0, $pos);
assert($part !== "");
assert(is_string($part) && $part !== "");
$parts[] = $part;
}
$parts[] = "\\";
@ -253,13 +254,13 @@ class NamespaceAnalyzer extends SourceAnalyzer
if (($pos = strpos($identifier, "::")) !== false) {
if ($pos > 0) {
$part = substr($identifier, 0, $pos);
assert($part !== "");
assert(is_string($part) && $part !== "");
$parts[] = $part;
}
$parts[] = "::";
$identifier = substr($identifier, $pos + 2);
}
if ($identifier !== "") {
if ($identifier !== "" && $identifier !== false) {
$parts[] = $identifier;
}

View File

@ -127,6 +127,84 @@ class KeyedArrayComparator
}
return false;
}
// check remaining $input_properties against container's fallback_params
if ($container_type_part instanceof TKeyedArray
&& $container_type_part->fallback_params !== null
) {
[$key_type, $value_type] = $container_type_part->fallback_params;
// treat fallback params as possibly undefined
// otherwise comparison below would fail for list{0?:int} <=> list{...<int<0,max>, int>}
// as the latter `int` is not marked as possibly_undefined
$value_type = $value_type->setPossiblyUndefined(true);
foreach ($input_properties as $key => $input_property_type) {
$key_type_comparison = new TypeComparisonResult();
if (!UnionTypeComparator::isContainedBy(
$codebase,
is_string($key) ? Type::getString($key) : Type::getInt(false, $key),
$key_type,
false,
false,
$key_type_comparison,
$allow_interface_equality,
)) {
if ($atomic_comparison_result) {
$atomic_comparison_result->type_coerced
= $key_type_comparison->type_coerced === true
&& $atomic_comparison_result->type_coerced !== false;
}
$all_types_contain = false;
}
$property_type_comparison = new TypeComparisonResult();
if (!UnionTypeComparator::isContainedBy(
$codebase,
$input_property_type,
$value_type,
false,
false,
$property_type_comparison,
$allow_interface_equality,
)) {
if ($atomic_comparison_result) {
$atomic_comparison_result->type_coerced
= $property_type_comparison->type_coerced === true
&& $atomic_comparison_result->type_coerced !== false;
}
$all_types_contain = false;
}
}
}
// finally, check input type fallback params against container type fallback params
if ($input_type_part instanceof TKeyedArray
&& $container_type_part instanceof TKeyedArray
&& $input_type_part->fallback_params !== null
&& $container_type_part->fallback_params !== null
) {
foreach ($input_type_part->fallback_params as $i => $input_param) {
$container_param = $container_type_part->fallback_params[$i];
$param_comparison = new TypeComparisonResult();
if (!UnionTypeComparator::isContainedBy(
$codebase,
$input_param,
$container_param,
false,
false,
$param_comparison,
$allow_interface_equality,
)) {
if ($atomic_comparison_result) {
$atomic_comparison_result->type_coerced
= $param_comparison->type_coerced === true
&& $atomic_comparison_result->type_coerced !== false;
}
$all_types_contain = false;
}
}
}
return $all_types_contain;
}

View File

@ -1721,6 +1721,18 @@ class ReturnTypeTest extends TestCase
}',
'error_message' => 'InvalidClass',
],
'listItems' => [
'code' => <<<'PHP'
<?php
/** @return list<int> */
function f(): array
{
return[ 1, new stdClass, "zzz"];
}
PHP,
'error_message' => 'InvalidReturnStatement',
],
];
}
}

View File

@ -94,7 +94,7 @@ class TypeComparatorTest extends TestCase
}
/**
* @dataProvider getAllowedChildTypes
* @dataProvider getSuccessfulComparisons
*/
public function testTypeAcceptsType(string $parent_type_string, string $child_type_string): void
{
@ -107,13 +107,32 @@ class TypeComparatorTest extends TestCase
$child_type,
$parent_type,
),
'Type ' . $parent_type_string . ' should contain ' . $child_type_string,
);
}
/**
* @dataProvider getUnsuccessfulComparisons
*/
public function testTypeDoesNotAcceptType(string $parent_type_string, string $child_type_string): void
{
$parent_type = Type::parseString($parent_type_string);
$child_type = Type::parseString($child_type_string);
$this->assertFalse(
UnionTypeComparator::isContainedBy(
$this->project_analyzer->getCodebase(),
$child_type,
$parent_type,
),
'Type ' . $parent_type_string . ' should not contain ' . $child_type_string,
);
}
/**
* @return array<array{string, string}>
*/
public function getAllowedChildTypes(): array
public function getSuccessfulComparisons(): array
{
return [
'iterableAcceptsArray' => [
@ -142,4 +161,17 @@ class TypeComparatorTest extends TestCase
],
];
}
/** @return iterable<string, list{string,string}> */
public function getUnsuccessfulComparisons(): iterable
{
yield 'genericListDoesNotAcceptListTupleWithMismatchedTypes' => [
'list<int>',
'list{int, string}',
];
yield 'genericListDoesNotAcceptArrayTupleWithMismatchedTypes' => [
'list<int>',
'array{int, string}',
];
}
}