rubocop 1.32.0 → 1.35.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 (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