# encoding: utf-8
require 'rails_best_practices/reviews/review'

module RailsBestPractices
  module Reviews
    # Review to make sure not to avoid the law of demeter.
    #
    # See the best practice details here http://rails-bestpractices.com/posts/15-the-law-of-demeter.
    #
    # Implementation:
    #
    # Review process:
    #   check all method calls to see if there is method call to the association object.
    #   if there is a call node whose subject is an object of model (compare by name),
    #   and whose message is an association of that model (also compare by name),
    #   and outer the call node, it is also a call node,
    #   then it violate the law of demeter.
    class LawOfDemeterReview < Review

      def url
        "http://rails-bestpractices.com/posts/15-the-law-of-demeter"
      end

      def interesting_nodes
        [:call]
      end

      # check the call node,
      #
      # if the subject of the call node is also a call node,
      # and the subject of the subject call node matchs one of the class names,
      # and the message of the subject call node matchs one of the association name with the class name, like
      #
      #     s(:call,
      #       s(:call, s(:ivar, :@invoice), :user, s(:arglist)),
      #       :name,
      #       s(:arglist)
      #     )
      #
      # then it violates the law of demeter.
      def start_call(node)
        if [:lvar, :ivar].include?(node.subject.subject.node_type) && need_delegate?(node)
          add_error "law of demeter"
        end
      end

      private
        # check if the call node can use delegate to avoid violating law of demeter.
        #
        # if the subject of subject of the call node matchs any in model names,
        # and the message of subject of the call node matchs any in association names,
        # then it needs delegate.
        #
        # e.g. the source code is
        #
        #     @invoic.user.name
        #
        # then the call node is
        #
        #     s(:call, s(:call, s(:ivar, :@invoice), :user, s(:arglist)), :name, s(:arglist))
        #
        # as you see the subject of subject of the call node is [:ivar, @invoice],
        # and the message of subject of the call node is :user
        def need_delegate?(node)
          class_name = node.subject.subject.to_s(:remove_at => true).classify
          association_methods.include? model_associations[class_name][node.subject.message.to_s]
        end

        # only check belongs_to and has_one association.
        def association_methods
          [:belongs_to, :has_one]
        end
    end
  end
end