diff --git a/config.xsd b/config.xsd
index 98bed198d..5932e97a9 100644
--- a/config.xsd
+++ b/config.xsd
@@ -172,6 +172,7 @@
+
diff --git a/docs/issues.md b/docs/issues.md
index f7fa9fb7b..cc928030c 100644
--- a/docs/issues.md
+++ b/docs/issues.md
@@ -881,10 +881,19 @@ $arr[0] = "hello";
Emitted when it’s possible that the array offset is not applicable to the value you’re trying to access.
```php
-$arr = rand(0, 5) > 2 ? ["a" => 5] : "hello";
+$arr = rand(0, 1) ? ["a" => 5] : "hello";
echo $arr[0];
```
+### PossiblyInvalidFunctionCall
+
+Emitted when trying to call a function on a value that may not be callable
+
+```php
+$a = rand(0, 1) ? 5 : function() : int { return 5; };
+$b = $a();
+```
+
### PossiblyInvalidMethodCall
Emitted when trying to call a method on a value that may not be an object
diff --git a/src/Psalm/Checker/Statements/Expression/CallChecker.php b/src/Psalm/Checker/Statements/Expression/CallChecker.php
index 81adff4af..3111bb979 100644
--- a/src/Psalm/Checker/Statements/Expression/CallChecker.php
+++ b/src/Psalm/Checker/Statements/Expression/CallChecker.php
@@ -36,6 +36,7 @@ use Psalm\Issue\ParentNotFound;
use Psalm\Issue\PossiblyFalseArgument;
use Psalm\Issue\PossiblyFalseReference;
use Psalm\Issue\PossiblyInvalidArgument;
+use Psalm\Issue\PossiblyInvalidFunctionCall;
use Psalm\Issue\PossiblyInvalidMethodCall;
use Psalm\Issue\PossiblyNullArgument;
use Psalm\Issue\PossiblyNullFunctionCall;
@@ -168,6 +169,9 @@ class CallChecker
}
}
+ $invalid_function_call_types = [];
+ $has_valid_function_call_type = false;
+
foreach ($stmt->name->inferredType->types as $var_type_part) {
if ($var_type_part instanceof Type\Atomic\Fn) {
$function_params = $var_type_part->params;
@@ -182,11 +186,14 @@ class CallChecker
}
$function_exists = true;
+ $has_valid_function_call_type = true;
} elseif ($var_type_part instanceof TMixed) {
+ $has_valid_function_call_type = true;
// @todo maybe emit issue here
} elseif (($var_type_part instanceof TNamedObject && $var_type_part->value === 'Closure') ||
$var_type_part instanceof TCallable
) {
+ $has_valid_function_call_type = true;
// this is fine
} elseif ($var_type_part instanceof TNull) {
// handled above
@@ -206,9 +213,27 @@ class CallChecker
$statements_checker
);
+ $invalid_function_call_types[] = (string)$var_type_part;
+ }
+ }
+
+ if ($invalid_function_call_types) {
+ $var_type_part = reset($invalid_function_call_types);
+
+ if ($has_valid_function_call_type) {
+ if (IssueBuffer::accepts(
+ new PossiblyInvalidFunctionCall(
+ 'Cannot treat type ' . $var_type_part . ' as callable',
+ new CodeLocation($statements_checker->getSource(), $stmt)
+ ),
+ $statements_checker->getSuppressedIssues()
+ )) {
+ return false;
+ }
+ } else {
if (IssueBuffer::accepts(
new InvalidFunctionCall(
- 'Cannot treat ' . $var_id . ' of type ' . $var_type_part . ' as function',
+ 'Cannot treat type ' . $var_type_part . ' as callable',
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
diff --git a/src/Psalm/Issue/PossiblyInvalidFunctionCall.php b/src/Psalm/Issue/PossiblyInvalidFunctionCall.php
new file mode 100644
index 000000000..6b9a12c25
--- /dev/null
+++ b/src/Psalm/Issue/PossiblyInvalidFunctionCall.php
@@ -0,0 +1,6 @@
+