rubocop 0.93.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -16
  3. data/config/default.yml +207 -77
  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 +26 -8
  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 +1 -1
  25. data/lib/rubocop/cop/layout/block_alignment.rb +3 -4
  26. data/lib/rubocop/cop/layout/class_structure.rb +7 -0
  27. data/lib/rubocop/cop/layout/def_end_alignment.rb +1 -1
  28. data/lib/rubocop/cop/layout/else_alignment.rb +15 -2
  29. data/lib/rubocop/cop/layout/empty_lines_around_access_modifier.rb +1 -0
  30. data/lib/rubocop/cop/layout/end_alignment.rb +3 -3
  31. data/lib/rubocop/cop/layout/extra_spacing.rb +1 -2
  32. data/lib/rubocop/cop/layout/hash_alignment.rb +4 -4
  33. data/lib/rubocop/cop/layout/line_length.rb +8 -1
  34. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +24 -18
  35. data/lib/rubocop/cop/layout/space_around_operators.rb +4 -1
  36. data/lib/rubocop/cop/layout/space_inside_parens.rb +35 -13
  37. data/lib/rubocop/cop/layout/trailing_whitespace.rb +37 -13
  38. data/lib/rubocop/cop/lint/ambiguous_regexp_literal.rb +6 -3
  39. data/lib/rubocop/cop/lint/constant_definition_in_block.rb +23 -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/redundant_safe_navigation.rb +44 -11
  56. data/lib/rubocop/cop/lint/shadowed_exception.rb +4 -5
  57. data/lib/rubocop/cop/lint/to_enum_arguments.rb +95 -0
  58. data/lib/rubocop/cop/lint/to_json.rb +1 -1
  59. data/lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb +185 -0
  60. data/lib/rubocop/cop/lint/useless_access_modifier.rb +2 -2
  61. data/lib/rubocop/cop/lint/useless_method_definition.rb +2 -4
  62. data/lib/rubocop/cop/lint/useless_setter_call.rb +6 -1
  63. data/lib/rubocop/cop/metrics/class_length.rb +9 -3
  64. data/lib/rubocop/cop/metrics/parameter_lists.rb +4 -1
  65. data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
  66. data/lib/rubocop/cop/mixin/configurable_numbering.rb +3 -3
  67. data/lib/rubocop/cop/mixin/line_length_help.rb +1 -1
  68. data/lib/rubocop/cop/mixin/statement_modifier.rb +9 -4
  69. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +12 -2
  70. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +11 -5
  71. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +67 -18
  72. data/lib/rubocop/cop/naming/predicate_name.rb +2 -1
  73. data/lib/rubocop/cop/naming/variable_number.rb +98 -8
  74. data/lib/rubocop/cop/offense.rb +3 -3
  75. data/lib/rubocop/cop/security/open.rb +12 -10
  76. data/lib/rubocop/cop/style/accessor_grouping.rb +1 -1
  77. data/lib/rubocop/cop/style/and_or.rb +1 -3
  78. data/lib/rubocop/cop/style/arguments_forwarding.rb +142 -0
  79. data/lib/rubocop/cop/style/bisected_attr_accessor.rb +0 -4
  80. data/lib/rubocop/cop/style/case_like_if.rb +0 -4
  81. data/lib/rubocop/cop/style/class_equality_comparison.rb +19 -4
  82. data/lib/rubocop/cop/style/collection_compact.rb +91 -0
  83. data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +162 -0
  84. data/lib/rubocop/cop/style/double_negation.rb +6 -1
  85. data/lib/rubocop/cop/style/format_string_token.rb +47 -2
  86. data/lib/rubocop/cop/style/hash_syntax.rb +3 -3
  87. data/lib/rubocop/cop/style/identical_conditional_branches.rb +7 -2
  88. data/lib/rubocop/cop/style/if_inside_else.rb +37 -1
  89. data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -3
  90. data/lib/rubocop/cop/style/infinite_loop.rb +4 -0
  91. data/lib/rubocop/cop/style/keyword_parameters_order.rb +12 -0
  92. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +10 -13
  93. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +6 -11
  94. data/lib/rubocop/cop/style/method_call_with_args_parentheses/require_parentheses.rb +7 -11
  95. data/lib/rubocop/cop/style/mixin_grouping.rb +0 -4
  96. data/lib/rubocop/cop/style/multiple_comparison.rb +55 -7
  97. data/lib/rubocop/cop/style/negated_if_else_condition.rb +104 -0
  98. data/lib/rubocop/cop/style/nil_lambda.rb +52 -0
  99. data/lib/rubocop/cop/style/raise_args.rb +21 -6
  100. data/lib/rubocop/cop/style/redundant_begin.rb +14 -4
  101. data/lib/rubocop/cop/style/redundant_parentheses.rb +4 -0
  102. data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +7 -1
  103. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +1 -1
  104. data/lib/rubocop/cop/style/redundant_self.rb +3 -0
  105. data/lib/rubocop/cop/style/safe_navigation.rb +16 -4
  106. data/lib/rubocop/cop/style/semicolon.rb +3 -0
  107. data/lib/rubocop/cop/style/static_class.rb +97 -0
  108. data/lib/rubocop/cop/style/string_concatenation.rb +13 -1
  109. data/lib/rubocop/cop/style/swap_values.rb +108 -0
  110. data/lib/rubocop/cop/style/ternary_parentheses.rb +1 -1
  111. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +3 -1
  112. data/lib/rubocop/cop/style/while_until_modifier.rb +9 -0
  113. data/lib/rubocop/cop/team.rb +6 -1
  114. data/lib/rubocop/cop/util.rb +5 -1
  115. data/lib/rubocop/ext/regexp_node.rb +17 -9
  116. data/lib/rubocop/ext/regexp_parser.rb +84 -0
  117. data/lib/rubocop/formatter/formatter_set.rb +2 -1
  118. data/lib/rubocop/formatter/git_hub_actions_formatter.rb +47 -0
  119. data/lib/rubocop/formatter/offense_count_formatter.rb +1 -1
  120. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -1
  121. data/lib/rubocop/magic_comment.rb +2 -2
  122. data/lib/rubocop/options.rb +6 -1
  123. data/lib/rubocop/rspec/shared_contexts.rb +4 -0
  124. data/lib/rubocop/target_ruby.rb +57 -1
  125. data/lib/rubocop/version.rb +56 -6
  126. metadata +21 -5
