From 1bf1a6e46b0c0bfa65d13b679d7c70c8c7297c69 Mon Sep 17 00:00:00 2001 From: Brown Date: Sat, 4 Apr 2020 17:14:33 -0400 Subject: [PATCH] Accept partial match of template type --- src/Psalm/Internal/Analyzer/TypeAnalyzer.php | 7 +++--- .../Internal/PhpVisitor/ReflectorVisitor.php | 18 +++++++++++++-- .../Internal/Type/UnionTemplateHandler.php | 19 +++++++++++++-- tests/FunctionCallTest.php | 11 +++++++++ tests/Template/ConditionalReturnTypeTest.php | 23 ++++++++++++++++++- tests/Template/FunctionTemplateTest.php | 19 +++++++++++++++ 6 files changed, 89 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php index 4bb0b106f..e9c55bd35 100644 --- a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php @@ -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; } /** diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 35430ff64..c3ba8bece 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -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 diff --git a/src/Psalm/Internal/Type/UnionTemplateHandler.php b/src/Psalm/Internal/Type/UnionTemplateHandler.php index 01fec9668..a843cba2b 100644 --- a/src/Psalm/Internal/Type/UnionTemplateHandler.php +++ b/src/Psalm/Internal/Type/UnionTemplateHandler.php @@ -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'); } diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index d10530d9b..4ecce5dbb 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1256,6 +1256,17 @@ class FunctionCallTest extends TestCase '$a' => 'int', ] ], + 'jsonEncodeString' => [ + ' 'array|null|scalar|stdClass>|null|scalar|stdClass>|null|scalar|stdClass', + '$b' => 'array|null|scalar>|null|scalar>|null|scalar', + '$c' => 'array|null|scalar|stdClass>|null|scalar|stdClass>|null|scalar|stdClass', + ] + ], ]; } diff --git a/tests/Template/ConditionalReturnTypeTest.php b/tests/Template/ConditionalReturnTypeTest.php index d38fdbcf4..fd5ef85e8 100644 --- a/tests/Template/ConditionalReturnTypeTest.php +++ b/tests/Template/ConditionalReturnTypeTest.php @@ -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' => [ + ' 'int', + ] + ], 'nestedClassConstantConditionalComparison' => [ ' 'iterable' ] ], + 'possiblyNullMatchesTemplateType' => [ + ' 'A', + ] + ], ]; }