# encoding: utf-8moduleRubocopmoduleCop# This module provides a way to track local variables and scopes of Ruby.# This is intended to be used as mix-in, and the user class may override# some of the hook methods.moduleVariableInspectorVARIABLE_ASSIGNMENT_TYPE=:lvasgnREGEXP_NAMED_CAPTURE_TYPE=:match_with_lvasgnVARIABLE_ASSIGNMENT_TYPES=[VARIABLE_ASSIGNMENT_TYPE,REGEXP_NAMED_CAPTURE_TYPE].freezeMETHOD_ARGUMENT_DECLARATION_TYPES=[:arg,:optarg,:restarg,:kwarg,:kwoptarg,:kwrestarg].freezeBLOCK_ARGUMENT_DECLARATION_TYPE=:blockargARGUMENT_DECLARATION_TYPES=(METHOD_ARGUMENT_DECLARATION_TYPES+[BLOCK_ARGUMENT_DECLARATION_TYPE]).freezeBLOCK_LOCAL_VARIABLE_DECLARATION_TYPE=:shadowargDECLARATION_TYPES=(ARGUMENT_DECLARATION_TYPES+[BLOCK_LOCAL_VARIABLE_DECLARATION_TYPE]).freezeLOGICAL_OPERATOR_ASSIGNMENT_TYPES=[:or_asgn,:and_asgn].freezeOPERATOR_ASSIGNMENT_TYPES=(LOGICAL_OPERATOR_ASSIGNMENT_TYPES+[:op_asgn]).freezeMULTIPLE_ASSIGNMENT_TYPE=:masgnVARIABLE_REFERENCE_TYPE=:lvarPOST_CONDITION_LOOP_TYPES=[:while_post,:until_post].freezeLOOP_TYPES=(POST_CONDITION_LOOP_TYPES+[:while,:until,:for]).freezeRESCUE_TYPE=:rescueZERO_ARITY_SUPER_TYPE=:zsuperTWISTED_SCOPE_TYPES=[:block,:class,:sclass,:defs].freezeSCOPE_TYPES=(TWISTED_SCOPE_TYPES+[:top_level,:module,:def]).freezedefvariable_table@variable_table||=VariableTable.new(self)end# Starting point.definspect_variables(root_node)returnunlessroot_node# Wrap the root node with :top_level scope node.top_level_node=wrap_with_top_level_node(root_node)inspect_variables_in_scope(top_level_node)enddefwrap_with_top_level_node(node)# This is a custom node type, not defined in Parser.Parser::AST::Node.new(:top_level,[node])endmodule_function:wrap_with_top_level_node# 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.children.eachdo|child|nextunlesschild.is_a?(Parser::AST::Node)nextifscanned_node?(child)process_node(child)endenddefprocess_node(node)catch(:skip_children)dodispatch_node(node)process_children(node)endenddefskip_children!throw:skip_childrenend# rubocop:disable MethodLength, CyclomaticComplexitydefdispatch_node(node)casenode.typewhen*DECLARATION_TYPESprocess_variable_declaration(node)whenVARIABLE_ASSIGNMENT_TYPEprocess_variable_assignment(node)whenREGEXP_NAMED_CAPTURE_TYPEprocess_regexp_named_captures(node)when*OPERATOR_ASSIGNMENT_TYPESprocess_variable_operator_assignment(node)whenMULTIPLE_ASSIGNMENT_TYPEprocess_variable_multiple_assignment(node)whenVARIABLE_REFERENCE_TYPEprocess_variable_referencing(node)when*LOOP_TYPESprocess_loop(node)whenRESCUE_TYPEprocess_rescue(node)whenZERO_ARITY_SUPER_TYPEprocess_zero_arity_super(node)when*SCOPE_TYPESprocess_scope(node)endend# rubocop:enable MethodLength, CyclomaticComplexitydefprocess_variable_declaration(node)# restarg would have no name:## def initialize(*)# endreturnifnode.type==:restarg&&node.children.empty?variable_name=node.children.firstvariable_table.declare_variable(variable_name,node)enddefprocess_variable_assignment(node)name=node.children.firstunlessvariable_table.variable_exist?(name)variable_table.declare_variable(name,node)end# 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=*noderegexp_string=regexp_node.children[0].children[0]regexp=Regexp.new(regexp_string)variable_names=regexp.named_captures.keysvariable_names.eachdo|name|unlessvariable_table.variable_exist?(name)variable_table.declare_variable(name,node)endendprocess_node(rhs_node)process_node(regexp_node)variable_names.eachdo|name|variable_table.assign_to_variable(name,node)endskip_children!enddefprocess_variable_operator_assignment(node)ifLOGICAL_OPERATOR_ASSIGNMENT_TYPES.include?(node.type)asgn_node,rhs_node=*nodeelseasgn_node,_operator,rhs_node=*nodeendreturnunlessasgn_node.type==:lvasgnname=asgn_node.children.firstunlessvariable_table.variable_exist?(name)variable_table.declare_variable(name,asgn_node)end# 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.children.selectdo|child|nextfalseunlesschild.is_a?(Parser::AST::Node)child.type==:resbodyendcontain_retry=resbody_nodes.any?do|resbody_node|scan(resbody_node)do|node_in_resbody|breaktrueifnode_in_resbody.type==:retryendend# 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.children[0]]twisted_nodes<<node.children[1]ifnode.type==:classtwisted_nodes.compact!twisted_nodes.eachdo|twisted_node|process_node(twisted_node)scanned_nodes<<twisted_nodeendendinspect_variables_in_scope(node)skip_children!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 catched 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!endendenddeffind_variables_in_loop(loop_node)referenced_variable_names_in_loop=[]assignment_nodes_in_loop=[]# #scan does not consider scope,# but we don't need to care about it here.scan(loop_node)do|node|casenode.typewhen:lvarreferenced_variable_names_in_loop<<node.children.firstwhen*OPERATOR_ASSIGNMENT_TYPESasgn_node=node.children.firstifasgn_node.type==:lvasgnreferenced_variable_names_in_loop<<asgn_node.children.firstendwhen:lvasgnassignment_nodes_in_loop<<nodeendend[referenced_variable_names_in_loop,assignment_nodes_in_loop]end# Simple recursive scandefscan(node,&block)node.children.eachdo|child|nextunlesschild.is_a?(Parser::AST::Node)yieldchildscan(child,&block)endnilend# Use Node#equal? for accurate check.defscanned_node?(node)scanned_nodes.any?do|scanned_node|scanned_node.equal?(node)endenddefscanned_nodes@scanned_nodes||=[]end# Hooksdefbefore_entering_scope(scope)enddefafter_entering_scope(scope)enddefbefore_leaving_scope(scope)enddefafter_leaving_scope(scope)enddefbefore_declaring_variable(variable_variable)enddefafter_declaring_variable(variable_variable)end# 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 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