rigortype 0.2.3 → 0.2.5
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/docs/manual/plugins/rigor-rails-i18n.md +22 -3
- data/lib/rigor/analysis/diagnostic.rb +3 -0
- data/lib/rigor/analysis/incremental_session.rb +7 -2
- data/lib/rigor/cli/skill_describe.rb +8 -3
- data/lib/rigor/environment/rbs_loader.rb +54 -9
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +52 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +123 -8
- metadata +23 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce4071258582638c452079484534e58c6d6b290a88ea51fdce715024d7c09297
|
|
4
|
+
data.tar.gz: 90197cb4711899857c8d242470b063e7da64947e46e240253206179a0e8f6baf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e967fdc0ca679a712860a20da09842c2668727c0068a4ed9657ae4111d0d94a5ab8efa9684c1a75b1ad25dbeb03efd9a4580f85e771aa5a61a05f56f38b6203
|
|
7
|
+
data.tar.gz: 8773e6364390cb36296345c4ec8974cee5bec585f7f9da292950859e4cb754081a68130db00dbc14f62d77c7f396477adace7480500d789bb838777ea268c6a2
|
|
@@ -43,9 +43,14 @@ errors_demo.rb:25:1: warning: `t('errors.messages.blank')` is missing from local
|
|
|
43
43
|
with a literal first argument. **Lazy keys** — `t('.title')` in a
|
|
44
44
|
controller — are expanded to `<controller_scope>.<action>.<key>`
|
|
45
45
|
from the file path and the innermost enclosing `def`, matching
|
|
46
|
-
Rails' convention; lazy keys in non-controller files
|
|
47
|
-
(the scope can't be determined
|
|
48
|
-
|
|
46
|
+
Rails' convention; lazy keys in non-controller Ruby files (models,
|
|
47
|
+
helpers, mailers) are skipped (the scope can't be determined
|
|
48
|
+
statically at that point). **View template lazy keys** —
|
|
49
|
+
`t('.title')` inside `app/views/setting/index.html.erb` expands
|
|
50
|
+
to `setting.index.title` and is validated for key existence and
|
|
51
|
+
per-locale coverage; ERB, Haml, and Slim templates under
|
|
52
|
+
`view_search_paths` (default `app/views`) are scanned. Calls with
|
|
53
|
+
a non-literal key (`t(some_variable)`) pass through unchecked.
|
|
49
54
|
|
|
50
55
|
Keys under the prefixes Rails and the `rails-i18n` gem ship
|
|
51
56
|
themselves (`date.` / `time.` / `datetime.` / `number.` /
|
|
@@ -62,11 +67,14 @@ plugins:
|
|
|
62
67
|
config:
|
|
63
68
|
locale_search_paths: ["config/locales"] # default
|
|
64
69
|
configured_locales: ["en"] # default
|
|
70
|
+
view_search_paths: ["app/views"] # default
|
|
65
71
|
```
|
|
66
72
|
|
|
67
73
|
`configured_locales` is the set of locales the project ships;
|
|
68
74
|
setting it to `["en", "ja"]` turns on `missing-locale` warnings
|
|
69
75
|
whenever a key resolves in one but not the other.
|
|
76
|
+
`view_search_paths` controls which directories are scanned for
|
|
77
|
+
view templates containing lazy `t('.key')` calls.
|
|
70
78
|
|
|
71
79
|
## Limitations
|
|
72
80
|
|
|
@@ -74,6 +82,17 @@ whenever a key resolves in one but not the other.
|
|
|
74
82
|
- **Lazy keys outside controllers are skipped** — the
|
|
75
83
|
controller/action scope `t('.x')` depends on isn't derivable in
|
|
76
84
|
a model / helper / mailer.
|
|
85
|
+
- **View template lazy keys** (`t('.key')` inside ERB / Haml /
|
|
86
|
+
Slim) are validated for key existence and per-locale coverage.
|
|
87
|
+
Interpolation validation is skipped for templates — the hash
|
|
88
|
+
may come from controller instance variables not visible in the
|
|
89
|
+
template source. Configure `view_search_paths:` to override the
|
|
90
|
+
default `["app/views"]`.
|
|
91
|
+
- **View diagnostics duplicate under `--workers`** — the view
|
|
92
|
+
scan is a project-wide pass surfaced through the per-file
|
|
93
|
+
diagnostic hook, so each fork-pool worker re-emits the full set
|
|
94
|
+
(the same once-per-run limitation the `load-error` diagnostics
|
|
95
|
+
carry). Default `rigor check` (sequential) is unaffected.
|
|
77
96
|
- **Pluralization is recognised but not validated** — `count:` is
|
|
78
97
|
treated as a reserved option; whether the locale defines
|
|
79
98
|
`:zero` / `:one` / `:other` is not checked.
|
|
@@ -47,6 +47,9 @@ module Rigor
|
|
|
47
47
|
def initialize(path:, line:, column:, message:, severity: :error, rule: nil, # rubocop:disable Metrics/ParameterLists
|
|
48
48
|
source_family: DEFAULT_SOURCE_FAMILY,
|
|
49
49
|
receiver_type: nil, method_name: nil, project_definition_site: nil)
|
|
50
|
+
raise ArgumentError, "line must be >= 1, got #{line}" if line < 1
|
|
51
|
+
raise ArgumentError, "column must be >= 1, got #{column}" if column < 1
|
|
52
|
+
|
|
50
53
|
@path = path
|
|
51
54
|
@line = line
|
|
52
55
|
@column = column
|
|
@@ -35,9 +35,14 @@ module Rigor
|
|
|
35
35
|
|
|
36
36
|
# @param paths [Array<String>, nil] explicit analysis roots; nil
|
|
37
37
|
# (the default) uses the configuration's `paths:`.
|
|
38
|
-
|
|
38
|
+
# @param environment [Rigor::Environment, nil] optional shared
|
|
39
|
+
# environment to thread into each internal Runner. Long-lived
|
|
40
|
+
# callers and specs can use this to avoid rebuilding the same
|
|
41
|
+
# RBS universe for every baseline / recheck / oracle run.
|
|
42
|
+
def initialize(configuration:, paths: nil, environment: nil)
|
|
39
43
|
@configuration = configuration
|
|
40
44
|
@paths = paths
|
|
45
|
+
@environment = environment
|
|
41
46
|
@cache = {} # analyzed path => [Diagnostic]
|
|
42
47
|
@sources = {} # analyzed path => Set<source path it read from>
|
|
43
48
|
@digests = {} # analyzed path => content digest at last analysis
|
|
@@ -312,7 +317,7 @@ module Rigor
|
|
|
312
317
|
end
|
|
313
318
|
|
|
314
319
|
def build_runner(**)
|
|
315
|
-
Runner.new(configuration: @configuration, cache_store: nil, **)
|
|
320
|
+
Runner.new(configuration: @configuration, cache_store: nil, environment: @environment, **)
|
|
316
321
|
end
|
|
317
322
|
|
|
318
323
|
# Run the runner over the session's explicit paths (or, when none were
|
|
@@ -208,8 +208,14 @@ module Rigor
|
|
|
208
208
|
["rigor-rbs-setup", "your gems ship no community RBS yet — install it so Rigor stops typing them as Dynamic."]
|
|
209
209
|
elsif state.fetch(:ci) != :wired
|
|
210
210
|
["rigor-ci-setup", "Rigor is configured but not wired into CI — lock in the regression guard."]
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
# A present baseline is deliberately NOT a recommendation trigger.
|
|
212
|
+
# A baseline is a healthy, finished onboarding state, not a problem to
|
|
213
|
+
# work off; pushing every baselined project to "reduce it" turns a
|
|
214
|
+
# working build into a chore and tempts scattering `# rigor:disable`
|
|
215
|
+
# through the code to make a number go down — means over ends.
|
|
216
|
+
# `rigor-baseline-reduce` stays in the catalogue for the intermediate
|
|
217
|
+
# user who *chooses* to invest in it; the headline routes to genuinely
|
|
218
|
+
# additive steps instead.
|
|
213
219
|
elsif state.fetch(:editor) == :unwired
|
|
214
220
|
["rigor-editor-setup",
|
|
215
221
|
"you have an editor config but no Rigor LSP — wire `rigor lsp` for live diagnostics and hover types."]
|
|
@@ -297,7 +303,6 @@ module Rigor
|
|
|
297
303
|
The recommendation above is from a presence-only probe — it does not run
|
|
298
304
|
`rigor check`. If you have run (or now run) `rigor check`, let its findings
|
|
299
305
|
refine the choice:
|
|
300
|
-
- errors present and no baseline yet → rigor-baseline-reduce
|
|
301
306
|
- a `call.unresolved-toplevel` / `call.undefined-method` cluster on the
|
|
302
307
|
project's own monkey-patches → rigor-monkeypatch-resolve
|
|
303
308
|
- framework calls (ActiveRecord, routes, i18n …) typing as Dynamic with no
|
|
@@ -170,7 +170,7 @@ module Rigor
|
|
|
170
170
|
source = missing.map { |name| "module #{name}\nend\n" }.join
|
|
171
171
|
buffer = ::RBS::Buffer.new(name: SYNTHETIC_NAMESPACE_BUFFER, content: source)
|
|
172
172
|
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
173
|
-
env
|
|
173
|
+
add_parsed_decls(env, buffer, directives, decls)
|
|
174
174
|
rescue ::RBS::BaseError
|
|
175
175
|
# Fail-soft: synthesis is an opportunistic uplift, never a
|
|
176
176
|
# hard requirement. A parse failure here just leaves the env
|
|
@@ -232,11 +232,52 @@ module Rigor
|
|
|
232
232
|
missing.uniq
|
|
233
233
|
end
|
|
234
234
|
|
|
235
|
+
# Normalises a `class_decls` entry's representative declaration
|
|
236
|
+
# across the gemspec's supported RBS range (`rbs >= 3.0, < 5.0`).
|
|
237
|
+
# RBS 4.x exposes it as `entry.primary_decl` (the AST declaration
|
|
238
|
+
# directly); RBS 3.x exposes `entry.primary` (a wrapper whose
|
|
239
|
+
# `#decl` is the AST declaration). Returns the AST declaration, or
|
|
240
|
+
# nil when neither accessor is present. Without this guard,
|
|
241
|
+
# `class_decl_paths` crashed under RBS 3.x with
|
|
242
|
+
# `undefined method 'primary_decl'`.
|
|
243
|
+
def primary_decl_for(entry)
|
|
244
|
+
if entry.respond_to?(:primary_decl)
|
|
245
|
+
entry.primary_decl
|
|
246
|
+
elsif entry.respond_to?(:primary)
|
|
247
|
+
primary = entry.primary
|
|
248
|
+
primary.respond_to?(:decl) ? primary.decl : primary
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Appends freshly-parsed declarations to an `RBS::Environment`
|
|
253
|
+
# across the gemspec's supported RBS range (`rbs >= 3.0, < 5.0`).
|
|
254
|
+
# RBS 4.x wraps the declarations in an `RBS::Source::RBS` and
|
|
255
|
+
# takes them through `env.add_source`; RBS 3.x has neither
|
|
256
|
+
# `RBS::Source` nor `add_source` and instead registers them with
|
|
257
|
+
# `env.add_signature(buffer:, directives:, decls:)` (a bare
|
|
258
|
+
# `env << decl` is NOT enough — it skips the `signatures` table
|
|
259
|
+
# that `resolve_type_names` rebuilds from, so the synthesized
|
|
260
|
+
# declarations silently vanish on resolve). Without this guard
|
|
261
|
+
# the synthesis paths (`synthesize_missing_namespaces`,
|
|
262
|
+
# `append_stub_declarations`, `add_virtual_rbs`) crashed under
|
|
263
|
+
# RBS 3.x with `uninitialized constant RBS::Source`.
|
|
264
|
+
def add_parsed_decls(env, buffer, directives, decls)
|
|
265
|
+
decls ||= []
|
|
266
|
+
directives ||= []
|
|
267
|
+
if env.respond_to?(:add_source)
|
|
268
|
+
env.add_source(::RBS::Source::RBS.new(buffer, directives, decls))
|
|
269
|
+
elsif env.respond_to?(:add_signature)
|
|
270
|
+
env.add_signature(buffer: buffer, directives: directives, decls: decls)
|
|
271
|
+
else
|
|
272
|
+
decls.each { |decl| env << decl }
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
235
276
|
# True when a `class_decls` entry was declared in one of the
|
|
236
277
|
# project's own signature files (by declaration location), so
|
|
237
278
|
# the sweep skips the bundled stdlib / vendored universe.
|
|
238
279
|
def project_entry?(entry, project_files)
|
|
239
|
-
decl = entry
|
|
280
|
+
decl = primary_decl_for(entry)
|
|
240
281
|
location = decl&.location
|
|
241
282
|
buffer_name = location&.buffer&.name
|
|
242
283
|
return false unless buffer_name
|
|
@@ -262,7 +303,7 @@ module Rigor
|
|
|
262
303
|
end.join
|
|
263
304
|
buffer = ::RBS::Buffer.new(name: SYNTHETIC_STUB_BUFFER, content: source)
|
|
264
305
|
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
265
|
-
base_env
|
|
306
|
+
add_parsed_decls(base_env, buffer, directives, decls)
|
|
266
307
|
rescue ::RBS::BaseError
|
|
267
308
|
nil
|
|
268
309
|
end
|
|
@@ -284,8 +325,7 @@ module Rigor
|
|
|
284
325
|
|
|
285
326
|
buffer = ::RBS::Buffer.new(name: filename.to_s, content: content.to_s)
|
|
286
327
|
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
287
|
-
|
|
288
|
-
env.add_source(source)
|
|
328
|
+
add_parsed_decls(env, buffer, directives, decls)
|
|
289
329
|
rescue ::RBS::BaseError
|
|
290
330
|
# WD6 fail-soft: a single broken virtual RBS contribution
|
|
291
331
|
# does not pull the whole env down. The plugin layer
|
|
@@ -589,7 +629,7 @@ module Rigor
|
|
|
589
629
|
|
|
590
630
|
result = {}
|
|
591
631
|
env.class_decls.each do |rbs_name, entry|
|
|
592
|
-
decl = entry
|
|
632
|
+
decl = self.class.primary_decl_for(entry)
|
|
593
633
|
next if decl.nil?
|
|
594
634
|
|
|
595
635
|
location = decl.location
|
|
@@ -911,12 +951,17 @@ module Rigor
|
|
|
911
951
|
end
|
|
912
952
|
|
|
913
953
|
# Collects the AST declaration nodes behind a `class_decls`
|
|
914
|
-
# entry
|
|
915
|
-
#
|
|
916
|
-
#
|
|
954
|
+
# entry across the supported RBS range (`rbs >= 3.0, < 5.0`).
|
|
955
|
+
# RBS 4's `ModuleEntry` / `ClassEntry` expose `each_decl` yielding
|
|
956
|
+
# bare AST declarations; RBS 3.x exposes `decls`, an array of
|
|
957
|
+
# `MultiEntry::D` wrappers whose `#decl` is the AST declaration.
|
|
958
|
+
# The single-`decl` shape is handled defensively so the loader
|
|
959
|
+
# survives an rbs-gem minor bump.
|
|
917
960
|
def entry_declarations(entry)
|
|
918
961
|
if entry.respond_to?(:each_decl)
|
|
919
962
|
[].tap { |acc| entry.each_decl { |decl| acc << decl } }
|
|
963
|
+
elsif entry.respond_to?(:decls)
|
|
964
|
+
entry.decls.map { |d| d.respond_to?(:decl) ? d.decl : d }
|
|
920
965
|
elsif entry.respond_to?(:decl)
|
|
921
966
|
[entry.decl]
|
|
922
967
|
else
|
data/lib/rigor/version.rb
CHANGED
|
@@ -42,6 +42,32 @@ module Rigor
|
|
|
42
42
|
# `_controller.rb` (e.g. `users`, `admin/users`).
|
|
43
43
|
CONTROLLER_PATH_RE = %r{(?:^|/)controllers/(.+)_controller\.rb$}
|
|
44
44
|
|
|
45
|
+
# Matches Rails view-template file paths to derive the
|
|
46
|
+
# I18n "virtual path" — the scope that Rails uses for
|
|
47
|
+
# lazy `t('.key')` lookups inside a template.
|
|
48
|
+
#
|
|
49
|
+
# Captures the path segment between `views/` and the
|
|
50
|
+
# format+variant+handler suffix, then strips a leading
|
|
51
|
+
# underscore from each segment — Rails templates resolve
|
|
52
|
+
# `_form.html.erb` as "form", not "_form".
|
|
53
|
+
#
|
|
54
|
+
# app/views/setting/index.html.erb → setting.index
|
|
55
|
+
# app/views/admin/users/new.html.erb → admin.users.new
|
|
56
|
+
# app/views/home/index.html+mobile.erb → home.index
|
|
57
|
+
# app/views/users/_form.html.erb → users.form
|
|
58
|
+
#
|
|
59
|
+
# The view-scope lazy expansion replaces the action
|
|
60
|
+
# part with the view's virtual path:
|
|
61
|
+
# `<%= t('.title') %>` → `setting.index.title`
|
|
62
|
+
VIEW_SCOPE_RE = %r{views/(.+?)\.(?:\w+)(?:\+\w+)?\.\w+\z}
|
|
63
|
+
|
|
64
|
+
# Regex to extract lazy-key arguments from ERB / HTML
|
|
65
|
+
# template content. Matches `t('.key')`, `t(".key")`,
|
|
66
|
+
# `I18n.t('.key')`, and `I18n.translate('.key')` with a
|
|
67
|
+
# leading dot on the key string. Captures only the key
|
|
68
|
+
# part after the dot.
|
|
69
|
+
LAZY_T_KEY_RE = /\b(?:I18n\.)?(?:t|translate)\s*\(\s*(?:"\.([^"\\]*)"|'\.([^'\\]*)')/
|
|
70
|
+
|
|
45
71
|
# Reserved option keys — these are recognised by I18n
|
|
46
72
|
# itself and not treated as interpolation variables.
|
|
47
73
|
RESERVED_OPTION_KEYS = %i[
|
|
@@ -146,6 +172,32 @@ module Rigor
|
|
|
146
172
|
m[1].tr("/", ".")
|
|
147
173
|
end
|
|
148
174
|
|
|
175
|
+
def view_scope_from_path(path)
|
|
176
|
+
m = VIEW_SCOPE_RE.match(path.to_s)
|
|
177
|
+
return nil unless m
|
|
178
|
+
|
|
179
|
+
m[1].split("/").map { |seg| seg.sub(/\A_/, "") }.join(".")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def extract_lazy_keys_from_erb(content)
|
|
183
|
+
content.scan(LAZY_T_KEY_RE).map { |dq, sq| dq || sq }.uniq
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def validate_view_key(key, locale_index:, configured_locales:)
|
|
187
|
+
entry = locale_index.find(key)
|
|
188
|
+
if entry.nil?
|
|
189
|
+
return [] if locale_index.pluralization_namespace?(key)
|
|
190
|
+
return [] if rails_shipped_key?(key)
|
|
191
|
+
|
|
192
|
+
return [unknown_key_violation(key, locale_index)]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
violations = [translation_call_info(key, entry)]
|
|
196
|
+
missing = locale_index.missing_locales_for(key, configured_locales: configured_locales)
|
|
197
|
+
violations << missing_locale_violation(key, missing) unless missing.empty?
|
|
198
|
+
violations
|
|
199
|
+
end
|
|
200
|
+
|
|
149
201
|
# Expands a lazy key (starting with `.`) to its full
|
|
150
202
|
# dotted path using the controller scope and action name.
|
|
151
203
|
# Returns the raw key unchanged for absolute keys.
|
|
@@ -26,6 +26,7 @@ module Rigor
|
|
|
26
26
|
# config:
|
|
27
27
|
# locale_search_paths: ["config/locales"] # default; optional
|
|
28
28
|
# configured_locales: ["en"] # default; optional — locales the project ships
|
|
29
|
+
# view_search_paths: ["app/views"] # default; optional — view template search roots
|
|
29
30
|
#
|
|
30
31
|
# ## What it checks
|
|
31
32
|
#
|
|
@@ -39,6 +40,11 @@ module Rigor
|
|
|
39
40
|
# `%{var}` placeholders must match the call's keyword
|
|
40
41
|
# arguments. Missing placeholders are errors; extra
|
|
41
42
|
# arguments are warnings.
|
|
43
|
+
# 4. **View template lazy keys** — `t('.title')` inside
|
|
44
|
+
# `app/views/setting/index.html.erb` expands to
|
|
45
|
+
# `setting.index.title` and is validated against the
|
|
46
|
+
# locale index. ERB, Haml, and Slim templates are
|
|
47
|
+
# scanned under `view_search_paths`.
|
|
42
48
|
#
|
|
43
49
|
# ## Limitations
|
|
44
50
|
#
|
|
@@ -51,6 +57,16 @@ module Rigor
|
|
|
51
57
|
# Lazy keys in non-controller `.rb` files (models, helpers,
|
|
52
58
|
# mailers, …) are silently skipped — the controller/action
|
|
53
59
|
# scope cannot be statically determined there.
|
|
60
|
+
# - View template lazy keys (`t('.key')` inside ERB / Haml /
|
|
61
|
+
# Slim) are validated for key existence and per-locale
|
|
62
|
+
# coverage. Interpolation variable validation is skipped
|
|
63
|
+
# for view templates (the hash may come from controller
|
|
64
|
+
# instance variables not visible in the template).
|
|
65
|
+
# The view scan is a project-wide pass surfaced through the
|
|
66
|
+
# per-file diagnostic hook, so under `--workers` each
|
|
67
|
+
# fork-pool worker re-emits the full set (the same
|
|
68
|
+
# once-per-run limitation the `load-error` path carries);
|
|
69
|
+
# sequential `rigor check` is unaffected.
|
|
54
70
|
# - Pluralization (`t('errors.messages.too_short',
|
|
55
71
|
# count: n)`) is recognised at the call site but the
|
|
56
72
|
# `count` key is not used to validate the locale's
|
|
@@ -61,14 +77,15 @@ module Rigor
|
|
|
61
77
|
class RailsI18n < Rigor::Plugin::Base
|
|
62
78
|
manifest(
|
|
63
79
|
id: "rails-i18n",
|
|
64
|
-
# Bumped 2026-
|
|
65
|
-
#
|
|
66
|
-
# `
|
|
67
|
-
version: "0.
|
|
80
|
+
# Bumped 2026-06-23 — view template lazy-key scanning
|
|
81
|
+
# (`t('.key')` inside ERB / Haml / Slim under
|
|
82
|
+
# `view_search_paths`).
|
|
83
|
+
version: "0.3.0",
|
|
68
84
|
description: "Validates I18n `t(key)` calls against `config/locales/*.yml`.",
|
|
69
85
|
config_schema: {
|
|
70
86
|
"locale_search_paths" => { kind: :array, default: ["config/locales"] },
|
|
71
|
-
"configured_locales" => { kind: :array, default: ["en"] }
|
|
87
|
+
"configured_locales" => { kind: :array, default: ["en"] },
|
|
88
|
+
"view_search_paths" => { kind: :array, default: ["app/views"] }
|
|
72
89
|
}
|
|
73
90
|
)
|
|
74
91
|
|
|
@@ -88,22 +105,49 @@ module Rigor
|
|
|
88
105
|
index
|
|
89
106
|
end
|
|
90
107
|
|
|
108
|
+
# Scans view templates under `view_search_paths` for lazy
|
|
109
|
+
# `t('.key')` / `I18n.translate('.key')` calls and validates
|
|
110
|
+
# each expanded key against the locale index. Interpolation
|
|
111
|
+
# validation is skipped — the hash may come from controller
|
|
112
|
+
# instance variables not visible in the template source.
|
|
113
|
+
#
|
|
114
|
+
# Watches `**/*.{erb,haml,slim}` under each search root so
|
|
115
|
+
# the cache invalidates when templates are edited.
|
|
116
|
+
producer :view_diagnostics, watch: -> { [[@view_search_paths, "**/*.erb", "**/*.haml", "**/*.slim"]] } do |_params|
|
|
117
|
+
index = producer_value(:locale_index)
|
|
118
|
+
next [] if index.nil? || index.empty?
|
|
119
|
+
|
|
120
|
+
scan_view_files(index)
|
|
121
|
+
end
|
|
122
|
+
|
|
91
123
|
def init(_services)
|
|
92
124
|
@locale_search_paths = Array(config.fetch("locale_search_paths")).map(&:to_s)
|
|
125
|
+
@view_search_paths = Array(config.fetch("view_search_paths")).map(&:to_s)
|
|
93
126
|
@configured_locales = Array(config.fetch("configured_locales")).map(&:to_s)
|
|
94
127
|
@load_errors = []
|
|
95
128
|
@load_errors_emitted = false
|
|
129
|
+
@view_diagnostics_emitted = false
|
|
96
130
|
end
|
|
97
131
|
|
|
98
132
|
# File-level only: the once-per-run YAML load errors + the
|
|
99
|
-
# runtime (cache-load) error. Per-call
|
|
100
|
-
# over the engine-owned walk via the
|
|
101
|
-
#
|
|
133
|
+
# runtime (cache-load) error + the view-template scan. Per-call
|
|
134
|
+
# `t('key')` validation runs over the engine-owned walk via the
|
|
135
|
+
# node_rule below (ADR-37). The locale index and view diagnostics
|
|
136
|
+
# are lazily loaded + memoised by `producer_value`.
|
|
102
137
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
103
138
|
index = producer_value(:locale_index)
|
|
104
139
|
diagnostics = []
|
|
105
140
|
diagnostics.concat(consume_load_error_diagnostics(path)) unless @load_errors.empty?
|
|
106
141
|
diagnostics << runtime_error_diagnostic(path) if index.nil? && producer_error(:locale_index)
|
|
142
|
+
unless @view_diagnostics_emitted
|
|
143
|
+
view_diags = producer_value(:view_diagnostics) || []
|
|
144
|
+
if (view_err = producer_error(:view_diagnostics))
|
|
145
|
+
diagnostics << view_runtime_error_diagnostic(path, view_err)
|
|
146
|
+
else
|
|
147
|
+
diagnostics.concat(view_diags)
|
|
148
|
+
end
|
|
149
|
+
@view_diagnostics_emitted = true
|
|
150
|
+
end
|
|
107
151
|
diagnostics
|
|
108
152
|
end
|
|
109
153
|
|
|
@@ -126,6 +170,68 @@ module Rigor
|
|
|
126
170
|
|
|
127
171
|
private
|
|
128
172
|
|
|
173
|
+
def scan_view_files(index)
|
|
174
|
+
view_files.flat_map do |view_path|
|
|
175
|
+
scan_view_file(view_path, index)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def scan_view_file(view_path, index)
|
|
180
|
+
content = read_view_template(view_path) or return []
|
|
181
|
+
scope = Analyzer.view_scope_from_path(view_path) or return []
|
|
182
|
+
|
|
183
|
+
display_path = relative_view_path(view_path)
|
|
184
|
+
Analyzer.extract_lazy_keys_from_erb(content).flat_map do |key|
|
|
185
|
+
full_key = "#{scope}.#{key}"
|
|
186
|
+
Analyzer.validate_view_key(
|
|
187
|
+
full_key, locale_index: index, configured_locales: @configured_locales
|
|
188
|
+
).map { |v| view_diagnostic(display_path, v) }
|
|
189
|
+
end
|
|
190
|
+
rescue StandardError => e
|
|
191
|
+
[Rigor::Analysis::Diagnostic.new(
|
|
192
|
+
path: relative_view_path(view_path), line: 1, column: 1,
|
|
193
|
+
message: "rigor-rails-i18n: failed to scan view template: #{e.class}: #{e.message}",
|
|
194
|
+
severity: :warning,
|
|
195
|
+
rule: "load-error"
|
|
196
|
+
)]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def view_files
|
|
200
|
+
@view_search_paths.flat_map do |root|
|
|
201
|
+
absolute = File.expand_path(root)
|
|
202
|
+
next [] unless File.directory?(absolute)
|
|
203
|
+
|
|
204
|
+
Dir.glob(File.join(absolute, "**", "*.{erb,haml,slim}"))
|
|
205
|
+
end.sort
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Diagnostics anchor on a path relative to the working
|
|
209
|
+
# directory — the same base `File.expand_path` globbed the
|
|
210
|
+
# view files against — so view diagnostics render and match
|
|
211
|
+
# baselines like every other diagnostic (which carry
|
|
212
|
+
# project-relative paths). Falls back to the absolute path
|
|
213
|
+
# for a view outside the working tree.
|
|
214
|
+
def relative_view_path(absolute_path)
|
|
215
|
+
Pathname.new(absolute_path).relative_path_from(Pathname.pwd).to_s
|
|
216
|
+
rescue ArgumentError
|
|
217
|
+
absolute_path
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def read_view_template(path)
|
|
221
|
+
io_boundary.read_file(path)
|
|
222
|
+
rescue Plugin::AccessDeniedError
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def view_diagnostic(view_path, violation)
|
|
227
|
+
Rigor::Analysis::Diagnostic.new(
|
|
228
|
+
path: view_path, line: 1, column: 1,
|
|
229
|
+
message: violation.message,
|
|
230
|
+
severity: violation.severity,
|
|
231
|
+
rule: violation.rule
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
|
|
129
235
|
# The runner only invokes `diagnostics_for_file` for
|
|
130
236
|
# Ruby files (`paths:` is filtered to `.rb`). YAML
|
|
131
237
|
# parse errors therefore can't be anchored on the
|
|
@@ -155,6 +261,15 @@ module Rigor
|
|
|
155
261
|
rule: "load-error"
|
|
156
262
|
)
|
|
157
263
|
end
|
|
264
|
+
|
|
265
|
+
def view_runtime_error_diagnostic(path, error)
|
|
266
|
+
Rigor::Analysis::Diagnostic.new(
|
|
267
|
+
path: path, line: 1, column: 1,
|
|
268
|
+
message: "rigor-rails-i18n: failed to scan view templates: #{error.class}: #{error.message}",
|
|
269
|
+
severity: :warning,
|
|
270
|
+
rule: "load-error"
|
|
271
|
+
)
|
|
272
|
+
end
|
|
158
273
|
end
|
|
159
274
|
|
|
160
275
|
Rigor::Plugin.register(RailsI18n)
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rigortype
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rigor contributors
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 1980-01-
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: language_server-protocol
|
|
@@ -109,6 +109,26 @@ dependencies:
|
|
|
109
109
|
- - "<"
|
|
110
110
|
- !ruby/object:Gem::Version
|
|
111
111
|
version: '4.0'
|
|
112
|
+
- !ruby/object:Gem::Dependency
|
|
113
|
+
name: binpacker
|
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: 0.1.0
|
|
119
|
+
- - "<"
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '1.0'
|
|
122
|
+
type: :development
|
|
123
|
+
prerelease: false
|
|
124
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
125
|
+
requirements:
|
|
126
|
+
- - ">="
|
|
127
|
+
- !ruby/object:Gem::Version
|
|
128
|
+
version: 0.1.0
|
|
129
|
+
- - "<"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '1.0'
|
|
112
132
|
- !ruby/object:Gem::Dependency
|
|
113
133
|
name: parallel_tests
|
|
114
134
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -934,7 +954,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
934
954
|
- !ruby/object:Gem::Version
|
|
935
955
|
version: '0'
|
|
936
956
|
requirements: []
|
|
937
|
-
rubygems_version:
|
|
957
|
+
rubygems_version: 4.0.10
|
|
938
958
|
specification_version: 4
|
|
939
959
|
summary: Inference-first static analysis for Ruby.
|
|
940
960
|
test_files: []
|