rubocop 1.60.2 → 1.63.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/assets/output.css.erb +159 -0
  4. data/assets/output.html.erb +1 -160
  5. data/config/default.yml +64 -15
  6. data/lib/rubocop/cli/command/auto_generate_config.rb +12 -3
  7. data/lib/rubocop/cli/command/lsp.rb +2 -2
  8. data/lib/rubocop/cli.rb +6 -1
  9. data/lib/rubocop/config.rb +37 -10
  10. data/lib/rubocop/config_finder.rb +12 -2
  11. data/lib/rubocop/config_obsoletion.rb +1 -1
  12. data/lib/rubocop/config_validator.rb +14 -5
  13. data/lib/rubocop/cop/autocorrect_logic.rb +6 -1
  14. data/lib/rubocop/cop/base.rb +52 -6
  15. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +4 -8
  16. data/lib/rubocop/cop/correctors/for_to_each_corrector.rb +5 -13
  17. data/lib/rubocop/cop/gemspec/required_ruby_version.rb +5 -1
  18. data/lib/rubocop/cop/internal_affairs/example_description.rb +1 -0
  19. data/lib/rubocop/cop/internal_affairs/method_name_end_with.rb +8 -6
  20. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +122 -28
  21. data/lib/rubocop/cop/internal_affairs/redundant_expect_offense_arguments.rb +34 -0
  22. data/lib/rubocop/cop/internal_affairs.rb +1 -0
  23. data/lib/rubocop/cop/layout/empty_line_after_magic_comment.rb +14 -7
  24. data/lib/rubocop/cop/layout/end_alignment.rb +3 -1
  25. data/lib/rubocop/cop/layout/redundant_line_break.rb +11 -3
  26. data/lib/rubocop/cop/layout/space_before_block_braces.rb +19 -10
  27. data/lib/rubocop/cop/layout/space_inside_hash_literal_braces.rb +1 -1
  28. data/lib/rubocop/cop/lint/assignment_in_condition.rb +2 -4
  29. data/lib/rubocop/cop/lint/debugger.rb +27 -2
  30. data/lib/rubocop/cop/lint/empty_conditional_body.rb +1 -1
  31. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +14 -9
  32. data/lib/rubocop/cop/lint/redundant_with_index.rb +4 -0
  33. data/lib/rubocop/cop/lint/rescue_type.rb +1 -3
  34. data/lib/rubocop/cop/lint/script_permission.rb +3 -3
  35. data/lib/rubocop/cop/lint/syntax.rb +1 -1
  36. data/lib/rubocop/cop/lint/to_enum_arguments.rb +7 -2
  37. data/lib/rubocop/cop/lint/useless_times.rb +1 -1
  38. data/lib/rubocop/cop/lint/void.rb +6 -1
  39. data/lib/rubocop/cop/mixin/code_length.rb +12 -1
  40. data/lib/rubocop/cop/mixin/method_complexity.rb +15 -6
  41. data/lib/rubocop/cop/mixin/multiline_expression_indentation.rb +1 -1
  42. data/lib/rubocop/cop/mixin/safe_assignment.rb +1 -1
  43. data/lib/rubocop/cop/naming/block_forwarding.rb +31 -12
  44. data/lib/rubocop/cop/naming/file_name.rb +2 -2
  45. data/lib/rubocop/cop/naming/inclusive_language.rb +1 -2
  46. data/lib/rubocop/cop/naming/predicate_name.rb +2 -2
  47. data/lib/rubocop/cop/registry.rb +1 -1
  48. data/lib/rubocop/cop/style/alias.rb +1 -0
  49. data/lib/rubocop/cop/style/arguments_forwarding.rb +29 -8
  50. data/lib/rubocop/cop/style/case_like_if.rb +1 -1
  51. data/lib/rubocop/cop/style/class_vars.rb +3 -3
  52. data/lib/rubocop/cop/style/collection_compact.rb +3 -3
  53. data/lib/rubocop/cop/style/commented_keyword.rb +5 -2
  54. data/lib/rubocop/cop/style/conditional_assignment.rb +4 -5
  55. data/lib/rubocop/cop/style/copyright.rb +16 -11
  56. data/lib/rubocop/cop/style/eval_with_location.rb +2 -0
  57. data/lib/rubocop/cop/style/exact_regexp_match.rb +2 -1
  58. data/lib/rubocop/cop/style/for.rb +2 -0
  59. data/lib/rubocop/cop/style/format_string.rb +9 -9
  60. data/lib/rubocop/cop/style/hash_each_methods.rb +1 -1
  61. data/lib/rubocop/cop/style/hash_syntax.rb +6 -2
  62. data/lib/rubocop/cop/style/inverse_methods.rb +8 -8
  63. data/lib/rubocop/cop/style/invertible_unless_condition.rb +10 -5
  64. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +5 -8
  65. data/lib/rubocop/cop/style/map_into_array.rb +175 -0
  66. data/lib/rubocop/cop/style/map_to_hash.rb +1 -1
  67. data/lib/rubocop/cop/style/map_to_set.rb +1 -1
  68. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +1 -1
  69. data/lib/rubocop/cop/style/multiline_method_signature.rb +10 -1
  70. data/lib/rubocop/cop/style/multiline_ternary_operator.rb +4 -0
  71. data/lib/rubocop/cop/style/nil_comparison.rb +2 -0
  72. data/lib/rubocop/cop/style/object_then.rb +5 -3
  73. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -3
  74. data/lib/rubocop/cop/style/raise_args.rb +4 -1
  75. data/lib/rubocop/cop/style/redundant_argument.rb +25 -2
  76. data/lib/rubocop/cop/style/redundant_assignment.rb +10 -2
  77. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +5 -4
  78. data/lib/rubocop/cop/style/redundant_each.rb +1 -1
  79. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  80. data/lib/rubocop/cop/style/redundant_line_continuation.rb +5 -0
  81. data/lib/rubocop/cop/style/redundant_percent_q.rb +1 -1
  82. data/lib/rubocop/cop/style/redundant_return.rb +6 -0
  83. data/lib/rubocop/cop/style/sample.rb +1 -3
  84. data/lib/rubocop/cop/team.rb +3 -0
  85. data/lib/rubocop/cop/utils/regexp_ranges.rb +1 -1
  86. data/lib/rubocop/cops_documentation_generator.rb +4 -2
  87. data/lib/rubocop/directive_comment.rb +10 -8
  88. data/lib/rubocop/formatter/clang_style_formatter.rb +3 -7
  89. data/lib/rubocop/formatter/html_formatter.rb +30 -10
  90. data/lib/rubocop/formatter/offense_count_formatter.rb +12 -2
  91. data/lib/rubocop/formatter/tap_formatter.rb +3 -7
  92. data/lib/rubocop/lockfile.rb +34 -4
  93. data/lib/rubocop/lsp/logger.rb +1 -1
  94. data/lib/rubocop/lsp/routes.rb +1 -1
  95. data/lib/rubocop/lsp/runtime.rb +1 -1
  96. data/lib/rubocop/lsp/server.rb +5 -2
  97. data/lib/rubocop/lsp/severity.rb +1 -1
  98. data/lib/rubocop/lsp.rb +29 -0
  99. data/lib/rubocop/magic_comment.rb +1 -1
  100. data/lib/rubocop/options.rb +11 -0
  101. data/lib/rubocop/path_util.rb +6 -2
  102. data/lib/rubocop/rspec/cop_helper.rb +8 -2
  103. data/lib/rubocop/rspec/expect_offense.rb +16 -8
  104. data/lib/rubocop/rspec/shared_contexts.rb +49 -18
  105. data/lib/rubocop/rspec/support.rb +2 -1
  106. data/lib/rubocop/runner.rb +12 -2
  107. data/lib/rubocop/target_finder.rb +84 -78
  108. data/lib/rubocop/target_ruby.rb +82 -80
  109. data/lib/rubocop/version.rb +19 -4
  110. data/lib/rubocop.rb +1 -0
  111. metadata +10 -6
