keela 0.5.0 → 0.5.1
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/CHANGELOG.md +6 -0
- data/lib/keela/scanner.rb +21 -4
- data/lib/keela/strategies/partials.rb +78 -6
- data/lib/keela/strategy.rb +14 -0
- data/lib/keela/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 40ac5a23ccbc726eeea66f99e4a6d9cb109360b987daa26e54c43c6d566c448b
|
|
4
|
+
data.tar.gz: 5c6b44f161613c7dea1aae79490918ce8233572345863febcf34a9417710bf60
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69de532e979e8bb052281e2fc58f2c4c6b9039f6a157b88139badff12c4f48abc0df134c817a44f6a6b1dcad131c2fe86c7a667fd51e36d1314184d5444b620d
|
|
7
|
+
data.tar.gz: fc86b2efb01f42db61d20f2a65d029082802a09c480bc6aeda5a6739f4b13c4ebfbb5d061f98520bb0248d10d15b9f4237bd18b8a5a0925b384553915d5f6ed4
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.1] - 2026-10-06
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **The `partials` strategy is dramatically faster on large codebases, up to ~27× less CPU.** It now collects every rendered partial in a single pass over the source instead of re-scanning the whole codebase once per partial. Measured against a large reference Rails monolith, partials went from the slowest strategy to effectively free, and no longer meaningfully slows a combined `--type all,partials` run. The report is unchanged ([#96](https://github.com/kerrizor/keela/pull/96))
|
|
15
|
+
|
|
10
16
|
## [0.5.0] - 2026-09-21
|
|
11
17
|
|
|
12
18
|
### Added
|
data/lib/keela/scanner.rb
CHANGED
|
@@ -214,6 +214,17 @@ module Keela
|
|
|
214
214
|
# (e.g., I18n lazy lookup)
|
|
215
215
|
additional_used = strategy.additional_used_names(source_files)
|
|
216
216
|
|
|
217
|
+
# A strategy whose #additional_used_names enumerates every used name in one
|
|
218
|
+
# pass (e.g. partials) decides purely by Set membership. Skip building the
|
|
219
|
+
# derived source views and the per-definition #used? scan entirely: that
|
|
220
|
+
# per-definition scan is O(N x whole-codebase) and is the hot path we are
|
|
221
|
+
# avoiding here.
|
|
222
|
+
#
|
|
223
|
+
if strategy.detects_all_usage?
|
|
224
|
+
unused = definitions.reject { |definition| additional_used.include?(definition[:name]) }
|
|
225
|
+
return record_unused(unused)
|
|
226
|
+
end
|
|
227
|
+
|
|
217
228
|
# Build the source views the strategy needs here, in the parent, so that
|
|
218
229
|
# the workers Parallel forks below inherit them instead of each building
|
|
219
230
|
# its own copy of a string the size of the whole codebase.
|
|
@@ -228,10 +239,16 @@ module Keela
|
|
|
228
239
|
strategy.used?(definition[:name], source) ? [] : definition
|
|
229
240
|
end
|
|
230
241
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
242
|
+
record_unused(unused)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Collect the unused definitions into @unused_collection / @source_locations.
|
|
246
|
+
#
|
|
247
|
+
# A single logical definition can be extracted from multiple lines (e.g. the
|
|
248
|
+
# same method delegated twice, or a constant declared and referenced), so
|
|
249
|
+
# guard against listing the same name twice per file.
|
|
250
|
+
#
|
|
251
|
+
def record_unused(unused)
|
|
235
252
|
unused.each do |unused_def|
|
|
236
253
|
names = @unused_collection[unused_def[:file]]
|
|
237
254
|
next if names.include?(unused_def[:name])
|
|
@@ -55,6 +55,13 @@ module Keela
|
|
|
55
55
|
#
|
|
56
56
|
CONTROLLER_REGEX = %r{(?:ee/)?app/controllers/(.+)_controller\.rb$}.freeze
|
|
57
57
|
|
|
58
|
+
# A view directly under app/views/ with no intermediate directory, e.g.
|
|
59
|
+
# app/views/index.html.erb. Its caller directory is the root (""), so a
|
|
60
|
+
# bareword render "foo" from here resolves to the root-level partial "foo".
|
|
61
|
+
# VIEW_DIR_REGEX requires an intermediate dir and so never matches these.
|
|
62
|
+
#
|
|
63
|
+
ROOT_VIEW_REGEX = %r{(?:ee/)?app/views/[^/]+\.html\.(?:erb|haml|slim)$}.freeze
|
|
64
|
+
|
|
58
65
|
# The render family we recognize: render, plus render_to_string and
|
|
59
66
|
# render_to_body, which render partials the same way (Rails/GitLab use both
|
|
60
67
|
# off-request, e.g. in mailers and background jobs). An optional opening
|
|
@@ -123,6 +130,14 @@ module Keela
|
|
|
123
130
|
true
|
|
124
131
|
end
|
|
125
132
|
|
|
133
|
+
# #additional_used_names collects every rendered partial name in a single
|
|
134
|
+
# pass, so the scanner decides by Set membership and skips the per-definition
|
|
135
|
+
# #used? scan. See the note on #additional_used_names.
|
|
136
|
+
#
|
|
137
|
+
def detects_all_usage?
|
|
138
|
+
true
|
|
139
|
+
end
|
|
140
|
+
|
|
126
141
|
# Build the comment-stripped view once in the parent so workers inherit it
|
|
127
142
|
# copy-on-write instead of each rebuilding a codebase-sized string.
|
|
128
143
|
#
|
|
@@ -136,12 +151,41 @@ module Keela
|
|
|
136
151
|
# demand if #prepare was not run for this source (defensive: #used? can be
|
|
137
152
|
# called directly in tests, or on a different source than #prepare saw).
|
|
138
153
|
#
|
|
154
|
+
# Retained as the Strategy contract and for direct callers (tests), but the
|
|
155
|
+
# scanner no longer drives detection through it: #additional_used_names now
|
|
156
|
+
# collects every used name in a single pass over the corpus, so the scanner
|
|
157
|
+
# decides via that Set instead of calling #used? per definition. See the
|
|
158
|
+
# note on #additional_used_names.
|
|
159
|
+
#
|
|
139
160
|
def used?(name, source)
|
|
140
161
|
usage_regex(name).match?(stripped_text_for(source))
|
|
141
162
|
end
|
|
142
163
|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
164
|
+
# The set of partial names used anywhere in +source_files+, collected in a
|
|
165
|
+
# SINGLE pass over the corpus.
|
|
166
|
+
#
|
|
167
|
+
# This is the whole-run detection path for partials. The scanner checks a
|
|
168
|
+
# definition against this Set and never calls #used? for a name that is in
|
|
169
|
+
# it, so extracting every used name here (not just the bareword/positional
|
|
170
|
+
# ones) turns detection from O(N_partials x codebase) per-definition regex
|
|
171
|
+
# scans into O(codebase + N_partials): we scan each file once, collect the
|
|
172
|
+
# rendered names, then membership-test each definition. This is the fix for
|
|
173
|
+
# the partials strategy being CPU-bound on large codebases.
|
|
174
|
+
#
|
|
175
|
+
# Three render forms are collected, each the single-pass twin of a branch
|
|
176
|
+
# that used to be matched per-definition:
|
|
177
|
+
#
|
|
178
|
+
# 1. Explicit slashed paths (the #usage_regex cases): render "users/form",
|
|
179
|
+
# render partial:/layout: "users/form", render_to_string/_to_body, and
|
|
180
|
+
# configured render_helpers. Captured verbatim as the logical name.
|
|
181
|
+
# 2. Positional-underscore paths: render "shared/notes/_note" names the
|
|
182
|
+
# file directly -> "shared/notes/note" (strip the basename's leading _).
|
|
183
|
+
# 3. Barewords resolved against the caller's directory: render "form" in
|
|
184
|
+
# app/views/users/* or users_controller.rb -> "users/form".
|
|
185
|
+
#
|
|
186
|
+
# All three share the comment-stripped content and the same left word-anchor
|
|
187
|
+
# and render-method rules as #usage_regex, so the collected strings are
|
|
188
|
+
# exactly what the per-name regex would have matched.
|
|
145
189
|
#
|
|
146
190
|
def additional_used_names(source_files)
|
|
147
191
|
used = Set.new
|
|
@@ -149,6 +193,14 @@ module Keela
|
|
|
149
193
|
source_files.each do |filepath, lines|
|
|
150
194
|
content = strip_erb_comments(lines.join("\n"))
|
|
151
195
|
|
|
196
|
+
# Explicit slashed paths: the single-pass twin of #usage_regex. A
|
|
197
|
+
# basename leading _ is stripped so "users/_form" and "users/form"
|
|
198
|
+
# agree with the positional-underscore form and with definition names.
|
|
199
|
+
#
|
|
200
|
+
content.scan(explicit_path_regex).each do |(path)|
|
|
201
|
+
used << path.sub(%r{(^|/)_([^/]+)$}, '\1\2')
|
|
202
|
+
end
|
|
203
|
+
|
|
152
204
|
# Positional-underscore paths are absolute logical names, so they are
|
|
153
205
|
# resolved regardless of the caller's directory. "shared/notes/_note"
|
|
154
206
|
# -> "shared/notes/note" (strip the leading _ of the basename only).
|
|
@@ -161,7 +213,10 @@ module Keela
|
|
|
161
213
|
next unless dir
|
|
162
214
|
|
|
163
215
|
content.scan(BAREWORD_RENDER_REGEX).each do |(bareword)|
|
|
164
|
-
|
|
216
|
+
# dir is "" for a root-level view, so join without a separator to
|
|
217
|
+
# resolve render "foo" -> "foo" rather than "/foo".
|
|
218
|
+
#
|
|
219
|
+
used << (dir.empty? ? bareword : "#{dir}/#{bareword}")
|
|
165
220
|
end
|
|
166
221
|
end
|
|
167
222
|
|
|
@@ -193,17 +248,34 @@ module Keela
|
|
|
193
248
|
text.gsub(/<%\s*-?\s*#.*?-?\s*%>/) { |match| " " * match.length }
|
|
194
249
|
end
|
|
195
250
|
|
|
196
|
-
# The directory a bareword render resolves against: the view's own dir,
|
|
197
|
-
#
|
|
198
|
-
#
|
|
251
|
+
# The directory a bareword render resolves against: the view's own dir, the
|
|
252
|
+
# controller-derived dir, or "" for a root-level view (render "foo" from
|
|
253
|
+
# app/views/index.html.erb resolves to the root partial "foo"). Returns nil
|
|
254
|
+
# for files that render nothing resolvable this way.
|
|
199
255
|
#
|
|
200
256
|
def caller_directory(filepath)
|
|
201
257
|
return Regexp.last_match(1) if filepath =~ VIEW_DIR_REGEX
|
|
202
258
|
return Regexp.last_match(1) if filepath =~ CONTROLLER_REGEX
|
|
259
|
+
return "" if filepath =~ ROOT_VIEW_REGEX
|
|
203
260
|
|
|
204
261
|
nil
|
|
205
262
|
end
|
|
206
263
|
|
|
264
|
+
# The single-pass twin of #usage_regex: instead of matching one known name,
|
|
265
|
+
# CAPTURE the rendered path so one scan collects every explicitly-rendered
|
|
266
|
+
# partial. Same left word-anchor, same optional partial:/layout: keyword,
|
|
267
|
+
# same #render_method_pattern (so configured render_helpers are honored),
|
|
268
|
+
# as #usage_regex.
|
|
269
|
+
#
|
|
270
|
+
# The captured path must contain at least one slash: barewords (no slash)
|
|
271
|
+
# are resolved relative to the caller by #additional_used_names via
|
|
272
|
+
# BAREWORD_RENDER_REGEX, exactly as #usage_regex only ever matched slashed
|
|
273
|
+
# logical names. Requiring a slash here keeps the two paths disjoint.
|
|
274
|
+
#
|
|
275
|
+
def explicit_path_regex
|
|
276
|
+
%r{(?<!\w)#{render_method_pattern}(?:(?:partial|layout):\s*)?["']([^"'/]+(?:/[^"'/]+)+)["']}
|
|
277
|
+
end
|
|
278
|
+
|
|
207
279
|
# A positional string whose basename starts with _ names a partial file
|
|
208
280
|
# directly: render_to_string("shared/notes/_note") uses shared/notes/note.
|
|
209
281
|
# There is no partial:/layout: keyword here, so it is handled separately
|
data/lib/keela/strategy.rb
CHANGED
|
@@ -83,6 +83,20 @@ module Keela
|
|
|
83
83
|
Set.new
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
# Whether #additional_used_names already collects EVERY used name in a single
|
|
87
|
+
# pass, making the per-definition #used? scan redundant.
|
|
88
|
+
#
|
|
89
|
+
# Defaults to false: the scanner checks #additional_used_names first and then
|
|
90
|
+
# falls back to #used? per definition. A strategy that returns true here is
|
|
91
|
+
# declaring its #additional_used_names is complete, so the scanner skips the
|
|
92
|
+
# per-definition scan entirely. This turns O(N_definitions x codebase) regex
|
|
93
|
+
# scanning into a single O(codebase) pass plus Set membership tests, for
|
|
94
|
+
# strategies whose usage can be enumerated in one sweep (e.g. partials).
|
|
95
|
+
#
|
|
96
|
+
def detects_all_usage?
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
86
100
|
private
|
|
87
101
|
|
|
88
102
|
def configured_pattern
|
data/lib/keela/version.rb
CHANGED