rubocop 0.93.1 → 1.3.1

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -16
  3. data/config/default.yml +207 -72
  4. data/exe/rubocop +1 -1
  5. data/lib/rubocop.rb +16 -2
  6. data/lib/rubocop/cli/command/auto_genenerate_config.rb +1 -1
  7. data/lib/rubocop/cli/command/version.rb +1 -1
  8. data/lib/rubocop/comment_config.rb +1 -1
  9. data/lib/rubocop/config.rb +4 -0
  10. data/lib/rubocop/config_loader.rb +33 -7
  11. data/lib/rubocop/config_loader_resolver.rb +7 -5
  12. data/lib/rubocop/config_validator.rb +7 -6
  13. data/lib/rubocop/cop/badge.rb +9 -24
  14. data/lib/rubocop/cop/base.rb +16 -1
  15. data/lib/rubocop/cop/bundler/duplicated_gem.rb +26 -6
  16. data/lib/rubocop/cop/bundler/gem_comment.rb +1 -1
  17. data/lib/rubocop/cop/commissioner.rb +37 -23
  18. data/lib/rubocop/cop/corrector.rb +3 -1
  19. data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +1 -1
  20. data/lib/rubocop/cop/force.rb +1 -1
  21. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +3 -3
  22. data/lib/rubocop/cop/gemspec/required_ruby_version.rb +4 -5
  23. data/lib/rubocop/cop/gemspec/ruby_version_globals_usage.rb +1 -1
  24. data/lib/rubocop/cop/generator.rb +2 -9
  25. data/lib/rubocop/cop/generator/configuration_injector.rb +1 -1
  26. data/lib/rubocop/cop/layout/block_alignment.rb +3 -4
  27. data/lib/rubocop/cop/layout/class_structure.rb +22 -3
  28. data/lib/rubocop/cop/layout/def_end_alignment.rb +1 -1
  29. data/lib/rubocop/cop/layout/else_alignment.rb +15 -2
  30. data/lib/rubocop/cop/layout/empty_lines_around_access_modifier.rb +1 -0
  31. data/lib/rubocop/cop/layout/end_alignment.rb +3 -3
  32. data/lib/rubocop/cop/layout/extra_spacing.rb +1 -2
  33. data/lib/rubocop/cop/layout/hash_alignment.rb +4 -4
  34. data/lib/rubocop/cop/layout/line_length.rb +8 -1
  35. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +24 -18
  36. data/lib/rubocop/cop/layout/space_around_operators.rb +4 -1
  37. data/lib/rubocop/cop/layout/space_inside_parens.rb +35 -13
  38. data/lib/rubocop/cop/layout/trailing_whitespace.rb +37 -13
  39. data/lib/rubocop/cop/lint/constant_definition_in_block.rb +26 -2
  40. data/lib/rubocop/cop/lint/debugger.rb +17 -28
  41. data/lib/rubocop/cop/lint/duplicate_branch.rb +93 -0
  42. data/lib/rubocop/cop/lint/duplicate_case_condition.rb +2 -12
  43. data/lib/rubocop/cop/lint/duplicate_regexp_character_class_element.rb +77 -0
  44. data/lib/rubocop/cop/lint/else_layout.rb +29 -3
  45. data/lib/rubocop/cop/lint/empty_block.rb +82 -0
  46. data/lib/rubocop/cop/lint/empty_class.rb +93 -0
  47. data/lib/rubocop/cop/lint/flip_flop.rb +8 -2
  48. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +38 -6
  49. data/lib/rubocop/cop/lint/loop.rb +4 -4
  50. data/lib/rubocop/cop/lint/nested_percent_literal.rb +14 -0
  51. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +58 -0
  52. data/lib/rubocop/cop/lint/number_conversion.rb +46 -13
  53. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +27 -8
  54. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +19 -16
  55. data/lib/rubocop/cop/lint/shadowed_exception.rb +4 -5
  56. data/lib/rubocop/cop/lint/to_enum_arguments.rb +86 -0
  57. data/lib/rubocop/cop/lint/to_json.rb +1 -1
  58. data/lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb +185 -0
  59. data/lib/rubocop/cop/lint/useless_access_modifier.rb +2 -2
  60. data/lib/rubocop/cop/lint/useless_method_definition.rb +2 -4
  61. data/lib/rubocop/cop/lint/useless_setter_call.rb +6 -1
  62. data/lib/rubocop/cop/metrics/parameter_lists.rb +4 -1
  63. data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
  64. data/lib/rubocop/cop/mixin/configurable_numbering.rb +3 -3
  65. data/lib/rubocop/cop/mixin/line_length_help.rb +1 -1
  66. data/lib/rubocop/cop/mixin/statement_modifier.rb +9 -4
  67. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +12 -2
  68. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +11 -5
  69. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +67 -18
  70. data/lib/rubocop/cop/naming/predicate_name.rb +2 -1
  71. data/lib/rubocop/cop/naming/variable_number.rb +98 -8
  72. data/lib/rubocop/cop/offense.rb +3 -3
  73. data/lib/rubocop/cop/security/open.rb +12 -10
  74. data/lib/rubocop/cop/style/accessor_grouping.rb +1 -1
  75. data/lib/rubocop/cop/style/and_or.rb +1 -3
  76. data/lib/rubocop/cop/style/arguments_forwarding.rb +142 -0
  77. data/lib/rubocop/cop/style/bisected_attr_accessor.rb +0 -4
  78. data/lib/rubocop/cop/style/case_like_if.rb +0 -4
  79. data/lib/rubocop/cop/style/collection_compact.rb +91 -0
  80. data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +169 -0
  81. data/lib/rubocop/cop/style/double_negation.rb +6 -1
  82. data/lib/rubocop/cop/style/format_string_token.rb +47 -2
  83. data/lib/rubocop/cop/style/hash_syntax.rb +3 -3
  84. data/lib/rubocop/cop/style/identical_conditional_branches.rb +7 -2
  85. data/lib/rubocop/cop/style/if_inside_else.rb +37 -1
  86. data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -3
  87. data/lib/rubocop/cop/style/infinite_loop.rb +4 -0
  88. data/lib/rubocop/cop/style/keyword_parameters_order.rb +12 -0
  89. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +10 -13
  90. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +6 -11
  91. data/lib/rubocop/cop/style/method_call_with_args_parentheses/require_parentheses.rb +7 -11
  92. data/lib/rubocop/cop/style/mixin_grouping.rb +0 -4
  93. data/lib/rubocop/cop/style/multiple_comparison.rb +55 -7
  94. data/lib/rubocop/cop/style/negated_if_else_condition.rb +106 -0
  95. data/lib/rubocop/cop/style/nil_lambda.rb +52 -0
  96. data/lib/rubocop/cop/style/raise_args.rb +21 -6
  97. data/lib/rubocop/cop/style/redundant_parentheses.rb +4 -0
  98. data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +7 -1
  99. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +1 -1
  100. data/lib/rubocop/cop/style/redundant_self.rb +3 -0
  101. data/lib/rubocop/cop/style/safe_navigation.rb +16 -4
  102. data/lib/rubocop/cop/style/semicolon.rb +3 -0
  103. data/lib/rubocop/cop/style/static_class.rb +97 -0
  104. data/lib/rubocop/cop/style/string_concatenation.rb +13 -1
  105. data/lib/rubocop/cop/style/swap_values.rb +108 -0
  106. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +3 -1
  107. data/lib/rubocop/cop/style/while_until_modifier.rb +9 -0
  108. data/lib/rubocop/cop/team.rb +6 -1
  109. data/lib/rubocop/cop/util.rb +5 -1
  110. data/lib/rubocop/ext/regexp_node.rb +17 -9
  111. data/lib/rubocop/ext/regexp_parser.rb +84 -0
  112. data/lib/rubocop/formatter/formatter_set.rb +2 -1
  113. data/lib/rubocop/formatter/git_hub_actions_formatter.rb +47 -0
  114. data/lib/rubocop/formatter/offense_count_formatter.rb +1 -1
  115. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -1
  116. data/lib/rubocop/magic_comment.rb +2 -2
  117. data/lib/rubocop/options.rb +6 -1
  118. data/lib/rubocop/rspec/shared_contexts.rb +4 -0
  119. data/lib/rubocop/target_finder.rb +1 -1
  120. data/lib/rubocop/target_ruby.rb +65 -1
  121. data/lib/rubocop/version.rb +56 -6
  122. metadata +22 -6