@@ -8,6 +8,12 @@ module RuboCop
8
8
  # This cop identifies places where `do_something(*args, &block)`
9
9
  # can be replaced by `do_something(...)`.
10
10
  #
11
+ # In Ruby 3.1, anonymous block forwarding has been added.
12
+ #
13
+ # This cop identifies places where `do_something(&block)` can be replaced
14
+ # by `do_something(&)`; if desired, this functionality can be disabled
15
+ # by setting `UseAnonymousForwarding: false`.
16
+ #
11
17
  # In Ruby 3.2, anonymous args/kwargs forwarding has been added.
12
18
  #
13
19
  # This cop also identifies places where `use_args(*args)`/`use_kwargs(**kwargs)` can be
@@ -41,22 +47,25 @@ module RuboCop
41
47
  #
42
48
  # @example UseAnonymousForwarding: true (default, only relevant for Ruby >= 3.2)
43
49
  # # bad
44
- # def foo(*args, **kwargs)
50
+ # def foo(*args, **kwargs, &block)
45
51
  # args_only(*args)
46
52
  # kwargs_only(**kwargs)
53
+ # block_only(&block)
47
54
  # end
48
55
  #
49
56
  # # good
50
- # def foo(*, **)
57
+ # def foo(*, **, &)
51
58
  # args_only(*)
