rubocop 1.32.0 → 1.35.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/config/default.yml +73 -16
  4. data/config/obsoletion.yml +23 -1
  5. data/lib/rubocop/cache_config.rb +29 -0
  6. data/lib/rubocop/cli/command/{auto_genenerate_config.rb → auto_generate_config.rb} +2 -2
  7. data/lib/rubocop/cli/command/init_dotfile.rb +1 -1
  8. data/lib/rubocop/cli/command/suggest_extensions.rb +53 -15
  9. data/lib/rubocop/config.rb +1 -1
  10. data/lib/rubocop/config_finder.rb +68 -0
  11. data/lib/rubocop/config_loader.rb +12 -40
  12. data/lib/rubocop/config_loader_resolver.rb +1 -5
  13. data/lib/rubocop/config_obsoletion/changed_parameter.rb +5 -0
  14. data/lib/rubocop/config_obsoletion/parameter_rule.rb +4 -0
  15. data/lib/rubocop/config_obsoletion.rb +7 -2
  16. data/lib/rubocop/cop/cop.rb +1 -1
  17. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +58 -0
  18. data/lib/rubocop/cop/gemspec/require_mfa.rb +1 -1
  19. data/lib/rubocop/cop/generator/require_file_injector.rb +2 -2
  20. data/lib/rubocop/cop/internal_affairs/numblock_handler.rb +69 -0
  21. data/lib/rubocop/cop/internal_affairs/single_line_comparison.rb +62 -0
  22. data/lib/rubocop/cop/internal_affairs.rb +2 -0
  23. data/lib/rubocop/cop/layout/block_alignment.rb +2 -0
  24. data/lib/rubocop/cop/layout/block_end_newline.rb +35 -5
  25. data/lib/rubocop/cop/layout/empty_lines_around_access_modifier.rb +5 -2
  26. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +2 -0
  27. data/lib/rubocop/cop/layout/end_of_line.rb +4 -4
  28. data/lib/rubocop/cop/layout/first_argument_indentation.rb +6 -1
  29. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +2 -2
  30. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +2 -2
  31. data/lib/rubocop/cop/layout/indentation_width.rb +2 -0
  32. data/lib/rubocop/cop/layout/line_length.rb +4 -1
  33. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +1 -1
  34. data/lib/rubocop/cop/layout/multiline_block_layout.rb +2 -0
  35. data/lib/rubocop/cop/layout/redundant_line_break.rb +1 -1
  36. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +1 -1
  37. data/lib/rubocop/cop/layout/space_around_keyword.rb +1 -1
  38. data/lib/rubocop/cop/layout/space_before_block_braces.rb +2 -0
  39. data/lib/rubocop/cop/legacy/corrections_proxy.rb +1 -1
  40. data/lib/rubocop/cop/legacy/corrector.rb +1 -1
  41. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +21 -8
  42. data/lib/rubocop/cop/lint/debugger.rb +26 -16
  43. data/lib/rubocop/cop/lint/deprecated_class_methods.rb +4 -4
  44. data/lib/rubocop/cop/lint/empty_block.rb +1 -1
  45. data/lib/rubocop/cop/lint/empty_conditional_body.rb +65 -1
  46. data/lib/rubocop/cop/lint/erb_new_arguments.rb +9 -9
  47. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +4 -0
  48. data/lib/rubocop/cop/lint/next_without_accumulator.rb +25 -6
  49. data/lib/rubocop/cop/lint/non_atomic_file_operation.rb +6 -6
  50. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +12 -0
  51. data/lib/rubocop/cop/lint/number_conversion.rb +24 -8
  52. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +9 -3
  53. data/lib/rubocop/cop/lint/redundant_with_index.rb +13 -10
  54. data/lib/rubocop/cop/lint/redundant_with_object.rb +12 -11
  55. data/lib/rubocop/cop/lint/shadowed_exception.rb +15 -0
  56. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +10 -1
  57. data/lib/rubocop/cop/lint/unreachable_loop.rb +7 -1
  58. data/lib/rubocop/cop/lint/useless_access_modifier.rb +6 -4
  59. data/lib/rubocop/cop/lint/void.rb +2 -0
  60. data/lib/rubocop/cop/metrics/abc_size.rb +3 -1
  61. data/lib/rubocop/cop/metrics/block_length.rb +6 -7
  62. data/lib/rubocop/cop/metrics/method_length.rb +8 -8
  63. data/lib/rubocop/cop/mixin/allowed_methods.rb +15 -1
  64. data/lib/rubocop/cop/mixin/allowed_pattern.rb +9 -1
  65. data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
  66. data/lib/rubocop/cop/mixin/comments_help.rb +5 -1
  67. data/lib/rubocop/cop/mixin/enforce_superclass.rb +2 -1
  68. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +76 -1
  69. data/lib/rubocop/cop/mixin/hash_transform_method.rb +1 -1
  70. data/lib/rubocop/cop/mixin/method_complexity.rb +8 -13
  71. data/lib/rubocop/cop/mixin/multiline_element_indentation.rb +1 -1
  72. data/lib/rubocop/cop/mixin/range_help.rb +4 -5
  73. data/lib/rubocop/cop/naming/block_parameter_name.rb +1 -1
  74. data/lib/rubocop/cop/naming/constant_name.rb +2 -2
  75. data/lib/rubocop/cop/naming/predicate_name.rb +24 -3
  76. data/lib/rubocop/cop/style/arguments_forwarding.rb +2 -2
  77. data/lib/rubocop/cop/style/block_delimiters.rb +26 -7
  78. data/lib/rubocop/cop/style/class_and_module_children.rb +4 -4
  79. data/lib/rubocop/cop/style/class_equality_comparison.rb +32 -7
  80. data/lib/rubocop/cop/style/class_methods_definitions.rb +2 -1
  81. data/lib/rubocop/cop/style/collection_methods.rb +2 -0
  82. data/lib/rubocop/cop/style/combinable_loops.rb +3 -1
  83. data/lib/rubocop/cop/style/double_negation.rb +2 -0
  84. data/lib/rubocop/cop/style/each_for_simple_loop.rb +1 -1
  85. data/lib/rubocop/cop/style/each_with_object.rb +39 -8
  86. data/lib/rubocop/cop/style/empty_block_parameter.rb +1 -1
  87. data/lib/rubocop/cop/style/empty_heredoc.rb +15 -1
  88. data/lib/rubocop/cop/style/empty_lambda_parameter.rb +1 -1
  89. data/lib/rubocop/cop/style/for.rb +2 -0
  90. data/lib/rubocop/cop/style/format_string_token.rb +21 -8
  91. data/lib/rubocop/cop/style/guard_clause.rb +27 -16
  92. data/lib/rubocop/cop/style/hash_each_methods.rb +3 -1
  93. data/lib/rubocop/cop/style/hash_except.rb +0 -4
  94. data/lib/rubocop/cop/style/hash_syntax.rb +17 -0
  95. data/lib/rubocop/cop/style/if_unless_modifier.rb +1 -1
  96. data/lib/rubocop/cop/style/inverse_methods.rb +8 -6
  97. data/lib/rubocop/cop/style/magic_comment_format.rb +307 -0
  98. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +2 -2
  99. data/lib/rubocop/cop/style/method_call_with_args_parentheses/require_parentheses.rb +5 -1
  100. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +7 -7
  101. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +11 -6
  102. data/lib/rubocop/cop/style/method_called_on_do_end_block.rb +4 -1
  103. data/lib/rubocop/cop/style/multiline_block_chain.rb +3 -1
  104. data/lib/rubocop/cop/style/multiline_in_pattern_then.rb +1 -1
  105. data/lib/rubocop/cop/style/next.rb +3 -5
  106. data/lib/rubocop/cop/style/nil_lambda.rb +1 -1
  107. data/lib/rubocop/cop/style/numeric_literals.rb +16 -1
  108. data/lib/rubocop/cop/style/numeric_predicate.rb +28 -8
  109. data/lib/rubocop/cop/style/object_then.rb +2 -0
  110. data/lib/rubocop/cop/style/proc.rb +4 -1
  111. data/lib/rubocop/cop/style/redundant_begin.rb +2 -0
  112. data/lib/rubocop/cop/style/redundant_condition.rb +19 -4
  113. data/lib/rubocop/cop/style/redundant_fetch_block.rb +1 -1
  114. data/lib/rubocop/cop/style/redundant_parentheses.rb +15 -22
  115. data/lib/rubocop/cop/style/redundant_self.rb +2 -0
  116. data/lib/rubocop/cop/style/redundant_sort.rb +21 -6
  117. data/lib/rubocop/cop/style/redundant_sort_by.rb +24 -8
  118. data/lib/rubocop/cop/style/safe_navigation.rb +4 -2
  119. data/lib/rubocop/cop/style/single_line_block_params.rb +1 -1
  120. data/lib/rubocop/cop/style/sole_nested_conditional.rb +14 -5
  121. data/lib/rubocop/cop/style/symbol_array.rb +1 -1
  122. data/lib/rubocop/cop/style/symbol_proc.rb +34 -9
  123. data/lib/rubocop/cop/style/ternary_parentheses.rb +1 -13
  124. data/lib/rubocop/cop/style/top_level_method_definition.rb +3 -1
  125. data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
  126. data/lib/rubocop/cop/style/word_array.rb +1 -1
  127. data/lib/rubocop/cop/util.rb +1 -1
  128. data/lib/rubocop/ext/range.rb +15 -0
  129. data/lib/rubocop/feature_loader.rb +94 -0
  130. data/lib/rubocop/formatter/clang_style_formatter.rb +1 -1
  131. data/lib/rubocop/formatter/disabled_config_formatter.rb +1 -1
  132. data/lib/rubocop/formatter/html_formatter.rb +3 -3
  133. data/lib/rubocop/formatter/markdown_formatter.rb +1 -1
  134. data/lib/rubocop/formatter/tap_formatter.rb +1 -1
  135. data/lib/rubocop/result_cache.rb +22 -20
  136. data/lib/rubocop/server/cache.rb +36 -1
  137. data/lib/rubocop/server/cli.rb +19 -2
  138. data/lib/rubocop/version.rb +1 -1
  139. data/lib/rubocop.rb +5 -3
  140. metadata +15 -9
  141. data/lib/rubocop/cop/mixin/ignored_methods.rb +0 -52