@@ -213,7 +213,7 @@ module RuboCop
213
213
  # returns `true` if two offenses contain same attributes
214
214
  def ==(other)
215
215
  COMPARISON_ATTRIBUTES.all? do |attribute|
216
- send(attribute) == other.send(attribute)
216
+ public_send(attribute) == other.public_send(attribute)
217
217
  end
218
218
  end
219
219
 
@@ -221,7 +221,7 @@ module RuboCop
221
221
 
222
222
  def hash
223
223
  COMPARISON_ATTRIBUTES.reduce(0) do |hash, attribute|
224
- hash ^ send(attribute).hash
224
+ hash ^ public_send(attribute).hash
225
225
  end
226
226
  end
227
227
 
@@ -234,7 +234,7 @@ module RuboCop
234
234
  # comparison result
235
235
  def <=>(other)
236
236
  COMPARISON_ATTRIBUTES.each do |attribute|
237
- result = send(attribute) <=> other.send(attribute)
237
+ result = public_send(attribute) <=> other.public_send(attribute)
238
238
  return result unless result.zero?
239
239
  end
240
240
  0
@@ -3,35 +3,37 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Security
6
- # This cop checks for the use of `Kernel#open`.
6
+ # This cop checks for the use of `Kernel#open` and `URI.open`.
7
7
  #