52
59
  # kwargs_only(**)
60
+ # block_only(&)
53
61
  # end
54
62
  #
55
63
  # @example UseAnonymousForwarding: false (only relevant for Ruby >= 3.2)
56
64
  # # good
57
- # def foo(*args, **kwargs)
65
+ # def foo(*args, **kwargs, &block)
58
66
  # args_only(*args)
59
67
  # kwargs_only(**kwargs)
68
+ # block_only(&block)
60
69
  # end
61
70
  #
62
71
  # @example AllowOnlyRestArgument: true (default, only relevant for Ruby < 3.2)
@@ -179,9 +188,12 @@ module RuboCop
179
188
 
180
189
  send_classifications.each do |send_node, _c, forward_rest, forward_kwrest, forward_block_arg| # rubocop:disable Layout/LineLength
181
190
  if !forward_rest && !forward_kwrest
182
- register_forward_block_arg_offense(!forward_rest, node.arguments, block_arg)
183
- register_forward_block_arg_offense(!forward_rest, send_node, forward_block_arg)
184
-
191
+ # Prevents `anonymous block parameter is also used within block (SyntaxError)` occurs
192
+ # in Ruby 3.3.0.
193
+ if outside_block?(forward_block_arg)
194
+ register_forward_block_arg_offense(!forward_rest, node.arguments, block_arg)
195
+ register_forward_block_arg_offense(!forward_rest, send_node, forward_block_arg)
196
+ end
185
197
  registered_block_arg_offense = true
186
198
  break
187
199
  else
@@ -196,12 +208,13 @@ module RuboCop
196
208
  end
197
209
  # rubocop:enable Metrics/MethodLength
198
210
 
211
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
199
212
  def add_post_ruby_32_offenses(def_node, send_classifications, forwardable_args)
200
213
  return unless use_anonymous_forwarding?
201
214
 
202
- rest_arg, kwrest_arg, _block_arg = *forwardable_args
215
+ rest_arg, kwrest_arg, block_arg = *forwardable_args
203
216
 
204
- send_classifications.each do |send_node, _c, forward_rest, forward_kwrest, _forward_block_arg| # rubocop:disable Layout/LineLength
217
+ send_classifications.each do |send_node, _c, forward_rest, forward_kwrest, forward_block_arg| # rubocop:disable Layout/LineLength
205
218
  if outside_block?(forward_rest)
206
219
  register_forward_args_offense(def_node.arguments, rest_arg)
207
220
  register_forward_args_offense(send_node, forward_rest)
@@ -211,8 +224,16 @@ module RuboCop
211
224
  register_forward_kwargs_offense(!forward_rest, def_node.arguments, kwrest_arg)
212
225
  register_forward_kwargs_offense(!forward_rest, send_node, forward_kwrest)
213
226
  end
227
+
228
+ # Prevents `anonymous block parameter is also used within block (SyntaxError)` occurs
229
+ # in Ruby 3.3.0.
230
+ if outside_block?(forward_block_arg)
231
+ register_forward_block_arg_offense(!forward_rest, def_node.arguments, block_arg)
232
+ register_forward_block_arg_offense(!forward_rest, send_node, forward_block_arg)
233
+ end
214
234
  end
215
235
  end
236
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
216
237
 
217
238
  def non_splat_or_block_pass_lvar_references(body)
218
239
  body.each_descendant(:lvar, :lvasgn).filter_map do |lvar|
@@ -230,7 +230,7 @@ module RuboCop
230
230
 
231
231
  def branch_conditions(node)
232
232
  conditions = []
233
- while node&.if_type?
233
+ while node&.if_type? && !node.ternary?
234
234
  conditions << node.condition
235
235
  node = node.else_branch
236
236
  end
@@ -54,9 +54,9 @@ module RuboCop
54
54
  end
55
55
 
56
56
  def on_send(node)
57
- add_offense(
58
- node.first_argument, message: format(MSG, class_var: node.first_argument.source)
59
- )
57
+ return unless (first_argument = node.first_argument)
58
+
59
+ add_offense(first_argument, message: format(MSG, class_var: first_argument.source))
60
60
  end
