lib/rubocop/cop/style/braces_around_hash_parameters.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Style
      # This cop checks for braces around the last parameter in a method call
      # if the last parameter is a hash.
      # It supports `braces`, `no_braces` and `context_dependent` styles.
      #
      # @example EnforcedStyle: braces
      #   # The `braces` style enforces braces around all method
      #   # parameters that are hashes.
      #
      #   # bad
      #   some_method(x, y, a: 1, b: 2)
      #
      #   # good
      #   some_method(x, y, {a: 1, b: 2})
      #
      # @example EnforcedStyle: no_braces (default)
      #   # The `no_braces` style checks that the last parameter doesn't
      #   # have braces around it.
      #
      #   # bad
      #   some_method(x, y, {a: 1, b: 2})
      #
      #   # good
      #   some_method(x, y, a: 1, b: 2)
      #
      # @example EnforcedStyle: context_dependent
      #   # The `context_dependent` style checks that the last parameter
      #   # doesn't have braces around it, but requires braces if the
      #   # second to last parameter is also a hash literal.
      #
      #   # bad
      #   some_method(x, y, {a: 1, b: 2})
      #   some_method(x, y, {a: 1, b: 2}, a: 1, b: 2)
      #
      #   # good
      #   some_method(x, y, a: 1, b: 2)
      #   some_method(x, y, {a: 1, b: 2}, {a: 1, b: 2})
      class BracesAroundHashParameters < Cop
        include ConfigurableEnforcedStyle
        include RangeHelp

        MSG = '%<type>s curly braces around a hash parameter.'.freeze

        def on_send(node)
          return if node.assignment_method? || node.operator_method?

          return unless node.arguments? && node.last_argument.hash_type? &&
                        !node.last_argument.empty?

          check(node.last_argument, node.arguments)
        end
        alias on_csend on_send

        # We let AutocorrectUnlessChangingAST#autocorrect work with the send
        # node, because that context is needed. When parsing the code to see if
        # the AST has changed, a braceless hash would not be parsed as a hash
        # otherwise.
        def autocorrect(send_node)
          hash_node = send_node.last_argument

          lambda do |corrector|
            if hash_node.braces?
              remove_braces_with_whitespace(corrector,
                                            hash_node,
                                            extra_space(hash_node))
            else
              add_braces(corrector, hash_node)
            end
          end
        end

        private

        def check(arg, args)
          if style == :braces && !arg.braces?
            add_arg_offense(arg, :missing)
          elsif style == :no_braces && arg.braces?
            add_arg_offense(arg, :redundant)
          elsif style == :context_dependent
            check_context_dependent(arg, args)
          end
        end

        def check_context_dependent(arg, args)
          braces_around_second_from_end = args.size > 1 && args[-2].hash_type?

          if arg.braces?
            unless braces_around_second_from_end
              add_arg_offense(arg, :redundant)
            end
          elsif braces_around_second_from_end
            add_arg_offense(arg, :missing)
          end
        end

        def add_arg_offense(arg, type)
          add_offense(arg.parent, location: arg.source_range,
                                  message: format(MSG,
                                                  type: type.to_s.capitalize))
        end

        def extra_space(hash_node)
          {
            newlines: extra_left_space?(hash_node) &&
              extra_right_space?(hash_node),
            left: extra_left_space?(hash_node),
            right: extra_right_space?(hash_node)
          }
        end

        def extra_left_space?(hash_node)
          @extra_left_space ||= begin
            top_line = hash_node.source_range.source_line
            top_line.delete(' ') == '{'
          end
        end

        def extra_right_space?(hash_node)
          @extra_right_space ||= begin
            bottom_line_number = hash_node.source_range.last_line
            bottom_line = processed_source.lines[bottom_line_number - 1]
            bottom_line.delete(' ') == '}'
          end
        end

        def remove_braces_with_whitespace(corrector, node, space)
          if node.multiline?
            remove_braces_with_range(corrector,
                                     left_whole_line_range(node.loc.begin),
                                     right_whole_line_range(node.loc.end))
          else
            right_brace_and_space = right_brace_and_space(node.loc.end, space)
            left_brace_and_space = left_brace_and_space(node.loc.begin, space)
            remove_braces_with_range(corrector,
                                     left_brace_and_space,
                                     right_brace_and_space)
          end
        end

        def remove_braces_with_range(corrector, left_range, right_range)
          corrector.remove(left_range)
          corrector.remove(right_range)
        end

        def left_whole_line_range(loc_begin)
          if range_by_whole_lines(loc_begin).source.strip == '{'
            range_by_whole_lines(loc_begin, include_final_newline: true)
          else
            loc_begin
          end
        end

        def right_whole_line_range(loc_end)
          if range_by_whole_lines(loc_end).source.strip =~ /\A}\s*,?\z/
            range_by_whole_lines(loc_end, include_final_newline: true)
          else
            loc_end
          end
        end

        def left_brace_and_space(loc_begin, space)
          range_with_surrounding_space(range: loc_begin,
                                       side: :right,
                                       newlines: space[:newlines],
                                       whitespace: space[:left])
        end

        def right_brace_and_space(loc_end, space)
          brace_and_space =
            range_with_surrounding_space(
              range: loc_end,
              side: :left,
              newlines: space[:newlines],
              whitespace: space[:right]
            )
          range_with_surrounding_comma(brace_and_space, :left)
        end

        def add_braces(corrector, node)
          corrector.insert_before(node.source_range, '{')
          corrector.insert_after(node.source_range, '}')
        end
      end
    end
  end
end