lib/rubocop/cop/rails/match_route.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Rails
      # Identifies places where defining routes with `match`
      # can be replaced with a specific HTTP method.
      #
      # Don't use `match` to define any routes unless there is a need to map multiple request types
      # among [:get, :post, :patch, :put, :delete] to a single action using the `:via` option.
      #
      # @example
      #   # bad
      #   match ':controller/:action/:id'
      #   match 'photos/:id', to: 'photos#show', via: :get
      #
      #   # good
      #   get ':controller/:action/:id'
      #   get 'photos/:id', to: 'photos#show'
      #   match 'photos/:id', to: 'photos#show', via: [:get, :post]
      #   match 'photos/:id', to: 'photos#show', via: :all
      #
      class MatchRoute < Base
        include RoutesHelper
        extend AutoCorrector

        MSG = 'Use `%<http_method>s` instead of `match` to define a route.'
        RESTRICT_ON_SEND = %i[match].freeze

        def_node_matcher :match_method_call?, <<~PATTERN
          (send nil? :match $_ $(hash ...) ?)
        PATTERN

        def on_send(node)
          match_method_call?(node) do |path_node, options_node|
            return unless within_routes?(node)

            options_node = path_node.hash_type? ? path_node : options_node.first

            if options_node.nil?
              register_offense(node, 'get')
            else
              via = extract_via(options_node)
              return unless via.size == 1 && http_method?(via.first)

              register_offense(node, via.first)
            end
          end
        end

        private

        def register_offense(node, http_method)
          add_offense(node, message: format(MSG, http_method: http_method)) do |corrector|
            match_method_call?(node) do |path_node, options_node|
              options_node = options_node.first

              corrector.replace(node, replacement(path_node, options_node))
            end
          end
        end

        def extract_via(node)
          via_pair = via_pair(node)
          return %i[get] unless via_pair

          _, via = *via_pair

          if via.basic_literal?
            [via.value]
          elsif via.array_type?
            via.values.map(&:value)
          else
            []
          end
        end

        def via_pair(node)
          node.pairs.find { |p| p.key.value == :via }
        end

        def http_method?(method)
          HTTP_METHODS.include?(method.to_sym)
        end

        def replacement(path_node, options_node)
          if path_node.hash_type?
            http_method, options = *http_method_and_options(path_node)
            "#{http_method} #{options.map(&:source).join(', ')}"
          elsif options_node.nil?
            "get #{path_node.source}"
          else
            http_method, options = *http_method_and_options(options_node)

            if options.any?
              "#{http_method} #{path_node.source}, #{options.map(&:source).join(', ')}"
            else
              "#{http_method} #{path_node.source}"
            end
          end
        end

        def http_method_and_options(node)
          via_pair = via_pair(node)
          http_method = extract_via(node).first
          rest_pairs = node.pairs - [via_pair]
          [http_method, rest_pairs]
        end
      end
    end
  end
end