61
61
  end
62
62
  end
@@ -19,9 +19,7 @@ module RuboCop
19
19
  # @example
20
20
  # # bad
21
21
  # array.reject(&:nil?)
22
- # array.delete_if(&:nil?)
23
22
  # array.reject { |e| e.nil? }
24
- # array.delete_if { |e| e.nil? }
25
23
  # array.select { |e| !e.nil? }
26
24
  # array.grep_v(nil)
27
25
  # array.grep_v(NilClass)
@@ -31,7 +29,9 @@ module RuboCop
31
29
  #
32
30
  # # bad
33
31
  # hash.reject!(&:nil?)
32
+ # array.delete_if(&:nil?)
34
33
  # hash.reject! { |k, v| v.nil? }
34
+ # array.delete_if { |e| e.nil? }
35
35
  # hash.select! { |k, v| !v.nil? }
36
36
  #
37
37
  # # good
@@ -127,7 +127,7 @@ module RuboCop
127
127
  end
128
128
 
129
129
  def good_method_name(node)
130
- if node.bang_method?
130
+ if node.bang_method? || node.method?(:delete_if)
131
131
  'compact!'
132
132
  else
133
133
  'compact'
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../../directive_comment'
4
+
3
5
  module RuboCop
4
6
  module Cop
5
7
  module Style
@@ -49,8 +51,9 @@ module RuboCop
49
51
  KEYWORDS = %w[begin class def end module].freeze
