rubocop 1.88.0 → 1.89.0

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 (197) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +72 -12
  3. data/lib/rubocop/comment_config.rb +35 -19
  4. data/lib/rubocop/config.rb +53 -0
  5. data/lib/rubocop/config_validator.rb +38 -1
  6. data/lib/rubocop/cop/badge.rb +8 -0
  7. data/lib/rubocop/cop/base.rb +19 -12
  8. data/lib/rubocop/cop/bundler/gem_comment.rb +3 -1
  9. data/lib/rubocop/cop/bundler.rb +19 -0
  10. data/lib/rubocop/cop/commissioner.rb +13 -11
  11. data/lib/rubocop/cop/corrector.rb +1 -1
  12. data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
  13. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  14. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  15. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  16. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  17. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +12 -2
  18. data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
  19. data/lib/rubocop/cop/gemspec.rb +22 -0
  20. data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
  21. data/lib/rubocop/cop/generator.rb +4 -0
  22. data/lib/rubocop/cop/layout/block_alignment.rb +17 -0
  23. data/lib/rubocop/cop/layout/class_structure.rb +14 -4
  24. data/lib/rubocop/cop/layout/comment_indentation.rb +9 -2
  25. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  26. data/lib/rubocop/cop/layout/else_alignment.rb +1 -14
  27. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  28. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
  29. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
  30. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
  31. data/lib/rubocop/cop/layout/indentation_width.rb +31 -1
  32. data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
  33. data/lib/rubocop/cop/layout/line_length.rb +95 -17
  34. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +18 -3
  35. data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
  36. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  37. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +33 -34
  38. data/lib/rubocop/cop/layout.rb +114 -0
  39. data/lib/rubocop/cop/lazy_loader.rb +41 -0
  40. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
  41. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
  42. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  43. data/lib/rubocop/cop/lint/constant_reassignment.rb +1 -10
  44. data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
  45. data/lib/rubocop/cop/lint/debugger.rb +13 -1
  46. data/lib/rubocop/cop/lint/deprecated_reference.rb +185 -0
  47. data/lib/rubocop/cop/lint/duplicate_methods.rb +202 -22
  48. data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
  49. data/lib/rubocop/cop/lint/literal_as_condition.rb +5 -0
  50. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +1 -1
  51. data/lib/rubocop/cop/lint/missing_super.rb +31 -1
  52. data/lib/rubocop/cop/lint/name_typo.rb +223 -0
  53. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +7 -0
  54. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +0 -4
  55. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +7 -0
  56. data/lib/rubocop/cop/lint/to_enum_arguments.rb +44 -2
  57. data/lib/rubocop/cop/lint/to_json.rb +8 -1
  58. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +32 -8
  59. data/lib/rubocop/cop/lint/unreachable_code.rb +9 -4
  60. data/lib/rubocop/cop/lint/unused_private_method.rb +157 -0
  61. data/lib/rubocop/cop/lint/useless_numeric_operation.rb +10 -10
  62. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
  63. data/lib/rubocop/cop/lint/void.rb +19 -8
  64. data/lib/rubocop/cop/lint.rb +176 -0
  65. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  66. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  67. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  68. data/lib/rubocop/cop/metrics.rb +31 -0
  69. data/lib/rubocop/cop/migration/department_name.rb +0 -4
  70. data/lib/rubocop/cop/migration.rb +13 -0
  71. data/lib/rubocop/cop/mixin/allowed_methods.rb +5 -5
  72. data/lib/rubocop/cop/mixin/allowed_pattern.rb +5 -1
  73. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
  74. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
  75. data/lib/rubocop/cop/mixin/forbidden_pattern.rb +5 -1
  76. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
  77. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  78. data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
  79. data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
  80. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
  81. data/lib/rubocop/cop/mixin/statement_modifier.rb +43 -7
  82. data/lib/rubocop/cop/mixin.rb +78 -77
  83. data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
  84. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  85. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  86. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
  87. data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
  88. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
  89. data/lib/rubocop/cop/naming/variable_number.rb +3 -1
  90. data/lib/rubocop/cop/naming.rb +31 -0
  91. data/lib/rubocop/cop/offense.rb +10 -6
  92. data/lib/rubocop/cop/registry.rb +153 -43
  93. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  94. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  95. data/lib/rubocop/cop/security.rb +19 -0
  96. data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
  97. data/lib/rubocop/cop/style/arguments_forwarding.rb +1 -1
  98. data/lib/rubocop/cop/style/array_intersect.rb +24 -5
  99. data/lib/rubocop/cop/style/class_and_module_children.rb +78 -18
  100. data/lib/rubocop/cop/style/collection_compact.rb +1 -1
  101. data/lib/rubocop/cop/style/conditional_assignment.rb +22 -0
  102. data/lib/rubocop/cop/style/copyright.rb +6 -1
  103. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  104. data/lib/rubocop/cop/style/def_with_parentheses.rb +6 -2
  105. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  106. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +41 -3
  107. data/lib/rubocop/cop/style/documentation.rb +47 -0
  108. data/lib/rubocop/cop/style/documentation_method.rb +5 -1
  109. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  110. data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
  111. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  112. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  113. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  114. data/lib/rubocop/cop/style/endless_method.rb +0 -10
  115. data/lib/rubocop/cop/style/env_home.rb +4 -0
  116. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  117. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  118. data/lib/rubocop/cop/style/file_null.rb +4 -2
  119. data/lib/rubocop/cop/style/format_string.rb +13 -1
  120. data/lib/rubocop/cop/style/hash_conversion.rb +14 -6
  121. data/lib/rubocop/cop/style/hash_lookup_method.rb +2 -6
  122. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  123. data/lib/rubocop/cop/style/identical_conditional_branches.rb +15 -4
  124. data/lib/rubocop/cop/style/if_unless_modifier.rb +11 -52
  125. data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
  126. data/lib/rubocop/cop/style/inline_comment.rb +1 -1
  127. data/lib/rubocop/cop/style/invertible_unless_condition.rb +25 -5
  128. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  129. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  130. data/lib/rubocop/cop/style/lambda.rb +11 -1
  131. data/lib/rubocop/cop/style/lambda_call.rb +11 -0
  132. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  133. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  134. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +20 -3
  135. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +11 -5
  136. data/lib/rubocop/cop/style/method_def_parentheses.rb +5 -1
  137. data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
  138. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +40 -7
  139. data/lib/rubocop/cop/style/mixin_usage.rb +1 -1
  140. data/lib/rubocop/cop/style/module_function.rb +4 -2
  141. data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
  142. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  143. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  144. data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
  145. data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
  146. data/lib/rubocop/cop/style/negative_array_index.rb +3 -1
  147. data/lib/rubocop/cop/style/nil_lambda.rb +8 -0
  148. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  149. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  150. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  151. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  152. data/lib/rubocop/cop/style/parallel_assignment.rb +11 -2
  153. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  154. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  155. data/lib/rubocop/cop/style/redundant_assignment.rb +11 -0
  156. data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
  157. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +13 -1
  158. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  159. data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
  160. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  161. data/lib/rubocop/cop/style/redundant_format.rb +40 -3
  162. data/lib/rubocop/cop/style/redundant_heredoc_delimiter_quotes.rb +6 -1
  163. data/lib/rubocop/cop/style/redundant_line_continuation.rb +80 -129
  164. data/lib/rubocop/cop/style/redundant_parentheses.rb +63 -63
  165. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  166. data/lib/rubocop/cop/style/redundant_self.rb +9 -0
  167. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
  168. data/lib/rubocop/cop/style/safe_navigation.rb +10 -0
  169. data/lib/rubocop/cop/style/semicolon.rb +4 -4
  170. data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
  171. data/lib/rubocop/cop/style/sole_nested_conditional.rb +20 -3
  172. data/lib/rubocop/cop/style/static_class.rb +21 -0
  173. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  174. data/lib/rubocop/cop/style/struct_inheritance.rb +3 -1
  175. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  176. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  177. data/lib/rubocop/cop/style/trivial_accessors.rb +1 -1
  178. data/lib/rubocop/cop/style.rb +310 -0
  179. data/lib/rubocop/cop/team.rb +66 -7
  180. data/lib/rubocop/cop/variable_force/variable.rb +18 -6
  181. data/lib/rubocop/cops_documentation_generator.rb +1 -1
  182. data/lib/rubocop/directive_comment.rb +2 -0
  183. data/lib/rubocop/feature_loader.rb +0 -6
  184. data/lib/rubocop/lsp/routes.rb +41 -3
  185. data/lib/rubocop/lsp/runtime.rb +4 -0
  186. data/lib/rubocop/lsp/server.rb +4 -0
  187. data/lib/rubocop/lsp/stdin_runner.rb +20 -0
  188. data/lib/rubocop/pending_cops_reporter.rb +1 -6
  189. data/lib/rubocop/project_index_loader.rb +16 -0
  190. data/lib/rubocop/result_cache.rb +6 -4
  191. data/lib/rubocop/rspec/shared_contexts.rb +14 -2
  192. data/lib/rubocop/runner.rb +100 -33
  193. data/lib/rubocop/server/cache.rb +10 -5
  194. data/lib/rubocop/target_ruby.rb +1 -1
  195. data/lib/rubocop/version.rb +1 -1
  196. data/lib/rubocop.rb +17 -626
  197. metadata +18 -3