@@ -27,7 +27,7 @@ module RuboCop
27
27
 
28
28
  MSG = 'Use `Integer#times` for a simple loop which iterates a fixed number of times.'
29
29
 
30
- def on_block(node)
30
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
31
31
  return unless offending_each_range(node)
32
32
 
33
33
  send_node = node.send_node
@@ -23,13 +23,8 @@ module RuboCop
23
23
  MSG = 'Use `each_with_object` instead of `%<method>s`.'
24
24
  METHODS = %i[inject reduce].freeze
25
25
 
26
- # @!method each_with_object_candidate?(node)
27
- def_node_matcher :each_with_object_candidate?, <<~PATTERN
28
- (block $(send _ {:inject :reduce} _) $_ $_)
29
- PATTERN
30
-
31
26
  def on_block(node)
32
- each_with_object_candidate?(node) do |method, args, body|
27
+ each_with_object_block_candidate?(node) do |method, args, body|
33
28
  _, method_name, method_arg = *method
34
29
  return if simple_method_arg?(method_arg)
35
30
 
@@ -40,14 +35,38 @@ module RuboCop
40
35
 
41
36
  message = format(MSG, method: method_name)
42
37
  add_offense(method.loc.selector, message: message) do |corrector|
43
- autocorrect(corrector, node, return_value)
38
+ autocorrect_block(corrector, node, return_value)
39
+ end
40
+ end
41
+ end
42
+
43
+ def on_numblock(node)
44
+ each_with_object_numblock_candidate?(node) do |method, body|
45
+ _, method_name, method_arg = *method
46
+ return if simple_method_arg?(method_arg)
47
+
48
+ return unless return_value(body)&.source == '_1'
49
+
50
+ message = format(MSG, method: method_name)
51
+ add_offense(method.loc.selector, message: message) do |corrector|
52
+ autocorrect_numblock(corrector, node)
44
53
  end
