rubocop 1.18.4 → 1.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +65 -13
  4. data/lib/rubocop/cli.rb +18 -0
  5. data/lib/rubocop/config_loader.rb +2 -2
  6. data/lib/rubocop/config_loader_resolver.rb +21 -6
  7. data/lib/rubocop/cop/base.rb +2 -2
  8. data/lib/rubocop/cop/bundler/gem_filename.rb +103 -0
  9. data/lib/rubocop/cop/bundler/insecure_protocol_source.rb +12 -11
  10. data/lib/rubocop/cop/bundler/ordered_gems.rb +1 -1
  11. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +2 -2
  12. data/lib/rubocop/cop/correctors/line_break_corrector.rb +1 -1
  13. data/lib/rubocop/cop/correctors/require_library_corrector.rb +23 -0
  14. data/lib/rubocop/cop/documentation.rb +1 -1
  15. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +1 -1
  16. data/lib/rubocop/cop/internal_affairs/inherit_deprecated_cop_class.rb +34 -0
  17. data/lib/rubocop/cop/internal_affairs/undefined_config.rb +71 -0
  18. data/lib/rubocop/cop/internal_affairs.rb +2 -0
  19. data/lib/rubocop/cop/layout/argument_alignment.rb +1 -1
  20. data/lib/rubocop/cop/layout/class_structure.rb +2 -1
  21. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +9 -0
  22. data/lib/rubocop/cop/layout/end_alignment.rb +3 -2
  23. data/lib/rubocop/cop/layout/hash_alignment.rb +7 -3
  24. data/lib/rubocop/cop/layout/heredoc_indentation.rb +0 -7
  25. data/lib/rubocop/cop/layout/leading_comment_space.rb +2 -2
  26. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +33 -14
  27. data/lib/rubocop/cop/layout/line_length.rb +1 -1
  28. data/lib/rubocop/cop/layout/multiline_block_layout.rb +1 -1
  29. data/lib/rubocop/cop/layout/multiline_method_argument_line_breaks.rb +3 -0
  30. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +22 -9
  31. data/lib/rubocop/cop/layout/single_line_block_chain.rb +15 -4
  32. data/lib/rubocop/cop/layout/space_after_not.rb +1 -0
  33. data/lib/rubocop/cop/layout/space_around_equals_in_parameter_default.rb +2 -1
  34. data/lib/rubocop/cop/layout/space_around_keyword.rb +2 -2
  35. data/lib/rubocop/cop/layout/space_around_operators.rb +8 -1
  36. data/lib/rubocop/cop/layout/space_before_brackets.rb +1 -0
  37. data/lib/rubocop/cop/layout/space_before_comment.rb +1 -1
  38. data/lib/rubocop/cop/layout/space_inside_parens.rb +5 -5
  39. data/lib/rubocop/cop/layout/space_inside_reference_brackets.rb +1 -1
  40. data/lib/rubocop/cop/layout/trailing_whitespace.rb +24 -1
  41. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +107 -0
  42. data/lib/rubocop/cop/lint/ambiguous_range.rb +105 -0
  43. data/lib/rubocop/cop/lint/ambiguous_regexp_literal.rb +5 -2
  44. data/lib/rubocop/cop/lint/debugger.rb +2 -4
  45. data/lib/rubocop/cop/lint/duplicate_methods.rb +8 -5
  46. data/lib/rubocop/cop/lint/empty_in_pattern.rb +1 -1
  47. data/lib/rubocop/cop/lint/erb_new_arguments.rb +1 -1
  48. data/lib/rubocop/cop/lint/float_out_of_range.rb +1 -1
  49. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +67 -0
  50. data/lib/rubocop/cop/lint/number_conversion.rb +7 -1
  51. data/lib/rubocop/cop/lint/shadowed_argument.rb +1 -1
  52. data/lib/rubocop/cop/lint/unused_method_argument.rb +2 -3
  53. data/lib/rubocop/cop/lint/useless_times.rb +1 -1
  54. data/lib/rubocop/cop/metrics/perceived_complexity.rb +1 -1
  55. data/lib/rubocop/cop/metrics/utils/abc_size_calculator.rb +1 -1
  56. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  57. data/lib/rubocop/cop/mixin/annotation_comment.rb +57 -34
  58. data/lib/rubocop/cop/mixin/code_length.rb +1 -1
  59. data/lib/rubocop/cop/mixin/documentation_comment.rb +5 -2
  60. data/lib/rubocop/cop/mixin/frozen_string_literal.rb +19 -1
  61. data/lib/rubocop/cop/mixin/heredoc.rb +7 -0
  62. data/lib/rubocop/cop/mixin/multiline_expression_indentation.rb +2 -2
  63. data/lib/rubocop/cop/mixin/percent_array.rb +13 -7
  64. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +9 -1
  65. data/lib/rubocop/cop/mixin/require_library.rb +59 -0
  66. data/lib/rubocop/cop/mixin/space_before_punctuation.rb +1 -1
  67. data/lib/rubocop/cop/naming/ascii_identifiers.rb +0 -3
  68. data/lib/rubocop/cop/naming/constant_name.rb +1 -1
  69. data/lib/rubocop/cop/naming/inclusive_language.rb +27 -10
  70. data/lib/rubocop/cop/style/accessor_grouping.rb +2 -2
  71. data/lib/rubocop/cop/style/and_or.rb +4 -0
  72. data/lib/rubocop/cop/style/ascii_comments.rb +0 -3
  73. data/lib/rubocop/cop/style/block_delimiters.rb +39 -6
  74. data/lib/rubocop/cop/style/case_equality.rb +6 -9
  75. data/lib/rubocop/cop/style/collection_methods.rb +2 -1
  76. data/lib/rubocop/cop/style/comment_annotation.rb +25 -39
  77. data/lib/rubocop/cop/style/commented_keyword.rb +2 -1
  78. data/lib/rubocop/cop/style/conditional_assignment.rb +19 -5
  79. data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +1 -1
  80. data/lib/rubocop/cop/style/documentation.rb +23 -8
  81. data/lib/rubocop/cop/style/double_negation.rb +12 -1
  82. data/lib/rubocop/cop/style/empty_method.rb +1 -1
  83. data/lib/rubocop/cop/style/encoding.rb +26 -15
  84. data/lib/rubocop/cop/style/explicit_block_argument.rb +46 -11
  85. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +1 -1
  86. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +11 -0
  87. data/lib/rubocop/cop/style/hash_except.rb +4 -3
  88. data/lib/rubocop/cop/style/hash_transform_keys.rb +0 -3
  89. data/lib/rubocop/cop/style/identical_conditional_branches.rb +30 -5
  90. data/lib/rubocop/cop/style/if_with_boolean_literal_branches.rb +3 -2
  91. data/lib/rubocop/cop/style/lambda_call.rb +1 -1
  92. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +6 -6
  93. data/lib/rubocop/cop/style/method_def_parentheses.rb +10 -1
  94. data/lib/rubocop/cop/style/missing_else.rb +7 -0
  95. data/lib/rubocop/cop/style/mutable_constant.rb +68 -6
  96. data/lib/rubocop/cop/style/negated_if.rb +1 -1
  97. data/lib/rubocop/cop/style/negated_unless.rb +1 -1
  98. data/lib/rubocop/cop/style/non_nil_check.rb +2 -2
  99. data/lib/rubocop/cop/style/not.rb +2 -2
  100. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -1
  101. data/lib/rubocop/cop/style/percent_q_literals.rb +2 -2
  102. data/lib/rubocop/cop/style/raise_args.rb +1 -1
  103. data/lib/rubocop/cop/style/redundant_begin.rb +25 -0
  104. data/lib/rubocop/cop/style/redundant_condition.rb +2 -3
  105. data/lib/rubocop/cop/style/redundant_freeze.rb +4 -3
  106. data/lib/rubocop/cop/style/redundant_interpolation.rb +1 -1
  107. data/lib/rubocop/cop/style/redundant_percent_q.rb +2 -3
  108. data/lib/rubocop/cop/style/redundant_self_assignment_branch.rb +83 -0
  109. data/lib/rubocop/cop/style/redundant_sort.rb +19 -4
  110. data/lib/rubocop/cop/style/regexp_literal.rb +3 -3
  111. data/lib/rubocop/cop/style/return_nil.rb +2 -1
  112. data/lib/rubocop/cop/style/semicolon.rb +32 -24
  113. data/lib/rubocop/cop/style/single_line_block_params.rb +3 -1
  114. data/lib/rubocop/cop/style/sole_nested_conditional.rb +4 -0
  115. data/lib/rubocop/cop/style/special_global_vars.rb +21 -0
  116. data/lib/rubocop/cop/style/static_class.rb +1 -2
  117. data/lib/rubocop/cop/style/string_concatenation.rb +1 -1
  118. data/lib/rubocop/cop/style/struct_inheritance.rb +3 -0
  119. data/lib/rubocop/cop/style/symbol_array.rb +3 -3
  120. data/lib/rubocop/cop/style/trivial_accessors.rb +1 -1
  121. data/lib/rubocop/cop/style/word_array.rb +23 -5
  122. data/lib/rubocop/cop/style/yoda_condition.rb +4 -7
  123. data/lib/rubocop/cop/util.rb +7 -2
  124. data/lib/rubocop/magic_comment.rb +44 -15
  125. data/lib/rubocop/options.rb +1 -1
  126. data/lib/rubocop/result_cache.rb +1 -1
  127. data/lib/rubocop/runner.rb +1 -2
  128. data/lib/rubocop/version.rb +1 -1
  129. data/lib/rubocop.rb +9 -2
  130. metadata +14 -5
