From 083fbc55e1f67c4eff6ccc6eb8569ebad3eb5528 Mon Sep 17 00:00:00 2001 From: Brown Date: Mon, 6 Jan 2020 14:23:33 -0500 Subject: [PATCH] Add more specific UndefinedMagicMethod issue --- config.xsd | 1 + docs/running_psalm/issues.md | 16 ++++++++++++ .../Expression/Call/MethodCallAnalyzer.php | 26 +++++++++++++++++-- src/Psalm/Issue/UndefinedMagicMethod.php | 6 +++++ tests/MagicMethodAnnotationTest.php | 2 +- 5 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 src/Psalm/Issue/UndefinedMagicMethod.php 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' => [ '