rubocop 1.41.1 → 1.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +2 -2
  4. data/config/default.yml +25 -1
  5. data/lib/rubocop/cli.rb +1 -1
  6. data/lib/rubocop/config.rb +7 -7
  7. data/lib/rubocop/config_loader_resolver.rb +5 -1
  8. data/lib/rubocop/cop/base.rb +62 -61
  9. data/lib/rubocop/cop/cop.rb +28 -28
  10. data/lib/rubocop/cop/corrector.rb +23 -11
  11. data/lib/rubocop/cop/gemspec/dependency_version.rb +16 -18
  12. data/lib/rubocop/cop/layout/class_structure.rb +32 -10
  13. data/lib/rubocop/cop/layout/comment_indentation.rb +3 -1
  14. data/lib/rubocop/cop/layout/indentation_style.rb +4 -1
  15. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +6 -6
  16. data/lib/rubocop/cop/layout/multiline_block_layout.rb +1 -1
  17. data/lib/rubocop/cop/layout/space_around_keyword.rb +1 -1
  18. data/lib/rubocop/cop/layout/trailing_whitespace.rb +5 -2
  19. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +19 -0
  20. data/lib/rubocop/cop/lint/regexp_as_condition.rb +6 -0
  21. data/lib/rubocop/cop/lint/require_parentheses.rb +3 -1
  22. data/lib/rubocop/cop/lint/unused_method_argument.rb +2 -1
  23. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +5 -3
  24. data/lib/rubocop/cop/metrics/perceived_complexity.rb +1 -1
  25. data/lib/rubocop/cop/metrics/utils/abc_size_calculator.rb +4 -4
  26. data/lib/rubocop/cop/mixin/annotation_comment.rb +1 -1
  27. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +1 -1
  28. data/lib/rubocop/cop/registry.rb +22 -22
  29. data/lib/rubocop/cop/security/compound_hash.rb +2 -1
  30. data/lib/rubocop/cop/style/block_comments.rb +1 -1
  31. data/lib/rubocop/cop/style/concat_array_literals.rb +22 -2
  32. data/lib/rubocop/cop/style/guard_clause.rb +11 -7
  33. data/lib/rubocop/cop/style/hash_syntax.rb +10 -7
  34. data/lib/rubocop/cop/style/identical_conditional_branches.rb +15 -0
  35. data/lib/rubocop/cop/style/map_to_set.rb +61 -0
  36. data/lib/rubocop/cop/style/method_def_parentheses.rb +11 -4
  37. data/lib/rubocop/cop/style/min_max_comparison.rb +73 -0
  38. data/lib/rubocop/cop/style/redundant_string_escape.rb +4 -2
  39. data/lib/rubocop/cop/style/require_order.rb +4 -2
  40. data/lib/rubocop/cop/style/select_by_regexp.rb +6 -2
  41. data/lib/rubocop/cop/style/signal_exception.rb +8 -6
  42. data/lib/rubocop/cop/style/trailing_comma_in_arguments.rb +4 -4
  43. data/lib/rubocop/cop/style/word_array.rb +41 -0
  44. data/lib/rubocop/cop/style/yoda_expression.rb +74 -0
  45. data/lib/rubocop/cop/style/zero_length_predicate.rb +31 -14
  46. data/lib/rubocop/cop/team.rb +29 -29
  47. data/lib/rubocop/cop/variable_force.rb +0 -3
  48. data/lib/rubocop/path_util.rb +6 -1
  49. data/lib/rubocop/result_cache.rb +1 -1
  50. data/lib/rubocop/runner.rb +10 -3
  51. data/lib/rubocop/target_ruby.rb +0 -1
  52. data/lib/rubocop/version.rb +1 -1
  53. data/lib/rubocop.rb +3 -0
  54. metadata +12 -9
@@ -47,7 +47,6 @@ module RuboCop
47
47
  MSG = 'Trailing whitespace detected.'
48
48
 
49
49
  def on_new_investigation
50
- @heredocs = extract_heredocs(processed_source.ast)
51
50
  processed_source.lines.each_with_index do |line, index|
52
51
  next unless line.end_with?(' ', "\t")
53
52
 
@@ -102,10 +101,14 @@ module RuboCop
102
101
  end
103
102
 