@@ -12,20 +12,28 @@ module RuboCop
12
12
  class << self
13
13
  attr_reader :processed_source
14
14
 
15
- def correct(corrector, processed_source, node, column_delta)
15
+ # `tab_indentation` may only be set by callers whose `column_delta` represents
16
+ # whole indentation levels (e.g. `Layout/IndentationWidth`).
17
+ # Alignment to an arbitrary column cannot be expressed with tabs, so those callers
18
+ # leave tab correction disabled to avoid producing a never-converging correction.
19
+ def correct(corrector, processed_source, node, column_delta, tab_indentation: false)
16
20
  return unless node
17
21
 
18
22
  @processed_source = processed_source
19
- # Disable autocorrection for tabs as it requires special handling
20
- return if using_tabs?
21
23
 
22
24
  expr = node.respond_to?(:loc) ? node.source_range : node
23
25
  return if block_comment_within?(expr)
24
26
 
25
27
  taboo_ranges = inside_string_ranges(node)
26
28
 
27
- each_line(expr) do |line_begin_pos|
28
- autocorrect_line(corrector, line_begin_pos, expr, column_delta, taboo_ranges)
29
+ if using_tabs?
30
+ return unless tab_indentation
31
+
32
+ correct_tab_indentation(corrector, expr, column_delta, taboo_ranges)
33
+ else
34
+ each_line(expr) do |line_begin_pos|
35
+ autocorrect_line(corrector, line_begin_pos, expr, column_delta, taboo_ranges)
36
+ end
29
37
  end