8
- # `Kernel#open` enables not only file access but also process invocation
9
- # by prefixing a pipe symbol (e.g., `open("| ls")`). So, it may lead to
10
- # a serious security risk by using variable input to the argument of
11
- # `Kernel#open`. It would be better to use `File.open`, `IO.popen` or
12
- # `URI#open` explicitly.
8
+ # `Kernel#open` and `URI.open` enable not only file access but also process
9
+ # invocation by prefixing a pipe symbol (e.g., `open("| ls")`).
10
+ # So, it may lead to a serious security risk by using variable input to
11
+ # the argument of `Kernel#open` and `URI.open`. It would be better to use
12
+ # `File.open`, `IO.popen` or `URI.parse#open` explicitly.
13
13
  #
14
14
  # @example
15
15
  # # bad
16
16
  # open(something)
17
+ # URI.open(something)
17
18
  #
18
19
  # # good
19
20
  # File.open(something)
20
21
  # IO.popen(something)
21
22
  # URI.parse(something).open
22
23
  class Open < Base
23
- MSG = 'The use of `Kernel#open` is a serious security risk.'
24
+ MSG = 'The use of `%<receiver>sopen` is a serious security risk.'
24
25
  RESTRICT_ON_SEND = %i[open].freeze
25
26
 
26
27
  def_node_matcher :open?, <<~PATTERN
27
- (send nil? :open $!str ...)
28
+ (send ${nil? (const {nil? cbase} :URI)} :open $!str ...)
28
29
  PATTERN
29
30
 
30
31
  def on_send(node)
31
- open?(node) do |code|
32
+ open?(node) do |receiver, code|
32
33
  return if safe?(code)
33
34
 
34
- add_offense(node.loc.selector)
35
+ message = format(MSG, receiver: receiver ? "#{receiver.source}." : 'Kernel#')
36
+ add_offense(node.loc.selector, message: message)
35
37
  end
36
38
  end
37
39
 
@@ -7,7 +7,7 @@ module RuboCop
7
7
  # By default it enforces accessors to be placed in grouped declarations,