45
54
  end
46
55
  end
47
56
 
48
57
  private
49
58
 
50
- def autocorrect(corrector, node, return_value)
59
+ # @!method each_with_object_block_candidate?(node)
60
+ def_node_matcher :each_with_object_block_candidate?, <<~PATTERN
61
+ (block $(send _ {:inject :reduce} _) $_ $_)
62
+ PATTERN
63
+
64
+ # @!method each_with_object_numblock_candidate?(node)
65
+ def_node_matcher :each_with_object_numblock_candidate?, <<~PATTERN
66
+ (numblock $(send _ {:inject :reduce} _) 2 $_)
67
+ PATTERN
68
+
69
+ def autocorrect_block(corrector, node, return_value)
51
70
  corrector.replace(node.send_node.loc.selector, 'each_with_object')
52
71
 
53
72
  first_arg, second_arg = *node.arguments
@@ -62,6 +81,18 @@ module RuboCop
62
81
  end
63
82
  end
64
83
 
84
+ def autocorrect_numblock(corrector, node)
85
+ corrector.replace(node.send_node.loc.selector, 'each_with_object')
86
+
87
+ # We don't remove the return value to avoid a clobbering error.
88
+ node.body.each_descendant do |var|
89
+ next unless var.lvar_type?
90
+
91
+ corrector.replace(var, '_2') if var.source == '_1'
92
+ corrector.replace(var, '_1') if var.source == '_2'
93
+ end
94
+ end
95
+
65
96
  def simple_method_arg?(method_arg)
