rubocop 1.88.1 → 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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +69 -11
  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.rb +19 -0
  9. data/lib/rubocop/cop/commissioner.rb +13 -11
  10. data/lib/rubocop/cop/corrector.rb +1 -1
  11. data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
  12. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +12 -2
  13. data/lib/rubocop/cop/gemspec.rb +22 -0
  14. data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
  15. data/lib/rubocop/cop/generator.rb +4 -0
  16. data/lib/rubocop/cop/layout/class_structure.rb +8 -2
  17. data/lib/rubocop/cop/layout/comment_indentation.rb +9 -2
  18. data/lib/rubocop/cop/layout/else_alignment.rb +1 -14
  19. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
  20. data/lib/rubocop/cop/layout/indentation_width.rb +3 -1
  21. data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
  22. data/lib/rubocop/cop/layout/line_length.rb +95 -17
  23. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +18 -3
  24. data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
  25. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +33 -34
  26. data/lib/rubocop/cop/layout.rb +114 -0
  27. data/lib/rubocop/cop/lazy_loader.rb +41 -0
  28. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
  29. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
  30. data/lib/rubocop/cop/lint/constant_reassignment.rb +1 -10
  31. data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
  32. data/lib/rubocop/cop/lint/debugger.rb +13 -1
  33. data/lib/rubocop/cop/lint/deprecated_reference.rb +185 -0
  34. data/lib/rubocop/cop/lint/duplicate_methods.rb +202 -22
  35. data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
  36. data/lib/rubocop/cop/lint/literal_as_condition.rb +5 -0
  37. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +1 -1
  38. data/lib/rubocop/cop/lint/missing_super.rb +31 -1
  39. data/lib/rubocop/cop/lint/name_typo.rb +223 -0
  40. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +7 -0
  41. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +0 -4
  42. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +7 -0
  43. data/lib/rubocop/cop/lint/to_enum_arguments.rb +37 -1
  44. data/lib/rubocop/cop/lint/to_json.rb +8 -1
  45. data/lib/rubocop/cop/lint/unreachable_code.rb +9 -4
  46. data/lib/rubocop/cop/lint/unused_private_method.rb +157 -0
  47. data/lib/rubocop/cop/lint/useless_numeric_operation.rb +10 -10
  48. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
  49. data/lib/rubocop/cop/lint/void.rb +19 -8
  50. data/lib/rubocop/cop/lint.rb +176 -0
  51. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  52. data/lib/rubocop/cop/metrics.rb +31 -0
  53. data/lib/rubocop/cop/migration/department_name.rb +0 -4
  54. data/lib/rubocop/cop/migration.rb +13 -0
  55. data/lib/rubocop/cop/mixin/allowed_methods.rb +5 -5
  56. data/lib/rubocop/cop/mixin/allowed_pattern.rb +5 -1
  57. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
  58. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
  59. data/lib/rubocop/cop/mixin/forbidden_pattern.rb +5 -1
  60. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
  61. data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
  62. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
  63. data/lib/rubocop/cop/mixin/statement_modifier.rb +43 -7
  64. data/lib/rubocop/cop/mixin.rb +78 -77
  65. data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
  66. data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
  67. data/lib/rubocop/cop/naming/variable_number.rb +3 -1
  68. data/lib/rubocop/cop/naming.rb +31 -0
  69. data/lib/rubocop/cop/offense.rb +10 -6
  70. data/lib/rubocop/cop/registry.rb +153 -43
  71. data/lib/rubocop/cop/security.rb +19 -0
  72. data/lib/rubocop/cop/style/arguments_forwarding.rb +1 -1
  73. data/lib/rubocop/cop/style/array_intersect.rb +24 -5
  74. data/lib/rubocop/cop/style/class_and_module_children.rb +78 -18
  75. data/lib/rubocop/cop/style/collection_compact.rb +1 -1
  76. data/lib/rubocop/cop/style/conditional_assignment.rb +22 -0
  77. data/lib/rubocop/cop/style/copyright.rb +6 -1
  78. data/lib/rubocop/cop/style/def_with_parentheses.rb +6 -2
  79. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +41 -3
  80. data/lib/rubocop/cop/style/documentation.rb +47 -0
  81. data/lib/rubocop/cop/style/documentation_method.rb +5 -1
  82. data/lib/rubocop/cop/style/endless_method.rb +0 -10
  83. data/lib/rubocop/cop/style/hash_conversion.rb +14 -6
  84. data/lib/rubocop/cop/style/hash_lookup_method.rb +2 -6
  85. data/lib/rubocop/cop/style/identical_conditional_branches.rb +15 -4
  86. data/lib/rubocop/cop/style/if_unless_modifier.rb +11 -52
  87. data/lib/rubocop/cop/style/invertible_unless_condition.rb +25 -5
  88. data/lib/rubocop/cop/style/lambda.rb +4 -0
  89. data/lib/rubocop/cop/style/lambda_call.rb +11 -0
  90. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +20 -3
  91. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +5 -3
  92. data/lib/rubocop/cop/style/method_def_parentheses.rb +4 -0
  93. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +40 -7
  94. data/lib/rubocop/cop/style/mixin_usage.rb +1 -1
  95. data/lib/rubocop/cop/style/module_function.rb +4 -2
  96. data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
  97. data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
  98. data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
  99. data/lib/rubocop/cop/style/negative_array_index.rb +3 -1
  100. data/lib/rubocop/cop/style/redundant_assignment.rb +11 -0
  101. data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
  102. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +13 -1
  103. data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
  104. data/lib/rubocop/cop/style/redundant_format.rb +12 -3
  105. data/lib/rubocop/cop/style/redundant_heredoc_delimiter_quotes.rb +6 -1
  106. data/lib/rubocop/cop/style/redundant_line_continuation.rb +78 -135
  107. data/lib/rubocop/cop/style/redundant_parentheses.rb +63 -63
  108. data/lib/rubocop/cop/style/safe_navigation.rb +10 -0
  109. data/lib/rubocop/cop/style/sole_nested_conditional.rb +20 -3
  110. data/lib/rubocop/cop/style/static_class.rb +21 -0
  111. data/lib/rubocop/cop/style/struct_inheritance.rb +3 -1
  112. data/lib/rubocop/cop/style/trivial_accessors.rb +1 -1
  113. data/lib/rubocop/cop/style.rb +310 -0
  114. data/lib/rubocop/cop/team.rb +66 -7
  115. data/lib/rubocop/cop/variable_force/variable.rb +18 -6
  116. data/lib/rubocop/cops_documentation_generator.rb +1 -1
  117. data/lib/rubocop/directive_comment.rb +2 -0
  118. data/lib/rubocop/feature_loader.rb +0 -6
  119. data/lib/rubocop/lsp/routes.rb +41 -3
  120. data/lib/rubocop/lsp/runtime.rb +4 -0
  121. data/lib/rubocop/lsp/server.rb +4 -0
  122. data/lib/rubocop/lsp/stdin_runner.rb +20 -0
  123. data/lib/rubocop/pending_cops_reporter.rb +1 -6
  124. data/lib/rubocop/project_index_loader.rb +16 -0
  125. data/lib/rubocop/result_cache.rb +6 -4
  126. data/lib/rubocop/rspec/shared_contexts.rb +14 -2
  127. data/lib/rubocop/runner.rb +95 -30
  128. data/lib/rubocop/server/cache.rb +10 -5
  129. data/lib/rubocop/target_ruby.rb +1 -1
  130. data/lib/rubocop/version.rb +1 -1
  131. data/lib/rubocop.rb +17 -626
  132. 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
 
