mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Improve dead code detection
This commit is contained in:
parent
3afd6053fd
commit
3670f066bb
@ -104,17 +104,6 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
$this->fq_class_name = $fq_class_name;
|
||||
|
||||
$this->storage = $this->file_checker->project_checker->classlike_storage_provider->get($fq_class_name);
|
||||
|
||||
if ($this->storage->location) {
|
||||
$storage_file_path = $this->storage->location->file_path;
|
||||
$source_file_path = $this->source->getFilePath();
|
||||
|
||||
if (!Config::getInstance()->use_case_sensitive_file_names) {
|
||||
// TODO: Use these variables
|
||||
$storage_file_path = strtolower($storage_file_path);
|
||||
$source_file_path = strtolower($source_file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -293,7 +293,9 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
$context->vars_possibly_in_scope['$' . $function_param->name] = true;
|
||||
|
||||
if ($context->collect_references && $function_param->location) {
|
||||
$context->unreferenced_vars['$' . $function_param->name] = $function_param->location;
|
||||
$context->unreferenced_vars['$' . $function_param->name] = [
|
||||
$function_param->location->getHash() => $function_param->location
|
||||
];
|
||||
}
|
||||
|
||||
if (!$function_param->type_location || !$function_param->location) {
|
||||
|
@ -8,6 +8,7 @@ class ScopeChecker
|
||||
const ACTION_END = 'END';
|
||||
const ACTION_BREAK = 'BREAK';
|
||||
const ACTION_CONTINUE = 'CONTINUE';
|
||||
const ACTION_LEAVE_SWITCH = 'LEAVE_SWITCH';
|
||||
const ACTION_NONE = 'NONE';
|
||||
const ACTION_RETURN = 'RETURN';
|
||||
|
||||
@ -51,14 +52,14 @@ class ScopeChecker
|
||||
|
||||
/**
|
||||
* @param array<PhpParser\Node> $stmts
|
||||
* @param bool $continue_is_break when checking inside a switch statement, continue is an alias of break
|
||||
* @param bool $in_switch when checking inside a switch statement, continue is an alias of break
|
||||
* @param bool $return_is_exit Exit and Throw statements are treated differently from return if this is false
|
||||
*
|
||||
* @return string[] one or more of 'LEAVE', 'CONTINUE', 'BREAK' (or empty if no single action is found)
|
||||
*/
|
||||
public static function getFinalControlActions(
|
||||
array $stmts,
|
||||
$continue_is_break = false,
|
||||
$in_switch = false,
|
||||
$return_is_exit = true
|
||||
) {
|
||||
if (empty($stmts)) {
|
||||
@ -82,23 +83,29 @@ class ScopeChecker
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
|
||||
if ($continue_is_break
|
||||
if ($in_switch
|
||||
&& (!$stmt->num || !$stmt->num instanceof PhpParser\Node\Scalar\LNumber || $stmt->num->value < 2)
|
||||
) {
|
||||
return [self::ACTION_BREAK];
|
||||
return [self::ACTION_LEAVE_SWITCH];
|
||||
}
|
||||
|
||||
return [self::ACTION_CONTINUE];
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Break_) {
|
||||
if ($in_switch
|
||||
&& (!$stmt->num || !$stmt->num instanceof PhpParser\Node\Scalar\LNumber || $stmt->num->value < 2)
|
||||
) {
|
||||
return [self::ACTION_LEAVE_SWITCH];
|
||||
}
|
||||
|
||||
return [self::ACTION_BREAK];
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
|
||||
$if_statement_actions = self::getFinalControlActions($stmt->stmts, $continue_is_break);
|
||||
$if_statement_actions = self::getFinalControlActions($stmt->stmts, $in_switch);
|
||||
$else_statement_actions = $stmt->else
|
||||
? self::getFinalControlActions($stmt->else->stmts, $continue_is_break)
|
||||
? self::getFinalControlActions($stmt->else->stmts, $in_switch)
|
||||
: [];
|
||||
|
||||
$all_same = count($if_statement_actions) === 1
|
||||
@ -109,7 +116,7 @@ class ScopeChecker
|
||||
|
||||
if ($stmt->elseifs) {
|
||||
foreach ($stmt->elseifs as $elseif) {
|
||||
$elseif_control_actions = self::getFinalControlActions($elseif->stmts, $continue_is_break);
|
||||
$elseif_control_actions = self::getFinalControlActions($elseif->stmts, $in_switch);
|
||||
|
||||
$all_same = $all_same && $elseif_control_actions == $if_statement_actions;
|
||||
|
||||
@ -142,10 +149,12 @@ class ScopeChecker
|
||||
|
||||
$case_actions = self::getFinalControlActions($case->stmts, true);
|
||||
|
||||
if (array_intersect([self::ACTION_BREAK, self::ACTION_CONTINUE], $case_actions)) {
|
||||
// clear out any default breaking notions
|
||||
$has_non_breaking_default = false;
|
||||
|
||||
if (array_intersect([
|
||||
self::ACTION_LEAVE_SWITCH,
|
||||
self::ACTION_BREAK,
|
||||
self::ACTION_CONTINUE
|
||||
], $case_actions)
|
||||
) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
@ -188,13 +197,13 @@ class ScopeChecker
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
|
||||
$try_statement_actions = self::getFinalControlActions($stmt->stmts, $continue_is_break);
|
||||
$try_statement_actions = self::getFinalControlActions($stmt->stmts, $in_switch);
|
||||
|
||||
if ($stmt->catches) {
|
||||
$all_same = count($try_statement_actions) === 1;
|
||||
|
||||
foreach ($stmt->catches as $catch) {
|
||||
$catch_actions = self::getFinalControlActions($catch->stmts, $continue_is_break);
|
||||
$catch_actions = self::getFinalControlActions($catch->stmts, $in_switch);
|
||||
|
||||
$all_same = $all_same && $try_statement_actions == $catch_actions;
|
||||
|
||||
@ -212,7 +221,7 @@ class ScopeChecker
|
||||
if ($stmt->finally->stmts) {
|
||||
$finally_statement_actions = self::getFinalControlActions(
|
||||
$stmt->finally->stmts,
|
||||
$continue_is_break
|
||||
$in_switch
|
||||
);
|
||||
|
||||
if (!in_array(self::ACTION_NONE, $finally_statement_actions, true)) {
|
||||
|
@ -367,7 +367,7 @@ class ForeachChecker
|
||||
$location = new CodeLocation($statements_checker, $stmt->keyVar);
|
||||
|
||||
if ($context->collect_references && !isset($foreach_context->byref_constraints[$key_var_id])) {
|
||||
$foreach_context->unreferenced_vars[$key_var_id] = $location;
|
||||
$foreach_context->unreferenced_vars[$key_var_id] = [$location->getHash() => $location];
|
||||
}
|
||||
|
||||
if (!$statements_checker->hasVariable($key_var_id)) {
|
||||
@ -378,8 +378,8 @@ class ForeachChecker
|
||||
);
|
||||
}
|
||||
|
||||
if ($stmt->byRef) {
|
||||
$statements_checker->registerVariableUse($location);
|
||||
if ($stmt->byRef && $context->collect_references) {
|
||||
$statements_checker->registerVariableUses([$location->getHash() => $location]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,16 +329,28 @@ class IfChecker
|
||||
// which vars of the if we can safely change
|
||||
$pre_assignment_else_redefined_vars = $temp_else_context->getRedefinedVars($context->vars_in_scope, true);
|
||||
|
||||
$old_unreferenced_vars = $context->unreferenced_vars;
|
||||
$newly_unreferenced_locations = [];
|
||||
|
||||
// this captures statements in the if conditional
|
||||
if ($context->collect_references) {
|
||||
foreach ($if_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;
|
||||
foreach ($if_context->unreferenced_vars as $var_id => $locations) {
|
||||
if (!isset($context->unreferenced_vars[$var_id])) {
|
||||
if (isset($if_scope->new_unreferenced_vars[$var_id])) {
|
||||
$if_scope->new_unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$if_scope->new_unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
} else {
|
||||
$new_locations = array_diff_key(
|
||||
$locations,
|
||||
$context->unreferenced_vars[$var_id]
|
||||
);
|
||||
|
||||
if ($new_locations) {
|
||||
if (isset($if_scope->new_unreferenced_vars[$var_id])) {
|
||||
$if_scope->new_unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$if_scope->new_unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -357,16 +369,6 @@ class IfChecker
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($context->collect_references && $if_scope->final_actions !== [ScopeChecker::ACTION_END]) {
|
||||
foreach ($if_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check the elseifs
|
||||
foreach ($stmt->elseifs as $elseif) {
|
||||
$elseif_context = clone $original_context;
|
||||
@ -386,16 +388,6 @@ class IfChecker
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($context->collect_references && $if_scope->final_actions !== [ScopeChecker::ACTION_END]) {
|
||||
foreach ($elseif_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check the else
|
||||
@ -419,16 +411,6 @@ class IfChecker
|
||||
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 ($loop_scope) {
|
||||
$loop_scope->final_actions = array_unique(
|
||||
array_merge(
|
||||
@ -495,7 +477,7 @@ class IfChecker
|
||||
|
||||
if ($if_scope->possibly_redefined_vars) {
|
||||
foreach ($if_scope->possibly_redefined_vars as $var_id => $type) {
|
||||
if ($context->hasVariable($var_id)
|
||||
if (isset($context->vars_in_scope[$var_id])
|
||||
&& !$type->failed_reconciliation
|
||||
&& !isset($if_scope->updated_vars[$var_id])
|
||||
) {
|
||||
@ -515,18 +497,24 @@ class IfChecker
|
||||
}
|
||||
|
||||
if ($context->collect_references) {
|
||||
foreach ($newly_unreferenced_locations as $var_id => $locations) {
|
||||
foreach ($if_scope->new_unreferenced_vars as $var_id => $locations) {
|
||||
if (($stmt->else
|
||||
&& (isset($if_scope->assigned_var_ids[$var_id]) || isset($if_scope->new_vars[$var_id])))
|
||||
|| !isset($context->vars_in_scope[$var_id])
|
||||
) {
|
||||
$context->unreferenced_vars[$var_id] = array_shift($locations);
|
||||
}
|
||||
|
||||
foreach ($locations as $location) {
|
||||
$statements_checker->registerVariableUse($location);
|
||||
$context->unreferenced_vars[$var_id] = $locations;
|
||||
} elseif (isset($if_scope->possibly_assigned_var_ids[$var_id])) {
|
||||
if (!isset($context->unreferenced_vars[$var_id])) {
|
||||
$context->unreferenced_vars[$var_id] = $locations;
|
||||
} else {
|
||||
$context->unreferenced_vars[$var_id] += $locations;
|
||||
}
|
||||
} else {
|
||||
$statements_checker->registerVariableUses($locations);
|
||||
}
|
||||
}
|
||||
|
||||
$context->possibly_assigned_var_ids += $if_scope->possibly_assigned_var_ids;
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -553,7 +541,7 @@ class IfChecker
|
||||
array $pre_assignment_else_redefined_vars,
|
||||
LoopScope $loop_scope = null
|
||||
) {
|
||||
$final_actions = ScopeChecker::getFinalControlActions($stmt->stmts);
|
||||
$final_actions = ScopeChecker::getFinalControlActions($stmt->stmts, $outer_context->inside_case);
|
||||
|
||||
$has_ending_statements = $final_actions === [ScopeChecker::ACTION_END];
|
||||
|
||||
@ -562,6 +550,7 @@ class IfChecker
|
||||
|
||||
$has_break_statement = $final_actions === [ScopeChecker::ACTION_BREAK];
|
||||
$has_continue_statement = $final_actions === [ScopeChecker::ACTION_CONTINUE];
|
||||
$has_leave_switch_statement = $final_actions === [ScopeChecker::ACTION_LEAVE_SWITCH];
|
||||
|
||||
if (!$has_ending_statements) {
|
||||
$if_context->parent_context = $outer_context;
|
||||
@ -768,15 +757,9 @@ class IfChecker
|
||||
$outer_context->vars_possibly_in_scope
|
||||
);
|
||||
|
||||
$possibly_assigned_var_ids = array_diff_key(
|
||||
$if_context->possibly_assigned_var_ids,
|
||||
$outer_context->possibly_assigned_var_ids
|
||||
);
|
||||
|
||||
if ($loop_scope) {
|
||||
if (!$has_continue_statement && !$has_break_statement) {
|
||||
$if_scope->new_vars_possibly_in_scope = $vars_possibly_in_scope;
|
||||
$if_scope->possibly_assigned_var_ids = $possibly_assigned_var_ids;
|
||||
}
|
||||
|
||||
$loop_scope->vars_possibly_in_scope = array_merge(
|
||||
@ -786,6 +769,31 @@ class IfChecker
|
||||
} elseif (!$has_leaving_statements) {
|
||||
$if_scope->new_vars_possibly_in_scope = $vars_possibly_in_scope;
|
||||
}
|
||||
|
||||
if ($if_context->collect_references && (!$has_leaving_statements || $has_leave_switch_statement)) {
|
||||
foreach ($if_context->unreferenced_vars as $var_id => $locations) {
|
||||
if (!isset($outer_context->unreferenced_vars[$var_id])) {
|
||||
if (isset($if_scope->new_unreferenced_vars[$var_id])) {
|
||||
$if_scope->new_unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$if_scope->new_unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
} else {
|
||||
$new_locations = array_diff_key(
|
||||
$locations,
|
||||
$outer_context->unreferenced_vars[$var_id]
|
||||
);
|
||||
|
||||
if ($new_locations) {
|
||||
if (isset($if_scope->new_unreferenced_vars[$var_id])) {
|
||||
$if_scope->new_unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$if_scope->new_unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -867,9 +875,15 @@ class IfChecker
|
||||
|
||||
$conditional_assigned_var_ids = $elseif_context->assigned_var_ids;
|
||||
|
||||
$elseif_context->assigned_var_ids = array_merge($pre_assigned_var_ids, $conditional_assigned_var_ids);
|
||||
$elseif_context->assigned_var_ids = array_merge(
|
||||
$pre_assigned_var_ids,
|
||||
$conditional_assigned_var_ids
|
||||
);
|
||||
|
||||
$new_assigned_var_ids = array_diff_key($conditional_assigned_var_ids, $pre_assigned_var_ids);
|
||||
$new_assigned_var_ids = array_diff_key(
|
||||
$conditional_assigned_var_ids,
|
||||
$pre_assigned_var_ids
|
||||
);
|
||||
|
||||
$new_referenced_var_ids = array_diff_key($new_referenced_var_ids, $new_assigned_var_ids);
|
||||
|
||||
@ -993,6 +1007,8 @@ class IfChecker
|
||||
|
||||
$pre_stmts_assigned_var_ids = $elseif_context->assigned_var_ids;
|
||||
$elseif_context->assigned_var_ids = [];
|
||||
$pre_stmts_possibly_assigned_var_ids = $elseif_context->possibly_assigned_var_ids;
|
||||
$elseif_context->possibly_assigned_var_ids = [];
|
||||
|
||||
if ($statements_checker->analyze(
|
||||
$elseif->stmts,
|
||||
@ -1005,7 +1021,12 @@ class IfChecker
|
||||
|
||||
/** @var array<string, bool> */
|
||||
$new_stmts_assigned_var_ids = $elseif_context->assigned_var_ids;
|
||||
$elseif_context->assigned_var_ids = $pre_stmts_assigned_var_ids;
|
||||
$elseif_context->assigned_var_ids = $pre_stmts_assigned_var_ids + $new_stmts_assigned_var_ids;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
$new_stmts_possibly_assigned_var_ids = $elseif_context->possibly_assigned_var_ids;
|
||||
$elseif_context->possibly_assigned_var_ids =
|
||||
$pre_stmts_possibly_assigned_var_ids + $new_stmts_possibly_assigned_var_ids;
|
||||
|
||||
if ($elseif_context->byref_constraints !== null) {
|
||||
foreach ($elseif_context->byref_constraints as $var_id => $byref_constraint) {
|
||||
@ -1034,7 +1055,7 @@ class IfChecker
|
||||
}
|
||||
}
|
||||
|
||||
$final_actions = ScopeChecker::getFinalControlActions($elseif->stmts);
|
||||
$final_actions = ScopeChecker::getFinalControlActions($elseif->stmts, $outer_context->inside_case);
|
||||
// has a return/throw at end
|
||||
$has_ending_statements = $final_actions === [ScopeChecker::ACTION_END];
|
||||
$has_leaving_statements = $has_ending_statements
|
||||
@ -1042,6 +1063,7 @@ class IfChecker
|
||||
|
||||
$has_break_statement = $final_actions === [ScopeChecker::ACTION_BREAK];
|
||||
$has_continue_statement = $final_actions === [ScopeChecker::ACTION_CONTINUE];
|
||||
$has_leave_switch_statement = $final_actions === [ScopeChecker::ACTION_LEAVE_SWITCH];
|
||||
|
||||
$if_scope->final_actions = array_merge($final_actions, $if_scope->final_actions);
|
||||
|
||||
@ -1197,10 +1219,7 @@ class IfChecker
|
||||
$outer_context->vars_possibly_in_scope
|
||||
);
|
||||
|
||||
$possibly_assigned_var_ids = array_diff_key(
|
||||
$elseif_context->possibly_assigned_var_ids,
|
||||
$outer_context->possibly_assigned_var_ids
|
||||
);
|
||||
$possibly_assigned_var_ids = $new_stmts_possibly_assigned_var_ids;
|
||||
|
||||
if ($has_leaving_statements && $loop_scope) {
|
||||
if (!$has_continue_statement && !$has_break_statement) {
|
||||
@ -1228,6 +1247,31 @@ class IfChecker
|
||||
$if_scope->possibly_assigned_var_ids
|
||||
);
|
||||
}
|
||||
|
||||
if ($outer_context->collect_references && (!$has_leaving_statements || $has_leave_switch_statement)) {
|
||||
foreach ($elseif_context->unreferenced_vars as $var_id => $locations) {
|
||||
if (!isset($outer_context->unreferenced_vars[$var_id])) {
|
||||
if (isset($if_scope->new_unreferenced_vars[$var_id])) {
|
||||
$if_scope->new_unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$if_scope->new_unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
} else {
|
||||
$new_locations = array_diff_key(
|
||||
$locations,
|
||||
$outer_context->unreferenced_vars[$var_id]
|
||||
);
|
||||
|
||||
if ($new_locations) {
|
||||
if (isset($if_scope->new_unreferenced_vars[$var_id])) {
|
||||
$if_scope->new_unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$if_scope->new_unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($outer_context->collect_references) {
|
||||
@ -1318,6 +1362,9 @@ class IfChecker
|
||||
$pre_stmts_assigned_var_ids = $else_context->assigned_var_ids;
|
||||
$else_context->assigned_var_ids = [];
|
||||
|
||||
$pre_possibly_assigned_var_ids = $else_context->possibly_assigned_var_ids;
|
||||
$else_context->possibly_assigned_var_ids = [];
|
||||
|
||||
if ($else) {
|
||||
if ($statements_checker->analyze(
|
||||
$else->stmts,
|
||||
@ -1333,6 +1380,10 @@ class IfChecker
|
||||
$new_assigned_var_ids = $else_context->assigned_var_ids;
|
||||
$else_context->assigned_var_ids = $pre_stmts_assigned_var_ids;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
$new_possibly_assigned_var_ids = $else_context->possibly_assigned_var_ids;
|
||||
$else_context->possibly_assigned_var_ids = $pre_possibly_assigned_var_ids + $new_possibly_assigned_var_ids;
|
||||
|
||||
if ($else && $else_context->byref_constraints !== null) {
|
||||
$project_checker = $statements_checker->getFileChecker()->project_checker;
|
||||
|
||||
@ -1369,7 +1420,9 @@ class IfChecker
|
||||
);
|
||||
}
|
||||
|
||||
$final_actions = $else ? ScopeChecker::getFinalControlActions($else->stmts) : [ScopeChecker::ACTION_NONE];
|
||||
$final_actions = $else
|
||||
? ScopeChecker::getFinalControlActions($else->stmts, $outer_context->inside_case)
|
||||
: [ScopeChecker::ACTION_NONE];
|
||||
// has a return/throw at end
|
||||
$has_ending_statements = $final_actions === [ScopeChecker::ACTION_END];
|
||||
$has_leaving_statements = $has_ending_statements
|
||||
@ -1377,6 +1430,7 @@ class IfChecker
|
||||
|
||||
$has_break_statement = $final_actions === [ScopeChecker::ACTION_BREAK];
|
||||
$has_continue_statement = $final_actions === [ScopeChecker::ACTION_CONTINUE];
|
||||
$has_leave_switch_statement = $final_actions === [ScopeChecker::ACTION_LEAVE_SWITCH];
|
||||
|
||||
$if_scope->final_actions = array_merge($final_actions, $if_scope->final_actions);
|
||||
|
||||
@ -1454,10 +1508,7 @@ class IfChecker
|
||||
$outer_context->vars_possibly_in_scope
|
||||
);
|
||||
|
||||
$possibly_assigned_var_ids = array_diff_key(
|
||||
$else_context->possibly_assigned_var_ids,
|
||||
$outer_context->possibly_assigned_var_ids
|
||||
);
|
||||
$possibly_assigned_var_ids = $new_possibly_assigned_var_ids;
|
||||
|
||||
if ($has_leaving_statements && $loop_scope) {
|
||||
if (!$has_continue_statement && !$has_break_statement) {
|
||||
@ -1487,6 +1538,31 @@ class IfChecker
|
||||
$if_scope->possibly_assigned_var_ids
|
||||
);
|
||||
}
|
||||
|
||||
if ($outer_context->collect_references && (!$has_leaving_statements || $has_leave_switch_statement)) {
|
||||
foreach ($else_context->unreferenced_vars as $var_id => $locations) {
|
||||
if (!isset($outer_context->unreferenced_vars[$var_id])) {
|
||||
if (isset($if_scope->new_unreferenced_vars[$var_id])) {
|
||||
$if_scope->new_unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$if_scope->new_unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
} else {
|
||||
$new_locations = array_diff_key(
|
||||
$locations,
|
||||
$outer_context->unreferenced_vars[$var_id]
|
||||
);
|
||||
|
||||
if ($new_locations) {
|
||||
if (isset($if_scope->new_unreferenced_vars[$var_id])) {
|
||||
$if_scope->new_unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$if_scope->new_unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($project_checker->infer_types_from_usage) {
|
||||
|
@ -87,6 +87,8 @@ class LoopChecker
|
||||
$inner_context = clone $loop_scope->loop_context;
|
||||
|
||||
$inner_context->parent_context = $loop_scope->loop_context;
|
||||
$old_referenced_var_ids = $inner_context->referenced_var_ids;
|
||||
$inner_context->referenced_var_ids = [];
|
||||
|
||||
if (!$is_do) {
|
||||
foreach ($pre_conditions as $pre_condition) {
|
||||
@ -116,6 +118,9 @@ class LoopChecker
|
||||
}
|
||||
}
|
||||
|
||||
$new_referenced_var_ids = $inner_context->referenced_var_ids;
|
||||
$inner_context->referenced_var_ids = $old_referenced_var_ids + $inner_context->referenced_var_ids;
|
||||
|
||||
$loop_scope->loop_parent_context->vars_possibly_in_scope = array_merge(
|
||||
$inner_context->vars_possibly_in_scope,
|
||||
$loop_scope->loop_parent_context->vars_possibly_in_scope
|
||||
@ -150,6 +155,9 @@ class LoopChecker
|
||||
$inner_context = clone $loop_scope->loop_context;
|
||||
$inner_context->parent_context = $loop_scope->loop_context;
|
||||
|
||||
$old_referenced_var_ids = $inner_context->referenced_var_ids;
|
||||
$inner_context->referenced_var_ids = [];
|
||||
|
||||
$asserted_var_ids = array_unique($asserted_var_ids);
|
||||
|
||||
$inner_context->protected_var_ids = $loop_scope->protected_var_ids;
|
||||
@ -166,6 +174,12 @@ class LoopChecker
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
$new_referenced_var_ids = $inner_context->referenced_var_ids;
|
||||
$inner_context->referenced_var_ids = $old_referenced_var_ids + $inner_context->referenced_var_ids;
|
||||
|
||||
$recorded_issues = IssueBuffer::clearRecordingLevel();
|
||||
IssueBuffer::stopRecording();
|
||||
|
||||
@ -231,16 +245,31 @@ class LoopChecker
|
||||
}
|
||||
|
||||
if ($inner_context->collect_references) {
|
||||
foreach ($inner_context->unreferenced_vars as $var_id => $location) {
|
||||
if (isset($loop_scope->loop_parent_context->vars_in_scope[$var_id])
|
||||
&& (!isset($loop_scope->loop_parent_context->unreferenced_vars[$var_id])
|
||||
|| $loop_scope->loop_parent_context->unreferenced_vars[$var_id] !== $location)
|
||||
) {
|
||||
$statements_checker->registerVariableUse($location);
|
||||
foreach ($loop_scope->possibly_unreferenced_vars as $var_id => $locations) {
|
||||
if (isset($inner_context->unreferenced_vars[$var_id])) {
|
||||
$inner_context->unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$inner_context->unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*if ($inner_context->collect_references) {
|
||||
foreach ($inner_context->unreferenced_vars as $var_id => $locations) {
|
||||
if (isset($loop_scope->loop_parent_context->vars_in_scope[$var_id])
|
||||
&& !isset($new_referenced_var_ids[$var_id])
|
||||
) {
|
||||
if (!isset($loop_scope->loop_parent_context->unreferenced_vars[$var_id])) {
|
||||
$loop_scope->loop_parent_context->unreferenced_vars[$var_id] = $locations;
|
||||
} else {
|
||||
$loop_scope->loop_parent_context->unreferenced_vars[$var_id] += $locations;
|
||||
}
|
||||
} else {
|
||||
$statements_checker->registerVariableUses($locations);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
// remove vars that were defined in the foreach
|
||||
foreach ($vars_to_remove as $var_id) {
|
||||
unset($inner_context->vars_in_scope[$var_id]);
|
||||
@ -395,11 +424,31 @@ class LoopChecker
|
||||
);
|
||||
|
||||
if ($inner_context->collect_references) {
|
||||
foreach ($inner_context->unreferenced_vars as $var_id => $location) {
|
||||
foreach ($loop_scope->possibly_unreferenced_vars as $var_id => $locations) {
|
||||
if (isset($inner_context->unreferenced_vars[$var_id])) {
|
||||
$inner_context->unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$inner_context->unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($inner_context->unreferenced_vars as $var_id => $locations) {
|
||||
if (!isset($new_referenced_var_ids[$var_id]) || $has_break_statement) {
|
||||
if (!isset($loop_scope->loop_context->unreferenced_vars[$var_id])) {
|
||||
$loop_scope->loop_context->unreferenced_vars[$var_id] = $locations;
|
||||
} else {
|
||||
$loop_scope->loop_context->unreferenced_vars[$var_id] += $locations;
|
||||
}
|
||||
} else {
|
||||
$statements_checker->registerVariableUses($locations);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($loop_scope->unreferenced_vars as $var_id => $locations) {
|
||||
if (!isset($loop_scope->loop_context->unreferenced_vars[$var_id])) {
|
||||
$loop_scope->loop_context->unreferenced_vars[$var_id] = $location;
|
||||
} elseif ($loop_scope->loop_context->unreferenced_vars[$var_id] !== $location) {
|
||||
$statements_checker->registerVariableUse($location);
|
||||
$loop_scope->loop_context->unreferenced_vars[$var_id] = $locations;
|
||||
} else {
|
||||
$loop_scope->loop_context->unreferenced_vars[$var_id] += $locations;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ class SwitchChecker
|
||||
$last_case_exit_type = 'return_throw';
|
||||
} elseif ($case_actions === [ScopeChecker::ACTION_CONTINUE]) {
|
||||
$last_case_exit_type = 'continue';
|
||||
} elseif (in_array(ScopeChecker::ACTION_BREAK, $case_actions, true)) {
|
||||
} elseif (in_array(ScopeChecker::ACTION_LEAVE_SWITCH, $case_actions, true)) {
|
||||
$last_case_exit_type = 'break';
|
||||
}
|
||||
}
|
||||
@ -84,6 +84,10 @@ class SwitchChecker
|
||||
|
||||
$project_checker = $statements_checker->getFileChecker()->project_checker;
|
||||
|
||||
$new_unreferenced_vars = [];
|
||||
$new_assigned_var_ids = null;
|
||||
$new_possibly_assigned_var_ids = [];
|
||||
|
||||
for ($i = 0, $l = count($stmt->cases); $i < $l; $i++) {
|
||||
$case = $stmt->cases[$i];
|
||||
|
||||
@ -322,8 +326,23 @@ class SwitchChecker
|
||||
);
|
||||
}
|
||||
|
||||
$pre_possibly_assigned_var_ids = $case_context->possibly_assigned_var_ids;
|
||||
$case_context->possibly_assigned_var_ids = [];
|
||||
|
||||
$pre_assigned_var_ids = $case_context->assigned_var_ids;
|
||||
$case_context->assigned_var_ids = [];
|
||||
|
||||
$statements_checker->analyze($case_stmts, $case_context, $loop_scope);
|
||||
|
||||
/** @var array<string, bool> */
|
||||
$new_case_assigned_var_ids = $case_context->assigned_var_ids;
|
||||
$case_context->assigned_var_ids = $pre_assigned_var_ids + $new_case_assigned_var_ids;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
$new_case_possibly_assigned_var_ids = $case_context->possibly_assigned_var_ids;
|
||||
$case_context->possibly_assigned_var_ids =
|
||||
$pre_possibly_assigned_var_ids + $new_case_possibly_assigned_var_ids;
|
||||
|
||||
$context->referenced_var_ids = array_merge(
|
||||
$context->referenced_var_ids,
|
||||
$case_context->referenced_var_ids
|
||||
@ -427,21 +446,40 @@ class SwitchChecker
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($context->collect_references) {
|
||||
foreach ($case_context->unreferenced_vars as $var_id => $location) {
|
||||
if (isset($context->unreferenced_vars[$var_id])
|
||||
&& $context->unreferenced_vars[$var_id] !== $location
|
||||
) {
|
||||
$context->hasVariable($var_id, $statements_checker);
|
||||
if ($context->collect_references) {
|
||||
$new_possibly_assigned_var_ids =
|
||||
$new_possibly_assigned_var_ids + $new_case_possibly_assigned_var_ids;
|
||||
|
||||
if ($new_assigned_var_ids === null) {
|
||||
$new_assigned_var_ids = $new_case_assigned_var_ids;
|
||||
} else {
|
||||
$new_assigned_var_ids = array_intersect_key($new_assigned_var_ids, $new_case_assigned_var_ids);
|
||||
}
|
||||
|
||||
foreach ($case_context->unreferenced_vars as $var_id => $locations) {
|
||||
if (!isset($original_context->unreferenced_vars[$var_id])) {
|
||||
if (isset($new_unreferenced_vars[$var_id])) {
|
||||
$new_unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$new_unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
} else {
|
||||
$new_locations = array_diff_key(
|
||||
$locations,
|
||||
$original_context->unreferenced_vars[$var_id]
|
||||
);
|
||||
|
||||
if ($new_locations) {
|
||||
if (isset($new_unreferenced_vars[$var_id])) {
|
||||
$new_unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$new_unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$context->unreferenced_vars = array_merge(
|
||||
$context->unreferenced_vars,
|
||||
$case_context->unreferenced_vars
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -501,6 +539,24 @@ class SwitchChecker
|
||||
}
|
||||
}
|
||||
|
||||
if ($context->collect_references) {
|
||||
foreach ($new_unreferenced_vars as $var_id => $locations) {
|
||||
if (($all_options_matched && isset($new_assigned_var_ids[$var_id]))
|
||||
|| !isset($context->vars_in_scope[$var_id])
|
||||
) {
|
||||
$context->unreferenced_vars[$var_id] = $locations;
|
||||
} elseif (isset($new_possibly_assigned_var_ids[$var_id])) {
|
||||
if (!isset($context->unreferenced_vars[$var_id])) {
|
||||
$context->unreferenced_vars[$var_id] = $locations;
|
||||
} else {
|
||||
$context->unreferenced_vars[$var_id] += $locations;
|
||||
}
|
||||
} else {
|
||||
$statements_checker->registerVariableUses($locations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$context->vars_possibly_in_scope = array_merge($context->vars_possibly_in_scope, $new_vars_possibly_in_scope);
|
||||
|
||||
return null;
|
||||
|
@ -99,11 +99,11 @@ class TryChecker
|
||||
)
|
||||
);
|
||||
|
||||
foreach ($context->unreferenced_vars as $var_id => $location) {
|
||||
foreach ($context->unreferenced_vars as $var_id => $locations) {
|
||||
if (isset($old_unreferenced_vars[$var_id])
|
||||
&& $old_unreferenced_vars[$var_id] !== $location
|
||||
&& $old_unreferenced_vars[$var_id] !== $locations
|
||||
) {
|
||||
$reassigned_vars[$var_id] = $location;
|
||||
$reassigned_vars[$var_id] = $locations;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -221,7 +221,7 @@ class TryChecker
|
||||
$location,
|
||||
$try_context->branch_point
|
||||
);
|
||||
$catch_context->unreferenced_vars[$catch_var_id] = $location;
|
||||
$catch_context->unreferenced_vars[$catch_var_id] = [$location->getHash() => $location];
|
||||
}
|
||||
|
||||
// this registers the variable to avoid unfair deadcode issues
|
||||
@ -259,16 +259,16 @@ class TryChecker
|
||||
)
|
||||
);
|
||||
|
||||
foreach ($catch_context->unreferenced_vars as $var_id => $location) {
|
||||
foreach ($catch_context->unreferenced_vars as $var_id => $locations) {
|
||||
if (!isset($old_unreferenced_vars[$var_id])
|
||||
&& (isset($context->unreferenced_vars[$var_id])
|
||||
|| isset($newly_assigned_var_ids[$var_id]))
|
||||
) {
|
||||
$statements_checker->registerVariableUse($location);
|
||||
$statements_checker->registerVariableUses($locations);
|
||||
} elseif (isset($old_unreferenced_vars[$var_id])
|
||||
&& $old_unreferenced_vars[$var_id] !== $location
|
||||
&& $old_unreferenced_vars[$var_id] !== $locations
|
||||
) {
|
||||
$statements_checker->registerVariableUse($location);
|
||||
$statements_checker->registerVariableUses($locations);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -304,9 +304,11 @@ class TryChecker
|
||||
}
|
||||
|
||||
if ($context->collect_references) {
|
||||
foreach ($old_unreferenced_vars as $var_id => $location) {
|
||||
if (isset($context->unreferenced_vars[$var_id]) && $context->unreferenced_vars[$var_id] !== $location) {
|
||||
$statements_checker->registerVariableUse($location);
|
||||
foreach ($old_unreferenced_vars as $var_id => $locations) {
|
||||
if (isset($context->unreferenced_vars[$var_id])
|
||||
&& $context->unreferenced_vars[$var_id] !== $locations
|
||||
) {
|
||||
$statements_checker->registerVariableUses($locations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -244,7 +244,7 @@ class AssignmentChecker
|
||||
$location = new CodeLocation($statements_checker, $assign_var);
|
||||
|
||||
if ($context->collect_references) {
|
||||
$context->unreferenced_vars[$var_id] = $location;
|
||||
$context->unreferenced_vars[$var_id] = [$location->getHash() => $location];
|
||||
}
|
||||
|
||||
if (!$statements_checker->hasVariable($var_id)) {
|
||||
@ -261,7 +261,7 @@ class AssignmentChecker
|
||||
}
|
||||
|
||||
if (isset($context->byref_constraints[$var_id])) {
|
||||
$statements_checker->registerVariableUse($location);
|
||||
$statements_checker->registerVariableUses([$location->getHash() => $location]);
|
||||
}
|
||||
} elseif ($assign_var instanceof PhpParser\Node\Expr\List_
|
||||
|| $assign_var instanceof PhpParser\Node\Expr\Array_
|
||||
@ -338,12 +338,16 @@ class AssignmentChecker
|
||||
|
||||
if ($list_var_id) {
|
||||
$context->vars_possibly_in_scope[$list_var_id] = true;
|
||||
$context->assigned_var_ids[$list_var_id] = true;
|
||||
$context->possibly_assigned_var_ids[$list_var_id] = true;
|
||||
|
||||
$already_in_scope = isset($context->vars_in_scope[$var_id]);
|
||||
|
||||
if (strpos($list_var_id, '-') === false && strpos($list_var_id, '[') === false) {
|
||||
$location = new CodeLocation($statements_checker, $assign_var);
|
||||
$location = new CodeLocation($statements_checker, $var);
|
||||
|
||||
if ($context->collect_references) {
|
||||
$context->unreferenced_vars[$list_var_id] = $location;
|
||||
$context->unreferenced_vars[$list_var_id] = [$location->getHash() => $location];
|
||||
}
|
||||
|
||||
if (!$statements_checker->hasVariable($list_var_id)) {
|
||||
@ -360,7 +364,7 @@ class AssignmentChecker
|
||||
}
|
||||
|
||||
if (isset($context->byref_constraints[$list_var_id])) {
|
||||
$statements_checker->registerVariableUse($location);
|
||||
$statements_checker->registerVariableUses([$location->getHash() => $location]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,7 +387,7 @@ class AssignmentChecker
|
||||
}
|
||||
}
|
||||
|
||||
if ($context->hasVariable($list_var_id)) {
|
||||
if ($already_in_scope) {
|
||||
// removes dependennt vars from $context
|
||||
$context->removeDescendents(
|
||||
$list_var_id,
|
||||
|
@ -526,7 +526,7 @@ class CallChecker
|
||||
null
|
||||
);
|
||||
|
||||
$statements_checker->registerVariableUse($location);
|
||||
$statements_checker->registerVariableUses([$location->getHash() => $location]);
|
||||
}
|
||||
} else {
|
||||
$context->removeVarFromConflictingClauses(
|
||||
@ -811,6 +811,7 @@ class CallChecker
|
||||
} else {
|
||||
if ($existing_generic_params_to_strings) {
|
||||
$empty_generic_params = [];
|
||||
|
||||
$param_type->replaceTemplateTypesWithStandins(
|
||||
$existing_generic_params_to_strings,
|
||||
$empty_generic_params,
|
||||
|
@ -255,7 +255,7 @@ class VariableFetchChecker
|
||||
}
|
||||
}
|
||||
|
||||
$statements_checker->registerVariableUse($first_appearance);
|
||||
$statements_checker->registerVariableUses([$first_appearance->getHash() => $first_appearance]);
|
||||
}
|
||||
} else {
|
||||
$stmt->inferredType = clone $context->vars_in_scope[$var_name];
|
||||
|
@ -572,7 +572,7 @@ class ExpressionChecker
|
||||
$statements_checker->registerVariable($var_id, $location, null);
|
||||
|
||||
if ($context->collect_references) {
|
||||
$context->unreferenced_vars[$var_id] = $location;
|
||||
$context->unreferenced_vars[$var_id] = [$location->getHash() => $location];
|
||||
}
|
||||
|
||||
$context->hasVariable($var_id, $statements_checker);
|
||||
|
@ -72,6 +72,11 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
||||
*/
|
||||
private $unused_var_locations = [];
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $used_var_locations = [];
|
||||
|
||||
/**
|
||||
* @param StatementsSource $source
|
||||
*/
|
||||
@ -310,6 +315,16 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($context->collect_references && (!$context->inside_case || $stmt->num)) {
|
||||
foreach ($context->unreferenced_vars as $var_id => $locations) {
|
||||
if (isset($loop_scope->unreferenced_vars[$var_id])) {
|
||||
$loop_scope->unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$loop_scope->unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$has_returned = true;
|
||||
@ -358,6 +373,16 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
||||
$loop_scope->possibly_redefined_loop_vars[$var] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($context->collect_references && (!$context->inside_case || $stmt->num)) {
|
||||
foreach ($context->unreferenced_vars as $var_id => $locations) {
|
||||
if (isset($loop_scope->possibly_unreferenced_vars[$var_id])) {
|
||||
$loop_scope->possibly_unreferenced_vars[$var_id] += $locations;
|
||||
} else {
|
||||
$loop_scope->possibly_unreferenced_vars[$var_id] = $locations;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$has_returned = true;
|
||||
@ -653,8 +678,8 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
||||
$source = $this->getSource();
|
||||
$function_storage = $source instanceof FunctionLikeChecker ? $source->getFunctionLikeStorage($this) : null;
|
||||
|
||||
foreach ($this->unused_var_locations as list($var_id, $original_location)) {
|
||||
if ($var_id === '$_') {
|
||||
foreach ($this->unused_var_locations as $hash => list($var_id, $original_location)) {
|
||||
if ($var_id === '$_' || isset($this->used_var_locations[$hash])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -701,7 +726,7 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
||||
$location = new CodeLocation($this, $stmt);
|
||||
|
||||
if ($context->collect_references) {
|
||||
$context->unreferenced_vars[$var_id] = $location;
|
||||
$context->unreferenced_vars[$var_id] = [$location->getHash() => $location];
|
||||
}
|
||||
|
||||
$this->registerVariable(
|
||||
@ -1174,15 +1199,19 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
||||
*/
|
||||
public function registerVariableAssignment($var_id, CodeLocation $location)
|
||||
{
|
||||
$this->unused_var_locations[spl_object_hash($location)] = [$var_id, $location];
|
||||
$this->unused_var_locations[$location->getHash()] = [$var_id, $location];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, CodeLocation> $locations
|
||||
* @return void
|
||||
*/
|
||||
public function registerVariableUse(CodeLocation $location)
|
||||
public function registerVariableUses(array $locations)
|
||||
{
|
||||
unset($this->unused_var_locations[spl_object_hash($location)]);
|
||||
foreach ($locations as $hash => $_) {
|
||||
unset($this->unused_var_locations[$hash]);
|
||||
$this->used_var_locations[$hash] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -353,4 +353,12 @@ class CodeLocation
|
||||
|
||||
return [$this->preview_start, $this->preview_end];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHash()
|
||||
{
|
||||
return (string) $this->file_start;
|
||||
}
|
||||
}
|
||||
|
@ -80,13 +80,6 @@ class Config
|
||||
*/
|
||||
public $cache_directory;
|
||||
|
||||
/**
|
||||
* Whether or not to care about casing of file names
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $use_case_sensitive_file_names = false;
|
||||
|
||||
/**
|
||||
* Path to the autoader
|
||||
*
|
||||
|
@ -157,7 +157,7 @@ class Context
|
||||
/**
|
||||
* A list of variables that have never been referenced
|
||||
*
|
||||
* @var array<string, CodeLocation>
|
||||
* @var array<string, array<string, CodeLocation>>
|
||||
*/
|
||||
public $unreferenced_vars = [];
|
||||
|
||||
@ -673,7 +673,7 @@ class Context
|
||||
|
||||
if ($this->collect_references && $statements_checker) {
|
||||
if (isset($this->unreferenced_vars[$var_name])) {
|
||||
$statements_checker->registerVariableUse($this->unreferenced_vars[$var_name]);
|
||||
$statements_checker->registerVariableUses($this->unreferenced_vars[$var_name]);
|
||||
}
|
||||
|
||||
unset($this->unreferenced_vars[$var_name]);
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace Psalm\Scope;
|
||||
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Clause;
|
||||
use Psalm\Type;
|
||||
|
||||
@ -81,4 +82,9 @@ class IfScope
|
||||
* @var string[]
|
||||
*/
|
||||
public $final_actions = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, CodeLocation>>
|
||||
*/
|
||||
public $new_unreferenced_vars = [];
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace Psalm\Scope;
|
||||
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Type;
|
||||
|
||||
@ -41,6 +42,16 @@ class LoopScope
|
||||
*/
|
||||
public $protected_var_ids = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, CodeLocation>>
|
||||
*/
|
||||
public $unreferenced_vars = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, CodeLocation>>
|
||||
*/
|
||||
public $possibly_unreferenced_vars = [];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
|
@ -294,8 +294,6 @@ class ParseTree
|
||||
throw new TypeParseTreeException('Unexpected token ' . $type_token);
|
||||
}
|
||||
|
||||
$added_null = false;
|
||||
|
||||
$current_parent = $current_leaf->parent;
|
||||
|
||||
if ($current_parent instanceof ParseTree\CallableWithReturnTypeTree) {
|
||||
|
@ -113,91 +113,6 @@ class UnusedCodeTest extends TestCase
|
||||
public function providerFileCheckerValidCodeParse()
|
||||
{
|
||||
return [
|
||||
'arrayOffset' => [
|
||||
'<?php
|
||||
/** @return void */
|
||||
function foo() {
|
||||
$a = 0;
|
||||
|
||||
$arr = ["hello"];
|
||||
|
||||
echo $arr[$a];
|
||||
}',
|
||||
],
|
||||
'unset' => [
|
||||
'<?php
|
||||
/** @return void */
|
||||
function foo() {
|
||||
$a = 0;
|
||||
|
||||
$arr = ["hello"];
|
||||
|
||||
unset($arr[$a]);
|
||||
}',
|
||||
],
|
||||
'usedVariables' => [
|
||||
'<?php
|
||||
/** @return string */
|
||||
function foo() {
|
||||
$a = 5;
|
||||
$b = [];
|
||||
$c[] = "hello";
|
||||
$d = "Foo";
|
||||
$e = "arg";
|
||||
$f = new $d($e);
|
||||
return $a . implode(",", $b) . $c[0] . get_class($f);
|
||||
}',
|
||||
'error_levels' => [
|
||||
'PossiblyUndefinedVariable',
|
||||
'MixedArrayAccess',
|
||||
'MixedOperand',
|
||||
'MixedAssignment',
|
||||
],
|
||||
],
|
||||
'ifInFunctionWithReference' => [
|
||||
'<?php
|
||||
/** @return string */
|
||||
function foo() {
|
||||
$a = 5;
|
||||
if (rand(0, 1)) {
|
||||
$b = "hello";
|
||||
} else {
|
||||
$b = "goodbye";
|
||||
}
|
||||
return $a . $b;
|
||||
}',
|
||||
],
|
||||
'byrefInForeachLoop' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = [1, 2, 3];
|
||||
foreach ($a as &$b) {
|
||||
$b = $b + 1;
|
||||
}
|
||||
}',
|
||||
],
|
||||
'definedInSecondBranchOfCondition' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
if (rand(0, 1) && $a = rand(0, 1)) {
|
||||
echo $a;
|
||||
}
|
||||
}',
|
||||
],
|
||||
'booleanOr' => [
|
||||
'<?php
|
||||
function foo(int $a, int $b): bool {
|
||||
return $a || $b;
|
||||
}',
|
||||
],
|
||||
'paramUsedInIf' => [
|
||||
'<?php
|
||||
function foo(string $a): void {
|
||||
if (rand(0, 1)) {
|
||||
echo $a;
|
||||
}
|
||||
}',
|
||||
],
|
||||
'magicCall' => [
|
||||
'<?php
|
||||
class A {
|
||||
@ -262,313 +177,6 @@ class UnusedCodeTest extends TestCase
|
||||
|
||||
(new A)->foo();',
|
||||
],
|
||||
'dummyByRefVar' => [
|
||||
'<?php
|
||||
function foo(string &$a = null, string $b = null): void {
|
||||
if ($a) {
|
||||
echo $a;
|
||||
}
|
||||
if ($b) {
|
||||
echo $b;
|
||||
}
|
||||
}
|
||||
|
||||
function bar(): void {
|
||||
foo($dummy_byref_var, "hello");
|
||||
}
|
||||
|
||||
bar();',
|
||||
],
|
||||
'foreachReassigned' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = false;
|
||||
|
||||
foreach ([1, 2, 3] as $b) {
|
||||
$a = true;
|
||||
echo $b;
|
||||
}
|
||||
|
||||
echo $a;
|
||||
}',
|
||||
],
|
||||
'doWhileReassigned' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = 5;
|
||||
|
||||
do {
|
||||
echo $a;
|
||||
$a = $a - rand(-3, 3);
|
||||
} while ($a > 3);
|
||||
}',
|
||||
],
|
||||
'whileTypeChangedInIfAndContinueWithReference' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = false;
|
||||
|
||||
while (rand(0, 1)) {
|
||||
if (rand(0, 1)) {
|
||||
$a = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$a = false;
|
||||
}
|
||||
|
||||
echo $a;
|
||||
}',
|
||||
],
|
||||
'whileReassignedInIfAndContinueWithReferenceAfter' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = 5;
|
||||
|
||||
while (rand(0, 1)) {
|
||||
if (rand(0, 1)) {
|
||||
$a = 7;
|
||||
continue;
|
||||
}
|
||||
|
||||
$a = 3;
|
||||
}
|
||||
|
||||
echo $a;
|
||||
}',
|
||||
],
|
||||
'whileReassignedInIfAndContinueWithReferenceBeforeAndAfter' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = 5;
|
||||
|
||||
if ($a) {}
|
||||
|
||||
while (rand(0, 1)) {
|
||||
if (rand(0, 1)) {
|
||||
$a = 7;
|
||||
continue;
|
||||
}
|
||||
|
||||
$a = 3;
|
||||
}
|
||||
|
||||
echo $a;
|
||||
}',
|
||||
],
|
||||
'whileReassigned' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = false;
|
||||
|
||||
while(rand(0, 1)) {
|
||||
$a = true;
|
||||
}
|
||||
|
||||
echo $a;
|
||||
}',
|
||||
],
|
||||
'ifVarReassignedInBranch' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = true;
|
||||
|
||||
if (rand(0, 1)) {
|
||||
$a = false;
|
||||
}
|
||||
|
||||
if ($a) {
|
||||
echo "cool";
|
||||
}
|
||||
}',
|
||||
],
|
||||
'elseVarReassignedInBranchAndReference' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = false;
|
||||
|
||||
if (rand(0, 1)) {
|
||||
// do nothing
|
||||
} else {
|
||||
$a = true;
|
||||
//echo $a;
|
||||
}
|
||||
|
||||
if ($a) {
|
||||
echo "cool";
|
||||
}
|
||||
}',
|
||||
],
|
||||
'switchVarReassignedInBranch' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = false;
|
||||
|
||||
switch (rand(0, 2)) {
|
||||
case 0:
|
||||
$a = true;
|
||||
}
|
||||
|
||||
if ($a) {
|
||||
echo "cool";
|
||||
}
|
||||
}',
|
||||
],
|
||||
'switchVarReassignedInBranchWithDefault' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = false;
|
||||
|
||||
switch (rand(0, 2)) {
|
||||
case 0:
|
||||
$a = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
$a = false;
|
||||
}
|
||||
|
||||
if ($a) {
|
||||
echo "cool";
|
||||
}
|
||||
}',
|
||||
],
|
||||
'throwWithMessageCall' => [
|
||||
'<?php
|
||||
function dangerous(): void {
|
||||
throw new \Exception("bad");
|
||||
}
|
||||
|
||||
function callDangerous(): void {
|
||||
try {
|
||||
dangerous();
|
||||
} catch (Exception $e) {
|
||||
echo $e->getMessage();
|
||||
}
|
||||
}',
|
||||
],
|
||||
'throwWithMessageCallAndAssignmentAndReference' => [
|
||||
'<?php
|
||||
function dangerous(): string {
|
||||
if (rand(0, 1)) {
|
||||
throw new \Exception("bad");
|
||||
}
|
||||
|
||||
return "hello";
|
||||
}
|
||||
|
||||
function callDangerous(): void {
|
||||
$s = null;
|
||||
|
||||
try {
|
||||
$s = dangerous();
|
||||
} catch (Exception $e) {
|
||||
echo $e->getMessage();
|
||||
}
|
||||
|
||||
if ($s) {}
|
||||
}',
|
||||
],
|
||||
'throwWithMessageCallAndAssignmentInCatchAndReference' => [
|
||||
'<?php
|
||||
function dangerous(): string {
|
||||
if (rand(0, 1)) {
|
||||
throw new \Exception("bad");
|
||||
}
|
||||
|
||||
return "hello";
|
||||
}
|
||||
|
||||
function callDangerous(): void {
|
||||
$s = null;
|
||||
|
||||
try {
|
||||
dangerous();
|
||||
} catch (Exception $e) {
|
||||
echo $e->getMessage();
|
||||
$s = "hello";
|
||||
}
|
||||
|
||||
if ($s) {}
|
||||
}',
|
||||
],
|
||||
'throwWithMessageCallAndAssignmentInTryAndCatchAndReference' => [
|
||||
'<?php
|
||||
function dangerous(): string {
|
||||
if (rand(0, 1)) {
|
||||
throw new \Exception("bad");
|
||||
}
|
||||
|
||||
return "hello";
|
||||
}
|
||||
|
||||
function callDangerous(): void {
|
||||
$s = null;
|
||||
|
||||
try {
|
||||
$s = dangerous();
|
||||
} catch (Exception $e) {
|
||||
echo $e->getMessage();
|
||||
$s = "hello";
|
||||
}
|
||||
|
||||
if ($s) {}
|
||||
}',
|
||||
],
|
||||
'throwWithMessageCallAndNestedAssignmentInTryAndCatchAndReference' => [
|
||||
'<?php
|
||||
function dangerous(): string {
|
||||
if (rand(0, 1)) {
|
||||
throw new \Exception("bad");
|
||||
}
|
||||
|
||||
return "hello";
|
||||
}
|
||||
|
||||
function callDangerous(): void {
|
||||
$s = null;
|
||||
|
||||
if (rand(0, 1)) {
|
||||
$s = "hello";
|
||||
} else {
|
||||
try {
|
||||
$t = dangerous();
|
||||
} catch (Exception $e) {
|
||||
echo $e->getMessage();
|
||||
$t = "hello";
|
||||
}
|
||||
|
||||
if ($t) {
|
||||
$s = $t;
|
||||
}
|
||||
}
|
||||
|
||||
if ($s) {}
|
||||
}',
|
||||
],
|
||||
'ifInReturnBlock' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$i = false;
|
||||
|
||||
foreach ([1, 2, 3] as $a) {
|
||||
if (rand(0, 1)) {
|
||||
$i = true;
|
||||
}
|
||||
|
||||
echo $a;
|
||||
}
|
||||
|
||||
if ($i) {}
|
||||
}',
|
||||
],
|
||||
'unknownMethodCallWithVar' => [
|
||||
'<?php
|
||||
/** @psalm-suppress MixedMethodCall */
|
||||
function passesByRef(object $a): void {
|
||||
$a->passedByRef($b);
|
||||
}',
|
||||
],
|
||||
'constructorIsUsed' => [
|
||||
'<?php
|
||||
class A {
|
||||
@ -627,63 +235,6 @@ class UnusedCodeTest extends TestCase
|
||||
|
||||
new A([1, 2, 3]);',
|
||||
],
|
||||
'usedMethodCallVariable' => [
|
||||
'<?php
|
||||
function reindex(array $arr, string $methodName): array {
|
||||
$ret = [];
|
||||
|
||||
foreach ($arr as $element) {
|
||||
$ret[$element->$methodName()] = true;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}',
|
||||
'error_levels' => [
|
||||
'MixedAssignment',
|
||||
'MixedMethodCall',
|
||||
'MixedArrayOffset',
|
||||
],
|
||||
],
|
||||
'globalVariableUsage' => [
|
||||
'<?php
|
||||
$a = "hello";
|
||||
function example() : void {
|
||||
global $a;
|
||||
echo $a;
|
||||
}
|
||||
example();',
|
||||
],
|
||||
'staticVar' => [
|
||||
'<?php
|
||||
function use_static() : void {
|
||||
static $token;
|
||||
if (!$token) {
|
||||
$token = rand(1, 10);
|
||||
}
|
||||
echo "token is $token\n";
|
||||
}',
|
||||
],
|
||||
'tryCatchWithUseInIf' => [
|
||||
'<?php
|
||||
function example_string() : string {
|
||||
if (rand(0, 1) > 0) {
|
||||
return "value";
|
||||
}
|
||||
throw new Exception("fail");
|
||||
}
|
||||
|
||||
function main() : void {
|
||||
try {
|
||||
$s = example_string();
|
||||
if (!$s) {
|
||||
echo "Failed to get string\n";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$s = "fallback";
|
||||
}
|
||||
printf("s is %s\n", $s);
|
||||
}',
|
||||
],
|
||||
'unusedParamWithUnderscore' => [
|
||||
'<?php
|
||||
function foo(int $_) : void {}
|
||||
@ -742,275 +293,6 @@ class UnusedCodeTest extends TestCase
|
||||
public function providerFileCheckerInvalidCodeParse()
|
||||
{
|
||||
return [
|
||||
'function' => [
|
||||
'<?php
|
||||
/** @return int */
|
||||
function foo() {
|
||||
$a = 5;
|
||||
$b = [];
|
||||
return $a;
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'ifInFunctionWithoutReference' => [
|
||||
'<?php
|
||||
/** @return int */
|
||||
function foo() {
|
||||
$a = 5;
|
||||
if (rand(0, 1)) {
|
||||
$b = "hello";
|
||||
} else {
|
||||
$b = "goodbye";
|
||||
}
|
||||
return $a;
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'varInNestedAssignmentWithoutReference' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
if (rand(0, 1)) {
|
||||
$a = "foo";
|
||||
}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'varInSecondNestedAssignmentWithoutReference' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
if (rand(0, 1)) {
|
||||
$a = "foo";
|
||||
echo $a;
|
||||
}
|
||||
|
||||
if (rand(0, 1)) {
|
||||
$a = "foo";
|
||||
}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'varReassignedInBothBranchesOfIf' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = "foo";
|
||||
|
||||
if (rand(0, 1)) {
|
||||
$a = "bar";
|
||||
} else {
|
||||
$a = "bat";
|
||||
}
|
||||
|
||||
echo $a;
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'varReassignedInNestedBranchesOfIf' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = "foo";
|
||||
|
||||
if (rand(0, 1)) {
|
||||
if (rand(0, 1)) {
|
||||
$a = "bar";
|
||||
} else {
|
||||
$a = "bat";
|
||||
}
|
||||
} else {
|
||||
$a = "bang";
|
||||
}
|
||||
|
||||
echo $a;
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'ifVarReassignedInBranch' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = true;
|
||||
|
||||
if (rand(0, 1)) {
|
||||
$a = false;
|
||||
}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'elseVarReassignedInBranchAndNoReference' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = true;
|
||||
|
||||
if (rand(0, 1)) {
|
||||
// do nothing
|
||||
} else {
|
||||
$a = false;
|
||||
}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'switchVarReassignedInBranch' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = false;
|
||||
|
||||
switch (rand(0, 2)) {
|
||||
case 0:
|
||||
$a = true;
|
||||
}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'switchVarReassignedInBranchWithDefault' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = false;
|
||||
|
||||
switch (rand(0, 2)) {
|
||||
case 0:
|
||||
$a = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
$a = false;
|
||||
}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'unusedListVar' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
list($a, $b) = explode(" ", "hello world");
|
||||
echo $a;
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'unusedPreForVar' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$i = 0;
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
echo $i;
|
||||
}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'unusedIfInReturnBlock' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$i = rand(0, 1);
|
||||
|
||||
foreach ([1, 2, 3] as $a) {
|
||||
if ($a % 2) {
|
||||
$i = 7;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($i) {}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'unusedIfVarInBranch' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
if (rand(0, 1)) {
|
||||
|
||||
} elseif (rand(0, 1)) {
|
||||
if (rand(0, 1)) {
|
||||
$a = "foo";
|
||||
} else {
|
||||
$a = "bar";
|
||||
echo $a;
|
||||
}
|
||||
}
|
||||
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'throwWithMessageCallAndAssignmentAndNoReference' => [
|
||||
'<?php
|
||||
function dangerous(): string {
|
||||
if (rand(0, 1)) {
|
||||
throw new \Exception("bad");
|
||||
}
|
||||
|
||||
return "hello";
|
||||
}
|
||||
|
||||
function callDangerous(): void {
|
||||
$s = null;
|
||||
|
||||
try {
|
||||
$s = dangerous();
|
||||
} catch (Exception $e) {
|
||||
echo $e->getMessage();
|
||||
}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'whileTypeChangedInIfWithoutReference' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = false;
|
||||
|
||||
while (rand(0, 1)) {
|
||||
if (rand(0, 1)) {
|
||||
$a = true;
|
||||
}
|
||||
}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'whileTypeChangedInIfAndContinueWithoutReference' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = false;
|
||||
|
||||
while (rand(0, 1)) {
|
||||
if (rand(0, 1)) {
|
||||
$a = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$a = false;
|
||||
}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'whileReassignedInIfAndContinueWithoutReferenceAfter' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = 5;
|
||||
|
||||
while (rand(0, 1)) {
|
||||
if (rand(0, 1)) {
|
||||
$a = 7;
|
||||
continue;
|
||||
}
|
||||
|
||||
$a = 3;
|
||||
}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'whileReassignedInIfAndContinueWithoutReference' => [
|
||||
'<?php
|
||||
function foo(): void {
|
||||
$a = 3;
|
||||
|
||||
if ($a) {}
|
||||
|
||||
while (rand(0, 1)) {
|
||||
if (rand(0, 1)) {
|
||||
$a = 5;
|
||||
continue;
|
||||
}
|
||||
|
||||
$a = 3;
|
||||
}
|
||||
}',
|
||||
'error_message' => 'UnusedVariable',
|
||||
],
|
||||
'unusedClass' => [
|
||||
'<?php
|
||||
class A { }',
|
||||
|
1162
tests/UnusedVariableTest.php
Normal file
1162
tests/UnusedVariableTest.php
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user