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

Accept partial match of template type

This commit is contained in:
Brown 2020-04-04 17:14:33 -04:00
parent c82834ebe4
commit 1bf1a6e46b
6 changed files with 89 additions and 8 deletions

View File

@ -392,7 +392,8 @@ class TypeAnalyzer
Type\Union $input_type,
Type\Union $container_type,
$ignore_null = false,
$ignore_false = false
$ignore_false = false,
array &$matching_input_keys = []
) {
if ($container_type->hasMixed()) {
return true;
@ -425,12 +426,12 @@ class TypeAnalyzer
if (($is_atomic_contained_by && !$atomic_comparison_result->to_string_cast)
|| $atomic_comparison_result->type_coerced_from_mixed
) {
return true;
$matching_input_keys[$input_type_part->getKey()] = true;
}
}
}
return false;
return !!$matching_input_keys;
}
/**

View File

@ -2475,9 +2475,15 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
if (!isset($param_type_mapping[$token_body])) {
$template_name = 'TGeneratedFromParam' . $j;
$template_function_id = 'fn-' . strtolower($cased_function_id);
$template_as_type = $param_storage->type
? clone $param_storage->type
: Type::getMixed();
$storage->template_types[$template_name] = [
'fn-' . strtolower($cased_function_id) => [
$param_storage->type ? clone $param_storage->type : Type::getMixed()
$template_function_id => [
$template_as_type
],
];
@ -2485,6 +2491,14 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
= $storage->template_types[$template_name];
$param_type_mapping[$token_body] = $template_name;
$param_storage->type = new Type\Union([
new Type\Atomic\TTemplateParam(
$template_name,
$template_as_type,
$template_function_id
)
]);
}
// spaces are allowed before $foo in get(string $foo) magic method

View File

@ -478,19 +478,34 @@ class UnionTemplateHandler
}
}
$matching_input_keys = [];
if ($input_type
&& (
$atomic_type->as->isMixed()
|| !$codebase
|| TypeAnalyzer::isContainedBy(
|| TypeAnalyzer::canBeContainedBy(
$codebase,
$input_type,
$atomic_type->as
$atomic_type->as,
false,
false,
$matching_input_keys
)
)
) {
$generic_param = clone $input_type;
if ($matching_input_keys) {
$generic_param_keys = \array_keys($generic_param->getAtomicTypes());
foreach ($generic_param_keys as $atomic_key) {
if (!isset($matching_input_keys[$atomic_key])) {
$generic_param->removeType($atomic_key);
}
}
}
if ($was_nullable && $generic_param->isNullable() && !$generic_param->isNull()) {
$generic_param->removeType('null');
}

View File

@ -1256,6 +1256,17 @@ class FunctionCallTest extends TestCase
'$a' => 'int',
]
],
'jsonEncodeString' => [
'<?php
$a = json_decode("hello");
$b = json_decode("hello", true);
$c = json_decode("hello", false);',
[
'$a' => 'array<int, array<int, array<array-key, mixed>|null|scalar|stdClass>|null|scalar|stdClass>|null|scalar|stdClass',
'$b' => 'array<array-key, array<array-key, array<array-key, mixed>|null|scalar>|null|scalar>|null|scalar',
'$c' => 'array<int, array<int, array<array-key, mixed>|null|scalar|stdClass>|null|scalar|stdClass>|null|scalar|stdClass',
]
],
];
}

View File

@ -139,7 +139,9 @@ class ConditionalReturnTypeTest extends TestCase
$int = add(3, 5);
$float1 = add(2.5, 3);
$float2 = add(2.7, 3.1);
$float3 = add(3, 3.5);',
$float3 = add(3, 3.5);
/** @psalm-suppress PossiblyNullArgument */
$int = add(rand(0, 1) ? null : 1, 1);',
[
'$int' => 'int',
'$float1' => 'float',
@ -147,6 +149,25 @@ class ConditionalReturnTypeTest extends TestCase
'$float3' => 'float',
]
],
'possiblyNullArgumentStillMatchesType' => [
'<?php
/**
* @template T as int|float
* @param T $a
* @param T $b
* @return int|float
* @psalm-return (T is int ? int : float)
*/
function add($a, $b) {
return $a + $b;
}
/** @psalm-suppress PossiblyNullArgument */
$int = add(rand(0, 1) ? null : 1, 4);',
[
'$int' => 'int',
]
],
'nestedClassConstantConditionalComparison' => [
'<?php
class A {

View File

@ -1114,6 +1114,25 @@ class FunctionTemplateTest extends TestCase
'$a' => 'iterable<mixed, string>'
]
],
'possiblyNullMatchesTemplateType' => [
'<?php
/**
* @template T as object
* @param T $o
* @return T
*/
function takesObject(object $o) : object {
return $o;
}
class A {}
/** @psalm-suppress PossiblyNullArgument */
$a = takesObject(rand(0, 1) ? new A() : null);',
[
'$a' => 'A',
]
],
];
}