rigortype 0.4.3 → 0.4.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/README.md +5 -5
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +56 -8
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +50 -35
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +16 -1
data/lib/rigor/type/constant.rb
CHANGED
|
@@ -78,7 +78,8 @@ module Rigor
|
|
|
78
78
|
# `RbsTypeTranslator#translate_literal` already maps the parsed RBS Literal back to `Constant`.
|
|
79
79
|
# Scalar carriers without RBS Literal support (Float, Range, Rational, Complex, Regexp, Pathname)
|
|
80
80
|
# keep their pre-existing widen-to-class-name behaviour because RBS rejects their literal spellings
|
|
81
|
-
# as syntax errors.
|
|
81
|
+
# as syntax errors. `Range` is generic (`Range[out E]`), and a bare `Range` fails `rbs validate`, so
|
|
82
|
+
# it carries the class of its endpoints, or `untyped` when both are nil (#1891).
|
|
82
83
|
def erase_to_rbs
|
|
83
84
|
case value
|
|
84
85
|
when true then "true"
|
|
@@ -86,6 +87,7 @@ module Rigor
|
|
|
86
87
|
when nil then "nil"
|
|
87
88
|
when Integer then value.to_s
|
|
88
89
|
when Symbol, String then value.inspect
|
|
90
|
+
when Range then "Range[#{range_element_rbs}]"
|
|
89
91
|
else value.class.name
|
|
90
92
|
end
|
|
91
93
|
end
|
|
@@ -106,6 +108,13 @@ module Rigor
|
|
|
106
108
|
def inspect
|
|
107
109
|
"#<Rigor::Type::Constant #{describe(:short)}>"
|
|
108
110
|
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def range_element_rbs
|
|
115
|
+
classes = [value.begin, value.end].compact.map { |endpoint| endpoint.class.name }.uniq.sort
|
|
116
|
+
classes.empty? ? "untyped" : classes.join(" | ")
|
|
117
|
+
end
|
|
109
118
|
end
|
|
110
119
|
end
|
|
111
120
|
end
|
data/lib/rigor/version.rb
CHANGED
|
@@ -99,7 +99,7 @@ module Rigor
|
|
|
99
99
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
100
100
|
next [] unless @io_boundary.directory?(absolute)
|
|
101
101
|
|
|
102
|
-
Dir.glob(File.join(absolute,
|
|
102
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
103
103
|
end
|
|
104
104
|
end
|
|
105
105
|
|
|
@@ -90,7 +90,7 @@ module Rigor
|
|
|
90
90
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
91
91
|
next [] unless @io_boundary.directory?(absolute)
|
|
92
92
|
|
|
93
|
-
Dir.glob(File.join(absolute,
|
|
93
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
|
|
@@ -58,7 +58,7 @@ module Rigor
|
|
|
58
58
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
59
59
|
next [] unless @io_boundary.directory?(absolute)
|
|
60
60
|
|
|
61
|
-
Dir.glob(File.join(absolute,
|
|
61
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -159,13 +159,18 @@ module Rigor
|
|
|
159
159
|
|
|
160
160
|
# Project-relative paths, the spelling `#template_units_for_file` is handed and the spelling the
|
|
161
161
|
# compiled cache is keyed by.
|
|
162
|
+
#
|
|
163
|
+
# #1803 — listed through the boundary's `#glob`, which records each root's membership: a helper
|
|
164
|
+
# added under `app/helpers`, or a template under a `view_search_paths:` root the template globs do
|
|
165
|
+
# not claim, is analysed by nobody, and without the row the run-result caches kept serving the
|
|
166
|
+
# index built before it appeared.
|
|
162
167
|
def self.source_files(io_boundary, roots, pattern)
|
|
163
168
|
root_prefix = "#{Dir.pwd}#{File::SEPARATOR}"
|
|
164
169
|
roots.flat_map do |root|
|
|
165
170
|
absolute = File.expand_path(root)
|
|
166
171
|
next [] unless io_boundary.directory?(absolute)
|
|
167
172
|
|
|
168
|
-
|
|
173
|
+
io_boundary.glob(absolute, "**/#{pattern}").map { |path| path.delete_prefix(root_prefix) }
|
|
169
174
|
end
|
|
170
175
|
end
|
|
171
176
|
|
|
@@ -127,7 +127,7 @@ module Rigor
|
|
|
127
127
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
128
128
|
next [] unless @io_boundary.directory?(absolute)
|
|
129
129
|
|
|
130
|
-
Dir.glob(File.join(absolute,
|
|
130
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
131
131
|
end
|
|
132
132
|
end
|
|
133
133
|
|
|
@@ -146,11 +146,12 @@ module Rigor
|
|
|
146
146
|
|
|
147
147
|
# ADR-45 WD1b (#613) — `config/application.rb` is probed at a fixed path, so its ABSENCE is what
|
|
148
148
|
# shapes "no adapter" on a project that has none yet; the probe goes through the boundary to record
|
|
149
|
-
# that. The
|
|
150
|
-
# (immediately superseded) presence row.
|
|
149
|
+
# that. The `config/environments/*.rb` listing goes through `#glob` (#1803), whose row records the
|
|
150
|
+
# directory's membership; each listed entry only picks up the (immediately superseded) presence row.
|
|
151
151
|
def config_files(io_boundary, root)
|
|
152
152
|
application = File.join(root, CONFIG_PATHS.first)
|
|
153
|
-
|
|
153
|
+
environments_dir = File.join(root, CONFIG_PATHS.last)
|
|
154
|
+
environments = io_boundary.glob(environments_dir, "*.rb")
|
|
154
155
|
([application] + environments).select { |path| io_boundary.file?(path) }
|
|
155
156
|
rescue StandardError
|
|
156
157
|
[]
|
|
@@ -92,7 +92,7 @@ module Rigor
|
|
|
92
92
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
93
93
|
next [] unless @io_boundary.directory?(absolute)
|
|
94
94
|
|
|
95
|
-
Dir.glob(File.join(absolute,
|
|
95
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
|
|
@@ -375,7 +375,7 @@ module Rigor
|
|
|
375
375
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
376
376
|
next [] unless @io_boundary.directory?(absolute)
|
|
377
377
|
|
|
378
|
-
Dir.glob(File.join(absolute,
|
|
378
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
379
379
|
end
|
|
380
380
|
end
|
|
381
381
|
|
|
@@ -69,7 +69,7 @@ module Rigor
|
|
|
69
69
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
70
70
|
next [] unless @io_boundary.directory?(absolute)
|
|
71
71
|
|
|
72
|
-
Dir.glob(File.join(absolute,
|
|
72
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
@@ -65,7 +65,7 @@ module Rigor
|
|
|
65
65
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
66
66
|
next [] unless @io_boundary.directory?(absolute)
|
|
67
67
|
|
|
68
|
-
Dir.glob(File.join(absolute,
|
|
68
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
end
|
|
@@ -168,10 +168,14 @@ module Rigor
|
|
|
168
168
|
|
|
169
169
|
# ADR-45 WD1b (#613 / #630) — the classification probes go through the boundary, so an entry that
|
|
170
170
|
# is not there yet (or stops being a directory) is a recorded dependency of the scan's input set.
|
|
171
|
+
# #1803 — so is the walk: `IoBoundary#glob` records which `.rb` files the entry holds, and a file
|
|
172
|
+
# `exclude:` keeps out of analysis, which no analysed-file row covers, reaches the run-result caches
|
|
173
|
+
# when it appears or disappears. The matches are spelled under `entry`, as `Dir.glob` spelled them.
|
|
171
174
|
def scannable_paths(services)
|
|
172
175
|
@scannable_paths ||= services.configuration.paths.flat_map do |entry|
|
|
173
176
|
if io_boundary.directory?(entry)
|
|
174
|
-
|
|
177
|
+
prefix = "#{File.absolute_path(entry)}/"
|
|
178
|
+
io_boundary.glob(entry, "**/*.rb").map { |path| File.join(entry, path.delete_prefix(prefix)) }
|
|
175
179
|
elsif io_boundary.file?(entry) && entry.end_with?(".rb")
|
|
176
180
|
[entry]
|
|
177
181
|
else
|
|
@@ -109,7 +109,7 @@ module Rigor
|
|
|
109
109
|
def scannable_paths
|
|
110
110
|
@scannable_paths ||= services.configuration.paths.flat_map do |entry|
|
|
111
111
|
if io_boundary.directory?(entry)
|
|
112
|
-
Dir.glob(File.join(entry,
|
|
112
|
+
Dir.glob("**/*.rb", base: entry).map { |path| File.join(entry, path) }
|
|
113
113
|
elsif io_boundary.file?(entry) && entry.end_with?(".rb")
|
|
114
114
|
[entry]
|
|
115
115
|
else
|
|
@@ -142,10 +142,14 @@ module Rigor
|
|
|
142
142
|
|
|
143
143
|
# ADR-45 WD1b (#613 / #630) — the classification probes go through the boundary, so an entry that
|
|
144
144
|
# is not there yet (or stops being a directory) is a recorded dependency of the scan's input set.
|
|
145
|
+
# #1803 — so is the walk: `IoBoundary#glob` records which `.rb` files the entry holds, and a file
|
|
146
|
+
# `exclude:` keeps out of analysis, which no analysed-file row covers, reaches the run-result caches
|
|
147
|
+
# when it appears or disappears. The matches are spelled under `entry`, as `Dir.glob` spelled them.
|
|
145
148
|
def scannable_paths(services)
|
|
146
149
|
@scannable_paths ||= services.configuration.paths.flat_map do |entry|
|
|
147
150
|
if io_boundary.directory?(entry)
|
|
148
|
-
|
|
151
|
+
prefix = "#{File.absolute_path(entry)}/"
|
|
152
|
+
io_boundary.glob(entry, "**/*.rb").map { |path| File.join(entry, path.delete_prefix(prefix)) }
|
|
149
153
|
elsif io_boundary.file?(entry) && entry.end_with?(".rb")
|
|
150
154
|
[entry]
|
|
151
155
|
else
|
|
@@ -5,13 +5,22 @@ require "prism"
|
|
|
5
5
|
module Rigor
|
|
6
6
|
module Plugin
|
|
7
7
|
class FFI < Base
|
|
8
|
+
# Issue #1803 — the walk lists and reads through the plugin's {Rigor::Plugin::IoBoundary} (`io:`), so every
|
|
9
|
+
# `.rb` the catalog was built from is a dependency row of the `:ffi_catalog` producer's cache entry and of
|
|
10
|
+
# every run-result cache built on the run. The producer declares no `watch:`, so before this its entry
|
|
11
|
+
# carried no row at all and was served after any edit, an analysed file's included, until `--no-cache`.
|
|
8
12
|
class Discoverer
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
RUBY_GLOB = "**/*.rb"
|
|
14
|
+
RUBY_FILE = "*.rb"
|
|
15
|
+
SKIPPED_DIRECTORIES = %w[vendor .git].freeze
|
|
16
|
+
|
|
17
|
+
def self.discover(root:, io:)
|
|
18
|
+
new(root: root, io: io).discover
|
|
11
19
|
end
|
|
12
20
|
|
|
13
|
-
def initialize(root:)
|
|
21
|
+
def initialize(root:, io:)
|
|
14
22
|
@root = root
|
|
23
|
+
@io = io
|
|
15
24
|
end
|
|
16
25
|
|
|
17
26
|
def discover
|
|
@@ -24,7 +33,7 @@ module Rigor
|
|
|
24
33
|
callbacks = {}
|
|
25
34
|
|
|
26
35
|
ruby_files.each do |file_path|
|
|
27
|
-
parse_result =
|
|
36
|
+
parse_result = parse(file_path)
|
|
28
37
|
next unless parse_result.value
|
|
29
38
|
|
|
30
39
|
walk_tree(parse_result.value, nil) do |node, mod_name|
|
|
@@ -60,10 +69,52 @@ module Rigor
|
|
|
60
69
|
|
|
61
70
|
private
|
|
62
71
|
|
|
72
|
+
# #1959 — one `:kinds` listing row over the root's top-level entries, then one `:names` `**/*.rb` row per
|
|
73
|
+
# real directory among them other than `vendor/`. A single `**/*.rb` row over the root re-walked an in-tree
|
|
74
|
+
# `vendor/bundle` on every warm validation and read stale on every `bundle install`. The listing carries
|
|
75
|
+
# each entry's kind and not just its name: the walk descends a directory but not a symlinked one (`**` never
|
|
76
|
+
# followed it), so a file `gen` that becomes a directory `gen/`, or a symlink that becomes a real
|
|
77
|
+
# directory, changes what it reads while every name stays put.
|
|
78
|
+
#
|
|
79
|
+
# The answer is `Dir.glob("**/*.rb")` minus the skipped paths and minus every match that is not a regular
|
|
80
|
+
# file (#2113), in its order: entry by entry, a top-level name matching `*.rb`, then that directory's
|
|
81
|
+
# matches. A directory so named is walked, never read. The boundary answers absolute paths; they are spelled
|
|
82
|
+
# back under `@root` as `Dir.glob` spells them.
|
|
63
83
|
def ruby_files
|
|
64
|
-
return [] unless @root &&
|
|
84
|
+
return [] unless @root && @io.directory?(@root.to_s)
|
|
85
|
+
|
|
86
|
+
root = @root.to_s
|
|
87
|
+
prefix = "#{File.absolute_path(root)}/"
|
|
88
|
+
@io.list_directory_kinds(root).flat_map do |path, kind|
|
|
89
|
+
name = path.delete_prefix(prefix)
|
|
90
|
+
own = File.fnmatch?(RUBY_FILE, name) && readable_file?(path, kind) ? [File.join(root, name)] : []
|
|
91
|
+
next own unless kind == :directory && !SKIPPED_DIRECTORIES.include?(name)
|
|
92
|
+
|
|
93
|
+
own + @io.glob(path, RUBY_GLOB).filter_map do |nested|
|
|
94
|
+
relative = nested.delete_prefix(prefix)
|
|
95
|
+
File.join(root, relative) if !skipped?(relative) && @io.file?(nested)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# #2113 — `*.rb` names a directory too, and a fifo, socket or device can carry the name. Only a regular file
|
|
101
|
+
# is read: `read_file` on a directory raises `EISDIR` and aborts the whole walk, and on a fifo it blocks
|
|
102
|
+
# until a writer appears. A top-level entry is judged by its `lstat` kind, and a symlink by what it
|
|
103
|
+
# resolves to; a nested match is judged by `file?` (which follows a symlink as `File.file?` does).
|
|
104
|
+
def readable_file?(path, kind)
|
|
105
|
+
kind == :file || (kind == :symlink && @io.file?(path))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# #1956 — the skip reads the path relative to the root, a `vendor` or `.git` directory segment at any
|
|
109
|
+
# depth under it. Matching `/vendor/` in the whole path also dropped every file of a project that itself
|
|
110
|
+
# lives under some `…/vendor/…` directory, and the catalog came out empty.
|
|
111
|
+
def skipped?(relative)
|
|
112
|
+
relative.split("/")[0...-1].intersect?(SKIPPED_DIRECTORIES)
|
|
113
|
+
end
|
|
65
114
|
|
|
66
|
-
|
|
115
|
+
# The glob's rows record which files match; this read's row carries an edit.
|
|
116
|
+
def parse(file_path)
|
|
117
|
+
Prism.parse(@io.read_file(file_path))
|
|
67
118
|
end
|
|
68
119
|
|
|
69
120
|
def walk_tree(node, current_module, &)
|
|
@@ -26,7 +26,7 @@ module Rigor
|
|
|
26
26
|
def extconf_uses_ffx?(root, io = nil)
|
|
27
27
|
return false if root.nil?
|
|
28
28
|
|
|
29
|
-
extconfs = io ? io.glob(root.to_s, EXTCONF_GLOB) : Dir.glob(File.join(root.to_s,
|
|
29
|
+
extconfs = io ? io.glob(root.to_s, EXTCONF_GLOB) : Dir.glob(EXTCONF_GLOB, base: root.to_s).map { |path| File.join(root.to_s, path) }
|
|
30
30
|
extconfs.any? do |extconf|
|
|
31
31
|
read_text(extconf, io).include?("FFX.create_makefile")
|
|
32
32
|
rescue SystemCallError
|
|
@@ -151,10 +151,14 @@ module Rigor
|
|
|
151
151
|
|
|
152
152
|
# ADR-45 WD1b (#613 / #630) — the classification probes go through the boundary, so an entry that
|
|
153
153
|
# is not there yet (or stops being a directory) is a recorded dependency of the scan's input set.
|
|
154
|
+
# #1803 — so is the walk: `IoBoundary#glob` records which `.rb` files the entry holds, and a file
|
|
155
|
+
# `exclude:` keeps out of analysis, which no analysed-file row covers, reaches the run-result caches
|
|
156
|
+
# when it appears or disappears. The matches are spelled under `entry`, as `Dir.glob` spelled them.
|
|
154
157
|
def scannable_paths(services)
|
|
155
158
|
@scannable_paths ||= services.configuration.paths.flat_map do |entry|
|
|
156
159
|
if io_boundary.directory?(entry)
|
|
157
|
-
|
|
160
|
+
prefix = "#{File.absolute_path(entry)}/"
|
|
161
|
+
io_boundary.glob(entry, "**/*.rb").map { |path| File.join(entry, path.delete_prefix(prefix)) }
|
|
158
162
|
elsif io_boundary.file?(entry) && entry.end_with?(".rb")
|
|
159
163
|
[entry]
|
|
160
164
|
else
|
|
@@ -72,7 +72,7 @@ module Rigor
|
|
|
72
72
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
73
73
|
next [] unless @io_boundary.directory?(absolute)
|
|
74
74
|
|
|
75
|
-
Dir.glob(File.join(absolute,
|
|
75
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
@@ -60,7 +60,7 @@ module Rigor
|
|
|
60
60
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
61
61
|
next [] unless @io_boundary.directory?(absolute)
|
|
62
62
|
|
|
63
|
-
Dir.glob(File.join(absolute,
|
|
63
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -34,12 +34,17 @@ module Rigor
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
attr_reader :entries, :locales
|
|
37
|
+
attr_reader :entries, :locales, :load_errors
|
|
38
38
|
|
|
39
39
|
# @param locales — all locale names that contributed at least one key.
|
|
40
|
-
|
|
40
|
+
# @param load_errors — the {LocaleLoader::LoadError}s of the files that did not parse (#1954). They
|
|
41
|
+
# ride in the index, not beside it, because the index is the cached producer value: a warm run
|
|
42
|
+
# serves it without re-reading a single locale file, so an error kept anywhere else is lost exactly
|
|
43
|
+
# when the cache hits.
|
|
44
|
+
def initialize(entries, locales:, load_errors: [])
|
|
41
45
|
@entries = entries.freeze
|
|
42
46
|
@locales = locales.dup.freeze
|
|
47
|
+
@load_errors = load_errors.map(&:freeze).freeze
|
|
43
48
|
@by_key = entries.to_h { |e| [e.dotted_key, e] }.freeze
|
|
44
49
|
freeze
|
|
45
50
|
end
|
|
@@ -15,9 +15,9 @@ module Rigor
|
|
|
15
15
|
# flattened into dotted keys (`users.welcome`, `errors.messages.blank`, …). For each leaf string,
|
|
16
16
|
# `%{var}` placeholders are extracted via a simple regex.
|
|
17
17
|
#
|
|
18
|
-
# Files that fail to parse are skipped
|
|
19
|
-
# error
|
|
20
|
-
# locale-keyed by convention.
|
|
18
|
+
# Files that fail to parse are skipped and recorded in the returned index's `load_errors`, which the
|
|
19
|
+
# plugin discloses as a run-scoped load-error row. Non-Hash YAML roots (e.g. a top-level sequence) are
|
|
20
|
+
# also skipped — the format is locale-keyed by convention.
|
|
21
21
|
class LocaleLoader
|
|
22
22
|
PLACEHOLDER_RE = /%\{(?<name>[^}]+)\}/
|
|
23
23
|
|
|
@@ -66,7 +66,7 @@ module Rigor
|
|
|
66
66
|
value_kinds: per_key_kinds[dotted_key].freeze
|
|
67
67
|
)
|
|
68
68
|
end
|
|
69
|
-
LocaleIndex.new(entries, locales: locales.to_a.sort)
|
|
69
|
+
LocaleIndex.new(entries, locales: locales.to_a.sort, load_errors: @load_errors)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
private
|
|
@@ -90,7 +90,7 @@ module Rigor
|
|
|
90
90
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
91
91
|
next [] unless @io_boundary.directory?(absolute)
|
|
92
92
|
|
|
93
|
-
Dir.glob(File.join(absolute,
|
|
93
|
+
Dir.glob("**/*.{yml,yaml}", base: absolute).map { |path| File.join(absolute, path) }
|
|
94
94
|
end.sort
|
|
95
95
|
end
|
|
96
96
|
|
|
@@ -57,7 +57,11 @@ module Rigor
|
|
|
57
57
|
target_gems: ["i18n"],
|
|
58
58
|
# Bumped 2026-06-23 — view template lazy-key scanning (`t('.key')` inside ERB / Haml / Slim under
|
|
59
59
|
# `view_search_paths`).
|
|
60
|
-
|
|
60
|
+
# Bumped 2026-10-17 (#1954) — the cached `:locale_index` value now carries the YAML load errors. A
|
|
61
|
+
# belt-and-braces key change: a released build already drops pre-change entries with the cache ABI
|
|
62
|
+
# (`Rigor::VERSION`) and a checkout with its engine-source identity, and the read below tolerates
|
|
63
|
+
# an index without errors either way.
|
|
64
|
+
version: "0.3.1",
|
|
61
65
|
description: "Validates I18n `t(key)` calls against `config/locales/*.yml`.",
|
|
62
66
|
config_schema: {
|
|
63
67
|
"locale_search_paths" => { kind: :array, default: ["config/locales"] },
|
|
@@ -72,17 +76,12 @@ module Rigor
|
|
|
72
76
|
)
|
|
73
77
|
|
|
74
78
|
# `watch:` covers every `.yml` / `.yaml` file under the locale search paths so the cache invalidates
|
|
75
|
-
# when locale files are added, removed, or edited (ADR-60 WD3).
|
|
76
|
-
#
|
|
77
|
-
#
|
|
79
|
+
# when locale files are added, removed, or edited (ADR-60 WD3). The YAML load errors travel inside the
|
|
80
|
+
# returned index (`LocaleIndex#load_errors`), not in an instance variable the block sets (#1954): a
|
|
81
|
+
# warm producer-cache hit skips the block, so a block-side capture lost the load-error row on every
|
|
82
|
+
# run whose result was not itself served from the run-result cache.
|
|
78
83
|
producer :locale_index, watch: -> { [[@locale_search_paths, "**/*.yml", "**/*.yaml"]] } do |_params|
|
|
79
|
-
|
|
80
|
-
io_boundary: io_boundary,
|
|
81
|
-
search_paths: @locale_search_paths
|
|
82
|
-
)
|
|
83
|
-
index = loader.load
|
|
84
|
-
@load_errors = loader.load_errors
|
|
85
|
-
index
|
|
84
|
+
LocaleLoader.new(io_boundary: io_boundary, search_paths: @locale_search_paths).load
|
|
86
85
|
end
|
|
87
86
|
|
|
88
87
|
# Scans view templates under `view_search_paths` for lazy `t('.key')` / `I18n.translate('.key')`
|
|
@@ -102,7 +101,6 @@ module Rigor
|
|
|
102
101
|
@locale_search_paths = Array(config.fetch("locale_search_paths")).map(&:to_s)
|
|
103
102
|
@view_search_paths = Array(config.fetch("view_search_paths")).map(&:to_s)
|
|
104
103
|
@configured_locales = Array(config.fetch("configured_locales")).map(&:to_s)
|
|
105
|
-
@load_errors = []
|
|
106
104
|
end
|
|
107
105
|
|
|
108
106
|
# Returns no rows of its own: every file-level outcome here is run-scoped. The YAML load errors and the
|
|
@@ -114,8 +112,11 @@ module Rigor
|
|
|
114
112
|
# are lazily loaded + memoised by `producer_value`, so the repeat registrations are no-ops.
|
|
115
113
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
116
114
|
index = producer_value(:locale_index)
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
if index
|
|
116
|
+
disclose_load_errors(Array(index.load_errors))
|
|
117
|
+
elsif producer_error(:locale_index)
|
|
118
|
+
disclose_locale_runtime_error
|
|
119
|
+
end
|
|
119
120
|
view_diags = producer_value(:view_diagnostics) || []
|
|
120
121
|
if (view_err = producer_error(:view_diagnostics))
|
|
121
122
|
disclose_view_runtime_error(view_err)
|
|
@@ -175,7 +176,7 @@ module Rigor
|
|
|
175
176
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
176
177
|
next [] unless io_boundary.directory?(absolute)
|
|
177
178
|
|
|
178
|
-
Dir.glob(
|
|
179
|
+
Dir.glob("**/*.{erb,haml,slim}", base: absolute).map { |path| File.join(absolute, path) }
|
|
179
180
|
end.sort
|
|
180
181
|
end
|
|
181
182
|
|
|
@@ -209,8 +210,8 @@ module Rigor
|
|
|
209
210
|
# be pinned to the first analysed Ruby file — once per plugin INSTANCE, so once per fork-pool worker
|
|
210
211
|
# (#1051). They go through the run-scoped channel instead: keyed by the locale path that failed, so a
|
|
211
212
|
# project with two broken YAML files still discloses both, each exactly once, at `.rigor.yml:1:1`.
|
|
212
|
-
def disclose_load_errors
|
|
213
|
-
|
|
213
|
+
def disclose_load_errors(load_errors)
|
|
214
|
+
load_errors.each do |err|
|
|
214
215
|
disclose_once(
|
|
215
216
|
"locale-parse:#{err.path}",
|
|
216
217
|
message: "rigor-rails-i18n: failed to parse `#{err.path}`: #{err.message}",
|
|
@@ -168,7 +168,7 @@ module Rigor
|
|
|
168
168
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
169
169
|
next unless io_boundary.directory?(absolute)
|
|
170
170
|
|
|
171
|
-
Dir.glob(File.join(absolute,
|
|
171
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }.each(&)
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
|