30
38
  end
31
39
 
@@ -58,6 +66,48 @@ module RuboCop
58
66
  end
59
67
  end
60
68
 
69
+ # Tab indentation is corrected by rewriting each line's leading whitespace to
70
+ # the target number of tabs. Working in whole tabs rather than applying a column delta
71
+ # keeps the result idempotent, which avoids the infinite loops that delta-based correction
72
+ # caused for tabs.
73
+ def correct_tab_indentation(corrector, expr, column_delta, taboo_ranges)
74
+ buffer = expr.source_buffer
75
+
76
+ each_line(expr) do |line_begin_pos|
77
+ line_range = buffer.line_range(buffer.line_for_position(line_begin_pos))
78
+ correct_tab_line(corrector, line_range, column_delta, taboo_ranges)
79
+ end
80
+ end
81
+
82
+ def correct_tab_line(corrector, line_range, column_delta, taboo_ranges)
83
+ leading = line_range.source[/\A[ \t]*/]
84
+ return if leading.length == line_range.source.length # blank line
85
+
86
+ leading_range = range_between(line_range.begin_pos, line_range.begin_pos + leading.length)
87
+ return if taboo_ranges.any? { |t| within?(leading_range, t) }
88
+
89
+ target = target_tab_indentation(leading, column_delta)
90
+ corrector.replace(leading_range, target) unless leading == target
91
+ end
92
+
93
+ def target_tab_indentation(leading, column_delta)
94
+ width = configured_indentation_width
95
+ return leading unless width.positive?
96
+
97
+ visual_width = leading.chars.sum { |char| char == "\t" ? width : 1 }
98
+
99
+ "\t" * ([visual_width + column_delta, 0].max / width)
100
+ end
101
+
102
+ # The number of columns a single tab spans. This must match the width
103
+ # `Layout/IndentationWidth` uses to compute `column_delta`, otherwise
104
+ # the division below would not land on a tab boundary. `Layout/IndentationStyle`
105
+ # has its own `IndentationWidth`, but that only governs how that cop replaces tabs
106
+ # with spaces, so it must not be consulted here.
107
+ def configured_indentation_width
108
+ processed_source.config.for_cop('Layout/IndentationWidth')['Width'] || 2
109
+ end
110
+
61
111
  def inside_string_ranges(node)
62
112
  return [] unless node.is_a?(Parser::AST::Node)
63
113
 
@@ -27,7 +27,7 @@ module RuboCop
27
27
  if block_node.arguments?
28
28
  format(CORRECTION_WITH_ARGUMENTS,
29
29
  collection: collection_node.source,
30
- variables: argument_node.children.first.source)
30
+ variables: argument_node.children.map(&:source).join(', '))
31
31
  else