50
52
  KEYWORD_REGEXES = KEYWORDS.map { |w| /^\s*#{w}\s/ }.freeze
51
53
 
52
- ALLOWED_COMMENTS = %w[:nodoc: :yields: rubocop:disable rubocop:todo].freeze
53
- ALLOWED_COMMENT_REGEXES = ALLOWED_COMMENTS.map { |c| /#\s*#{c}/ }.freeze
54
+ ALLOWED_COMMENTS = %w[:nodoc: :yields:].freeze
55
+ ALLOWED_COMMENT_REGEXES = (ALLOWED_COMMENTS.map { |c| /#\s*#{c}/ } +
56
+ [DirectiveComment::DIRECTIVE_COMMENT_REGEXP]).freeze
54
57
 
55
58
  REGEXP = /(?<keyword>\S+).*#/.freeze
56
59
 
@@ -115,8 +115,8 @@ module RuboCop
115
115
  end
116
116
 
117
117
  # Check for `if` and `case` statements where each branch is used for
118
- # assignment to the same variable when using the return of the
119
- # condition can be used instead.
118
+ # both the assignment and comparison of the same variable
119
+ # when using the return of the condition can be used instead.
120
120
  #
121
121
  # @example EnforcedStyle: assign_to_condition (default)
122
122
  # # bad
@@ -460,9 +460,8 @@ module RuboCop
460
460
 
461
461
  def assignment(node)
462
462
  *_, condition = *node
463
- Parser::Source::Range.new(node.source_range.source_buffer,
464
- node.source_range.begin_pos,
465
- condition.source_range.begin_pos)
463
+
464
+ node.source_range.begin.join(condition.source_range.begin)
466
465
  end
467
466
 
468
467
  def correct_if_branches(corrector, cop, node)
@@ -28,18 +28,27 @@ module RuboCop
28
28
  def on_new_investigation
29
29
  return if notice.empty? || notice_found?(processed_source)
30
30
 
31
- add_offense(offense_range, message: format(MSG, notice: notice)) do |corrector|
32
- verify_autocorrect_notice!
33
-
34
- token = insert_notice_before(processed_source)
35
- range = token.nil? ? range_between(0, 0) : token.pos
36
-
37
- corrector.insert_before(range, "#{autocorrect_notice}\n")
31
+ verify_autocorrect_notice!
32
+ message = format(MSG, notice: notice)
33
+ if processed_source.blank?
34
+ add_global_offense(message)
35
+ else
36
+ offense_range = source_range(processed_source.buffer, 1, 0)
37
+ add_offense(offense_range, message: message) do |corrector|
38
+ autocorrect(corrector)
39
+ end
38
40
  end
39
41
  end
40
42
 
41
43
  private
42
44
 
45
+ def autocorrect(corrector)
46
+ token = insert_notice_before(processed_source)
47
+ range = token.nil? ? range_between(0, 0) : token.pos
48
+
49
+ corrector.insert_before(range, "#{autocorrect_notice}\n")
50
+ end
51
+
43
52
  def notice
44
53
  cop_config['Notice']
45
54
  end
@@ -48,10 +57,6 @@ module RuboCop
48
57
  cop_config['AutocorrectNotice']
49
58
  end
50
59
 
51
- def offense_range
52
- source_range(processed_source.buffer, 1, 0)
53
- end
54
-
55
60
  def verify_autocorrect_notice!
56
61
  raise Warning, AUTOCORRECT_EMPTY_WARNING if autocorrect_notice.empty?
57
62
 
@@ -155,6 +155,8 @@ module RuboCop
155
155
 
156
156
  def check_line(node, code)
157
157
  line_node = node.last_argument
158
+ return if line_node.variable? || (line_node.send_type? && !line_node.method?(:+))
159
+
158
160
  line_diff = line_difference(line_node, code)
159
161
  if line_diff.zero?
160
162
  add_offense_for_same_line(node, line_node)
@@ -38,12 +38,13 @@ module RuboCop
38
38
  PATTERN
39
39
 
40
40
  def on_send(node)
41
+ return unless (receiver = node.receiver)
41
42
  return unless (regexp = exact_regexp_match(node))
42
43
 
43
44
  parsed_regexp = Regexp::Parser.parse(regexp)
44
45
  return unless exact_match_pattern?(parsed_regexp)
45
46
 
46
- prefer = "#{node.receiver.source} #{new_method(node)} '#{parsed_regexp[1].text}'"
47
+ prefer = "#{receiver.source} #{new_method(node)} '#{parsed_regexp[1].text}'"
47
48
 
48
49
  add_offense(node, message: format(MSG, prefer: prefer)) do |corrector|
49
50
  corrector.replace(node, prefer)
@@ -66,6 +66,8 @@ module RuboCop
66
66
  return unless suspect_enumerable?(node)
67
67
 
68
68
  if style == :for
69
+ return unless node.receiver
70
+
69
71
  add_offense(node, message: PREFER_FOR) do |corrector|
70
72
  EachToForCorrector.new(node).call(corrector)
71
73
  opposite_style_detected
@@ -25,27 +25,27 @@ module RuboCop
25
25
  #
26
26
  # @example EnforcedStyle: format (default)
27
27
  # # bad
28
- # puts sprintf('%10s', 'hoge')
29
- # puts '%10s' % 'hoge'
28
+ # puts sprintf('%10s', 'foo')
29
+ # puts '%10s' % 'foo'
30
30
  #
31
31
  # # good
32
- # puts format('%10s', 'hoge')
32
+ # puts format('%10s', 'foo')
33
33
  #
34
34
  # @example EnforcedStyle: sprintf
35
35
  # # bad
36
- # puts format('%10s', 'hoge')
37
- # puts '%10s' % 'hoge'
36
+ # puts format('%10s', 'foo')
37
+ # puts '%10s' % 'foo'
38
38
  #
39
39
  # # good
40
- # puts sprintf('%10s', 'hoge')
40
+ # puts sprintf('%10s', 'foo')
41
41
  #
42
42
  # @example EnforcedStyle: percent
43
43
  # # bad
44
- # puts format('%10s', 'hoge')
45
- # puts sprintf('%10s', 'hoge')
44
+ # puts format('%10s', 'foo')
45
+ # puts sprintf('%10s', 'foo')
46
46
  #
47
47
  # # good
48
- # puts '%10s' % 'hoge'
48
+ # puts '%10s' % 'foo'
49
49
  #
50
50
  class FormatString < Base
51
51
  include ConfigurableEnforcedStyle
@@ -40,7 +40,7 @@ module RuboCop
40
40
 
41
41
  MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
42
42
  UNUSED_BLOCK_ARG_MSG = "#{MSG.chop} and remove the unused `%<unused_code>s` block argument."
43
- ARRAY_CONVERTER_METHODS = %i[assoc flatten rassoc sort sort_by to_a].freeze
43
+ ARRAY_CONVERTER_METHODS = %i[assoc chunk flatten rassoc sort sort_by to_a].freeze
44
44
 
45
45
  # @!method kv_each(node)
46
46
  def_node_matcher :kv_each, <<~PATTERN
@@ -195,6 +195,7 @@ module RuboCop
195
195
  acceptable_19_syntax_symbol?(pair.key.source)
196
196
  end
197
197
 
198
+ # rubocop:disable Metrics/CyclomaticComplexity
198
199
  def acceptable_19_syntax_symbol?(sym_name)
199
200
  sym_name.delete_prefix!(':')
200
201
 
@@ -209,9 +210,12 @@ module RuboCop
209
210
  # Most hash keys can be matched against a simple regex.
210
211
  return true if /\A[_a-z]\w*[?!]?\z/i.match?(sym_name)
211
212
 
212
- # For more complicated hash keys, let the parser validate the syntax.
213
- parse("{ #{sym_name}: :foo }").valid_syntax?
213
+ return false if target_ruby_version <= 2.1
214
+
215
+ (sym_name.start_with?("'") && sym_name.end_with?("'")) ||
216
+ (sym_name.start_with?('"') && sym_name.end_with?('"'))
214
217
  end
218
+ # rubocop:enable Metrics/CyclomaticComplexity
215
219
 
216
220
  def check(pairs, delim, msg)
217
221
  pairs.each do |pair|
@@ -76,9 +76,9 @@ module RuboCop
76
76
  PATTERN
77
77
 
78
78
  def on_send(node)
79
- inverse_candidate?(node) do |_method_call, lhs, method, rhs|
79
+ inverse_candidate?(node) do |method_call, lhs, method, rhs|
80
80
  return unless inverse_methods.key?(method)
81
- return if negated?(node)
81
+ return if negated?(node) || relational_comparison_with_safe_navigation?(method_call)
82
82
  return if part_of_ignored_node?(node)
83
83
  return if possible_class_hierarchy_check?(lhs, rhs, method)
84
84
 
@@ -155,16 +155,16 @@ module RuboCop
155
155
  node.parent.respond_to?(:method?) && node.parent.method?(:!)
156
156
  end
157
157
 
158
+ def relational_comparison_with_safe_navigation?(node)
159
+ node.csend_type? && CLASS_COMPARISON_METHODS.include?(node.method_name)
160
+ end
161
+
158
162
  def not_to_receiver(node, method_call)
159
- Parser::Source::Range.new(node.source_range.source_buffer,
160
- node.loc.selector.begin_pos,
161
- method_call.source_range.begin_pos)
163
+ node.loc.selector.begin.join(method_call.source_range.begin)
162
164
  end
163
165
 
164
166
  def end_parentheses(node, method_call)
165
- Parser::Source::Range.new(node.source_range.source_buffer,
166
- method_call.source_range.end_pos,
167
- node.source_range.end_pos)
167
+ method_call.source_range.end.join(node.source_range.end)
168
168
  end
169
169
 
170
170
  # When comparing classes, `!(Integer < Numeric)` is not the same as
@@ -32,12 +32,14 @@ module RuboCop
32
32
  # foo unless x != y
33
33
  # foo unless x >= 10
34
34
  # foo unless x.even?
35
+ # foo unless odd?
35
36
  #
36
37
  # # good
37
38
  # foo if bar
38
39
  # foo if x == y
39
40
  # foo if x < 10
40
41
  # foo if x.odd?
42
+ # foo if even?
41
43
  #
42
44
  # # bad (complex condition)
43
45
  # foo unless x != y || x.even?
@@ -99,12 +101,15 @@ module RuboCop
99
101
  end
100
102
  end
101
103
 
102
- def preferred_send_condition(node)
103
- receiver_source = node.receiver.source
104
+ def preferred_send_condition(node) # rubocop:disable Metrics/CyclomaticComplexity
105
+ receiver_source = node.receiver&.source
104
106
  return receiver_source if node.method?(:!)
105
107
 
108
+ # receiver may be implicit (self)
109
+ dotted_receiver_source = receiver_source ? "#{receiver_source}." : ''
110
+
106
111
  inverse_method_name = inverse_methods[node.method_name]
107
- return "#{receiver_source}.#{inverse_method_name}" unless node.arguments?
112
+ return "#{dotted_receiver_source}#{inverse_method_name}" unless node.arguments?
108
113
 
109
114
  argument_list = node.arguments.map(&:source).join(', ')
110
115
  if node.operator_method?
@@ -112,10 +117,10 @@ module RuboCop
112
117
  end
113
118
 
114
119
  if node.parenthesized?
115
- return "#{receiver_source}.#{inverse_method_name}(#{argument_list})"
120
+ return "#{dotted_receiver_source}#{inverse_method_name}(#{argument_list})"
116
121
  end
117
122
 
118
- "#{receiver_source}.#{inverse_method_name} #{argument_list}"
123
+ "#{dotted_receiver_source}#{inverse_method_name} #{argument_list}"
119
124
  end
120
125
 
121
126
  def preferred_logical_condition(node)
@@ -116,20 +116,17 @@ module RuboCop
116
116
  def truthy_branch_for_guard?(node)
117
117
  if_node = node.left_sibling
118
118
 
119
- if if_node.if? || if_node.ternary?
120
- if_node.else_branch.nil?
121
- elsif if_node.unless?
122
- if_node.if_branch.nil?
119
+ if if_node.if?
120
+ if_node.if_branch.arguments.any?
121
+ else
122
+ if_node.if_branch.arguments.none?
123
123
  end
124
124
  end
125
125
 
126
126
  def range(node)
127
- buffer = node.source_range.source_buffer
128
127
  map_node = node.receiver.send_node
129
- begin_pos = map_node.loc.selector.begin_pos
130
- end_pos = node.source_range.end_pos
131
128
 
132
- Parser::Source::Range.new(buffer, begin_pos, end_pos)
129
+ map_node.loc.selector.join(node.source_range.end)
133
130
  end
134
131
  end
135
132
  end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Checks for usages of `each` with `<<`, `push`, or `append` which
7
+ # can be replaced by `map`.
8
+ #
9
+ # If `PreferredMethods` is configured for `map` in `Style/CollectionMethods`,
10
+ # this cop uses the specified method for replacement.
11
+ #
12
+ # NOTE: The return value of `Enumerable#each` is `self`, whereas the
13
+ # return value of `Enumerable#map` is an `Array`. They are not autocorrected
14
+ # when a return value could be used because these types differ.
15
+ #
16
+ # NOTE: It only detects when the mapping destination is a local variable
17
+ # initialized as an empty array and referred to only by the pushing operation.
18
+ # This is because, if not, it's challenging to statically guarantee that the
19
+ # mapping destination variable remains an empty array:
20
+ #
21
+ # [source,ruby]
22
+ # ----
23
+ # @dest = []
24
+ # src.each { |e| @dest << e * 2 } # `src` method may mutate `@dest`
25
+ #
26
+ # dest = []
27
+ # src.each { |e| dest << transform(e, dest) } # `transform` method may mutate `dest`
28
+ # ----
29
+ #
30
+ # @safety
31
+ # This cop is unsafe because not all objects that have an `each`
32
+ # method also have a `map` method (e.g. `ENV`). Additionally, for calls
33
+ # with a block, not all objects that have a `map` method return an array
34
+ # (e.g. `Enumerator::Lazy`).
35
+ #
36
+ # @example
37
+ # # bad
38
+ # dest = []
39
+ # src.each { |e| dest << e * 2 }
40
+ # dest
41
+ #
42
+ # # good
43
+ # dest = src.map { |e| e * 2 }
44
+ #
45
+ # # good - contains another operation
46
+ # dest = []
47
+ # src.each { |e| dest << e * 2; puts e }
48
+ # dest
49
+ #
50
+ class MapIntoArray < Base
51
+ include RangeHelp
52
+ extend AutoCorrector
53
+
54
+ MSG = 'Use `%<new_method_name>s` instead of `each` to map elements into an array.'
55
+
56
+ # @!method each_block_with_push?(node)
57
+ def_node_matcher :each_block_with_push?, <<-PATTERN
58
+ [
59
+ ^({begin kwbegin} ...)
60
+ ({block numblock} (send _ :each) _
61
+ (send (lvar _) {:<< :push :append} _))
62
+ ]
63
+ PATTERN
64
+
65
+ # @!method empty_array_asgn?(node)
66
+ def_node_matcher :empty_array_asgn?, '(lvasgn _ (array))'
67
+
68
+ # @!method lvar_ref?(node, name)
69
+ def_node_matcher :lvar_ref?, '(lvar %1)'
70
+
71
+ def self.joining_forces
72
+ VariableForce
73
+ end
74
+
75
+ def after_leaving_scope(scope, _variable_table)
76
+ (@scopes ||= []) << scope
77
+ end
78
+
79
+ def on_block(node)
80
+ return unless each_block_with_push?(node)
81
+
82
+ dest_var = find_dest_var(node)
83
+ return unless (asgn = find_closest_assignment(node, dest_var))
84
+ return unless empty_array_asgn?(asgn)
85
+ return unless dest_used_only_for_mapping?(node, dest_var, asgn)
86
+
87
+ register_offense(node, dest_var, asgn)
88
+ end
89
+
90
+ alias on_numblock on_block
91
+
92
+ private
93
+
94
+ def find_dest_var(block)
95
+ node = block.body.receiver
96
+ name = node.children.first
97
+
98
+ candidates = @scopes.lazy.filter_map { |s| s.variables[name] }
99
+ candidates.find { |v| v.references.any? { |n| n.node.equal?(node) } }
100
+ end
101
+
102
+ def find_closest_assignment(block, dest_var)
103
+ dest_var.assignments.reverse_each.lazy.map(&:node).find do |node|
104
+ node.source_range.end_pos < block.source_range.begin_pos
105
+ end
106
+ end
107
+
108
+ def dest_used_only_for_mapping?(block, dest_var, asgn)
109
+ range = asgn.source_range.join(block.source_range)
110
+
111
+ asgn.parent.equal?(block.parent) &&
112
+ dest_var.references.one? { |r| range.contains?(r.node.source_range) } &&
113
+ dest_var.assignments.one? { |a| range.contains?(a.node.source_range) }
114
+ end
115
+
116
+ def register_offense(block, dest_var, asgn)
117
+ add_offense(block, message: format(MSG, new_method_name: new_method_name)) do |corrector|
118
+ next if return_value_used?(block)
119
+
120
+ corrector.replace(block.send_node.selector, new_method_name)
121
+ remove_assignment(corrector, asgn)
122
+ correct_push_node(corrector, block.body)
123
+ correct_return_value_handling(corrector, block, dest_var)
124
+ end
125
+ end
126
+
127
+ def new_method_name
128
+ default = 'map'
129
+ alternative = config.for_cop('Style/CollectionMethods').dig('PreferredMethods', default)
130
+ alternative || default
131
+ end
132
+
133
+ def return_value_used?(node)
134
+ parent = node.parent
135
+
136
+ case parent&.type
137
+ when nil
138
+ false
139
+ when :begin, :kwbegin
140
+ !node.right_sibling && return_value_used?(parent)
141
+ when :block, :numblock
142
+ !parent.void_context?
143
+ else
144
+ true
145
+ end
146
+ end
147
+
148
+ def remove_assignment(corrector, asgn)
149
+ range = range_with_surrounding_space(asgn.source_range, side: :right)
150
+ range = range_with_surrounding_space(range, side: :right, newlines: false)
151
+
152
+ corrector.remove(range)
153
+ end
154
+
155
+ def correct_push_node(corrector, push_node)
156
+ range = push_node.source_range
157
+ arg_range = push_node.first_argument.source_range
158
+
159
+ corrector.remove(range_between(range.begin_pos, arg_range.begin_pos))
160
+ corrector.remove(range_between(arg_range.end_pos, range.end_pos))
161
+ end
162
+
163
+ def correct_return_value_handling(corrector, block, dest_var)
164
+ next_node = block.right_sibling
165
+
166
+ if lvar_ref?(next_node, dest_var.name)
167
+ corrector.remove(range_with_surrounding_space(next_node.source_range, side: :left))
168
+ end
169
+
170
+ corrector.insert_before(block, "#{dest_var.name} = ")
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -55,7 +55,7 @@ module RuboCop
55
55
  message = format(MSG, method: map_node.loc.selector.source, dot: to_h_node.loc.dot.source)
56
56
  add_offense(map_node.loc.selector, message: message) do |corrector|
57
57
  # If the `to_h` call already has a block, do not autocorrect.
58
- next if to_h_node.block_node
58
+ next if to_h_node.block_literal?
59
59
 
60
60
  autocorrect(corrector, to_h_node, map_node)
61
61
  end
@@ -44,7 +44,7 @@ module RuboCop
44
44
  message = format(MSG, method: map_node.loc.selector.source)
45
45
  add_offense(map_node.loc.selector, message: message) do |corrector|
46
46
  # If the `to_set` call already has a block, do not autocorrect.
47
- next if to_set_node.block_node
47
+ next if to_set_node.block_literal?
48
48
 
49
49
  autocorrect(corrector, to_set_node, map_node)
50
50
  end
@@ -132,7 +132,7 @@ module RuboCop
132
132
  call_in_match_pattern?(node) ||
133
133
  hash_literal_in_arguments?(node) ||
134
134
  node.descendants.any? do |n|
135
- n.forwarded_args_type? || n.block_type? ||
135
+ n.forwarded_args_type? || n.block_type? || n.numblock_type? ||
136
136
  ambiguous_literal?(n) || logical_operator?(n)
137
137
  end
138
138
  end