@@ -176,7 +176,7 @@ module RuboCop
176
176
  def ignored_line?(line, line_index)
177
177
  matches_ignored_pattern?(line) ||
178
178
  shebang?(line, line_index) ||
179
- heredocs && line_in_permitted_heredoc?(line_index.succ)
179
+ (heredocs && line_in_permitted_heredoc?(line_index.succ))
180
180
  end
181
181
 
182
182
  def shebang?(line, line_index)
@@ -89,7 +89,7 @@ module RuboCop
89
89
  if node.source.lines.first.end_with?("|\n")
90
90
  PIPE_SIZE
91
91
  else
92
- 1 + PIPE_SIZE * 2
92
+ 1 + (PIPE_SIZE * 2)
93
93
  end
94
94
  end
95
95
 
@@ -6,6 +6,9 @@ module RuboCop
6
6
  # This cop ensures that each argument in a multi-line method call
7
7
  # starts on a separate line.
8
8
  #
9
+ # NOTE: this cop does not move the first argument, if you want that to
10
+ # be on a separate line, see `Layout/FirstMethodArgumentLineBreak`.
11
+ #
9
12
  # @example
10
13
  #
11
14
  # # bad
@@ -29,8 +29,7 @@ module RuboCop
29
29
  MSG = '`%<kw_loc>s` at %<kw_loc_line>d, %<kw_loc_column>d is not ' \
