kube_cluster 1.9.1 → 2.0.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.
- checksums.yaml +4 -4
- data/.envrc +2 -0
- data/.github/FUNDING.yml +3 -0
- data/.rubocop.yml +226 -5984
- data/CHANGELOG.md +17 -0
- data/Gemfile +13 -1
- data/Gemfile.lock +44 -14
- data/bin/rubocop +3 -0
- data/cops/begin_block_assignment.rb +34 -0
- data/cops/comment_structure.rb +307 -0
- data/cops/constant_matches_path.rb +142 -0
- data/cops/multiline_method_arguments.rb +89 -0
- data/cops/no_conditional_assignment.rb +186 -0
- data/cops/no_console_logging.rb +62 -0
- data/cops/no_modifier_if.rb +74 -0
- data/cops/no_return.rb +152 -0
- data/cops/support/prism_source.rb +234 -0
- data/cops/use_tap.rb +100 -0
- data/flake.nix +17 -17
- data/gemset.nix +164 -19
- data/kube_cluster.gemspec +3 -2
- data/lib/kube/cluster.rb +1 -1
- data/lib/{kube/cluster → kube_cluster}/version.rb +1 -1
- data/lib/kube_cluster.rb +3 -0
- metadata +32 -9
- data/bin/increment-version +0 -45
- data/bin/release-gem +0 -32
- data/bin/tag-version +0 -19
- data/lib/kube/cluster/version.rb.erb +0 -7
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bans a call with more than `MaxArguments` arguments.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# walk(rest, current_dir, controller_segments, dynamic_pairs + [[param_name, segment]], last_dir_name)
|
|
7
|
+
#
|
|
8
|
+
# @example Each argument on its own line
|
|
9
|
+
# walk( # YES
|
|
10
|
+
# rest,
|
|
11
|
+
# current_dir,
|
|
12
|
+
# controller_segments,
|
|
13
|
+
# dynamic_pairs + [[param_name, segment]],
|
|
14
|
+
# last_dir_name,
|
|
15
|
+
# )
|
|
16
|
+
module RuboCop
|
|
17
|
+
module Cop
|
|
18
|
+
module Local
|
|
19
|
+
class MultilineMethodArguments < Base
|
|
20
|
+
extend AutoCorrector
|
|
21
|
+
|
|
22
|
+
MSG = "Call takes %<count>d arguments; put each one on its own line."
|
|
23
|
+
|
|
24
|
+
def on_send(node)
|
|
25
|
+
if too_many_on_one_line?(node)
|
|
26
|
+
add_offense(node, message: format(MSG, count: node.arguments.count)) do |corrector|
|
|
27
|
+
break_arguments(corrector, node)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
alias on_csend on_send
|
|
33
|
+
alias on_super on_send
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def too_many_on_one_line?(node)
|
|
38
|
+
node.parenthesized? &&
|
|
39
|
+
node.arguments.count > max_arguments &&
|
|
40
|
+
!every_argument_on_its_own_line?(node)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The first argument must clear the line that opened the call, and no two arguments may share a line.
|
|
44
|
+
def every_argument_on_its_own_line?(node)
|
|
45
|
+
lines = node.arguments.map { |argument| argument.source_range.first_line }
|
|
46
|
+
|
|
47
|
+
lines.first > node.loc.begin.line && lines == lines.uniq
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def break_arguments(corrector, node)
|
|
51
|
+
if safe_to_break?(node)
|
|
52
|
+
corrector.insert_after(node.loc.begin, "\n")
|
|
53
|
+
|
|
54
|
+
node.arguments.each_cons(2) do |left, right|
|
|
55
|
+
corrector.replace(between(left, right), ",\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
corrector.insert_before(node.loc.end, "\n")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A heredoc's body sits below its line, and comments between arguments would be overwritten; both are left alone.
|
|
63
|
+
def safe_to_break?(node)
|
|
64
|
+
node.arguments.none? { |argument| contains_heredoc?(argument) } &&
|
|
65
|
+
node.arguments.each_cons(2).none? do |left, right|
|
|
66
|
+
between(left, right).source.include?("#")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def contains_heredoc?(node)
|
|
71
|
+
node.each_node(:str, :dstr, :xstr).any?(&:heredoc?)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The comma-and-whitespace run separating two arguments.
|
|
75
|
+
def between(left, right)
|
|
76
|
+
Parser::Source::Range.new(
|
|
77
|
+
processed_source.buffer,
|
|
78
|
+
left.source_range.end_pos,
|
|
79
|
+
right.source_range.begin_pos,
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def max_arguments
|
|
84
|
+
cop_config.fetch("MaxArguments", 3)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bans assigning a conditional expression to a variable.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# status = case task.state # NO
|
|
7
|
+
# when :done then :ok
|
|
8
|
+
# else :pending
|
|
9
|
+
# end
|
|
10
|
+
# obj.status = cond ? :ok : :pending # NO
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# status = :pending # YES
|
|
14
|
+
# case task.state
|
|
15
|
+
# when :done
|
|
16
|
+
# status = :ok
|
|
17
|
+
# end
|
|
18
|
+
module RuboCop
|
|
19
|
+
module Cop
|
|
20
|
+
module Local
|
|
21
|
+
class NoConditionalAssignment < Base
|
|
22
|
+
extend AutoCorrector
|
|
23
|
+
|
|
24
|
+
MSG = "Do not assign a `%<keyword>s` expression to a variable; " \
|
|
25
|
+
"assign inside each branch of the %<keyword>s."
|
|
26
|
+
|
|
27
|
+
CONDITIONAL_TYPES = %i[if case case_match].freeze
|
|
28
|
+
|
|
29
|
+
def on_lvasgn(node)
|
|
30
|
+
check(node, node.children.last)
|
|
31
|
+
end
|
|
32
|
+
alias on_ivasgn on_lvasgn
|
|
33
|
+
alias on_cvasgn on_lvasgn
|
|
34
|
+
alias on_gvasgn on_lvasgn
|
|
35
|
+
alias on_casgn on_lvasgn
|
|
36
|
+
alias on_op_asgn on_lvasgn
|
|
37
|
+
alias on_or_asgn on_lvasgn
|
|
38
|
+
alias on_and_asgn on_lvasgn
|
|
39
|
+
alias on_masgn on_lvasgn
|
|
40
|
+
|
|
41
|
+
def on_send(node)
|
|
42
|
+
if node.assignment_method?
|
|
43
|
+
check(node, node.last_argument)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def check(node, rhs)
|
|
50
|
+
if conditional?(rhs)
|
|
51
|
+
add_offense(node, message: format(MSG, keyword: keyword(rhs))) do |corrector|
|
|
52
|
+
if safe_to_push_inside?(node, rhs)
|
|
53
|
+
corrector.replace(node, pushed_inside(node, rhs))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The rewrite repeats the target per branch, so it must be side-effect free, on its own line, and free of heredocs.
|
|
60
|
+
def safe_to_push_inside?(node, rhs)
|
|
61
|
+
# `case/in` pattern matching is left alone — its branch headers are patterns, not expressions.
|
|
62
|
+
rhs.type != :case_match &&
|
|
63
|
+
own_line?(node) &&
|
|
64
|
+
simple_target?(node) &&
|
|
65
|
+
!contains_heredoc?(node) &&
|
|
66
|
+
branches(rhs).all? { |_cond, body| body.nil? || !body.source.include?("\n") }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def simple_target?(node)
|
|
70
|
+
case node.type
|
|
71
|
+
when :lvasgn, :ivasgn, :cvasgn, :gvasgn, :casgn
|
|
72
|
+
true
|
|
73
|
+
when :send
|
|
74
|
+
node.assignment_method? && node.arguments.one? && node.receiver&.type == :send &&
|
|
75
|
+
node.receiver.arguments.empty?
|
|
76
|
+
else
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def own_line?(node)
|
|
82
|
+
line = processed_source.lines[node.first_line - 1].to_s
|
|
83
|
+
line[0...node.loc.column].strip.empty?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def contains_heredoc?(node)
|
|
87
|
+
node.each_descendant(:str, :dstr, :xstr).any? { |n| n.respond_to?(:heredoc?) && n.heredoc? }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The assignment prefix repeated in every branch — "x = ", "@y = ", "obj.z = ".
|
|
91
|
+
def target_source(node)
|
|
92
|
+
if node.type == :send
|
|
93
|
+
"#{node.receiver.source}.#{node.method_name.to_s.chomp('=')} = "
|
|
94
|
+
else
|
|
95
|
+
"#{node.children.first} = "
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# [[condition, body], ...] in source order; a nil condition is the `else`.
|
|
100
|
+
def branches(rhs)
|
|
101
|
+
if rhs.type == :if
|
|
102
|
+
if_branches(rhs)
|
|
103
|
+
else
|
|
104
|
+
case_branches(rhs)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def if_branches(node, acc = [])
|
|
109
|
+
cond, then_body, else_body = *node
|
|
110
|
+
acc << [cond.source, then_body]
|
|
111
|
+
|
|
112
|
+
if else_body&.type == :if && !else_body.ternary?
|
|
113
|
+
if_branches(else_body, acc)
|
|
114
|
+
else
|
|
115
|
+
acc << [nil, else_body]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
acc
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def case_branches(node)
|
|
122
|
+
node.when_branches.map { |w| [w, w.body] } + [[nil, node.else_branch]]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def pushed_inside(node, rhs)
|
|
126
|
+
indent = " " * node.loc.column
|
|
127
|
+
target = target_source(node)
|
|
128
|
+
|
|
129
|
+
if rhs.type == :if
|
|
130
|
+
rendered_if(rhs, indent, target)
|
|
131
|
+
else
|
|
132
|
+
rendered_case(rhs, indent, target)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def rendered_if(rhs, indent, target)
|
|
137
|
+
parts = if_branches(rhs).each_with_index.map { |(cond, body), i|
|
|
138
|
+
if cond.nil?
|
|
139
|
+
header = "#{indent}else"
|
|
140
|
+
elsif i.zero?
|
|
141
|
+
header = "#{indent}if #{cond}"
|
|
142
|
+
else
|
|
143
|
+
header = "#{indent}elsif #{cond}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
"#{header}\n#{indent} #{target}#{body ? body.source : 'nil'}"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# The replacement begins at the node's own column, so the first line must NOT carry the indent again.
|
|
150
|
+
"#{parts.join("\n").sub(/\A#{indent}/, '')}\n#{indent}end"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def rendered_case(rhs, indent, target)
|
|
154
|
+
head = ["case", rhs.condition&.source].compact.join(" ")
|
|
155
|
+
|
|
156
|
+
parts = rhs.when_branches.map { |w|
|
|
157
|
+
conditions = w.conditions.map(&:source).join(", ")
|
|
158
|
+
"#{indent}when #{conditions}\n#{indent} #{target}#{w.body ? w.body.source : 'nil'}"
|
|
159
|
+
}
|
|
160
|
+
else_body = rhs.else_branch
|
|
161
|
+
parts << "#{indent}else\n#{indent} #{target}#{else_body ? else_body.source : 'nil'}"
|
|
162
|
+
|
|
163
|
+
"#{head}\n#{parts.join("\n")}\n#{indent}end"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def conditional?(rhs)
|
|
167
|
+
rhs.is_a?(RuboCop::AST::Node) &&
|
|
168
|
+
CONDITIONAL_TYPES.include?(rhs.type)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# `keyword` is "" on a ternary — it has no `if` token to point at.
|
|
172
|
+
def keyword(rhs)
|
|
173
|
+
if rhs.type == :if
|
|
174
|
+
if rhs.ternary?
|
|
175
|
+
"ternary"
|
|
176
|
+
else
|
|
177
|
+
rhs.keyword
|
|
178
|
+
end
|
|
179
|
+
else
|
|
180
|
+
"case"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bans logging straight at the `Console` singleton.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# Console.info(self, "started", id: 1) # NO
|
|
7
|
+
# Console.logger.warn(self, "slow") # NO
|
|
8
|
+
# Console.debug(self) { "expensive" } # NO
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# Rails.logger.info("started") # YES
|
|
12
|
+
module RuboCop
|
|
13
|
+
module Cop
|
|
14
|
+
module Local
|
|
15
|
+
class NoConsoleLogging < Base
|
|
16
|
+
MSG = "Do not log through `Console` directly; use `Rails.logger.%<level>s` " \
|
|
17
|
+
"so logging goes through Console::Adapter::Rails (tags, silencing, config)."
|
|
18
|
+
|
|
19
|
+
MSG_LOGGER = "Do not reach for `Console.logger`; use `Rails.logger`, which " \
|
|
20
|
+
"console-adapter-rails already backs with Console."
|
|
21
|
+
|
|
22
|
+
# Console's severity methods, plus `call`, its lowest-level entry point.
|
|
23
|
+
LEVELS = %i[debug info warn error fatal call].freeze
|
|
24
|
+
|
|
25
|
+
def on_send(node)
|
|
26
|
+
if console_logger?(node)
|
|
27
|
+
add_offense(node, message: MSG_LOGGER)
|
|
28
|
+
elsif LEVELS.include?(node.method_name) && console_receiver?(node.receiver)
|
|
29
|
+
add_offense(node, message: format(MSG, level: node.method_name))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# `Console.logger`, but NOT the `Console.logger` inside a `Console.logger.info(...)` — that whole call is reported once, as a level offence, rather than twice.
|
|
36
|
+
def console_logger?(node)
|
|
37
|
+
node.method_name == :logger &&
|
|
38
|
+
const_console?(node.receiver) &&
|
|
39
|
+
!level_call_on?(node)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Is `node` the receiver of a level call, i.e. the `Console.logger` in `Console.logger.info(...)`?
|
|
43
|
+
def level_call_on?(node)
|
|
44
|
+
parent = node.parent
|
|
45
|
+
|
|
46
|
+
parent&.send_type? && parent.receiver.equal?(node) && LEVELS.include?(parent.method_name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The receiver of a level call: `Console` itself or `Console.logger`.
|
|
50
|
+
def console_receiver?(receiver)
|
|
51
|
+
const_console?(receiver) ||
|
|
52
|
+
(receiver&.send_type? && receiver.method_name == :logger && const_console?(receiver.receiver))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# `Console` or `::Console` — and nothing nested, so a `Foo::Console` of your own is left alone.
|
|
56
|
+
def const_console?(node)
|
|
57
|
+
node&.const_type? && node.short_name == :Console && (node.namespace.nil? || node.namespace.cbase_type?)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bans the modifier `if` / `unless` form.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# return true if task.state_task_completed? # NO
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# if task.state_task_completed? # YES
|
|
10
|
+
# return true
|
|
11
|
+
# end
|
|
12
|
+
module RuboCop
|
|
13
|
+
module Cop
|
|
14
|
+
module Local
|
|
15
|
+
class NoModifierIf < Base
|
|
16
|
+
extend AutoCorrector
|
|
17
|
+
|
|
18
|
+
MSG = "Do not use modifier `%<keyword>s`; use the full `%<keyword>s ... end` block form."
|
|
19
|
+
|
|
20
|
+
def on_if(node)
|
|
21
|
+
if node.modifier_form?
|
|
22
|
+
add_offense(node, message: format(MSG, keyword: node.keyword)) do |corrector|
|
|
23
|
+
if safe_to_unwrap?(node)
|
|
24
|
+
corrector.replace(node, block_form(node))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# A modifier that is not the whole statement, or that contains a heredoc, is left alone.
|
|
33
|
+
def safe_to_unwrap?(node)
|
|
34
|
+
own_line?(node) && !contains_heredoc?(node)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def own_line?(node)
|
|
38
|
+
line = processed_source.lines[node.first_line - 1].to_s
|
|
39
|
+
line[0...node.loc.column].strip.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def contains_heredoc?(node)
|
|
43
|
+
node.each_descendant(:str, :dstr, :xstr).any? { |n| n.respond_to?(:heredoc?) && n.heredoc? }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def block_form(node)
|
|
47
|
+
indent = " " * node.loc.column
|
|
48
|
+
body = node.if_branch || node.else_branch
|
|
49
|
+
|
|
50
|
+
"#{node.keyword} #{node.condition.source}\n" \
|
|
51
|
+
"#{reindent(body, indent)}\n" \
|
|
52
|
+
"#{indent}end"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Move the body to a new indent, shifting later lines by the delta so the body's shape survives.
|
|
56
|
+
def reindent(node, indent)
|
|
57
|
+
target = indent.length + 2
|
|
58
|
+
delta = target - node.loc.column
|
|
59
|
+
lines = node.source.lines
|
|
60
|
+
|
|
61
|
+
rest = lines[1..].to_a.map { |line|
|
|
62
|
+
if delta.negative?
|
|
63
|
+
line.chomp.sub(/\A {0,#{-delta}}/, "")
|
|
64
|
+
else
|
|
65
|
+
"#{' ' * delta}#{line.chomp}"
|
|
66
|
+
end
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
[("#{' ' * target}#{lines.first.chomp}")].concat(rest).join("\n")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/cops/no_return.rb
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bans the `return` keyword. A method's last expression is its return value;
|
|
4
|
+
# early exits must be expressed through `if` / `unless` / `case` structure,
|
|
5
|
+
# not by short-circuiting out of the middle of a block.
|
|
6
|
+
#
|
|
7
|
+
# Autocorrection handles ONE shape — the guard clause at the top level of a
|
|
8
|
+
# method body:
|
|
9
|
+
#
|
|
10
|
+
# if cond → if cond
|
|
11
|
+
# return X X
|
|
12
|
+
# end else
|
|
13
|
+
# rest rest
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Everything else (a `return` inside a block, a loop, or a `rescue`) is left
|
|
17
|
+
# alone: unwinding those changes which construct the value escapes from, and
|
|
18
|
+
# there is no rewrite that is correct without reading the surrounding intent.
|
|
19
|
+
module RuboCop
|
|
20
|
+
module Cop
|
|
21
|
+
module Local
|
|
22
|
+
class NoReturn < Base
|
|
23
|
+
extend AutoCorrector
|
|
24
|
+
|
|
25
|
+
MSG = "Do not use `return`; let the method's last expression be the return value."
|
|
26
|
+
|
|
27
|
+
def on_new_investigation
|
|
28
|
+
super
|
|
29
|
+
# One guard per body per pass — two corrections inside the same body
|
|
30
|
+
# would overlap and clobber each other. `rubocop -a` re-runs the cop
|
|
31
|
+
# until it stops changing anything, so chained guards still unwind,
|
|
32
|
+
# one per pass.
|
|
33
|
+
@corrected_bodies = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def on_return(node)
|
|
37
|
+
add_offense(node) do |corrector|
|
|
38
|
+
guard = correctable_guard(node)
|
|
39
|
+
if guard
|
|
40
|
+
body, index = guard
|
|
41
|
+
if @corrected_bodies[body.object_id].nil?
|
|
42
|
+
@corrected_bodies[body.object_id] = true
|
|
43
|
+
corrector.replace(replaced_range(body, index), rewritten(body, index))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Returns [body_node, index_of_guard] when +node+ is the whole body of
|
|
52
|
+
# an `if`/`unless` that is itself a plain statement in a method body
|
|
53
|
+
# with at least one statement after it. Otherwise nil.
|
|
54
|
+
def correctable_guard(node)
|
|
55
|
+
if_node = node.parent
|
|
56
|
+
|
|
57
|
+
if if_node.nil? || if_node.type != :if || if_node.modifier_form? ||
|
|
58
|
+
if_node.ternary? || if_node.elsif? || node != if_node.if_branch ||
|
|
59
|
+
if_node.else_branch
|
|
60
|
+
nil
|
|
61
|
+
else
|
|
62
|
+
body = if_node.parent
|
|
63
|
+
if body.nil? || body.type != :begin || !tail_position?(body)
|
|
64
|
+
nil
|
|
65
|
+
else
|
|
66
|
+
index = body.children.index(if_node)
|
|
67
|
+
if index.nil? || index == body.children.size - 1
|
|
68
|
+
nil
|
|
69
|
+
else
|
|
70
|
+
[body, index]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# "Everything after the guard" is only the method's value if this
|
|
77
|
+
# statement list is in TAIL position — the method body itself, or a
|
|
78
|
+
# branch of a conditional that is itself in tail position (which is
|
|
79
|
+
# what a chain of guards unwinds into, one pass at a time).
|
|
80
|
+
#
|
|
81
|
+
# Recursion stops at anything else, and in particular at a `block`: a
|
|
82
|
+
# `return` inside one exits the enclosing METHOD, not the block, so
|
|
83
|
+
# turning it into a trailing value would change what the block yields.
|
|
84
|
+
# `rescue` / `ensure` bodies stop it too.
|
|
85
|
+
def tail_position?(node)
|
|
86
|
+
parent = node.parent
|
|
87
|
+
|
|
88
|
+
if parent.nil?
|
|
89
|
+
false
|
|
90
|
+
elsif %i[def defs].include?(parent.type)
|
|
91
|
+
parent.body == node
|
|
92
|
+
elsif parent.type == :if && !parent.ternary?
|
|
93
|
+
(node == parent.if_branch || node == parent.else_branch) && tail_position?(parent)
|
|
94
|
+
elsif parent.type == :begin
|
|
95
|
+
node == parent.children.last && tail_position?(parent)
|
|
96
|
+
else
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def replaced_range(body, index)
|
|
102
|
+
body.children[index].source_range.join(body.children.last.source_range)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def rewritten(body, index)
|
|
106
|
+
if_node = body.children[index]
|
|
107
|
+
indent = " " * if_node.loc.column
|
|
108
|
+
guarded = if_node.if_branch.children.first # the return's value, or nil
|
|
109
|
+
rest = body.children[(index + 1)..]
|
|
110
|
+
|
|
111
|
+
if guarded
|
|
112
|
+
guard_value = reindent(guarded, indent)
|
|
113
|
+
else
|
|
114
|
+
guard_value = "#{indent} nil"
|
|
115
|
+
end
|
|
116
|
+
rest_source = rest.map { |s| reindent(s, indent) }.join("\n")
|
|
117
|
+
|
|
118
|
+
# `unless cond / return X / end` means "X unless cond", so the
|
|
119
|
+
# branches swap rather than the condition being negated.
|
|
120
|
+
if if_node.keyword == "unless"
|
|
121
|
+
first, second = rest_source, guard_value
|
|
122
|
+
else
|
|
123
|
+
first, second = guard_value, rest_source
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
"if #{if_node.condition.source}\n#{first}\n#{indent}else\n#{second}\n#{indent}end"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Move a statement to a new indent. Only line 1 is positioned
|
|
130
|
+
# absolutely — `source` has already stripped its leading whitespace.
|
|
131
|
+
# Lines 2..n still carry their ORIGINAL absolute indentation, so they
|
|
132
|
+
# get shifted by the delta instead, which is what keeps a multi-line
|
|
133
|
+
# body's internal shape intact.
|
|
134
|
+
def reindent(node, indent)
|
|
135
|
+
target = indent.length + 2
|
|
136
|
+
delta = target - node.loc.column
|
|
137
|
+
lines = node.source.lines
|
|
138
|
+
|
|
139
|
+
rest = lines[1..].to_a.map { |line|
|
|
140
|
+
if delta.negative?
|
|
141
|
+
line.chomp.sub(/\A {0,#{-delta}}/, "")
|
|
142
|
+
else
|
|
143
|
+
"#{' ' * delta}#{line.chomp}"
|
|
144
|
+
end
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
[("#{' ' * target}#{lines.first.chomp}")].concat(rest).join("\n")
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|