rubocop 0.67.2 → 0.68.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +86 -233
  4. data/exe/rubocop +0 -12
  5. data/lib/rubocop.rb +13 -30
  6. data/lib/rubocop/ast/builder.rb +4 -0
  7. data/lib/rubocop/ast/node/alias_node.rb +24 -0
  8. data/lib/rubocop/ast/node/class_node.rb +31 -0
  9. data/lib/rubocop/ast/node/module_node.rb +24 -0
  10. data/lib/rubocop/ast/node/range_node.rb +7 -0
  11. data/lib/rubocop/ast/node/resbody_node.rb +12 -0
  12. data/lib/rubocop/ast/node/self_class_node.rb +24 -0
  13. data/lib/rubocop/cli.rb +40 -4
  14. data/lib/rubocop/config.rb +9 -7
  15. data/lib/rubocop/config_loader.rb +48 -7
  16. data/lib/rubocop/config_loader_resolver.rb +5 -4
  17. data/lib/rubocop/cop/commissioner.rb +24 -0
  18. data/lib/rubocop/cop/correctors/unused_arg_corrector.rb +18 -6
  19. data/lib/rubocop/cop/internal_affairs/node_destructuring.rb +12 -14
  20. data/lib/rubocop/cop/layout/access_modifier_indentation.rb +9 -20
  21. data/lib/rubocop/cop/layout/align_arguments.rb +93 -0
  22. data/lib/rubocop/cop/layout/align_parameters.rb +57 -33
  23. data/lib/rubocop/cop/layout/class_structure.rb +5 -5
  24. data/lib/rubocop/cop/layout/empty_lines_around_access_modifier.rb +6 -8
  25. data/lib/rubocop/cop/layout/empty_lines_around_class_body.rb +3 -6
  26. data/lib/rubocop/cop/layout/empty_lines_around_module_body.rb +1 -2
  27. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +1 -0
  28. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +292 -0
  29. data/lib/rubocop/cop/layout/{first_parameter_indentation.rb → indent_first_argument.rb} +11 -12
  30. data/lib/rubocop/cop/layout/{indent_array.rb → indent_first_array_element.rb} +2 -2
  31. data/lib/rubocop/cop/layout/{indent_hash.rb → indent_first_hash_element.rb} +2 -2
  32. data/lib/rubocop/cop/layout/indent_first_parameter.rb +96 -0
  33. data/lib/rubocop/cop/layout/indentation_width.rb +4 -16
  34. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +2 -4
  35. data/lib/rubocop/cop/layout/space_in_lambda_literal.rb +1 -16
  36. data/lib/rubocop/cop/layout/space_inside_reference_brackets.rb +1 -2
  37. data/lib/rubocop/cop/lint/duplicate_methods.rb +6 -8
  38. data/lib/rubocop/cop/lint/format_parameter_mismatch.rb +4 -8
  39. data/lib/rubocop/cop/lint/heredoc_method_call_position.rb +157 -0
  40. data/lib/rubocop/cop/lint/inherit_exception.rb +3 -4
  41. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +18 -1
  42. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +3 -5
  43. data/lib/rubocop/cop/lint/underscore_prefixed_variable_name.rb +25 -5
  44. data/lib/rubocop/cop/lint/useless_assignment.rb +2 -6
  45. data/lib/rubocop/cop/lint/useless_setter_call.rb +1 -2
  46. data/lib/rubocop/cop/message_annotator.rb +1 -0
  47. data/lib/rubocop/cop/metrics/line_length.rb +139 -28
  48. data/lib/rubocop/cop/metrics/perceived_complexity.rb +3 -4
  49. data/lib/rubocop/cop/mixin/check_line_breakable.rb +190 -0
  50. data/lib/rubocop/cop/mixin/{array_hash_indentation.rb → multiline_element_indentation.rb} +3 -2
  51. data/lib/rubocop/cop/mixin/too_many_lines.rb +3 -7
  52. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +33 -4
  53. data/lib/rubocop/cop/rails/active_record_override.rb +23 -8
  54. data/lib/rubocop/cop/rails/delegate.rb +5 -8
  55. data/lib/rubocop/cop/rails/environment_comparison.rb +5 -3
  56. data/lib/rubocop/cop/rails/find_each.rb +1 -1
  57. data/lib/rubocop/cop/rails/redundant_allow_nil.rb +3 -3
  58. data/lib/rubocop/cop/rails/reflection_class_name.rb +1 -1
  59. data/lib/rubocop/cop/rails/skips_model_validations.rb +6 -7
  60. data/lib/rubocop/cop/rails/time_zone.rb +3 -10
  61. data/lib/rubocop/cop/rails/validation.rb +3 -0
  62. data/lib/rubocop/cop/registry.rb +3 -3
  63. data/lib/rubocop/cop/style/alias.rb +13 -7
  64. data/lib/rubocop/cop/style/block_delimiters.rb +20 -0
  65. data/lib/rubocop/cop/style/class_and_module_children.rb +19 -21
  66. data/lib/rubocop/cop/style/class_methods.rb +16 -24
  67. data/lib/rubocop/cop/style/conditional_assignment.rb +20 -49
  68. data/lib/rubocop/cop/style/documentation.rb +3 -7
  69. data/lib/rubocop/cop/style/format_string.rb +18 -21
  70. data/lib/rubocop/cop/style/hash_syntax.rb +1 -1
  71. data/lib/rubocop/cop/style/inverse_methods.rb +4 -0
  72. data/lib/rubocop/cop/style/lambda.rb +12 -8
  73. data/lib/rubocop/cop/style/mixin_grouping.rb +8 -10
  74. data/lib/rubocop/cop/style/module_function.rb +2 -3
  75. data/lib/rubocop/cop/style/next.rb +10 -14
  76. data/lib/rubocop/cop/style/one_line_conditional.rb +5 -3
  77. data/lib/rubocop/cop/style/optional_arguments.rb +1 -4
  78. data/lib/rubocop/cop/style/random_with_offset.rb +44 -47
  79. data/lib/rubocop/cop/style/redundant_return.rb +6 -14
  80. data/lib/rubocop/cop/style/redundant_sort_by.rb +1 -1
  81. data/lib/rubocop/cop/style/safe_navigation.rb +3 -0
  82. data/lib/rubocop/cop/style/struct_inheritance.rb +2 -3
  83. data/lib/rubocop/cop/style/symbol_proc.rb +20 -40
  84. data/lib/rubocop/cop/style/unless_else.rb +1 -2
  85. data/lib/rubocop/cop/style/yoda_condition.rb +8 -7
  86. data/lib/rubocop/cop/util.rb +2 -4
  87. data/lib/rubocop/file_finder.rb +5 -10
  88. data/lib/rubocop/formatter/disabled_config_formatter.rb +5 -0
  89. data/lib/rubocop/node_pattern.rb +304 -170
  90. data/lib/rubocop/options.rb +4 -1
  91. data/lib/rubocop/rspec/shared_contexts.rb +3 -0
  92. data/lib/rubocop/version.rb +1 -1
  93. data/lib/rubocop/yaml_duplication_checker.rb +1 -1
  94. metadata +26 -50
  95. data/lib/rubocop/cop/performance/caller.rb +0 -69
  96. data/lib/rubocop/cop/performance/case_when_splat.rb +0 -177
  97. data/lib/rubocop/cop/performance/casecmp.rb +0 -108
  98. data/lib/rubocop/cop/performance/chain_array_allocation.rb +0 -78
  99. data/lib/rubocop/cop/performance/compare_with_block.rb +0 -122
  100. data/lib/rubocop/cop/performance/count.rb +0 -102
  101. data/lib/rubocop/cop/performance/detect.rb +0 -110
  102. data/lib/rubocop/cop/performance/double_start_end_with.rb +0 -94
  103. data/lib/rubocop/cop/performance/end_with.rb +0 -56
  104. data/lib/rubocop/cop/performance/fixed_size.rb +0 -97
  105. data/lib/rubocop/cop/performance/flat_map.rb +0 -78
  106. data/lib/rubocop/cop/performance/inefficient_hash_search.rb +0 -99
  107. data/lib/rubocop/cop/performance/open_struct.rb +0 -46
  108. data/lib/rubocop/cop/performance/range_include.rb +0 -50
  109. data/lib/rubocop/cop/performance/redundant_block_call.rb +0 -93
  110. data/lib/rubocop/cop/performance/redundant_match.rb +0 -56
  111. data/lib/rubocop/cop/performance/redundant_merge.rb +0 -183
  112. data/lib/rubocop/cop/performance/regexp_match.rb +0 -265
  113. data/lib/rubocop/cop/performance/reverse_each.rb +0 -42
  114. data/lib/rubocop/cop/performance/size.rb +0 -77
  115. data/lib/rubocop/cop/performance/start_with.rb +0 -59
  116. data/lib/rubocop/cop/performance/string_replacement.rb +0 -173
  117. data/lib/rubocop/cop/performance/times_map.rb +0 -71
  118. data/lib/rubocop/cop/performance/unfreeze_string.rb +0 -50
  119. data/lib/rubocop/cop/performance/uri_default_parser.rb +0 -47
