mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Improve reliability of elseif resolution
This commit is contained in:
parent
549e90eca4
commit
eb10b15cfc
@ -423,7 +423,7 @@ return [
|
||||
'BadMethodCallException::getPrevious' => ['?Throwable|?BadMethodCallException'],
|
||||
'BadMethodCallException::getTrace' => ['array'],
|
||||
'BadMethodCallException::getTraceAsString' => ['string'],
|
||||
'base64_decode' => ['string', 'str'=>'string', 'strict='=>'bool'],
|
||||
'base64_decode' => ['string|false', 'str'=>'string', 'strict='=>'bool'],
|
||||
'base64_encode' => ['string', 'str'=>'string'],
|
||||
'base_convert' => ['string', 'number'=>'string', 'frombase'=>'int', 'tobase'=>'int'],
|
||||
'basename' => ['string', 'path'=>'string', 'suffix='=>'string'],
|
||||
|
@ -188,6 +188,12 @@ class IfChecker
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($if_context->vars_possibly_in_scope as $var_id => $_) {
|
||||
if (!isset($if_context->vars_in_scope[$var_id])) {
|
||||
$mixed_var_ids[] = $var_id;
|
||||
}
|
||||
}
|
||||
|
||||
$if_clauses = Algebra::getFormula(
|
||||
$stmt->cond,
|
||||
$context->self,
|
||||
@ -389,36 +395,34 @@ class IfChecker
|
||||
}
|
||||
|
||||
// check the else
|
||||
if ($stmt->else) {
|
||||
$else_context = clone $original_context;
|
||||
$else_context = clone $original_context;
|
||||
|
||||
if ($stmt->else) {
|
||||
if ($project_checker->alter_code) {
|
||||
$else_context->branch_point =
|
||||
$else_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
|
||||
}
|
||||
}
|
||||
|
||||
if (self::analyzeElseBlock(
|
||||
$statements_checker,
|
||||
$stmt->else,
|
||||
$if_scope,
|
||||
$else_context,
|
||||
$context,
|
||||
$loop_scope
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
if (self::analyzeElseBlock(
|
||||
$statements_checker,
|
||||
$stmt->else,
|
||||
$if_scope,
|
||||
$else_context,
|
||||
$context,
|
||||
$loop_scope
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($context->collect_references && $if_scope->final_actions !== [ScopeChecker::ACTION_END]) {
|
||||
foreach ($else_context->unreferenced_vars as $var_id => $location) {
|
||||
if (!isset($old_unreferenced_vars[$var_id])
|
||||
|| $old_unreferenced_vars[$var_id] !== $location
|
||||
) {
|
||||
$newly_unreferenced_locations[$var_id][] = $location;
|
||||
}
|
||||
if ($context->collect_references && $if_scope->final_actions !== [ScopeChecker::ACTION_END]) {
|
||||
foreach ($else_context->unreferenced_vars as $var_id => $location) {
|
||||
if (!isset($old_unreferenced_vars[$var_id])
|
||||
|| $old_unreferenced_vars[$var_id] !== $location
|
||||
) {
|
||||
$newly_unreferenced_locations[$var_id][] = $location;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$if_scope->final_actions[] = ScopeChecker::ACTION_NONE;
|
||||
}
|
||||
|
||||
if ($loop_scope) {
|
||||
@ -441,56 +445,50 @@ class IfChecker
|
||||
);
|
||||
|
||||
// vars can only be defined/redefined if there was an else (defined in every block)
|
||||
if ($stmt->else) {
|
||||
$context->assigned_var_ids = array_merge(
|
||||
$context->assigned_var_ids,
|
||||
$if_scope->assigned_var_ids ?: []
|
||||
);
|
||||
$context->assigned_var_ids = array_merge(
|
||||
$context->assigned_var_ids,
|
||||
$if_scope->assigned_var_ids ?: []
|
||||
);
|
||||
|
||||
if ($if_scope->new_vars) {
|
||||
$context->vars_in_scope = array_merge($context->vars_in_scope, $if_scope->new_vars);
|
||||
}
|
||||
if ($if_scope->new_vars) {
|
||||
$context->vars_in_scope = array_merge($context->vars_in_scope, $if_scope->new_vars);
|
||||
}
|
||||
|
||||
if ($if_scope->redefined_vars) {
|
||||
foreach ($if_scope->redefined_vars as $var_id => $type) {
|
||||
$context->vars_in_scope[$var_id] = $type;
|
||||
$if_scope->updated_vars[$var_id] = true;
|
||||
if ($if_scope->redefined_vars) {
|
||||
foreach ($if_scope->redefined_vars as $var_id => $type) {
|
||||
$context->vars_in_scope[$var_id] = $type;
|
||||
$if_scope->updated_vars[$var_id] = true;
|
||||
|
||||
if ($if_scope->reasonable_clauses) {
|
||||
$if_scope->reasonable_clauses = Context::filterClauses(
|
||||
$var_id,
|
||||
$if_scope->reasonable_clauses,
|
||||
isset($context->vars_in_scope[$var_id])
|
||||
? $context->vars_in_scope[$var_id]
|
||||
: null,
|
||||
$statements_checker
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($if_scope->possible_param_types) {
|
||||
foreach ($if_scope->possible_param_types as $var => $type) {
|
||||
$context->possible_param_types[$var] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($if_scope->reasonable_clauses
|
||||
&& (count($if_scope->reasonable_clauses) > 1 || !$if_scope->reasonable_clauses[0]->wedge)
|
||||
) {
|
||||
$context->clauses = Algebra::simplifyCNF(
|
||||
array_merge(
|
||||
if ($if_scope->reasonable_clauses) {
|
||||
$if_scope->reasonable_clauses = Context::filterClauses(
|
||||
$var_id,
|
||||
$if_scope->reasonable_clauses,
|
||||
$context->clauses
|
||||
)
|
||||
);
|
||||
isset($context->vars_in_scope[$var_id])
|
||||
? $context->vars_in_scope[$var_id]
|
||||
: null,
|
||||
$statements_checker
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($loop_scope && !in_array(ScopeChecker::ACTION_NONE, $if_scope->final_actions, true)) {
|
||||
$loop_scope->redefined_loop_vars = null;
|
||||
}
|
||||
|
||||
if ($if_scope->possible_param_types) {
|
||||
foreach ($if_scope->possible_param_types as $var => $type) {
|
||||
$context->possible_param_types[$var] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($if_scope->reasonable_clauses
|
||||
&& (count($if_scope->reasonable_clauses) > 1 || !$if_scope->reasonable_clauses[0]->wedge)
|
||||
) {
|
||||
$context->clauses = Algebra::simplifyCNF(
|
||||
array_merge(
|
||||
$if_scope->reasonable_clauses,
|
||||
$context->clauses
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ($if_scope->possibly_redefined_vars) {
|
||||
foreach ($if_scope->possibly_redefined_vars as $var_id => $type) {
|
||||
if ($context->hasVariable($var_id)
|
||||
@ -1178,7 +1176,7 @@ class IfChecker
|
||||
array_keys($negated_elseif_types),
|
||||
$if_scope->updated_vars
|
||||
);
|
||||
} else {
|
||||
} elseif ($entry_clauses && (count($entry_clauses) > 1 || !$entry_clauses[0]->wedge)) {
|
||||
$outer_context->update(
|
||||
$old_elseif_context,
|
||||
$elseif_context,
|
||||
@ -1243,7 +1241,7 @@ class IfChecker
|
||||
|
||||
/**
|
||||
* @param StatementsChecker $statements_checker
|
||||
* @param PhpParser\Node\Stmt\Else_ $else
|
||||
* @param PhpParser\Node\Stmt\Else_|null $else
|
||||
* @param IfScope $if_scope
|
||||
* @param Context $else_context
|
||||
* @param Context $outer_context
|
||||
@ -1252,7 +1250,7 @@ class IfChecker
|
||||
*/
|
||||
protected static function analyzeElseBlock(
|
||||
StatementsChecker $statements_checker,
|
||||
PhpParser\Node\Stmt\Else_ $else,
|
||||
$else,
|
||||
IfScope $if_scope,
|
||||
Context $else_context,
|
||||
Context $outer_context,
|
||||
@ -1280,7 +1278,9 @@ class IfChecker
|
||||
$changed_var_ids,
|
||||
[],
|
||||
$statements_checker,
|
||||
new CodeLocation($statements_checker->getSource(), $else, $outer_context->include_location),
|
||||
$else
|
||||
? new CodeLocation($statements_checker->getSource(), $else, $outer_context->include_location)
|
||||
: null,
|
||||
$statements_checker->getSuppressedIssues()
|
||||
);
|
||||
|
||||
@ -1294,20 +1294,22 @@ class IfChecker
|
||||
$pre_stmts_assigned_var_ids = $else_context->assigned_var_ids;
|
||||
$else_context->assigned_var_ids = [];
|
||||
|
||||
if ($statements_checker->analyze(
|
||||
$else->stmts,
|
||||
$else_context,
|
||||
$loop_scope
|
||||
) === false
|
||||
) {
|
||||
return false;
|
||||
if ($else) {
|
||||
if ($statements_checker->analyze(
|
||||
$else->stmts,
|
||||
$else_context,
|
||||
$loop_scope
|
||||
) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<string, bool> */
|
||||
$new_assigned_var_ids = $else_context->assigned_var_ids;
|
||||
$else_context->assigned_var_ids = $pre_stmts_assigned_var_ids;
|
||||
|
||||
if ($else_context->byref_constraints !== null) {
|
||||
if ($else && $else_context->byref_constraints !== null) {
|
||||
$project_checker = $statements_checker->getFileChecker()->project_checker;
|
||||
|
||||
foreach ($else_context->byref_constraints as $var_id => $byref_constraint) {
|
||||
@ -1336,14 +1338,14 @@ class IfChecker
|
||||
}
|
||||
}
|
||||
|
||||
if ($outer_context->collect_references) {
|
||||
if ($else && $outer_context->collect_references) {
|
||||
$outer_context->referenced_var_ids = array_merge(
|
||||
$outer_context->referenced_var_ids,
|
||||
$else_context->referenced_var_ids
|
||||
);
|
||||
}
|
||||
|
||||
$final_actions = ScopeChecker::getFinalControlActions($else->stmts);
|
||||
$final_actions = $else ? ScopeChecker::getFinalControlActions($else->stmts) : [ScopeChecker::ACTION_NONE];
|
||||
// has a return/throw at end
|
||||
$has_ending_statements = $final_actions === [ScopeChecker::ACTION_END];
|
||||
$has_leaving_statements = $has_ending_statements
|
||||
|
@ -155,6 +155,7 @@ class LoopChecker
|
||||
$inner_context->protected_var_ids = $loop_scope->protected_var_ids;
|
||||
|
||||
$statements_checker->analyze($stmts, $inner_context, $loop_scope);
|
||||
|
||||
self::updateLoopScopeContexts($loop_scope, $pre_outer_context);
|
||||
|
||||
$inner_context->protected_var_ids = $original_protected_var_ids;
|
||||
|
@ -283,6 +283,42 @@ class BinaryOpChecker
|
||||
$statements_checker
|
||||
);
|
||||
|
||||
$mixed_var_ids = [];
|
||||
|
||||
foreach ($context->vars_in_scope as $var_id => $type) {
|
||||
if ($type->isMixed()) {
|
||||
$mixed_var_ids[] = $var_id;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($context->vars_possibly_in_scope as $var_id => $_) {
|
||||
if (!isset($context->vars_in_scope[$var_id])) {
|
||||
$mixed_var_ids[] = $var_id;
|
||||
}
|
||||
}
|
||||
|
||||
$if_clauses = array_values(
|
||||
array_map(
|
||||
/**
|
||||
* @return \Psalm\Clause
|
||||
*/
|
||||
function (\Psalm\Clause $c) use ($mixed_var_ids) {
|
||||
$keys = array_keys($c->possibilities);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
foreach ($mixed_var_ids as $mixed_var_id) {
|
||||
if (preg_match('/^' . preg_quote($mixed_var_id, '/') . '(\[|-)/', $key)) {
|
||||
return new \Psalm\Clause([], true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $c;
|
||||
},
|
||||
$if_clauses
|
||||
)
|
||||
);
|
||||
|
||||
$ternary_clauses = Algebra::simplifyCNF(array_merge($context->clauses, $if_clauses));
|
||||
|
||||
$negated_clauses = Algebra::negateFormula($if_clauses);
|
||||
|
@ -254,11 +254,11 @@ class Reconciler
|
||||
if (($new_var_type === 'isset' && !$is_negation)
|
||||
|| ($new_var_type === 'empty' && $is_negation)
|
||||
) {
|
||||
return Type::getMixed();
|
||||
return Type::getMixed(true);
|
||||
}
|
||||
|
||||
if ($new_var_type === 'array-key-exists') {
|
||||
return Type::getMixed();
|
||||
return Type::getMixed(true);
|
||||
}
|
||||
|
||||
if (!$is_negation && $new_var_type !== 'falsy' && $new_var_type !== 'empty') {
|
||||
|
@ -275,6 +275,18 @@ class IssetTest extends TestCase
|
||||
return $arr;
|
||||
}',
|
||||
],
|
||||
'arrayAccessAfterOneIsset' => [
|
||||
'<?php
|
||||
$arr = [];
|
||||
|
||||
foreach ([1, 2, 3] as $foo) {
|
||||
if (!isset($arr["bar"])) {
|
||||
$arr["bar"] = 0;
|
||||
}
|
||||
|
||||
echo $arr["bar"];
|
||||
}',
|
||||
],
|
||||
'arrayAccessAfterTwoIssets' => [
|
||||
'<?php
|
||||
$arr = [];
|
||||
|
@ -1007,6 +1007,18 @@ class LoopScopeTest extends TestCase
|
||||
}
|
||||
}',
|
||||
],
|
||||
'additionSubtractionOps' => [
|
||||
'<?php
|
||||
$a = 0;
|
||||
|
||||
while (rand(0, 1)) {
|
||||
if (rand(0, 1)) {
|
||||
$a++;
|
||||
} elseif ($a) {
|
||||
$a--;
|
||||
}
|
||||
}'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -343,7 +343,7 @@ class RedundantConditionTest extends TestCase
|
||||
|
||||
if ($option) {}',
|
||||
'assignments' => [],
|
||||
'error_levels' => ['MixedAssignment'],
|
||||
'error_levels' => ['MixedAssignment', 'MixedArrayAccess'],
|
||||
],
|
||||
'allowIntValueCheckAfterComparisonDueToOverflow' => [
|
||||
'<?php
|
||||
|
@ -528,7 +528,7 @@ class TypeReconciliationTest extends TestCase
|
||||
if ($a !== null) { }
|
||||
$b = $a;',
|
||||
'assertions' => [
|
||||
'$b' => 'null|int',
|
||||
'$b' => 'int|null',
|
||||
],
|
||||
],
|
||||
'ternaryByRefVar' => [
|
||||
@ -867,6 +867,19 @@ class TypeReconciliationTest extends TestCase
|
||||
function takesStdClass(stdClass $s) : void {}
|
||||
takesStdClass($a);',
|
||||
],
|
||||
'noReconciliationInElseIf' => [
|
||||
'<?php
|
||||
class A {}
|
||||
$a = rand(0, 1) ? new A : null;
|
||||
|
||||
if (rand(0, 1)) {
|
||||
// do nothing
|
||||
} elseif (!$a) {
|
||||
$a = new A();
|
||||
}
|
||||
|
||||
if ($a) {}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -1003,6 +1016,20 @@ class TypeReconciliationTest extends TestCase
|
||||
}',
|
||||
'error_message' => 'TypeDoesNotContainType',
|
||||
],
|
||||
'properReconciliationInElseIf' => [
|
||||
'<?php
|
||||
class A {}
|
||||
$a = rand(0, 1) ? new A : null;
|
||||
|
||||
if (rand(0, 1)) {
|
||||
$a = new A();
|
||||
} elseif (!$a) {
|
||||
$a = new A();
|
||||
}
|
||||
|
||||
if ($a) {}',
|
||||
'error_message' => 'RedundantCondition',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user