diff --git a/changelog/new_add_new_lint_require_range_parentheses_cop.md b/changelog/new_add_new_lint_require_range_parentheses_cop.md new file mode 100644 index 00000000000..1f1be5e9c7d --- /dev/null +++ b/changelog/new_add_new_lint_require_range_parentheses_cop.md @@ -0,0 +1 @@ +* [#10792](https://github.com/rubocop/rubocop/pull/10792): Add new `Lint/RequireRangeParentheses` cop. ([@koic][]) diff --git a/config/default.yml b/config/default.yml index 001014f3df1..b54082bf827 100644 --- a/config/default.yml +++ b/config/default.yml @@ -2128,6 +2128,11 @@ Lint/RequireParentheses: Enabled: true VersionAdded: '0.18' +Lint/RequireRangeParentheses: + Description: 'Checks that a range literal is enclosed in parentheses when the end of the range is at a line break.' + Enabled: pending + VersionAdded: '<>' + Lint/RequireRelativeSelfPath: Description: 'Checks for uses a file requiring itself with `require_relative`.' Enabled: pending diff --git a/lib/rubocop.rb b/lib/rubocop.rb index fa353a09d65..7a7539001c1 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -356,6 +356,7 @@ require_relative 'rubocop/cop/lint/refinement_import_methods' require_relative 'rubocop/cop/lint/regexp_as_condition' require_relative 'rubocop/cop/lint/require_parentheses' +require_relative 'rubocop/cop/lint/require_range_parentheses' require_relative 'rubocop/cop/lint/require_relative_self_path' require_relative 'rubocop/cop/lint/rescue_exception' require_relative 'rubocop/cop/lint/rescue_type' diff --git a/lib/rubocop/cop/lint/require_range_parentheses.rb b/lib/rubocop/cop/lint/require_range_parentheses.rb new file mode 100644 index 00000000000..f1b2f5c2958 --- /dev/null +++ b/lib/rubocop/cop/lint/require_range_parentheses.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Lint + # Checks that a range literal is enclosed in parentheses when the end of the range is + # at a line break. + # + # NOTE: The following is maybe intended for `(42..)`. But, compatible is `42..do_something`. + # So, this cop does not provide autocorrection because it is left to user. + # + # [source,ruby] + # ---- + # case condition + # when 42.. + # do_something + # end + # ---- + # + # @example + # + # # bad - Represents `(1..42)`, not endless range. + # 1.. + # 42 + # + # # good - It's incompatible, but your intentions when using endless range may be: + # (1..) + # 42 + # + # # good + # 1..42 + # + # # good + # (1..42) + # + # # good + # (1.. + # 42) + # + class RequireRangeParentheses < Base + MSG = 'Wrap the endless range literal `%s` to avoid precedence ambiguity.' + + def on_irange(node) + return if node.parent&.begin_type? + return unless node.begin && node.end + return if same_line?(node.begin, node.end) + + message = format(MSG, range: "#{node.begin.source}#{node.loc.operator.source}") + + add_offense(node, message: message) + end + + alias on_erange on_irange + end + end + end +end diff --git a/spec/rubocop/cop/lint/require_range_parentheses_spec.rb b/spec/rubocop/cop/lint/require_range_parentheses_spec.rb new file mode 100644 index 00000000000..2b9038a4efa --- /dev/null +++ b/spec/rubocop/cop/lint/require_range_parentheses_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Lint::RequireRangeParentheses, :config do + it 'registers an offense when the end of the range (`..`) is line break' do + expect_offense(<<~RUBY) + 42.. + ^^^^ Wrap the endless range literal `42..` to avoid precedence ambiguity. + do_something + RUBY + end + + it 'registers an offense when the end of the range (`...`) is line break' do + expect_offense(<<~RUBY) + 42... + ^^^^^ Wrap the endless range literal `42...` to avoid precedence ambiguity. + do_something + RUBY + end + + it 'does not register an offense when the end of the range (`..`) is line break and is enclosed in parentheses' do + expect_no_offenses(<<~RUBY) + (42.. + do_something) + RUBY + end + + context 'Ruby >= 2.6', :ruby26 do + it 'does not register an offense when using endless range only' do + expect_no_offenses(<<~RUBY) + 42.. + RUBY + end + end + + context 'Ruby >= 2.7', :ruby27 do + it 'does not register an offense when using beginless range only' do + expect_no_offenses(<<~RUBY) + ..42 + RUBY + end + end + + it 'does not register an offense when using `42..nil`' do + expect_no_offenses(<<~RUBY) + 42..nil + RUBY + end + + it 'does not register an offense when using `nil..42`' do + expect_no_offenses(<<~RUBY) + nil..42 + RUBY + end + + it 'does not register an offense when begin and end of the range are on the same line' do + expect_no_offenses(<<~RUBY) + 42..do_something + RUBY + end +end