From 19faa318656fe57b9a51a17224679de241ea948e Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Mon, 30 Dec 2019 10:01:31 -0500 Subject: [PATCH] Allow assertions on nested properties --- .../Statements/Expression/AssertionFinder.php | 10 +-- .../Expression/Call/FunctionCallAnalyzer.php | 4 +- .../Expression/Call/MethodCallAnalyzer.php | 10 +-- .../Expression/Call/StaticCallAnalyzer.php | 4 +- src/Psalm/Storage/Assertion.php | 6 +- tests/AssertAnnotationTest.php | 64 +++++++++++++++++++ 6 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 868f51a11..a008f9d47 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -130,7 +130,11 @@ class AssertionFinder if ($var_name) { $if_types[$var_name] = [['!falsy']]; - return $if_types; + if (!$conditional instanceof PhpParser\Node\Expr\MethodCall + && !$conditional instanceof PhpParser\Node\Expr\StaticCall + ) { + return $if_types; + } } if ($conditional instanceof PhpParser\Node\Expr\Assign) { @@ -332,7 +336,7 @@ class AssertionFinder if ($conditional instanceof PhpParser\Node\Expr\MethodCall || $conditional instanceof PhpParser\Node\Expr\StaticCall ) { - $if_types = self::processCustomAssertion($conditional, $this_class_name, $source, false); + $if_types += self::processCustomAssertion($conditional, $this_class_name, $source, false); return $if_types; } @@ -1950,7 +1954,6 @@ class AssertionFinder } } } elseif (\is_string($assertion->var_id) - && strpos($assertion->var_id, '$this->') === 0 && $expr instanceof PhpParser\Node\Expr\MethodCall ) { if ($prefix === $assertion->rule[0][0][0]) { @@ -1999,7 +2002,6 @@ class AssertionFinder } } } elseif (\is_string($assertion->var_id) - && strpos($assertion->var_id, '$this->') === 0 && $expr instanceof PhpParser\Node\Expr\MethodCall ) { if ($prefix === $assertion->rule[0][0][0]) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 904c1a2f7..27d03b2ba 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -778,7 +778,7 @@ class FunctionCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expressio $stmt, array_map( function (Assertion $assertion) use ($generic_params) : Assertion { - return $assertion->getUntemplatedCopy($generic_params ?: []); + return $assertion->getUntemplatedCopy($generic_params ?: [], null); }, $function_storage->if_true_assertions ) @@ -790,7 +790,7 @@ class FunctionCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expressio $stmt, array_map( function (Assertion $assertion) use ($generic_params) : Assertion { - return $assertion->getUntemplatedCopy($generic_params ?: []); + return $assertion->getUntemplatedCopy($generic_params ?: [], null); }, $function_storage->if_false_assertions ) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index 129040055..9ff8d6562 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -1398,8 +1398,9 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ $statements_analyzer->node_data->setIfTrueAssertions( $stmt, array_map( - function (Assertion $assertion) use ($class_template_params) : Assertion { - return $assertion->getUntemplatedCopy($class_template_params ?: []); + function (Assertion $assertion) + use ($class_template_params, $lhs_var_id) : Assertion { + return $assertion->getUntemplatedCopy($class_template_params ?: [], $lhs_var_id); }, $method_storage->if_true_assertions ) @@ -1410,8 +1411,9 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ $statements_analyzer->node_data->setIfFalseAssertions( $stmt, array_map( - function (Assertion $assertion) use ($class_template_params) : Assertion { - return $assertion->getUntemplatedCopy($class_template_params ?: []); + function (Assertion $assertion) + use ($class_template_params, $lhs_var_id) : Assertion { + return $assertion->getUntemplatedCopy($class_template_params ?: [], $lhs_var_id); }, $method_storage->if_false_assertions ) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 7336b1136..26807d42b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -973,7 +973,7 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ $stmt, array_map( function (Assertion $assertion) use ($generic_params) : Assertion { - return $assertion->getUntemplatedCopy($generic_params); + return $assertion->getUntemplatedCopy($generic_params, null); }, $method_storage->if_true_assertions ) @@ -985,7 +985,7 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ $stmt, array_map( function (Assertion $assertion) use ($generic_params) : Assertion { - return $assertion->getUntemplatedCopy($generic_params); + return $assertion->getUntemplatedCopy($generic_params, null); }, $method_storage->if_false_assertions ) diff --git a/src/Psalm/Storage/Assertion.php b/src/Psalm/Storage/Assertion.php index 70f5e16ff..0baf88a24 100644 --- a/src/Psalm/Storage/Assertion.php +++ b/src/Psalm/Storage/Assertion.php @@ -30,10 +30,12 @@ class Assertion /** * @param array> $template_type_map */ - public function getUntemplatedCopy(array $template_type_map) : self + public function getUntemplatedCopy(array $template_type_map, ?string $this_var_id) : self { return new Assertion( - $this->var_id, + is_string($this->var_id) && $this_var_id + ? str_replace('$this->', $this_var_id . '->', $this->var_id) + : $this->var_id, array_map( /** * @param array $rules diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index d57b3f65d..fbc7cabb7 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -730,6 +730,70 @@ class AssertAnnotationTest extends TestCase throw new \Exception(); }' ], + 'assertOnNestedProperty' => [ + 'arr = $arr; + } + } + + /** @psalm-immutable */ + class A { + public B $b; + public function __construct(B $b) { + $this->b = $b; + } + + /** @psalm-assert-if-true !null $this->b->arr */ + public function hasArray() : bool { + return $this->b->arr !== null; + } + } + + function foo(A $a) : void { + if ($a->hasArray()) { + echo count($a->b->arr); + } + }' + ], + 'assertOnNestedMethod' => [ + 'arr = $arr; + } + + public function getArray() : ?array { + return $this->arr; + } + } + + /** @psalm-immutable */ + class A { + public B $b; + public function __construct(B $b) { + $this->b = $b; + } + + /** @psalm-assert-if-true !null $this->b->getarray() */ + public function hasArray() : bool { + return $this->b->getArray() !== null; + } + } + + function foo(A $a) : void { + if ($a->hasArray()) { + echo count($a->b->getArray()); + } + }' + ], ]; }