# frozen_string_literal: truemoduleRuboCopmoduleCop# This force provides a way to track local variables and scopes of Ruby.# Cops interact with this force need to override some of the hook methods.## def before_entering_scope(scope, variable_table)# end## def after_entering_scope(scope, variable_table)# end## def before_leaving_scope(scope, variable_table)# end## def after_leaving_scope(scope, variable_table)# end## def before_declaring_variable(variable, variable_table)# end## def after_declaring_variable(variable, variable_table)# end## @api privateclassVariableForce<Force# rubocop:disable Metrics/ClassLengthVARIABLE_ASSIGNMENT_TYPE=:lvasgnREGEXP_NAMED_CAPTURE_TYPE=:match_with_lvasgnVARIABLE_ASSIGNMENT_TYPES=[VARIABLE_ASSIGNMENT_TYPE,REGEXP_NAMED_CAPTURE_TYPE].freezeARGUMENT_DECLARATION_TYPES=[:arg,:optarg,:restarg,:kwarg,:kwoptarg,:kwrestarg,:blockarg,# This doesn't mean block argument, it's block-pass (&block).:shadowarg# This means block local variable (obj.each { |arg; this| }).].freezeLOGICAL_OPERATOR_ASSIGNMENT_TYPES=%i[or_asgn and_asgn].freezeOPERATOR_ASSIGNMENT_TYPES=(LOGICAL_OPERATOR_ASSIGNMENT_TYPES+[:op_asgn]).freezeMULTIPLE_ASSIGNMENT_TYPE=:masgnVARIABLE_REFERENCE_TYPE=:lvarPOST_CONDITION_LOOP_TYPES=%i[while_post until_post].freezeLOOP_TYPES=(POST_CONDITION_LOOP_TYPES+%i[while until for]).freezeRESCUE_TYPE=:rescueZERO_ARITY_SUPER_TYPE=:zsuperTWISTED_SCOPE_TYPES=%i[block class sclass defs module].freezeSCOPE_TYPES=(TWISTED_SCOPE_TYPES+[:def]).freezeSEND_TYPE=:sendVariableReference=Struct.new(:name)dodefassignment?falseendendAssignmentReference=Struct.new(:node)dodefassignment?trueendenddefvariable_table@variable_table||=VariableTable.new(self)end# Starting point.definvestigate(processed_source)root_node=processed_source.astreturnunlessroot_nodevariable_table.push_scope(root_node)process_node(root_node)variable_table.pop_scopeenddefprocess_node(node)method_name=node_handler_method_name(node)retval=send(method_name,node)ifmethod_nameprocess_children(node)unlessretval==:skip_childrenendprivate# This is called for each scope recursively.definspect_variables_in_scope(scope_node)variable_table.push_scope(scope_node)process_children(scope_node)variable_table.pop_scopeenddefprocess_children(origin_node)origin_node.each_child_nodedo|child_node|nextifscanned_node?(child_node)process_node(child_node)endenddefskip_children!:skip_childrenend# rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexitydefnode_handler_method_name(node)casenode.typewhenVARIABLE_ASSIGNMENT_TYPE:process_variable_assignmentwhenREGEXP_NAMED_CAPTURE_TYPE:process_regexp_named_captureswhenMULTIPLE_ASSIGNMENT_TYPE:process_variable_multiple_assignmentwhenVARIABLE_REFERENCE_TYPE:process_variable_referencingwhenRESCUE_TYPE:process_rescuewhenZERO_ARITY_SUPER_TYPE:process_zero_arity_superwhenSEND_TYPE:process_sendwhen*ARGUMENT_DECLARATION_TYPES:process_variable_declarationwhen*OPERATOR_ASSIGNMENT_TYPES:process_variable_operator_assignmentwhen*LOOP_TYPES:process_loopwhen*SCOPE_TYPES:process_scopeendend# rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexitydefprocess_variable_declaration(node)variable_name=node.children.first# restarg and kwrestarg would have no name:## def initialize(*)# endreturnunlessvariable_namevariable_table.declare_variable(variable_name,node)enddefprocess_variable_assignment(node)name=node.children.firstvariable_table.declare_variable(name,node)unlessvariable_table.variable_exist?(name)# Need to scan rhs before assignment so that we can mark previous# assignments as referenced if rhs has referencing to the variable# itself like:## foo = 1# foo = foo + 1process_children(node)variable_table.assign_to_variable(name,node)skip_children!enddefprocess_regexp_named_captures(node)regexp_node,rhs_node=*nodevariable_names=regexp_captured_names(regexp_node)variable_names.eachdo|name|nextifvariable_table.variable_exist?(name)variable_table.declare_variable(name,node)endprocess_node(rhs_node)process_node(regexp_node)variable_names.each{|name|variable_table.assign_to_variable(name,node)}skip_children!enddefregexp_captured_names(node)regexp=node.to_regexpregexp.named_captures.keysenddefprocess_variable_operator_assignment(node)ifLOGICAL_OPERATOR_ASSIGNMENT_TYPES.include?(node.type)asgn_node,rhs_node=*nodeelseasgn_node,_operator,rhs_node=*nodeendreturnunlessasgn_node.lvasgn_type?name=asgn_node.children.firstvariable_table.declare_variable(name,asgn_node)unlessvariable_table.variable_exist?(name)# The following statements:## foo = 1# foo += foo = 2# # => 3## are equivalent to:## foo = 1# foo = foo + (foo = 2)# # => 3## So, at operator assignment node, we need to reference the variable# before processing rhs nodes.variable_table.reference_variable(name,node)process_node(rhs_node)variable_table.assign_to_variable(name,asgn_node)skip_children!enddefprocess_variable_multiple_assignment(node)lhs_node,rhs_node=*nodeprocess_node(rhs_node)process_node(lhs_node)skip_children!enddefprocess_variable_referencing(node)name=node.children.firstvariable_table.reference_variable(name,node)enddefprocess_loop(node)ifPOST_CONDITION_LOOP_TYPES.include?(node.type)# See the comment at the end of file for this behavior.condition_node,body_node=*nodeprocess_node(body_node)process_node(condition_node)elseprocess_children(node)endmark_assignments_as_referenced_in_loop(node)skip_children!enddefprocess_rescue(node)resbody_nodes=node.each_child_node(:resbody)contain_retry=resbody_nodes.any?do|resbody_node|resbody_node.each_descendant.any?(&:retry_type?)end# Treat begin..rescue..end with retry as a loop.process_loop(node)ifcontain_retryenddefprocess_zero_arity_super(node)variable_table.accessible_variables.eachdo|variable|nextunlessvariable.method_argument?variable.reference!(node)endenddefprocess_scope(node)ifTWISTED_SCOPE_TYPES.include?(node.type)# See the comment at the end of file for this behavior.twisted_nodes(node).eachdo|twisted_node|process_node(twisted_node)scanned_nodes<<twisted_nodeendendinspect_variables_in_scope(node)skip_children!enddeftwisted_nodes(node)twisted_nodes=[node.children[0]]twisted_nodes<<node.children[1]ifnode.class_type?twisted_nodes.compactenddefprocess_send(node)_receiver,method_name,args=*nodereturnunlessmethod_name==:bindingreturnifargs&&!args.children.empty?variable_table.accessible_variables.each{|variable|variable.reference!(node)}end# Mark all assignments which are referenced in the same loop# as referenced by ignoring AST order since they would be referenced# in next iteration.defmark_assignments_as_referenced_in_loop(node)referenced_variable_names_in_loop,assignment_nodes_in_loop=find_variables_in_loop(node)referenced_variable_names_in_loop.eachdo|name|variable=variable_table.find_variable(name)# Non related references which are caught in the above scan# would be skipped here.nextunlessvariablevariable.assignments.eachdo|assignment|nextifassignment_nodes_in_loop.none?do|assignment_node|assignment_node.equal?(assignment.node)endassignment.reference!(node)endendenddeffind_variables_in_loop(loop_node)referenced_variable_names_in_loop=[]assignment_nodes_in_loop=[]each_descendant_reference(loop_node)do|reference|ifreference.assignment?assignment_nodes_in_loop<<reference.nodeelsereferenced_variable_names_in_loop<<reference.nameendend[referenced_variable_names_in_loop,assignment_nodes_in_loop]enddefeach_descendant_reference(loop_node)# #each_descendant does not consider scope,# but we don't need to care about it here.loop_node.each_descendantdo|node|reference=descendant_reference(node)yieldreferenceifreferenceendenddefdescendant_reference(node)casenode.typewhen:lvarVariableReference.new(node.children.first)when:lvasgnAssignmentReference.new(node)when*OPERATOR_ASSIGNMENT_TYPESasgn_node=node.children.firstVariableReference.new(asgn_node.children.first)ifasgn_node.lvasgn_type?endend# Use Node#equal? for accurate check.defscanned_node?(node)scanned_nodes.any?{|scanned_node|scanned_node.equal?(node)}enddefscanned_nodes@scanned_nodes||=[]end# Hooks invoked by VariableTable.%i[
before_entering_scope
after_entering_scope
before_leaving_scope
after_leaving_scope
before_declaring_variable
after_declaring_variable
].eachdo|hook|define_method(hook)do|arg|# Invoke hook in cops.run_hook(hook,arg,variable_table)endend# Post condition loops## Loop body nodes need to be scanned first.## Ruby:# begin# foo = 1# end while foo > 10# puts foo## AST:# (begin# (while-post# (send# (lvar :foo) :># (int 10))# (kwbegin# (lvasgn :foo# (int 1))))# (send nil :puts# (lvar :foo)))# Twisted scope types## The variable foo belongs to the top level scope,# but in AST, it's under the block node.## Ruby:# some_method(foo = 1) do# end# puts foo## AST:# (begin# (block# (send nil :some_method# (lvasgn :foo# (int 1)))# (args) nil)# (send nil :puts# (lvar :foo)))## So the method argument nodes need to be processed# in current scope.## Same thing.## Ruby:# instance = Object.new# class << instance# foo = 1# end## AST:# (begin# (lvasgn :instance# (send# (const nil :Object) :new))# (sclass# (lvar :instance)# (begin# (lvasgn :foo# (int 1))endendend