30
30
  'aligned with `%<beginning>s` at ' \
31
31
  '%<begin_loc_line>d, %<begin_loc_column>d.'
32
- ANCESTOR_TYPES = %i[kwbegin def defs class module].freeze
33
- RUBY_2_5_ANCESTOR_TYPES = (ANCESTOR_TYPES + %i[block]).freeze
32
+ ANCESTOR_TYPES = %i[kwbegin def defs class module block].freeze
34
33
  ANCESTOR_TYPES_WITH_ACCESS_MODIFIERS = %i[def defs].freeze
35
34
  ALTERNATIVE_ACCESS_MODIFIERS = %i[public_class_method private_class_method].freeze
36
35
 
@@ -118,6 +117,8 @@ module RuboCop
118
117
  ancestor_node = ancestor_node(node)
119
118
 
120
119
  return ancestor_node if ancestor_node.nil? || ancestor_node.kwbegin_type?
120
+ return if ancestor_node.respond_to?(:send_node) &&
121
+ aligned_with_line_break_method?(ancestor_node, node)
121
122
 
122
123
  assignment_node = assignment_node(ancestor_node)
123
124
  return assignment_node if same_line?(ancestor_node, assignment_node)
@@ -129,14 +130,26 @@ module RuboCop
129
130
  end
