Skip to content

Commit

Permalink
Fix #4782 - don’t replace closure types with upper bounds when replac…
Browse files Browse the repository at this point in the history
…ing class param types
  • Loading branch information
muglug committed Dec 5, 2020
1 parent 1bb8b73 commit cec8d71
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 18 deletions.
Expand Up @@ -257,15 +257,26 @@ private static function checkFunctionLikeTypeMatches(
);

if ($class_generic_params) {
$empty_generic_params = [];

$empty_template_result = new TemplateResult($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, []);

// This flag ensures that the template results will never be written to
// It also supercedes the `$add_upper_bounds` flag so that closure params
// don’t get overwritten
$readonly_template_result->readonly = true;

$arg_value_type = $statements_analyzer->node_data->getType($arg->value);

$param_type = TemplateStandinTypeReplacer::replace(
$param_type,
$empty_template_result,
$readonly_template_result,
$codebase,
$statements_analyzer,
$arg_value_type,
Expand All @@ -275,7 +286,7 @@ private static function checkFunctionLikeTypeMatches(

$arg_type = TemplateStandinTypeReplacer::replace(
$arg_type,
$empty_template_result,
$readonly_template_result,
$codebase,
$statements_analyzer,
$arg_value_type,
Expand Down
7 changes: 7 additions & 0 deletions src/Psalm/Internal/Type/TemplateResult.php
Expand Up @@ -22,6 +22,13 @@ class TemplateResult
*/
public $lower_bounds = [];

/**
* If set to true then we shouldn't update the template bounds
*
* @var bool
*/
public $readonly = false;

/**
* @var list<Union>
*/
Expand Down
10 changes: 7 additions & 3 deletions src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php
Expand Up @@ -619,6 +619,7 @@ private static function handleTemplateParamStandin(
);

if ($input_type
&& !$template_result->readonly
&& (
$atomic_type->as->isMixed()
|| !$codebase
Expand Down Expand Up @@ -686,20 +687,23 @@ private static function handleTemplateParamStandin(
);
}

foreach ($atomic_types as $atomic_type) {
foreach ($atomic_types as &$atomic_type) {
if ($atomic_type instanceof Atomic\TNamedObject
|| $atomic_type instanceof Atomic\TTemplateParam
|| $atomic_type instanceof Atomic\TIterable
|| $atomic_type instanceof Atomic\TObjectWithProperties
) {
$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;
}

if ($add_upper_bound && $input_type) {
if ($add_upper_bound && $input_type && !$template_result->readonly) {
$matching_input_keys = [];

if ($codebase
Expand Down Expand Up @@ -782,7 +786,7 @@ public static function handleTemplateParamClassStandin(

$atomic_types[] = $class_string;

if ($input_type) {
if ($input_type && !$template_result->readonly) {
$valid_input_atomic_types = [];

foreach ($input_type->getAtomicTypes() as $input_atomic_type) {
Expand Down
42 changes: 32 additions & 10 deletions tests/Template/ClassTemplateTest.php
Expand Up @@ -2394,21 +2394,18 @@ function order(array $collection, callable $sorter): array {
'intersectOnTOfObject' => [
'<?php
/**
* @psalm-template InterceptedObjectType of object
* @psalm-template TO of object
*/
interface AccessInterceptorInterface
{
interface A {
/**
* @psalm-param Closure(
* InterceptedObjectType&AccessInterceptorInterface
* ) : mixed $prefixInterceptor
* @psalm-param Closure(TO&A):mixed $c
*/
public function setMethodPrefixInterceptor(Closure $prefixInterceptor = null) : void;
public function setClosure(Closure $c): void;
}
function foo(AccessInterceptorInterface $i) : void {
$i->setMethodPrefixInterceptor(
function(AccessInterceptorInterface $i) : string {
function foo(A $i) : void {
$i->setClosure(
function(A $i) : string {
return "hello";
}
);
Expand Down Expand Up @@ -3801,6 +3798,31 @@ public function __invoke(array $in) : array {
false,
'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',
],
];
}
}

0 comments on commit cec8d71

Please sign in to comment.