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

Fix #4782 - don’t replace closure types with upper bounds when replacing class param types

This commit is contained in:
Matt Brown 2020-12-05 11:58:55 -05:00 committed by Daniil Gentili
parent 4f9449b742
commit 8bfb0412e7
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
4 changed files with 61 additions and 17 deletions

View File

@ -257,15 +257,26 @@ class ArgumentAnalyzer
); );
if ($class_generic_params) { if ($class_generic_params) {
$empty_generic_params = []; // here we're replacing the param types and arg types with the bound
// class template params.
//
// For example, if we're operating on a class Foo with params TKey and TValue,
// and we're calling a method "add(TKey $key, TValue $value)" on an instance
// of that class where we know that TKey is int and TValue is string, then we
// want to replace the substitute the expected values so it's as if we were actually
// calling "add(int $key, string $value)"
$readonly_template_result = new TemplateResult($class_generic_params, []);
$empty_template_result = new TemplateResult($class_generic_params, $empty_generic_params); // This flag ensures that the template results will never be written to
// It also supercedes the `$add_upper_bounds` flag so that closure params
// dont get overwritten
$readonly_template_result->readonly = true;
$arg_value_type = $statements_analyzer->node_data->getType($arg->value); $arg_value_type = $statements_analyzer->node_data->getType($arg->value);
$param_type = TemplateStandinTypeReplacer::replace( $param_type = TemplateStandinTypeReplacer::replace(
$param_type, $param_type,
$empty_template_result, $readonly_template_result,
$codebase, $codebase,
$statements_analyzer, $statements_analyzer,
$arg_value_type, $arg_value_type,
@ -275,7 +286,7 @@ class ArgumentAnalyzer
$arg_type = TemplateStandinTypeReplacer::replace( $arg_type = TemplateStandinTypeReplacer::replace(
$arg_type, $arg_type,
$empty_template_result, $readonly_template_result,
$codebase, $codebase,
$statements_analyzer, $statements_analyzer,
$arg_value_type, $arg_value_type,

View File

@ -22,6 +22,13 @@ class TemplateResult
*/ */
public $lower_bounds = []; public $lower_bounds = [];
/**
* If set to true then we shouldn't update the template bounds
*
* @var bool
*/
public $readonly = false;
/** /**
* @var list<Union> * @var list<Union>
*/ */

View File

@ -619,6 +619,7 @@ class TemplateStandinTypeReplacer
); );
if ($input_type if ($input_type
&& !$template_result->readonly
&& ( && (
$atomic_type->as->isMixed() $atomic_type->as->isMixed()
|| !$codebase || !$codebase
@ -686,20 +687,23 @@ class TemplateStandinTypeReplacer
); );
} }
foreach ($atomic_types as $atomic_type) { foreach ($atomic_types as &$atomic_type) {
if ($atomic_type instanceof Atomic\TNamedObject if ($atomic_type instanceof Atomic\TNamedObject
|| $atomic_type instanceof Atomic\TTemplateParam || $atomic_type instanceof Atomic\TTemplateParam
|| $atomic_type instanceof Atomic\TIterable || $atomic_type instanceof Atomic\TIterable
|| $atomic_type instanceof Atomic\TObjectWithProperties || $atomic_type instanceof Atomic\TObjectWithProperties
) { ) {
$atomic_type->extra_types = $extra_types; $atomic_type->extra_types = $extra_types;
} elseif ($atomic_type instanceof Atomic\TObject && $extra_types) {
$atomic_type = \reset($extra_types);
$atomic_type->extra_types = \array_slice($extra_types, 1);
} }
} }
return $atomic_types; return $atomic_types;
} }
if ($add_upper_bound && $input_type) { if ($add_upper_bound && $input_type && !$template_result->readonly) {
$matching_input_keys = []; $matching_input_keys = [];
if ($codebase if ($codebase
@ -782,7 +786,7 @@ class TemplateStandinTypeReplacer
$atomic_types[] = $class_string; $atomic_types[] = $class_string;
if ($input_type) { if ($input_type && !$template_result->readonly) {
$valid_input_atomic_types = []; $valid_input_atomic_types = [];
foreach ($input_type->getAtomicTypes() as $input_atomic_type) { foreach ($input_type->getAtomicTypes() as $input_atomic_type) {

View File

@ -2394,21 +2394,18 @@ class ClassTemplateTest extends TestCase
'intersectOnTOfObject' => [ 'intersectOnTOfObject' => [
'<?php '<?php
/** /**
* @psalm-template InterceptedObjectType of object * @psalm-template TO of object
*/ */
interface AccessInterceptorInterface interface A {
{
/** /**
* @psalm-param Closure( * @psalm-param Closure(TO&A):mixed $c
* InterceptedObjectType&AccessInterceptorInterface
* ) : mixed $prefixInterceptor
*/ */
public function setMethodPrefixInterceptor(Closure $prefixInterceptor = null) : void; public function setClosure(Closure $c): void;
} }
function foo(AccessInterceptorInterface $i) : void { function foo(A $i) : void {
$i->setMethodPrefixInterceptor( $i->setClosure(
function(AccessInterceptorInterface $i) : string { function(A $i) : string {
return "hello"; return "hello";
} }
); );
@ -3801,6 +3798,31 @@ class ClassTemplateTest extends TestCase
false, false,
'8.0' '8.0'
], ],
'bindClosureParamAccurately' => [
'<?php
/**
* @template TKey
* @template TValue
*/
interface Collection {
/**
* @template T
* @param Closure(TValue):T $func
* @return Collection<TKey,T>
*/
public function map(Closure $func);
}
/**
* @param Collection<int, string> $c
*/
function f(Collection $c): void {
$fn = function(int $_p): bool { return true; };
$c->map($fn);
}',
'error_message' => 'InvalidScalarArgument',
],
]; ];
} }
} }