@@ -49,16 +49,14 @@ module RuboCop
49
49
  return_node.each_ancestor(:block, :def, :defs) do |node|
50
50
  break if scoped_node?(node)
51
51
 
52
- send_node, args_node, _body_node = *node
53
-
54
52
  # if a proc is passed to `Module#define_method` or
55
53
  # `Object#define_singleton_method`, `return` will not cause a
56
54
  # non-local exit error
57
- break if define_method?(send_node)
55
+ break if define_method?(node.send_node)
58
56
 
59
- next if args_node.children.empty?
57
+ next unless node.arguments?
60
58
 
61
- if chained_send?(send_node)
59
+ if chained_send?(node.send_node)
62
60
  add_offense(return_node, location: :keyword)
63
61
  break
64
62
  end
@@ -6,7 +6,11 @@ module RuboCop
6
6
  # This cop checks for underscore-prefixed variables that are actually
7
7
  # used.
8
8
  #
9
- # @example
9
+ # Since block keyword arguments cannot be arbitrarily named at call
10
+ # sites, the `AllowKeywordBlockArguments` will allow use of underscore-
11
+ # prefixed block keyword arguments.
12
+ #
13
+ # @example AllowKeywordBlockArguments: false (default)
10
14
  #
11
15
  # # bad
12
16
  #
