diff --git a/config.xsd b/config.xsd
index eb42a124e..5b67cc759 100644
--- a/config.xsd
+++ b/config.xsd
@@ -341,6 +341,7 @@
+
diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md
index 1b462ca75..399f58f71 100644
--- a/docs/running_psalm/issues.md
+++ b/docs/running_psalm/issues.md
@@ -2304,6 +2304,22 @@ class C {}
interface I extends C {}
```
+### UndefinedMagicMethod
+
+Emitted when calling a magic method that doesn’t exist
+
+```php
+/**
+ * @method bar():string
+ */
+class A {
+ public function __call(string $name, array $args) {
+ return "cool";
+ }
+}
+(new A)->foo();
+```
+
### UndefinedMagicPropertyAssignment
Emitted when assigning a property on an object that doesn’t have that magic property defined
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php
index 04a73bcd5..54f318709 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php
@@ -27,6 +27,7 @@ use Psalm\Issue\PossiblyNullReference;
use Psalm\Issue\PossiblyUndefinedMethod;
use Psalm\Issue\PropertyTypeCoercion;
use Psalm\Issue\UndefinedInterfaceMethod;
+use Psalm\Issue\UndefinedMagicMethod;
use Psalm\Issue\UndefinedMethod;
use Psalm\Issue\UndefinedThisPropertyAssignment;
use Psalm\Issue\UndefinedThisPropertyFetch;
@@ -188,6 +189,7 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
$non_existent_class_method_ids = [];
$non_existent_interface_method_ids = [];
+ $non_existent_magic_method_ids = [];
$existent_method_ids = [];
$has_mixed_method_call = false;
@@ -228,7 +230,8 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
$invalid_method_call_types,
$existent_method_ids,
$non_existent_class_method_ids,
- $non_existent_interface_method_ids
+ $non_existent_interface_method_ids,
+ $non_existent_magic_method_ids
);
if ($result === false) {
@@ -266,6 +269,21 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
}
}
+ if ($non_existent_magic_method_ids) {
+ if ($context->check_methods) {
+ if (IssueBuffer::accepts(
+ new UndefinedMagicMethod(
+ 'Magic method ' . $non_existent_magic_method_ids[0] . ' does not exist',
+ new CodeLocation($source, $stmt->name),
+ $non_existent_magic_method_ids[0]
+ ),
+ $statements_analyzer->getSuppressedIssues()
+ )) {
+ // keep going
+ }
+ }
+ }
+
if ($non_existent_class_method_ids) {
if ($context->check_methods) {
if ($existent_method_ids || $has_mixed_method_call) {
@@ -417,6 +435,7 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
* @param array &$existent_method_ids
* @param array &$non_existent_class_method_ids
* @param array &$non_existent_interface_method_ids
+ * @param array &$non_existent_magic_method_ids
* @return null|bool
*/
private static function analyzeAtomicCall(
@@ -436,6 +455,7 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
&$existent_method_ids,
&$non_existent_class_method_ids,
&$non_existent_interface_method_ids,
+ &$non_existent_magic_method_ids,
bool &$check_visibility = true
) {
$config = $codebase->config;
@@ -638,6 +658,7 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
foreach ($intersection_types as $intersection_type) {
$i_non_existent_class_method_ids = [];
$i_non_existent_interface_method_ids = [];
+ $i_non_existent_magic_method_ids = [];
$intersection_return_type = null;
@@ -658,6 +679,7 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
$all_intersection_existent_method_ids,
$i_non_existent_class_method_ids,
$i_non_existent_interface_method_ids,
+ $i_non_existent_magic_method_ids,
$check_visibility
);
@@ -801,7 +823,7 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
}
if ($class_storage->sealed_methods) {
- $non_existent_class_method_ids[] = $method_id;
+ $non_existent_magic_method_ids[] = $method_id;
return true;
}
}
diff --git a/src/Psalm/Issue/UndefinedMagicMethod.php b/src/Psalm/Issue/UndefinedMagicMethod.php
new file mode 100644
index 000000000..8714565e7
--- /dev/null
+++ b/src/Psalm/Issue/UndefinedMagicMethod.php
@@ -0,0 +1,6 @@
+getString();
$child->foo();',
- 'error_message' => 'UndefinedMethod - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:29 - Method Child::foo does not exist',
+ 'error_message' => 'UndefinedMagicMethod - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:29 - Magic method Child::foo does not exist',
],
'annotationInvalidArg' => [
'