rigortype 0.2.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 749e0e15c9b85dcec97c9afc1b107d5ebc1a1a685cc19aa39d316a9826992d4a
4
- data.tar.gz: 45bcee26284594e35b51a050ce8ac234935bee954063b50fdfe1b0c6a9bcddea
3
+ metadata.gz: ce4071258582638c452079484534e58c6d6b290a88ea51fdce715024d7c09297
4
+ data.tar.gz: 90197cb4711899857c8d242470b063e7da64947e46e240253206179a0e8f6baf
5
5
  SHA512:
6
- metadata.gz: fbf022b6e92a96e1c095cb3d887936161e836201662f051c60de5d0d78494f263228a28df509c3b937768735b5ce18162ee28657b2aaf1cd7a8b2d4851338a53
7
- data.tar.gz: a49e89a07950f54fcf8f38e328b8fdb658aaf0802154df457c7b9b1370f35279c1d6d5fe3acf6a45c9e49ff2f7c09ff9f209b1f7dab68d20b283c9baa4b2760b
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 are skipped
47
- (the scope can't be determined statically). Calls with a
48
- non-literal key (`t(some_variable)`) pass through unchecked.
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.
@@ -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
- def initialize(configuration:, paths: nil)
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
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.2.4"
4
+ VERSION = "0.2.5"
5
5
  end
@@ -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-05-28skip `unknown-key` on Rails / rails-
65
- # i18n shipped defaults (`date.order`, `time.am`,
66
- # `support.array.*`, `errors.format`, …).
67
- version: "0.2.0",
80
+ # Bumped 2026-06-23view 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 `t('key')` validation runs
100
- # over the engine-owned walk via the node_rule below (ADR-37). The
101
- # locale index is lazily loaded + memoised by `producer_value`.
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
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-01 00:00:00.000000000 Z
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: 3.7.2
957
+ rubygems_version: 4.0.10
938
958
  specification_version: 4
939
959
  summary: Inference-first static analysis for Ruby.
940
960
  test_files: []