forked from rubocop/rubocop
-
Notifications
You must be signed in to change notification settings - Fork 0
/
infinite_loop.rb
128 lines (107 loc) · 3.69 KB
/
infinite_loop.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
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# Use `Kernel#loop` for infinite loops.
#
# @example
# # bad
# while true
# work
# end
#
# # good
# loop do
# work
# end
class InfiniteLoop < Cop
LEADING_SPACE = /\A(\s*)/.freeze
MSG = 'Use `Kernel#loop` for infinite loops.'
def join_force?(force_class)
force_class == VariableForce
end
def after_leaving_scope(scope, _variable_table)
@variables ||= []
@variables.concat(scope.variables.values)
end
def on_while(node)
while_or_until(node) if node.condition.truthy_literal?
end
def on_until(node)
while_or_until(node) if node.condition.falsey_literal?
end
alias on_while_post on_while
alias on_until_post on_until
def autocorrect(node)
if node.while_post_type? || node.until_post_type?
replace_begin_end_with_modifier(node)
elsif node.modifier_form?
replace_source(node.source_range, modifier_replacement(node))
else
replace_source(non_modifier_range(node), 'loop do')
end
end
private
def while_or_until(node)
range = node.source_range
# Not every `while true` and `until false` can be turned into a
# `loop do` without further modification. The reason is that a
# variable that's introduced inside a while/until loop is in scope
# outside of that loop too, but a variable that's assigned for the
# first time inside a block cannot be accessed after the block. In
# those more complicated cases we don't report an offense.
return if @variables.any? do |var|
assigned_inside_loop?(var, range) &&
!assigned_before_loop?(var, range) &&
referenced_after_loop?(var, range)
end
add_offense(node, location: :keyword)
end
def assigned_inside_loop?(var, range)
var.assignments.any? { |a| range.contains?(a.node.source_range) }
end
def assigned_before_loop?(var, range)
b = range.begin_pos
var.assignments.any? { |a| a.node.source_range.end_pos < b }
end
def referenced_after_loop?(var, range)
e = range.end_pos
var.references.any? { |r| r.node.source_range.begin_pos > e }
end
def replace_begin_end_with_modifier(node)
lambda do |corrector|
corrector.replace(node.body.loc.begin, 'loop do')
corrector.remove(node.body.loc.end.end.join(node.source_range.end))
end
end
def replace_source(range, replacement)
lambda do |corrector|
corrector.replace(range, replacement)
end
end
def modifier_replacement(node)
body = node.body
if node.single_line?
"loop { #{body.source} }"
else
indentation = body.source_range.source_line[LEADING_SPACE]
['loop do', body.source.gsub(/^/, configured_indent),
'end'].join("\n#{indentation}")
end
end
def non_modifier_range(node)
start_range = node.loc.keyword.begin
end_range = if node.do?
node.loc.begin.end
else
node.condition.source_range.end
end
start_range.join(end_range)
end
def configured_indent
' ' * config.for_cop('Layout/IndentationWidth')['Width']
end
end
end
end
end