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:
commit
20a2e6653f
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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}',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user