8
8
  # but it can be configured to enforce separating them in multiple declarations.
9
9
  #
10
- # Note: `Sorbet` is not compatible with "grouped" style. Consider "separated" style
10
+ # NOTE: `Sorbet` is not compatible with "grouped" style. Consider "separated" style
11
11
  # or disabling this cop.
12
12
  #
13
13
  # @example EnforcedStyle: grouped (default)
@@ -66,9 +66,7 @@ module RuboCop
66
66
  node.each_child_node do |expr|
67
67
  if expr.send_type?
68
68
  correct_send(expr, corrector)
69
- elsif expr.return_type?
70
- correct_other(expr, corrector)
71
- elsif expr.assignment?
69
+ elsif expr.return_type? || expr.assignment?
72
70
  correct_other(expr, corrector)
73
71
  end
74
72
  end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # In Ruby 2.7, arguments forwarding has been added.
7
+ #
8
+ # This cop identifies places where `do_something(*args, &block)`
9
+ # can be replaced by `do_something(...)`.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # def foo(*args, &block)
14
+ # bar(*args, &block)
15
+ # end
16
+ #
17
+ # # bad
18
+ # def foo(*args, **kwargs, &block)
19
+ # bar(*args, **kwargs, &block)
20
+ # end
21
+ #
22
+ # # good
23
+ # def foo(...)
24
+ # bar(...)
25
+ # end
26
+ #
27
+ # @example AllowOnlyRestArgument: true (default)
28
+ # # good
29
+ # def foo(*args)
30
+ # bar(*args)
31
+ # end
32
+ #
33
+ # @example AllowOnlyRestArgument: false
34
+ # # bad
35
+ # # The following code can replace the arguments with `...`,
36
+ # # but it will change the behavior. Because `...` forwards block also.
37
+ # def foo(*args)
38
+ # bar(*args)
39
+ # end
40
+ #
41
+ class ArgumentsForwarding < Base
42
+ include RangeHelp
43
+ extend AutoCorrector
44
+ extend TargetRubyVersion
45
+
46
+ minimum_target_ruby_version 2.7
47
+
48
+ MSG = 'Use arguments forwarding.'
49
+
50
+ def_node_matcher :use_rest_arguments?, <<~PATTERN
51
+ (args (restarg $_) $...)
52
+ PATTERN
53
+
54
+ def_node_matcher :only_rest_arguments?, <<~PATTERN
55
+ (send _ _ (splat (lvar %1)))
56
+ PATTERN
57
+
58
+ def_node_matcher :forwarding_method_arguments?, <<~PATTERN
59
+ {
60
+ (send _ _
61
+ (splat (lvar %1))
62
+ (block-pass (lvar %2)))
63
+ (send _ _
64
+ (splat (lvar %1))
65
+ (hash (kwsplat (lvar %3)))
66
+ (block-pass (lvar %2)))
67
+ }
68
+ PATTERN
69
+
70
+ def on_def(node)
71
+ return unless node.body
72
+ return unless (rest_args_name, args = use_rest_arguments?(node.arguments))
73
+
74
+ node.each_descendant(:send) do |send_node|
75
+ kwargs_name, block_name = extract_argument_names_from(args)
76
+
77
+ next unless forwarding_method?(send_node, rest_args_name, kwargs_name, block_name) &&
78
+ all_lvars_as_forwarding_method_arguments?(node, send_node)
79
+
80
+ register_offense_to_forwarding_method_arguments(send_node)
81
+ register_offense_to_method_definition_arguments(node)
82
+ end
83
+ end
84
+ alias on_defs on_def
85
+
86
+ private
87
+
88
+ def extract_argument_names_from(args)
89
+ kwargs_name = args.first.source.delete('**') if args.first&.kwrestarg_type?
90
+ block_arg_name = args.last.source.delete('&') if args.last&.blockarg_type?
91
+
92
+ [kwargs_name, block_arg_name].map { |name| name&.to_sym }
93
+ end
94
+
95
+ def forwarding_method?(node, rest_arg, kwargs, block_arg)
96
+ return only_rest_arguments?(node, rest_arg) unless allow_only_rest_arguments?
97
+
98
+ forwarding_method_arguments?(node, rest_arg, block_arg, kwargs)
99
+ end
100
+
101
+ def all_lvars_as_forwarding_method_arguments?(def_node, forwarding_method)
102
+ lvars = def_node.body.each_descendant(:lvar, :lvasgn)
103
+
104
+ begin_pos = forwarding_method.source_range.begin_pos
105
+ end_pos = forwarding_method.source_range.end_pos
106
+
107
+ lvars.all? do |lvar|
108
+ lvar.source_range.begin_pos.between?(begin_pos, end_pos)
109
+ end
110
+ end
111
+
112
+ def register_offense_to_forwarding_method_arguments(forwarding_method)
113
+ add_offense(arguments_range(forwarding_method)) do |corrector|
114
+ range = range_between(
115
+ forwarding_method.loc.selector.end_pos, forwarding_method.source_range.end_pos
116
+ )
117
+ corrector.replace(range, '(...)')
118
+ end
119
+ end
120
+
121
+ def register_offense_to_method_definition_arguments(method_definition)
122
+ add_offense(arguments_range(method_definition)) do |corrector|
123
+ arguments_range = range_with_surrounding_space(
124
+ range: method_definition.arguments.source_range, side: :left
125
+ )
126
+ corrector.replace(arguments_range, '(...)')
127
+ end
128
+ end
129
+
130
+ def arguments_range(node)
131
+ arguments = node.arguments
132
+
133
+ range_between(arguments.first.source_range.begin_pos, arguments.last.source_range.end_pos)
134
+ end
135
+
136
+ def allow_only_rest_arguments?
137
+ cop_config.fetch('AllowOnlyRestArgument', true)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -131,10 +131,6 @@ module RuboCop
131
131
  "#{indent(macro)}#{macro.method_name} #{rest_args.map(&:source).join(', ')}"