@@ -14,7 +18,9 @@ module RuboCop
14
18
  # do_something(_num)
15
19
  # end
16
20
  #
17
- # @example
21
+ # query(:sales) do |_id:, revenue:, cost:|
22
+ # {_id: _id, profit: revenue - cost}
23
+ # end
18
24
  #
19
25
  # # good
20
26
  #
@@ -22,13 +28,18 @@ module RuboCop
22
28
  # do_something(num)
23
29
  # end
24
30
  #
25
- # @example
31
+ # [1, 2, 3].each do |_num|
32
+ # do_something # not using `_num`
33
+ # end
34
+ #
35
+ # @example AllowKeywordBlockArguments: true
26
36
  #
27
37
  # # good
28
38
  #
29
- # [1, 2, 3].each do |_num|
30
- # do_something # not using `_num`
39
+ # query(:sales) do |_id:, revenue:, cost:|
40
+ # {_id: _id, profit: revenue - cost}
31
41
  # end
42
+ #
32
43
  class UnderscorePrefixedVariableName < Cop
33
44
  MSG = 'Do not use prefix `_` for a variable that is used.'.freeze
34
45
 
@@ -45,6 +56,7 @@ module RuboCop
45
56
  def check_variable(variable)
46
57
  return unless variable.should_be_unused?