66
97
  method_arg&.basic_literal?
67
98
  end
@@ -28,7 +28,7 @@ module RuboCop
28
28
 
29
29
  MSG = 'Omit pipes for the empty block parameters.'
30
30
 
31
- def on_block(node)
31
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
32
32
  send_node = node.send_node
33
33
  check(node) unless send_node.send_type? && send_node.lambda_literal?
34
34
  end
@@ -48,11 +48,25 @@ module RuboCop
48
48
  add_offense(node) do |corrector|
49
49
  heredoc_end = node.loc.heredoc_end
50
50
 
51
- corrector.replace(node, "''")
51
+ corrector.replace(node, preferred_string_literal)
52
52
  corrector.remove(range_by_whole_lines(heredoc_body, include_final_newline: true))
53
53
  corrector.remove(range_by_whole_lines(heredoc_end, include_final_newline: true))
54
54
  end
55
55
  end
56
+
57
+ private
58
+
59
+ def preferred_string_literal
60
+ enforce_double_quotes? ? '""' : "''"
61
+ end
62
+
63
+ def enforce_double_quotes?
64
+ string_literals_config['EnforcedStyle'] == 'double_quotes'
65
+ end
66
+
67
+ def string_literals_config
68
+ config.for_cop('Style/StringLiterals')
69
+ end
56
70
  end
57
71
  end
58
72
  end
@@ -23,7 +23,7 @@ module RuboCop
23
23
 
24
24
  MSG = 'Omit parentheses for the empty lambda parameters.'
25
25
 
26
- def on_block(node)
26
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
27
27
  send_node = node.send_node
28
28
  return unless send_node.send_type?
29
29
 
@@ -75,6 +75,8 @@ module RuboCop
75
75
  end
76
76
  end
77
77
 
78
+ alias on_numblock on_block
79
+
78
80
  private
79
81
 
80
82
  def suspect_enumerable?(node)
@@ -11,8 +11,8 @@ module RuboCop
11
11
  # The reason is that _unannotated_ format is very similar
12
12
  # to encoded URLs or Date/Time formatting strings.
13
13
  #
14
- # This cop can be customized ignored methods with `IgnoredMethods`.
15
- # By default, there are no methods to ignored.
14
+ # This cop can be customized allowed methods with `AllowedMethods`.
15
+ # By default, there are no methods to allowed.
16
16
  #
17
17
  # @example EnforcedStyle: annotated (default)
18
18
  #
@@ -62,23 +62,34 @@ module RuboCop
62
62
  # # good
63
63
  # format('%06d', 10)
64
64
  #
65
- # @example IgnoredMethods: [] (default)
65
+ # @example AllowedMethods: [] (default)
66
66
  #
67
67
  # # bad
68
68
  # redirect('foo/%{bar_id}')
69
69
  #
70
- # @example IgnoredMethods: [redirect]
70
+ # @example AllowedMethods: [redirect]
71
+ #
72
+ # # good
73
+ # redirect('foo/%{bar_id}')
74
+ #
75
+ # @example AllowedPatterns: [] (default)
76
+ #
77
+ # # bad
78
+ # redirect('foo/%{bar_id}')
79
+ #
80
+ # @example AllowedPatterns: [/redirect/]
71
81
  #
