YorickPeterse/ruby-lint

View on GitHub
lib/ruby-lint/virtual_machine.rb

Summary

Maintainability
F
3 days
Test Coverage
module RubyLint
  ##
  # {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]
  #
  class VirtualMachine < Iterator
    include MethodEvaluation

    attr_reader :associations,
      :comments,
      :definitions,
      :docstring_tags,
      :value_stack,
      :variable_stack

    private :value_stack, :variable_stack, :docstring_tags

    ##
    # Hash containing variable assignment types and the corresponding variable
    # reference types.
    #
    # @return [Hash]
    #
    ASSIGNMENT_TYPES = {
      :lvasgn => :lvar,
      :ivasgn => :ivar,
      :cvasgn => :cvar,
      :gvasgn => :gvar
    }

    ##
    # Collection of primitive value types.
    #
    # @return [Array]
    #
    PRIMITIVES = [
      :int,
      :float,
      :str,
      :dstr,
      :sym,
      :regexp,
      :true,
      :false,
      :nil,
      :erange,
      :irange
    ]

    ##
    # Returns a Hash containing the method call evaluators to use for `(send)`
    # nodes.
    #
    # @return [Hash]
    #
    SEND_MAPPING = {
      '[]='           => MethodCall::AssignMember,
      'include'       => MethodCall::Include,
      'extend'        => MethodCall::Include,
      'alias_method'  => MethodCall::Alias,
      'attr'          => MethodCall::Attribute,
      'attr_reader'   => MethodCall::Attribute,
      'attr_writer'   => MethodCall::Attribute,
      'attr_accessor' => MethodCall::Attribute,
      'define_method' => MethodCall::DefineMethod
    }

    ##
    # Array containing the various argument types of method definitions.
    #
    # @return [Array]
    #
    ARGUMENT_TYPES = [:arg, :optarg, :restarg, :blockarg, :kwoptarg]

    ##
    # The types of variables to export outside of a method definition.
    #
    # @return [Array]
    #
    EXPORT_VARIABLES = [:ivar, :cvar, :const]

    ##
    # List of variable types that should be assigned in the global scope.
    #
    # @return [Array]
    #
    ASSIGN_GLOBAL = [:gvar]

    ##
    # The available method visibilities.
    #
    # @return [Array]
    #
    VISIBILITIES = [:public, :protected, :private].freeze

    ##
    # Called after a new instance of the virtual machine has been created.
    #
    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 the given AST or a collection of AST nodes.
    #
    # @see #iterate
    # @param [Array|RubyLint::AST::Node] ast
    #
    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

    ##
    # Freezes the VM along with all the instance variables.
    #
    def freeze
      @associations.freeze
      @definitions.freeze
      @scopes.freeze

      super
    end

    ##
    # @param [RubyLint::AST::Node] node
    #
    def on_root(node)
      associate_node(node, current_scope)
    end

    ##
    # Processes a regular variable assignment.
    #
    def on_assign
      reset_assignment_value
      value_stack.add_stack
    end

    ##
    # @see #on_assign
    #
    # @param [RubyLint::AST::Node] node
    #
    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

    ASSIGNMENT_TYPES.each do |callback, _|
      alias_method :"on_#{callback}", :on_assign
      alias_method :"after_#{callback}", :after_assign
    end

    ##
    # Processes the assignment of a constant.
    #
    # @param [RubyLint::AST::Node] node
    #
    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

    ##
    # @see #on_casgn
    #
    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

    def on_masgn
      add_stacks
    end

    ##
    # Processes a mass variable assignment using the stacks created by
    # {#on_masgn}.
    #
    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

    def on_or_asgn
      add_stacks
    end

    ##
    # Processes an `or` assignment in the form of `variable ||= value`.
    #
    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

    def on_and_asgn
      add_stacks
    end

    ##
    # Processes an `and` assignment in the form of `variable &&= value`.
    #
    def after_and_asgn
      variable = variable_stack.pop.first
      value    = value_stack.pop.first

      conditional_assignment(variable, value)
    end

    # Creates the callback methods for various primitives such as integers.
    PRIMITIVES.each do |type|
      define_method("on_#{type}") do |node|
        push_value(create_primitive(node))
      end
    end

    # Creates the callback methods for various variable types such as local
    # variables.
    ASSIGNMENT_TYPES.each do |_, type|
      define_method("on_#{type}") do |node|
        increment_reference_amount(node)
        push_variable_value(node)
      end
    end

    ##
    # Called whenever a magic regexp global variable is referenced (e.g. `$1`).
    #
    # @param [RubyLint::AST::Node] node
    #
    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

    ##
    # @param [RubyLint::AST::Node] node
    #
    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

    ##
    # Adds a new stack for Array values.
    #
    def on_array
      value_stack.add_stack
    end

    ##
    # Builds an Array.
    #
    # @param [RubyLint::AST::Node] node
    #
    def after_array(node)
      builder = DefinitionBuilder::RubyArray.new(
        node,
        self,
        :values => value_stack.pop
      )

      push_value(builder.build)
    end

    ##
    # Adds a new stack for Hash values.
    #
    def on_hash
      value_stack.add_stack
    end

    ##
    # Builds a Hash.
    #
    # @param [RubyLint::AST::Node] node
    #
    def after_hash(node)
      builder = DefinitionBuilder::RubyHash.new(
        node,
        self,
        :values => value_stack.pop
      )

      push_value(builder.build)
    end

    ##
    # Adds a new value stack for key/value pairs.
    #
    def on_pair
      value_stack.add_stack
    end

    ##
    # @see #on_pair
    #
    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

    ##
    # Pushes the value of `self` onto the current stack.
    #
    def on_self
      scope  = current_scope
      method = scope.lookup(scope.method_call_type, 'self')

      push_value(method.return_value)
    end

    ##
    # Pushes the return value of the block yielded to, that is, an unknown one.
    #
    def on_yield
      push_unknown_value
    end

    ##
    # Creates the definition for a module.
    #
    # @param [RubyLint::AST::Node] node
    #
    def on_module(node)
      define_module(node, DefinitionBuilder::RubyModule)
    end

    ##
    # Pops the scope created by the module.
    #
    def after_module
      pop_scope
    end

    ##
    # Creates the definition for a class.
    #
    # @param [RubyLint::AST::Node] node
    #
    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

    ##
    # Pops the scope created by the class.
    #
    def after_class
      pop_scope
    end

    ##
    # Builds the definition for a block.
    #
    # @param [RubyLint::AST::Node] node
    #
    def on_block(node)
      builder    = DefinitionBuilder::RubyBlock.new(node, self)
      definition = builder.build

      associate_node(node, definition)

      push_scope(definition)
    end

    ##
    # Pops the scope created by the block.
    #
    def after_block
      pop_scope
    end

    ##
    # Processes an sclass block. Sclass blocks look like the following:
    #
    #     class << self
    #
    #     end
    #
    # @param [RubyLint::AST::Node] node
    #
    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

    ##
    # Pops the scope created by the `sclass` block and resets the method
    # definition/send type.
    #
    def after_sclass
      reset_method_type
      pop_scope
    end

    ##
    # Creates the definition for a method definition.
    #
    # @param [RubyLint::AST::Node] node
    #
    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

    ##
    # Exports various variables to the outer scope of the method definition.
    #
    def after_def
      previous = pop_scope
      current  = current_scope

      reset_docstring_tags

      EXPORT_VARIABLES.each do |type|
        current.copy(previous, type)
      end
    end

    # Creates callbacks for various argument types such as :arg and :optarg.
    ARGUMENT_TYPES.each do |type|
      define_method("on_#{type}") do
        value_stack.add_stack
      end

      define_method("after_#{type}") do |node|
        value = value_stack.pop.first
        name  = node.children[0].to_s
        var   = Definition::RubyObject.new(
          :type          => :lvar,
          :name          => name,
          :value         => value,
          :instance_type => :instance
        )

        if docstring_tags and docstring_tags.param_tags[name]
          update_parents_from_tag(docstring_tags.param_tags[name], var)
        end

        associate_node(node, var)
        current_scope.add(type, name, var)
        current_scope.add_definition(var)
      end
    end

    alias_method :on_defs, :on_def
    alias_method :after_defs, :after_def

    ##
    # 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
    #
    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

    ##
    # @param [RubyLint::AST::Node] node
    #
    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

    VISIBILITIES.each do |vis|
      define_method("on_send_#{vis}") do
        @visibility = vis
      end
    end

    ##
    # Adds a new value stack for the values of an alias.
    #
    def on_alias
      value_stack.add_stack
    end

    ##
    # Processes calls to `alias`. Two types of data can be aliased:
    #
    # 1. Methods (using the syntax `alias ALIAS SOURCE`)
    # 2. 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
    #
    def after_alias(node)
      arguments = value_stack.pop
      evaluator = MethodCall::Alias.new(node, self)

      evaluator.evaluate(arguments, current_scope)
    end

    ##
    # @return [RubyLint::Definition::RubyObject]
    #
    def current_scope
      return @scopes.last
    end

    ##
    #
    # @return [RubyLint::Definition::RubyObject]
    #
    def previous_scope
      return @scopes[-2]
    end

    ##
    # @param [String] name
    # @return [RubyLint::Definition::RubyObject]
    #
    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

    ##
    # Evaluates and returns the value of the given node.
    #
    # @param [RubyLint::AST::Node] node
    # @return [RubyLint::Definition::RubyObject]
    #
    def evaluate_node(node)
      value_stack.add_stack

      iterate(node)

      return value_stack.pop.first
    end

    private

    ##
    # Returns the initial set of definitions to use.
    #
    # @return [RubyLint::Definition::RubyObject]
    #
    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

    ##
    # Defines a new module/class based on the supplied node.
    #
    # @param [RubyLint::Node] node
    # @param [RubyLint::DefinitionBuilder::Base] definition_builder
    # @param [Hash] options
    #
    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

    ##
    # Associates the given node and defintion with each other.
    #
    # @param [RubyLint::AST::Node] node
    # @param [RubyLint::Definition::RubyObject] definition
    #
    def associate_node(node, definition)
      @associations[node] = definition
    end

    ##
    # Pushes a new scope on the list of available scopes.
    #
    # @param [RubyLint::Definition::RubyObject] definition
    #
    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

    ##
    # Removes a scope from the list.
    #
    def pop_scope
      raise 'Trying to pop an empty scope' if @scopes.empty?

      @scopes.pop
    end

    ##
    # Pushes the value of a variable onto the value stack.
    #
    # @param [RubyLint::AST::Node] node
    #
    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

    ##
    # Pushes a definition (of a value) onto the value stack.
    #
    # @param [RubyLint::Definition::RubyObject] definition
    #
    def push_value(definition)
      value_stack.push(definition) if definition && !value_stack.empty?
    end

    ##
    # Pushes an unknown value object onto the value stack.
    #
    def push_unknown_value
      push_value(Definition::RubyObject.create_unknown)
    end

    ##
    # Adds a new variable and value stack.
    #
    def add_stacks
      variable_stack.add_stack
      value_stack.add_stack
    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
    #
    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]
    #
    def assignment_scope(type)
      return ASSIGN_GLOBAL.include?(type) ? definitions : current_scope
    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
    #
    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

    ##
    # Creates a primitive value such as an integer.
    #
    # @param [RubyLint::AST::Node] node
    # @param [Hash] options
    #
    def create_primitive(node, options = {})
      builder = DefinitionBuilder::Primitive.new(node, self, options)

      return builder.build
    end

    ##
    # Resets the variable used for storing the last assignment value.
    #
    def reset_assignment_value
      @assignment_value = nil
    end

    ##
    # Returns the value of the last assignment.
    #
    def assignment_value
      return @assignment_value
    end

    ##
    # Stores the value as the last assigned value.
    #
    # @param [RubyLint::Definition::RubyObject] value
    #
    def buffer_assignment_value(value)
      @assignment_value = value
    end

    ##
    # Resets the method assignment/call type.
    #
    def reset_method_type
      @method_type = :instance_method
    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.
    #
    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

    ##
    # Returns the definition for the given node.
    #
    # @param [RubyLint::AST::Node] node
    # @return [RubyLint::Definition::RubyObject]
    #
    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

    ##
    # Increments the reference amount of a node's definition unless the
    # definition is frozen.
    #
    # @param [RubyLint::AST::Node] node
    #
    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
    #
    def inherit_definition(definition, inherit)
      unless definition.parents.include?(inherit)
        definition.parents << inherit
      end
    end

    ##
    # Extracts all the docstring tags from the documentation of the given
    # node, retrieves the corresponding types and stores them for later use.
    #
    # @param [RubyLint::AST::Node] node
    #
    def buffer_docstring_tags(node)
      return unless comments[node]

      parser = Docstring::Parser.new
      tags   = parser.parse(comments[node].map(&:text))

      @docstring_tags = Docstring::Mapping.new(tags)
    end

    ##
    # Resets the docstring tags collection back to its initial value.
    #
    def reset_docstring_tags
      @docstring_tags = nil
    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
    #
    def update_parents_from_tag(tag, definition)
      extra_parents = definitions_for_types(tag.types)

      definition.parents.concat(extra_parents)
    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]
    #
    def create_unknown_with_method(name)
      definition = Definition::RubyObject.create_unknown

      definition.send("define_#{@method_type}", name)

      return definition
    end

    ##
    # Returns a collection of definitions for a set of YARD types.
    #
    # @param [Array] types
    # @return [Array]
    #
    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

    ##
    # 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]
    #
    def lookup_type_definition(name)
      return current_scope.lookup(:const, name) || global_constant(name)
    end

    ##
    # @param [RubyLint::Docstring::ReturnTag] tag
    # @param [RubyLint::Definition::RubyMethod] definition
    #
    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

    ##
    # Tracks a method call.
    #
    # @param [RubyLint::Definition::RubyMethod] definition
    # @param [String] name
    # @param [RubyLint::AST::Node] node
    #
    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
  end # VirtualMachine
end # RubyLint