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:
parent
ee50542b8f
commit
77270dc9b7
@ -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));
|
||||
|
||||
|
@ -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 = [];
|
||||
|
||||
|
@ -67,4 +67,9 @@ class MethodStorage extends FunctionLikeStorage
|
||||
* @var ?array<string, bool>
|
||||
*/
|
||||
public $this_property_mutations = null;
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
public $plain_getter = null;
|
||||
}
|
||||
|
@ -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'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user