72
82
  # # good
73
83
  # redirect('foo/%{bar_id}')
74
84
  #
75
85
  class FormatStringToken < Base
76
86
  include ConfigurableEnforcedStyle
77
- include IgnoredMethods
87
+ include AllowedMethods
88
+ include AllowedPattern
78
89
  extend AutoCorrector
79
90
 
80
91
  def on_str(node)
81
- return if format_string_token?(node) || use_ignored_method?(node)
92
+ return if format_string_token?(node) || use_allowed_method?(node)
82
93
 
83
94
  detections = collect_detections(node)
84
95
  return if detections.empty?
@@ -103,9 +114,11 @@ module RuboCop
103
114
  !node.value.include?('%') || node.each_ancestor(:xstr, :regexp).any?
104
115
  end
105
116
 
106
- def use_ignored_method?(node)
117
+ def use_allowed_method?(node)
107
118
  send_parent = node.each_ancestor(:send).first
108
- send_parent && ignored_method?(send_parent.method_name)
119
+ send_parent &&
120
+ (allowed_method?(send_parent.method_name) ||
121
+ matches_allowed_pattern?(send_parent.method_name))
109
122
  end
110
123
 
111
124
  def check_sequence(detected_sequence, token_range)
@@ -6,6 +6,10 @@ module RuboCop
6
6
  # Use a guard clause instead of wrapping the code inside a conditional
7
7
  # expression
8
8
  #
9
+ # A condition with an `elsif` or `else` branch is allowed unless
10
+ # one of `return`, `break`, `next`, `raise`, or `fail` is used
11
+ # in the body of the conditional expression.
12
+ #
9
13
  # @example
10
14
  # # bad
11
15
  # def test
@@ -50,34 +54,41 @@ module RuboCop
50
54
  #
51
55
  # @example AllowConsecutiveConditionals: false (default)
52
56
  # # bad
53
- # if foo?
54
- # work
55
- # end
57
+ # def test
58
+ # if foo?
59
+ # work
60
+ # end
56
61
  #
57
- # if bar? # <- reports an offense
58
- # work
62
+ # if bar? # <- reports an offense
63
+ # work
64
+ # end
59
65
  # end
60
66
  #
61
67
  # @example AllowConsecutiveConditionals: true
62
68
  # # good
63
- # if foo?
64
- # work
65
- # end
69
+ # def test
70
+ # if foo?
71
+ # work
72
+ # end
66
73
  #
67
- # if bar?
68
- # work
74
+ # if bar?
75
+ # work
76
+ # end
69
77
  # end
70
78
  #
71
79
  # # bad
72
- # if foo?
73
- # work
74
- # end
80
+ # def test
81
+ # if foo?
82
+ # work
83
+ # end
75
84
  #
76
- # do_something
85
+ # do_something
77
86
  #
78
- # if bar? # <- reports an offense
79
- # work
87
+ # if bar? # <- reports an offense
88
+ # work
89
+ # end
80
90
  # end
91
+ #
81
92
  class GuardClause < Base
82
93
  include MinBodyLength
83
94
  include StatementModifier
@@ -35,13 +35,15 @@ module RuboCop
35
35
 
36
36
  # @!method kv_each(node)
37
37
  def_node_matcher :kv_each, <<~PATTERN
38
- (block $(send (send _ ${:keys :values}) :each) ...)
38
+ ({block numblock} $(send (send _ ${:keys :values}) :each) ...)
39
39
  PATTERN
40
40
 
41
41
  def on_block(node)
42
42
  register_kv_offense(node)
43
43
  end
44
44
 
45
+ alias on_numblock on_block
46
+
45
47
  private
46
48
 
47
49
  def register_kv_offense(node)
@@ -159,10 +159,6 @@ module RuboCop
159
159
  key_argument = node.argument_list.first.source
160
160
  body = extract_body_if_nagated(node.body)
161
161
  lhs, _method_name, rhs = *body
162
-
163
- return lhs if body.method?('include?')
164
- return lhs if body.method?('exclude?')
165
- return rhs if body.method?('in?')
166
162
  return if [lhs, rhs].map(&:source).none?(key_argument)
167
163
 
168
164
  [lhs, rhs].find { |operand| operand.source != key_argument }
