# frozen_string_literal: true module RuboCop module Cop module Lint # This cop checks for shadowed arguments. # # This cop has `IgnoreImplicitReferences` configuration option. # It means argument shadowing is used in order to pass parameters # to zero arity `super` when `IgnoreImplicitReferences` is `true`. # # @example # # # bad # do_something do |foo| # foo = 42 # puts foo # end # # def do_something(foo) # foo = 42 # puts foo # end # # # good # do_something do |foo| # foo = foo + 42 # puts foo # end # # def do_something(foo) # foo = foo + 42 # puts foo # end # # def do_something(foo) # puts foo # end # # @example IgnoreImplicitReferences: false (default) # # # bad # def do_something(foo) # foo = 42 # super # end # # def do_something(foo) # foo = super # bar # end # # @example IgnoreImplicitReferences: true # # # good # def do_something(foo) # foo = 42 # super # end # # def do_something(foo) # foo = super # bar # end # class ShadowedArgument < Cop MSG = 'Argument `%s` was shadowed by a local variable ' \ 'before it was used.'.freeze def_node_search :uses_var?, '(lvar %)' def join_force?(force_class) force_class == VariableForce end def after_leaving_scope(scope, _variable_table) scope.variables.each_value do |variable| check_argument(variable) end end private def check_argument(argument) return unless argument.method_argument? || argument.block_argument? # Block local variables, i.e., variables declared after ; inside # |...| aren't really arguments. return if argument.explicit_block_local_variable? shadowing_assignment(argument) do |node| message = format(MSG, argument: argument.name) add_offense(node, message: message) end end def shadowing_assignment(argument) return unless argument.referenced? assignment_without_argument_usage(argument) do |node, location_known| assignment_without_usage_pos = node.source_range.begin_pos references = argument_references(argument) # If argument was referenced before it was reassigned # then it's not shadowed next if references.any? do |reference| next true if !reference.explicit? && ignore_implicit_references? reference_pos(reference.node) <= assignment_without_usage_pos end yield location_known ? node : argument.declaration_node end end # Find the first argument assignment, which doesn't reference the # argument at the rhs. If the assignment occurs inside a branch or # block, it is impossible to tell whether it's executed, so precise # shadowing location is not known. # def assignment_without_argument_usage(argument) argument.assignments.reduce(true) do |location_known, assignment| assignment_node = assignment.meta_assignment_node || assignment.node # Shorthand assignments always use their arguments next false if assignment_node.shorthand_asgn? node_within_block_or_conditional = node_within_block_or_conditional?(assignment_node.parent, argument.scope.node) unless uses_var?(assignment_node, argument.name) # It's impossible to decide whether a branch or block is executed, # so the precise reassignment location is undecidable. next false if node_within_block_or_conditional yield(assignment.node, location_known) break end location_known end end def reference_pos(node) node = node.parent.masgn_type? ? node.parent : node node.source_range.begin_pos end # Check whether the given node is nested into block or conditional. # def node_within_block_or_conditional?(node, stop_search_node) return false if node == stop_search_node node.conditional? || node.block_type? || node_within_block_or_conditional?(node.parent, stop_search_node) end # Get argument references without assignments' references # def argument_references(argument) assignment_references = argument .assignments .flat_map(&:references) .map(&:source_range) argument.references.reject do |ref| next false unless ref.explicit? assignment_references.include?(ref.node.source_range) end end def ignore_implicit_references? cop_config['IgnoreImplicitReferences'] end end end end end