@@ -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)
@@ -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 =
@@ -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
@@ -283,7 +283,7 @@ module RuboCop
283
283
  # Exploding such a node into its children would yield non-node values (e.g. a
284
284
  # method-name `Symbol` from a `csend`) and crash later checks.
285
285
  if class_def.type?(:begin, :kwbegin)
286
- class_def.children.compact
286
+ flatten_class_elements(class_def)
287
287
  else
288
288
  [class_def]
289
289
  end
@@ -296,6 +296,12 @@ module RuboCop
296
296
  private_constant?(node)
297
297
  end
298
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
+
299
305
  def ignore_for_autocorrect?(node, sibling)
300
306
  classification = classify(node)
301
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)
@@ -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
@@ -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.'
@@ -206,7 +206,9 @@ module RuboCop
206
206
  def autocorrect(corrector, node)
207
207
  return unless node
208
208
 
209
- AlignmentCorrector.correct(corrector, processed_source, node, @column_delta)
209
+ AlignmentCorrector.correct(
210
+ corrector, processed_source, node, @column_delta, tab_indentation: true
211
+ )
210
212
  end
211
213
 
212
214
  # Returns a range at the first non-space column of the line the opening parenthesis is on,
@@ -98,6 +98,24 @@ module RuboCop
98
98
  #