47
58
  return if variable.references.none?(&:explicit?)
59
+ return if allowed_keyword_block_argument?(variable)
48
60
 
49
61
  node = variable.declaration_node
50
62
 
@@ -56,6 +68,14 @@ module RuboCop
56
68
 
57
69
  add_offense(nil, location: location)
58
70
  end
71
+
72
+ private
73
+
74
+ def allowed_keyword_block_argument?(variable)
75
+ variable.block_argument? &&
76
+ variable.keyword_argument? &&
77
+ cop_config['AllowKeywordBlockArguments']
78
+ end
59
79
  end
60
80
  end
61
81
  end
@@ -111,10 +111,7 @@ module RuboCop
111
111
 
112
112
  def collect_variable_like_names(scope)
113
113
  names = scope.each_node.with_object(Set.new) do |node, set|
114
- if variable_like_method_invocation?(node)
115
- _receiver, method_name, = *node
116
- set << method_name
117
- end
114
+ set << node.method_name if variable_like_method_invocation?(node)
118
115
  end
119
116
 
120
117
  variable_names = scope.variables.each_value.map(&:name)
@@ -124,8 +121,7 @@ module RuboCop
124
121
  def variable_like_method_invocation?(node)
125
122
  return false unless node.send_type?
126
123
 
127
- receiver, _method_name, *args = *node
128
- receiver.nil? && args.empty?
124
+ node.receiver.nil? && !node.arguments?
129
125
  end
130
126
  end
131
127
  end
@@ -155,8 +155,7 @@ module RuboCop
155
155
  return true if node.literal?
156
156
  return false unless node.send_type?
157
157
 
158
- _receiver, method = *node
159
- method == :new
158
+ node.method_name == :new
160
159
  end
161
160
  end
162
161
  end
@@ -106,6 +106,7 @@ module RuboCop
106
106
  return true if debug?
107
107
  return false if options[:display_cop_names] == false
108
108
  return true if options[:display_cop_names]
109
+ return false if options[:format] == 'json'
109
110
 
110
111
  config.for_all_cops['DisplayCopNames']
111
112
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'uri'
4
4
 
5
+ # rubocop:disable Metrics/ClassLength
5
6
  module RuboCop
6
7
  module Cop
7
8
  module Metrics
@@ -9,22 +10,95 @@ module RuboCop
9
10
  # The maximum length is configurable.
10
11
  # The tab size is configured in the `IndentationWidth`
11
12
  # of the `Layout/Tab` cop.
13
+ #
14
+ # This cop has some autocorrection capabilities.
15
+ # It can programmatically shorten certain long lines by
16
+ # inserting line breaks into expressions that can be safely
17
+ # split across lines. These include arrays, hashes, and
18
+ # method calls with argument lists.
19
+ #
20
+ # If autocorrection is enabled, the following Layout cops
21
+ # are recommended to further format the broken lines.
22
+ #
23
+ # - AlignArray
24
+ # - AlignHash
25
+ # - AlignParameters
26
+ # - ClosingParenthesisIndentation
27
+ # - IndentFirstArgument
28
+ # - IndentFirstArrayElement
29
+ # - IndentFirstHashElement
30
+ # - IndentFirstParameter
31
+ # - MultilineArrayLineBreaks
32
+ # - MultilineHashBraceLayout
33
+ # - MultilineHashKeyLineBreaks
34
+ # - MultilineMethodArgumentLineBreaks
35
+ #
36
+ # Together, these cops will pretty print hashes, arrays,
37
+ # method calls, etc. For example, let's say the max columns
38
+ # is 25:
39
+ #
40
+ # @example
41
+ #
42
+ # # bad
43
+ # {foo: "0000000000", bar: "0000000000", baz: "0000000000"}
44
+ #
45
+ # # good
46
+ # {foo: "0000000000",
47
+ # bar: "0000000000", baz: "0000000000"}
48
+ #
49
+ # # good (with recommended cops enabled)
50
+ # {
51
+ # foo: "0000000000",
52
+ # bar: "0000000000",
53
+ # baz: "0000000000",
54
+ # }
12
55
  class LineLength < Cop
