rigortype 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- 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 +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- 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 +78 -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 +344 -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_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- 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 +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- 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 +169 -31
- 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 +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- 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 +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- 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 +17 -1
- 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/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- 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/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- 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 +207 -49
- 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-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- 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/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- 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 +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -30,6 +30,65 @@ module Rigor
|
|
|
30
30
|
|
|
31
31
|
module_function
|
|
32
32
|
|
|
33
|
+
# The analyzed project's resolved bundler install root (e.g. `<project>/vendor/bundle`), or nil.
|
|
34
|
+
# Set from the same `bundler.*` resolution that feeds bundle sig discovery (O4 / ADR-82 WD9) — by
|
|
35
|
+
# the runner's pre-passes BEFORE any plugin `#prepare` runs, by `Environment.for_project` (which
|
|
36
|
+
# also covers pool workers), and by `rigor plugins`' probe. When a target library cannot be
|
|
37
|
+
# required from Rigor's own gem
|
|
38
|
+
# environment (the standalone `gem install rigortype` case — activesupport is deliberately NOT a
|
|
39
|
+
# runtime dependency), {require_with_target_bundle} falls back to requiring it from this bundle:
|
|
40
|
+
# the analyzed Rails project always carries its own activesupport on disk, and loading the
|
|
41
|
+
# project's locked copy is the higher-fidelity source of inflection rules anyway (ADR-79).
|
|
42
|
+
def target_bundle_root
|
|
43
|
+
@target_bundle_root
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def target_bundle_root=(root)
|
|
47
|
+
@target_bundle_root = root.nil? ? nil : File.expand_path(root.to_s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Requires `feature`, falling back to the analyzed project's bundler install tree. The fallback
|
|
51
|
+
# appends every bundle gem's `full_require_paths` (from its RubyGems-generated `specifications/`
|
|
52
|
+
# gemspec — the gem's own metadata, so nonstandard require paths like concurrent-ruby's
|
|
53
|
+
# `lib/concurrent-ruby` resolve correctly) to `$LOAD_PATH` and retries. `$LOAD_PATH` is appended,
|
|
54
|
+
# never prepended, and only on a failed require: Rigor's own activated gems keep precedence, and a
|
|
55
|
+
# host environment that carries the gem never consults the bundle. (`Gem.paths` augmentation was
|
|
56
|
+
# rejected — Bundler-locked processes silently ignore it.) Under the default `process` strategy the
|
|
57
|
+
# mutation happens inside the forked worker only; under `none`/Direct it lands in the main space,
|
|
58
|
+
# which is what that strategy means (documented tradeoff — the invoked library is trusted + pure).
|
|
59
|
+
def require_with_target_bundle(feature, bundle_root)
|
|
60
|
+
require feature
|
|
61
|
+
rescue ::LoadError
|
|
62
|
+
added = bundle_require_paths(bundle_root) - $LOAD_PATH
|
|
63
|
+
raise if added.empty?
|
|
64
|
+
|
|
65
|
+
$LOAD_PATH.concat(added)
|
|
66
|
+
require feature
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Every bundle gem's require paths, newest version per gem name. Loading a `specifications/*.gemspec`
|
|
70
|
+
# evaluates RubyGems-generated metadata of an installed third-party gem — the same trust level as
|
|
71
|
+
# requiring the gem, which is what the caller is about to do.
|
|
72
|
+
def bundle_require_paths(bundle_root)
|
|
73
|
+
bundle_gem_dirs(bundle_root).flat_map do |gem_home|
|
|
74
|
+
specs = Dir.glob(File.join(gem_home, "specifications", "*.gemspec"))
|
|
75
|
+
.filter_map { |file| Gem::Specification.load(file) }
|
|
76
|
+
specs.group_by(&:name)
|
|
77
|
+
.flat_map { |_, versions| versions.max_by(&:version).full_require_paths }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The RubyGems-shaped directories under a bundler install root: bundler nests them as
|
|
82
|
+
# `<root>/ruby/<ruby-version>/`; a `BUNDLE_PATH` may also point directly at such a directory. Only
|
|
83
|
+
# directories that actually carry a `specifications/` index are returned.
|
|
84
|
+
def bundle_gem_dirs(bundle_root)
|
|
85
|
+
return [] if bundle_root.nil? || bundle_root.to_s.empty?
|
|
86
|
+
|
|
87
|
+
root = bundle_root.to_s
|
|
88
|
+
(Dir.glob(File.join(root, "ruby", "*")) + [root])
|
|
89
|
+
.select { |dir| File.directory?(File.join(dir, "specifications")) }
|
|
90
|
+
end
|
|
91
|
+
|
|
33
92
|
# The default strategy. `process` (a crash-contained forked worker) is the default: it isolates the
|
|
34
93
|
# target library's monkey-patches + crashes from Rigor with no in-process contamination, and forks a
|
|
35
94
|
# single persistent worker (not one per call). It falls back to `none` where fork is unavailable (see
|
|
@@ -68,10 +127,15 @@ module Rigor
|
|
|
68
127
|
module Direct
|
|
69
128
|
module_function
|
|
70
129
|
|
|
130
|
+
# NOTE: rescue classes are `::`-qualified throughout this file: `Rigor::Plugin::LoadError` (the
|
|
131
|
+
# plugin-loading error) shadows the global `LoadError` in this lexical scope once plugin machinery
|
|
132
|
+
# is loaded, so a bare `rescue LoadError` matches the WRONG class — the real `::LoadError` (a
|
|
133
|
+
# ScriptError, not a StandardError) then escapes, kills the worker, and surfaces as an opaque
|
|
134
|
+
# EOFError instead of the clean decline (the standalone-install regression of 2026-07-16).
|
|
71
135
|
def call(feature:, receiver:, method:, args:)
|
|
72
|
-
|
|
136
|
+
Isolation.require_with_target_bundle(feature, Isolation.target_bundle_root)
|
|
73
137
|
Object.const_get(receiver).public_send(method, *args)
|
|
74
|
-
rescue LoadError, NameError => e
|
|
138
|
+
rescue ::LoadError, ::NameError => e
|
|
75
139
|
raise Unavailable, "#{receiver} could not be loaded (#{e.class}: #{e.message})"
|
|
76
140
|
end
|
|
77
141
|
end
|
|
@@ -109,13 +173,13 @@ module Rigor
|
|
|
109
173
|
def call(feature:, receiver:, method:, args:)
|
|
110
174
|
raise Unavailable, "process isolation unavailable: fork is not supported" unless available?
|
|
111
175
|
|
|
112
|
-
status, value = exchange([feature, receiver, method, args])
|
|
176
|
+
status, value = exchange([feature, receiver, method, args, Isolation.target_bundle_root])
|
|
113
177
|
raise Unavailable, "process isolation worker error: #{value}" if status == :error
|
|
114
178
|
|
|
115
179
|
value
|
|
116
180
|
rescue Unavailable
|
|
117
181
|
raise
|
|
118
|
-
rescue StandardError => e
|
|
182
|
+
rescue ::StandardError => e
|
|
119
183
|
# A dead worker surfaces as EOFError (Marshal.load) or Errno::EPIPE (Marshal.dump) — both
|
|
120
184
|
# StandardError. The crash is contained: the parent resets the worker (respawn next call) and
|
|
121
185
|
# declines.
|
|
@@ -123,7 +187,9 @@ module Rigor
|
|
|
123
187
|
raise Unavailable, "process isolation worker failed (#{e.class})"
|
|
124
188
|
end
|
|
125
189
|
|
|
126
|
-
# Sends one request to the persistent worker and reads its reply.
|
|
190
|
+
# Sends one request to the persistent worker and reads its reply. The request carries the
|
|
191
|
+
# target-bundle root so the fallback require (see {Isolation.require_with_target_bundle}) happens
|
|
192
|
+
# inside the worker — the parent's gem environment is never touched.
|
|
127
193
|
def exchange(request)
|
|
128
194
|
w = worker
|
|
129
195
|
Marshal.dump(request, w[:req])
|
|
@@ -152,22 +218,26 @@ module Rigor
|
|
|
152
218
|
end
|
|
153
219
|
end
|
|
154
220
|
|
|
155
|
-
# The child loop: read a `[feature, receiver, method, args]` request, require + call,
|
|
156
|
-
# `[:ok, result]` or `[:error, message]`. EOF (parent gone) ends the loop.
|
|
221
|
+
# The child loop: read a `[feature, receiver, method, args, bundle_root]` request, require + call,
|
|
222
|
+
# and write `[:ok, result]` or `[:error, message]`. EOF (parent gone) ends the loop.
|
|
223
|
+
#
|
|
224
|
+
# The rescue is `::`-qualified (see {Direct.call}) and covers `::ScriptError` so a failed require
|
|
225
|
+
# (`::LoadError` is a ScriptError) replies with the clean `[:error, …]` decline instead of
|
|
226
|
+
# silently killing the worker — the parent would otherwise see only an opaque EOFError.
|
|
157
227
|
def run_worker_loop(req_r, res_w)
|
|
158
228
|
loop do
|
|
159
|
-
feature, receiver, method, args = Marshal.load(req_r) # rubocop:disable Security/MarshalLoad -- parent input
|
|
229
|
+
feature, receiver, method, args, bundle_root = Marshal.load(req_r) # rubocop:disable Security/MarshalLoad -- parent input
|
|
160
230
|
reply =
|
|
161
231
|
begin
|
|
162
|
-
|
|
232
|
+
Isolation.require_with_target_bundle(feature, bundle_root)
|
|
163
233
|
[:ok, Object.const_get(receiver).public_send(method, *args)]
|
|
164
|
-
rescue StandardError,
|
|
234
|
+
rescue ::StandardError, ::ScriptError => e
|
|
165
235
|
[:error, "#{e.class}: #{e.message}"]
|
|
166
236
|
end
|
|
167
237
|
Marshal.dump(reply, res_w)
|
|
168
238
|
res_w.flush
|
|
169
239
|
end
|
|
170
|
-
rescue EOFError
|
|
240
|
+
rescue ::EOFError
|
|
171
241
|
# parent closed the request pipe — exit quietly
|
|
172
242
|
ensure
|
|
173
243
|
exit!(0)
|
|
@@ -11,6 +11,14 @@ module Rigor
|
|
|
11
11
|
class LoadError < StandardError
|
|
12
12
|
attr_reader :plugin_ref, :cause_class, :reason
|
|
13
13
|
|
|
14
|
+
# #194 slice 1 — the file `require` resolved the plugin gem to, when the require SUCCEEDED but the
|
|
15
|
+
# later configuration / instantiation step then failed. The loader stamps it in a rescue after a
|
|
16
|
+
# successful require (a require that failed outright never resolves a file and leaves this nil), so a
|
|
17
|
+
# config/init failure names the exact plugin copy it loaded from — the engine↔plugin version skew that
|
|
18
|
+
# made #194 a multi-round diagnosis. Consumed by the `plugin_loader.load-error` diagnostic message and
|
|
19
|
+
# the `rigor plugins` load-error row.
|
|
20
|
+
attr_accessor :resolved_path
|
|
21
|
+
|
|
14
22
|
# ADR-9 slice 5 introduces two new reason codes alongside the implicit "load failure" used for require /
|
|
15
23
|
# configuration / init failures:
|
|
16
24
|
#
|
|
@@ -19,11 +27,12 @@ module Rigor
|
|
|
19
27
|
# - `:dependency-cycle` — the consumes graph forms a cycle.
|
|
20
28
|
#
|
|
21
29
|
# Older callers omit `reason:` and the field defaults to nil (the legacy "load failure" envelope).
|
|
22
|
-
def initialize(message, plugin_ref:, cause: nil, reason: nil)
|
|
30
|
+
def initialize(message, plugin_ref:, cause: nil, reason: nil, resolved_path: nil)
|
|
23
31
|
super(message)
|
|
24
32
|
@plugin_ref = plugin_ref
|
|
25
33
|
@cause_class = cause&.class
|
|
26
34
|
@reason = reason&.to_sym
|
|
35
|
+
@resolved_path = resolved_path
|
|
27
36
|
end
|
|
28
37
|
end
|
|
29
38
|
end
|
data/lib/rigor/plugin/loader.rb
CHANGED
|
@@ -12,7 +12,8 @@ module Rigor
|
|
|
12
12
|
#
|
|
13
13
|
# Steps per entry (in order):
|
|
14
14
|
#
|
|
15
|
-
#
|
|
15
|
+
# 0. Skip the entry entirely when it carries `enabled: false` (the ADR-93 WD2 opt-out).
|
|
16
|
+
# 1. Normalise the entry into `{ gem:, id:, config:, enabled: }`.
|
|
16
17
|
# 2. `require` the gem (failures surface as a {LoadError}).
|
|
17
18
|
# 3. Look up the registered plugin class by id (or by gem name if the entry omitted an explicit id).
|
|
18
19
|
# 4. Validate the user's config against the manifest's `config_schema`.
|
|
@@ -22,18 +23,57 @@ module Rigor
|
|
|
22
23
|
# entries that resolve to the same gem. Failures do not abort the run; the loader collects them on the
|
|
23
24
|
# {Registry} so the runner can convert each one into a `:plugin_loader` diagnostic.
|
|
24
25
|
class Loader # rubocop:disable Metrics/ClassLength
|
|
25
|
-
attr_reader :services, :requirer
|
|
26
|
+
attr_reader :services, :requirer, :feature_resolver
|
|
27
|
+
|
|
28
|
+
# #194 slice 1 — resolve the file that satisfied `require <gem>` by scanning `$LOADED_FEATURES` for the
|
|
29
|
+
# entry ending in `/<gem>.rb`, LAST match preferred. Provenance-agnostic: it pins the actual file for a
|
|
30
|
+
# Bundler path gem, an installed gem, and a `-I`-injected checkout alike, and — because the feature is
|
|
31
|
+
# already present from the first load — it still resolves when `require` no-ops on a repeat in-process
|
|
32
|
+
# load (ADR-88 WD4b). Returns nil, never raises, in the degenerate cases: a gem whose entry-file
|
|
33
|
+
# basename differs from the gem name, and a spec's fake requirer that never populated `$LOADED_FEATURES`.
|
|
34
|
+
FEATURE_RESOLVER = lambda do |gem_name|
|
|
35
|
+
suffix = "/#{gem_name}.rb"
|
|
36
|
+
$LOADED_FEATURES.rfind { |feature| feature.end_with?(suffix) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# #194 slice 2 (ADR-93 WD5) — the engine's own root, anchored from THIS file's location: the loader
|
|
40
|
+
# lives at `<root>/lib/rigor/plugin/loader.rb`, so three levels up is the engine root. It resolves
|
|
41
|
+
# identically in a git checkout and inside an installed `rigortype` gem, because the gem packages the
|
|
42
|
+
# `plugins/` tree at the same relative path — which is exactly what makes it a trustworthy anchor for
|
|
43
|
+
# the engine's own bundled plugin copies.
|
|
44
|
+
ENGINE_ROOT = File.expand_path("../../..", __dir__)
|
|
26
45
|
|
|
27
46
|
# @param services [Rigor::Plugin::Services]
|
|
28
|
-
# @param requirer [#call] takes a gem name
|
|
29
|
-
#
|
|
30
|
-
|
|
47
|
+
# @param requirer [#call] takes a gem name OR an absolute file path (#194 slice 2 — a bundled plugin is
|
|
48
|
+
# required by its {.bundled_plugin_path}) and returns truthy on successful require. Defaulted to
|
|
49
|
+
# `Kernel.require` via a lambda, which accepts both forms; the spec injects a fake to avoid touching
|
|
50
|
+
# the real load path.
|
|
51
|
+
# @param feature_resolver [#call] takes a gem name and returns the absolute path `require` resolved it
|
|
52
|
+
# to (or nil). Defaulted to {FEATURE_RESOLVER}; the spec injects a fake so it never has to mutate the
|
|
53
|
+
# real `$LOADED_FEATURES` global.
|
|
54
|
+
def initialize(services:, requirer: ->(name) { require name }, feature_resolver: FEATURE_RESOLVER)
|
|
31
55
|
@services = services
|
|
32
56
|
@requirer = requirer
|
|
57
|
+
@feature_resolver = feature_resolver
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.load(configuration:, services:, requirer: ->(name) { require name }, feature_resolver: FEATURE_RESOLVER)
|
|
61
|
+
new(services: services, requirer: requirer, feature_resolver: feature_resolver).load(configuration.plugins)
|
|
33
62
|
end
|
|
34
63
|
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
# #194 slice 2 (ADR-93 WD5) — the absolute path of the engine's OWN bundled copy of a plugin gem
|
|
65
|
+
# (`<ENGINE_ROOT>/plugins/<gem>/lib/<gem>.rb`), or nil when the engine does not bundle it. A bundled
|
|
66
|
+
# plugin is required by this path rather than by gem name, so a stale installed `rigortype` gem can
|
|
67
|
+
# never displace the engine's own versioned copy through RubyGems name resolution — the engine and its
|
|
68
|
+
# bundled plugins are versioned together. Returns nil for a third-party / project-bundle plugin and for
|
|
69
|
+
# a trimmed packaging (the ADR-27 single-binary target that ships no `plugins/` tree), where the caller
|
|
70
|
+
# falls back to today's gem-name require so no install mode regresses. The rule is uniform across the
|
|
71
|
+
# auto-wired `rigor-rbs-inline` default and every user-listed entry, because the skew mechanism is
|
|
72
|
+
# identical for both. Also read by `rigor doctor`'s skew check (#194 slice 3) to decide which loaded
|
|
73
|
+
# plugins the engine bundles.
|
|
74
|
+
def self.bundled_plugin_path(gem_name)
|
|
75
|
+
path = File.join(ENGINE_ROOT, "plugins", gem_name, "lib", "#{gem_name}.rb")
|
|
76
|
+
File.file?(path) ? path : nil
|
|
37
77
|
end
|
|
38
78
|
|
|
39
79
|
# @param entries [Array<String, Hash>] the raw `plugins:` list from the configuration.
|
|
@@ -42,12 +82,21 @@ module Rigor
|
|
|
42
82
|
plugins = []
|
|
43
83
|
load_errors = []
|
|
44
84
|
seen_ids = {}
|
|
85
|
+
# #194 slice 1 — `gem name => resolved file path` for every gem the loader successfully required.
|
|
86
|
+
# A frozen plugin instance (e.g. `rigor-rbs-inline` self-freezes per ADR-32) can't carry the path,
|
|
87
|
+
# so it rides on the Registry keyed by gem name; the `rigor plugins` loaded row reads it back.
|
|
88
|
+
@resolved_gem_paths = {}
|
|
45
89
|
|
|
46
90
|
Array(entries).each_with_index do |raw, index|
|
|
47
91
|
entry = normalise_entry(raw, index)
|
|
48
92
|
rescue LoadError => e
|
|
49
93
|
load_errors << e
|
|
50
94
|
else
|
|
95
|
+
# ADR-93 WD2 — `enabled: false` opts a listed plugin out entirely: it is neither required nor
|
|
96
|
+
# trusted nor id-checked. This is the project-level opt-out for the auto-wired `rigor-rbs-inline`
|
|
97
|
+
# default (a user re-lists it with `enabled: false`), but it works for any entry.
|
|
98
|
+
next unless entry[:enabled]
|
|
99
|
+
|
|
51
100
|
begin
|
|
52
101
|
plugin = resolve_and_instantiate(entry, seen_ids)
|
|
53
102
|
plugins << plugin if plugin
|
|
@@ -64,19 +113,23 @@ module Rigor
|
|
|
64
113
|
load_errors.concat(sort_errors)
|
|
65
114
|
|
|
66
115
|
blueprints = plugins.map { |plugin| Blueprint.new(klass_name: plugin.class.name, config: plugin.config) }
|
|
67
|
-
Registry.new(plugins: plugins, blueprints: blueprints, load_errors: load_errors
|
|
116
|
+
Registry.new(plugins: plugins, blueprints: blueprints, load_errors: load_errors,
|
|
117
|
+
resolved_gem_paths: @resolved_gem_paths)
|
|
68
118
|
end
|
|
69
119
|
|
|
70
120
|
private
|
|
71
121
|
|
|
72
122
|
# Accepts:
|
|
73
|
-
# "rigor-
|
|
74
|
-
# { "gem" => "rigor-
|
|
75
|
-
# { gem: "rigor-
|
|
123
|
+
# "rigor-activerecord"
|
|
124
|
+
# { "gem" => "rigor-activerecord", "id" => "activerecord", "config" => {...} }
|
|
125
|
+
# { gem: "rigor-activerecord", id: "activerecord", config: {...} }
|
|
126
|
+
#
|
|
127
|
+
# The `id:` form selects one plugin from a gem that registers several — the bare-string form
|
|
128
|
+
# raises for those (see `lookup_plugin_class!`).
|
|
76
129
|
def normalise_entry(raw, index)
|
|
77
130
|
case raw
|
|
78
131
|
when String
|
|
79
|
-
{ gem: raw, id: nil, config: {} }
|
|
132
|
+
{ gem: raw, id: nil, config: {}, enabled: true }
|
|
80
133
|
when Hash
|
|
81
134
|
string_keyed = raw.to_h { |k, v| [k.to_s, v] }
|
|
82
135
|
gem_name = string_keyed["gem"] || string_keyed["id"]
|
|
@@ -87,7 +140,9 @@ module Rigor
|
|
|
87
140
|
)
|
|
88
141
|
end
|
|
89
142
|
|
|
90
|
-
|
|
143
|
+
# `enabled:` defaults to true; only the explicit `false` disables (nil / absent stays enabled).
|
|
144
|
+
{ gem: gem_name, id: string_keyed["id"], config: string_keyed["config"] || {},
|
|
145
|
+
enabled: string_keyed["enabled"] != false }
|
|
91
146
|
else
|
|
92
147
|
raise LoadError.new(
|
|
93
148
|
"plugin entry ##{index} must be a String or Hash, got #{raw.class}",
|
|
@@ -99,9 +154,24 @@ module Rigor
|
|
|
99
154
|
def resolve_and_instantiate(entry, seen_ids) # rubocop:disable Metrics/AbcSize
|
|
100
155
|
before = Plugin.registered.keys.to_set
|
|
101
156
|
require_gem!(entry)
|
|
157
|
+
# The require SUCCEEDED — pin the file it resolved to (nil when the resolver can't, never a raise).
|
|
158
|
+
# Recorded before the fallible steps below so a config/init failure can still name the loaded copy.
|
|
159
|
+
@resolved_gem_paths[entry[:gem]] = @feature_resolver.call(entry[:gem])
|
|
102
160
|
after = Plugin.registered.keys.to_set
|
|
103
161
|
newly_registered = (after - before).to_a
|
|
104
162
|
|
|
163
|
+
# ADR-88 WD4b — `require` runs a gem's body (and its `Rigor::Plugin.register` calls) at most ONCE per
|
|
164
|
+
# process. A first load captures the gem's ids in `newly_registered` and memoises the gem→ids mapping;
|
|
165
|
+
# a SECOND in-process load (the `--verify-incremental` full-run oracle, the incremental session's
|
|
166
|
+
# subset re-analysis, LSP re-loads) sees `require` no-op and an empty delta, so a bare-string
|
|
167
|
+
# (`id:`-less) entry recovers the gem's ids from the memo instead of raising "did not register any
|
|
168
|
+
# plugin". An explicit `id:` entry never needs this — it resolves by id directly in `lookup_plugin_class!`.
|
|
169
|
+
if newly_registered.any?
|
|
170
|
+
Plugin.record_gem_registration(entry[:gem], newly_registered)
|
|
171
|
+
elsif entry[:id].nil?
|
|
172
|
+
newly_registered = Plugin.ids_for_gem(entry[:gem])
|
|
173
|
+
end
|
|
174
|
+
|
|
105
175
|
plugin_class = lookup_plugin_class!(entry, newly_registered)
|
|
106
176
|
manifest = plugin_class.manifest
|
|
107
177
|
|
|
@@ -118,6 +188,13 @@ module Rigor
|
|
|
118
188
|
plugin = instantiate(plugin_class, entry[:config])
|
|
119
189
|
validate_signature_paths!(plugin)
|
|
120
190
|
plugin
|
|
191
|
+
rescue LoadError => e
|
|
192
|
+
# #194 slice 1 — annotate a POST-require failure (bad config, a raising `#init`, a missing signature
|
|
193
|
+
# dir, a duplicate id, no/many registrations) with the file the gem loaded from, so the surfaced
|
|
194
|
+
# diagnostic names the exact plugin copy. A require that failed outright never populated the map for
|
|
195
|
+
# this gem, so the lookup is nil and the message is left unchanged.
|
|
196
|
+
e.resolved_path ||= @resolved_gem_paths[entry[:gem]]
|
|
197
|
+
raise
|
|
121
198
|
end
|
|
122
199
|
|
|
123
200
|
# ADR-25 — a plugin's manifest-declared `signature_paths:` are resolved (by
|
|
@@ -137,8 +214,13 @@ module Rigor
|
|
|
137
214
|
end
|
|
138
215
|
end
|
|
139
216
|
|
|
217
|
+
# #194 slice 2 (ADR-93 WD5) — a bundled plugin is required by its engine-anchored absolute path; every
|
|
218
|
+
# other gem keeps today's bare-name require. The injectable `requirer` seam therefore widens from gem
|
|
219
|
+
# names to name-or-absolute-path (both are valid arguments to `Kernel.require`). The slice-1
|
|
220
|
+
# `feature_resolver` is unaffected: an absolute-path require's `$LOADED_FEATURES` entry still ends in
|
|
221
|
+
# `/<gem>.rb`, the suffix {FEATURE_RESOLVER} matches.
|
|
140
222
|
def require_gem!(entry)
|
|
141
|
-
@requirer.call(entry[:gem])
|
|
223
|
+
@requirer.call(self.class.bundled_plugin_path(entry[:gem]) || entry[:gem])
|
|
142
224
|
rescue ::LoadError => e
|
|
143
225
|
raise LoadError.new(
|
|
144
226
|
"could not load plugin gem #{entry[:gem].inspect}: #{e.message}",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "node_context"
|
|
4
|
+
require_relative "../source/node_children"
|
|
4
5
|
require_relative "../source/node_walker"
|
|
5
6
|
require_relative "../analysis/check_rules/rule_walk"
|
|
6
7
|
|
|
@@ -82,8 +83,9 @@ module Rigor
|
|
|
82
83
|
# The single converged DFS pre-order traversal. Threads both the live `ancestors` stack (for plugin
|
|
83
84
|
# {NodeContext}) and the immutable built-in {RuleWalk::Context} (for the collectors), derived together
|
|
84
85
|
# as the walk descends — the cheap-ancestors option from the ADR-53 B4 design note. Identical pre-order
|
|
85
|
-
# over
|
|
86
|
-
# `RuleWalk.walk`, so every node is visited in
|
|
86
|
+
# over each node's children (`#rigor_each_child`, which yields `compact_child_nodes` order) to
|
|
87
|
+
# both the legacy `Source::NodeWalker.each_with_ancestors` and `RuleWalk.walk`, so every node is visited in
|
|
88
|
+
# the same order each side saw before.
|
|
87
89
|
def walk_node(node, ancestors, context, path, scope, states, collector_driver)
|
|
88
90
|
return unless node.is_a?(Prism::Node)
|
|
89
91
|
|
|
@@ -92,7 +94,7 @@ module Rigor
|
|
|
92
94
|
|
|
93
95
|
child_context = collector_driver&.descend(node, context)
|
|
94
96
|
ancestors.push(node)
|
|
95
|
-
node.
|
|
97
|
+
node.rigor_each_child do |child|
|
|
96
98
|
walk_node(child, ancestors, child_context, path, scope, states, collector_driver)
|
|
97
99
|
end
|
|
98
100
|
ancestors.pop
|
|
@@ -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)
|