1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Fix type assignment checks

This commit is contained in:
Matthew Brown 2018-01-20 11:48:16 -05:00
parent 5200ffa214
commit 799aef628e
5 changed files with 342 additions and 206 deletions

View File

@ -226,6 +226,8 @@ class IfChecker
if ($changed_var_ids) {
$if_context->removeReconciledClauses($changed_var_ids);
}
$if_scope->if_cond_changed_var_ids = $changed_var_ids;
}
$old_if_context = clone $if_context;
@ -476,6 +478,15 @@ class IfChecker
$changed_var_ids = array_keys($new_assigned_var_ids);
// if the variable was only set in the conditional, it's not possibly redefined
foreach ($if_scope->possibly_redefined_vars as $var_id => $_) {
if (!isset($new_assigned_var_ids[$var_id])
&& in_array($var_id, $if_scope->if_cond_changed_var_ids, true)
) {
unset($if_scope->possibly_redefined_vars[$var_id]);
}
}
if ($if_scope->reasonable_clauses) {
// remove all reasonable clauses that would be negated by the if stmts
foreach ($changed_var_ids as $var_id) {
@ -761,10 +772,10 @@ class IfChecker
}
}
$changed_var_ids = [];
// if the elseif has an || in the conditional, we cannot easily reason about it
if ($reconcilable_elseif_types) {
$changed_var_ids = [];
$elseif_vars_reconciled = Reconciler::reconcileKeyedTypes(
$reconcilable_elseif_types,
$elseif_context->vars_in_scope,
@ -784,6 +795,9 @@ class IfChecker
$old_elseif_context = clone $elseif_context;
$pre_stmts_assigned_var_ids = $elseif_context->assigned_var_ids;
$elseif_context->assigned_var_ids = [];
if ($statements_checker->analyze(
$elseif->stmts,
$elseif_context,
@ -793,6 +807,10 @@ class IfChecker
return false;
}
/** @var array<string, bool> */
$new_stmts_assigned_var_ids = $elseif_context->assigned_var_ids;
$elseif_context->assigned_var_ids = $pre_stmts_assigned_var_ids;
if ($elseif_context->byref_constraints !== null) {
foreach ($elseif_context->byref_constraints as $var_id => $byref_constraint) {
if ($outer_context->byref_constraints !== null &&
@ -829,143 +847,151 @@ class IfChecker
$if_scope->final_actions = array_merge($final_actions, $if_scope->final_actions);
if (count($elseif->stmts)) {
// update the parent context as necessary
$elseif_redefined_vars = $elseif_context->getRedefinedVars($original_context->vars_in_scope);
// update the parent context as necessary
$elseif_redefined_vars = $elseif_context->getRedefinedVars($original_context->vars_in_scope);
if (!$has_leaving_statements) {
if ($if_scope->new_vars === null) {
$if_scope->new_vars = array_diff_key($elseif_context->vars_in_scope, $outer_context->vars_in_scope);
} else {
foreach ($if_scope->new_vars as $new_var => $type) {
if (!$elseif_context->hasVariable($new_var)) {
unset($if_scope->new_vars[$new_var]);
} else {
$if_scope->new_vars[$new_var] = Type::combineUnionTypes(
$type,
$elseif_context->vars_in_scope[$new_var]
);
}
if (!$has_leaving_statements) {
if ($if_scope->new_vars === null) {
$if_scope->new_vars = array_diff_key($elseif_context->vars_in_scope, $outer_context->vars_in_scope);
} else {
foreach ($if_scope->new_vars as $new_var => $type) {
if (!$elseif_context->hasVariable($new_var)) {
unset($if_scope->new_vars[$new_var]);
} else {
$if_scope->new_vars[$new_var] = Type::combineUnionTypes(
$type,
$elseif_context->vars_in_scope[$new_var]
);
}
}
}
$possibly_redefined_vars = $elseif_redefined_vars;
foreach ($possibly_redefined_vars as $var_id => $_) {
if (!isset($new_stmts_assigned_var_ids[$var_id])
&& in_array($var_id, $changed_var_ids, true)
) {
unset($possibly_redefined_vars[$var_id]);
}
}
if ($if_scope->redefined_vars === null) {
$if_scope->redefined_vars = $elseif_redefined_vars;
$if_scope->possibly_redefined_vars = $possibly_redefined_vars;
} else {
foreach ($if_scope->redefined_vars as $redefined_var => $type) {
if (!isset($elseif_redefined_vars[$redefined_var])) {
unset($if_scope->redefined_vars[$redefined_var]);
} else {
$if_scope->redefined_vars[$redefined_var] = Type::combineUnionTypes(
$elseif_redefined_vars[$redefined_var],
$type
);
}
}
if ($if_scope->redefined_vars === null) {
$if_scope->redefined_vars = $elseif_redefined_vars;
$if_scope->possibly_redefined_vars = $if_scope->redefined_vars;
} else {
foreach ($if_scope->redefined_vars as $redefined_var => $type) {
if (!isset($elseif_redefined_vars[$redefined_var])) {
unset($if_scope->redefined_vars[$redefined_var]);
} else {
$if_scope->redefined_vars[$redefined_var] = Type::combineUnionTypes(
$elseif_redefined_vars[$redefined_var],
$type
);
}
}
foreach ($elseif_redefined_vars as $var => $type) {
if ($type->isMixed()) {
$if_scope->possibly_redefined_vars[$var] = $type;
} elseif (isset($if_scope->possibly_redefined_vars[$var])) {
$if_scope->possibly_redefined_vars[$var] = Type::combineUnionTypes(
$type,
$if_scope->possibly_redefined_vars[$var]
);
} else {
$if_scope->possibly_redefined_vars[$var] = $type;
}
foreach ($possibly_redefined_vars as $var => $type) {
if ($type->isMixed()) {
$if_scope->possibly_redefined_vars[$var] = $type;
} elseif (isset($if_scope->possibly_redefined_vars[$var])) {
$if_scope->possibly_redefined_vars[$var] = Type::combineUnionTypes(
$type,
$if_scope->possibly_redefined_vars[$var]
);
} else {
$if_scope->possibly_redefined_vars[$var] = $type;
}
}
}
if ($if_scope->reasonable_clauses && $elseif_clauses) {
$if_scope->reasonable_clauses = AlgebraChecker::combineOredClauses(
$if_scope->reasonable_clauses,
$elseif_clauses
);
} else {
$if_scope->reasonable_clauses = [];
}
if ($if_scope->reasonable_clauses && $elseif_clauses) {
$if_scope->reasonable_clauses = AlgebraChecker::combineOredClauses(
$if_scope->reasonable_clauses,
$elseif_clauses
);
} else {
$if_scope->reasonable_clauses = [];
}
} else {
$if_scope->reasonable_clauses = [];
}
if ($project_checker->infer_types_from_usage) {
$elseif_possible_param_types = $elseif_context->possible_param_types;
if ($project_checker->infer_types_from_usage) {
$elseif_possible_param_types = $elseif_context->possible_param_types;
if ($if_scope->possible_param_types) {
$vars_to_remove = [];
if ($if_scope->possible_param_types) {
$vars_to_remove = [];
foreach ($if_scope->possible_param_types as $var => $type) {
if (isset($elseif_possible_param_types[$var])) {
$if_scope->possible_param_types[$var] = Type::combineUnionTypes(
$elseif_possible_param_types[$var],
$type
);
} else {
$vars_to_remove[] = $var;
}
}
foreach ($vars_to_remove as $var) {
unset($if_scope->possible_param_types[$var]);
}
}
}
if ($negated_elseif_types) {
if ($has_leaving_statements) {
$changed_var_ids = [];
$leaving_vars_reconciled = Reconciler::reconcileKeyedTypes(
$negated_elseif_types,
$pre_conditional_context->vars_in_scope,
$changed_var_ids,
[],
$statements_checker,
new CodeLocation($statements_checker->getSource(), $elseif, $outer_context->include_location),
$statements_checker->getSuppressedIssues()
);
$implied_outer_context = clone $elseif_context;
$implied_outer_context->vars_in_scope = $leaving_vars_reconciled;
$outer_context->update(
$elseif_context,
$implied_outer_context,
false,
array_keys($negated_elseif_types),
$if_scope->updated_vars
);
} else {
$outer_context->update(
$old_elseif_context,
$elseif_context,
false,
array_keys($negated_elseif_types),
$if_scope->updated_vars
);
}
}
if (!$has_ending_statements) {
$vars = array_diff_key($elseif_context->vars_possibly_in_scope, $outer_context->vars_possibly_in_scope);
if ($has_leaving_statements && $loop_scope) {
if (!$has_continue_statement && !$has_break_statement) {
$if_scope->new_vars_possibly_in_scope = array_merge(
$vars,
$if_scope->new_vars_possibly_in_scope
foreach ($if_scope->possible_param_types as $var => $type) {
if (isset($elseif_possible_param_types[$var])) {
$if_scope->possible_param_types[$var] = Type::combineUnionTypes(
$elseif_possible_param_types[$var],
$type
);
} else {
$vars_to_remove[] = $var;
}
$loop_scope->vars_possibly_in_scope = array_merge(
$vars,
$loop_scope->vars_possibly_in_scope
);
} elseif (!$has_leaving_statements) {
$if_scope->new_vars_possibly_in_scope = array_merge($vars, $if_scope->new_vars_possibly_in_scope);
}
foreach ($vars_to_remove as $var) {
unset($if_scope->possible_param_types[$var]);
}
}
}
if ($negated_elseif_types) {
if ($has_leaving_statements) {
$changed_var_ids = [];
$leaving_vars_reconciled = Reconciler::reconcileKeyedTypes(
$negated_elseif_types,
$pre_conditional_context->vars_in_scope,
$changed_var_ids,
[],
$statements_checker,
new CodeLocation($statements_checker->getSource(), $elseif, $outer_context->include_location),
$statements_checker->getSuppressedIssues()
);
$implied_outer_context = clone $elseif_context;
$implied_outer_context->vars_in_scope = $leaving_vars_reconciled;
$outer_context->update(
$elseif_context,
$implied_outer_context,
false,
array_keys($negated_elseif_types),
$if_scope->updated_vars
);
} else {
$outer_context->update(
$old_elseif_context,
$elseif_context,
false,
array_keys($negated_elseif_types),
$if_scope->updated_vars
);
}
}
if (!$has_ending_statements) {
$vars = array_diff_key($elseif_context->vars_possibly_in_scope, $outer_context->vars_possibly_in_scope);
if ($has_leaving_statements && $loop_scope) {
if (!$has_continue_statement && !$has_break_statement) {
$if_scope->new_vars_possibly_in_scope = array_merge(
$vars,
$if_scope->new_vars_possibly_in_scope
);
}
$loop_scope->vars_possibly_in_scope = array_merge(
$vars,
$loop_scope->vars_possibly_in_scope
);
} elseif (!$has_leaving_statements) {
$if_scope->new_vars_possibly_in_scope = array_merge($vars, $if_scope->new_vars_possibly_in_scope);
}
}
@ -1086,93 +1112,91 @@ class IfChecker
$if_scope->final_actions = array_merge($final_actions, $if_scope->final_actions);
if (count($else->stmts)) {
$else_redefined_vars = $else_context->getRedefinedVars($original_context->vars_in_scope);
$else_redefined_vars = $else_context->getRedefinedVars($original_context->vars_in_scope);
// if it doesn't end in a return
if (!$has_leaving_statements) {
if ($if_scope->new_vars === null) {
$if_scope->new_vars = array_diff_key($else_context->vars_in_scope, $outer_context->vars_in_scope);
} else {
foreach ($if_scope->new_vars as $new_var => $type) {
if (!$else_context->hasVariable($new_var)) {
unset($if_scope->new_vars[$new_var]);
} else {
$if_scope->new_vars[$new_var] = Type::combineUnionTypes(
$type,
$else_context->vars_in_scope[$new_var]
);
}
}
}
if ($if_scope->redefined_vars === null) {
$if_scope->redefined_vars = $else_redefined_vars;
$if_scope->possibly_redefined_vars = $if_scope->redefined_vars;
} else {
foreach ($if_scope->redefined_vars as $redefined_var => $type) {
if (!isset($else_redefined_vars[$redefined_var])) {
unset($if_scope->redefined_vars[$redefined_var]);
} else {
$if_scope->redefined_vars[$redefined_var] = Type::combineUnionTypes(
$else_redefined_vars[$redefined_var],
$type
);
}
}
foreach ($else_redefined_vars as $var => $type) {
if ($type->isMixed()) {
$if_scope->possibly_redefined_vars[$var] = $type;
} elseif (isset($if_scope->possibly_redefined_vars[$var])) {
$if_scope->possibly_redefined_vars[$var] = Type::combineUnionTypes(
$type,
$if_scope->possibly_redefined_vars[$var]
);
} else {
$if_scope->possibly_redefined_vars[$var] = $type;
}
}
}
} elseif ($if_scope->reasonable_clauses) {
$outer_context->clauses = AlgebraChecker::simplifyCNF(
array_merge(
$if_scope->reasonable_clauses,
$original_context->clauses
)
);
}
// update the parent context as necessary
if ($if_scope->negatable_if_types) {
$outer_context->update(
$old_else_context,
$else_context,
$has_leaving_statements,
array_keys($if_scope->negatable_if_types),
$if_scope->updated_vars
);
}
if (!$has_ending_statements) {
$vars = array_diff_key($else_context->vars_possibly_in_scope, $outer_context->vars_possibly_in_scope);
if ($has_leaving_statements && $loop_scope) {
if (!$has_continue_statement && !$has_break_statement) {
$if_scope->new_vars_possibly_in_scope = array_merge(
$vars,
$if_scope->new_vars_possibly_in_scope
// if it doesn't end in a return
if (!$has_leaving_statements) {
if ($if_scope->new_vars === null) {
$if_scope->new_vars = array_diff_key($else_context->vars_in_scope, $outer_context->vars_in_scope);
} else {
foreach ($if_scope->new_vars as $new_var => $type) {
if (!$else_context->hasVariable($new_var)) {
unset($if_scope->new_vars[$new_var]);
} else {
$if_scope->new_vars[$new_var] = Type::combineUnionTypes(
$type,
$else_context->vars_in_scope[$new_var]
);
}
$loop_scope->vars_possibly_in_scope = array_merge(
$vars,
$loop_scope->vars_possibly_in_scope
);
} elseif (!$has_leaving_statements) {
$if_scope->new_vars_possibly_in_scope = array_merge($vars, $if_scope->new_vars_possibly_in_scope);
}
}
if ($if_scope->redefined_vars === null) {
$if_scope->redefined_vars = $else_redefined_vars;
$if_scope->possibly_redefined_vars = $if_scope->redefined_vars;
} else {
foreach ($if_scope->redefined_vars as $redefined_var => $type) {
if (!isset($else_redefined_vars[$redefined_var])) {
unset($if_scope->redefined_vars[$redefined_var]);
} else {
$if_scope->redefined_vars[$redefined_var] = Type::combineUnionTypes(
$else_redefined_vars[$redefined_var],
$type
);
}
}
foreach ($else_redefined_vars as $var => $type) {
if ($type->isMixed()) {
$if_scope->possibly_redefined_vars[$var] = $type;
} elseif (isset($if_scope->possibly_redefined_vars[$var])) {
$if_scope->possibly_redefined_vars[$var] = Type::combineUnionTypes(
$type,
$if_scope->possibly_redefined_vars[$var]
);
} else {
$if_scope->possibly_redefined_vars[$var] = $type;
}
}
}
} elseif ($if_scope->reasonable_clauses) {
$outer_context->clauses = AlgebraChecker::simplifyCNF(
array_merge(
$if_scope->reasonable_clauses,
$original_context->clauses
)
);
}
// update the parent context as necessary
if ($if_scope->negatable_if_types) {
$outer_context->update(
$old_else_context,
$else_context,
$has_leaving_statements,
array_keys($if_scope->negatable_if_types),
$if_scope->updated_vars
);
}
if (!$has_ending_statements) {
$vars = array_diff_key($else_context->vars_possibly_in_scope, $outer_context->vars_possibly_in_scope);
if ($has_leaving_statements && $loop_scope) {
if (!$has_continue_statement && !$has_break_statement) {
$if_scope->new_vars_possibly_in_scope = array_merge(
$vars,
$if_scope->new_vars_possibly_in_scope
);
}
$loop_scope->vars_possibly_in_scope = array_merge(
$vars,
$loop_scope->vars_possibly_in_scope
);
} elseif (!$has_leaving_statements) {
$if_scope->new_vars_possibly_in_scope = array_merge($vars, $if_scope->new_vars_possibly_in_scope);
}
}
if ($project_checker->infer_types_from_usage) {

View File

@ -41,6 +41,11 @@ class IfScope
*/
public $negated_types = [];
/**
* @var array<mixed, string>
*/
public $if_cond_changed_var_ids = [];
/**
* @var array<string, string>|null
*/

View File

@ -736,7 +736,7 @@ class AnnotationTest extends TestCase
*/
function bar(array $arr): void {}',
'error_message' => 'InvalidDocblock',
]
],
];
}
}

View File

@ -766,6 +766,53 @@ class ArrayAssignmentTest extends TestCase
'$f' => 'array{0:string}',
],
],
'assignArrayOrSetNull' => [
'<?php
$a = [];
if (rand(0, 1)) {
$a[] = 4;
}
if (!$a) {
$a = null;
}',
'assertions' => [
'$a' => 'array<int, int>|null',
],
],
'assignArrayOrSetNullInElseIf' => [
'<?php
$a = [];
if (rand(0, 1)) {
$a[] = 4;
}
if ($a) {
} elseif (rand(0, 1)) {
$a = null;
}',
'assertions' => [
'$a' => 'array<int, int>|null',
],
],
'assignArrayOrSetNullInElse' => [
'<?php
$a = [];
if (rand(0, 1)) {
$a[] = 4;
}
if ($a) {
} else {
$a = null;
}',
'assertions' => [
'$a' => 'array<int, int>|null',
],
],
];
}

View File

@ -964,6 +964,26 @@ class TypeTest extends TestCase
interface I {}',
],
'intersectionTypeInsideInstanceof' => [
'<?php
abstract class A {
/** @var string|null */
public $foo;
public static function getFoo(): void {
$a = new static();
if ($a instanceof I) {
takesI($a);
takesA($a);
}
}
}
interface I {}
function takesI(I $i): void {}
function takesA(A $i): void {}',
],
];
}
@ -1329,6 +1349,46 @@ class TypeTest extends TestCase
',
'error_message' => 'InvalidReturnStatement',
],
'intersectionTypeClassCheckAfterInstanceof' => [
'<?php
abstract class A {
/** @var string|null */
public $foo;
public static function getFoo(): void {
$a = new static();
if ($a instanceof B) {}
elseif ($a instanceof C) {}
else {}
takesB($a);
}
}
class B extends A {}
class C extends A {}
function takesB(B $i): void {}',
'error_message' => 'TypeCoercion - src/somefile.php:11 - Argument 1 of takesb expects B,'
. ' parent type A provided',
],
'intersectionTypeInterfaceCheckAfterInstanceof' => [
'<?php
abstract class A {
/** @var string|null */
public $foo;
public static function getFoo(): void {
$a = new static();
if ($a instanceof I) {}
takesI($a);
}
}
interface I {}
function takesI(I $i): void {}',
'error_message' => 'InvalidArgument - src/somefile.php:9 - Argument 1 of takesi expects I, A provided',
],
];
}
}