diff --git a/src/Psalm/Checker/ClassChecker.php b/src/Psalm/Checker/ClassChecker.php index 834d0f54b..5805797f1 100644 --- a/src/Psalm/Checker/ClassChecker.php +++ b/src/Psalm/Checker/ClassChecker.php @@ -380,7 +380,6 @@ class ClassChecker extends ClassLikeChecker foreach ($storage->appearing_property_ids as $property_name => $appearing_property_id) { $property_class_name = self::getDeclaringClassForProperty($project_checker, $appearing_property_id); $property_class_storage = $classlike_storage_provider->get((string)$property_class_name); - $property_class_name = self::getDeclaringClassForProperty($project_checker, $appearing_property_id); $property = $property_class_storage->properties[$property_name]; @@ -510,7 +509,6 @@ class ClassChecker extends ClassLikeChecker foreach ($storage->appearing_property_ids as $property_name => $appearing_property_id) { $property_class_name = self::getDeclaringClassForProperty($project_checker, $appearing_property_id); $property_class_storage = $classlike_storage_provider->get((string)$property_class_name); - $property_class_name = self::getDeclaringClassForProperty($project_checker, $appearing_property_id); $property = $property_class_storage->properties[$property_name]; diff --git a/src/Psalm/Checker/FunctionChecker.php b/src/Psalm/Checker/FunctionChecker.php index cbcd0feba..58060d08c 100644 --- a/src/Psalm/Checker/FunctionChecker.php +++ b/src/Psalm/Checker/FunctionChecker.php @@ -636,7 +636,6 @@ class FunctionChecker extends FunctionLikeChecker && $stmt instanceof PhpParser\Node\Stmt\Return_ && $stmt->expr ) { - $first_param_name = $first_param->name; $assertions = AssertionFinder::getAssertions($stmt->expr, null, $statements_checker); if (isset($assertions['$' . $first_param->name])) { diff --git a/src/Psalm/Checker/FunctionLikeChecker.php b/src/Psalm/Checker/FunctionLikeChecker.php index c1bbb0dc7..fb8b9fa9e 100644 --- a/src/Psalm/Checker/FunctionLikeChecker.php +++ b/src/Psalm/Checker/FunctionLikeChecker.php @@ -32,7 +32,6 @@ use Psalm\Issue\OverriddenMethodAccess; use Psalm\Issue\ReservedWord; use Psalm\Issue\UntypedParam; use Psalm\Issue\UnusedParam; -use Psalm\Issue\UnusedVariable; use Psalm\IssueBuffer; use Psalm\StatementsSource; use Psalm\Storage\ClassLikeStorage; @@ -162,17 +161,22 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo $context->vars_possibly_in_scope['$this'] = true; } - $declaring_method_id = MethodChecker::getDeclaringMethodId($project_checker, $method_id); - - if (!is_string($declaring_method_id)) { - throw new \UnexpectedValueException('The declaring method of ' . $method_id . ' should not be null'); - } - $fq_class_name = (string)$context->self; $class_storage = $classlike_storage_provider->get($fq_class_name); - $storage = $codebase->getMethodStorage($declaring_method_id); + try { + $storage = $codebase->getMethodStorage($real_method_id); + } catch (\UnexpectedValueException $e) { + if (!$class_storage->parent_classes) { + throw $e; + } + + $declaring_method_id = (string) MethodChecker::getDeclaringMethodId($project_checker, $method_id); + + // happens for fake constructors + $storage = $codebase->getMethodStorage($declaring_method_id); + } $cased_method_id = $fq_class_name . '::' . $storage->cased_name; @@ -285,6 +289,10 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo $context->vars_in_scope['$' . $function_param->name] = $param_type; $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; + } + if (!$function_param->type_location || !$function_param->location) { continue; } @@ -404,8 +412,9 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo } } - if ($function_param->by_ref && !$param_type->isMixed()) { - $context->byref_constraints['$' . $function_param->name] = new \Psalm\ReferenceConstraint($param_type); + if ($function_param->by_ref) { + $context->byref_constraints['$' . $function_param->name] + = new \Psalm\ReferenceConstraint(!$param_type->isMixed() ? $param_type : null); } if ($function_param->by_ref) { @@ -555,71 +564,57 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo } } - if ($context->collect_references && - !$this->getFileChecker()->project_checker->find_references_to && - $context->check_variables + if ($context->collect_references + && !$project_checker->find_references_to + && $context->check_variables ) { - foreach ($context->vars_possibly_in_scope as $var_name => $_) { - if (strpos($var_name, '->') === false && - $var_name !== '$this' && - strpos($var_name, '::$') === false && - strpos($var_name, '[') === false && - $var_name !== '$_' + foreach ($context->unreferenced_vars as $var_name => $original_location) { + if (!array_key_exists(substr($var_name, 1), $storage->param_types)) { + continue; + } + + if (!($storage instanceof MethodStorage) + || $storage->visibility === ClassLikeChecker::VISIBILITY_PRIVATE ) { - $original_location = $statements_checker->getFirstAppearance($var_name); + if (IssueBuffer::accepts( + new UnusedParam( + 'Param ' . $var_name . ' is never referenced in this method', + $original_location + ), + $this->getSuppressedIssues() + )) { + // fall through + } + } else { + $fq_class_name = (string)$context->self; - if (!isset($context->referenced_var_ids[$var_name]) && $original_location) { - if (!array_key_exists(substr($var_name, 1), $storage->param_types)) { - if (IssueBuffer::accepts( - new UnusedVariable( - 'Variable ' . $var_name . ' is never referenced', - $original_location - ), - $this->getSuppressedIssues() - )) { - // fall through - } - } elseif (!$storage instanceof MethodStorage - || $storage->visibility === ClassLikeChecker::VISIBILITY_PRIVATE + $class_storage = $codebase->classlike_storage_provider->get($fq_class_name); + + if (!$class_storage || $storage->abstract) { + continue; + } + + $method_name_lc = strtolower($storage->cased_name); + $parent_method_id = end($class_storage->overridden_method_ids[$method_name_lc]); + + $position = array_search(substr($var_name, 1), array_keys($storage->param_types), true); + + if ($position === false) { + throw new \UnexpectedValueException('$position should not be false here'); + } + + if ($parent_method_id) { + $parent_method_storage = $codebase->getMethodStorage($parent_method_id); + + // if the parent method has a param at that position and isn't abstract + if (!$parent_method_storage->abstract + && isset($parent_method_storage->params[$position]) ) { - if (IssueBuffer::accepts( - new UnusedParam( - 'Param ' . $var_name . ' is never referenced in this method', - $original_location - ), - $this->getSuppressedIssues() - )) { - // fall through - } - } else { - if (!$class_storage || $storage->abstract) { - continue; - } - - /** @var ClassMethod $this->function */ - $method_name_lc = strtolower((string)$this->function->name); - $parent_method_id = end($class_storage->overridden_method_ids[$method_name_lc]); - - $position = array_search(substr($var_name, 1), array_keys($storage->param_types), true); - - if ($position === false) { - throw new \UnexpectedValueException('$position should not be false here'); - } - - if ($parent_method_id) { - $parent_method_storage = $codebase->getMethodStorage($parent_method_id); - - // if the parent method has a param at that position and isn't abstract - if (!$parent_method_storage->abstract - && isset($parent_method_storage->params[$position]) - ) { - continue; - } - } - - $storage->unused_params[$position] = $original_location; + continue; } } + + $storage->unused_params[$position] = $original_location; } } @@ -629,7 +624,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo $storage->used_params[$i] = true; /** @var ClassMethod $this->function */ - $method_name_lc = strtolower((string)$this->function->name); + $method_name_lc = strtolower($storage->cased_name); if (!isset($class_storage->overridden_method_ids[$method_name_lc])) { continue; @@ -1066,22 +1061,23 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo */ public function getFunctionLikeStorage(StatementsChecker $statements_checker) { - $function_id = $this->getMethodId(); - $project_checker = $this->getFileChecker()->project_checker; $codebase = $project_checker->codebase; - if (strpos($function_id, '::')) { - $declaring_method_id = MethodChecker::getDeclaringMethodId($project_checker, $function_id); + if ($this->function instanceof ClassMethod) { + $method_id = (string) $this->getMethodId(); - if (!$declaring_method_id) { - throw new \UnexpectedValueException('The declaring method of ' . $function_id . ' should not be null'); + try { + return $codebase->getMethodStorage($method_id); + } catch (\UnexpectedValueException $e) { + $declaring_method_id = (string) MethodChecker::getDeclaringMethodId($project_checker, $method_id); + + // happens for fake constructors + return $codebase->getMethodStorage($declaring_method_id); } - - return $codebase->getMethodStorage($declaring_method_id); } - return $codebase->getFunctionStorage($statements_checker, $function_id); + return $codebase->getFunctionStorage($statements_checker, (string) $this->getMethodId()); } /** diff --git a/src/Psalm/Checker/Statements/Block/DoChecker.php b/src/Psalm/Checker/Statements/Block/DoChecker.php index 810995e68..86c4f4848 100644 --- a/src/Psalm/Checker/Statements/Block/DoChecker.php +++ b/src/Psalm/Checker/Statements/Block/DoChecker.php @@ -72,6 +72,13 @@ class DoChecker $do_context->referenced_var_ids ); + if ($context->collect_references) { + $context->unreferenced_vars = array_intersect_key( + $do_context->unreferenced_vars, + $context->unreferenced_vars + ); + } + return ExpressionChecker::analyze($statements_checker, $stmt->cond, $context); } } diff --git a/src/Psalm/Checker/Statements/Block/ForChecker.php b/src/Psalm/Checker/Statements/Block/ForChecker.php index 960d984ed..f538fad04 100644 --- a/src/Psalm/Checker/Statements/Block/ForChecker.php +++ b/src/Psalm/Checker/Statements/Block/ForChecker.php @@ -98,6 +98,13 @@ class ForChecker $context->referenced_var_ids ); + if ($context->collect_references) { + $context->unreferenced_vars = array_intersect_key( + $for_context->unreferenced_vars, + $context->unreferenced_vars + ); + } + return null; } } diff --git a/src/Psalm/Checker/Statements/Block/ForeachChecker.php b/src/Psalm/Checker/Statements/Block/ForeachChecker.php index 8a2f45e66..90cddaa39 100644 --- a/src/Psalm/Checker/Statements/Block/ForeachChecker.php +++ b/src/Psalm/Checker/Statements/Block/ForeachChecker.php @@ -272,6 +272,15 @@ class ForeachChecker } } + if ($context->collect_references + && $stmt->byRef + && $stmt->valueVar instanceof PhpParser\Node\Expr\Variable + && is_string($stmt->valueVar->name) + ) { + $foreach_context->byref_constraints['$' . $stmt->valueVar->name] + = new \Psalm\ReferenceConstraint($value_type); + } + AssignmentChecker::analyze( $statements_checker, $stmt->valueVar, @@ -335,6 +344,10 @@ class ForeachChecker $context->referenced_var_ids ); + if ($context->collect_references) { + $context->unreferenced_vars = $foreach_context->unreferenced_vars; + } + return null; } } diff --git a/src/Psalm/Checker/Statements/Block/IfChecker.php b/src/Psalm/Checker/Statements/Block/IfChecker.php index 1d00ff0cb..c7199b5c9 100644 --- a/src/Psalm/Checker/Statements/Block/IfChecker.php +++ b/src/Psalm/Checker/Statements/Block/IfChecker.php @@ -283,6 +283,22 @@ class IfChecker return false; } + $newly_unreferenced_vars = []; + $old_unreferenced_vars = $context->unreferenced_vars; + + if ($context->collect_references) { + foreach ($context->unreferenced_vars as $var_id => $_) { + if (!isset($if_context->unreferenced_vars[$var_id])) { + unset($context->unreferenced_vars[$var_id]); + } + } + + $newly_unreferenced_vars = array_diff_key( + $if_context->unreferenced_vars, + $old_unreferenced_vars + ); + } + // check the elseifs foreach ($stmt->elseifs as $elseif) { $elseif_context = clone $original_context; @@ -302,6 +318,22 @@ class IfChecker ) === false) { return false; } + + if ($context->collect_references) { + foreach ($context->unreferenced_vars as $var_id => $_) { + if (!isset($elseif_context->unreferenced_vars[$var_id])) { + unset($context->unreferenced_vars[$var_id]); + } + } + + $newly_unreferenced_vars = array_merge( + $newly_unreferenced_vars, + array_diff_key( + $elseif_context->unreferenced_vars, + $old_unreferenced_vars + ) + ); + } } // check the else @@ -323,6 +355,22 @@ class IfChecker ) === false) { return false; } + + if ($context->collect_references) { + foreach ($context->unreferenced_vars as $var_id => $_) { + if (!isset($else_context->unreferenced_vars[$var_id])) { + unset($context->unreferenced_vars[$var_id]); + } + } + + $newly_unreferenced_vars = array_merge( + $newly_unreferenced_vars, + array_diff_key( + $else_context->unreferenced_vars, + $old_unreferenced_vars + ) + ); + } } else { $if_scope->final_actions[] = ScopeChecker::ACTION_NONE; } @@ -385,6 +433,13 @@ class IfChecker } } + if ($context->collect_references) { + $context->unreferenced_vars = array_merge( + $context->unreferenced_vars, + $newly_unreferenced_vars + ); + } + return null; } @@ -441,12 +496,14 @@ class IfChecker if ($if_context->byref_constraints !== null) { foreach ($if_context->byref_constraints as $var_id => $byref_constraint) { - if ($outer_context->byref_constraints !== null && - isset($outer_context->byref_constraints[$var_id]) && - !TypeChecker::isContainedBy( + if ($outer_context->byref_constraints !== null + && isset($outer_context->byref_constraints[$var_id]) + && $byref_constraint->type + && ($outer_constraint_type = $outer_context->byref_constraints[$var_id]->type) + && !TypeChecker::isContainedBy( $project_checker, $byref_constraint->type, - $outer_context->byref_constraints[$var_id]->type + $outer_constraint_type ) ) { if (IssueBuffer::accepts( @@ -829,12 +886,14 @@ class IfChecker if ($elseif_context->byref_constraints !== null) { foreach ($elseif_context->byref_constraints as $var_id => $byref_constraint) { - if ($outer_context->byref_constraints !== null && - isset($outer_context->byref_constraints[$var_id]) && - !TypeChecker::isContainedBy( + if ($outer_context->byref_constraints !== null + && isset($outer_context->byref_constraints[$var_id]) + && ($outer_constraint_type = $outer_context->byref_constraints[$var_id]->type) + && $byref_constraint->type + && !TypeChecker::isContainedBy( $project_checker, $byref_constraint->type, - $outer_context->byref_constraints[$var_id]->type + $outer_constraint_type ) ) { if (IssueBuffer::accepts( @@ -1087,12 +1146,14 @@ class IfChecker $project_checker = $statements_checker->getFileChecker()->project_checker; foreach ($else_context->byref_constraints as $var_id => $byref_constraint) { - if ($outer_context->byref_constraints !== null && - isset($outer_context->byref_constraints[$var_id]) && - !TypeChecker::isContainedBy( + if ($outer_context->byref_constraints !== null + && isset($outer_context->byref_constraints[$var_id]) + && ($outer_constraint_type = $outer_context->byref_constraints[$var_id]->type) + && $byref_constraint->type + && !TypeChecker::isContainedBy( $project_checker, $byref_constraint->type, - $outer_context->byref_constraints[$var_id]->type + $outer_constraint_type ) ) { if (IssueBuffer::accepts( diff --git a/src/Psalm/Checker/Statements/Block/LoopChecker.php b/src/Psalm/Checker/Statements/Block/LoopChecker.php index 8b35133d2..691481ee7 100644 --- a/src/Psalm/Checker/Statements/Block/LoopChecker.php +++ b/src/Psalm/Checker/Statements/Block/LoopChecker.php @@ -365,6 +365,13 @@ class LoopChecker $inner_context->referenced_var_ids, $loop_scope->loop_context->referenced_var_ids ); + + if ($inner_context->collect_references) { + $loop_scope->loop_context->unreferenced_vars = array_diff_key( + $inner_context->unreferenced_vars, + $inner_context->referenced_var_ids + ); + } } /** diff --git a/src/Psalm/Checker/Statements/Block/SwitchChecker.php b/src/Psalm/Checker/Statements/Block/SwitchChecker.php index 5d4424558..8bfe0103f 100644 --- a/src/Psalm/Checker/Statements/Block/SwitchChecker.php +++ b/src/Psalm/Checker/Statements/Block/SwitchChecker.php @@ -198,6 +198,13 @@ class SwitchChecker $case_context->referenced_var_ids ); + if ($context->collect_references) { + $context->unreferenced_vars = array_intersect_key( + $case_context->unreferenced_vars, + $context->unreferenced_vars + ); + } + if ($case_exit_type !== 'return_throw') { $vars = array_diff_key( $case_context->vars_possibly_in_scope, diff --git a/src/Psalm/Checker/Statements/Block/TryChecker.php b/src/Psalm/Checker/Statements/Block/TryChecker.php index af95cec06..14d0f3ada 100644 --- a/src/Psalm/Checker/Statements/Block/TryChecker.php +++ b/src/Psalm/Checker/Statements/Block/TryChecker.php @@ -75,6 +75,18 @@ class TryChecker } $try_context->vars_possibly_in_scope = $context->vars_possibly_in_scope; + + $context->referenced_var_ids = array_merge( + $try_context->referenced_var_ids, + $context->referenced_var_ids + ); + + if ($context->collect_references) { + $context->unreferenced_vars = array_intersect_key( + $try_context->unreferenced_vars, + $context->unreferenced_vars + ); + } } $try_leaves_loop = $loop_scope @@ -200,6 +212,13 @@ class TryChecker $context->referenced_var_ids ); + if ($context->collect_references) { + $context->unreferenced_vars = array_intersect_key( + $catch_context->unreferenced_vars, + $context->unreferenced_vars + ); + } + if ($catch_actions[$i] !== [ScopeChecker::ACTION_END]) { foreach ($catch_context->vars_in_scope as $var_id => $type) { if ($catch->var !== $var_id && diff --git a/src/Psalm/Checker/Statements/Block/WhileChecker.php b/src/Psalm/Checker/Statements/Block/WhileChecker.php index f9ebbc000..1d64b71e8 100644 --- a/src/Psalm/Checker/Statements/Block/WhileChecker.php +++ b/src/Psalm/Checker/Statements/Block/WhileChecker.php @@ -83,6 +83,13 @@ class WhileChecker $while_context->referenced_var_ids ); + if ($context->collect_references) { + $context->unreferenced_vars = array_intersect_key( + $while_context->unreferenced_vars, + $context->unreferenced_vars + ); + } + return null; } } diff --git a/src/Psalm/Checker/Statements/Expression/ArrayChecker.php b/src/Psalm/Checker/Statements/Expression/ArrayChecker.php index dba5e43fb..096cdf5f6 100644 --- a/src/Psalm/Checker/Statements/Expression/ArrayChecker.php +++ b/src/Psalm/Checker/Statements/Expression/ArrayChecker.php @@ -120,7 +120,7 @@ class ArrayChecker ) { $array_key = $item->key ? $item->key->value : $int_offset; - $property_types[$item->key ? $item->key->value : $int_offset] = $item_value_type; + $property_types[$array_key] = $item_value_type; } else { $can_create_objectlike = false; } diff --git a/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php b/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php index 47115f26e..22d35fab6 100644 --- a/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php +++ b/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php @@ -165,11 +165,14 @@ class AssignmentChecker )) { // fall through } - } elseif ($var_id && isset($context->byref_constraints[$var_id])) { + } elseif ($var_id + && isset($context->byref_constraints[$var_id]) + && ($outer_constraint_type = $context->byref_constraints[$var_id]->type) + ) { if (!TypeChecker::isContainedBy( $statements_checker->getFileChecker()->project_checker, $assign_value_type, - $context->byref_constraints[$var_id]->type, + $outer_constraint_type, $assign_value_type->ignore_nullable_issues, $assign_value_type->ignore_falsable_issues ) @@ -214,6 +217,10 @@ class AssignmentChecker $context->vars_possibly_in_scope[$var_id] = true; $context->assigned_var_ids[$var_id] = true; + if ($context->collect_references && !isset($context->byref_constraints[$var_id])) { + $context->unreferenced_vars[$var_id] = new CodeLocation($statements_checker, $assign_var); + } + if (!$statements_checker->hasVariable($var_id)) { $statements_checker->registerVariable( $var_id, diff --git a/src/Psalm/Checker/Statements/Expression/BinaryOpChecker.php b/src/Psalm/Checker/Statements/Expression/BinaryOpChecker.php index 6d54d3f1f..52d37221a 100644 --- a/src/Psalm/Checker/Statements/Expression/BinaryOpChecker.php +++ b/src/Psalm/Checker/Statements/Expression/BinaryOpChecker.php @@ -105,6 +105,13 @@ class BinaryOpChecker $context->referenced_var_ids ); + if ($context->collect_references) { + $context->unreferenced_vars = array_intersect_key( + $op_context->unreferenced_vars, + $context->unreferenced_vars + ); + } + foreach ($op_context->vars_in_scope as $var_id => $type) { if (isset($context->vars_in_scope[$var_id])) { $context->vars_in_scope[$var_id] = Type::combineUnionTypes($context->vars_in_scope[$var_id], $type); @@ -230,6 +237,13 @@ class BinaryOpChecker $context->referenced_var_ids ); + if ($context->collect_references) { + $context->unreferenced_vars = array_intersect_key( + $op_context->unreferenced_vars, + $context->unreferenced_vars + ); + } + $context->vars_possibly_in_scope = array_merge( $op_context->vars_possibly_in_scope, $context->vars_possibly_in_scope @@ -296,6 +310,13 @@ class BinaryOpChecker $t_if_context->referenced_var_ids ); + if ($context->collect_references) { + $context->unreferenced_vars = array_intersect_key( + $t_if_context->unreferenced_vars, + $context->unreferenced_vars + ); + } + $t_else_context = clone $context; if ($negated_if_types) { @@ -325,6 +346,13 @@ class BinaryOpChecker $t_else_context->referenced_var_ids ); + if ($context->collect_references) { + $context->unreferenced_vars = array_intersect_key( + $t_else_context->unreferenced_vars, + $context->unreferenced_vars + ); + } + $lhs_type = null; if (isset($stmt->left->inferredType)) { diff --git a/src/Psalm/Checker/Statements/Expression/TernaryChecker.php b/src/Psalm/Checker/Statements/Expression/TernaryChecker.php index 42c67d388..21d104865 100644 --- a/src/Psalm/Checker/Statements/Expression/TernaryChecker.php +++ b/src/Psalm/Checker/Statements/Expression/TernaryChecker.php @@ -87,6 +87,11 @@ class TernaryChecker $context->referenced_var_ids, $t_if_context->referenced_var_ids ); + + $context->unreferenced_vars = array_intersect_key( + $context->unreferenced_vars, + $t_if_context->unreferenced_vars + ); } if ($negated_if_types) { @@ -122,6 +127,11 @@ class TernaryChecker $t_else_context->referenced_var_ids ); + $context->unreferenced_vars = array_intersect_key( + $context->unreferenced_vars, + $t_else_context->unreferenced_vars + ); + $lhs_type = null; if ($stmt->if) { diff --git a/src/Psalm/Checker/StatementsChecker.php b/src/Psalm/Checker/StatementsChecker.php index dbfb7aa32..6d7bcd09d 100644 --- a/src/Psalm/Checker/StatementsChecker.php +++ b/src/Psalm/Checker/StatementsChecker.php @@ -26,6 +26,7 @@ use Psalm\Issue\InvalidDocblock; use Psalm\Issue\InvalidGlobal; use Psalm\Issue\UnevaluatedCode; use Psalm\Issue\UnrecognizedStatement; +use Psalm\Issue\UnusedVariable; use Psalm\IssueBuffer; use Psalm\Scope\LoopScope; use Psalm\StatementsSource; @@ -516,6 +517,14 @@ class StatementsChecker extends SourceChecker implements StatementsSource } } + if ($root_scope + && $context->collect_references + && !$project_checker->find_references_to + && $context->check_variables + ) { + $this->checkUnreferencedVars($context); + } + if ($project_checker->alter_code && $root_scope && $this->vars_to_initialize) { $file_contents = $project_checker->codebase->getFileContents($this->getFilePath()); @@ -531,6 +540,35 @@ class StatementsChecker extends SourceChecker implements StatementsSource return null; } + /** + * @param Context $context + * + * @return void + */ + private function checkUnreferencedVars(Context $context) + { + $source = $this->getSource(); + $function_storage = $source instanceof FunctionLikeChecker ? $source->getFunctionLikeStorage($this) : null; + + foreach ($context->unreferenced_vars as $var_name => $original_location) { + if ($var_name === '$_') { + continue; + } + + if (!$function_storage || !array_key_exists(substr($var_name, 1), $function_storage->param_types)) { + if (IssueBuffer::accepts( + new UnusedVariable( + 'Variable ' . $var_name . ' is never referenced', + $original_location + ), + $this->getSuppressedIssues() + )) { + // fall through + } + } + } + } + /** * @param PhpParser\Node\Stmt\Static_ $stmt * @param Context $context diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index 9a2fa03c4..dda0f2538 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -138,6 +138,13 @@ class Context */ public $referenced_var_ids = []; + /** + * A list of variables that have never been referenced + * + * @var array + */ + public $unreferenced_vars = []; + /** * A list of variables that have been passed by reference (where we know their type) * @@ -629,6 +636,10 @@ class Context if ($stripped_var[0] === '$' && $stripped_var !== '$this') { $this->referenced_var_ids[$var_name] = true; + + if ($this->collect_references) { + unset($this->unreferenced_vars[$var_name]); + } } return isset($this->vars_in_scope[$var_name]); diff --git a/src/Psalm/ReferenceConstraint.php b/src/Psalm/ReferenceConstraint.php index 9c42a0b73..9350f0ce3 100644 --- a/src/Psalm/ReferenceConstraint.php +++ b/src/Psalm/ReferenceConstraint.php @@ -3,13 +3,13 @@ namespace Psalm; class ReferenceConstraint { - /** @var Type\Union */ + /** @var Type\Union|null */ public $type; /** * @param Type\Union $type */ - public function __construct(Type\Union $type) + public function __construct(Type\Union $type = null) { $this->type = $type; } diff --git a/src/Psalm/Stubs/CoreGenericFunctions.php b/src/Psalm/Stubs/CoreGenericFunctions.php index eccd57853..c430725aa 100644 --- a/src/Psalm/Stubs/CoreGenericFunctions.php +++ b/src/Psalm/Stubs/CoreGenericFunctions.php @@ -49,6 +49,18 @@ function array_slice(array $arr, int $offset, int $length = null, bool $preserve */ function array_intersect(array $arr, array $arr2, array $arr3 = null, array $arr4 = null) {} +/** + * @template TKey + * @template TValue + * + * @param array $arr + * @param array $arr2 + * @param array|null $arr3 + * @param array|null $arr4 + * @return array + */ +function array_intersect_key(array $arr, array $arr2, array $arr3 = null, array $arr4 = null) {} + /** * @template TKey * @template TValue diff --git a/tests/UnusedCodeTest.php b/tests/UnusedCodeTest.php index adc878b1a..585ed7f7f 100644 --- a/tests/UnusedCodeTest.php +++ b/tests/UnusedCodeTest.php @@ -247,6 +247,20 @@ class UnusedCodeTest extends TestCase }', 'error_message' => 'UnusedVariable', ], + 'unusuedVariableInBranchOfIf' => [ + ' 'UnusedVariable', + ], 'unusedClass' => [ '