130
131
 
131
132
  def ancestor_node(node)
132
- ancestor_types =
133
- if target_ruby_version >= 2.5
134
- RUBY_2_5_ANCESTOR_TYPES
135
- else
136
- ANCESTOR_TYPES
137
- end
133
+ node.each_ancestor(*ANCESTOR_TYPES).first
134
+ end
135
+
136
+ def aligned_with_line_break_method?(ancestor_node, node)
137
+ send_node_loc = ancestor_node.send_node.loc
138
+ do_keyword_line = ancestor_node.loc.begin.line
139
+ rescue_keyword_column = node.loc.keyword.column
140
+ selector = send_node_loc.respond_to?(:selector) ? send_node_loc.selector : send_node_loc
141
+
142
+ if aligned_with_leading_dot?(do_keyword_line, send_node_loc, rescue_keyword_column)
143
+ return true
144
+ end
145
+
146
+ do_keyword_line == selector.line && rescue_keyword_column == selector.column
147
+ end
148
+
149
+ def aligned_with_leading_dot?(do_keyword_line, send_node_loc, rescue_keyword_column)
150
+ return false unless send_node_loc.respond_to?(:dot) && (dot = send_node_loc.dot)
138
151
 
139
- node.each_ancestor(*ancestor_types).first
152
+ do_keyword_line == dot.line && rescue_keyword_column == dot.column
140
153
  end
141
154
 
142
155
  def assignment_node(node)
@@ -37,15 +37,26 @@ module RuboCop
37
37
  return unless receiver&.block_type?
38
38
 
39
39
  receiver_location = receiver.loc
40
- closing_block_delimiter_line_number = receiver_location.end.line
41
- return if receiver_location.begin.line < closing_block_delimiter_line_number
40
+ closing_block_delimiter_line_num = receiver_location.end.line
41
+ return if receiver_location.begin.line < closing_block_delimiter_line_num
42
42
 
43
43
  node_location = node.loc
44
44
  dot_range = node_location.dot
45
45
  return unless dot_range
46
- return if dot_range.line > closing_block_delimiter_line_number
46
+ return unless call_method_after_block?(node, dot_range, closing_block_delimiter_line_num)
47
47
 
48
- range_between(dot_range.begin_pos, node_location.selector.end_pos)
48
+ range_between(dot_range.begin_pos, selector_range(node).end_pos)
49
+ end
50
+
51
+ def call_method_after_block?(node, dot_range, closing_block_delimiter_line_num)
52
+ return false if dot_range.line > closing_block_delimiter_line_num
53
+
54
+ dot_range.column < selector_range(node).column
55
+ end
56
+
57
+ def selector_range(node)
58
+ # l.(1) has no selector, so we use the opening parenthesis instead
59
+ node.loc.selector || node.loc.begin
49
60
  end
50
61
  end
51
62
  end
@@ -16,6 +16,7 @@ module RuboCop
16
16
  extend AutoCorrector
17
17
 
18
18
  MSG = 'Do not leave space between `!` and its argument.'
19
+ RESTRICT_ON_SEND = %i[!].freeze
19
20
 
20
21
  def on_send(node)
21
22
  return unless node.prefix_bang? && whitespace_after_operator?(node)
@@ -47,7 +47,8 @@ module RuboCop
47
47
  space_on_both_sides = space_on_both_sides?(arg, equals)
48
48
  no_surrounding_space = no_surrounding_space?(arg, equals)
49
49
 
50
- if style == :space && space_on_both_sides || style == :no_space && no_surrounding_space
50
+ if (style == :space && space_on_both_sides) ||
51
+ (style == :no_space && no_surrounding_space)
51
52
  correct_style_detected
52
53
  else
53
54
  incorrect_style_detected(arg, value)
@@ -228,8 +228,8 @@ module RuboCop
228
228
  def accepted_opening_delimiter?(range, char)
229
229
  return true unless char
230
230
 
231
- accept_left_square_bracket?(range) && char == '[' ||
232
- accept_left_parenthesis?(range) && char == '('
231
+ (accept_left_square_bracket?(range) && char == '[') ||
232
+ (accept_left_parenthesis?(range) && char == '(')
233
233
  end
