mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
parent
3f4083eac8
commit
7e3a1ec9c3
@ -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
|
||||
*
|
||||
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user