32
32
  format(CORRECTION_WITHOUT_ARGUMENTS, enumerable: collection_node.source)
33
33
  end
@@ -86,7 +86,13 @@ module RuboCop
86
86
  end
87
87
 
88
88
  def lambda_arg_string
89
- arguments.children.map(&:source).join(', ')
89
+ # Block-local (shadow) arguments are separated from regular arguments by a
90
+ # `;`; joining everything with `,` would turn them into extra parameters
91
+ # and change the lambda's arity.
92
+ regular, shadow = arguments.children.partition { |arg| !arg.shadowarg_type? }
93
+ arg_string = regular.map(&:source).join(', ')
94
+ arg_string += "; #{shadow.map(&:source).join(', ')}" unless shadow.empty?
95
+ arg_string
90
96
  end
91
97
 
92
98
  def needs_separating_space?
@@ -18,7 +18,14 @@ module RuboCop
18
18
  current_range = declaration_with_comment(node)
19
19
  previous_range = declaration_with_comment(previous_declaration)
20
20
 
21
- ->(corrector) { corrector.swap(current_range, previous_range) }
21
+ lambda do |corrector|
22
+ if current_range.source.end_with?("\n")
23
+ corrector.swap(current_range, previous_range)
24
+ else
25
+ corrector.insert_before(previous_range, "#{current_range.source}\n")
26
+ corrector.remove(current_range)
27
+ end
28
+ end
22
29
  end
23
30
 
24
31
  private
@@ -95,7 +95,7 @@ module RuboCop
95
95
  private
96
96
 
97
97
  def forbidden_gem?(gem_name)
98
- !cop_config['AllowedGems'].include?(gem_name)
98
+ !Array(cop_config['AllowedGems']).include?(gem_name)
99
99
  end
100
100
 
101
101
  def message(_range)
@@ -83,18 +83,28 @@ module RuboCop
83
83
  def duplicated_assignment_method_nodes
84
84
  assignment_method_declarations(processed_source.ast)
85
85
  .select(&:assignment_method?)
86
- .group_by(&:method_name)
86
+ .group_by { |node| [enclosing_specification(node), node.method_name] }
87
87
  .values
88
88
  .select { |nodes| nodes.size > 1 }
89
89
  end
90
90
 
91
91
  def duplicated_indexed_assignment_method_nodes
92
92
  indexed_assignment_method_declarations(processed_source.ast)
93
- .group_by { |node| [node.children.first.method_name, node.first_argument] }
93
+ .group_by { |node| indexed_assignment_key(node) }
94
94
  .values
95
95
  .select { |nodes| nodes.size > 1 }
96
96
  end
97
97
 
98
+ def indexed_assignment_key(node)
99
+ [enclosing_specification(node), node.children.first.method_name, node.first_argument]
100
+ end
101
+
102
+ # Assignments in separate `Gem::Specification.new` blocks are not duplicates of
103
+ # one another, so the enclosing specification is part of the grouping key.
104
+ def enclosing_specification(node)
105
+ node.each_ancestor(:block).find { |block| gem_specification?(block) }
106
+ end
107
+
98
108
  def register_offense(node, assignment, line_of_first_occurrence)
99
109
  line_range = node.loc.column...node.loc.last_column
100
110
  offense_location = source_range(processed_source.buffer, node.first_line, line_range)
@@ -143,7 +143,10 @@ module RuboCop
143
143
  #{block_var}.metadata['rubygems_mfa_required'] = 'true'
144
144
  RUBY
145
145
 
146
- if (last_assignment = metadata_assignment(processed_source.ast).to_a.last)
146
+ # Scope the search to the current spec block. Searching the whole file
147
+ # would, for a second `Gem::Specification.new` block, insert the directive
148
+ # into the first block, leaving this block uncorrected and looping forever.
149
+ if (last_assignment = metadata_assignment(node).to_a.last)
147
150
  corrector.insert_after(last_assignment, "\n#{require_mfa_directive}")
148
151
  else