132
132
  end
133
133
  end
134
-
135
- def indent(node)
136
- ' ' * node.loc.column
137
- end
138
134
  end
139
135
  end
140
136
  end
@@ -227,10 +227,6 @@ module RuboCop
227
227
  range_between(node.parent.loc.keyword.begin_pos, node.loc.expression.end_pos)
228
228
  end
229
229
 
230
- def indent(node)
231
- ' ' * node.loc.column
232
- end
233
-
234
230
  # Named captures work with `=~` (if regexp is on lhs) and with `match` (both sides)
235
231
  def regexp_with_working_captures?(node)
236
232
  case node.type
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # This cop checks for places where custom logic on rejection nils from arrays
7
+ # and hashes can be replaced with `{Array,Hash}#{compact,compact!}`.
8
+ #
9
+ # It is marked as unsafe by default because false positives may occur in the
10
+ # nil check of block arguments to the receiver object.
11
+ # For example, `[[1, 2], [3, nil]].reject { |first, second| second.nil? }`
12
+ # and `[[1, 2], [3, nil]].compact` are not compatible. This will work fine
13
+ # when the receiver is a hash object.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # array.reject { |e| e.nil? }
18
+ # array.select { |e| !e.nil? }
19
+ #
20
+ # # good
21
+ # array.compact
22
+ #
23
+ # # bad
24
+ # hash.reject! { |k, v| v.nil? }
25
+ # hash.select! { |k, v| !v.nil? }
26
+ #
27
+ # # good
28
+ # hash.compact!
29
+ #
30
+ class CollectionCompact < Base
31
+ include RangeHelp
32
+ extend AutoCorrector
33
+
34
+ MSG = 'Use `%<good>s` instead of `%<bad>s`.'
35
+
36
+ RESTRICT_ON_SEND = %i[reject reject! select select!].freeze
37
+
38
+ def_node_matcher :reject_method?, <<~PATTERN
39
+ (block
40
+ (send
41
+ _ ${:reject :reject!})
42
+ $(args ...)
43
+ (send
44
+ $(lvar _) :nil?))
45
+ PATTERN
46
+
47
+ def_node_matcher :select_method?, <<~PATTERN
48
+ (block
49
+ (send
50
+ _ ${:select :select!})
51
+ $(args ...)
52
+ (send
53
+ (send
54
+ $(lvar _) :nil?) :!))
55
+ PATTERN
56
+
57
+ def on_send(node)
58
+ block_node = node.parent
59
+ return unless block_node&.block_type?
60
+
61
+ return unless (method_name, args, receiver =
62
+ reject_method?(block_node) || select_method?(block_node))
63
+
64
+ return unless args.last.source == receiver.source
65
+
66
+ range = offense_range(node, block_node)
67
+ good = good_method_name(method_name)
68
+ message = format(MSG, good: good, bad: range.source)
69
+
70
+ add_offense(range, message: message) do |corrector|
71
+ corrector.replace(range, good)
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def good_method_name(method_name)
78
+ if method_name.to_s.end_with?('!')
79
+ 'compact!'
80
+ else
81
+ 'compact'
82
+ end
83
+ end
84
+
85
+ def offense_range(send_node, block_node)
86
+ range_between(send_node.loc.selector.begin_pos, block_node.loc.end.end_pos)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # When using `class_eval` (or other `eval`) with string interpolation,
7
+ # add a comment block showing its appearance if interpolated (a practice used in Rails code).
8
+ #
9
+ # @example
10
+ # # from activesupport/lib/active_support/core_ext/string/output_safety.rb
11
+ #
12
+ # # bad
13
+ # UNSAFE_STRING_METHODS.each do |unsafe_method|
14
+ # if 'String'.respond_to?(unsafe_method)
15
+ # class_eval <<-EOT, __FILE__, __LINE__ + 1
16
+ # def #{unsafe_method}(*params, &block)
17
+ # to_str.#{unsafe_method}(*params, &block)
18
+ # end
19
+ #
20
+ # def #{unsafe_method}!(*params)
21
+ # @dirty = true
22
+ # super
23
+ # end
24
+ # EOT
25
+ # end
26
+ # end
27
+ #
28
+ # # good, inline comments in heredoc
29
+ # UNSAFE_STRING_METHODS.each do |unsafe_method|
30
+ # if 'String'.respond_to?(unsafe_method)
31
+ # class_eval <<-EOT, __FILE__, __LINE__ + 1
32
+ # def #{unsafe_method}(*params, &block) # def capitalize(*params, &block)
33
+ # to_str.#{unsafe_method}(*params, &block) # to_str.capitalize(*params, &block)
34
+ # end # end
35
+ #
36
+ # def #{unsafe_method}!(*params) # def capitalize!(*params)
37
+ # @dirty = true # @dirty = true
38
+ # super # super
39
+ # end # end
40
+ # EOT
41
+ # end
42
+ # end
43
+ #
44
+ # # good, block comments in heredoc
45
+ # class_eval <<-EOT, __FILE__, __LINE__ + 1
46
+ # # def capitalize!(*params)
47
+ # # @dirty = true
48
+ # # super
49
+ # # end
50
+ #
51
+ # def #{unsafe_method}!(*params)
52
+ # @dirty = true
53
+ # super
54
+ # end
55
+ # EOT
56
+ #
57
+ # # good, block comments before heredoc
58
+ # class_eval(
59
+ # # def capitalize!(*params)
60
+ # # @dirty = true
61
+ # # super
62
+ # # end
63
+ #
64
+ # <<-EOT, __FILE__, __LINE__ + 1
65
+ # def #{unsafe_method}!(*params)
66
+ # @dirty = true
67
+ # super
68
+ # end
69
+ # EOT
70
+ # )
71
+ #
72
+ # # bad - interpolated string without comment
73
+ # class_eval("def #{unsafe_method}!(*params); end")
74
+ #
75
+ # # good - with inline comment or replace it with block comment using heredoc
76
+ # class_eval("def #{unsafe_method}!(*params); end # def capitalize!(*params); end")
77
+ class DocumentDynamicEvalDefinition < Base
78
+ BLOCK_COMMENT_REGEXP = /^\s*#(?!{)/.freeze
79
+ COMMENT_REGEXP = /\s*#(?!{).*/.freeze
80
+ MSG = 'Add a comment block showing its appearance if interpolated.'
81
+
82
+ RESTRICT_ON_SEND = %i[eval class_eval module_eval instance_eval].freeze
83
+
84
+ def on_send(node)
85
+ arg_node = node.first_argument
86
+
87
+ return unless arg_node&.dstr_type? && interpolated?(arg_node)
88
+ return if inline_comment_docs?(arg_node) ||
89
+ arg_node.heredoc? && comment_block_docs?(arg_node)
90
+
91
+ add_offense(node.loc.selector)
92
+ end
93
+
94
+ private
95
+
96
+ def interpolated?(arg_node)
97
+ arg_node.each_child_node(:begin).any?
98
+ end
99
+
100
+ def inline_comment_docs?(node)
101
+ node.each_child_node(:begin).all? do |begin_node|
102
+ source_line = processed_source.lines[begin_node.first_line - 1]
103
+ source_line.match?(COMMENT_REGEXP)
104
+ end
105
+ end
106
+
107
+ def comment_block_docs?(arg_node)
108
+ comments = heredoc_comment_blocks(arg_node.loc.heredoc_body.line_span)
109
+ .concat(preceding_comment_blocks(arg_node.parent))
110
+
111
+ return if comments.none?
112
+
113
+ regexp = comment_regexp(arg_node)
114
+ comments.any? { |comment| regexp.match?(comment) } || regexp.match?(comments.join)
115
+ end
116
+
117
+ def preceding_comment_blocks(node)
118
+ # Collect comments in the method call, but outside the heredoc
119
+ comments = processed_source.each_comment_in_lines(node.loc.expression.line_span)
120
+
121
+ comments.each_with_object({}) do |comment, hash|
122
+ merge_adjacent_comments(comment.text, comment.loc.line, hash)
123
+ end.values
124
+ end
125
+
126
+ def heredoc_comment_blocks(heredoc_body)
127
+ # Collect comments inside the heredoc
128
+ line_range = (heredoc_body.begin - 1)..(heredoc_body.end - 1)
129
+ lines = processed_source.lines[line_range]
130
+
131
+ lines.each_with_object({}).with_index(line_range.begin) do |(line, hash), index|
132
+ merge_adjacent_comments(line, index, hash)
133
+ end.values
134
+ end
135
+
136
+ def merge_adjacent_comments(line, index, hash)
137
+ # Combine adjacent comment lines into a single string
138
+ return unless (line = line.dup.gsub!(BLOCK_COMMENT_REGEXP, ''))
139
+
140
+ hash[index] = if hash.keys.last == index - 1
141
+ [hash.delete(index - 1), line].join("\n")
142
+ else
143
+ line
144
+ end
145
+ end
146
+
147
+ def comment_regexp(arg_node)
148
+ # Replace the interpolations with wildcards
149
+ regexp_parts = arg_node.child_nodes.map do |n|
150
+ n.begin_type? ? /.+/ : source_to_regexp(n.source)
151
+ end
152
+
153
+ Regexp.new(regexp_parts.join)
154
+ end
155
+
156
+ def source_to_regexp(source)
157
+ # Get the source in the heredoc being `eval`ed, without any comments
158
+ # and turn it into a regexp
159
+ return /\s+/ if source.blank?
160
+
161
+ source = source.gsub(COMMENT_REGEXP, '')
162
+ return if source.blank?
163
+
164
+ /\s*#{Regexp.escape(source.strip)}/
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end