99
99
  # name = 'John' #: String
100
100
  #
101
+ # @example AllowYARDCommentBlockSeparator: false (default)
102
+ #
103
+ # # bad
104
+ #
105
+ # # Copyright (c) Example Corp
106
+ # #-
107
+ # class Client
108
+ # end
109
+ #
110
+ # @example AllowYARDCommentBlockSeparator: true
111
+ #
112
+ # # good
113
+ #
114
+ # # Copyright (c) Example Corp
115
+ # #-
116
+ # class Client
117
+ # end
118
+ #
101
119
  class LeadingCommentSpace < Base
102
120
  include RangeHelp
103
121
  extend AutoCorrector
@@ -113,6 +131,7 @@ module RuboCop
113
131
  next if gemfile_ruby_comment?(comment)
114
132
  next if rbs_inline_annotation?(comment)
115
133
  next if steep_annotation?(comment)
134
+ next if yard_comment_block_separator?(comment)
116
135
 
117
136
  add_offense(comment) do |corrector|
118
137
  expr = comment.source_range
@@ -197,6 +216,14 @@ module RuboCop
197
216
  def steep_annotation?(comment)
198
217
  allow_steep_annotation? && comment.text.start_with?(/#[$:]/)
199
218
  end
219
+
220
+ def allow_yard_comment_block_separator?
221
+ cop_config['AllowYARDCommentBlockSeparator']
222
+ end
223
+
224
+ def yard_comment_block_separator?(comment)
225
+ allow_yard_comment_block_separator? && comment.text.match?(/\A#-\s*\z/)
226
+ end
200
227
  end
201
228
  end
202
229
  end
@@ -65,6 +65,7 @@ module RuboCop
65
65
  include AllowedPattern
66
66
  include RangeHelp
67
67
  include LineLengthHelp
68
+ include EndlessMethodRewriter
68
69
  extend AutoCorrector
69
70
 
70
71
  exclude_limit 'Max'
@@ -92,8 +93,15 @@ module RuboCop
92
93
  alias on_hash on_potential_breakable_node
93
94
  alias on_send on_potential_breakable_node
94
95
  alias on_csend on_potential_breakable_node
95
- alias on_def on_potential_breakable_node
96
- alias on_defs on_potential_breakable_node
96
+
97
+ def on_def(node)
98
+ if node.endless?
99
+ track_endless_method(node)
100
+ else
101
+ check_for_breakable_node(node)
102
+ end
103
+ end
104
+ alias on_defs on_def
97
105
 
98
106
  def on_new_investigation
99
107
  return unless processed_source.raw_source.include?(';')
@@ -111,6 +119,70 @@ module RuboCop
111
119
 
112
120
  attr_accessor :breakable_range
113
121
 
122
+ def track_endless_method(node)
123
+ line_index = node.first_line - processed_source.buffer.first_line
124
+ endless_methods_by_line[line_index] = node
125
+ end
126
+
127
+ def handle_endless_method_line(line, line_index)
128
+ if require_endless_methods?
129
+ return register_required_endless_method_offense(line, line_index)
130
+ end
131
+
132
+ register_endless_method_offense(line, line_index)
133
+ end
134
+
135
+ def require_endless_methods?
136
+ config.cop_enabled?('Style/EndlessMethod') &&
137
+ config.for_cop('Style/EndlessMethod')['EnforcedStyle'] == 'require_always'
138
+ end
139
+
140
+ def register_endless_method_offense(line, line_index)
141
+ message = format(MSG, length: line_length(line), max: max)
142
+ loc = excess_range(nil, line, line_index)
143
+
144
+ add_offense(loc, message: message) do |corrector|
145
+ self.max = line_length(line)
146
+
147
+ correct_to_multiline(corrector, endless_methods_by_line[line_index])
148
+ end
149
+ end
150
+
151
+ def register_required_endless_method_offense(line, line_index)
152
+ node = endless_methods_by_line[line_index]
153
+ loc = excess_range(nil, line, line_index)
154
+
155
+ add_offense(loc, message: format(MSG, length: line_length(line), max: max)) do |corrector|
156
+ self.max = line_length(line)
157
+
158
+ if correctable_endless_method_block?(node)
159
+ correct_endless_method_block_to_multiline(corrector, node)
160
+ end
161
+ end
162
+ end
163
+
164
+ def correctable_endless_method_block?(node)
165
+ block_node = node.body
166
+
167
+ block_node&.type?(:any_block) &&
168
+ block_node.braces? &&
169
+ block_node.single_line? &&
170
+ block_node.body &&
171
+ !receiver_contains_heredoc?(block_node)
172
+ end
173
+
174
+ def correct_endless_method_block_to_multiline(corrector, node)
175
+ block_node = node.body
176
+ block_arguments = block_node.arguments? ? " #{block_node.arguments.source}" : ''
177
+ replacement = [
178
+ "#{block_node.send_node.source} do#{block_arguments}",
179
+ "#{indent(node, offset: 2)}#{block_node.body.source}",
180
+ "#{indent(node)}end"
181
+ ].join("\n")
182
+
183
+ corrector.replace(block_node, replacement)
184
+ end
185
+
114
186
  def check_for_breakable_node(node)
115
187
  breakable_node = extract_breakable_node(node, max)
116
188
  return if breakable_node.nil?
@@ -237,6 +309,10 @@ module RuboCop
237
309
  @breakable_range_by_line_index ||= {}
238
310
  end
239
311
 
312
+ def endless_methods_by_line
313
+ @endless_methods_by_line ||= {}
314
+ end
315
+
240
316
  def breakable_string_delimiters
241
317
  @breakable_string_delimiters ||= {}
242
318
  end
@@ -252,10 +328,15 @@ module RuboCop
252
328
  [max - indentation_difference(line), 0].max
253
329
  end
254
330
 
255
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
331
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
256
332
  def check_line(line, line_index)
257
333
  return if line_length(line) <= max
258
334
  return if allowed_line?(line, line_index)
335
+
336
+ if endless_methods_by_line.key?(line_index)
337
+ return handle_endless_method_line(line, line_index)
338
+ end
339
+
259
340
  if allow_rbs_inline_annotation? && rbs_inline_annotation_on_source_line?(line_index)
260
341
  return
261
342
  end
@@ -267,7 +348,7 @@ module RuboCop
267
348
 
268
349
  register_offense(excess_range(nil, line, line_index), line, line_index)
269
350
  end
270
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
351
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
271
352
 
272
353
  def allowed_line?(line, line_index)
273
354
  matches_allowed_pattern?(line) ||
@@ -313,10 +394,6 @@ module RuboCop
313
394
  end
314
395
  alias max_line_length max
315
396
 
316
- def allow_heredoc?
317
- allowed_heredoc
318
- end
319
-
320
397
  def allowed_heredoc
321
398
  cop_config['AllowHeredoc']
322
399
  end
@@ -344,10 +421,6 @@ module RuboCop
344
421
  end
345
422
  end
346
423
 
347
- def line_in_heredoc?(line_number)
348
- heredocs.any? { |range, _delimiter| range.cover?(line_number) }
349
- end
350
-
351
424
  def receiver_contains_heredoc?(node)
352
425
  return false unless (receiver = node.receiver)
353
426
  return true if receiver.any_str_type? && receiver.heredoc?
@@ -420,11 +493,16 @@ module RuboCop
420
493
  # The maximum allowed length of a string value is:
421
494
  # `Max` - end delimiter (quote) - continuation characters (space and slash)
422
495
  max_length = max - 3
423
- # If the string is on the same line as its parent, offset by the column difference
424
- # (Only apply when on same line to avoid negative offsets for multi-line dstr)
425
- if same_line?(node, node.parent)
426
- max_length -= column_offset_between(node.loc, node.parent.loc)
427
- end
496
+ # Offset by the string's starting column so the broken line actually fits
497
+ # within `Max`. When on the same line as its parent, use the column difference;
498
+ # otherwise the string is indented on its own line, so subtract that indentation.
499
+ # (Without this, an indented string under a multi-line parent never shortens
500
+ # below `Max` and the autocorrect loops, inserting empty `"" \` fragments.)
501
+ max_length -= if same_line?(node, node.parent)
502
+ column_offset_between(node.loc, node.parent.loc)
503
+ else
504
+ node.loc.column
505
+ end
428
506
  node.source[0...(max_length)]
429
507
  end
430
508
  end