/
indentation_width.rb
373 lines (296 loc) · 11.2 KB
/
indentation_width.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# frozen_string_literal: true
module RuboCop
module Cop
module Layout
# This cop checks for indentation that doesn't use the specified number
# of spaces.
#
# See also the IndentationConsistency cop which is the companion to this
# one.
#
# @example
# # bad
# class A
# def test
# puts 'hello'
# end
# end
#
# # good
# class A
# def test
# puts 'hello'
# end
# end
#
# @example IgnoredPatterns: ['^\s*module']
# # bad
# module A
# class B
# def test
# puts 'hello'
# end
# end
# end
#
# # good
# module A
# class B
# def test
# puts 'hello'
# end
# end
# end
class IndentationWidth < Base # rubocop:disable Metrics/ClassLength
include EndKeywordAlignment
include Alignment
include CheckAssignment
include IgnoredPattern
include RangeHelp
extend AutoCorrector
MSG = 'Use %<configured_indentation_width>d (not %<indentation>d) ' \
'spaces for%<name>s indentation.'
# @!method access_modifier?(node)
def_node_matcher :access_modifier?, <<~PATTERN
[(send ...) access_modifier?]
PATTERN
def on_rescue(node)
_begin_node, *_rescue_nodes, else_node = *node
check_indentation(node.loc.else, else_node)
end
def on_ensure(node)
check_indentation(node.loc.keyword, node.body)
end
alias on_resbody on_ensure
alias on_for on_ensure
def on_kwbegin(node)
# Check indentation against end keyword but only if it's first on its
# line.
return unless begins_its_line?(node.loc.end)
check_indentation(node.loc.end, node.children.first)
end
def on_block(node)
end_loc = node.loc.end
return unless begins_its_line?(end_loc)
check_indentation(end_loc, node.body)
return unless indented_internal_methods_style?
check_members(end_loc, [node.body])
end
def on_class(node)
check_members(node.loc.keyword, [node.body])
end
alias on_sclass on_class
alias on_module on_class
def on_send(node)
super
return unless node.adjacent_def_modifier?
def_end_config = config.for_cop('Layout/DefEndAlignment')
style = def_end_config['EnforcedStyleAlignWith'] || 'start_of_line'
base = if style == 'def'
node.first_argument
else
leftmost_modifier_of(node) || node
end
check_indentation(base.source_range, node.first_argument.body)
ignore_node(node.first_argument)
end
alias on_csend on_send
def on_def(node)
return if ignored_node?(node)
check_indentation(node.loc.keyword, node.body)
end
alias on_defs on_def
def on_while(node, base = node)
return if ignored_node?(node)
return unless node.single_line_condition?
check_indentation(base.loc, node.body)
end
alias on_until on_while
def on_case(case_node)
case_node.each_when do |when_node|
check_indentation(when_node.loc.keyword, when_node.body)
end
check_indentation(case_node.when_branches.last.loc.keyword,
case_node.else_branch)
end
def on_if(node, base = node)
return if ignored_node?(node)
return if node.ternary? || node.modifier_form?
check_if(node, node.body, node.else_branch, base.loc)
end
private
def autocorrect(corrector, node)
AlignmentCorrector.correct(corrector, processed_source, node, @column_delta)
end
def check_members(base, members)
check_indentation(base, select_check_member(members.first))
return unless members.any? && members.first.begin_type?
if indentation_consistency_style == 'indented_internal_methods'
check_members_for_indented_internal_methods_style(members)
else
check_members_for_normal_style(base, members)
end
end
def select_check_member(member)
return unless member
if access_modifier?(member.children.first)
return if access_modifier_indentation_style == 'outdent'
member.children.first
else
member
end
end
def check_members_for_indented_internal_methods_style(members)
each_member(members) do |member, previous_modifier|
check_indentation(previous_modifier, member,
indentation_consistency_style)
end
end
def check_members_for_normal_style(base, members)
members.first.children.each do |member|
next if member.send_type? && member.access_modifier?
check_indentation(base, member)
end
end
def each_member(members)
previous_modifier = nil
members.first.children.each do |member|
if member.send_type? && member.special_modifier?
previous_modifier = member
elsif previous_modifier
yield member, previous_modifier.source_range
previous_modifier = nil
end
end
end
def indented_internal_methods_style?
indentation_consistency_style == 'indented_internal_methods'
end
def special_modifier?(node)
node.bare_access_modifier? && SPECIAL_MODIFIERS.include?(node.source)
end
def access_modifier_indentation_style
config.for_cop('Layout/AccessModifierIndentation')['EnforcedStyle']
end
def indentation_consistency_style
config.for_cop('Layout/IndentationConsistency')['EnforcedStyle']
end
def check_assignment(node, rhs)
# If there are method calls chained to the right hand side of the
# assignment, we let rhs be the receiver of those method calls before
# we check its indentation.
rhs = first_part_of_call_chain(rhs)
return unless rhs
end_config = config.for_cop('Layout/EndAlignment')
style = end_config['EnforcedStyleAlignWith'] || 'keyword'
base = variable_alignment?(node.loc, rhs, style.to_sym) ? node : rhs
case rhs.type
when :if then on_if(rhs, base)
when :while, :until then on_while(rhs, base)
else return
end
ignore_node(rhs)
end
def check_if(node, body, else_clause, base_loc)
return if node.ternary?
check_indentation(base_loc, body)
return unless else_clause
# If the else clause is an elsif, it will get its own on_if call so
# we don't need to process it here.
return if else_clause.if_type? && else_clause.elsif?
check_indentation(node.loc.else, else_clause)
end
def check_indentation(base_loc, body_node, style = 'normal')
return unless indentation_to_check?(base_loc, body_node)
indentation = column_offset_between(body_node.loc, base_loc)
@column_delta = configured_indentation_width - indentation
return if @column_delta.zero?
offense(body_node, indentation, style)
end
def offense(body_node, indentation, style)
# This cop only auto-corrects the first statement in a def body, for
# example.
body_node = body_node.children.first if body_node.begin_type? && !parentheses?(body_node)
# Since autocorrect changes a number of lines, and not only the line
# where the reported offending range is, we avoid auto-correction if
# this cop has already found other offenses is the same
# range. Otherwise, two corrections can interfere with each other,
# resulting in corrupted code.
node = if autocorrect? && other_offense_in_same_range?(body_node)
nil
else
body_node
end
name = style == 'normal' ? '' : " #{style}"
message = message(configured_indentation_width, indentation, name)
add_offense(offending_range(body_node, indentation), message: message) do |corrector|
autocorrect(corrector, node)
end
end
def message(configured_indentation_width, indentation, name)
format(
MSG,
configured_indentation_width: configured_indentation_width,
indentation: indentation,
name: name
)
end
# Returns true if the given node is within another node that has
# already been marked for auto-correction by this cop.
def other_offense_in_same_range?(node)
expr = node.source_range
@offense_ranges ||= []
return true if @offense_ranges.any? { |r| within?(expr, r) }
@offense_ranges << expr
false
end
def indentation_to_check?(base_loc, body_node)
return false if skip_check?(base_loc, body_node)
if body_node.rescue_type?
check_rescue?(body_node)
elsif body_node.ensure_type?
block_body, = *body_node
return unless block_body
check_rescue?(block_body) if block_body.rescue_type?
else
true
end
end
def check_rescue?(rescue_node)
rescue_node.body
end
def skip_check?(base_loc, body_node)
return true if ignored_line?(base_loc)
return true unless body_node
# Don't check if expression is on same line as "then" keyword, etc.
return true if body_node.loc.line == base_loc.line
return true if starts_with_access_modifier?(body_node)
# Don't check indentation if the line doesn't start with the body.
# For example, lines like "else do_something".
first_char_pos_on_line = body_node.source_range.source_line =~ /\S/
return true unless body_node.loc.column == first_char_pos_on_line
end
def offending_range(body_node, indentation)
expr = body_node.source_range
begin_pos = expr.begin_pos
ind = expr.begin_pos - indentation
pos = indentation >= 0 ? ind..begin_pos : begin_pos..ind
range_between(pos.begin, pos.end)
end
def starts_with_access_modifier?(body_node)
return unless body_node.begin_type?
starting_node = body_node.children.first
return unless starting_node
starting_node.send_type? && starting_node.bare_access_modifier?
end
def configured_indentation_width
cop_config['Width']
end
def leftmost_modifier_of(node)
return node unless node.parent&.send_type?
leftmost_modifier_of(node.parent)
end
end
end
end
end