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.
@@ -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