1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Add support for checking callable/closure param types

Fixes #580
This commit is contained in:
Matthew Brown 2018-04-08 12:03:35 -04:00
parent 3f4083eac8
commit 7e3a1ec9c3
2 changed files with 381 additions and 0 deletions

View File

@ -344,6 +344,27 @@ class TypeChecker
}
}
if ($container_type_part instanceof Type\Atomic\Fn) {
if (!$input_type_part instanceof Type\Atomic\Fn) {
$type_coerced = true;
$type_coerced_from_mixed = true;
return false;
}
if (self::compareCallable(
$codebase,
$input_type_part,
$container_type_part,
$type_coerced,
$type_coerced_from_mixed,
$all_types_contain
) === false
) {
return false;
}
}
if (($input_type_part instanceof TArray || $input_type_part instanceof ObjectLike)
&& ($container_type_part instanceof TArray || $container_type_part instanceof ObjectLike)
) {
@ -445,6 +466,26 @@ class TypeChecker
return true;
}
if ($container_type_part instanceof TCallable && $input_type_part instanceof Type\Atomic\Fn) {
$all_types_contain = true;
if (self::compareCallable(
$codebase,
$input_type_part,
$container_type_part,
$type_coerced,
$type_coerced_from_mixed,
$all_types_contain
) === false
) {
return false;
}
if (!$all_types_contain) {
return false;
}
}
if ($input_type_part instanceof TNamedObject &&
$input_type_part->value === 'Closure' &&
$container_type_part instanceof TCallable
@ -571,6 +612,12 @@ class TypeChecker
}
}
if ($container_type_part instanceof Type\Atomic\Fn && $input_type_part instanceof TCallable) {
$type_coerced = true;
return false;
}
if ($container_type_part instanceof TCallable &&
(
$input_type_part instanceof TString ||
@ -631,6 +678,84 @@ class TypeChecker
return false;
}
/**
* @param TCallable|Type\Atomic\Fn $input_type_part
* @param TCallable|Type\Atomic\Fn $container_type_part
* @param bool &$type_coerced
* @param bool &$type_coerced_from_mixed
* @param bool &$all_types_contain
*
* @return null|false
*
* @psalm-suppress ConflictingReferenceConstraint
*/
private static function compareCallable(
Codebase $codebase,
$input_type_part,
$container_type_part,
&$type_coerced,
&$type_coerced_from_mixed,
&$all_types_contain
) {
if ($container_type_part->params !== null && $input_type_part->params === null) {
$type_coerced = true;
$type_coerced_from_mixed = true;
return false;
}
if ($container_type_part->params !== null) {
foreach ($input_type_part->params as $i => $input_param) {
if (!isset($container_type_part->params[$i])) {
$type_coerced = true;
$type_coerced_from_mixed = true;
$all_types_contain = false;
break;
}
$container_param = $container_type_part->params[$i];
if (!self::isContainedBy(
$codebase,
$input_param->type ?: Type::getMixed(),
$container_param->type ?: Type::getMixed(),
false,
false,
$has_scalar_match,
$type_coerced,
$type_coerced_from_mixed
)
) {
$all_types_contain = false;
}
}
if (isset($container_type_part->return_type)) {
if (!isset($input_type_part->return_type)) {
$type_coerced = true;
$type_coerced_from_mixed = true;
$all_types_contain = false;
} else {
if (!self::isContainedBy(
$codebase,
$input_type_part->return_type,
$container_type_part->return_type,
false,
false,
$has_scalar_match,
$type_coerced,
$type_coerced_from_mixed
)
) {
$all_types_contain = false;
}
}
}
}
}
/**
* Takes two arrays of types and merges them
*

View File

@ -298,6 +298,7 @@ class CallableTest extends TestCase
/**
* @param Closure(int):int $f
* @param Closure(int):int $g
*
* @return Closure(int):int
*/
function foo(Closure $f, Closure $g) : Closure {
@ -306,6 +307,112 @@ class CallableTest extends TestCase
}
}'
],
'returnsTypedClosureWithClasses' => [
'<?php
class A {}
class B {}
class C {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return Closure(C):A
*/
function foo(Closure $f, Closure $g) : Closure {
return function (C $x) use ($f, $g) : A {
return $f($g($x));
}
}
$a = foo(
function(B $b) : A { return new A;},
function(C $c) : B { return new B;}
)(new C);',
'assertions' => [
'$a' => 'A',
],
],
'returnsTypedClosureWithSubclassParam' => [
'<?php
class A {}
class B {}
class C {}
class C2 extends C {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return Closure(C):A
*/
function foo(Closure $f, Closure $g) : Closure {
return function (C2 $x) use ($f, $g) : A {
return $f($g($x));
}
}
$a = foo(
function(B $b) : A { return new A;},
function(C $c) : B { return new B;}
)(new C2);',
'assertions' => [
'$a' => 'A',
],
],
'returnsTypedClosureWithParentReturn' => [
'<?php
class A {}
class B {}
class C {}
class A2 extends A {}
/**
* @param Closure(B):A2 $f
* @param Closure(C):B $g
*
* @return Closure(C):A
*/
function foo(Closure $f, Closure $g) : Closure {
return function (C $x) use ($f, $g) : A2 {
return $f($g($x));
}
}
$a = foo(
function(B $b) : A2 { return new A2;},
function(C $c) : B { return new B;}
)(new C);',
'assertions' => [
'$a' => 'A',
],
],
'returnsTypedCallableFromClosure' => [
'<?php
class A {}
class B {}
class C {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return callable(C):A
*/
function foo(Closure $f, Closure $g) : callable {
return function (C $x) use ($f, $g) : A {
return $f($g($x));
}
}
$a = foo(
function(B $b) : A { return new A;},
function(C $c) : B { return new B;}
)(new C);',
'assertions' => [
'$a' => 'A',
],
],
];
}
@ -447,6 +554,155 @@ class CallableTest extends TestCase
bar($add_one);',
'error_message' => 'InvalidReturnStatement',
],
'returnsTypedClosureWithBadReturnType' => [
'<?php
/**
* @param Closure(int):int $f
* @param Closure(int):int $g
*
* @return Closure(int):string
*/
function foo(Closure $f, Closure $g) : Closure {
return function (int $x) use ($f, $g) : int {
return $f($g($x));
}
}',
'error_message' => 'InvalidReturnStatement',
],
'returnsTypedCallableWithBadReturnType' => [
'<?php
/**
* @param Closure(int):int $f
* @param Closure(int):int $g
*
* @return callable(int):string
*/
function foo(Closure $f, Closure $g) : callable {
return function (int $x) use ($f, $g) : int {
return $f($g($x));
}
}',
'error_message' => 'InvalidReturnStatement',
],
'returnsTypedClosureWithBadParamType' => [
'<?php
/**
* @param Closure(int):int $f
* @param Closure(int):int $g
*
* @return Closure(string):int
*/
function foo(Closure $f, Closure $g) : Closure {
return function (int $x) use ($f, $g) : int {
return $f($g($x));
}
}',
'error_message' => 'InvalidReturnStatement',
],
'returnsTypedCallableWithBadParamType' => [
'<?php
/**
* @param Closure(int):int $f
* @param Closure(int):int $g
*
* @return callable(string):int
*/
function foo(Closure $f, Closure $g) : callable {
return function (int $x) use ($f, $g) : int {
return $f($g($x));
}
}',
'error_message' => 'InvalidReturnStatement',
],
'returnsTypedClosureWithBadCall' => [
'<?php
class A {}
class B {}
class C {}
class D {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return Closure(C):A
*/
function foo(Closure $f, Closure $g) : Closure {
return function (int $x) use ($f, $g) : int {
return $f($g($x));
}
}',
'error_message' => 'InvalidArgument',
],
'returnsTypedClosureWithSubclassParam' => [
'<?php
class A {}
class B {}
class C {}
class C2 extends C {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return Closure(C2):A
*/
function foo(Closure $f, Closure $g) : Closure {
return function (C $x) use ($f, $g) : A {
return $f($g($x));
}
}',
'error_message' => 'LessSpecificReturnStatement',
],
'returnsTypedClosureWithSubclassReturn' => [
'<?php
class A {}
class B {}
class C {}
class A2 extends A {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return Closure(C):A2
*/
function foo(Closure $f, Closure $g) : Closure {
return function (C $x) use ($f, $g) : A {
return $f($g($x));
}
}',
'error_message' => 'LessSpecificReturnStatement',
],
'returnsTypedClosureFromCallable' => [
'<?php
class A {}
class B {}
class C {}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return callable(C):A
*/
function foo(Closure $f, Closure $g) : callable {
return function (C $x) use ($f, $g) : A {
return $f($g($x));
}
}
/**
* @param Closure(B):A $f
* @param Closure(C):B $g
*
* @return Closure(C):A
*/
function bar(Closure $f, Closure $g) : Closure {
return foo($f, $g);
}',
'error_message' => 'LessSpecificReturnStatement',
],
];
}
}