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
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [2.0.0] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `require "kube_cluster"` now works as an entry point alongside the existing
|
|
14
|
+
`require "kube/cluster"`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Breaking:** requires kube_schema `~> 2.0`, which compiles schema
|
|
18
|
+
definitions lazily, one at a time, on demand, instead of compiling the
|
|
19
|
+
whole OpenAPI document at boot. `Kube::Cluster[kind].schema` is now a
|
|
20
|
+
`Kube::Schema::Lazy::Source` rather than a `JSONSchemer::Schema`; it answers
|
|
21
|
+
the same `#value`/`#valid?`/`#validate`, so call sites that only use those
|
|
22
|
+
methods need no changes, but code that inspects the schema object's class
|
|
23
|
+
will need updating.
|
|
24
|
+
|
|
8
25
|
## [1.9.1] - 2026-09-28
|
|
9
26
|
|
|
10
27
|
### Fixed
|
data/Gemfile
CHANGED
|
@@ -2,7 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
source "https://rubygems.org"
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
gem "activesupport", "~> 8.0"
|
|
6
|
+
gem "kube_kubectl", "~> 2.0"
|
|
7
|
+
gem "kube_schema", "~> 2.0"
|
|
8
|
+
|
|
9
|
+
gem "lefthook", "~> 2.1"
|
|
10
|
+
gem "rake", "~> 13.0"
|
|
11
|
+
gem "scampi", "~> 1.0"
|
|
12
|
+
gem "rubocop"
|
|
13
|
+
gem "decode"
|
|
14
|
+
|
|
15
|
+
group :release do
|
|
16
|
+
gem "gem_kit-release", "~> 0.3"
|
|
17
|
+
end
|
|
6
18
|
|
|
7
19
|
group :maintenance, optional: true do
|
|
8
20
|
gem "utopia-project"
|
data/Gemfile.lock
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
PATH
|
|
2
|
-
remote: .
|
|
3
|
-
specs:
|
|
4
|
-
kube_cluster (1.8.0)
|
|
5
|
-
activesupport (~> 8.0)
|
|
6
|
-
kube_kubectl (~> 2.0)
|
|
7
|
-
kube_schema (~> 1.11.0)
|
|
8
|
-
|
|
9
1
|
GEM
|
|
10
2
|
remote: https://rubygems.org/
|
|
11
3
|
specs:
|
|
@@ -22,6 +14,7 @@ GEM
|
|
|
22
14
|
securerandom (>= 0.3)
|
|
23
15
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
24
16
|
uri (>= 0.13.1)
|
|
17
|
+
ast (2.4.3)
|
|
25
18
|
async (2.39.0)
|
|
26
19
|
console (~> 1.29)
|
|
27
20
|
fiber-annotation
|
|
@@ -73,7 +66,7 @@ GEM
|
|
|
73
66
|
bake (~> 0.21)
|
|
74
67
|
markly (~> 0.8)
|
|
75
68
|
base64 (0.3.0)
|
|
76
|
-
bigdecimal (4.1.
|
|
69
|
+
bigdecimal (4.1.3)
|
|
77
70
|
build-files (1.10.2)
|
|
78
71
|
concurrent-ruby (1.3.6)
|
|
79
72
|
connection_pool (3.0.2)
|
|
@@ -107,6 +100,10 @@ GEM
|
|
|
107
100
|
fiber-local (1.1.0)
|
|
108
101
|
fiber-storage
|
|
109
102
|
fiber-storage (1.0.1)
|
|
103
|
+
gem_kit (0.2.0)
|
|
104
|
+
gem_kit-release (0.3.2)
|
|
105
|
+
gem_kit (>= 0.1, < 1.0)
|
|
106
|
+
thor (~> 1.3)
|
|
110
107
|
hana (1.3.7)
|
|
111
108
|
http-accept (2.2.1)
|
|
112
109
|
i18n (1.14.8)
|
|
@@ -132,10 +129,13 @@ GEM
|
|
|
132
129
|
rubyshell (~> 1.5)
|
|
133
130
|
shellwords (~> 0.2.2)
|
|
134
131
|
string_builder (~> 1.2.2)
|
|
135
|
-
kube_schema (
|
|
132
|
+
kube_schema (2.0.0)
|
|
133
|
+
gem_kit (~> 0.2)
|
|
136
134
|
json_schemer (~> 2.5.0)
|
|
137
135
|
rubyshell (~> 1.5.0)
|
|
136
|
+
language_server-protocol (3.17.0.6)
|
|
138
137
|
lefthook (2.1.10)
|
|
138
|
+
lint_roller (1.1.0)
|
|
139
139
|
localhost (1.8.0)
|
|
140
140
|
bake
|
|
141
141
|
logger (1.7.0)
|
|
@@ -167,6 +167,10 @@ GEM
|
|
|
167
167
|
net-smtp (0.5.1)
|
|
168
168
|
net-protocol
|
|
169
169
|
openssl (4.0.2)
|
|
170
|
+
parallel (2.3.0)
|
|
171
|
+
parser (3.3.12.0)
|
|
172
|
+
ast (~> 2.4.1)
|
|
173
|
+
racc
|
|
170
174
|
pp (0.6.3)
|
|
171
175
|
prettyprint
|
|
172
176
|
prettyprint (0.2.0)
|
|
@@ -186,11 +190,13 @@ GEM
|
|
|
186
190
|
psych (5.3.1)
|
|
187
191
|
date
|
|
188
192
|
stringio
|
|
193
|
+
racc (1.8.1)
|
|
189
194
|
rack (3.2.6)
|
|
190
195
|
rackula (1.4.1)
|
|
191
196
|
falcon (~> 0.46)
|
|
192
197
|
samovar (~> 2.1)
|
|
193
198
|
variant
|
|
199
|
+
rainbow (3.1.1)
|
|
194
200
|
rake (13.4.2)
|
|
195
201
|
rbs (4.0.2)
|
|
196
202
|
logger
|
|
@@ -200,9 +206,24 @@ GEM
|
|
|
200
206
|
erb
|
|
201
207
|
psych (>= 4.0.0)
|
|
202
208
|
tsort
|
|
203
|
-
regexp_parser (2.
|
|
209
|
+
regexp_parser (2.13.1)
|
|
204
210
|
reline (0.6.3)
|
|
205
211
|
io-console (~> 0.5)
|
|
212
|
+
rubocop (1.92.0)
|
|
213
|
+
json (>= 2.3)
|
|
214
|
+
language_server-protocol (~> 3.17.0.2)
|
|
215
|
+
lint_roller (~> 1.1.0)
|
|
216
|
+
parallel (>= 1.10)
|
|
217
|
+
parser (>= 3.3.0.2)
|
|
218
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
219
|
+
regexp_parser (>= 2.11.3, < 3.0)
|
|
220
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
221
|
+
ruby-progressbar (~> 1.7)
|
|
222
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
223
|
+
rubocop-ast (1.50.0)
|
|
224
|
+
parser (>= 3.3.7.2)
|
|
225
|
+
prism (~> 1.7)
|
|
226
|
+
ruby-progressbar (1.13.0)
|
|
206
227
|
rubyshell (1.5.0)
|
|
207
228
|
rugged (1.9.0)
|
|
208
229
|
samovar (2.4.1)
|
|
@@ -211,16 +232,20 @@ GEM
|
|
|
211
232
|
scampi (1.0.0)
|
|
212
233
|
securerandom (0.4.1)
|
|
213
234
|
shellwords (0.2.2)
|
|
214
|
-
simpleidn (0.
|
|
235
|
+
simpleidn (0.3.0)
|
|
215
236
|
string-format (0.2.0)
|
|
216
237
|
string_builder (1.2.4)
|
|
217
238
|
stringio (3.2.0)
|
|
239
|
+
thor (1.5.0)
|
|
218
240
|
thread-local (1.1.0)
|
|
219
241
|
timeout (0.6.1)
|
|
220
242
|
traces (0.18.2)
|
|
221
243
|
tsort (0.2.0)
|
|
222
244
|
tzinfo (2.0.6)
|
|
223
245
|
concurrent-ruby (~> 1.0)
|
|
246
|
+
unicode-display_width (3.3.0)
|
|
247
|
+
unicode-emoji (~> 4.3)
|
|
248
|
+
unicode-emoji (4.3.0)
|
|
224
249
|
uri (1.1.1)
|
|
225
250
|
utopia (2.32.1)
|
|
226
251
|
bake (~> 0.20)
|
|
@@ -254,14 +279,19 @@ PLATFORMS
|
|
|
254
279
|
x86_64-linux
|
|
255
280
|
|
|
256
281
|
DEPENDENCIES
|
|
282
|
+
activesupport (~> 8.0)
|
|
257
283
|
bake-gem
|
|
258
284
|
bake-modernize
|
|
259
285
|
bake-releases
|
|
260
|
-
|
|
286
|
+
decode
|
|
287
|
+
gem_kit-release (~> 0.3)
|
|
288
|
+
kube_kubectl (~> 2.0)
|
|
289
|
+
kube_schema (~> 2.0)
|
|
261
290
|
lefthook (~> 2.1)
|
|
262
291
|
rake (~> 13.0)
|
|
292
|
+
rubocop
|
|
263
293
|
scampi (~> 1.0)
|
|
264
294
|
utopia-project
|
|
265
295
|
|
|
266
296
|
BUNDLED WITH
|
|
267
|
-
2.
|
|
297
|
+
2.7.2
|
data/bin/rubocop
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bans assigning a `begin ... end` to a variable.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# begin
|
|
7
|
+
# lvar = ...
|
|
8
|
+
# end
|
|
9
|
+
module RuboCop
|
|
10
|
+
module Cop
|
|
11
|
+
module Local
|
|
12
|
+
class BeginBlockAssignment < Base
|
|
13
|
+
MSG = "Do not assign a `begin...end` expression to a variable; " \
|
|
14
|
+
"move the assignment inside the begin block."
|
|
15
|
+
|
|
16
|
+
def on_lvasgn(node)
|
|
17
|
+
check(node)
|
|
18
|
+
end
|
|
19
|
+
alias on_ivasgn on_lvasgn
|
|
20
|
+
alias on_cvasgn on_lvasgn
|
|
21
|
+
alias on_gvasgn on_lvasgn
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def check(node)
|
|
26
|
+
rhs = node.children.last
|
|
27
|
+
if rhs.is_a?(RuboCop::AST::Node) && rhs.type == :kwbegin
|
|
28
|
+
add_offense(node)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the Apache License, Version 2.0.
|
|
4
|
+
# Copyright, 2026, by General Intelligence Systems.
|
|
5
|
+
|
|
6
|
+
# Checks the structure of a comment, which decode parses into three parts: the summary, the annotations, and the summary belonging to each annotation.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # Connect to the given endpoint, returning the HTTP client and reference. <- summary
|
|
10
|
+
# # @parameter endpoint [Endpoint] used to connect to the remote system. <- annotation
|
|
11
|
+
# # @returns [Tuple(Client, Reference)] the client and reference.
|
|
12
|
+
#
|
|
13
|
+
# Four things are checked, each measured against how async-rest, async-discord and async-ollama document themselves:
|
|
14
|
+
#
|
|
15
|
+
# A paragraph occupies one line. None of the 99 summary paragraphs in those three projects is wrapped, and async-rest's run to 544 characters.
|
|
16
|
+
#
|
|
17
|
+
# A summary is short, and no longer than the code it describes earns. Across 76 documented methods the median summary is 4 to 9 words whatever the length of the method, so the allowance starts at ten words and grows by three per line of body, to a ceiling of thirty.
|
|
18
|
+
#
|
|
19
|
+
# A module's summary is ten words or fewer. The only commented module in async-rest is exactly ten.
|
|
20
|
+
#
|
|
21
|
+
# A documented method says what it takes and returns. A summary with no annotations describes the method in prose and leaves the signature undocumented, which is what annotations are for. A method with no comment at all is not this cop's business: that is documentation coverage, which decode reports on separately.
|
|
22
|
+
#
|
|
23
|
+
# Nothing here is corrected automatically. A comment which breaks these rules has to be rewritten rather than reflowed, and a correction which only moved the words around would hide that.
|
|
24
|
+
#
|
|
25
|
+
# Each check is configured on its own, because they are not all worth applying everywhere:
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# Local/CommentStructure:
|
|
29
|
+
# Annotations:
|
|
30
|
+
# Include:
|
|
31
|
+
# - "lib/**/*.rb"
|
|
32
|
+
# Length:
|
|
33
|
+
# Exclude:
|
|
34
|
+
# - "bin/**/*"
|
|
35
|
+
# Wrapping:
|
|
36
|
+
# Enabled: true
|
|
37
|
+
#
|
|
38
|
+
# A check with no `Include` runs everywhere; given one, it runs only where a path matches. `Exclude` then removes paths from whatever is left. `Annotations` defaults to `lib` alone: a script in `bin` and a case in `test` have no interface for anyone else to call, so annotating them documents nothing.
|
|
39
|
+
#
|
|
40
|
+
# What a declaration says is read from the run of comment lines touching it. Where an offence is reported is read from the wider span the library attributes to the declaration, less any magic comment, because decode reports a part's position within that span rather than the line it came from.
|
|
41
|
+
|
|
42
|
+
require "decode/language/ruby/generic"
|
|
43
|
+
|
|
44
|
+
require_relative "support/prism_source"
|
|
45
|
+
|
|
46
|
+
module RuboCop
|
|
47
|
+
module Cop
|
|
48
|
+
module Local
|
|
49
|
+
class CommentStructure < Base
|
|
50
|
+
include PrismSource
|
|
51
|
+
|
|
52
|
+
MSG_WRAPPED_SUMMARY = "Do not wrap a summary paragraph: a paragraph is one line, however long."
|
|
53
|
+
|
|
54
|
+
MSG_WRAPPED_ANNOTATION = "Do not wrap the summary of `@%<directive>s`: an annotation's summary is one line."
|
|
55
|
+
|
|
56
|
+
MSG_TOO_LONG = "This summary is %<words>d words; %<allowed>d is the most %<subject>s may have."
|
|
57
|
+
|
|
58
|
+
MSG_NO_ANNOTATIONS = "This method is described in prose but its parameters and return value are not: document them with annotations, or say nothing."
|
|
59
|
+
|
|
60
|
+
TAGS = Decode::Language::Ruby::Generic::TAGS
|
|
61
|
+
|
|
62
|
+
# Each check, and the paths it applies to when the configuration names none.
|
|
63
|
+
CHECKS = {
|
|
64
|
+
"Wrapping" => nil,
|
|
65
|
+
"Length" => nil,
|
|
66
|
+
"Annotations" => ["lib/**/*.rb"].freeze,
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
GLOB = File::FNM_PATHNAME | File::FNM_EXTGLOB
|
|
70
|
+
|
|
71
|
+
HEADER = /\A#\s*(frozen_string_literal:|Released under|Copyright,|!)/
|
|
72
|
+
|
|
73
|
+
# What a Ruby file says to the interpreter rather than to a reader.
|
|
74
|
+
MAGIC = /\A#\s*(frozen_string_literal:|encoding:|coding:|warn_indent:|shareable_constant_value:|!|-\*-)/
|
|
75
|
+
|
|
76
|
+
TERMINAL = [".", ":", "?", "!"].freeze
|
|
77
|
+
|
|
78
|
+
# Multi-line content is the point of these, so their summaries are left alone.
|
|
79
|
+
FREEFORM = [Decode::Comment::Example].freeze
|
|
80
|
+
|
|
81
|
+
def on_new_investigation
|
|
82
|
+
if self.applies?("Wrapping")
|
|
83
|
+
self.blocks.each do |block|
|
|
84
|
+
self.check_wrapping(block)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if self.applies?("Length")
|
|
89
|
+
self.prism_each(::Prism::ModuleNode).each do |node|
|
|
90
|
+
self.check_summary(node, "a module summary", self.cop_config.fetch("ModuleWords", 10))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
self.prism_each(::Prism::ClassNode).each do |node|
|
|
94
|
+
self.check_summary(node, "a class summary", self.cop_config.fetch("MaximumWords", 30))
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
self.prism_each(::Prism::DefNode).each do |node|
|
|
99
|
+
self.check_method(node)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Whether a check applies to the file being inspected.
|
|
106
|
+
def applies?(check)
|
|
107
|
+
configuration = self.cop_config[check] || {}
|
|
108
|
+
|
|
109
|
+
if configuration["Enabled"] == false
|
|
110
|
+
false
|
|
111
|
+
else
|
|
112
|
+
path = ::RuboCop::PathUtil.relative_path(processed_source.file_path.to_s)
|
|
113
|
+
|
|
114
|
+
included = configuration["Include"] || CHECKS.fetch(check)
|
|
115
|
+
excluded = configuration["Exclude"] || []
|
|
116
|
+
|
|
117
|
+
self.matches?(path, included, true) and !self.matches?(path, excluded, false)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @parameter default [Boolean] what an absent or empty list of patterns means.
|
|
122
|
+
def matches?(path, patterns, default)
|
|
123
|
+
if patterns.nil? or patterns.empty?
|
|
124
|
+
default
|
|
125
|
+
else
|
|
126
|
+
patterns.any?{|pattern| File.fnmatch?(pattern, path, GLOB)}
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def check_method(node)
|
|
131
|
+
lines = node.location.end_line - node.location.start_line + 1
|
|
132
|
+
base = self.cop_config.fetch("BaseWords", 10)
|
|
133
|
+
per_line = self.cop_config.fetch("WordsPerLine", 3)
|
|
134
|
+
ceiling = self.cop_config.fetch("MaximumWords", 30)
|
|
135
|
+
|
|
136
|
+
allowed = [base + (per_line * lines), ceiling].min
|
|
137
|
+
|
|
138
|
+
if self.applies?("Length")
|
|
139
|
+
self.check_summary(node, "a summary of #{lines} #{lines == 1 ? "line" : "lines"} of code", allowed)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if self.applies?("Annotations")
|
|
143
|
+
self.check_annotations(node)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The comment which documents a declaration, as the parts decode reads it as.
|
|
148
|
+
def parts_of(node)
|
|
149
|
+
comments = self.documenting(node)
|
|
150
|
+
|
|
151
|
+
if comments.empty?
|
|
152
|
+
nil
|
|
153
|
+
else
|
|
154
|
+
self.parse(comments)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# The comment above a declaration, without the file's own header.
|
|
159
|
+
def documenting(node)
|
|
160
|
+
self.prism_leading_comments(node).reject{|comment| self.prism_source(comment).match?(HEADER)}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Every comment the declaration owns, which is what the parts are indexed against.
|
|
164
|
+
def owned(node)
|
|
165
|
+
self.prism_owned_comments(node).drop_while{|comment| self.prism_source(comment).match?(MAGIC)}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def check_summary(node, subject, allowed)
|
|
169
|
+
parts = self.parts_of(node)
|
|
170
|
+
|
|
171
|
+
if parts
|
|
172
|
+
summary = parts.map(&:first).grep(Decode::Comment::Text)
|
|
173
|
+
words = summary.sum{|text| text.line.split.size}
|
|
174
|
+
|
|
175
|
+
if words > allowed
|
|
176
|
+
message = format(MSG_TOO_LONG, words: words, allowed: allowed, subject: subject)
|
|
177
|
+
|
|
178
|
+
add_offense(self.summary_range(node, parts, summary.first), message: message)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def check_annotations(node)
|
|
184
|
+
parts = self.parts_of(node)
|
|
185
|
+
|
|
186
|
+
if parts and parts.any?{|part, _| part.is_a?(Decode::Comment::Text)}
|
|
187
|
+
annotations = parts.map(&:first).reject{|part| part.is_a?(Decode::Comment::Text)}
|
|
188
|
+
|
|
189
|
+
if annotations.empty?
|
|
190
|
+
add_offense(self.prism_range(node.name_loc), message: MSG_NO_ANNOTATIONS)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Consecutive comment lines: a block is what documents one thing, so a wrap can only happen inside one.
|
|
196
|
+
def blocks
|
|
197
|
+
[].tap do |found|
|
|
198
|
+
current = []
|
|
199
|
+
|
|
200
|
+
self.prism_comments.each do |comment|
|
|
201
|
+
text = self.prism_source(comment)
|
|
202
|
+
|
|
203
|
+
if text.match?(HEADER) or text.start_with?("# rubocop:")
|
|
204
|
+
next
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
if current.empty? or comment.location.start_line == current.last.location.start_line + 1
|
|
208
|
+
current << comment
|
|
209
|
+
else
|
|
210
|
+
found << current
|
|
211
|
+
current = [comment]
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
unless current.empty?
|
|
216
|
+
found << current
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def check_wrapping(block)
|
|
222
|
+
nodes = self.parse(block)
|
|
223
|
+
|
|
224
|
+
nodes.each_with_index do |(node, lines), index|
|
|
225
|
+
if node.is_a?(Decode::Comment::Text)
|
|
226
|
+
following = nodes[index + 1]&.first
|
|
227
|
+
|
|
228
|
+
if following.is_a?(Decode::Comment::Text) and !self.terminated?(node.line)
|
|
229
|
+
add_offense(self.prism_range(block[lines.first.first].location), message: MSG_WRAPPED_SUMMARY)
|
|
230
|
+
end
|
|
231
|
+
elsif !FREEFORM.include?(node.class)
|
|
232
|
+
summary = lines.drop(1).reject{|line| line.last.strip.start_with?("@")}
|
|
233
|
+
|
|
234
|
+
if summary.size > 1
|
|
235
|
+
message = format(MSG_WRAPPED_ANNOTATION, directive: node.directive)
|
|
236
|
+
|
|
237
|
+
add_offense(self.prism_range(block[summary.first.first].location), message: message)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# decode consumes the lines it parses and does not report where they came from, so they are handed over through something which records what it gave away.
|
|
244
|
+
def parse(block)
|
|
245
|
+
source = Consumable.new(block.each_with_index.map{|comment, index| [index, self.text_of(comment)]})
|
|
246
|
+
nodes = []
|
|
247
|
+
cursor = 0
|
|
248
|
+
|
|
249
|
+
TAGS.parse(source) do |node|
|
|
250
|
+
if node
|
|
251
|
+
nodes << [node, source.log[cursor..]]
|
|
252
|
+
cursor = source.log.size
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
nodes
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# The comment without its `#`, keeping the indentation which tells decode what is nested.
|
|
260
|
+
def text_of(comment)
|
|
261
|
+
self.prism_source(comment).sub(/\A#\s?/, "")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Where to report a summary which is too long: the comment line it starts on, or the declaration itself when the summary cannot be traced back to one.
|
|
265
|
+
def summary_range(node, parts, text)
|
|
266
|
+
comments = self.owned(node)
|
|
267
|
+
entry = text && parts.find{|part, _| part.equal?(text)}
|
|
268
|
+
|
|
269
|
+
if entry and comments[entry.last.first.first]
|
|
270
|
+
self.prism_range(comments[entry.last.first.first].location)
|
|
271
|
+
else
|
|
272
|
+
self.prism_node_range(node)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def terminated?(line)
|
|
277
|
+
text = line.strip
|
|
278
|
+
|
|
279
|
+
text.empty? or TERMINAL.any?{|mark| text.end_with?(mark)}
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# What decode shifts lines off, which remembers them.
|
|
283
|
+
class Consumable
|
|
284
|
+
def initialize(entries)
|
|
285
|
+
@entries = entries
|
|
286
|
+
@log = []
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
attr :log
|
|
290
|
+
|
|
291
|
+
def first = @entries.first&.last
|
|
292
|
+
|
|
293
|
+
def shift
|
|
294
|
+
entry = @entries.shift
|
|
295
|
+
|
|
296
|
+
if entry
|
|
297
|
+
@log << entry
|
|
298
|
+
entry.last
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def empty? = @entries.empty?
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bans a constant that does not match its file's path.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# lib/mailers/welcome.rb -> Mailers::Welcome # YES
|
|
7
|
+
# lib/mailers/welcome.rb -> WelcomeMailer # NO
|
|
8
|
+
# lib/models/11_organization.rb -> Models::Organization # YES
|
|
9
|
+
# lib/jobs/sync_tenant_claims.rb -> Jobs::SyncTenantClaims # YES
|
|
10
|
+
# lib/jobs/sync_tenant_claims.rb -> SyncTenantClaimsJob # NO
|
|
11
|
+
module RuboCop
|
|
12
|
+
module Cop
|
|
13
|
+
module Local
|
|
14
|
+
class ConstantMatchesPath < Base
|
|
15
|
+
MSG = "`%<actual>s` does not match its path; this file should define " \
|
|
16
|
+
"`%<expected>s` (or a constant nested inside it)."
|
|
17
|
+
|
|
18
|
+
MSG_EMPTY = "This file defines no namespaced constant; it should define " \
|
|
19
|
+
"`%<expected>s`."
|
|
20
|
+
|
|
21
|
+
# Directories that are a load path root, not a namespace segment.
|
|
22
|
+
def roots = Array(cop_config.fetch("Roots", ["lib"]))
|
|
23
|
+
|
|
24
|
+
def inflections = cop_config.fetch("Inflections", {})
|
|
25
|
+
|
|
26
|
+
# Paths that legitimately define something else, such as monkey patches.
|
|
27
|
+
def ignored = Array(cop_config.fetch("IgnorePaths", []))
|
|
28
|
+
|
|
29
|
+
def on_new_investigation
|
|
30
|
+
if processed_source.blank?
|
|
31
|
+
nil
|
|
32
|
+
else
|
|
33
|
+
expected_namespace(processed_source.file_path).then do |expected|
|
|
34
|
+
# A file outside a configured root, or an ignored path, has no namespace to be wrong about. Nor does one that defines no constant at all -- an empty file, or one that only runs code.
|
|
35
|
+
unless expected.nil?
|
|
36
|
+
report(top_level_constants(processed_source.ast), expected)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def report(defined, expected)
|
|
45
|
+
defined.each do |name, node|
|
|
46
|
+
unless matches?(name, expected)
|
|
47
|
+
add_offense(node, message: format(MSG, actual: name, expected: expected))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# `Mailers::Welcome` is right for lib/mailers/welcome.rb, and so is `Mailers` alone (a file may open its own namespace to add to it).
|
|
53
|
+
def matches?(actual, expected)
|
|
54
|
+
actual == expected ||
|
|
55
|
+
expected.start_with?("#{actual}::") ||
|
|
56
|
+
actual.start_with?("#{expected}::")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def expected_namespace(path)
|
|
60
|
+
parts = path.to_s.split(File::SEPARATOR)
|
|
61
|
+
root = parts.rindex { |part| roots.include?(part) }
|
|
62
|
+
segments = root && parts[(root + 1)..]
|
|
63
|
+
|
|
64
|
+
if segments.nil? || segments.empty? || ignored?(segments)
|
|
65
|
+
nil
|
|
66
|
+
else
|
|
67
|
+
named = segments.dup
|
|
68
|
+
named[-1] = named[-1].sub(/\.rb\z/, "")
|
|
69
|
+
named.map { |segment| camelize(segment) }.join("::")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def ignored?(segments)
|
|
74
|
+
ignored.any? do |glob|
|
|
75
|
+
File.fnmatch?(glob, segments.join("/"), File::FNM_PATHNAME)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# `11_organization` -> `Organization`. The prefix orders loading; it is not part of the name.
|
|
80
|
+
def camelize(segment)
|
|
81
|
+
name = segment.sub(/\A\d+_/, "")
|
|
82
|
+
|
|
83
|
+
if inflections.key?(name)
|
|
84
|
+
inflections[name]
|
|
85
|
+
else
|
|
86
|
+
name.split("_").map { |word| inflections[word] || word.capitalize }.join
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The constants a file opens at its top level, flattened through compact forms so `module A; class B` and `class A::B` both read as `A::B`.
|
|
91
|
+
def top_level_constants(node, prefix = nil)
|
|
92
|
+
if node.nil?
|
|
93
|
+
[]
|
|
94
|
+
elsif node.begin_type?
|
|
95
|
+
node.children.flat_map { |child| top_level_constants(child, prefix) }
|
|
96
|
+
elsif node.module_type? || node.class_type?
|
|
97
|
+
constants_in(node, prefix)
|
|
98
|
+
else
|
|
99
|
+
[]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def constants_in(node, prefix)
|
|
104
|
+
name = qualified(node.children.first)
|
|
105
|
+
|
|
106
|
+
if name.nil?
|
|
107
|
+
[]
|
|
108
|
+
else
|
|
109
|
+
if prefix
|
|
110
|
+
full = "#{prefix}::#{name}"
|
|
111
|
+
else
|
|
112
|
+
full = name
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
body = node.children[node.class_type? ? 2 : 1]
|
|
116
|
+
nested = top_level_constants(body, full)
|
|
117
|
+
|
|
118
|
+
# Report the DEEPEST constant: `module Jobs; class Foo` is an offence about `Jobs::Foo`, not about `Jobs`.
|
|
119
|
+
if nested.empty?
|
|
120
|
+
[[full, node]]
|
|
121
|
+
else
|
|
122
|
+
nested
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def qualified(const)
|
|
128
|
+
if const&.const_type?
|
|
129
|
+
namespace = const.namespace
|
|
130
|
+
|
|
131
|
+
if namespace.nil? || namespace.cbase_type?
|
|
132
|
+
const.short_name.to_s
|
|
133
|
+
else
|
|
134
|
+
inner = qualified(namespace)
|
|
135
|
+
inner && "#{inner}::#{const.short_name}"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|