@@ -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
@@ -21,7 +21,7 @@ module RuboCop
21
21
  include IgnoredMethods
22
22
  extend AutoCorrector
23
23
 
24
- MSG = 'Use `Object.instance_of?` instead of comparing classes.'
24
+ MSG = 'Use `instance_of?(%<class_name>s)` instead of comparing classes.'
25
25
 
26
26
  RESTRICT_ON_SEND = %i[== equal? eql?].freeze
27
27
 
@@ -36,13 +36,28 @@ module RuboCop
36
36
  return if def_node && ignored_method?(def_node.method_name)
37
37
 
38
38
  class_comparison_candidate?(node) do |receiver_node, class_node|
39
- range = range_between(receiver_node.loc.selector.begin_pos, node.source_range.end_pos)
39
+ range = offense_range(receiver_node, node)
40
+ class_name = class_name(class_node, node)
40
41
 
41
- add_offense(range) do |corrector|
42
- corrector.replace(range, "instance_of?(#{class_node.source})")
42
+ add_offense(range, message: format(MSG, class_name: class_name)) do |corrector|
43
+ corrector.replace(range, "instance_of?(#{class_name})")
43
44
  end
44
45
  end
45
46
  end
47
+
48
+ private
49
+
50
+ def class_name(class_node, node)
51
+ if node.children.first.method?(:name)
52
+ class_node.source.delete('"').delete("'")
53
+ else
54
+ class_node.source
55
+ end
56
+ end
57
+
58
+ def offense_range(receiver_node, node)
59
+ range_between(receiver_node.loc.selector.begin_pos, node.source_range.end_pos)
60
+ end
46
61
  end