104
103
  def find_heredoc(line_number)
105
- @heredocs.each { |node, r| return node if r.include?(line_number) }
104
+ heredocs.each { |node, r| return node if r.include?(line_number) }
106
105
  nil
107
106
  end
108
107
 
108
+ def heredocs
109
+ @heredocs ||= extract_heredocs(processed_source.ast)
110
+ end
111
+
109
112
  def extract_heredocs(ast)
110
113
  return [] unless ast
111
114
 
@@ -68,6 +68,12 @@ module RuboCop
68
68
  @valid_ref = regexp_conditions.map { |condition| check_regexp(condition) }.compact.max
69
69
  end
70
70
 
71
+ def on_in_pattern(node)
72
+ regexp_patterns = patterns(node).select(&:regexp_type?)
73
+
74
+ @valid_ref = regexp_patterns.map { |pattern| check_regexp(pattern) }.compact.max
75
+ end
76
+
71
77
  def on_nth_ref(node)
72
78
  backref, = *node
73
79
  return if @valid_ref.nil? || backref <= @valid_ref
@@ -84,6 +90,19 @@ module RuboCop
84
90
 
85
91
  private
86
92
 
93
+ def patterns(pattern_node)
94
+ pattern = pattern_node.node_parts[0]
95
+
96
+ case pattern.type
97
+ when :array_pattern, :match_alt
98
+ pattern.children
99
+ when :match_as
100
+ patterns(pattern)
101
+ else
102
+ [pattern]
103
+ end
104
+ end
105
+
87
106
  def check_regexp(node)
88
107
  return if node.interpolation?
89
108
 
@@ -17,13 +17,19 @@ module RuboCop
17
17
  # do_something
18
18
  # end
19
19
  class RegexpAsCondition < Base
20
+ include IgnoredNode
20
21
  extend AutoCorrector
21
22
 
22
23
  MSG = 'Do not use regexp literal as a condition. ' \
23
24
  'The regexp literal matches `$_` implicitly.'
24
25
 
25
26
  def on_match_current_line(node)
27
+ return if node.ancestors.none?(&:conditional?)
28
+ return if part_of_ignored_node?(node)
29
+
26
30
  add_offense(node) { |corrector| corrector.replace(node, "#{node.source} =~ $_") }
31
+
32
+ ignore_node(node)
27
33
  end
28
34
  end
29
35
  end
@@ -46,7 +46,9 @@ module RuboCop
46
46
  private
47
47
 
48
48
  def check_ternary(ternary, node)
49
- return if node.method?(:[]) || !ternary.condition.operator_keyword?
49
+ if node.method?(:[]) || node.assignment_method? || !ternary.condition.operator_keyword?
50
+ return
51
+ end
50
52
 
51
53
  range = range_between(node.source_range.begin_pos, ternary.condition.source_range.end_pos)
52
54
 
@@ -100,7 +100,8 @@ module RuboCop
100
100
 
101
101
  unless variable.keyword_argument?
102
102
  message << " If it's necessary, use `_` or `_#{variable.name}` " \
103
- "as an argument name to indicate that it won't be used."
103
+ "as an argument name to indicate that it won't be used. " \
104
+ "If it's unnecessary, remove it."
104
105
  end
105
106
 
106
107
  scope = variable.scope
@@ -77,10 +77,12 @@ module RuboCop
77
77
  PATTERN
78
78
 
79
79
  def on_send(node)
80
- if node.first_argument.def_type?
81
- inspect_def(node, node.first_argument)
80
+ return unless (first_argument = node.first_argument)
81
+
82
+ if first_argument.def_type?
83
+ inspect_def(node, first_argument)
82
84
  elsif node.first_argument.sym_type?
83
- inspect_sym(node, node.first_argument)
85
+ inspect_sym(node, first_argument)
84
86
  end
85
87
  end
86
88
 
@@ -45,7 +45,7 @@ module RuboCop
45
45
  else
46
46
  # Otherwise, the case node gets 0.8 complexity points and each
47
47
  # when gets 0.2.
48
- (0.8 + (0.2 * nb_branches)).round
48
+ ((nb_branches * 0.2) + 0.8).round
49
49
  end
50
50
  when :if
51
51
  node.else? && !node.elsif? ? 2 : 1