234
234
 
235
235
  def accept_left_parenthesis?(range)
@@ -108,6 +108,14 @@ module RuboCop
108
108
  check_operator(:assignment, node.loc.operator, rhs.source_range)
109
109
  end
110
110
 
111
+ def on_casgn(node)
112
+ _, _, right, = *node
113
+
114
+ return unless right
115
+
116
+ check_operator(:assignment, node.loc.operator, right.source_range)
117
+ end
118
+
111
119
  def on_binary(node)
112
120
  _, rhs, = *node
113
121
 
@@ -134,7 +142,6 @@ module RuboCop
134
142
  alias on_and on_binary
135
143
  alias on_lvasgn on_assignment
136
144
  alias on_masgn on_assignment
137
- alias on_casgn on_special_asgn
138
145
  alias on_ivasgn on_assignment
139
146
  alias on_cvasgn on_assignment
140
147
  alias on_gvasgn on_assignment
@@ -19,6 +19,7 @@ module RuboCop
19
19
  extend AutoCorrector
20
20
 
21
21
  MSG = 'Remove the space before the opening brackets.'
22
+ RESTRICT_ON_SEND = %i[[] []=].freeze
22
23
 
23
24
  def on_send(node)
24
25
  return unless (first_argument = node.first_argument)
@@ -18,7 +18,7 @@ module RuboCop
18
18
  MSG = 'Put a space before an end-of-line comment.'
19
19
 
20
20
  def on_new_investigation
21
- processed_source.tokens.each_cons(2) do |token1, token2|
21
+ processed_source.sorted_tokens.each_cons(2) do |token1, token2|
22
22
  next unless token2.comment?
23
23
  next unless token1.line == token2.line
24
24
  next unless token1.pos.end == token2.pos.begin
@@ -43,12 +43,12 @@ module RuboCop
43
43
  MSG_SPACE = 'No space inside parentheses detected.'
44
44
 
45
45
  def on_new_investigation
46
- @processed_source = processed_source
46
+ tokens = processed_source.sorted_tokens
47
47
 
48
48
  if style == :space
49
- process_with_space_style(processed_source)
49
+ process_with_space_style(tokens)
50
50
  else
51
- each_extraneous_space(processed_source.tokens) do |range|
51
+ each_extraneous_space(tokens) do |range|
52
52
  add_offense(range) do |corrector|
53
53
  corrector.remove(range)
54
54
  end
@@ -58,8 +58,8 @@ module RuboCop
58
58
 
59
59
  private
60
60
 
61
- def process_with_space_style(processed_source)
62
- processed_source.tokens.each_cons(2) do |token1, token2|
61
+ def process_with_space_style(tokens)
62
+ tokens.each_cons(2) do |token1, token2|
63
63
  each_extraneous_space_in_empty_parens(token1, token2) do |range|
64
64
  add_offense(range) do |corrector|
65
65
  corrector.remove(range)
@@ -107,7 +107,7 @@ module RuboCop
107
107
  current_token = tokens.reverse.find(&:left_ref_bracket?)
108
108
  previous_token = previous_token(current_token)
109
109
 
110
- if node.method?(:[]=) || previous_token && !previous_token.right_bracket?
110
+ if node.method?(:[]=) || (previous_token && !previous_token.right_bracket?)
111
111
  tokens.find(&:left_ref_bracket?)
112
112
  else
113
113
  current_token
@@ -41,6 +41,7 @@ module RuboCop
41
41
  #
42
42
  class TrailingWhitespace < Base
43
43
  include RangeHelp
44
+ include Heredoc
44
45
  extend AutoCorrector
45
46
 
46
47
  MSG = 'Trailing whitespace detected.'
@@ -54,6 +55,8 @@ module RuboCop
54
55
  end
55
56
  end
56
57
 
58
+ def on_heredoc(_node); end
59
+
57
60
  private
58
61
 
59
62
  def process_line(line, lineno)
@@ -63,13 +66,33 @@ module RuboCop
63
66
  range = offense_range(lineno, line)
