rigortype 0.2.9 → 0.3.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/README.md +2 -2
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +1 -0
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +49 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +19 -27
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +42 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -37,7 +37,7 @@ module Rigor
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def dynamic?(plugin) = @dynamic.include?(plugin)
|
|
40
|
-
def
|
|
40
|
+
def narrowing_facts?(plugin) = @narrowing_facts.include?(plugin)
|
|
41
41
|
|
|
42
42
|
# O(1) "could any plugin contribute a return type for a call named `method_name`?" — false only when
|
|
43
43
|
# every `dynamic_return` rule is `methods:`-gated on other names, in which case the ungated walk would
|
|
@@ -48,12 +48,12 @@ module Rigor
|
|
|
48
48
|
@dynamic_global_gate.include?(method_name)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# O(1) statement-path sibling of {#dispatch_candidate?} over the `
|
|
51
|
+
# O(1) statement-path sibling of {#dispatch_candidate?} over the `narrowing_facts` rules (which are
|
|
52
52
|
# always `methods:`-gated).
|
|
53
53
|
def statement_candidate?(method_name)
|
|
54
|
-
return true if @
|
|
54
|
+
return true if @narrowing_facts_global_gate.nil?
|
|
55
55
|
|
|
56
|
-
@
|
|
56
|
+
@narrowing_facts_global_gate.include?(method_name)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
# Per-plugin gate: false when the plugin declares no `dynamic_return` rules at all, or when every rule
|
|
@@ -67,11 +67,11 @@ module Rigor
|
|
|
67
67
|
gate.nil? || gate.include?(method_name)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
# Per-plugin gate over `
|
|
71
|
-
def
|
|
72
|
-
return false unless @
|
|
70
|
+
# Per-plugin gate over `narrowing_facts` rules; same contract as {#dynamic_candidate_for?}.
|
|
71
|
+
def narrowing_facts_candidate_for?(plugin, method_name)
|
|
72
|
+
return false unless @narrowing_facts.include?(plugin)
|
|
73
73
|
|
|
74
|
-
gate = @
|
|
74
|
+
gate = @narrowing_facts_gates[plugin]
|
|
75
75
|
gate.nil? || gate.include?(method_name)
|
|
76
76
|
end
|
|
77
77
|
|
|
@@ -101,13 +101,13 @@ module Rigor
|
|
|
101
101
|
def compile_memberships(plugins)
|
|
102
102
|
plugins.each { |p| reject_legacy_flow_hook!(p) }
|
|
103
103
|
@dynamic = plugins.reject { |p| p.class.dynamic_returns.empty? }.to_set
|
|
104
|
-
@
|
|
104
|
+
@narrowing_facts = plugins.reject { |p| p.class.narrowing_facts_rules.empty? }.to_set
|
|
105
105
|
compile_collector_subsets(plugins)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def compile_collector_subsets(plugins)
|
|
109
109
|
@for_method_dispatch = plugins.select { |p| @dynamic.include?(p) }.freeze
|
|
110
|
-
@for_statement = plugins.select { |p| @
|
|
110
|
+
@for_statement = plugins.select { |p| @narrowing_facts.include?(p) }.freeze
|
|
111
111
|
@for_file_diagnostics =
|
|
112
112
|
plugins.select { |p| file_diagnostics_overridden?(p) || !p.class.node_rules.empty? }.freeze
|
|
113
113
|
end
|
|
@@ -115,9 +115,9 @@ module Rigor
|
|
|
115
115
|
# The per-plugin and registry-global method-name gates.
|
|
116
116
|
def compile_gates
|
|
117
117
|
@dynamic_gates = build_name_gates(@dynamic) { |p| p.class.dynamic_returns }
|
|
118
|
-
@
|
|
118
|
+
@narrowing_facts_gates = build_name_gates(@narrowing_facts) { |p| p.class.narrowing_facts_rules }
|
|
119
119
|
@dynamic_global_gate = union_gate(@dynamic_gates)
|
|
120
|
-
@
|
|
120
|
+
@narrowing_facts_global_gate = union_gate(@narrowing_facts_gates)
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
# ADR-52 WD3 — the legacy ungated `flow_contribution_for` hook was deleted pre-1.0. A plugin still
|
|
@@ -215,7 +215,7 @@ module Rigor
|
|
|
215
215
|
# Ractor. The eventual Phase 4 pool ships `blueprints` across the boundary and calls {.materialize}
|
|
216
216
|
# per-Ractor; the live `plugins` carriage on the coordinator registry stays unchanged.
|
|
217
217
|
class Registry
|
|
218
|
-
attr_reader :plugins, :load_errors, :blueprints, :contribution_index
|
|
218
|
+
attr_reader :plugins, :load_errors, :blueprints, :contribution_index, :resolved_gem_paths
|
|
219
219
|
|
|
220
220
|
# @param plugins [Array<Rigor::Plugin::Base>] instantiated plugin instances in deterministic order.
|
|
221
221
|
# @param load_errors [Array<Rigor::Plugin::LoadError>] failures surfaced during loading. Each error is
|
|
@@ -223,20 +223,17 @@ module Rigor
|
|
|
223
223
|
# @param blueprints [Array<Rigor::Plugin::Blueprint>] frozen, Ractor-shareable replay descriptors
|
|
224
224
|
# aligned 1:1 with `plugins`. The loader fills this in; callers that construct Registry manually MAY
|
|
225
225
|
# pass `[]` and accept that {.materialize} cannot replay the set.
|
|
226
|
-
|
|
226
|
+
# @param resolved_gem_paths [Hash{String=>String,nil}] #194 slice 1 — `gem name => resolved file path`
|
|
227
|
+
# for each successfully required plugin gem, so `rigor plugins` can print where a loaded (and
|
|
228
|
+
# possibly frozen) plugin actually loaded from. Defaults to empty; a worker registry built by
|
|
229
|
+
# {.materialize} carries none (the provenance surface runs only on the coordinator).
|
|
230
|
+
def initialize(plugins: [], load_errors: [], blueprints: [], resolved_gem_paths: {})
|
|
227
231
|
@plugins = plugins.dup.freeze
|
|
228
232
|
@load_errors = load_errors.dup.freeze
|
|
229
233
|
@blueprints = blueprints.dup.freeze
|
|
234
|
+
@resolved_gem_paths = resolved_gem_paths.dup.freeze
|
|
230
235
|
@contribution_index = ContributionIndex.new(@plugins)
|
|
231
|
-
|
|
232
|
-
# compiled once here (the registry is frozen, so the flat_map-on-every-call versions re-derived an
|
|
233
|
-
# invariant). `@contracts_by_path` is a mutable per-path memo inside the frozen registry — safe
|
|
234
|
-
# because the contract set and the glob semantics are fixed for the lifetime of the run.
|
|
235
|
-
@additional_initializers = @plugins.flat_map { |p| safe_manifest(p)&.additional_initializers || [] }.freeze
|
|
236
|
-
@open_receivers = @plugins.flat_map { |p| (safe_manifest(p)&.open_receivers || []).map(&:to_s) }.uniq.freeze
|
|
237
|
-
@open_receivers_set = @open_receivers.to_set.freeze
|
|
238
|
-
@protocol_contracts = @plugins.flat_map { |p| safe_protocol_contracts(p) }.freeze
|
|
239
|
-
@contracts_by_path = {}
|
|
236
|
+
compile_aggregates
|
|
240
237
|
# ADR-52 WD4 — the single engine-owned node-rule walk, compiled once per run from the node-rule
|
|
241
238
|
# plugin subset (registry order). The runner reuses it for every file; it builds fresh per-file
|
|
242
239
|
# state internally, so it is safe to freeze and share.
|
|
@@ -372,6 +369,18 @@ module Rigor
|
|
|
372
369
|
|
|
373
370
|
private
|
|
374
371
|
|
|
372
|
+
# ADR-52 WD1 — aggregate queries the engine issues per def / per diagnostic candidate / per path are
|
|
373
|
+
# compiled once at construction (the registry is frozen, so the flat_map-on-every-call versions
|
|
374
|
+
# re-derived an invariant). `@contracts_by_path` is a mutable per-path memo inside the frozen registry
|
|
375
|
+
# — safe because the contract set and the glob semantics are fixed for the lifetime of the run.
|
|
376
|
+
def compile_aggregates
|
|
377
|
+
@additional_initializers = @plugins.flat_map { |p| safe_manifest(p)&.additional_initializers || [] }.freeze
|
|
378
|
+
@open_receivers = @plugins.flat_map { |p| (safe_manifest(p)&.open_receivers || []).map(&:to_s) }.uniq.freeze
|
|
379
|
+
@open_receivers_set = @open_receivers.to_set.freeze
|
|
380
|
+
@protocol_contracts = @plugins.flat_map { |p| safe_protocol_contracts(p) }.freeze
|
|
381
|
+
@contracts_by_path = {}
|
|
382
|
+
end
|
|
383
|
+
|
|
375
384
|
def path_matches_glob?(glob, path)
|
|
376
385
|
File.fnmatch?(glob, path, FNMATCH_FLAGS) ||
|
|
377
386
|
File.fnmatch?(File.join("**", glob), path, FNMATCH_FLAGS)
|
data/lib/rigor/plugin.rb
CHANGED
|
@@ -19,6 +19,15 @@ require_relative "plugin/inflector"
|
|
|
19
19
|
module Rigor
|
|
20
20
|
module Plugin
|
|
21
21
|
@registered = {}
|
|
22
|
+
# ADR-88 WD4b — process-global map `gem name (String) => [registered plugin id, ...]`, recorded by the
|
|
23
|
+
# loader the FIRST time a gem's `require` runs its body and registers plugins. A second in-process
|
|
24
|
+
# `Loader.load` of the same bare-string (`id:`-less) entry sees `require` return false (the gem is already
|
|
25
|
+
# loaded, so its `Rigor::Plugin.register` calls do not re-run) and an empty newly-registered delta — the
|
|
26
|
+
# loader recovers the gem's plugin id(s) from this memo instead of failing with "did not register any
|
|
27
|
+
# plugin". Populated by {.record_gem_registration}, read by {.ids_for_gem}. This is what makes a second
|
|
28
|
+
# in-process analysis (the `--verify-incremental` full-run oracle, the incremental session's subset
|
|
29
|
+
# re-analysis, the LSP's re-loads) load the same plugins the first run did.
|
|
30
|
+
@gem_registrations = {}
|
|
22
31
|
@mutex = Mutex.new
|
|
23
32
|
|
|
24
33
|
class << self
|
|
@@ -52,10 +61,27 @@ module Rigor
|
|
|
52
61
|
@mutex.synchronize { @registered.dup.freeze }
|
|
53
62
|
end
|
|
54
63
|
|
|
64
|
+
# ADR-88 WD4b — remember, for a gem loaded for the first time this process, the plugin id(s) its
|
|
65
|
+
# `require` registered. Idempotent (a repeated first-load records the same ids). See {@gem_registrations}.
|
|
66
|
+
def record_gem_registration(gem_name, ids)
|
|
67
|
+
return if gem_name.nil? || ids.nil? || ids.empty?
|
|
68
|
+
|
|
69
|
+
@mutex.synchronize { @gem_registrations[gem_name.to_s] = ids.map(&:to_s).freeze }
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ADR-88 WD4b — the plugin id(s) `gem_name` registered on its first in-process load, or `[]` when the gem
|
|
74
|
+
# was never loaded this process. The loader consults this when a re-load's `require` no-ops and the
|
|
75
|
+
# newly-registered delta is empty, so a bare-string plugin entry still resolves on the second load.
|
|
76
|
+
def ids_for_gem(gem_name)
|
|
77
|
+
@mutex.synchronize { @gem_registrations[gem_name.to_s] } || []
|
|
78
|
+
end
|
|
79
|
+
|
|
55
80
|
def unregister!(id = nil)
|
|
56
81
|
@mutex.synchronize do
|
|
57
82
|
if id.nil?
|
|
58
83
|
@registered.clear
|
|
84
|
+
@gem_registrations.clear
|
|
59
85
|
else
|
|
60
86
|
@registered.delete(id.to_s)
|
|
61
87
|
end
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../scope"
|
|
6
6
|
require_relative "../inference/scope_indexer"
|
|
7
|
+
require_relative "../source/node_children"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
# ADR-63 Tier 2 — the productized subset of the dev-only mutation-testing harness (`tool/mutation/`, ADR-62).
|
|
@@ -111,7 +112,7 @@ module Rigor
|
|
|
111
112
|
return if node.nil?
|
|
112
113
|
|
|
113
114
|
blk.call(node)
|
|
114
|
-
node.
|
|
115
|
+
node.rigor_each_child { |child| walk(child, &blk) }
|
|
115
116
|
end
|
|
116
117
|
|
|
117
118
|
def collect(node, out)
|
|
@@ -136,7 +137,7 @@ module Rigor
|
|
|
136
137
|
@anchor_for[arg] = [node.receiver, node.name.to_s] if literal?(arg)
|
|
137
138
|
end
|
|
138
139
|
end
|
|
139
|
-
node.
|
|
140
|
+
node.rigor_each_child { |child| index_literal_anchors(child) }
|
|
140
141
|
end
|
|
141
142
|
|
|
142
143
|
def literal?(node)
|
data/lib/rigor/reflection.rb
CHANGED
|
@@ -93,6 +93,11 @@ module Rigor
|
|
|
93
93
|
# Returns the type of the named constant. Joins in-source constants (recorded by
|
|
94
94
|
# `ScopeIndexer`) and RBS-side constants. In-source wins on collision because the user's
|
|
95
95
|
# source is the authoritative declaration.
|
|
96
|
+
#
|
|
97
|
+
# This is the flat lookup keyed on the literal `constant_name`. Callers that need Ruby's
|
|
98
|
+
# lexical constant resolution (walking the enclosing class path, and folding in registry
|
|
99
|
+
# classes / source-discovered classes as `Singleton` / class types) use
|
|
100
|
+
# {.resolve_constant_type} instead.
|
|
96
101
|
def constant_type_for(constant_name, scope: Scope.empty)
|
|
97
102
|
key = constant_name.to_s
|
|
98
103
|
in_source = scope.in_source_constants[key]
|
|
@@ -101,6 +106,65 @@ module Rigor
|
|
|
101
106
|
scope.environment.constant_for_name(constant_name)
|
|
102
107
|
end
|
|
103
108
|
|
|
109
|
+
# Resolves a constant *reference* to its type through Ruby's lexical constant lookup: the
|
|
110
|
+
# most-qualified candidate first (the enclosing class path joined to `name`), then
|
|
111
|
+
# progressively less-qualified, then the bare `name`. Each candidate consults, in order,
|
|
112
|
+
# the class registry (yielding a `Singleton[C]`), source-discovered classes, in-source
|
|
113
|
+
# value constants, and finally RBS-side constants — in-source value constants winning over
|
|
114
|
+
# RBS because the user's source is authoritative for its own constants. Returns the matched
|
|
115
|
+
# `Rigor::Type`, or nil when no source knows the constant.
|
|
116
|
+
#
|
|
117
|
+
# This is the shared owner of the lexical-constant resolution: `Inference::ExpressionTyper`
|
|
118
|
+
# reads it to type a constant read, and `Inference::Narrowing` reads it to recognise a
|
|
119
|
+
# value-pinned `Constant[Regexp]` match-predicate operand.
|
|
120
|
+
def resolve_constant_type(name, scope: Scope.empty)
|
|
121
|
+
env = scope.environment
|
|
122
|
+
discovered = scope.discovered_classes
|
|
123
|
+
in_source = scope.in_source_constants
|
|
124
|
+
lexical_constant_candidates(name, scope: scope).each do |candidate|
|
|
125
|
+
singleton = env.singleton_for_name(candidate)
|
|
126
|
+
return singleton if singleton
|
|
127
|
+
|
|
128
|
+
in_source_class = discovered[candidate]
|
|
129
|
+
return in_source_class if in_source_class
|
|
130
|
+
|
|
131
|
+
# In-source value-bearing constants take precedence over RBS constant decls because
|
|
132
|
+
# user code is the authoritative source for its own constants.
|
|
133
|
+
in_source_value = in_source[candidate]
|
|
134
|
+
return in_source_value if in_source_value
|
|
135
|
+
|
|
136
|
+
value = env.constant_for_name(candidate)
|
|
137
|
+
return value if value
|
|
138
|
+
end
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The candidate qualified names to try, in Ruby's lexical order: most-qualified first (the
|
|
143
|
+
# enclosing class path joined to `name`), then progressively less-qualified, then the bare
|
|
144
|
+
# `name`. A top-level scope (no `self_type`) yields only `[name]`.
|
|
145
|
+
def lexical_constant_candidates(name, scope: Scope.empty)
|
|
146
|
+
prefix = enclosing_class_path(scope)
|
|
147
|
+
candidates = []
|
|
148
|
+
while prefix && !prefix.empty?
|
|
149
|
+
candidates << "#{prefix}::#{name}"
|
|
150
|
+
idx = prefix.rindex("::")
|
|
151
|
+
prefix = idx ? prefix[0, idx] : nil
|
|
152
|
+
end
|
|
153
|
+
candidates << name
|
|
154
|
+
candidates
|
|
155
|
+
end
|
|
156
|
+
private_class_method :lexical_constant_candidates
|
|
157
|
+
|
|
158
|
+
# Pulls the enclosing qualified class name out of `scope.self_type` when one is set.
|
|
159
|
+
# `Nominal[T]` and `Singleton[T]` both expose `class_name`. Returns nil at the top level.
|
|
160
|
+
def enclosing_class_path(scope)
|
|
161
|
+
st = scope.self_type
|
|
162
|
+
case st
|
|
163
|
+
when Type::Nominal, Type::Singleton then st.class_name
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
private_class_method :enclosing_class_path
|
|
167
|
+
|
|
104
168
|
# Returns the RBS `RBS::Definition::Method` for the instance method, or nil when the
|
|
105
169
|
# class or method is not in RBS. The source-side discovered-method facts are reachable
|
|
106
170
|
# through {.discovered_method?}; a future slice will unify the two under a
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# Process-level runtime concerns that sit beside the analyzer rather than
|
|
5
|
+
# inside it. Currently just deferred YJIT enablement.
|
|
6
|
+
module Runtime
|
|
7
|
+
# Deferred YJIT enablement for the long-running analysis commands.
|
|
8
|
+
#
|
|
9
|
+
# Ruby 3.3+ ships YJIT but leaves it off unless the process opts in
|
|
10
|
+
# (`--yjit`, `RUBY_YJIT_ENABLE=1`, or `RubyVM::YJIT.enable`). Enabling it
|
|
11
|
+
# unconditionally at boot is a net LOSS on short `rigor check` / `coverage`
|
|
12
|
+
# runs: the JIT compile cost is paid up front, but a run that finishes in a
|
|
13
|
+
# couple of seconds ends before it amortizes. Measured cold A/B (2026-07-13,
|
|
14
|
+
# Ruby 4.0.5): mastodon `app/models` 4.0s -> 5.0s (0.80x), kramdown `lib`
|
|
15
|
+
# 1.7s -> 2.55s (0.66x), dependabot 1.6s -> 2.34s (0.68x). Long runs win big
|
|
16
|
+
# in the same measurement: mastodon `app`+`lib` 25.2s -> 14.9s (1.69x),
|
|
17
|
+
# redmine `app/models` 8.6s -> 5.5s (1.56x). Allocations and diagnostics are
|
|
18
|
+
# byte-identical under YJIT — the tradeoff is purely wall-time.
|
|
19
|
+
#
|
|
20
|
+
# The resolution is to enable YJIT only once a run has proven long enough to
|
|
21
|
+
# amortize the compile cost. {enable_after} spawns one daemon thread that
|
|
22
|
+
# sleeps the deadline then enables: a run that finishes before the deadline
|
|
23
|
+
# never pays JIT compile (the sub-deadline penalty zone is avoided by
|
|
24
|
+
# construction), while a long run enables mid-flight and JITs the remaining
|
|
25
|
+
# — dominant — work. Long-lived servers (`rigor lsp` / `rigor mcp`) instead
|
|
26
|
+
# enable at boot via {enable_now}, since they always run long.
|
|
27
|
+
#
|
|
28
|
+
# Opt-out: `RIGOR_DISABLE_YJIT=1` makes every entry point a no-op (the pin
|
|
29
|
+
# the byte-identical-diagnostics gate uses). If YJIT is already enabled (the
|
|
30
|
+
# user passed `--yjit` / `RUBY_YJIT_ENABLE=1`) or is unavailable (a build
|
|
31
|
+
# without YJIT, or a non-MRI Ruby), both entry points are no-ops too.
|
|
32
|
+
module Jit
|
|
33
|
+
# Opt-out switch: `RIGOR_DISABLE_YJIT=1` disables both entry points.
|
|
34
|
+
DISABLE_ENV = "RIGOR_DISABLE_YJIT"
|
|
35
|
+
|
|
36
|
+
# Advanced / calibration override of the {enable_after} deadline, in
|
|
37
|
+
# float seconds. Unset (or unparseable / negative) falls back to
|
|
38
|
+
# {DEFAULT_DEADLINE_SECONDS}. Exposed so the deadline can be swept during
|
|
39
|
+
# calibration and tuned per-project without an engine change.
|
|
40
|
+
DEADLINE_ENV = "RIGOR_YJIT_DEADLINE"
|
|
41
|
+
|
|
42
|
+
# The amortization deadline for {enable_after}, in seconds.
|
|
43
|
+
#
|
|
44
|
+
# Calibrated by interleaved cold A/B measurement on 2026-07-13 (Ruby
|
|
45
|
+
# 4.0.5, arm64-darwin25). Enabling YJIT mid-run costs a ~1s compile burst,
|
|
46
|
+
# so a run whose length is only ~0-1s longer than the deadline pays the
|
|
47
|
+
# burst without time to amortize and LOSES (measured: `app/models` at a
|
|
48
|
+
# 3.0s deadline regressed 4.14s -> 4.68s). The short-run cases cluster at
|
|
49
|
+
# <=1.7s (kramdown / dependabot) and ~4.0-4.9s (mastodon `app/models` ~4.1s,
|
|
50
|
+
# mail ~4.9s), with the next case at redmine ~7s. 5.0s parks that danger
|
|
51
|
+
# zone in the empty 5-6s gap: every case <=4.9s finishes before the
|
|
52
|
+
# deadline fires (guaranteed no-YJIT parity — the sleeping thread is killed
|
|
53
|
+
# at exit), while runs long enough to pay off (>=~15s: gitlab, mastodon
|
|
54
|
+
# `app`+`lib`) enable mid-flight and JIT their dominant tail — the mastodon
|
|
55
|
+
# `app`+`lib` win is preserved ~100% (25.4s -> 15.1s, vs 15.1s always-on).
|
|
56
|
+
DEFAULT_DEADLINE_SECONDS = 5.0
|
|
57
|
+
|
|
58
|
+
module_function
|
|
59
|
+
|
|
60
|
+
# Enables YJIT now, unless opted out, already enabled, or unavailable.
|
|
61
|
+
#
|
|
62
|
+
# An imperative command, not a query — the boolean reports whether this
|
|
63
|
+
# call is what enabled YJIT (so the `?`-suffix predicate convention does
|
|
64
|
+
# not apply; hence the Naming/PredicateMethod exemption).
|
|
65
|
+
#
|
|
66
|
+
# @return [Boolean] true if this call enabled YJIT; false if it was a
|
|
67
|
+
# no-op (unavailable / opted out / already enabled).
|
|
68
|
+
def enable_now # rubocop:disable Naming/PredicateMethod
|
|
69
|
+
return false unless available?
|
|
70
|
+
return false if disabled?
|
|
71
|
+
return false if RubyVM::YJIT.enabled?
|
|
72
|
+
|
|
73
|
+
RubyVM::YJIT.enable
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Spawns one background thread that sleeps `seconds` then calls
|
|
78
|
+
# {enable_now}. The thread is a daemon in effect — Ruby kills all live
|
|
79
|
+
# threads when the main thread exits — so a run that finishes before the
|
|
80
|
+
# deadline simply never enables YJIT, which is the point.
|
|
81
|
+
#
|
|
82
|
+
# Returns nil (and spawns no thread) when the feature is already a no-op
|
|
83
|
+
# up front (unavailable / opted out / already enabled), so callers never
|
|
84
|
+
# pay for a thread that could only no-op.
|
|
85
|
+
#
|
|
86
|
+
# @param seconds [Numeric] the amortization deadline.
|
|
87
|
+
# @return [Thread, nil] the deadline thread, or nil when no-op up front.
|
|
88
|
+
def enable_after(seconds)
|
|
89
|
+
return nil unless available?
|
|
90
|
+
return nil if disabled?
|
|
91
|
+
return nil if RubyVM::YJIT.enabled?
|
|
92
|
+
|
|
93
|
+
thread = Thread.new do
|
|
94
|
+
sleep(seconds)
|
|
95
|
+
enable_now
|
|
96
|
+
end
|
|
97
|
+
# A background perf optimization must never spill a backtrace onto the
|
|
98
|
+
# run's stderr; on any unexpected failure, stay silent and leave YJIT
|
|
99
|
+
# off. The deadline sleep gives ample time to set this before the body
|
|
100
|
+
# could raise.
|
|
101
|
+
thread.report_on_exception = false
|
|
102
|
+
thread
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The resolved {enable_after} deadline: {DEADLINE_ENV} when it parses to a
|
|
106
|
+
# non-negative float, else {DEFAULT_DEADLINE_SECONDS}.
|
|
107
|
+
#
|
|
108
|
+
# @return [Numeric]
|
|
109
|
+
def deadline_seconds
|
|
110
|
+
raw = ENV.fetch(DEADLINE_ENV, nil)
|
|
111
|
+
return DEFAULT_DEADLINE_SECONDS if raw.nil? || raw.empty?
|
|
112
|
+
|
|
113
|
+
value = Float(raw, exception: false)
|
|
114
|
+
value && value >= 0 ? value : DEFAULT_DEADLINE_SECONDS
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @return [Boolean] whether this Ruby exposes `RubyVM::YJIT.enable`.
|
|
118
|
+
def available?
|
|
119
|
+
defined?(RubyVM::YJIT.enable) ? true : false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @return [Boolean] whether the opt-out env switch is set.
|
|
123
|
+
def disabled?
|
|
124
|
+
ENV[DISABLE_ENV] == "1"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -21,13 +21,15 @@ module Rigor
|
|
|
21
21
|
:discovered_def_nodes,
|
|
22
22
|
:discovered_singleton_def_nodes,
|
|
23
23
|
:discovered_def_sources,
|
|
24
|
+
:discovered_singleton_def_sources,
|
|
24
25
|
:discovered_method_visibilities,
|
|
25
26
|
:discovered_superclasses,
|
|
26
27
|
:discovered_includes,
|
|
27
28
|
:discovered_class_sources,
|
|
28
29
|
:data_member_layouts,
|
|
29
30
|
:struct_member_layouts,
|
|
30
|
-
:param_inferred_types
|
|
31
|
+
:param_inferred_types,
|
|
32
|
+
:run_generation
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
class DiscoveryIndex
|
|
@@ -48,6 +50,7 @@ module Rigor
|
|
|
48
50
|
discovered_def_nodes: EMPTY_TABLE,
|
|
49
51
|
discovered_singleton_def_nodes: EMPTY_TABLE,
|
|
50
52
|
discovered_def_sources: EMPTY_TABLE,
|
|
53
|
+
discovered_singleton_def_sources: EMPTY_TABLE,
|
|
51
54
|
discovered_method_visibilities: EMPTY_TABLE,
|
|
52
55
|
discovered_superclasses: EMPTY_TABLE,
|
|
53
56
|
discovered_includes: EMPTY_TABLE,
|
|
@@ -60,7 +63,14 @@ module Rigor
|
|
|
60
63
|
# `{param_name(Symbol) => Rigor::Type}` map of the union of resolved call-site argument types. Empty on
|
|
61
64
|
# every normal run; only the `coverage --protection` collection pass populates it today, so a `check` run
|
|
62
65
|
# leaves it empty and seeds nothing (byte-identical).
|
|
63
|
-
param_inferred_types: EMPTY_TABLE
|
|
66
|
+
param_inferred_types: EMPTY_TABLE,
|
|
67
|
+
# ADR-84 WD2 — the run-scope identity token `Analysis::Runner#run_analysis` mints per run (a frozen
|
|
68
|
+
# bare Object) and seeds through `project_scope_seed_tables`. The user-method return memo keys its
|
|
69
|
+
# bucket on this token's identity so hits cross consumer-file boundaries within one run but never
|
|
70
|
+
# cross a run boundary (LSP / ADR-62 warm-loop re-runs land in a fresh bucket). Nil on scopes that
|
|
71
|
+
# never see the runner seed (single-file probes, `run_source` before the seed applies): the memo
|
|
72
|
+
# falls back to today's per-file `discovered_def_nodes` identity for those.
|
|
73
|
+
run_generation: nil
|
|
64
74
|
)
|
|
65
75
|
end
|
|
66
76
|
end
|