From 956199c688e92f81c4ea7a14bbb13a196154cad2 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Sat, 3 Oct 2020 20:21:44 -0400 Subject: [PATCH] 4.x - add support for the nullsafe operator --- .../Expression/NullsafeAnalyzer.php | 87 +++++++++++++++++++ .../Statements/ExpressionAnalyzer.php | 6 ++ tests/TypeReconciliation/ConditionalTest.php | 44 ++++++++++ 3 files changed, 137 insertions(+) create mode 100644 src/Psalm/Internal/Analyzer/Statements/Expression/NullsafeAnalyzer.php diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/NullsafeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/NullsafeAnalyzer.php new file mode 100644 index 000000000..2bc890172 --- /dev/null +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/NullsafeAnalyzer.php @@ -0,0 +1,87 @@ +var instanceof PhpParser\Node\Expr\Variable) { + ExpressionAnalyzer::analyze($statements_analyzer, $stmt->var, $context); + + $tmp_name = '__tmp_nullsafe__' . (int) $stmt->var->getAttribute('startFilePos'); + + $condition_type = $statements_analyzer->node_data->getType($stmt->var); + + if ($condition_type) { + $context->vars_in_scope['$' . $tmp_name] = $condition_type; + + $tmp_var = new PhpParser\Node\Expr\Variable($tmp_name, $stmt->var->getAttributes()); + } else { + $tmp_var = $stmt->var; + } + } else { + $tmp_var = $stmt->var; + } + + $old_node_data = $statements_analyzer->node_data; + $statements_analyzer->node_data = clone $statements_analyzer->node_data; + + $null_value1 = new PhpParser\Node\Expr\ConstFetch( + new PhpParser\Node\Name('null'), + $stmt->var->getAttributes() + ); + + $null_comparison = new PhpParser\Node\Expr\BinaryOp\Identical( + $tmp_var, + $null_value1, + $stmt->var->getAttributes() + ); + + $null_value2 = new PhpParser\Node\Expr\ConstFetch( + new PhpParser\Node\Name('null'), + $stmt->var->getAttributes() + ); + + if ($stmt instanceof PhpParser\Node\Expr\NullsafePropertyFetch) { + $ternary = new PhpParser\Node\Expr\Ternary( + $null_comparison, + $null_value2, + new PhpParser\Node\Expr\PropertyFetch($tmp_var, $stmt->name, $stmt->getAttributes()), + $stmt->getAttributes() + ); + } else { + $ternary = new PhpParser\Node\Expr\Ternary( + $null_comparison, + $null_value2, + new PhpParser\Node\Expr\MethodCall($tmp_var, $stmt->name, $stmt->args, $stmt->getAttributes()), + $stmt->getAttributes() + ); + } + + ExpressionAnalyzer::analyze($statements_analyzer, $ternary, $context); + + $ternary_type = $statements_analyzer->node_data->getType($ternary); + + $statements_analyzer->node_data = $old_node_data; + + $statements_analyzer->node_data->setType($stmt, $ternary_type ?: Type::getMixed()); + + return true; + } +} diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 18d70c498..e7edcff9d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -375,6 +375,12 @@ class ExpressionAnalyzer return ThrowAnalyzer::analyze($statements_analyzer, $stmt, $context); } + if ($stmt instanceof PhpParser\Node\Expr\NullsafePropertyFetch + || $stmt instanceof PhpParser\Node\Expr\NullsafeMethodCall + ) { + return Expression\NullsafeAnalyzer::analyze($statements_analyzer, $stmt, $context); + } + if ($stmt instanceof PhpParser\Node\Expr\Error) { // do nothing return true; diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index ecfe0e22c..5546b0260 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -2822,6 +2822,50 @@ class ConditionalTest extends \Psalm\Tests\TestCase } }' ], + 'nullsafePropertyAccess' => [ + 'next?->value; + } + + function skipTwo(IntLinkedList $l) : ?int { + return $l->next?->next?->value; + }', + [], + [], + '8.0' + ], + 'nullsafeMethodCall' => [ + 'next; + } + } + + function skipOne(IntLinkedList $l) : ?int { + return $l->getNext()?->value; + } + + function skipTwo(IntLinkedList $l) : ?int { + return $l->getNext()?->getNext()?->value; + }', + [], + [], + '8.0' + ] ]; }