lib/rubocop/cop/style/method_call_with_args_parentheses.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Style
      # Enforces the presence (default) or absence of parentheses in
      # method calls containing parameters.
      #
      # In the default style (require_parentheses), macro methods are allowed.
      # Additional methods can be added to the `AllowedMethods` or
      # `AllowedPatterns` list. These options are valid only in the default
      # style. Macros can be included by either setting `IgnoreMacros` to false
      # or adding specific macros to the `IncludedMacros` list.
      #
      # Precedence of options is as follows:
      #
      # 1. `AllowedMethods`
      # 2. `AllowedPatterns`
      # 3. `IncludedMacros`
      #
      # If a method is listed in both `IncludedMacros` and `AllowedMethods`,
      # then the latter takes precedence (that is, the method is allowed).
      #
      # In the alternative style (omit_parentheses), there are three additional
      # options.
      #
      # 1. `AllowParenthesesInChaining` is `false` by default. Setting it to
      #    `true` allows the presence of parentheses in the last call during
      #    method chaining.
      #
      # 2. `AllowParenthesesInMultilineCall` is `false` by default. Setting it
      #     to `true` allows the presence of parentheses in multi-line method
      #     calls.
      #
      # 3. `AllowParenthesesInCamelCaseMethod` is `false` by default. This
      #     allows the presence of parentheses when calling a method whose name
      #     begins with a capital letter and which has no arguments. Setting it
      #     to `true` allows the presence of parentheses in such a method call
      #     even with arguments.
      #
      # NOTE: The style of `omit_parentheses` allows parentheses in cases where
      # omitting them results in ambiguous or syntactically incorrect code.
      #
      # Non-exhaustive list of examples:
      #
      # - Parentheses are required allowed in method calls with arguments inside
      #   literals, logical operators, setting default values in position and
      #   keyword arguments, chaining and more.
      # - Parentheses are allowed in method calls with arguments inside
      #   operators to avoid ambiguity.
      #   triple-dot syntax introduced in Ruby 2.7 as omitting them starts an
      #   endless range.
      # - Parentheses are allowed when forwarding arguments with the
      #   triple-dot syntax introduced in Ruby 2.7 as omitting them starts an
      #   endless range.
      # - Parentheses are required in calls with arguments when inside an
      #   endless method definition introduced in Ruby 3.0.
      # - Ruby 3.1's hash omission syntax allows parentheses if the method call
      #   is in conditionals and requires parentheses if the call
      #   is not the value-returning expression. See
      #   https://bugs.ruby-lang.org/issues/18396.
      # - Parentheses are required in anonymous arguments, keyword arguments
      #   and block passing in Ruby 3.2.
      #
      # @example EnforcedStyle: require_parentheses (default)
      #
      #   # bad
      #   array.delete e
      #
      #   # good
      #   array.delete(e)
      #
      #   # good
      #   # Operators don't need parens
      #   foo == bar
      #
      #   # good
      #   # Setter methods don't need parens
      #   foo.bar = baz
      #
      #   # okay with `puts` listed in `AllowedMethods`
      #   puts 'test'
      #
      #   # okay with `^assert` listed in `AllowedPatterns`
      #   assert_equal 'test', x
      #
      # @example EnforcedStyle: omit_parentheses
      #
      #   # bad
      #   array.delete(e)
      #
      #   # good
      #   array.delete e
      #
      #   # bad
      #   action.enforce(strict: true)
      #
      #   # good
      #   action.enforce strict: true
      #
      #   # good
      #   # Parentheses are allowed for code that can be ambiguous without
      #   # them.
      #   action.enforce(condition) || other_condition
      #
      #   # good
      #   # Parentheses are allowed for calls that won't produce valid Ruby
      #   # without them.
      #   yield path, File.basename(path)
      #
      #   # good
      #   # Omitting the parentheses in Ruby 3.1 hash omission syntax can lead
      #   # to ambiguous code. We allow them in conditionals and non-last
      #   # expressions. See https://bugs.ruby-lang.org/issues/18396
      #   if meets(criteria:, action:)
      #     safe_action(action) || dangerous_action(action)
      #   end
      #
      # @example IgnoreMacros: true (default)
      #
      #   # good
      #   class Foo
      #     bar :baz
      #   end
      #
      # @example IgnoreMacros: false
      #
      #   # bad
      #   class Foo
      #     bar :baz
      #   end
      #
      # @example AllowParenthesesInMultilineCall: false (default)
      #
      #   # bad
      #   foo.enforce(
      #     strict: true
      #   )
      #
      #   # good
      #   foo.enforce \
      #     strict: true
      #
      # @example AllowParenthesesInMultilineCall: true
      #
      #   # good
      #   foo.enforce(
      #     strict: true
      #   )
      #
      #   # good
      #   foo.enforce \
      #     strict: true
      #
      # @example AllowParenthesesInChaining: false (default)
      #
      #   # bad
      #   foo().bar(1)
      #
      #   # good
      #   foo().bar 1
      #
      # @example AllowParenthesesInChaining: true
      #
      #   # good
      #   foo().bar(1)
      #
      #   # good
      #   foo().bar 1
      #
      # @example AllowParenthesesInCamelCaseMethod: false (default)
      #
      #   # bad
      #   Array(1)
      #
      #   # good
      #   Array 1
      #
      # @example AllowParenthesesInCamelCaseMethod: true
      #
      #   # good
      #   Array(1)
      #
      #   # good
      #   Array 1
      #
      # @example AllowParenthesesInStringInterpolation: false (default)
      #
      #   # bad
      #   "#{t('this.is.bad')}"
      #
      #   # good
      #   "#{t 'this.is.better'}"
      #
      # @example AllowParenthesesInStringInterpolation: true
      #
      #   # good
      #   "#{t('this.is.good')}"
      #
      #   # good
      #   "#{t 'this.is.also.good'}"
      class MethodCallWithArgsParentheses < Base
        require_relative 'method_call_with_args_parentheses/omit_parentheses'
        require_relative 'method_call_with_args_parentheses/require_parentheses'

        include ConfigurableEnforcedStyle
        include AllowedMethods
        include AllowedPattern
        include RequireParentheses
        include OmitParentheses
        extend AutoCorrector

        def self.autocorrect_incompatible_with
          [Style::NestedParenthesizedCalls, Style::RescueModifier]
        end

        def on_send(node)
          send(style, node) # call require_parentheses or omit_parentheses
        end
        alias on_csend on_send
        alias on_yield on_send

        private

        def args_begin(node)
          loc = node.loc
          selector = node.yield_type? ? loc.keyword : loc.selector

          resize_by = args_parenthesized?(node) ? 2 : 1
          selector.end.resize(resize_by)
        end

        def args_end(node)
          node.source_range.end
        end

        def args_parenthesized?(node)
          return false unless node.arguments.one?

          first_node = node.first_argument
          first_node.begin_type? && first_node.parenthesized_call?
        end
      end
    end
  end
end