56
+ include CheckLineBreakable
13
57
  include ConfigurableMax
14
58
  include IgnoredPattern
15
59
  include RangeHelp
16
60
 
17
61
  MSG = 'Line is too long. [%<length>d/%<max>d]'.freeze
18
62
 
19
- def investigate(processed_source)
20
- heredocs = extract_heredocs(processed_source.ast) if allow_heredoc?
21
- processed_source.lines.each_with_index do |line, index|
22
- check_line(line, index, heredocs)
63
+ def on_potential_breakable_node(node)
64
+ check_for_breakable_node(node)
65
+ end
66
+ alias on_array on_potential_breakable_node
67
+ alias on_hash on_potential_breakable_node
68
+ alias on_send on_potential_breakable_node
69
+
70
+ def investigate_post_walk(processed_source)
71
+ processed_source.lines.each_with_index do |line, line_index|
72
+ check_line(line, line_index)
73
+ end
74
+ end
75
+
76
+ def autocorrect(range)
77
+ return if range.nil?
78
+
79
+ lambda do |corrector|
80
+ corrector.insert_before(range, "\n")
23
81
  end
24
82
  end
25
83
 
26
84
  private
27
85
 
86
+ def check_for_breakable_node(node)
87
+ breakable_node = extract_breakable_node(node, max)
88
+ return if breakable_node.nil?
89
+
90
+ line_index = breakable_node.first_line - 1
91
+ breakable_nodes_by_line_index[line_index] = breakable_node
92
+ end
93
+
94
+ def breakable_nodes_by_line_index
95
+ @breakable_nodes_by_line_index ||= {}
96
+ end
97
+
98
+ def heredocs
99
+ @heredocs ||= extract_heredocs(processed_source.ast)
100
+ end
101
+
28
102
  def tab_indentation_width
29
103
  config.for_cop('Layout/Tab')['IndentationWidth']
30
104
  end
@@ -39,49 +113,72 @@ module RuboCop
39
113
  line.length + indentation_difference(line)
40
114
  end
41
115
 
42
- def highligh_start(line)
116
+ def highlight_start(line)
43
117
  max - indentation_difference(line)
44
118
  end
45
119
 
46
- def check_line(line, index, heredocs)
120
+ def check_line(line, line_index)
47
121
  return if line_length(line) <= max
48
- return if ignored_line?(line, index, heredocs)
122
+ return if ignored_line?(line, line_index)
49
123
 
50
- if ignore_cop_directives? && directive_on_source_line?(index)
51
- return check_directive_line(line, index)
124
+ if ignore_cop_directives? && directive_on_source_line?(line_index)
125
+ return check_directive_line(line, line_index)
52
126
  end
53
- return check_uri_line(line, index) if allow_uri?
127
+ return check_uri_line(line, line_index) if allow_uri?
54
128
 
55
129
  register_offense(
56
130
  source_range(
57
- processed_source.buffer, index,
58
- highligh_start(line)...line_length(line)
131
+ processed_source.buffer, line_index,
132
+ highlight_start(line)...line_length(line)
59
133
  ),
60
- line
134
+ line,
135
+ line_index
61
136
  )
62
137
  end
63
138
 
64
- def ignored_line?(line, index, heredocs)
139
+ def ignored_line?(line, line_index)
65
140
  matches_ignored_pattern?(line) ||
66
- heredocs && line_in_permitted_heredoc?(heredocs, index.succ)
141
+ heredocs && line_in_permitted_heredoc?(line_index.succ)
67
142
  end
68
143
 