@@ -25,15 +25,15 @@ module RuboCop
25
25
  # > http://c2.com/cgi/wiki?AbcMetric
26
26
  CONDITION_NODES = CyclomaticComplexity::COUNTED_NODES.freeze
27
27
 
28
- def self.calculate(node, discount_repeated_attributes: false)
29
- new(node, discount_repeated_attributes: discount_repeated_attributes).calculate
30
- end
31
-
32
28
  # TODO: move to rubocop-ast
33
29
  ARGUMENT_TYPES = %i[arg optarg restarg kwarg kwoptarg kwrestarg blockarg].freeze
34
30
 
35
31
  private_constant :BRANCH_NODES, :CONDITION_NODES, :ARGUMENT_TYPES
36
32
 
33
+ def self.calculate(node, discount_repeated_attributes: false)
34
+ new(node, discount_repeated_attributes: discount_repeated_attributes).calculate
35
+ end
36
+
37
37
  def initialize(node)
38
38
  @assignment = 0
39
39
  @branch = 0
@@ -47,7 +47,7 @@ module RuboCop
47
47
  match.captures
48
48
  end
49
49
 
50
- KEYWORDS_REGEX_CACHE = {} # rubocop:disable Layout/ClassStructure, Style/MutableConstant
50
+ KEYWORDS_REGEX_CACHE = {} # rubocop:disable Style/MutableConstant
51
51
  private_constant :KEYWORDS_REGEX_CACHE
52
52
 
53
53
  def regex
@@ -175,7 +175,7 @@ module RuboCop
175
175
 
176
176
  def remove_optarg_equals(asgn_tokens, processed_source)
177
177
  optargs = processed_source.ast.each_node(:optarg)
178
- optarg_eql = optargs.map { |o| o.loc.operator.begin_pos }.to_set
178
+ optarg_eql = optargs.to_set { |o| o.loc.operator.begin_pos }
179
179
  asgn_tokens.reject { |t| optarg_eql.include?(t.begin_pos) }
180
180
  end
181
181
  end
@@ -19,6 +19,28 @@ module RuboCop
19
19
  class Registry
20
20
  include Enumerable
21
21
 
22
+ def self.all
23
+ global.without_department(:Test).cops
24
+ end
25
+
26
+ def self.qualified_cop_name(name, origin)
27
+ global.qualified_cop_name(name, origin)
28
+ end
29
+
30
+ # Changes momentarily the global registry
31
+ # Intended for testing purposes
32
+ def self.with_temporary_global(temp_global = global.dup)
33
+ previous = @global
34
+ @global = temp_global
35
+ yield
36
+ ensure
37
+ @global = previous
38
+ end
39
+
40
+ def self.reset!
41
+ @global = new
42
+ end
43
+
22
44
  attr_reader :options
23
45
 
24
46
  def initialize(cops = [], options = {})
@@ -238,28 +260,6 @@ module RuboCop
238
260
  attr_reader :global
239
261
  end
240
262
 
241
- def self.all
242
- global.without_department(:Test).cops
243
- end
244
-
245
- def self.qualified_cop_name(name, origin)
246
- global.qualified_cop_name(name, origin)
247
- end
248
-
249
- # Changes momentarily the global registry
250
- # Intended for testing purposes
251
- def self.with_temporary_global(temp_global = global.dup)
252
- previous = @global
253
- @global = temp_global
254
- yield
255
- ensure
256
- @global = previous
257
- end
258
-
259
- def self.reset!
260
- @global = new
261
- end
262
-
263
263
  private
264
264
 
265
265
  def initialize_copy(reg)
@@ -9,7 +9,8 @@ module RuboCop
9
9
  # Manually combining hashes is error prone and hard to follow, especially
10
10
  # when there are many values. Poor implementations may also introduce
11
11
  # performance or security concerns if they are prone to collisions.
12
- # Delegating to `Array#hash` is clearer, faster, and safer.
12
+ # Delegating to `Array#hash` is clearer and safer, although it might be slower
13
+ # depending on the use case.
13
14
  #
14
15
  # @safety
15
16
  # This cop may be unsafe if the application logic depends on the hash
@@ -32,7 +32,7 @@ module RuboCop
32
32
  eq_begin, eq_end, contents = parts(comment)