@@ -28,6 +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 "always", but will avoid mixing styles in a single hash
31
32
  #
32
33
  # @example EnforcedStyle: ruby19 (default)
33
34
  # # bad
@@ -89,6 +90,20 @@ module RuboCop
89
90
  # # good
90
91
  # {foo:, bar:}
91
92
  #
93
+ # @example EnforcedShorthandSyntax: consistent
94
+ #
95
+ # # bad
96
+ # {foo: , bar: bar}
97
+ #
98
+ # # good
99
+ # {foo:, bar:}
100
+ #
101
+ # # bad
102
+ # {foo: , bar: baz}
103
+ #
104
+ # # good
105
+ # {foo: foo, bar: baz}
106
+ #
92
107
  class HashSyntax < Base
93
108
  include ConfigurableEnforcedStyle
94
109
  include HashShorthandSyntax
@@ -104,6 +119,8 @@ module RuboCop
104
119
 
105
120
  return if pairs.empty?
106
121
 
122
+ on_hash_for_mixed_shorthand(node)
123
+
107
124
  if style == :hash_rockets || force_hash_rockets?(pairs)
108
125
  hash_rockets_check(pairs)
109
126
  elsif style == :ruby19_no_mixed_keys
@@ -96,7 +96,7 @@ module RuboCop
96
96
  return false unless max_line_length
97
97
 
98
98
  range = node.source_range
99
- return false unless range.first_line == range.last_line
99
+ return false unless range.single_line?
100
100
  return false unless line_length_enabled_at_line?(range.first_line)
101
101
 
102
102
  line = range.source_line
@@ -59,18 +59,18 @@ module RuboCop
59
59
  def_node_matcher :inverse_candidate?, <<~PATTERN
60
60
  {
61
61
  (send $(send $(...) $_ $...) :!)
62
- (send (block $(send $(...) $_) $...) :!)
62
+ (send ({block numblock} $(send $(...) $_) $...) :!)
63
63
  (send (begin $(send $(...) $_ $...)) :!)
64
64
  }
65
65
  PATTERN
66
66
 
67
67
  # @!method inverse_block?(node)
68
68
  def_node_matcher :inverse_block?, <<~PATTERN
69
- (block $(send (...) $_) ... { $(send ... :!)
70
- $(send (...) {:!= :!~} ...)
71
- (begin ... $(send ... :!))
72
- (begin ... $(send (...) {:!= :!~} ...))
73
- })
69
+ ({block numblock} $(send (...) $_) ... { $(send ... :!)
70
+ $(send (...) {:!= :!~} ...)
71
+ (begin ... $(send ... :!))
72
+ (begin ... $(send (...) {:!= :!~} ...))
73
+ })
74
74
  PATTERN
75
75
 
76
76
  def on_send(node)
@@ -102,6 +102,8 @@ module RuboCop
102
102
  end
103
103
  end
104
104
 
105
+ alias on_numblock on_block
106
+
105
107
  private
106
108
 