69
- def register_offense(loc, line)
144
+ def register_offense(loc, line, line_index)
70
145
  message = format(MSG, length: line_length(line), max: max)
71
146
 
72
- add_offense(nil, location: loc, message: message) do
147
+ breakable_range = breakable_range(line, line_index)
148
+ add_offense(breakable_range, location: loc, message: message) do
73
149
  self.max = line_length(line)
74
150
  end
75
151
  end
76
152
 
77
- def excess_range(uri_range, line, index)
153
+ def breakable_range(line, line_index)
154
+ return if line_in_heredoc?(line_index + 1)
155
+
156
+ semicolon_range = breakable_semicolon_range(line, line_index)
157
+ return semicolon_range if semicolon_range
158
+
159
+ breakable_node = breakable_nodes_by_line_index[line_index]
160
+ return breakable_node.source_range if breakable_node
161
+ end
162
+
163
+ def breakable_semicolon_range(line, line_index)
164
+ semicolon_separated_parts = line.split(';')
165
+ return if semicolon_separated_parts.length <= 1
166
+
167
+ column = semicolon_separated_parts.first.length + 1
168
+ range = source_range(processed_source.buffer, line_index, column, 1)
169
+ return if processed_source.commented?(range)
170
+
171
+ range
172
+ end
173
+
174
+ def excess_range(uri_range, line, line_index)
78
175
  excessive_position = if uri_range && uri_range.begin < max
79
176
  uri_range.end
80
177
  else
81
- highligh_start(line)
178
+ highlight_start(line)
82
179
  end
83
180
 
