lib/rubocop/cop/rails/relative_date_constant.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Rails
      # This cop checks whether constant value isn't relative date.
      # Because the relative date will be evaluated only once.
      #
      # @example
      #   # bad
      #   class SomeClass
      #     EXPIRED_AT = 1.week.since
      #   end
      #
      #   # good
      #   class SomeClass
      #     def self.expired_at
      #       1.week.since
      #     end
      #   end
      class RelativeDateConstant < Cop
        MSG = 'Do not assign %<method_name>s to constants as it ' \
              'will be evaluated only once.'.freeze

        RELATIVE_DATE_METHODS = %i[ago from_now since until].freeze

        def on_casgn(node)
          _scope, _constant, rhs = *node

          # rhs would be nil in a or_asgn node
          return unless rhs

          check_node(rhs)
        end

        def on_masgn(node)
          lhs, rhs = *node

          return unless rhs && rhs.array_type?

          lhs.children.zip(rhs.children).each do |(name, value)|
            check_node(value) if name.casgn_type?
          end
        end

        def on_or_asgn(node)
          lhs, rhs = *node

          return unless lhs.casgn_type?

          check_node(rhs)
        end

        def autocorrect(node)
          _scope, const_name, value = *node
          indent = ' ' * node.loc.column
          new_code = ["def self.#{const_name.downcase}",
                      "#{indent}#{value.source}",
                      'end'].join("\n#{indent}")
          ->(corrector) { corrector.replace(node.source_range, new_code) }
        end

        private

        def check_node(node)
          return unless node.irange_type? ||
                        node.erange_type? ||
                        node.send_type?

          # for range nodes we need to check both their boundaries
          nodes = node.send_type? ? [node] : node.children

          nodes.each do |n|
            if relative_date_method?(n)
              add_offense(node.parent,
                          message: format(MSG, method_name: n.method_name))
            end
          end
        end

        def relative_date_method?(node)
          node.send_type? &&
            RELATIVE_DATE_METHODS.include?(node.method_name) &&
            !node.arguments?
        end
      end
    end
  end
end