149
152
  corrector.insert_before(node.loc.end, "#{require_mfa_directive}\n")
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Cops for the `Gemspec` department. The department's cops are registered for lazy loading
6
+ # and their files are loaded on demand.
7
+ module Gemspec
8
+ extend LazyLoader
9
+
10
+ register_cop :AddRuntimeDependency, "#{__dir__}/gemspec/add_runtime_dependency"
11
+ register_cop :AttributeAssignment, "#{__dir__}/gemspec/attribute_assignment"
12
+ register_cop :DependencyVersion, "#{__dir__}/gemspec/dependency_version"
13
+ register_cop :DeprecatedAttributeAssignment, "#{__dir__}/gemspec/deprecated_attribute_assignment"
14
+ register_cop :DevelopmentDependencies, "#{__dir__}/gemspec/development_dependencies"
15
+ register_cop :DuplicatedAssignment, "#{__dir__}/gemspec/duplicated_assignment"
16
+ register_cop :OrderedDependencies, "#{__dir__}/gemspec/ordered_dependencies"
17
+ register_cop :RequireMFA, "#{__dir__}/gemspec/require_mfa"
18
+ register_cop :RequiredRubyVersion, "#{__dir__}/gemspec/required_ruby_version"
19
+ register_cop :RubyVersionGlobalsUsage, "#{__dir__}/gemspec/ruby_version_globals_usage"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ class Generator
6
+ # A class that injects a `register_cop` directive into the cop's department module
7
+ # so that the new cop is registered for lazy loading.
8
+ # The directive is injected in alphabetical order among the existing directives without
9
+ # reordering them, since the registration order defines the cop execution order.
10
+ # When the department module file does not exist yet, it is created, and a require for
11
+ # it is injected into the root RuboCop file.
12
+ #
13
+ # Anything unexpected in the department module, such as a commented-out or otherwise
14
+ # unparsable `register_cop` line, raises an error instead of silently producing
15
+ # a broken registration.
16
+ class RegistrationInjector
17
+ EXTEND_PATTERN = /\A(?<indent>\s*)extend LazyLoader\n\z/.freeze
18
+ ENTRY_PATTERN = %r{
19
+ \A(?<indent>\s*)register_cop\s
20
+ :(?<constant>[A-Za-z0-9]+),\s
21
+ "\#\{__dir__\}/(?<path>[a-z0-9_/]+)"\n\z
22
+ }x.freeze
23
+ DEPARTMENT_REQUIRE_PATTERN = %r{\Arequire_relative 'rubocop/cop/[a-z0-9_/]+'\n\z}.freeze
24
+
25
+ def initialize(source_path:, badge:, root_file_path: 'lib/rubocop.rb', output: $stdout)
26
+ @source_path = Pathname(source_path)
27
+ @badge = badge
28
+ @root_file_path = root_file_path
29
+ @department_file_path = Pathname("#{@source_path.dirname}.rb")
30
+ @output = output
31
+ end
32
+
33
+ def inject
34
+ if File.exist?(department_file_path)
35
+ inject_directive
36
+ else
37
+ create_department_file
38
+ inject_department_require
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :source_path, :badge, :root_file_path, :department_file_path, :output
45
+
46
+ def inject_directive
47
+ lines = File.readlines(department_file_path)
48
+ entries = parse_entries(lines)
49
+ return if entries.any? { |entry| entry[:path] == cop_path }
50
+
51
+ line, indent = insertion_point(lines, entries)
52
+ lines.insert(line, "#{indent}#{directive}\n")
53
+
54
+ write_department_file(lines)
55
+ end
56
+
57
+ def create_department_file
58
+ File.write(department_file_path, department_module_source)
59
+
60
+ output.puts "[create] #{department_file_path}"
61
+ end
62
+
63
+ def inject_department_require
64
+ require_path = department_file_path.sub_ext('').to_s.delete_prefix('lib/')
65
+ require_directive = "require_relative '#{require_path}'\n"
66
+
67
+ lines = File.readlines(root_file_path)
68
+ return if lines.include?(require_directive)
69
+
70
+ lines.insert(department_require_index(lines, require_directive), require_directive)
71
+ File.write(root_file_path, lines.join)
72
+
73
+ output.puts "[modify] #{root_file_path} - `#{require_directive.strip}` was injected."
74
+ end
75
+
76
+ def write_department_file(lines)
77
+ File.write(department_file_path, lines.join)
78
+
79
+ output.puts "[modify] #{department_file_path} - `#{directive}` was injected."
80
+ end
81
+
82
+ # @return [Array<Hash>] the `register_cop` entries with their line numbers
83
+ def parse_entries(lines)
84
+ lines.filter_map.with_index do |line, index|
85
+ match = ENTRY_PATTERN.match(line)
86
+
87
+ if match
88
+ { line: index, indent: match[:indent], constant: match[:constant],
89
+ path: match[:path] }
90
+ elsif line.include?('register_cop')
91
+ raise Error, "unexpected `register_cop` line in #{department_file_path}:" \
92
+ "#{index + 1}; fix it or register the cop manually: #{line}"
93
+ end
94
+ end
95
+ end
96
+
97
+ # The directive path is relative to the department module file's directory,
98
+ # which `__dir__` resolves to at load time.
99
+ def cop_path
100
+ source_path.relative_path_from(department_file_path.dirname).to_s.delete_suffix('.rb')
101
+ end
102
+
103
+ def insertion_point(lines, entries)
104
+ if entries.empty?
105
+ first_directive_position(lines)
106
+ else
107
+ successor = entries.find { |entry| entry[:path] > cop_path }
108
+ entry = successor || entries.last
109
+
110
+ [entry[:line] + (successor ? 0 : 1), entry[:indent]]
111
+ end
112
+ end
113
+
114
+ def directive
115
+ "register_cop :#{badge.cop_name}, \"\#{__dir__}/#{cop_path}\""
116
+ end
117
+
118
+ # The department requires form the contiguous block right before
119
+ # the `rubocop/cop/team` require in the root RuboCop file.
120
+ def department_require_index(lines, require_directive)
121
+ anchor = lines.index("require_relative 'rubocop/cop/team'\n")
122
+ unless anchor
123
+ raise Error, "could not find the department require block in #{root_file_path}"
124
+ end
125
+
126
+ block_end = anchor - 1
127
+ block_end -= 1 while lines[block_end] == "\n"
128
+ block_start = block_end
129
+ block_start -= 1 while lines[block_start - 1]&.match?(DEPARTMENT_REQUIRE_PATTERN)
130
+
131
+ (block_start..block_end).find do |index|
132
+ lines[index] > require_directive
133
+ end || (block_end + 1)
134
+ end
135
+
136
+ def first_directive_position(lines)
137
+ extend_lines = lines.filter_map.with_index do |line, index|
138
+ [index, EXTEND_PATTERN.match(line)[:indent]] if EXTEND_PATTERN.match?(line)
139
+ end
140
+
141
+ unless extend_lines.size == 1
142
+ raise Error, "expected `extend LazyLoader` in #{department_file_path}; " \
143
+ 'add it or register the cop manually'
144
+ end
145
+
146
+ index, indent = extend_lines.first
147
+ blank_after_extend = lines[index + 1] == "\n"
148
+
149
+ [index + (blank_after_extend ? 2 : 1), indent]
150
+ end
151
+
152
+ def department_module_source
153
+ modules = %w[RuboCop Cop] + badge.department.to_s.split('/')
154
+ body_indent = ' ' * modules.size
155
+
156
+ lines = ['# frozen_string_literal: true', '']
157
+ lines.concat(module_opening_lines(modules))
158
+ lines << "#{body_indent}extend LazyLoader"
159
+ lines << ''
160
+ lines << "#{body_indent}#{directive}"
161
+ lines.concat(module_closing_lines(modules))
162
+
163
+ "#{lines.join("\n")}\n"
164
+ end
165
+
166
+ def module_opening_lines(modules)
167
+ modules.flat_map.with_index do |mod, nesting|
168
+ indent = ' ' * nesting
169
+ lines = []
170
+
171
+ if mod == modules.last
172
+ lines << "#{indent}# Cops for the `#{badge.department}` department. " \
173
+ "The department's cops are"
174
+ lines << "#{indent}# registered for lazy loading and their files are " \
175
+ 'loaded on demand.'
176
+ end
177
+
178
+ lines << "#{indent}module #{mod}"
179
+ end
180
+ end
181
+
182
+ def module_closing_lines(modules)
183
+ (modules.size - 1).downto(0).map { |nesting| "#{' ' * nesting}end" }
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -136,6 +136,10 @@ module RuboCop
136
136
  RequireFileInjector.new(source_path: source_path, root_file_path: root_file_path).inject
