module Pelusa
  module Lint
    class DemeterLaw
      def initialize
        @violations = Set.new
      end

      def check(klass)
        iterate_lines!(klass)

        return SuccessfulAnalysis.new(name) if @violations.empty?

        FailedAnalysis.new(name, @violations) do |violations|
          "There are #{violations.length} Demeter law violations in lines #{violations.to_a.join(', ')}."
        end
      end

      private

      def name
        "Respects Demeter law"
      end

      def iterate_lines!(klass)
        ClassAnalyzer.walk(klass) do |node|
          if node.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Send) && node.receiver.is_a?(Rubinius::ToolSets::Runtime::ToolSet::AST::Send)
            @violations << node.line unless white_listed?(node.receiver.name)
          end
        end
      end

      # Internal: Default modules whose methods are whitelisted.
      DEFAULT_WHITELIST = [Class, Fixnum, Enumerable]

      # Internal: Methods on these common, fundamental classes and modules are
      # allowed to be called on other objects.
      #
      # Note: this doesn't currently work with namespaced objects, but would be
      # easy to extend.
      #
      # Returns an array of classes specified in .pelusa.yml (a comma-separated
      # list of class names) or the default values above.
      def whitelist_sources
        if whitelist = Pelusa.configuration['DemeterLaw']['whitelist']
          whitelist.split(",").map {|k| Kernel.const_get(k.strip)}
        else
          DEFAULT_WHITELIST
        end
      end

      # Internal: Allow conversion methods -- matching /^(to_|as_)/ to violate
      # the law of Demeter.
      def allow_conversions?
        Pelusa.configuration['DemeterLaw'].fetch('allow_conversions', false)
      end

      def white_listed? method
        whitelist_sources.any? do |enclosing_module|
          enclosing_module.instance_methods.any? do |instance_method|
            instance_method == method or
              allow_conversions? && instance_method =~ /^(to_|as_)/
          end
        end
      end
    end
  end
end
