diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php index b18d6dbeb..8dcbfe931 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php @@ -490,20 +490,30 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer $can_memoize = false; - $return_type_candidate = MethodCallReturnTypeFetcher::fetch( - $statements_analyzer, - $codebase, - $stmt, - $context, - $method_id, - $declaring_method_id, - $cased_method_id, - $lhs_type_part, - $static_type, - $args, - $result, - $template_result - ); + $class_storage_for_method = $codebase->methods->getClassLikeStorageForMethod($method_id); + $plain_getter_property = null; + if ((isset($class_storage_for_method->methods[$method_name_lc])) + && !$class_storage_for_method->methods[$method_name_lc]->overridden_somewhere + && !$class_storage_for_method->methods[$method_name_lc]->overridden_downstream + && ($plain_getter_property = $class_storage_for_method->methods[$method_name_lc]->plain_getter) + && isset($context->vars_in_scope[$getter_var_id = $lhs_var_id . '->' . $plain_getter_property])) { + $return_type_candidate = $context->vars_in_scope[$getter_var_id]; + } else { + $return_type_candidate = MethodCallReturnTypeFetcher::fetch( + $statements_analyzer, + $codebase, + $stmt, + $context, + $method_id, + $declaring_method_id, + $cased_method_id, + $lhs_type_part, + $static_type, + $args, + $result, + $template_result + ); + } $in_call_map = CallMap::inCallMap((string) ($declaring_method_id ?: $method_id)); diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index c3ba8bece..70b1ee4ae 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -1862,6 +1862,14 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse $storage->mutation_free = true; $storage->external_mutation_free = true; $storage->mutation_free_inferred = true; + + if ($stmt->stmts[0]->expr->name instanceof PhpParser\Node\Identifier) { + $storage->plain_getter = $stmt->stmts[0]->expr->name->name; + $storage->if_true_assertions[] = new \Psalm\Storage\Assertion( + '$this->' . $storage->plain_getter, + [['!falsy']] + ); + } } elseif (strpos($stmt->name->name, 'assert') === 0) { $var_assertions = []; diff --git a/src/Psalm/Storage/MethodStorage.php b/src/Psalm/Storage/MethodStorage.php index e6575e50b..7fd83ada5 100644 --- a/src/Psalm/Storage/MethodStorage.php +++ b/src/Psalm/Storage/MethodStorage.php @@ -67,4 +67,9 @@ class MethodStorage extends FunctionLikeStorage * @var ?array */ public $this_property_mutations = null; + + /** + * @var ?string + */ + public $plain_getter = null; } diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 4afb1070a..de2ee967a 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -575,6 +575,51 @@ class MethodCallTest extends TestCase takesWithoutArguments(new C);' ], + 'getterTypeInferring' => [ + 'a; + } + + function takesNullOrA(?A $a) : void {} + } + + $a = new A(); + + $a->a = 1; + echo $a->getA() + 2; + + $a->a = "string"; + echo strlen($a->getA()); + + $a->a = null; + $a->takesNullOrA($a->getA()); + ' + ], + 'getterAutomagicAssertion' => [ + 'a; + } + } + + $a = new A(); + + if ($a->getA()) { + echo strlen($a->getA()); + } + ' + ], ]; } @@ -959,6 +1004,65 @@ class MethodCallTest extends TestCase (new A)->fooFoo();', 'error_message' => 'TooFewArguments', ], + 'getterAutomagicOverridden' => [ + 'a; + } + } + + class AChild extends A { + function getA() { + return rand(0, 1) ? $this->a : null; + } + } + + function foo(A $a) : void { + if ($a->getA()) { + echo strlen($a->getA()); + } + } + + foo(new AChild());', + 'error_message' => 'PossiblyNullArgument' + ], + 'getterAutomagicOverriddenWithAssertion' => [ + 'a */ + function hasA() { + return is_string($this->a); + } + + /** @return string|null */ + function getA() { + return $this->a; + } + } + + class AChild extends A { + function getA() { + return rand(0, 1) ? $this->a : null; + } + } + + function foo(A $a) : void { + if ($a->hasA()) { + echo strlen($a->getA()); + } + } + + foo(new AChild());', + 'error_message' => 'PossiblyNullArgument' + ] ]; } }