137
137
  end
138
138
 
139
+ def inject_registration
140
+ RegistrationInjector.new(source_path: source_path, badge: badge).inject
141
+ end
142
+
139
143
  def inject_config(config_file_path: 'config/default.yml',
140
144
  version_added: '<<next>>')
141
145
  injector =
@@ -281,15 +281,32 @@ module RuboCop
281
281
  end
282
282
  end
283
283
 
284
+ # rubocop:disable Metrics/AbcSize
284
285
  def do_line_begins_inside_argument?(node, do_loc)
285
286
  line_begin_pos = do_loc.begin_pos - do_loc.column
286
287
  first_char_pos = line_begin_pos + (do_loc.source_line =~ /\S/)
288
+ return false unless inside_parentheses?(node, first_char_pos)
287
289
 
288
290
  (node.send_node.arguments + node.arguments).any? do |argument|
289
291
  argument.source_range.begin_pos <= first_char_pos &&
290
292
  first_char_pos < argument.source_range.end_pos
291
293
  end
292
294
  end
295
+ # rubocop:enable Metrics/AbcSize
296
+
297
+ # The continuation line indentation is only an unmeaningful alignment target when
298
+ # it is dictated by an opening delimiter, i.e. the line begins inside `(` or `[`.
299
+ # For a bare argument list without parentheses the indentation is the author's
300
+ # deliberate alignment, so the anchor must not move to the method dispatch line.
301
+ def inside_parentheses?(node, pos)
302
+ preceding_tokens = processed_source.tokens.select do |token|
303
+ token.begin_pos >= node.source_range.begin_pos && token.begin_pos < pos
304
+ end
305
+ opens = preceding_tokens.count { |token| token.left_parens? || token.left_bracket? }
306
+ closes = preceding_tokens.count { |token| token.right_parens? || token.right_bracket? }
307
+
308
+ opens > closes
309
+ end
293
310
  end
