diff --git a/config.xsd b/config.xsd
index 01ad02abc..061d84db6 100644
--- a/config.xsd
+++ b/config.xsd
@@ -158,6 +158,8 @@
+
+
diff --git a/src/Psalm/Checker/Statements/Expression/CallChecker.php b/src/Psalm/Checker/Statements/Expression/CallChecker.php
index 74a602d19..0a0f66210 100644
--- a/src/Psalm/Checker/Statements/Expression/CallChecker.php
+++ b/src/Psalm/Checker/Statements/Expression/CallChecker.php
@@ -35,6 +35,7 @@ use Psalm\Issue\ParentNotFound;
use Psalm\Issue\PossiblyFalseArgument;
use Psalm\Issue\PossiblyFalseReference;
use Psalm\Issue\PossiblyInvalidArgument;
+use Psalm\Issue\PossiblyInvalidMethodCall;
use Psalm\Issue\PossiblyNullArgument;
use Psalm\Issue\PossiblyNullFunctionCall;
use Psalm\Issue\PossiblyNullReference;
@@ -764,6 +765,9 @@ class CallChecker
$non_existent_method_ids = [];
$existent_method_ids = [];
+ $invalid_method_call_types = [];
+ $has_valid_method_call_type = false;
+
$code_location = new CodeLocation($source, $stmt);
if ($class_type && is_string($stmt->name)) {
@@ -782,15 +786,7 @@ class CallChecker
case 'Psalm\\Type\\Atomic\\TArray':
case 'Psalm\\Type\\Atomic\\TString':
case 'Psalm\\Type\\Atomic\\TNumericString':
- if (IssueBuffer::accepts(
- new InvalidMethodCall(
- 'Cannot call method ' . $stmt->name . ' on ' . $class_type . ' variable ' . $var_id,
- $code_location
- ),
- $statements_checker->getSuppressedIssues()
- )) {
- return false;
- }
+ $invalid_method_call_types[] = (string)$class_type_part;
break;
case 'Psalm\\Type\\Atomic\\TMixed':
@@ -810,6 +806,8 @@ class CallChecker
continue;
}
+ $has_valid_method_call_type = true;
+
$fq_class_name = $class_type_part->value;
$intersection_types = $class_type_part->getIntersectionTypes();
@@ -1030,6 +1028,32 @@ class CallChecker
}
}
+ if ($invalid_method_call_types) {
+ $class_type = $invalid_method_call_types[0];
+
+ if ($has_valid_method_call_type) {
+ if (IssueBuffer::accepts(
+ new PossiblyInvalidMethodCall(
+ 'Cannot call method on possible ' . $class_type . ' variable ' . $var_id,
+ $code_location
+ ),
+ $statements_checker->getSuppressedIssues()
+ )) {
+ return false;
+ }
+ } else {
+ if (IssueBuffer::accepts(
+ new InvalidMethodCall(
+ 'Cannot call method on ' . $class_type . ' variable ' . $var_id,
+ $code_location
+ ),
+ $statements_checker->getSuppressedIssues()
+ )) {
+ return false;
+ }
+ }
+ }
+
if ($non_existent_method_ids) {
if ($existent_method_ids) {
if (IssueBuffer::accepts(
diff --git a/src/Psalm/Issue/PossiblyInvalidMethodCall.php b/src/Psalm/Issue/PossiblyInvalidMethodCall.php
new file mode 100644
index 000000000..6413fb65d
--- /dev/null
+++ b/src/Psalm/Issue/PossiblyInvalidMethodCall.php
@@ -0,0 +1,6 @@
+ [
+ 'someMethod();',
+ 'error_message' => 'InvalidMethodCall',
+ ],
+ 'possiblyInvalidMethodCall' => [
+ 'methodOfA();
+ }
+ }',
+ 'error_message' => 'PossiblyInvalidMethodCall',
+ ],
'selfNonStaticInvocation' => [
'