107
109
  def correct_inverse_method(corrector, node)
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Ensures magic comments are written consistently throughout your code base.
7
+ # Looks for discrepancies in separators (`-` vs `_`) and capitalization for
8
+ # both magic comment directives and values.
9
+ #
10
+ # Required capitalization can be set with the `DirectiveCapitalization` and
11
+ # `ValueCapitalization` configuration keys.
12
+ #
13
+ # NOTE: If one of these configuration is set to nil, any capitalization is allowed.
14
+ #
15
+ # @example EnforcedStyle: snake_case (default)
16
+ # # The `snake_case` style will enforce that the frozen string literal
17
+ # # comment is written in snake case. (Words separated by underscores)
18
+ # # bad
19
+ # # frozen-string-literal: true
20
+ #
21
+ # module Bar
22
+ # # ...
23
+ # end
24
+ #
25
+ # # good
26
+ # # frozen_string_literal: false
27
+ #
28
+ # module Bar
29
+ # # ...
30
+ # end
31
+ #
32
+ # @example EnforcedStyle: kebab_case
33
+ # # The `kebab_case` style will enforce that the frozen string literal
34
+ # # comment is written in kebab case. (Words separated by hyphens)
35
+ # # bad
36
+ # # frozen_string_literal: true
37
+ #
38
+ # module Baz
39
+ # # ...
40
+ # end
41
+ #
42
+ # # good
43
+ # # frozen-string-literal: true
44
+ #
45
+ # module Baz
46
+ # # ...
47
+ # end
48
+ #
49
+ # @example DirectiveCapitalization: lowercase (default)
50
+ # # bad
51
+ # # FROZEN-STRING-LITERAL: true
52
+ #
53
+ # # good
54
+ # # frozen-string-literal: true
55
+ #
56
+ # @example DirectiveCapitalization: uppercase
57
+ # # bad
58
+ # # frozen-string-literal: true
59
+ #
60
+ # # good
61
+ # # FROZEN-STRING-LITERAL: true
62
+ #
63
+ # @example DirectiveCapitalization: nil
64
+ # # any capitalization is accepted
65
+ #
66
+ # # good
67
+ # # frozen-string-literal: true
68
+ #
69
+ # # good
70
+ # # FROZEN-STRING-LITERAL: true
71
+ #
72
+ # @example ValueCapitalization: nil (default)
73
+ # # any capitalization is accepted
74
+ #
75
+ # # good
76
+ # # frozen-string-literal: true
77
+ #
78
+ # # good
79
+ # # frozen-string-literal: TRUE
80
+ #
81
+ # @example ValueCapitalization: lowercase
82
+ # # when a value is not given, any capitalization is accepted
83
+ #
84
+ # # bad
85
+ # # frozen-string-literal: TRUE
86
+ #
87
+ # # good
88
+ # # frozen-string-literal: TRUE
89
+ #
90
+ # @example ValueCapitalization: uppercase
91
+ # # bad
92
+ # # frozen-string-literal: true
93
+ #
94
+ # # good
95
+ # # frozen-string-literal: TRUE
96
+ #
97
+ class MagicCommentFormat < Base
98
+ include ConfigurableEnforcedStyle
99
+ extend AutoCorrector
100
+
101
+ SNAKE_SEPARATOR = '_'
102
+ KEBAB_SEPARATOR = '-'
103
+ MSG = 'Prefer %<style>s case for magic comments.'
104
+ MSG_VALUE = 'Prefer %<case>s for magic comment values.'
105
+
106
+ # Value object to extract source ranges for the different parts of a magic comment
107
+ class CommentRange
108
+ extend Forwardable
109
+
110
+ DIRECTIVE_REGEXP = Regexp.union(MagicComment::KEYWORDS.map do |_, v|
111
+ Regexp.new(v, Regexp::IGNORECASE)
112
+ end).freeze
113
+
114
+ VALUE_REGEXP = Regexp.new("(?:#{DIRECTIVE_REGEXP}:\s*)(.*?)(?=;|$)")
115
+
116
+ def_delegators :@comment, :text, :loc
117
+ attr_reader :comment
118
+
119
+ def initialize(comment)
120
+ @comment = comment
121
+ end
122
+
123
+ # A magic comment can contain one directive (normal style) or
124
+ # multiple directives (emacs style)
125
+ def directives
126
+ @directives ||= begin
127
+ matches = []
128
+
129
+ text.scan(DIRECTIVE_REGEXP) do
130
+ offset = Regexp.last_match.offset(0)
131
+ matches << loc.expression.adjust(begin_pos: offset.first)
132
+ .with(end_pos: loc.expression.begin_pos + offset.last)
133
+ end
134
+
135
+ matches
136
+ end
137
+ end
138
+
139
+ # A magic comment can contain one value (normal style) or
140
+ # multiple directives (emacs style)
141
+ def values
142
+ @values ||= begin
143
+ matches = []
144
+
145
+ text.scan(VALUE_REGEXP) do
146
+ offset = Regexp.last_match.offset(1)
147
+ matches << loc.expression.adjust(begin_pos: offset.first)
148
+ .with(end_pos: loc.expression.begin_pos + offset.last)
149
+ end
150
+
151
+ matches
152
+ end
153
+ end
154
+ end
155
+
156
+ def on_new_investigation
157
+ return unless processed_source.ast
158
+
159
+ magic_comments.each do |comment|
160
+ issues = find_issues(comment)
161
+ register_offenses(issues) if issues.any?
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def magic_comments
168
+ processed_source.each_comment_in_lines(leading_comment_lines)
169
+ .select { |comment| MagicComment.parse(comment.text).valid? }
170
+ .map { |comment| CommentRange.new(comment) }
171
+ end
172
+
173
+ def leading_comment_lines
174
+ first_non_comment_token = processed_source.tokens.find { |token| !token.comment? }
175
+
176
+ if first_non_comment_token
177
+ 0...first_non_comment_token.line
178
+ else
179
+ (0..)
180
+ end
181
+ end
182
+
183
+ def find_issues(comment)
184
+ issues = { directives: [], values: [] }
185
+
186
+ comment.directives.each do |directive|
187
+ issues[:directives] << directive if directive_offends?(directive)
188
+ end
189
+
190
+ comment.values.each do |value| # rubocop:disable Style/HashEachMethods
191
+ issues[:values] << value if wrong_capitalization?(value.source, value_capitalization)
192
+ end
193
+
194
+ issues
195
+ end
196
+
197
+ def directive_offends?(directive)
198
+ incorrect_separator?(directive.source) ||
199
+ wrong_capitalization?(directive.source, directive_capitalization)
200
+ end
201
+
202
+ def register_offenses(issues)
203
+ fix_directives(issues[:directives])
204
+ fix_values(issues[:values])
205
+ end
206
+
207
+ def fix_directives(issues)
208
+ return if issues.empty?
209
+
210
+ msg = format(MSG, style: expected_style)
211
+
212
+ issues.each do |directive|
213
+ add_offense(directive, message: msg) do |corrector|
214
+ replacement = replace_separator(replace_capitalization(directive.source,
215
+ directive_capitalization))
216
+ corrector.replace(directive, replacement)
217
+ end
218
+ end
219
+ end
220
+
221
+ def fix_values(issues)
222
+ return if issues.empty?
223
+
224
+ msg = format(MSG_VALUE, case: value_capitalization)
225
+
226
+ issues.each do |value|
227
+ add_offense(value, message: msg) do |corrector|
228
+ corrector.replace(value, replace_capitalization(value.source, value_capitalization))
229
+ end
230
+ end
231
+ end
232
+
233
+ def expected_style
234
+ [directive_capitalization, style].compact.join(' ').gsub(/_?case\b/, '')
235
+ end
236
+
237
+ def wrong_separator
238
+ style == :snake_case ? KEBAB_SEPARATOR : SNAKE_SEPARATOR
239
+ end
240
+
241
+ def correct_separator
242
+ style == :snake_case ? SNAKE_SEPARATOR : KEBAB_SEPARATOR
243
+ end
244
+
245
+ def incorrect_separator?(text)
246
+ text[wrong_separator]
247
+ end
248
+
249
+ def wrong_capitalization?(text, expected_case)
250
+ return false unless expected_case
251
+
252
+ case expected_case
253
+ when :lowercase
254
+ text != text.downcase
255
+ when :uppercase
256
+ text != text.upcase
257
+ end
258
+ end
259
+
260
+ def replace_separator(text)
261
+ text.tr(wrong_separator, correct_separator)
262
+ end
263
+
264
+ def replace_capitalization(text, style)
265
+ return text unless style
266
+
267
+ case style
268
+ when :lowercase
269
+ text.downcase
270
+ when :uppercase
271
+ text.upcase
272
+ end
273
+ end
274
+
275
+ def line_range(line)
276
+ processed_source.buffer.line_range(line)
277
+ end
278
+
279
+ def directive_capitalization
280
+ cop_config['DirectiveCapitalization']&.to_sym.tap do |style|
281
+ unless valid_capitalization?(style)
282
+ raise "Unknown `DirectiveCapitalization` #{style} selected!"
283
+ end
284
+ end
285
+ end
286
+
287
+ def value_capitalization
288
+ cop_config['ValueCapitalization']&.to_sym.tap do |style|
289
+ unless valid_capitalization?(style)
290
+ raise "Unknown `ValueCapitalization` #{style} selected!"
291
+ end
292
+ end
293
+ end
294
+
295
+ def valid_capitalization?(style)
296
+ return true unless style
297
+
298
+ supported_capitalizations.include?(style)
299
+ end
300
+
301
+ def supported_capitalizations
302
+ cop_config['SupportedCapitalizations'].map(&:to_sym)
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end