84
- source_range(processed_source.buffer, index + 1,
181
+ source_range(processed_source.buffer, line_index + 1,
85
182
  excessive_position...(line_length(line)))
86
183
  end
87
184
 
@@ -107,13 +204,21 @@ module RuboCop
107
204
  end
108
205
  end
109
206
 
110
- def line_in_permitted_heredoc?(heredocs, line_number)
207
+ def line_in_permitted_heredoc?(line_number)
208
+ return false unless allowed_heredoc
209
+
111
210
  heredocs.any? do |range, delimiter|
112
211
  range.cover?(line_number) &&
113
212
  (allowed_heredoc == true || allowed_heredoc.include?(delimiter))
114
213
  end
115
214
  end
116
215
 
216
+ def line_in_heredoc?(line_number)
217
+ heredocs.any? do |range, _delimiter|
218
+ range.cover?(line_number)
219
+ end
220
+ end
221
+
117
222
  def allow_uri?
118
223
  cop_config['AllowURI']
119
224
  end
@@ -161,22 +266,23 @@ module RuboCop
161
266
  URI::DEFAULT_PARSER.make_regexp(cop_config['URISchemes'])
162
267
  end
163
268
 
164
- def check_directive_line(line, index)
269
+ def check_directive_line(line, line_index)
165
270
  return if line_length_without_directive(line) <= max
166
271
 
167
272
  range = max..(line_length_without_directive(line) - 1)
168
273
  register_offense(
169
274
  source_range(
170
275
  processed_source.buffer,
171
- index + 1,
276
+ line_index + 1,
172
277
  range
173
278
  ),
174
- line
279
+ line,
280
+ line_index
175
281
  )
176
282
  end
177
283
 
178
- def directive_on_source_line?(index)
179
- source_line_number = index + processed_source.buffer.first_line
284
+ def directive_on_source_line?(line_index)
285
+ source_line_number = line_index + processed_source.buffer.first_line
180
286
  comment =
181
287
  processed_source
182
288
  .comments
@@ -192,13 +298,18 @@ module RuboCop
192
298
  before_comment.rstrip.length
193
299
  end
194
300
 
195
- def check_uri_line(line, index)
301
+ def check_uri_line(line, line_index)
196
302
  uri_range = find_excessive_uri_range(line)
197
303
  return if uri_range && allowed_uri_position?(line, uri_range)
198
304
 
199
- register_offense(excess_range(uri_range, line, index), line)
305
+ register_offense(
306
+ excess_range(uri_range, line, line_index),
307
+ line,
308
+ line_index
309
+ )
200
310
  end
201
311
  end
202
312
  end
203
313
  end
204
314
  end
315
+ # rubocop:enable Metrics/ClassLength
@@ -39,16 +39,15 @@ module RuboCop
39
39
  def complexity_score_for(node)
40
40
  case node.type
41
41
  when :case
42
- expression, *whens, _else = *node
43
42
  # If cond is nil, that means each when has an expression that
44
43
  # evaluates to true or false. It's just an alternative to
45
44
  # if/elsif/elsif... so the when nodes count.
46
- if expression.nil?
47
- whens.length
45
+ if node.condition.nil?
46
+ node.when_branches.length
48
47
  else
49
48
  # Otherwise, the case node gets 0.8 complexity points and each
50
49
  # when gets 0.2.
51
- (0.8 + 0.2 * whens.length).round
50
+ (0.8 + 0.2 * node.when_branches.length).round
52
51
  end
53
52
  when :if
54
53
  node.else? && !node.elsif? ? 2 : 1
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # This mixin detects collections that are safe to "break"
6
+ # by inserting new lines. This is useful for breaking
7
+ # up long lines.
8
+ #
9
+ # Let's look at hashes as an example:
10
+ #
11
+ # We know hash keys are safe to break across lines. We can add
12
+ # linebreaks into hashes on lines longer than the specified maximum.
13
+ # Then in further passes cops can clean up the multi-line hash.
14
+ # For example, say the maximum line length is as indicated below:
15
+ #
16
+ # |
17
+ # v
18
+ # {foo: "0000000000", bar: "0000000000", baz: "0000000000"}
19
+ #
20
+ # In a LineLength autocorrection pass, a line is added before
21
+ # the first key that exceeds the column limit:
22
+ #
23
+ # {foo: "0000000000", bar: "0000000000",
24
+ # baz: "0000000000"}
25
+ #
26
+ # In a MultilineHashKeyLineBreaks pass, lines are inserted
27
+ # before all keys:
28
+ #
29
+ # {foo: "0000000000",
30
+ # bar: "0000000000",
31
+ # baz: "0000000000"}
32
+ #
33
+ # Then in future passes FirstHashElementLineBreak,
34
+ # MultilineHashBraceLayout, and TrailingCommaInHashLiteral will
35
+ # manipulate as well until we get:
36
+ #
37
+ # {
38
+ # foo: "0000000000",
39
+ # bar: "0000000000",
40
+ # baz: "0000000000",
41
+ # }
42
+ #
43
+ # (Note: Passes may not happen exactly in this sequence.)
44
+ module CheckLineBreakable
45
+ def extract_breakable_node(node, max)
46
+ if node.send_type?
47
+ args = process_args(node.arguments)
48
+ return extract_breakable_node_from_elements(node, args, max)
49
+ elsif node.array_type? || node.hash_type?
50
+ return extract_breakable_node_from_elements(node, node.children, max)
51
+ end
52
+ nil
53
+ end
54
+
55
+ private
56
+
57
+ def extract_breakable_node_from_elements(node, elements, max)
58
+ return unless breakable_collection?(node, elements)
59
+ return if safe_to_ignore?(node)
60
+
61
+ line = processed_source.lines[node.first_line - 1]
62
+ return if processed_source.commented?(node.loc.begin)
63
+ return if line.length <= max
64
+
65
+ extract_first_element_over_column_limit(node, elements, max)
66
+ end
67
+
68
+ def extract_first_element_over_column_limit(node, elements, max)
69
+ line = node.first_line
70
+ i = 0
71
+ i += 1 while within_column_limit?(elements[i], max, line)
72
+ return elements.first if i.zero?
73
+
74
+ elements[i - 1]
75
+ end
76
+
77
+ def within_column_limit?(element, max, line)
78
+ element && element.loc.column < max && element.loc.line == line
79
+ end
80
+
81
+ def safe_to_ignore?(node)
82
+ return true unless max
83
+ return true if already_on_multiple_lines?(node)
84
+
85
+ # If there's a containing breakable collection on the same
86
+ # line, we let that one get broken first. In a separate pass,
87
+ # this one might get broken as well, but to avoid conflicting
88
+ # or redundant edits, we only mark one offense at a time.
89
+ return true if contained_by_breakable_collection_on_same_line?(node)
90
+
91
+ if contained_by_multiline_collection_that_could_be_broken_up?(node)
92
+ return true
93
+ end
94
+
95
+ false
96
+ end
97
+
98
+ def breakable_collection?(node, elements)
99
+ # For simplicity we only want to insert breaks in normal
100
+ # hashes wrapped in a set of curly braces like {foo: 1}.
101
+ # That is, not a kwargs hash. For method calls, this ensures
102
+ # the method call is made with parens.
103
+ starts_with_bracket = node.loc.begin
104
+
105
+ # If the call has a second argument, we can insert a line
106
+ # break before the second argument and the rest of the
107
+ # argument will get auto-formatted onto separate lines
108
+ # by other cops.
109
+ has_second_element = elements.length >= 2
110
+
111
+ starts_with_bracket && has_second_element
112
+ end
113
+
114
+ def contained_by_breakable_collection_on_same_line?(node)
115
+ node.each_ancestor.find do |ancestor|
116
+ # Ignore ancestors on different lines.
117
+ break if ancestor.first_line != node.first_line
118
+
119
+ if ancestor.hash_type? || ancestor.array_type?
120
+ elements = ancestor.children
121
+ elsif ancestor.send_type?
122
+ elements = process_args(ancestor.arguments)
123
+ else
124
+ next
125
+ end
126
+
127
+ return true if breakable_collection?(ancestor, elements)
128
+ end
129
+
130
+ false
131
+ end
132
+
133
+ def contained_by_multiline_collection_that_could_be_broken_up?(node)
134
+ node.each_ancestor.find do |ancestor|
135
+ if ancestor.hash_type? || ancestor.array_type?
136
+ if breakable_collection?(ancestor, ancestor.children)
137
+ return children_could_be_broken_up?(ancestor.children)
138
+ end
139
+ end
140
+
141
+ next unless ancestor.send_type?
142
+
143
+ args = process_args(ancestor.arguments)
144
+ if breakable_collection?(ancestor, args)
145
+ return children_could_be_broken_up?(args)
146
+ end
147
+ end
148
+
149
+ false
150
+ end
151
+
152
+ def children_could_be_broken_up?(children)
153
+ return false if all_on_same_line?(children)
154
+
155
+ last_seen_line = -1
156
+ children.each do |child|
157
+ return true if last_seen_line >= child.first_line
158
+
159
+ last_seen_line = child.last_line
160
+ end
161
+ false
162
+ end
163
+
164
+ def all_on_same_line?(nodes)
165
+ return true if nodes.empty?
166
+
167
+ nodes.first.first_line == nodes.last.last_line
168
+ end
169
+
170
+ def process_args(args)
171
+ # If there is a trailing hash arg without explicit braces, like this:
172
+ #
173
+ # method(1, 'key1' => value1, 'key2' => value2)
174
+ #
175
+ # ...then each key/value pair is treated as a method 'argument'
176
+ # when determining where line breaks should appear.
177
+ if (last_arg = args.last)
178
+ if last_arg.hash_type? && !last_arg.braces?
179
+ args = args.concat(args.pop.children)
180
+ end
181
+ end
182
+ args
183
+ end
184
+
185
+ def already_on_multiple_lines?(node)
186
+ node.first_line != node.last_line
187
+ end
188
+ end
189
+ end
190
+ end