64
67
  add_offense(range) do |corrector|
65
68
  if heredoc
66
- corrector.wrap(range, "\#{'", "'}") unless static?(heredoc)
69
+ process_line_in_heredoc(corrector, range, heredoc)
67
70
  else
68
71
  corrector.remove(range)
69
72
  end
70
73
  end
71
74
  end
72
75
 
76
+ def process_line_in_heredoc(corrector, range, heredoc)
77
+ indent_level = indent_level(find_heredoc(range.line).loc.heredoc_body.source)
78
+ whitespace_only = whitespace_only?(range)
79
+ if whitespace_only && whitespace_is_indentation?(range, indent_level)
80
+ corrector.remove(range)
81
+ elsif !static?(heredoc)
82
+ range = range_between(range.begin_pos + indent_level, range.end_pos) if whitespace_only
83
+ corrector.wrap(range, "\#{'", "'}")
84
+ end
85
+ end
86
+
87
+ def whitespace_is_indentation?(range, level)
88
+ range.source[/ +/].length <= level
89
+ end
90
+
91
+ def whitespace_only?(range)
92
+ source = range_with_surrounding_space(range: range).source
93
+ source.start_with?("\n") && source.end_with?("\n")
94
+ end
95
+
73
96
  def static?(heredoc)
74
97
  heredoc.loc.expression.source.end_with? "'"
