1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00

Getters automagic (#3122)

* When method is a plain getter: (1) correct method return type if property type is known (2) auto assert-if-true that corresponding property is not falsy

* do not use getter automagic if getter is overridden somewhere
This commit is contained in:
m0003r 2020-04-12 15:40:24 +03:00 committed by GitHub
parent ee50542b8f
commit 77270dc9b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 141 additions and 14 deletions

View File

@ -490,6 +490,15 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
$can_memoize = false;
$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,
@ -504,6 +513,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
$result,
$template_result
);
}
$in_call_map = CallMap::inCallMap((string) ($declaring_method_id ?: $method_id));

View File

@ -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 = [];

View File

@ -67,4 +67,9 @@ class MethodStorage extends FunctionLikeStorage
* @var ?array<string, bool>
*/
public $this_property_mutations = null;
/**
* @var ?string
*/
public $plain_getter = null;
}

View File

@ -575,6 +575,51 @@ class MethodCallTest extends TestCase
takesWithoutArguments(new C);'
],
'getterTypeInferring' => [
'<?php
class A {
/** @var int|string|null */
public $a;
/** @return int|string|null */
function getA() {
return $this->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' => [
'<?php
class A {
/** @var string|null */
public $a;
/** @return string|null */
function getA() {
return $this->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' => [
'<?php
class A {
/** @var string|null */
public $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->getA()) {
echo strlen($a->getA());
}
}
foo(new AChild());',
'error_message' => 'PossiblyNullArgument'
],
'getterAutomagicOverriddenWithAssertion' => [
'<?php
class A {
/** @var string|null */
public $a;
/** @psalm-assert-if-true string $this->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'
]
];
}
}