1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Fix #99 - treat null coalesce more respectfully

This commit is contained in:
Matthew Brown 2017-02-17 20:50:47 -05:00
parent 870a4486a8
commit 9137727993
4 changed files with 252 additions and 19 deletions

View File

@ -384,6 +384,37 @@ class AssertionFinder
return $if_types;
}
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Coalesce) {
$var_name = ExpressionChecker::getArrayVarId(
$conditional->left,
$this_class_name,
$source
);
if ($var_name) {
$if_types[$var_name] = 'isset';
} else {
// look for any variables we *can* use for an isset assertion
$array_root = $conditional->left;
while ($array_root instanceof PhpParser\Node\Expr\ArrayDimFetch && !$var_name) {
$array_root = $array_root->var;
$var_name = ExpressionChecker::getArrayVarId(
$array_root,
$this_class_name,
$source
);
}
if ($var_name) {
$if_types[$var_name] = '^isset';
}
}
return $if_types;
}
return [];
}

View File

@ -854,6 +854,92 @@ class ExpressionChecker
if (self::analyze($statements_checker, $stmt->right, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Coalesce) {
$t_if_context = clone $context;
$if_clauses = TypeChecker::getFormula(
$stmt,
$statements_checker->getFQCLN(),
$statements_checker
);
$ternary_clauses = TypeChecker::simplifyCNF(array_merge($context->clauses, $if_clauses));
$negated_clauses = TypeChecker::negateFormula($if_clauses);
$negated_if_types = TypeChecker::getTruthsFromFormula($negated_clauses);
$reconcilable_if_types = TypeChecker::getTruthsFromFormula($ternary_clauses);
$changed_vars = [];
$t_if_vars_in_scope_reconciled = TypeChecker::reconcileKeyedTypes(
$reconcilable_if_types,
$t_if_context->vars_in_scope,
$changed_vars,
$statements_checker->getFileChecker(),
new CodeLocation($statements_checker->getSource(), $stmt->left),
$statements_checker->getSuppressedIssues()
);
if ($t_if_vars_in_scope_reconciled === false) {
return false;
}
$t_if_context->vars_in_scope = $t_if_vars_in_scope_reconciled;
if (self::analyze($statements_checker, $stmt->left, $t_if_context) === false) {
return false;
}
if ($context->count_references) {
$context->referenced_vars = array_merge(
$context->referenced_vars,
$t_if_context->referenced_vars
);
}
$t_else_context = clone $context;
if ($negated_if_types) {
$t_else_vars_in_scope_reconciled = TypeChecker::reconcileKeyedTypes(
$negated_if_types,
$t_else_context->vars_in_scope,
$changed_vars,
$statements_checker->getFileChecker(),
new CodeLocation($statements_checker->getSource(), $stmt->right),
$statements_checker->getSuppressedIssues()
);
if ($t_else_vars_in_scope_reconciled === false) {
return false;
}
$t_else_context->vars_in_scope = $t_else_vars_in_scope_reconciled;
}
if (self::analyze($statements_checker, $stmt->right, $t_else_context) === false) {
return false;
}
if ($context->count_references) {
$context->referenced_vars = array_merge(
$context->referenced_vars,
$t_else_context->referenced_vars
);
}
$lhs_type = null;
if (isset($stmt->left->inferredType)) {
$lhs_type = $stmt->left->inferredType;
}
if (!$lhs_type || !isset($stmt->right->inferredType)) {
$stmt->inferredType = Type::getMixed();
} else {
$stmt->inferredType = Type::combineUnionTypes($lhs_type, $stmt->right->inferredType);
}
} else {
if ($stmt->left instanceof PhpParser\Node\Expr\BinaryOp) {
if (self::analyzeBinaryOp($statements_checker, $stmt->left, $context, ++$nesting) === false) {

View File

@ -537,25 +537,7 @@ class ArrayAssignmentTest extends PHPUnit_Framework_TestCase
$this->assertEquals('array{a:string, b:int}', (string) $context->vars_in_scope['$foo']);
}
/**
* @return void
*/
public function testIssetKeyedOffset()
{
$file_checker = new FileChecker(
'somefile.php',
$this->project_checker,
self::$parser->parse('<?php
if (!isset($foo["a"])) {
$foo["a"] = "hello";
}
')
);
$context = new Context();
$context->vars_in_scope['$foo'] = \Psalm\Type::getArray();
$file_checker->visitAndAnalyzeMethods($context);
$this->assertEquals('mixed', (string) $context->vars_in_scope['$foo[\'a\']']);
}
/**
* @return void

134
tests/IssetTest.php Normal file
View File

@ -0,0 +1,134 @@
<?php
namespace Psalm\Tests;
use PhpParser\ParserFactory;
use PHPUnit_Framework_TestCase;
use Psalm\Checker\FileChecker;
use Psalm\Config;
use Psalm\Context;
class IssetTest extends PHPUnit_Framework_TestCase
{
/** @var \PhpParser\Parser */
protected static $parser;
/** @var \Psalm\Checker\ProjectChecker */
protected $project_checker;
/**
* @return void
*/
public static function setUpBeforeClass()
{
self::$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
}
/**
* @return void
*/
public function setUp()
{
FileChecker::clearCache();
$this->project_checker = new \Psalm\Checker\ProjectChecker();
$this->project_checker->setConfig(new TestConfig());
}
/**
* @return void
*/
public function testIsset()
{
Config::getInstance()->setCustomErrorLevel('MixedAssignment', Config::REPORT_SUPPRESS);
$file_checker = new FileChecker(
'somefile.php',
$this->project_checker,
self::$parser->parse('<?php
$a = isset($b) ? $b : null;
')
);
$context = new Context();
$context->vars_in_scope['$foo'] = \Psalm\Type::getArray();
$file_checker->visitAndAnalyzeMethods($context);
$this->assertEquals('mixed', (string) $context->vars_in_scope['$a']);
}
/**
* @return void
*/
public function testNullCoalesce()
{
Config::getInstance()->setCustomErrorLevel('MixedAssignment', Config::REPORT_SUPPRESS);
$file_checker = new FileChecker(
'somefile.php',
$this->project_checker,
self::$parser->parse('<?php
$a = $b ?? null;
')
);
$context = new Context();
$context->vars_in_scope['$foo'] = \Psalm\Type::getArray();
$file_checker->visitAndAnalyzeMethods($context);
$this->assertEquals('mixed', (string) $context->vars_in_scope['$a']);
}
/**
* @return void
*/
public function testNullCoalesceWithGoodVariable()
{
$file_checker = new FileChecker(
'somefile.php',
$this->project_checker,
self::$parser->parse('<?php
$b = false;
$a = $b ?? null;
')
);
$context = new Context();
$context->vars_in_scope['$foo'] = \Psalm\Type::getArray();
$file_checker->visitAndAnalyzeMethods($context);
$this->assertEquals('false|null', (string) $context->vars_in_scope['$a']);
}
/**
* @return void
*/
public function testIssetKeyedOffset()
{
$file_checker = new FileChecker(
'somefile.php',
$this->project_checker,
self::$parser->parse('<?php
if (!isset($foo["a"])) {
$foo["a"] = "hello";
}
')
);
$context = new Context();
$context->vars_in_scope['$foo'] = \Psalm\Type::getArray();
$file_checker->visitAndAnalyzeMethods($context);
$this->assertEquals('mixed', (string) $context->vars_in_scope['$foo[\'a\']']);
}
/**
* @return void
*/
public function testNullCoalesceKeyedOffset()
{
Config::getInstance()->setCustomErrorLevel('MixedAssignment', Config::REPORT_SUPPRESS);
$file_checker = new FileChecker(
'somefile.php',
$this->project_checker,
self::$parser->parse('<?php
$foo["a"] = $foo["a"] ?? "hello";
')
);
$context = new Context();
$context->vars_in_scope['$foo'] = \Psalm\Type::getArray();
$file_checker->visitAndAnalyzeMethods($context);
$this->assertEquals('mixed', (string) $context->vars_in_scope['$foo[\'a\']']);
}
}