kube_schema 1.11.0 → 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,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