mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Update outer clauses in mic drop situation
This commit is contained in:
parent
0c0a30d6ea
commit
cecfe25df7
@ -177,6 +177,7 @@ class IfChecker
|
|||||||
$if_context,
|
$if_context,
|
||||||
$old_if_context,
|
$old_if_context,
|
||||||
$context,
|
$context,
|
||||||
|
$negated_clauses,
|
||||||
$pre_assignment_else_redefined_vars
|
$pre_assignment_else_redefined_vars
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -272,6 +273,7 @@ class IfChecker
|
|||||||
* @param Context $if_context
|
* @param Context $if_context
|
||||||
* @param Context $old_if_context
|
* @param Context $old_if_context
|
||||||
* @param Context $outer_context
|
* @param Context $outer_context
|
||||||
|
* @param array<Clause> $negated_clauses
|
||||||
* @param array<string,Type\Union> $pre_assignment_else_redefined_vars
|
* @param array<string,Type\Union> $pre_assignment_else_redefined_vars
|
||||||
* @return false|null
|
* @return false|null
|
||||||
*/
|
*/
|
||||||
@ -282,6 +284,7 @@ class IfChecker
|
|||||||
Context $if_context,
|
Context $if_context,
|
||||||
Context $old_if_context,
|
Context $old_if_context,
|
||||||
Context $outer_context,
|
Context $outer_context,
|
||||||
|
array $negated_clauses,
|
||||||
array $pre_assignment_else_redefined_vars
|
array $pre_assignment_else_redefined_vars
|
||||||
) {
|
) {
|
||||||
$has_ending_statements = ScopeChecker::doesAlwaysReturnOrThrow($stmt->stmts);
|
$has_ending_statements = ScopeChecker::doesAlwaysReturnOrThrow($stmt->stmts);
|
||||||
@ -338,28 +341,34 @@ class IfChecker
|
|||||||
|
|
||||||
$if_scope->redefined_vars = Context::getRedefinedVars($outer_context, $if_context);
|
$if_scope->redefined_vars = Context::getRedefinedVars($outer_context, $if_context);
|
||||||
$if_scope->possibly_redefined_vars = $if_scope->redefined_vars;
|
$if_scope->possibly_redefined_vars = $if_scope->redefined_vars;
|
||||||
} elseif (!$stmt->else && !$stmt->elseifs && $if_scope->negated_types) {
|
} elseif (!$stmt->else && !$stmt->elseifs) {
|
||||||
$changed_vars = [];
|
if ($if_scope->negated_types) {
|
||||||
|
$changed_vars = [];
|
||||||
|
|
||||||
$outer_context_vars_reconciled = TypeChecker::reconcileKeyedTypes(
|
$outer_context_vars_reconciled = TypeChecker::reconcileKeyedTypes(
|
||||||
$if_scope->negated_types,
|
$if_scope->negated_types,
|
||||||
$outer_context->vars_in_scope,
|
$outer_context->vars_in_scope,
|
||||||
$changed_vars,
|
$changed_vars,
|
||||||
$statements_checker->getFileChecker(),
|
$statements_checker->getFileChecker(),
|
||||||
new CodeLocation($statements_checker->getSource(), $stmt->cond),
|
new CodeLocation($statements_checker->getSource(), $stmt->cond),
|
||||||
$statements_checker->getSuppressedIssues()
|
$statements_checker->getSuppressedIssues()
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($changed_vars as $changed_var) {
|
||||||
|
$outer_context->removeVarFromClauses($changed_var);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($outer_context_vars_reconciled === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$outer_context->vars_in_scope = $outer_context_vars_reconciled;
|
||||||
|
$mic_drop = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$outer_context->clauses = TypeChecker::simplifyCNF(
|
||||||
|
array_merge($outer_context->clauses, $negated_clauses)
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ($changed_vars as $changed_var) {
|
|
||||||
$outer_context->removeVarFromClauses($changed_var);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($outer_context_vars_reconciled === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$outer_context->vars_in_scope = $outer_context_vars_reconciled;
|
|
||||||
$mic_drop = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the parent context as necessary, but only if we can safely reason about type negation.
|
// update the parent context as necessary, but only if we can safely reason about type negation.
|
||||||
@ -902,10 +911,6 @@ class IfChecker
|
|||||||
return $stmt;
|
return $stmt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp) {
|
|
||||||
return self::getDefinitelyEvaluatedExpression($stmt->left);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($stmt instanceof PhpParser\Node\Expr\BooleanNot) {
|
if ($stmt instanceof PhpParser\Node\Expr\BooleanNot) {
|
||||||
return self::getDefinitelyEvaluatedExpression($stmt->expr);
|
return self::getDefinitelyEvaluatedExpression($stmt->expr);
|
||||||
}
|
}
|
||||||
|
@ -416,6 +416,8 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
|||||||
unset($loop_context->vars_in_scope[$var_id]);
|
unset($loop_context->vars_in_scope[$var_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$loop_context->clauses = $pre_loop_context->clauses;
|
||||||
|
|
||||||
IssueBuffer::startRecording();
|
IssueBuffer::startRecording();
|
||||||
$this->analyze($stmts, $loop_context, $outer_context);
|
$this->analyze($stmts, $loop_context, $outer_context);
|
||||||
$recorded_issues = IssueBuffer::clearRecordingLevel();
|
$recorded_issues = IssueBuffer::clearRecordingLevel();
|
||||||
|
@ -413,121 +413,6 @@ class ScopeTest extends PHPUnit_Framework_TestCase
|
|||||||
$file_checker->visitAndAnalyzeMethods();
|
$file_checker->visitAndAnalyzeMethods();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testTwoVarLogic()
|
|
||||||
{
|
|
||||||
$stmts = self::$parser->parse('<?php
|
|
||||||
$a = rand(0, 10) ? "hello" : null;
|
|
||||||
$b = rand(0, 10) ? "goodbye" : null;
|
|
||||||
|
|
||||||
if ($a !== null || $b !== null) {
|
|
||||||
if ($a !== null) {
|
|
||||||
$c = $a;
|
|
||||||
} else {
|
|
||||||
$c = $b;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo strpos($c, "e");
|
|
||||||
}
|
|
||||||
');
|
|
||||||
|
|
||||||
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
|
||||||
$file_checker->visitAndAnalyzeMethods();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testThreeVarLogic()
|
|
||||||
{
|
|
||||||
$stmts = self::$parser->parse('<?php
|
|
||||||
$a = rand(0, 10) ? "hello" : null;
|
|
||||||
$b = rand(0, 10) ? "goodbye" : null;
|
|
||||||
$c = rand(0, 10) ? "hello" : null;
|
|
||||||
|
|
||||||
if ($a !== null || $b !== null || $c !== null) {
|
|
||||||
if ($a !== null) {
|
|
||||||
$d = $a;
|
|
||||||
} elseif ($b !== null) {
|
|
||||||
$d = $b;
|
|
||||||
} else {
|
|
||||||
$d = $c;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo strpos($d, "e");
|
|
||||||
}
|
|
||||||
');
|
|
||||||
|
|
||||||
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
|
||||||
$file_checker->visitAndAnalyzeMethods();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @expectedException \Psalm\Exception\CodeException
|
|
||||||
* @expectedExceptionMessage NullArgument
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testThreeVarLogicWithChange()
|
|
||||||
{
|
|
||||||
$stmts = self::$parser->parse('<?php
|
|
||||||
$a = rand(0, 10) ? "hello" : null;
|
|
||||||
$b = rand(0, 10) ? "goodbye" : null;
|
|
||||||
$c = rand(0, 10) ? "hello" : null;
|
|
||||||
|
|
||||||
if ($a !== null || $b !== null || $c !== null) {
|
|
||||||
$c = null;
|
|
||||||
|
|
||||||
if ($a !== null) {
|
|
||||||
$d = $a;
|
|
||||||
} elseif ($b !== null) {
|
|
||||||
$d = $b;
|
|
||||||
} else {
|
|
||||||
$d = $c;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo strpos($d, "e");
|
|
||||||
}
|
|
||||||
');
|
|
||||||
|
|
||||||
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
|
||||||
$file_checker->visitAndAnalyzeMethods();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @expectedException \Psalm\Exception\CodeException
|
|
||||||
* @expectedExceptionMessage NullArgument
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testThreeVarLogicWithException()
|
|
||||||
{
|
|
||||||
$stmts = self::$parser->parse('<?php
|
|
||||||
$a = rand(0, 10) ? "hello" : null;
|
|
||||||
$b = rand(0, 10) ? "goodbye" : null;
|
|
||||||
$c = rand(0, 10) ? "hello" : null;
|
|
||||||
|
|
||||||
if ($a !== null || $b !== null || $c !== null) {
|
|
||||||
if ($c !== null) {
|
|
||||||
throw new \Exception("bad");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($a !== null) {
|
|
||||||
$d = $a;
|
|
||||||
} elseif ($b !== null) {
|
|
||||||
$d = $b;
|
|
||||||
} else {
|
|
||||||
$d = $c;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo strpos($d, "e");
|
|
||||||
}
|
|
||||||
');
|
|
||||||
|
|
||||||
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
|
||||||
$file_checker->visitAndAnalyzeMethods();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
|
171
tests/TypeAlgebraTest.php
Normal file
171
tests/TypeAlgebraTest.php
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
namespace Psalm\Tests;
|
||||||
|
|
||||||
|
use PhpParser\ParserFactory;
|
||||||
|
use PHPUnit_Framework_TestCase;
|
||||||
|
use Psalm\Checker\FileChecker;
|
||||||
|
use Psalm\Config;
|
||||||
|
use Psalm\Context;
|
||||||
|
|
||||||
|
class TypeAlgebraTest extends PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
/** @var \PhpParser\Parser */
|
||||||
|
protected static $parser;
|
||||||
|
|
||||||
|
/** @var TestConfig */
|
||||||
|
protected static $config;
|
||||||
|
|
||||||
|
/** @var \Psalm\Checker\ProjectChecker */
|
||||||
|
protected $project_checker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function setUpBeforeClass()
|
||||||
|
{
|
||||||
|
self::$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
||||||
|
self::$config = new TestConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
FileChecker::clearCache();
|
||||||
|
$this->project_checker = new \Psalm\Checker\ProjectChecker();
|
||||||
|
$this->project_checker->setConfig(self::$config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testTwoVarLogic()
|
||||||
|
{
|
||||||
|
$stmts = self::$parser->parse('<?php
|
||||||
|
$a = rand(0, 10) ? "hello" : null;
|
||||||
|
$b = rand(0, 10) ? "goodbye" : null;
|
||||||
|
|
||||||
|
if ($a !== null || $b !== null) {
|
||||||
|
if ($a !== null) {
|
||||||
|
$c = $a;
|
||||||
|
} else {
|
||||||
|
$c = $b;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo strpos($c, "e");
|
||||||
|
}
|
||||||
|
');
|
||||||
|
|
||||||
|
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
||||||
|
$file_checker->visitAndAnalyzeMethods();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testThreeVarLogic()
|
||||||
|
{
|
||||||
|
$stmts = self::$parser->parse('<?php
|
||||||
|
$a = rand(0, 10) ? "hello" : null;
|
||||||
|
$b = rand(0, 10) ? "goodbye" : null;
|
||||||
|
$c = rand(0, 10) ? "hello" : null;
|
||||||
|
|
||||||
|
if ($a !== null || $b !== null || $c !== null) {
|
||||||
|
if ($a !== null) {
|
||||||
|
$d = $a;
|
||||||
|
} elseif ($b !== null) {
|
||||||
|
$d = $b;
|
||||||
|
} else {
|
||||||
|
$d = $c;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo strpos($d, "e");
|
||||||
|
}
|
||||||
|
');
|
||||||
|
|
||||||
|
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
||||||
|
$file_checker->visitAndAnalyzeMethods();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \Psalm\Exception\CodeException
|
||||||
|
* @expectedExceptionMessage NullArgument
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testThreeVarLogicWithChange()
|
||||||
|
{
|
||||||
|
$stmts = self::$parser->parse('<?php
|
||||||
|
$a = rand(0, 10) ? "hello" : null;
|
||||||
|
$b = rand(0, 10) ? "goodbye" : null;
|
||||||
|
$c = rand(0, 10) ? "hello" : null;
|
||||||
|
|
||||||
|
if ($a !== null || $b !== null || $c !== null) {
|
||||||
|
$c = null;
|
||||||
|
|
||||||
|
if ($a !== null) {
|
||||||
|
$d = $a;
|
||||||
|
} elseif ($b !== null) {
|
||||||
|
$d = $b;
|
||||||
|
} else {
|
||||||
|
$d = $c;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo strpos($d, "e");
|
||||||
|
}
|
||||||
|
');
|
||||||
|
|
||||||
|
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
||||||
|
$file_checker->visitAndAnalyzeMethods();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \Psalm\Exception\CodeException
|
||||||
|
* @expectedExceptionMessage NullArgument
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testThreeVarLogicWithException()
|
||||||
|
{
|
||||||
|
$stmts = self::$parser->parse('<?php
|
||||||
|
$a = rand(0, 10) ? "hello" : null;
|
||||||
|
$b = rand(0, 10) ? "goodbye" : null;
|
||||||
|
$c = rand(0, 10) ? "hello" : null;
|
||||||
|
|
||||||
|
if ($a !== null || $b !== null || $c !== null) {
|
||||||
|
if ($c !== null) {
|
||||||
|
throw new \Exception("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($a !== null) {
|
||||||
|
$d = $a;
|
||||||
|
} elseif ($b !== null) {
|
||||||
|
$d = $b;
|
||||||
|
} else {
|
||||||
|
$d = $c;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo strpos($d, "e");
|
||||||
|
}
|
||||||
|
');
|
||||||
|
|
||||||
|
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
||||||
|
$file_checker->visitAndAnalyzeMethods();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testTwoVarLogicNotNested()
|
||||||
|
{
|
||||||
|
$stmts = self::$parser->parse('<?php
|
||||||
|
function foo(?string $a, ?string $b) : string {
|
||||||
|
if (!$a && !$b) return "bad";
|
||||||
|
if (!$a) return $b;
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
');
|
||||||
|
|
||||||
|
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
||||||
|
$file_checker->visitAndAnalyzeMethods();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user