47
62
  end
48
63
  end
@@ -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,162 @@
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
+ class DocumentDynamicEvalDefinition < Base
72
+ BLOCK_COMMENT_REGEXP = /^\s*#(?!{)/.freeze
73
+ COMMENT_REGEXP = /\s*#(?!{).*/.freeze
74
+ MSG = 'Add a comment block showing its appearance if interpolated.'
75
+
76
+ RESTRICT_ON_SEND = %i[eval class_eval module_eval instance_eval].freeze
77
+
78
+ def on_send(node)
79
+ arg_node = node.first_argument
80
+
81
+ return unless arg_node&.dstr_type? && interpolated?(arg_node)
82
+ return if inline_comment_docs?(arg_node) || comment_block_docs?(arg_node)
83
+
84
+ add_offense(node.loc.selector)
85
+ end
86
+
87
+ private
88
+
89
+ def interpolated?(arg_node)
90
+ arg_node.each_child_node(:begin).any?
91
+ end
92
+
93
+ def inline_comment_docs?(node)
94
+ node.each_child_node(:begin).all? do |begin_node|
95
+ source_line = processed_source.lines[begin_node.first_line - 1]
96
+ source_line.match?(COMMENT_REGEXP)
97
+ end
98
+ end
99
+
100
+ def comment_block_docs?(arg_node)
101
+ comments = heredoc_comment_blocks(arg_node.loc.heredoc_body.line_span)
102
+ .concat(preceding_comment_blocks(arg_node.parent))
103
+
104
+ return if comments.none?
105
+
106
+ regexp = comment_regexp(arg_node)
107
+ comments.any? { |comment| regexp.match?(comment) } || regexp.match?(comments.join)
108
+ end
109
+
110
+ def preceding_comment_blocks(node)
111
+ # Collect comments in the method call, but outside the heredoc
112
+ comments = processed_source.each_comment_in_lines(node.loc.expression.line_span)
113
+
114
+ comments.each_with_object({}) do |comment, hash|
115
+ merge_adjacent_comments(comment.text, comment.loc.line, hash)
116
+ end.values
117
+ end
118
+
119
+ def heredoc_comment_blocks(heredoc_body)
120
+ # Collect comments inside the heredoc
121
+ line_range = (heredoc_body.begin - 1)..(heredoc_body.end - 1)
122
+ lines = processed_source.lines[line_range]
123
+
124
+ lines.each_with_object({}).with_index(line_range.begin) do |(line, hash), index|
125
+ merge_adjacent_comments(line, index, hash)
126
+ end.values
127
+ end
128
+
129
+ def merge_adjacent_comments(line, index, hash)
130
+ # Combine adjacent comment lines into a single string
131
+ return unless (line = line.dup.gsub!(BLOCK_COMMENT_REGEXP, ''))
132
+
133
+ hash[index] = if hash.keys.last == index - 1
134
+ [hash.delete(index - 1), line].join("\n")
135
+ else
136
+ line
137
+ end
138
+ end
139
+
140
+ def comment_regexp(arg_node)
141
+ # Replace the interpolations with wildcards
142
+ regexp_parts = arg_node.child_nodes.map do |n|
143
+ n.begin_type? ? /.+/ : source_to_regexp(n.source)
144
+ end
145
+
146
+ Regexp.new(regexp_parts.join)
147
+ end
148
+
149
+ def source_to_regexp(source)
150
+ # Get the source in the heredoc being `eval`ed, without any comments
151
+ # and turn it into a regexp
152
+ return /\s+/ if source.blank?
153
+
154
+ source = source.gsub(COMMENT_REGEXP, '')
155
+ return if source.blank?
156
+
157
+ /\s*#{Regexp.escape(source.strip)}/
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end