75
98
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # This cop looks for expressions containing multiple binary operators
7
+ # where precedence is ambiguous due to lack of parentheses. For example,
8
+ # in `1 + 2 * 3`, the multiplication will happen before the addition, but
9
+ # lexically it appears that the addition will happen first.
10
+ #
11
+ # The cop does not consider unary operators (ie. `!a` or `-b`) or comparison
12
+ # operators (ie. `a =~ b`) because those are not ambiguous.
13
+ #
14
+ # NOTE: Ranges are handled by `Lint/AmbiguousRange`.
15
+ #
16
+ # @example
17
+ # # bad
18
+ # a + b * c
19
+ # a || b && c
20
+ # a ** b + c
21
+ #
22
+ # # good (different precedence)
23
+ # a + (b * c)
24
+ # a || (b && c)
25
+ # (a ** b) + c
26
+ #
27
+ # # good (same precedence)
28
+ # a + b + c
29
+ # a * b / c % d
30
+ class AmbiguousOperatorPrecedence < Base
31
+ extend AutoCorrector
32
+
33
+ # See https://ruby-doc.org/core-3.0.2/doc/syntax/precedence_rdoc.html
34
+ PRECEDENCE = [
35
+ %i[**],
36
+ %i[* / %],
37
+ %i[+ -],
38
+ %i[<< >>],
39
+ %i[&],
40
+ %i[| ^],
41
+ %i[&&],
42
+ %i[||]
43
+ ].freeze
44
+ RESTRICT_ON_SEND = PRECEDENCE.flatten.freeze
45
+ MSG = 'Wrap expressions with varying precedence with parentheses to avoid ambiguity.'
46
+
47
+ def on_new_investigation
48
+ # Cache the precedence of each node being investigated
49
+ # so that we only need to calculate it once
50
+ @node_precedences = {}
51
+ super
52
+ end
53
+
54
+ def on_and(node)
55
+ return unless (parent = node.parent)
56
+
57
+ return if parent.begin_type? # if the `and` is in a `begin`, it's parenthesized already
58
+ return unless parent.or_type?
59
+
60
+ add_offense(node) do |corrector|
61
+ autocorrect(corrector, node)
62
+ end
63
+ end
64
+
65
+ def on_send(node)
66
+ return if node.parenthesized?
67
+
68
+ return unless (parent = node.parent)
69
+ return unless operator?(parent)
70
+ return unless greater_precedence?(node, parent)
71
+
72
+ add_offense(node) do |corrector|
73
+ autocorrect(corrector, node)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def precedence(node)
80
+ @node_precedences.fetch(node) do
81
+ PRECEDENCE.index { |operators| operators.include?(operator_name(node)) }
82
+ end
83
+ end
84
+
85
+ def operator?(node)
86
+ (node.send_type? && RESTRICT_ON_SEND.include?(node.method_name)) || node.operator_keyword?
87
+ end
88
+
89
+ def greater_precedence?(node1, node2)
90
+ precedence(node2) > precedence(node1)
91
+ end
92
+
93
+ def operator_name(node)
94
+ if node.send_type?
95
+ node.method_name
96
+ else
97
+ node.operator.to_sym
98
+ end
99
+ end
100
+
101
+ def autocorrect(corrector, node)
102
+ corrector.wrap(node, '(', ')')
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # This cop checks for ambiguous ranges.
7
+ #
8
+ # Ranges have quite low precedence, which leads to unexpected behaviour when
9
+ # using a range with other operators. This cop avoids that by making ranges
10
+ # explicit by requiring parenthesis around complex range boundaries (anything
11
+ # that is not a basic literal: numerics, strings, symbols, etc.).
12
+ #
13
+ # NOTE: The cop auto-corrects by wrapping the entire boundary in parentheses, which
14
+ # makes the outcome more explicit but is possible to not be the intention of the
15
+ # programmer. For this reason, this cop's auto-correct is marked as unsafe (it
16
+ # will not change the behaviour of the code, but will not necessarily match the
17
+ # intent of the program).
18
+ #
19
+ # This cop can be configured with `RequireParenthesesForMethodChains` in order to
20
+ # specify whether method chains (including `self.foo`) should be wrapped in parens
21
+ # by this cop.
22
+ #
23
+ # NOTE: Regardless of this configuration, if a method receiver is a basic literal
24
+ # value, it will be wrapped in order to prevent the ambiguity of `1..2.to_a`.
25
+ #
26
+ # @example
27
+ # # bad
28
+ # x || 1..2
29
+ # (x || 1..2)
30
+ # 1..2.to_a
31
+ #
32
+ # # good, unambiguous
33
+ # 1..2
34
+ # 'a'..'z'
35
+ # :bar..:baz
36
+ # MyClass::MIN..MyClass::MAX
37
+ # @min..@max
38
+ # a..b
39
+ # -a..b
40
+ #
41
+ # # good, ambiguity removed
42
+ # x || (1..2)
43
+ # (x || 1)..2
44
+ # (x || 1)..(y || 2)
45
+ # (1..2).to_a
46
+ #
47
+ # @example RequireParenthesesForMethodChains: false (default)
48
+ # # good
49
+ # a.foo..b.bar
50
+ # (a.foo)..(b.bar)
51
+ #
52
+ # @example RequireParenthesesForMethodChains: true
53
+ # # bad
54
+ # a.foo..b.bar
55
+ #
56
+ # # good
57
+ # (a.foo)..(b.bar)
58
+ #
59
+ class AmbiguousRange < Base
60
+ extend AutoCorrector
61
+
62
+ MSG = 'Wrap complex range boundaries with parentheses to avoid ambiguity.'
63
+
64
+ def on_irange(node)
65
+ each_boundary(node) do |boundary|
66
+ next if acceptable?(boundary)
67
+
68
+ add_offense(boundary) do |corrector|
69
+ corrector.wrap(boundary, '(', ')')
70
+ end
71
+ end
72
+ end
73
+ alias on_erange on_irange
74
+
75
+ private
76
+
77
+ def each_boundary(range)
78
+ yield range.begin if range.begin
79
+ yield range.end if range.end
80
+ end
81
+
82
+ def acceptable?(node)
83
+ node.begin_type? ||
84
+ node.basic_literal? ||
85
+ node.variable? || node.const_type? ||
86
+ (node.call_type? && acceptable_call?(node))
87
+ end
88
+
89
+ def acceptable_call?(node)
90
+ return true if node.unary_operation?
91
+
92
+ # Require parentheses when making a method call on a literal
93
+ # to avoid the ambiguity of `1..2.to_a`.
94
+ return false if node.receiver&.basic_literal?
95
+
96
+ require_parentheses_for_method_chain? || node.receiver.nil?
97
+ end
98
+
99
+ def require_parentheses_for_method_chain?
100
+ !cop_config['RequireParenthesesForMethodChains']
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end