troessner/reek

View on GitHub
lib/reek/context/code_context.rb

Summary

Maintainability
A
25 mins
Test Coverage
# frozen_string_literal: true

require_relative '../code_comment'
require_relative '../ast/object_refs'
require_relative 'statement_counter'

require 'forwardable'

module Reek
  module Context
    #
    # Superclass for all types of source code context. Each instance represents
    # a code element of some kind, and each provides behaviour relevant to that
    # code element. CodeContexts form a tree in the same way the code does,
    # with each context holding a reference to a unique outer context.
    #
    # @quality :reek:TooManyMethods { max_methods: 19 }
    # @quality :reek:TooManyInstanceVariables { max_instance_variables: 8 }
    class CodeContext
      include Enumerable
      extend Forwardable
      delegate [:name, :type] => :exp

      attr_reader :children, :parent, :exp, :statement_counter

      # Initializes a new CodeContext.
      #
      # @param exp [Reek::AST::Node] The code described by this context
      def initialize(exp)
        @exp                = exp
        @children           = []
        @statement_counter  = StatementCounter.new
        @refs               = AST::ObjectRefs.new
      end

      # Iterate over each AST node (see `Reek::AST::Node`) of a given type for the current expression.
      #
      # @param type [Symbol] the type of the nodes we are looking for, e.g. :defs.
      # @yield block that is executed for every node.
      #
      def local_nodes(type, ignored = [], &blk)
        ignored |= [:class, :module]
        exp.each_node(type, ignored, &blk)
      end

      # Iterate over `self` and child contexts.
      # The main difference (among others) to `local_nodes` is that we are traversing
      # `CodeContexts` here, not AST nodes (see `Reek::AST::Node`).
      #
      # @yield block that is executed for every node.
      # @return [Enumerator]
      #
      def each(&block)
        return enum_for(:each) unless block

        yield self
        children.each do |child|
          child.each(&block)
        end
      end

      # Link the present context to its parent.
      #
      # @param parent [Reek::AST::Node] The parent context of the code described by this context
      #
      # For example, given the following code:
      #
      #   class Omg
      #     def foo(x)
      #       puts x
      #     end
      #   end
      #
      # The {ContextBuilder} object first instantiates a {RootContext}, which has no parent.
      #
      # Next, it instantiates a {ModuleContext}, with +exp+ looking like this:
      #
      #  (class
      #    (const nil :Omg) nil
      #    (def :foo
      #      (args
      #        (arg :x))
      #      (send nil :puts
      #        (lvar :x))))
      #
      # It will then call #register_with_parent on the {ModuleContext}, passing
      # in the parent {RootContext}.
      #
      # Finally, {ContextBuilder} will instantiate a {MethodContext}. This time,
      # +exp+ is:
      #
      #   (def :foo
      #     (args
      #       (arg :x))
      #     (send nil :puts
      #       (lvar :x)))
      #
      # Then it will call #register_with_parent on the {MethodContext}, passing
      # in the parent {ModuleContext}.
      def register_with_parent(parent)
        @parent = parent.append_child_context(self) if parent
      end

      # Register a context as a child context of this context. This is
      # generally used by a child context to register itself with its parent.
      #
      # @param child [CodeContext] the child context to register
      def append_child_context(child)
        children << child
        self
      end

      # @quality :reek:TooManyStatements { max_statements: 6 }
      # @quality :reek:FeatureEnvy
      def record_call_to(exp)
        receiver = exp.receiver
        type = receiver ? receiver.type : :self
        line = exp.line
        case type
        when :lvar, :lvasgn
          refs.record_reference(name: receiver.name, line: line) unless exp.object_creation_call?
        when :self
          refs.record_reference(name: :self, line: line)
        end
      end

      def record_use_of_self
        refs.record_reference(name: :self)
      end

      def matches?(candidates)
        my_fq_name = full_name
        candidates.any? do |candidate|
          candidate = Regexp.quote(candidate) if candidate.is_a?(String)
          /#{candidate}/ =~ my_fq_name
        end
      end

      def full_name
        exp.full_name(parent ? parent.full_name : '')
      end

      def config_for(detector_class)
        parent_config_for(detector_class).merge(
          configuration_via_code_commment[detector_class.smell_type] || {})
      end

      def number_of_statements
        statement_counter.value
      end

      def singleton_method?
        false
      end

      def instance_method?
        false
      end

      def apply_current_visibility(_current_visibility)
        # Nothing to do by default
      end

      private

      attr_reader :refs

      def configuration_via_code_commment
        @configuration_via_code_commment ||= CodeComment.new(comment: full_comment,
                                                             line: exp.line,
                                                             source: exp.source).config
      end

      def full_comment
        exp.full_comment || ''
      end

      def parent_config_for(detector_class)
        parent ? parent.config_for(detector_class) : {}
      end
    end
  end
end