YorickPeterse/ruby-lint

View on GitHub
lib/ruby-lint/definition/ruby_object.rb

Summary

Maintainability
C
1 day
Test Coverage
module RubyLint
  module Definition
    ##
    # The RubyObject class is the base definition class of ruby-lint. These so
    # called definition classes are used for storing information about Ruby
    # classes and instances. At their most basic form they are a mix between
    # {RubyLint::Node} and a lookup table.
    #
    # ruby-lint currently provides the following two definition classes:
    #
    # * {RubyLint::Definition::RubyObject}: the base definition class, used for
    #   most Ruby types and values.
    # * {RubyLint::Definition::RubyMethod} definition class that is used for
    #   methods exclusively.
    #
    # Using the RubyObject class one could create a definition for the String
    # class as following:
    #
    #     string  = RubyObject.new(:name => 'String', :type => :const)
    #     newline = RubyObject.new(
    #       :name  => 'NEWLINE',
    #       :type  => :const,
    #       :value => "\n"
    #     )
    #
    #     string.add(:const, newline.name, newline)
    #
    # For more information see the documentation of the corresponding methods.
    #
    # @!attribute [r] name
    #  @return [String] The name of the object.
    #
    # @!attribute [rw] value
    #  @return [Mixed] The value of the object.
    #
    # @!attribute [r] type
    #  @return [Symbol] The type of object, e.g. `:const`.
    #
    # @!attribute [r] definitions
    #  @return [Hash{Symbol => Hash{String => Object}}]
    #    Hash keyed by type and name, containing all child the definitions.
    #
    # @!attribute [rw] parents
    #  @return [Array<RubyLint::Definition::RubyObject>]
    #    Array containing the parent definitions.
    #
    # @!attribute [rw] reference_amount
    #  @return [Numeric] The amount of times an object was referenced.
    #   Currently this is only used for variables.
    #
    # @!attribute [rw] instance_type
    #  @return [Symbol] Indicates if the object represents a class or an
    #   instance.
    #
    # @!attribute [r] update_parents
    #  @return [Array<Symbol>] A list of data types to also add to the parent
    #   definitions when adding an object to the current one.
    #
    # @!attribute [r] members_as_value
    #  @return [TrueClass|FalseClass] When set to `true` the {#value} getter
    #   returns a collection of the members instead of the manually defined
    #   value.
    #
    # @!attribute [r] line
    #  @return [Numeric] The line number of the definition.
    #
    # @!attribute [r] column
    #  @return [Numeric] The column number of the definition.
    #
    # @!attribute [r] file
    #  @return [String] The file path of the definition.
    #
    # @!attribute [r] inherit_self
    #  @return [TrueClass|FalseClass] When set to `false` child definitions
    #   created using `define_constant` do not inherit the current definition.
    #
    class RubyObject
      include VariablePredicates

      ##
      # Array containing items that should be looked up in the parent
      # definition if they're not found in the current one.
      #
      # @return [Array<Symbol>]
      #
      LOOKUP_PARENT = [
        :const,
        :cvar,
        :gvar,
        :instance_method,
        :ivar,
        :method
      ].freeze

      ##
      # String used to separate segments in a constant path.
      #
      # @return [String]
      #
      PATH_SEPARATOR = '::'.freeze

      ##
      # Array containing the valid data types that can be stored.
      #
      # @return [Array<Symbol>]
      #
      VALID_TYPES = [
        :arg,
        :blockarg,
        :const,
        :cvar,
        :gvar,
        :instance_method,
        :ivar,
        :kwoptarg,
        :lvar,
        :member,
        :method,
        :optarg,
        :restarg,
        :unknown
      ].freeze

      attr_reader :update_parents,
        :column,
        :definitions,
        :file,
        :inherit_self,
        :line,
        :members_as_value,
        :name,
        :type

      attr_accessor :instance_type, :parents, :reference_amount

      ##
      # Creates an object that represents an unknown value.
      #
      # @return [RubyLint::Definition::RubyObject]
      #
      def self.create_unknown
        return new(:type => :unknown, :name => 'unknown')
      end

      ##
      # @example
      #  string = RubyObject.new(:name => 'String', :type => :const)
      #
      # @param [Hash] options Hash containing additional options such as the
      #  parent definitions. For a list of available options see the
      #  corresponding getter/setter methods of this class.
      #
      # @yieldparam [RubyLint::Definition::RubyObject]
      #
      def initialize(options = {})
        @inherit_self = true

        options.each do |key, value|
          instance_variable_set("@#{key}", value)
        end

        @update_parents   ||= []
        @instance_type    ||= :class
        @parents          ||= []
        @reference_amount ||= 0

        @definitions = Hash.new { |hash, key| hash[key] = {} }
        @value       = nil if members_as_value

        after_initialize if respond_to?(:after_initialize)

        yield self if block_given?
      end

      ##
      # Returns the value of the definition. If `members_as_value` is set to
      # `true` the return value is a Hash containing the names and values of
      # each member.
      #
      # @return [Hash|RubyLint::Definition::RubyObject]
      #
      def value
        return members_as_value ? list(:member) : @value
      end

      ##
      # Sets the value of the definition.
      #
      # @param [Mixed] value
      #
      def value=(value)
        @value = value
      end

      ##
      # Adds the definition object to the current one.
      #
      # @see #add
      # @param [RubyLint::Definition::RubyObject] definition
      #
      def add_definition(definition)
        add(definition.type, definition.name, definition)
      end

      ##
      # Adds a new definition to the definitions list.
      #
      # @example
      #  string  = RubyObject.new(:name => 'String', :type => :const)
      #  newline = RubyObject.new(
      #    :name  => 'NEWLINE',
      #    :type  => :const,
      #    :value => "\n"
      #  )
      #
      #  string.add(newline.type, newline.name, newline)
      #
      # @param [#to_sym] type The type of definition to add.
      # @param [String] name The name of the definition.
      # @param [RubyLint::Definition::RubyObject] value
      #
      # @raise [TypeError] Raised when a value that is not a RubyObject
      #  instance (or a subclass of this class) is given.
      #
      # @raise [ArgumentError] Raised when the specified type was invalid.
      #
      def add(type, name, value)
        type = type.to_sym

        unless value.is_a?(RubyObject)
          raise TypeError, "Expected RubyObject but got #{value.class}"
        end

        unless VALID_TYPES.include?(type)
          raise ArgumentError, ":#{type} is not a valid type of data to add"
        end

        definitions[type][name] = value

        if update_parents.include?(type)
          update_parent_definitions(type, name, value)
        end
      end

      ##
      # Looks up a definition by the given type and name. If no data was found
      # this method will try to look it up in any parent definitions.
      #
      # If no definition was found `nil` will be returned.
      #
      # @example
      #  string  = RubyObject.new(:name => 'String', :type => :const)
      #  newline = RubyObject.new(
      #    :name  => 'NEWLINE',
      #    :type  => :const,
      #    :value => "\n"
      #  )
      #
      #  string.add(newline.type, newline.name, newline)
      #
      #  string.lookup(:const, 'NEWLINE') # => #<RubyLint::Definition...>
      #
      # @param [#to_sym] type
      # @param [String] name
      #
      # @param [TrueClass|FalseClass] lookup_parent Whether definitions should
      #  be looked up from parent definitions.
      #
      # @param [Array<RubyLint::Definition::RubyObject>] exclude
      #  A list of definitions to skip when looking up
      #  parents. This list is used to prevent stack errors when dealing with
      #  recursive definitions. A good example of this is `Logger` and
      #  `Logger::Severity` which both inherit from each other.
      #
      # @return [RubyLint::Definition::RubyObject|NilClass]
      #
      def lookup(type, name, lookup_parent = true, exclude = [])
        type, name = prepare_lookup(type, name)
        found      = nil

        if defines?(type, name)
          found = definitions[type][name]

        elsif type == :cbase
          found = top_scope
        # Look up the definition in the parent scope(s) (if any are set). This
        # takes the parents themselves also into account.
        elsif lookup_parent?(type) and lookup_parent
          parents.each do |parent|
            # If we've already processed the parent we'll skip it.
            next if exclude.include?(parent)

            parent_definition = determine_parent(parent, type, name, exclude)

            if parent_definition
              found = parent_definition
              break
            end
          end
        end

        return found
      end

      ##
      # Returns the definition for the given constant path.
      #
      # @example
      #  example.lookup_constant_path('A::B') # => #<RubyLint::Definition...>
      #
      # @param [String|Array<String>] path
      # @return [RubyLint::Definition::RubyObject]
      #
      def lookup_constant_path(path)
        constant = self
        path     = path.split(PATH_SEPARATOR) if path.is_a?(String)

        path.each do |segment|
          found = constant.lookup(:const, segment)

          found ? constant = found : return
        end

        return constant
      end

      ##
      # Mimics a method call by executing the method for the given name. This
      # method should be defined in the current definition.
      #
      # @param [String] name The name of the method.
      # @return [Mixed]
      #
      def call_method(name)
        method = lookup(method_call_type, name)

        unless method
          raise NoMethodError, "Undefined method #{name} for #{self.inspect}"
        end

        return method.call(self)
      end

      ##
      # Returns `true` if a method is defined, similar to `respond_to?`.
      #
      # @return [TrueClass|FalseClass]
      #
      def method_defined?(name)
        return has_definition?(method_call_type, name)
      end

      ##
      # Performs a method call on the current definition.
      #
      # If the return value of a method definition is set to a Proc (or any
      # other object that responds to `:call`) it will be called and passed the
      # current instance as an argument.
      #
      # TODO: add support for specifying method arguments.
      #
      # @param [RubyLint::Definition::RubyObject] context The context in which
      #  the method was called.
      # @return [Mixed]
      #
      def call(context = self)
        retval = respond_to?(:return_value) ? return_value : nil
        retval = retval.call(context) if retval.is_a?(Proc)

        return retval
      end

      ##
      # Returns `true` if the current definition list or one of the parents has
      # the specified definition.
      #
      # @example
      #  string.has_definition?(:instance_method, 'downcase') # => true
      #
      # @param [#to_sym] type
      # @param [String] name
      # @param [Array<RubyLint::Definition::RubyObject>] exclude
      #   Parent definitions to exclude.
      # @return [TrueClass|FalseClass]
      #
      def has_definition?(type, name, exclude = [])
        type, name = prepare_lookup(type, name)

        if definitions.key?(type) and definitions[type].key?(name)
          return true

        elsif lookup_parent?(type)
          parents.each do |parent|
            next if exclude.include?(parent)

            return true if parent.has_definition?(type, name, exclude | [self])
          end
        end

        return false
      end

      ##
      # Determines the call types for methods called on the current definition.
      #
      # @return [Symbol]
      #
      def method_call_type
        return class? ? :method : :instance_method
      end

      ##
      # @return [TrueClass|FalseClass]
      #
      def class?
        return instance_type == :class
      end

      ##
      # @return [TrueClass|FalseClass]
      #
      def instance?
        return instance_type == :instance
      end

      ##
      # Checks if the specified definition is defined in the current object,
      # ignoring data in any parent definitions.
      #
      # @see RubyLint::Definition::RubyObject#has_definition?
      # @return [TrueClass|FalseClass]
      #
      def defines?(type, name)
        type, name = prepare_lookup(type, name)

        return definitions.key?(type) && definitions[type].key?(name)
      end

      ##
      # Returns a list of all the definitions for the specific type.  This list
      # excludes anything defined in parent definitions.
      #
      # @example
      #  string.list(:instance_method) # => [..., ..., ...]
      #
      # @param [#to_sym] type
      # @return [Array]
      #
      def list(type)
        type = type.to_sym

        return definitions.key?(type) ? definitions[type].values : []
      end

      ##
      # Returns the amount of definitions stored for a given type.
      #
      # @param [#to_sym] type
      # @return [Numeric]
      #
      def amount(type)
        return list(type).length
      end

      ##
      # Merges the definitions object `other` into the current one.
      #
      # @param [RubyLint::Definition::RubyObject] other
      #
      def merge(other)
        other.definitions.each do |type, values|
          values.each do |name, definition|
            definitions[type][name] = definition
          end
        end
      end

      ##
      # Copies all the definitions in `source` of type `type` into the current
      # definitions object.
      #
      # @param [RubyLint::Definition::RubyObject] source
      # @param [Symbol] source_type The type of definitions to copy from the
      #  source.
      # @param [Symbol] target_type The type to store the definitions under,
      #  set to the `source_type` value by default.
      #
      def copy(source, source_type, target_type = source_type)
        return unless source.definitions.key?(source_type)

        source.list(source_type).each do |definition|
          unless defines?(target_type, definition.name)
            add(target_type, definition.name, definition)
          end
        end
      end

      ##
      # Creates a new definition object based on the current one that
      # represents an instance of a Ruby value (instead of a class).
      #
      # @param [Hash] options Attributes to override in the new definition.
      # @return [RubyLint::Definition::RubyObject]
      #
      def instance(options = {})
        return shim(:instance_type => :instance)
      end

      ##
      # Creates a new definition that inherits from the current one, acting as
      # sort of a shim around the original one.
      #
      # @param [Hash] options Attributes to set in the new definition.
      # @return [RubyLint::Definition::RubyObject]
      #
      def shim(options = {})
        options = {
          :name          => name,
          :type          => type,
          :instance_type => instance_type,
          :value         => value,
          :parents       => [self]
        }.merge(options)

        return self.class.new(options)
      end

      ##
      # Changes the instance type of the current definition to `:instance`. If
      # you want to return a new definition use {#instance} instead.
      #
      def instance!
        @instance_type = :instance
      end

      ##
      # Returns `true` if the object was referenced more than once.
      #
      # @return [TrueClass|FalseClass]
      #
      def used?
        return reference_amount > 0
      end

      ##
      # Defines a new child constant.
      #
      # @example
      #  string.define_constant('NEWLINE')
      #
      # @param [String] name
      # @yieldparam [RubyLint::Definition::RubyObject] defs
      # @return [RubyLint::Definition::RubyObject]
      #
      def define_constant(name, &block)
        if name.include?(PATH_SEPARATOR)
          path       = name.split(PATH_SEPARATOR)
          target     = lookup_constant_path(path[0..-2])
          definition = target.define_constant(path[-1], &block)
        else
          definition = add_child_definition(:const, name, &block)
        end

        definition.define_self

        return definition
      end

      ##
      # Defines a new global variable in the current definition.
      #
      # @example
      #  string.define_global_variable('$name', '...')
      #
      # @param [String] name
      # @param [Mixed] value
      #
      def define_global_variable(name, value = self.class.create_unknown)
        return add_child_definition(:gvar, name, value)
      end

      ##
      # Defines a new class method.
      #
      # @example
      #  string.define_method(:new)
      #
      # @param [String,Symbol] name
      # @yieldparam [RubyLint::Definition::RubyMethod] method
      # @return [RubyLint::Definition::RubyMethod]
      #
      def define_method(name, &block)
        return add_child_method(:method, name, &block)
      end

      ##
      # Defines a new instance method.
      #
      # @example
      #  string.define_instance_method(:gsub)
      #
      # @see RubyLint::Definition::RubyObject#define_method
      # @param [String,Symbol] name
      # @yieldparam [RubyLint::Definition::RubyMethod] method
      # @return [RubyLint::Definition::RubyMethod]
      #
      def define_instance_method(name, &block)
        return add_child_method(:instance_method, name, &block)
      end

      ##
      # Helper method that makes it easier to provide the two constructor
      # methods `new` and `initialize`. The supplied block is yielded on both
      # method definitions.
      #
      # @example
      #  some_object.define_constructors do |method|
      #    method.argument('name')
      #  end
      #
      def define_constructors(&block)
        define_method('new', &block)
        define_instance_method('initialize', &block)
      end

      ##
      # Adds the object(s) to the list of parent definitions.
      #
      # @param [Array<RubyLint::Definition::ConstantProxy>] definitions
      #
      def inherits(*definitions)
        self.parents.concat(definitions)
      end

      ##
      # @see {RubyLint::Definition::ConstantProxy#initialize}
      # @param [String] name
      # @param [RubyLint::Definition::Registry] registry
      # @return [RubyLint::Definition::ConstantProxy]
      #
      def constant_proxy(name, registry = nil)
        return ConstantProxy.new(self, name, registry)
      end

      ##
      # Defines `self` on the current definition as both a class and instance
      # method.
      #
      def define_self
        if instance?
          self_instance = self
          self_class    = instance(:instance_type => :class)
        else
          self_instance = self.instance
          self_class    = self
        end

        define_method('self') do |method|
          method.returns(self_class)
        end

        define_instance_method('self') do |method|
          method.returns(self_instance)
        end
      end

      ##
      # Returns a pretty formatted String that shows some info about the
      # current definition.
      #
      # @return [String]
      #
      def inspect
        attributes = [
          %Q(@name="#{name}"),
          %Q(@type="#{type}"),
          %Q(@instance_type="#{instance_type}")
        ]

        # See <http://stackoverflow.com/a/2818916> for more info.
        address = (object_id << 1).to_s(16)

        return %Q(#<#{self.class}:0x#{address} #{attributes.join(' ')}>)
      end

      def top_scope
        return self if type == :root
        scope = parents.last   # the enclosing scope
        scope ? scope.top_scope : self
      end

      private

      ##
      # Updates each parent definition if it has an existing definition for hte
      # given type and name.
      #
      # @see #add
      #
      def update_parent_definitions(type, name, value)
        parents.each do |parent|
          parent.add(type, name, value) if parent.has_definition?(type, name)
        end
      end

      ##
      # Determines what parent definition to use.
      #
      # @param [RubyLint::Definition::RubyObject] parent
      # @param [Symbol] type
      # @param [String] name
      # @param [Array<RubyLint::Definition::RubyObject>] exclude
      # @return [RubyLint::Definition::RubyObject]
      #
      def determine_parent(parent, type, name, exclude = [])
        if parent.type == type and parent.name == name
          parent_definition = parent
        else
          exclude = exclude + [self] unless exclude.include?(self)

          parent_definition = parent.lookup(type, name, true, exclude)
        end

        return parent_definition
      end

      ##
      # Adds a new child definition to the current definition.
      #
      # @param [Symbol] type The definition type.
      # @param [String] name The name of the definition.
      # @param [Mixed] value
      # @return [RubyLint::Definition::RubyObject]
      #
      def add_child_definition(type, name, value = nil, &block)
        definition = self.class.new(
          :name    => name,
          :type    => type,
          :value   => value,
          :parents => inherit_self ? [self] : nil,
          &block
        )

        add(definition.type, definition.name, definition)

        return definition
      end

      ##
      # Adds a new child method to the current definition.
      #
      # @see RubyLint::Definition::RubyObject#add_child_definition
      #
      def add_child_method(type, name, &block)
        definition = RubyMethod.new(
          :name          => name,
          :type          => type,
          :parents       => [self],
          :instance_type => :instance,
          &block
        )

        add(definition.type, definition.name, definition)

        return definition
      end

      ##
      # Returns a boolean that indicates if the current definition type should
      # be looked up in a parent definition.
      #
      # @param  [Symbol] type The type of definition.
      # @return [Trueclass|FalseClass]
      #
      def lookup_parent?(type)
        return LOOKUP_PARENT.include?(type) && !parents.empty?
      end

      ##
      # Casts the type and name of data to look up to the correct values.
      #
      # @param [#to_sym] type
      # @param [#to_s] name
      # @return [Array(Symbol,String)]
      #
      def prepare_lookup(type, name)
        return type.to_sym, name.to_s
      end
    end # RubyObject
  end # Definition
end # RubyLint