33
33
 
34
34
  corrector.remove(eq_begin)
35
- unless contents.length.zero?
35
+ unless contents.empty?
36
36
  corrector.replace(
37
37
  contents,
38
38
  contents.source.gsub(/\A/, '# ').gsub(/\n\n/, "\n#\n").gsub(/\n(?=[^#])/, "\n# ")
@@ -30,6 +30,7 @@ module RuboCop
30
30
  'Use `push` with elements as arguments without array brackets instead of `%<current>s`.'
31
31
  RESTRICT_ON_SEND = %i[concat].freeze
32
32
 
33
+ # rubocop:disable Metrics
33
34
  def on_send(node)
34
35
  return if node.arguments.empty?
35
36
  return unless node.arguments.all?(&:array_type?)
@@ -38,7 +39,12 @@ module RuboCop
38
39
  current = offense.source
39
40
 
40
41
  if node.arguments.any?(&:percent_literal?)
41
- message = format(MSG_FOR_PERCENT_LITERALS, current: current)
42
+ if percent_literals_includes_only_basic_literals?(node)
43
+ prefer = preferred_method(node)
44
+ message = format(MSG, prefer: prefer, current: current)
45
+ else
46
+ message = format(MSG_FOR_PERCENT_LITERALS, current: current)
47
+ end
42
48
  else
43
49
  prefer = preferred_method(node)
44
50
  message = format(MSG, prefer: prefer, current: current)
@@ -48,6 +54,7 @@ module RuboCop
48
54
  corrector.replace(offense, prefer)
49
55
  end
50
56
  end
57
+ # rubocop:enable Metrics
51
58
 
52
59
  private
53
60
 
@@ -56,10 +63,23 @@ module RuboCop
56
63
  end
57
64
 
58
65
  def preferred_method(node)
59
- new_arguments = node.arguments.map { |arg| arg.children.map(&:source) }.join(', ')
66
+ new_arguments =
67
+ node.arguments.map do |arg|
68
+ if arg.percent_literal?
69
+ arg.children.map(&:value).map(&:inspect)
70
+ else
71
+ arg.children.map(&:source)
72
+ end
73
+ end.join(', ')
60
74
 
61
75
  "push(#{new_arguments})"
62
76
  end
77
+
78
+ def percent_literals_includes_only_basic_literals?(node)
79
+ node.arguments.select(&:percent_literal?).all? do |arg|
80
+ arg.children.all? { |child| child.str_type? || child.sym_type? }
81
+ end
82
+ end
63
83
  end
64
84
  end
65
85
  end
@@ -196,7 +196,7 @@ module RuboCop
196
196
  return unless node.else?
197
197
 
198
198
  corrector.remove(node.loc.else)
199
- corrector.remove(branch_to_remove(node, guard))
199
+ corrector.remove(range_of_branch_to_remove(node, guard))
200
200
  end
201
201
  end
202
202
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -206,20 +206,24 @@ module RuboCop
206
206
  end
207
207
 
208
208
  def autocorrect_heredoc_argument(corrector, node, heredoc_branch, leave_branch, guard)
209
+ return unless node.else?
210
+
209
211
  remove_whole_lines(corrector, leave_branch.source_range)
210
212
  remove_whole_lines(corrector, node.loc.else)
211
213
  remove_whole_lines(corrector, node.loc.end)
212
- remove_whole_lines(corrector, branch_to_remove(node, guard).source_range)
214
+ remove_whole_lines(corrector, range_of_branch_to_remove(node, guard))
213
215
  corrector.insert_after(
214
216
  heredoc_branch.last_argument.loc.heredoc_end, "\n#{leave_branch.source}"
215
217
  )
216
218
  end
217
219
 
218
- def branch_to_remove(node, guard)
219
- case guard
220
- when :if then node.if_branch
221
- when :else then node.else_branch
222
- end
220
+ def range_of_branch_to_remove(node, guard)
221
+ branch = case guard
222
+ when :if then node.if_branch
223
+ when :else then node.else_branch
224
+ end
225
+
226
+ branch.source_range
223
227
  end
224
228
 
225
229
  def guard_clause_source(guard_clause)
@@ -28,7 +28,7 @@ module RuboCop
28
28
  # * always - forces use of the 3.1 syntax (e.g. {foo:})
29
29
  # * never - forces use of explicit hash literal value
30
30
  # * either - accepts both shorthand and explicit use of hash literal value
31
- # * consistent - like "either", but will avoid mixing styles in a single hash
31
+ # * consistent - forces use of the 3.1 syntax only if all values can be omitted in the hash
32
32
  #
33
33
  # @example EnforcedStyle: ruby19 (default)
34
34
  # # bad
@@ -92,16 +92,19 @@ module RuboCop
92
92
  #
93
93
  # @example EnforcedShorthandSyntax: consistent
94
94
  #
95
- # # bad
96
- # {foo: , bar: bar}
95
+ # # bad - `foo` and `bar` values can be omitted
96
+ # {foo: foo, bar: bar}
97
97
  #
98
- # # good
99
- # {foo:, bar:}
98
+ # # bad - `bar` value can be omitted
99
+ # {foo:, bar: bar}
100
100
  #
101
- # # bad
102
- # {foo: , bar: baz}
101
+ # # bad - mixed syntaxes
102
+ # {foo:, bar: baz}
103
103
  #
104
104
  # # good
105
+ # {foo:, bar:}
106
+ #
107
+ # # good - can't omit `baz`
105
108
  # {foo: foo, bar: baz}
106
109
  #
107
110
  class HashSyntax < Base
@@ -136,6 +136,7 @@ module RuboCop
136
136
 
137
137
  private
138
138
 
139
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
139
140
  def check_branches(node, branches)
140
141
  # return if any branch is empty. An empty branch can be an `if`
141
142
  # without an `else` or a branch that contains only comments.
@@ -144,9 +145,13 @@ module RuboCop
144
145
  tails = branches.map { |branch| tail(branch) }
145
146
  check_expressions(node, tails, :after_condition) if duplicated_expressions?(node, tails)
146
147
 
148
+ return if last_child_of_parent?(node) &&
149
+ branches.any? { |branch| single_child_branch?(branch) }
150
+
147
151
  heads = branches.map { |branch| head(branch) }
148
152
  check_expressions(node, heads, :before_condition) if duplicated_expressions?(node, heads)
149
153
  end
154
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
150
155
 
151
156
  def duplicated_expressions?(node, expressions)
152
157
  unique_expressions = expressions.uniq
@@ -180,6 +185,16 @@ module RuboCop
180
185
  end
181
186
  end
182
187
 
188
+ def last_child_of_parent?(node)
189
+ return true unless (parent = node.parent)
190
+
191
+ parent.child_nodes.last == node
192
+ end
193
+
194
+ def single_child_branch?(branch_node)
195
+ !branch_node.begin_type? || branch_node.children.size == 1
196
+ end
197
+
183
198
  def message(node)
184
199
  format(MSG, source: node.source)
185
200
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Looks for uses of `map.to_set` or `collect.to_set` that could be
7
+ # written with just `to_set`.
8
+ #
9
+ # @safety
10
+ # This cop is unsafe, as it can produce false positives if the receiver
11
+ # is not an `Enumerable`.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # something.map { |i| i * 2 }.to_set
16
+ #
17
+ # # good
18
+ # something.to_set { |i| i * 2 }
19
+ #
20
+ # # bad
21
+ # [1, 2, 3].collect { |i| i.to_s }.to_set
22
+ #
23
+ # # good
24
+ # [1, 2, 3].to_set { |i| i.to_s }
25
+ #
26
+ class MapToSet < Base
27
+ extend AutoCorrector
28
+ include RangeHelp
29
+
30
+ MSG = 'Pass a block to `to_set` instead of calling `%<method>s.to_set`.'
31
+ RESTRICT_ON_SEND = %i[to_set].freeze
32
+
33
+ # @!method map_to_set?(node)
34
+ def_node_matcher :map_to_set?, <<~PATTERN
35
+ $(send (block $(send _ {:map :collect}) ...) :to_set)
36
+ PATTERN
37
+
38
+ def on_send(node)
39
+ return unless (to_set_node, map_node = map_to_set?(node))
40
+
41
+ message = format(MSG, method: map_node.loc.selector.source)
42
+ add_offense(map_node.loc.selector, message: message) do |corrector|
43
+ # If the `to_set` call already has a block, do not autocorrect.
44
+ next if to_set_node.block_node
45
+
46
+ autocorrect(corrector, to_set_node, map_node)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def autocorrect(corrector, to_set, map)
53
+ removal_range = range_between(to_set.loc.dot.begin_pos, to_set.loc.selector.end_pos)
54
+
55
+ corrector.remove(range_with_surrounding_space(removal_range, side: :left))
56
+ corrector.replace(map.loc.selector, 'to_set')
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -10,7 +10,9 @@ module RuboCop
10
10
  #
11
11
  # 1. Endless methods
12
12
  # 2. Argument lists containing a `forward-arg` (`...`)
13
- # 3. Argument lists containing an anonymous block forwarding (`&`)
13
+ # 3. Argument lists containing an anonymous rest arguments forwarding (`*`)
14
+ # 4. Argument lists containing an anonymous keyword rest arguments forwarding (`**`)
15
+ # 5. Argument lists containing an anonymous block forwarding (`&`)
14
16
  #
15
17
  # Removing the parens would be a syntax error here.
16
18
  #
@@ -130,9 +132,11 @@ module RuboCop
130
132
  # Regardless of style, parentheses are necessary for:
131
133
  # 1. Endless methods
132
134
  # 2. Argument lists containing a `forward-arg` (`...`)
133
- # 3. Argument lists containing an anonymous block forwarding (`&`)
135
+ # 3. Argument lists containing an anonymous rest arguments forwarding (`*`)
136
+ # 4. Argument lists containing an anonymous keyword rest arguments forwarding (`**`)
137
+ # 5. Argument lists containing an anonymous block forwarding (`&`)
134
138
  # Removing the parens would be a syntax error here.
135
- node.endless? || node.arguments.any?(&:forward_arg_type?) || anonymous_block_arg?(node)
139
+ node.endless? || anonymous_arguments?(node)
136
140
  end
137
141
 
138
142
  def require_parentheses?(args)
@@ -162,7 +166,10 @@ module RuboCop
162
166
  end
163
167
  end
164
168
 
165
- def anonymous_block_arg?(node)
169
+ def anonymous_arguments?(node)
170
+ return true if node.arguments.any? do |arg|
171
+ arg.forward_arg_type? || arg.restarg_type? || arg.kwrestarg_type?
172
+ end
166
173
  return false unless (last_argument = node.arguments.last)
167
174
 
168
175
  last_argument.blockarg_type? && last_argument.name.nil?
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Enforces the use of `max` or `min` instead of comparison for greater or less.
7
+ #
8
+ # NOTE: It can be used if you want to present limit or threshold in Ruby 2.7+.
9
+ # That it is slow though. So autocorrection will apply generic `max` or `min`:
10
+ #
11
+ # [source,ruby]
12
+ # ----
13
+ # a.clamp(b..) # Same as `[a, b].max`
14
+ # a.clamp(..b) # Same as `[a, b].min`
15
+ # ----
16
+ #
17
+ # @safety
18
+ # This cop is unsafe because even if a value has `<` or `>` method,
19
+ # it is not necessarily `Comparable`.
20
+ #
21
+ # @example
22
+ #
23
+ # # bad
24
+ # a > b ? a : b
25
+ # a >= b ? a : b
26
+ #
27
+ # # good
28
+ # [a, b].max
29
+ #
30
+ # # bad
31
+ # a < b ? a : b
32
+ # a <= b ? a : b
33
+ #
34
+ # # good
35
+ # [a, b].min
36
+ #
37
+ class MinMaxComparison < Base
38
+ extend AutoCorrector
39
+
40
+ MSG = 'Use `%<prefer>s` instead.'
41
+ GRATER_OPERATORS = %i[> >=].freeze
42
+ LESS_OPERATORS = %i[< <=].freeze
43
+ COMPARISON_OPERATORS = GRATER_OPERATORS + LESS_OPERATORS
44
+
45
+ def on_if(node)
46
+ lhs, operator, rhs = *node.condition
47
+ return unless COMPARISON_OPERATORS.include?(operator)
48
+
49
+ if_branch = node.if_branch
50
+ else_branch = node.else_branch
51
+ preferred_method = preferred_method(operator, lhs, rhs, if_branch, else_branch)
52
+ return unless preferred_method
53
+
54
+ replacement = "[#{lhs.source}, #{rhs.source}].#{preferred_method}"
55
+
56
+ add_offense(node, message: format(MSG, prefer: replacement)) do |corrector|
57
+ corrector.replace(node, replacement)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def preferred_method(operator, lhs, rhs, if_branch, else_branch)
64
+ if lhs == if_branch && rhs == else_branch
65
+ GRATER_OPERATORS.include?(operator) ? 'max' : 'min'
66
+ elsif lhs == else_branch && rhs == if_branch
67
+ LESS_OPERATORS.include?(operator) ? 'max' : 'min'
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -58,7 +58,7 @@ module RuboCop
58
58
  private
59
59
 
60
60
  def message(range)
61
- format(MSG, char: range.source.chars.last)
61
+ format(MSG, char: range.source[-1])
62
62
  end
63
63
 
64
64
  def str_contents_range(node)
@@ -76,6 +76,7 @@ module RuboCop
76
76
  node.loc.to_hash.key?(:begin) && !node.loc.begin.nil?
77
77
  end
78
78
 
79
+ # rubocop:disable Metrics/CyclomaticComplexity
79
80
  def allowed_escape?(node, range)
80
81
  escaped = range.source[(1..-1)]
81
82
 
@@ -88,13 +89,14 @@ module RuboCop
88
89
  # with different versions of Ruby so that e.g. /\d/ != /d/
89
90
  return true if /[\n\\[[:alnum:]]]/.match?(escaped[0])
90
91
 
91
- return true if escaped[0] == ' ' && percent_array_literal?(node)
92
+ return true if escaped[0] == ' ' && (percent_array_literal?(node) || node.heredoc?)
92
93
 
93
94
  return true if disabling_interpolation?(range)
94
95
  return true if delimiter?(node, escaped[0])
95
96
 
96
97
  false
97
98
  end
99
+ # rubocop:enable Metrics/CyclomaticComplexity
98
100
 
99
101
  def interpolation_not_enabled?(node)
100
102
  single_quoted?(node) ||
@@ -81,7 +81,7 @@ module RuboCop
81
81
  PATTERN
82
82
 
83
83
  def on_send(node)
84
- return unless node.arguments?
84
+ return unless node.parent && node.arguments?
85
85
  return if not_modifier_form?(node.parent)
86
86
 
87
87
  previous_older_sibling = find_previous_older_sibling(node)
@@ -102,8 +102,10 @@ module RuboCop
102
102
  node.if_type? && !node.modifier_form?
103
103
  end
104
104
 
105
- def find_previous_older_sibling(node) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
105
+ def find_previous_older_sibling(node) # rubocop:disable Metrics
106
106
  search_node(node).left_siblings.reverse.find do |sibling|
107
+ next unless sibling.is_a?(AST::Node)
108
+
107
109
  sibling = sibling_node(sibling)
108
110
  break unless sibling&.send_type? && sibling&.method?(node.method_name)
109
111
  break unless sibling.arguments? && !sibling.receiver
@@ -86,12 +86,12 @@ module RuboCop
86
86
 
87
87
  def on_send(node)
88
88
  return unless (block_node = node.block_node)
89
- return if block_node.body.begin_type?
89
+ return if block_node.body&.begin_type?
90
90
  return if receiver_allowed?(block_node.receiver)
91
91
  return unless (regexp_method_send_node = extract_send_node(block_node))
92
92
  return if match_predicate_without_receiver?(regexp_method_send_node)
93
93
 
94
- opposite = regexp_method_send_node.send_type? && regexp_method_send_node.method?(:!~)
94
+ opposite = opposite?(regexp_method_send_node)
95
95
  regexp = find_regexp(regexp_method_send_node, block_node)
96
96
 
97
97
  register_offense(node, block_node, regexp, opposite)
@@ -128,6 +128,10 @@ module RuboCop
128
128
  regexp_method_send_node
129
129
  end
130
130
 
131
+ def opposite?(regexp_method_send_node)
132
+ regexp_method_send_node.send_type? && regexp_method_send_node.method?(:!~)
133
+ end
134
+
131
135
  def find_regexp(node, block)
132
136
  return node.child_nodes.first if node.match_with_lvasgn_type?
133
137