294
311
  end
295
312
  end
@@ -251,7 +251,7 @@ module RuboCop
251
251
  key = category || name
252
252
  visibility_key =
253
253
  if node.def_modifier?
254
- "#{name}_methods"
254
+ name.end_with?('_class_method') ? "#{name}s" : "#{name}_methods"
255
255
  else
256
256
  "#{node_visibility(node)}_#{key}"
257
257
  end
@@ -278,10 +278,14 @@ module RuboCop
278
278
 
279
279
  return [] unless class_def
280
280
 
281
- if class_def.type?(:def, :send)
282
- [class_def]
281
+ # Only a multi-statement body (`begin`/`kwbegin`) wraps several elements; any
282
+ # single statement (`def`, `send`, `csend`, `if`, ...) is itself the sole element.
283
+ # Exploding such a node into its children would yield non-node values (e.g. a
284
+ # method-name `Symbol` from a `csend`) and crash later checks.
285
+ if class_def.type?(:begin, :kwbegin)
286
+ flatten_class_elements(class_def)
283
287
  else
284
- class_def.children.compact
288
+ [class_def]
285
289
  end
286
290
  end
287
291
 
@@ -292,6 +296,12 @@ module RuboCop
292
296
  private_constant?(node)
293
297
  end
294
298
 
299
+ def flatten_class_elements(node)
300
+ node.children.compact.flat_map do |child|
301
+ child.kwbegin_type? ? flatten_class_elements(child) : [child]
302
+ end
303
+ end
304
+
295
305
  def ignore_for_autocorrect?(node, sibling)
296
306
  classification = classify(node)
297
307
  sibling_class = classify(sibling)
@@ -155,8 +155,15 @@ module RuboCop
155
155
 
156
156
  def less_indented?(line)
157
157
  rule = config.for_cop('Layout/AccessModifierIndentation')['EnforcedStyle'] == 'outdent'
