lib/rubocop/cop/variable_force/variable.rb
# frozen_string_literal: true
module RuboCop
module Cop
class VariableForce
# A Variable represents existence of a local variable.
# This holds a variable declaration node and some states of the variable.
class Variable
VARIABLE_DECLARATION_TYPES = (VARIABLE_ASSIGNMENT_TYPES + ARGUMENT_DECLARATION_TYPES).freeze
attr_reader :name, :declaration_node, :scope, :assignments, :references, :captured_by_block
alias captured_by_block? captured_by_block
def initialize(name, declaration_node, scope)
unless VARIABLE_DECLARATION_TYPES.include?(declaration_node.type)
raise ArgumentError,
"Node type must be any of #{VARIABLE_DECLARATION_TYPES}, " \
"passed #{declaration_node.type}"
end
@name = name.to_sym
@declaration_node = declaration_node
@scope = scope
@assignments = []
@references = []
@captured_by_block = false
end
def assign(node)
assignment = Assignment.new(node, self)
@assignments.last&.reassigned! unless captured_by_block?
@assignments << assignment
end
def referenced?
!@references.empty?
end
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def reference!(node)
reference = Reference.new(node, @scope)
@references << reference
consumed_branches = nil
@assignments.reverse_each do |assignment|
next if consumed_branches&.include?(assignment.branch)
assignment.reference!(node) unless assignment.run_exclusively_with?(reference)
# Modifier if/unless conditions are special. Assignments made in
# them do not put the assigned variable in scope to the left of the
# if/unless keyword. A preceding assignment is needed to put the
# variable in scope. For this reason we skip to the next assignment
# here.
next if in_modifier_conditional?(assignment)
break if !assignment.branch || assignment.branch == reference.branch
unless assignment.branch.may_run_incompletely?
(consumed_branches ||= Set.new) << assignment.branch
end
end
end
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def in_modifier_conditional?(assignment)
parent = assignment.node.parent
parent = parent.parent if parent&.begin_type?
return false if parent.nil?
(parent.if_type? || parent.while_type? || parent.until_type?) && parent.modifier_form?
end
def capture_with_block!
@captured_by_block = true
end
# This is a convenient way to check whether the variable is used
# in its entire variable lifetime.
# For more precise usage check, refer Assignment#used?.
#
# Once the variable is captured by a block, we have no idea
# when, where, and how many times the block would be invoked.
# This means we cannot track the usage of the variable.
# So we consider it's used to suppress false positive offenses.
def used?
@captured_by_block || referenced?
end
def should_be_unused?
name.to_s.start_with?('_')
end
def argument?
ARGUMENT_DECLARATION_TYPES.include?(@declaration_node.type)
end
def method_argument?
argument? && %i[def defs].include?(@scope.node.type)
end
def block_argument?
argument? && @scope.node.block_type?
end
def keyword_argument?
%i[kwarg kwoptarg].include?(@declaration_node.type)
end
def explicit_block_local_variable?
@declaration_node.shadowarg_type?
end
end
end
end
end