class RubyLint::VirtualMachine
{RubyLint::VirtualMachine} is the heart of ruby-lint. It takes a AST
generated by {RubyLint::Parser}, iterates it and builds various definitions of methods, variables, etc.
The virtual machine is a stack based virtual machine. Whenever certain expressions are processed their values are stored in a stack which is then later used for creating definitions (where applicable). For example, when creating a new class a definition for the class is pushed on to a stack. All code defined in this class is then stored in the definition at the end of the stack.
After a certain AST
has been processed the VM will enter a read-only state to prevent code from modifying it (either on purpose or by accident).
## Stacks
The virtual machine uses two stacks:
-
`value_stack`
-
`variable_stack`
The value stack is used for storing raw values (e.g. integers) while the variable stack is used for storing variable definitions (which in turn store their values inside themselves).
## Definitions
Built definitions are stored in {RubyLint::VirtualMachine#definitions} as a single root definition called “root”. This definition in turn contains everything defined in a block of code that was processed by the VM.
## Associations
The VM also keeps track of various nodes and their corresponding definitions to make it easier to retrieve them later on. These are only nodes/definitions related to a new scope such as a class or method definition node.
These associations are stored as a Hash in {RubyLint::VirtualMachine#associations} with the keys set to the nodes and the values to the corresponding definitions.
## Options
The following extra options can be set in the constructor:
-
`:comments`: a Hash containing the comments for various
AST
nodes.
@!attribute [r] associations
@return [Hash]
@!attribute [r] comments
@return [Hash]
@!attribute [r] definitions
@return [RubyLint::Definition::RubyObject]
@!attribute [r] extra_definitions
@return [Array]
@!attribute [r] value_stack
@return [RubyLint::NestedStack]
@!attribute [r] variable_stack
@return [RubyLint::NestedStack]
@!attribute [r] docstring_tags
@return [RubyLint::Docstring::Mapping]
Constants
- ARGUMENT_TYPES
Array containing the various argument types of method definitions.
@return [Array]
- ASSIGNMENT_TYPES
Hash containing variable assignment types and the corresponding variable reference types.
@return [Hash]
- ASSIGN_GLOBAL
List of variable types that should be assigned in the global scope.
@return [Array]
- EXPORT_VARIABLES
The types of variables to export outside of a method definition.
@return [Array]
- PRIMITIVES
Collection of primitive value types.
@return [Array]
- SEND_MAPPING
Returns a Hash containing the method call evaluators to use for `(send)` nodes.
@return [Hash]
- VISIBILITIES
The available method visibilities.
@return [Array]
Attributes
Public Instance Methods
Processes calls to `alias`. Two types of data can be aliased:
-
Methods (using the syntax `alias ALIAS SOURCE`)
-
Global variables
This method dispatches the alias process to two possible methods:
-
on_alias_sym: aliasing methods (using symbols)
-
on_alias_gvar: aliasing global variables
# File lib/ruby-lint/virtual_machine.rb, line 750 def after_alias(node) arguments = value_stack.pop evaluator = MethodCall::Alias.new(node, self) evaluator.evaluate(arguments, current_scope) end
Processes an `and` assignment in the form of `variable &&= value`.
# File lib/ruby-lint/virtual_machine.rb, line 327 def after_and_asgn variable = variable_stack.pop.first value = value_stack.pop.first conditional_assignment(variable, value) end
Builds an Array.
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 387 def after_array(node) builder = DefinitionBuilder::RubyArray.new( node, self, :values => value_stack.pop ) push_value(builder.build) end
@see on_assign
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 231 def after_assign(node) type = ASSIGNMENT_TYPES[node.type] name = node.children[0].to_s value = value_stack.pop.first if !value and assignment_value value = assignment_value end assign_variable(type, name, value, node) end
Pops the scope created by the block.
# File lib/ruby-lint/virtual_machine.rb, line 521 def after_block pop_scope end
@see on_casgn
# File lib/ruby-lint/virtual_machine.rb, line 264 def after_casgn(node) values = value_stack.pop scope = current_scope if node.children[0] scope = ConstantPath.new(node.children[0]).resolve(current_scope) return unless scope end variable = Definition::RubyObject.new( :type => :const, :name => node.children[1].to_s, :value => values.first, :instance_type => :instance ) add_variable(variable, scope) end
Pops the scope created by the class.
# File lib/ruby-lint/virtual_machine.rb, line 500 def after_class pop_scope end
Exports various variables to the outer scope of the method definition.
# File lib/ruby-lint/virtual_machine.rb, line 594 def after_def previous = pop_scope current = current_scope reset_docstring_tags EXPORT_VARIABLES.each do |type| current.copy(previous, type) end end
Builds a Hash.
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 409 def after_hash(node) builder = DefinitionBuilder::RubyHash.new( node, self, :values => value_stack.pop ) push_value(builder.build) end
Called after a new instance of the virtual machine has been created.
# File lib/ruby-lint/virtual_machine.rb, line 165 def after_initialize @comments ||= {} @associations = {} @definitions = initial_definitions @constant_loader = ConstantLoader.new(:definitions => @definitions) @scopes = [@definitions] @value_stack = NestedStack.new @variable_stack = NestedStack.new @ignored_nodes = [] @visibility = :public reset_docstring_tags reset_method_type @constant_loader.bootstrap end
Processes a mass variable assignment using the stacks created by {#on_masgn}.
# File lib/ruby-lint/virtual_machine.rb, line 292 def after_masgn variables = variable_stack.pop values = value_stack.pop.first values = values && values.value ? values.value : [] variables.each_with_index do |variable, index| variable.value = values[index].value if values[index] current_scope.add(variable.type, variable.name, variable) end end
Pops the scope created by the module.
# File lib/ruby-lint/virtual_machine.rb, line 472 def after_module pop_scope end
Processes an `or` assignment in the form of `variable ||= value`.
# File lib/ruby-lint/virtual_machine.rb, line 311 def after_or_asgn variable = variable_stack.pop.first value = value_stack.pop.first if variable and value conditional_assignment(variable, value, false) end end
@see on_pair
# File lib/ruby-lint/virtual_machine.rb, line 429 def after_pair key, value = value_stack.pop return unless key member = Definition::RubyObject.new( :name => key.value.to_s, :type => :member, :value => value ) push_value(member) end
Pops the scope created by the `sclass` block and resets the method definition/send type.
# File lib/ruby-lint/virtual_machine.rb, line 548 def after_sclass reset_method_type pop_scope end
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 653 def after_send(node) receiver, name, _ = *node receiver = unpack_block(receiver) name = name.to_s args_length = node.children[2..-1].length values = value_stack.pop arguments = values.pop(args_length) block = nil receiver_definition = values.first if arguments.length != args_length raise <<-EOF Not enough argument definitions for #{node.inspect_oneline}. Location: #{node.file} on line #{node.line}, column #{node.column} Expected: #{args_length} Received: #{arguments.length} EOF end # Associate the argument definitions with their nodes. arguments.each_with_index do |obj, index| arg_node = unpack_block(node.children[2 + index]) associate_node(arg_node, obj) end # If the receiver doesn't exist there's no point in associating a context # with it. if receiver and !receiver_definition push_unknown_value return end if receiver and receiver_definition context = receiver_definition else context = current_scope # `parser` wraps (block) nodes around (send) calls which is a bit # inconvenient if context.block? block = context context = previous_scope end end if SEND_MAPPING[name] evaluator = SEND_MAPPING[name].new(node, self) evaluator.evaluate(arguments, context, block) end # Associate the receiver node with the context so that it becomes # easier to retrieve later on. if receiver and context associate_node(receiver, context) end if context and context.method_defined?(name) retval = context.call_method(name) retval ? push_value(retval) : push_unknown_value # Track the method call track_method_call(context, name, node) else push_unknown_value end end
@return [RubyLint::Definition::RubyObject]
# File lib/ruby-lint/virtual_machine.rb, line 760 def current_scope return @scopes.last end
Evaluates and returns the value of the given node.
@param [RubyLint::AST::Node] node @return [RubyLint::Definition::RubyObject]
# File lib/ruby-lint/virtual_machine.rb, line 795 def evaluate_node(node) value_stack.add_stack iterate(node) return value_stack.pop.first end
Freezes the VM along with all the instance variables.
# File lib/ruby-lint/virtual_machine.rb, line 203 def freeze @associations.freeze @definitions.freeze @scopes.freeze super end
@param [String] name @return [RubyLint::Definition::RubyObject]
# File lib/ruby-lint/virtual_machine.rb, line 776 def global_constant(name) found = definitions.lookup(:const, name) # Didn't find it? Lets see if the constant loader knows about it. unless found @constant_loader.load_constant(name) found = definitions.lookup(:const, name) end return found end
Adds a new value stack for the values of an alias.
# File lib/ruby-lint/virtual_machine.rb, line 735 def on_alias value_stack.add_stack end
# File lib/ruby-lint/virtual_machine.rb, line 320 def on_and_asgn add_stacks end
Adds a new stack for Array values.
# File lib/ruby-lint/virtual_machine.rb, line 378 def on_array value_stack.add_stack end
Processes a regular variable assignment.
# File lib/ruby-lint/virtual_machine.rb, line 221 def on_assign reset_assignment_value value_stack.add_stack end
Builds the definition for a block.
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 509 def on_block(node) builder = DefinitionBuilder::RubyBlock.new(node, self) definition = builder.build associate_node(node, definition) push_scope(definition) end
Processes the assignment of a constant.
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 253 def on_casgn(node) # Don't push values for the receiver constant. @ignored_nodes << node.children[0] if node.children[0] reset_assignment_value value_stack.add_stack end
Creates the definition for a class.
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 481 def on_class(node) parent = nil parent_node = node.children[1] if parent_node parent = evaluate_node(parent_node) if !parent or !parent.const? # FIXME: this should use `definitions` instead. parent = current_scope.lookup(:const, 'Object') end end define_module(node, DefinitionBuilder::RubyClass, :parent => parent) end
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 366 def on_const(node) increment_reference_amount(node) push_variable_value(node) # The root node is also used in such a way that it processes child (= # receiver) constants. skip_child_nodes!(node) end
Creates the definition for a method definition.
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 558 def on_def(node) receiver = nil if node.type == :defs receiver = evaluate_node(node.children[0]) end builder = DefinitionBuilder::RubyMethod.new( node, self, :type => @method_type, :receiver => receiver, :visibility => @visibility ) definition = builder.build builder.scope.add_definition(definition) associate_node(node, definition) buffer_docstring_tags(node) if docstring_tags and docstring_tags.return_tag assign_return_value_from_tag( docstring_tags.return_tag, definition ) end push_scope(definition) end
Adds a new stack for Hash values.
# File lib/ruby-lint/virtual_machine.rb, line 400 def on_hash value_stack.add_stack end
# File lib/ruby-lint/virtual_machine.rb, line 284 def on_masgn add_stacks end
Creates the definition for a module.
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 465 def on_module(node) define_module(node, DefinitionBuilder::RubyModule) end
Called whenever a magic regexp global variable is referenced (e.g. `$1`).
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 355 def on_nth_ref(node) var = definitions.lookup(:gvar, "$#{node.children[0]}") # If the number is not found, then add it as there is no limit for them var = definitions.define_global_variable(node.children[0]) if !var && node.children[0].is_a?(Fixnum) push_value(var.value) end
# File lib/ruby-lint/virtual_machine.rb, line 304 def on_or_asgn add_stacks end
Adds a new value stack for key/value pairs.
# File lib/ruby-lint/virtual_machine.rb, line 422 def on_pair value_stack.add_stack end
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 214 def on_root(node) associate_node(node, current_scope) end
Processes an sclass block. Sclass blocks look like the following:
class << self end
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 534 def on_sclass(node) parent = node.children[0] definition = evaluate_node(parent) @method_type = definition.method_call_type associate_node(node, definition) push_scope(definition) end
Pushes the value of `self` onto the current stack.
# File lib/ruby-lint/virtual_machine.rb, line 446 def on_self scope = current_scope method = scope.lookup(scope.method_call_type, 'self') push_value(method.return_value) end
Processes a method call. If a certain method call has its own dedicated callback that one will be called as well.
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 640 def on_send(node) name = node.children[1].to_s name = SEND_MAPPING.fetch(name, name) callback = "on_send_#{name}" value_stack.add_stack execute_callback(callback, node) end
Pushes the return value of the block yielded to, that is, an unknown one.
# File lib/ruby-lint/virtual_machine.rb, line 456 def on_yield push_unknown_value end
@return [RubyLint::Definition::RubyObject]
# File lib/ruby-lint/virtual_machine.rb, line 768 def previous_scope return @scopes[-2] end
Processes the given AST
or a collection of AST
nodes.
@see iterate
@param [Array|RubyLint::AST::Node] ast
# File lib/ruby-lint/virtual_machine.rb, line 189 def run(ast) ast = [ast] unless ast.is_a?(Array) # pre-load all the built-in definitions. @constant_loader.run(ast) ast.each { |node| iterate(node) } freeze end
Private Instance Methods
Adds a new variable and value stack.
# File lib/ruby-lint/virtual_machine.rb, line 927 def add_stacks variable_stack.add_stack value_stack.add_stack end
Adds a variable to the current scope of, if a the variable stack is not empty, add it to the stack instead.
@param [RubyLint::Definition::RubyObject] variable @param [RubyLint::Definition::RubyObject] scope
# File lib/ruby-lint/virtual_machine.rb, line 992 def add_variable(variable, scope = current_scope) if variable_stack.empty? scope.add(variable.type, variable.name, variable) else variable_stack.push(variable) end end
@param [RubyLint::Docstring::ReturnTag] tag @param [RubyLint::Definition::RubyMethod] definition
# File lib/ruby-lint/virtual_machine.rb, line 1195 def assign_return_value_from_tag(tag, definition) definitions = definitions_for_types(tag.types) # THINK: currently ruby-lint assumes methods always return a single type # but YARD allows you to specify multiple ones. For now we'll take the # first one but there should be a nicer way to do this. definition.returns(definitions[0].instance) if definitions[0] end
Assigns a basic variable.
@param [Symbol] type The type of variable. @param [String] name The name of the variable @param [RubyLint::Definition::RubyObject] value @param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 940 def assign_variable(type, name, value, node) scope = assignment_scope(type) variable = scope.lookup(type, name) # If there's already a variable we'll just update it. if variable variable.reference_amount += 1 # `value` is not for conditional assignments as those are handled # manually. variable.value = value if value else variable = Definition::RubyObject.new( :type => type, :name => name, :value => value, :instance_type => :instance, :reference_amount => 0, :line => node.line, :column => node.column, :file => node.file ) end buffer_assignment_value(value) # Primarily used by #after_send to support variable assignments as method # call arguments. if value and !value_stack.empty? value_stack.push(variable.value) end add_variable(variable, scope) end
Determines the scope to use for a variable assignment.
@param [Symbol] type @return [RubyLint::Definition::RubyObject]
# File lib/ruby-lint/virtual_machine.rb, line 981 def assignment_scope(type) return ASSIGN_GLOBAL.include?(type) ? definitions : current_scope end
Returns the value of the last assignment.
# File lib/ruby-lint/virtual_machine.rb, line 1022 def assignment_value return @assignment_value end
Associates the given node and defintion with each other.
@param [RubyLint::AST::Node] node @param [RubyLint::Definition::RubyObject] definition
# File lib/ruby-lint/virtual_machine.rb, line 861 def associate_node(node, definition) @associations[node] = definition end
Stores the value as the last assigned value.
@param [RubyLint::Definition::RubyObject] value
# File lib/ruby-lint/virtual_machine.rb, line 1031 def buffer_assignment_value(value) @assignment_value = value end
Performs a conditional assignment.
@param [RubyLint::Definition::RubyObject] variable @param [RubyLint::Definition::RubyValue] value @param [TrueClass|FalseClass] bool When set to `true` existing variables
will be overwritten.
# File lib/ruby-lint/virtual_machine.rb, line 1050 def conditional_assignment(variable, value, bool = true) variable.reference_amount += 1 if current_scope.has_definition?(variable.type, variable.name) == bool variable.value = value current_scope.add_definition(variable) buffer_assignment_value(variable.value) end end
Creates a primitive value such as an integer.
@param [RubyLint::AST::Node] node @param [Hash] options
# File lib/ruby-lint/virtual_machine.rb, line 1006 def create_primitive(node, options = {}) builder = DefinitionBuilder::Primitive.new(node, self, options) return builder.build end
Creates an “unknown” definition with the given method in it.
@param [String] name The name of the method to add. @return [RubyLint::Definition::RubyObject]
# File lib/ruby-lint/virtual_machine.rb, line 1148 def create_unknown_with_method(name) definition = Definition::RubyObject.create_unknown definition.send("define_#{@method_type}", name) return definition end
Defines a new module/class based on the supplied node.
@param [RubyLint::Node] node @param [RubyLint::DefinitionBuilder::Base] definition_builder @param [Hash] options
# File lib/ruby-lint/virtual_machine.rb, line 834 def define_module(node, definition_builder, options = {}) builder = definition_builder.new(node, self, options) definition = builder.build scope = builder.scope existing = scope.lookup(definition.type, definition.name, false) if existing definition = existing inherit_definition(definition, current_scope) else definition.add_definition(definition) scope.add_definition(definition) end associate_node(node, definition) push_scope(definition) end
Returns the definition for the given node.
@param [RubyLint::AST::Node] node @return [RubyLint::Definition::RubyObject]
# File lib/ruby-lint/virtual_machine.rb, line 1068 def definition_for_node(node) if node.const? and node.children[0] definition = ConstantPath.new(node).resolve(current_scope) else definition = current_scope.lookup(node.type, node.name) end definition = Definition::RubyObject.create_unknown unless definition return definition end
Returns a collection of definitions for a set of YARD types.
@param [Array] types @return [Array]
# File lib/ruby-lint/virtual_machine.rb, line 1162 def definitions_for_types(types) definitions = [] # There are basically two type signatures: either the name(s) of a # constant or a method in the form of `#method_name`. types.each do |type| if type[0] == '#' found = create_unknown_with_method(type[1..-1]) else found = lookup_type_definition(type) end definitions << found if found end return definitions end
Increments the reference amount of a node's definition unless the definition is frozen.
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 1086 def increment_reference_amount(node) definition = definition_for_node(node) if definition and !definition.frozen? definition.reference_amount += 1 end end
Includes the definition `inherit` in the list of parent definitions of `definition`.
@param [RubyLint::Definition::RubyObject] definition @param [RubyLint::Definition::RubyObject] inherit
# File lib/ruby-lint/virtual_machine.rb, line 1101 def inherit_definition(definition, inherit) unless definition.parents.include?(inherit) definition.parents << inherit end end
Returns the initial set of definitions to use.
@return [RubyLint::Definition::RubyObject]
# File lib/ruby-lint/virtual_machine.rb, line 810 def initial_definitions definitions = Definition::RubyObject.new( :name => 'root', :type => :root, :instance_type => :instance, :inherit_self => false ) definitions.parents = [ definitions.constant_proxy('Object', RubyLint.registry) ] definitions.define_self return definitions end
Tries to look up the given type/constant in the current scope and falls back to the global scope if it couldn't be found in the former.
@param [String] name @return [RubyLint::Definition::RubyObject]
# File lib/ruby-lint/virtual_machine.rb, line 1187 def lookup_type_definition(name) return current_scope.lookup(:const, name) || global_constant(name) end
Removes a scope from the list.
# File lib/ruby-lint/virtual_machine.rb, line 885 def pop_scope raise 'Trying to pop an empty scope' if @scopes.empty? @scopes.pop end
Pushes a new scope on the list of available scopes.
@param [RubyLint::Definition::RubyObject] definition
# File lib/ruby-lint/virtual_machine.rb, line 870 def push_scope(definition) unless definition.is_a?(RubyLint::Definition::RubyObject) raise( ArgumentError, "Expected a RubyLint::Definition::RubyObject but got " \ "#{definition.class} instead" ) end @scopes << definition end
Pushes an unknown value object onto the value stack.
# File lib/ruby-lint/virtual_machine.rb, line 920 def push_unknown_value push_value(Definition::RubyObject.create_unknown) end
Pushes a definition (of a value) onto the value stack.
@param [RubyLint::Definition::RubyObject] definition
# File lib/ruby-lint/virtual_machine.rb, line 913 def push_value(definition) value_stack.push(definition) if definition && !value_stack.empty? end
Pushes the value of a variable onto the value stack.
@param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 896 def push_variable_value(node) return if value_stack.empty? || @ignored_nodes.include?(node) definition = definition_for_node(node) if definition value = definition.value ? definition.value : definition push_value(value) end end
Resets the variable used for storing the last assignment value.
# File lib/ruby-lint/virtual_machine.rb, line 1015 def reset_assignment_value @assignment_value = nil end
Resets the method assignment/call type.
# File lib/ruby-lint/virtual_machine.rb, line 1038 def reset_method_type @method_type = :instance_method end
Tracks a method call.
@param [RubyLint::Definition::RubyMethod] definition @param [String] name @param [RubyLint::AST::Node] node
# File lib/ruby-lint/virtual_machine.rb, line 1211 def track_method_call(definition, name, node) method = definition.lookup(definition.method_call_type, name) current = current_scope location = { :line => node.line, :column => node.column, :file => node.file } # Add the call to the current scope if we're dealing with a writable # method definition. if current.respond_to?(:calls) and !current.frozen? current.calls.push( MethodCallInfo.new(location.merge(:definition => method)) ) end # Add the caller to the called method, this allows for inverse lookups. unless method.frozen? method.callers.push( MethodCallInfo.new(location.merge(:definition => definition)) ) end end
Updates the parents of a definition according to the types of a `@param` tag.
@param [RubyLint::Docstring::ParamTag] tag @param [RubyLint::Definition::RubyObject] definition
# File lib/ruby-lint/virtual_machine.rb, line 1136 def update_parents_from_tag(tag, definition) extra_parents = definitions_for_types(tag.types) definition.parents.concat(extra_parents) end