158
- access_modifier = 'private|protected|public'
159
- /\A\s*(end\b|[)}\]])/.match?(line) || (rule && /\A\s*(#{access_modifier})\b/.match?(line))
158
+ /\A\s*(end\b|[)}\]])/.match?(line) || (rule && bare_access_modifier?(line))
159
+ end
160
+
161
+ # Only a bare access modifier (the keyword alone on its line) is outdented by
162
+ # `Layout/AccessModifierIndentation`. An inline modifier such as `private def foo`
163
+ # keeps the regular method indentation, so a comment above it must not be pushed
164
+ # one level deeper.
165
+ def bare_access_modifier?(line)
166
+ /\A\s*(private|protected|public)\s*(#.*)?\z/.match?(line)
160
167
  end
161
168
 
162
169
  def two_alternatives?(line)
@@ -44,16 +44,26 @@ module RuboCop
44
44
  message = message(condition)
45
45
 
46
46
  add_offense(condition, message: message) do |corrector|
47
- range = range_by_whole_lines(condition.source_range, include_final_newline: true)
48
-
49
47
  corrector.insert_after(condition.parent.loc.keyword, " #{condition.source}")
50
- corrector.remove(range)
48
+ corrector.remove(removal_range(node, condition))
51
49
  end
52
50
  end
53
51
 
54
52
  def message(condition)
55
53
  format(MSG, keyword: condition.parent.keyword)
56
54
  end
55
+
56
+ # When a body statement shares the condition's line (e.g. `while\n cond; body\nend`),
57
+ # removing the whole line would delete the body too. In that case only remove the
58
+ # condition and its trailing separator, preserving the body statement.
59
+ def removal_range(node, condition)
60
+ body = node.body
61
+ if body && body.source_range.line == condition.source_range.last_line
62
+ range_between(condition.source_range.begin_pos, body.source_range.begin_pos)
63
+ else
64
+ range_by_whole_lines(condition.source_range, include_final_newline: true)
65
+ end
66
+ end
57
67
  end
58
68
  end
59
69
  end
@@ -92,13 +92,7 @@ module RuboCop
92
92
  case parent.type
93
93
  when :def, :defs then base_for_method_definition(parent)
94
94
  when :kwbegin then parent.loc.begin
95
- when :block, :numblock, :itblock
96
- assignment_node = assignment_node(parent)
97
- if same_line?(parent, assignment_node)
98
- assignment_node.source_range
99
- else
100
- parent.send_node.source_range
101
- end
95
+ when :block, :numblock, :itblock then start_line_range(parent)
102
96
  else node.loc.keyword
103
97
  end
104
98
  end
@@ -143,13 +137,6 @@ module RuboCop
143
137
  autocorrect(corrector, else_range)
144
138
  end
145
139
  end
146
-
147
- def assignment_node(node)
148
- assignment_node = node.ancestors.first
149
- return unless assignment_node&.assignment?
150
-
151
- assignment_node
152
- end
153
140
  end
154
141
  end
155
142
  end
@@ -95,8 +95,7 @@ module RuboCop
95
95
  end
96
96
 
97
97
  def autocorrect(corrector, node)
98
- previous_token = previous_token(node)
99
- range = if previous_token && same_line?(node, previous_token)
98
+ range = if inline_comment?(node)
100
99
  range_with_surrounding_space(node.source_range, newlines: false)
101
100
  else
102
101
  range_by_whole_lines(node.source_range, include_final_newline: true)
@@ -138,14 +137,13 @@ module RuboCop
138
137
  cop_config['AllowMarginComment']
139
138
  end
140
139
 
141
- def current_token(comment)
142
- processed_source.tokens.find { |token| token.pos == comment.source_range }
143
- end
144
-
145
- def previous_token(node)
146
- current_token = current_token(node)
147
- index = processed_source.tokens.index(current_token)
148
- index.zero? ? nil : processed_source.tokens[index - 1]
140
+ # A comment is inline when code precedes it on the same line. Detecting this
141
+ # from the source (rather than the token stream) is required because, for a
142
+ # comment trailing a heredoc opener, the preceding token is the heredoc end on
143
+ # a later line, which would wrongly trigger whole-line removal of the opener.
144
+ def inline_comment?(comment)
145
+ preceding_source = processed_source.lines[comment.loc.line - 1][0...comment.loc.column]
146
+ !preceding_source.strip.empty?
149
147
  end
150
148
  end
151
149
  end
@@ -274,7 +274,20 @@ module RuboCop
274
274
  end
275
275
 
276
276
  def end_loc(node)
277
- node.source_range.end
277
+ end_location = node.source_range.end
278
+ trailing_heredoc_end(node, end_location) || end_location
279
+ end
280
+
281
+ # For an endless method whose body is a heredoc (e.g. `def a = <<~TEXT`), the
282
+ # node's source range ends at the heredoc opening line, before the heredoc body.
283
+ # Use the heredoc's closing delimiter so the def's real end is located after the
284
+ # heredoc and blank-line insertion does not land inside the heredoc body.
285
+ def trailing_heredoc_end(node, end_location)
286
+ heredocs = node.each_descendant(:any_str).select(&:heredoc?)
287
+ return if heredocs.empty?
288
+
289
+ heredoc_end = heredocs.map { |heredoc| heredoc.loc.heredoc_end }.max_by(&:end_pos)
290
+ heredoc_end if heredoc_end.end_pos > end_location.end_pos
278
291
  end
279
292
 
280
293
  def autocorrect_remove_lines(corrector, newline_pos, count)
@@ -172,7 +172,7 @@ module RuboCop
172
172
  'Indent the right bracket the same as the first position ' \
173
173
  'after the preceding left parenthesis.'
174
174
  when :parent_hash_key
175
- 'Indent the right bracket the same as the parent hash key.' \
175
+ 'Indent the right bracket the same as the parent hash key.'
176
176
  else
177
177
  'Indent the right bracket the same as the start of the line ' \
178
178
  'where the left bracket is.'