rigortype 0.2.8 → 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/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- 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 +4 -3
- 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 +99 -4
- data/docs/manual/02-cli-reference.md +65 -19
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +5 -5
- 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/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +21 -0
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -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 +207 -60
- 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 +56 -30
- data/lib/rigor/cli/diagnostic_formats.rb +4 -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/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_report.rb +7 -1
- 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/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +71 -43
- 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 +379 -113
- data/lib/rigor/inference/fork_map.rb +87 -0
- 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 +245 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
- 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 +460 -49
- data/lib/rigor/inference/statement_evaluator.rb +111 -17
- 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/analyzer.rb +23 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
- 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-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- 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 +191 -0
- 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/helper_table.rb +19 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
- 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/environment.rbs +1 -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 +49 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "void_origin"
|
|
6
|
+
require_relative "../reflection"
|
|
7
|
+
require_relative "../source/node_children"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# ADR-100 WD4 — the lazy per-`def` "void tail" summary that carries an author-declared `-> void`
|
|
12
|
+
# provenance ACROSS an intermediate method whose own signature declares nothing. The direct case
|
|
13
|
+
# (`x = logger.log(...)`, an RBS `-> void` resolved on the receiver's own class) is recorded at the
|
|
14
|
+
# call site by {MethodDispatcher::RbsDispatch}; this summary answers the transitive case
|
|
15
|
+
# (`def bar; foo; end; b = bar`) where the `void` reaches the use through `bar`'s body, so no rule
|
|
16
|
+
# reading `bar`'s signature can see it.
|
|
17
|
+
#
|
|
18
|
+
# `VoidTail(def_node) -> VoidOrigin | nil`: computed on demand at the first consult, memoised per
|
|
19
|
+
# `def_node` (identity-keyed), cycle-guarded (a def already on the current chain rejects), and
|
|
20
|
+
# **pure** — AST shape + RBS reflection ({Reflection.instance_method_definition} /
|
|
21
|
+
# {Reflection.singleton_method_definition}) + discovery-index lookups ({Scope#user_def_for} /
|
|
22
|
+
# {Scope#singleton_def_for}) only, never expression evaluation. Because it never evaluates, it can
|
|
23
|
+
# neither re-enter the dispatcher nor depend on evaluation order or the fork-pool's file
|
|
24
|
+
# partitioning — the eager record-at-body-evaluation table WD4 rejected failed exactly there.
|
|
25
|
+
#
|
|
26
|
+
# A `def` (resolved on `owner` / `kind`) is admitted iff all four hold:
|
|
27
|
+
#
|
|
28
|
+
# 1. its OWN resolved RBS signature for `(owner, name, kind)` is absent, or every overload
|
|
29
|
+
# returns `untyped` (`RBS::Types::Bases::Any`). An author-declared concrete return — including
|
|
30
|
+
# `-> void` itself, which the direct rule already serves at the call site — keeps it out, so
|
|
31
|
+
# the direct rule and the summary are disjoint by construction (no double record).
|
|
32
|
+
# 2. its body is a plain statements body (no `rescue` / `else` / `ensure` on the `def` — those
|
|
33
|
+
# shapes make `def_node.body` a `Prism::BeginNode`) containing NO `Prism::ReturnNode` anywhere:
|
|
34
|
+
# a bare `return` yields `nil`, a non-void path, so the void tail must be the SOLE return path.
|
|
35
|
+
# The walk is deliberately over-conservative — a `return` inside a nested block also
|
|
36
|
+
# disqualifies.
|
|
37
|
+
# 3. its tail (last) expression is a `Prism::CallNode` with an implicit-`self` receiver (`nil`
|
|
38
|
+
# receiver or a literal `self`).
|
|
39
|
+
# 4. that tail resolves on the SAME owner (exact class, no ancestor walk on the discovery side) to
|
|
40
|
+
# either (a) an RBS definition whose EVERY overload returns `void` — the leaf, whose origin is
|
|
41
|
+
# `VoidOrigin(owner, tail_name, kind)` — or (b) another discovered `def` of the same kind, which
|
|
42
|
+
# recurses. Composition (`def baz; bar; end`) is the recursion, and the origin stays the leaf,
|
|
43
|
+
# so the message still names the author's `-> void` method. The RBS-void-leaf branch is checked
|
|
44
|
+
# FIRST because under ADR-93 auto-wire the leaf `def` is BOTH RBS-`void` and a discovered `def`;
|
|
45
|
+
# recursing into its body would miss the leaf.
|
|
46
|
+
#
|
|
47
|
+
# Everything else — explicit-receiver tails, conditional / boolean / `begin` tails, unresolvable
|
|
48
|
+
# names, non-void concrete RBS — rejects.
|
|
49
|
+
#
|
|
50
|
+
# Each tail chain is a functional graph (every `def` has exactly one tail, hence at most one
|
|
51
|
+
# successor), so a `def`'s result is fully determined by its own forward chain and is independent of
|
|
52
|
+
# the entry point: a chain either reaches a void leaf (`VoidOrigin`), a non-admitted node (nil), or a
|
|
53
|
+
# cycle (nil). Memoising is therefore always sound, and the cycle guard only ever rejects a node that
|
|
54
|
+
# is still mid-computation on the current chain (never yet memoised).
|
|
55
|
+
class VoidTailSummary
|
|
56
|
+
# Run-scoped memo, the ADR-84 `return_memo_bucket` house pattern: a single retained slot on
|
|
57
|
+
# `Thread.current` keyed by the run-generation token's identity, so a re-run in one process (LSP,
|
|
58
|
+
# ADR-62 warm loop) lands in a fresh bucket and can neither serve stale entries nor accumulate
|
|
59
|
+
# them. Thread-local ⇒ each fork-pool worker (a separate process) and each Ractor-pool worker (its
|
|
60
|
+
# own Ractor-local `Thread.current`) builds its own bucket; the bucket hash is created and mutated
|
|
61
|
+
# inside the worker and never shared, so no frozen / shared object is mutated and no
|
|
62
|
+
# graceful-degradation branch is needed. `def_node` identity is stable per run (the ADR-85
|
|
63
|
+
# {DefNodeResolver} yields one node object per `(path, node_id)`), the same identity the ADR-84
|
|
64
|
+
# return memo relies on.
|
|
65
|
+
MEMO_KEY = :__rigor_void_tail_summary_memo__
|
|
66
|
+
private_constant :MEMO_KEY
|
|
67
|
+
|
|
68
|
+
def initialize(scope)
|
|
69
|
+
@scope = scope
|
|
70
|
+
@environment = scope.environment
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The recovered `-> void` origin for `def_node` (whose owner / kind the caller resolved through
|
|
74
|
+
# the discovery index), or nil when the `def` is not admitted. A fresh visited set per top-level
|
|
75
|
+
# consult; the memo persists across consults for the run. The memo is probed before the visited
|
|
76
|
+
# set is allocated so the common post-warmup hit path allocates nothing.
|
|
77
|
+
#
|
|
78
|
+
# @param def_node [Prism::DefNode]
|
|
79
|
+
# @param owner [String] the qualified receiver class the `def` belongs to.
|
|
80
|
+
# @param kind [Symbol] `:instance` or `:singleton`.
|
|
81
|
+
# @return [Inference::VoidOrigin, nil]
|
|
82
|
+
def origin_for(def_node, owner, kind)
|
|
83
|
+
return nil if def_node.nil?
|
|
84
|
+
|
|
85
|
+
memo = memo_bucket
|
|
86
|
+
return memo[def_node] if memo.key?(def_node)
|
|
87
|
+
|
|
88
|
+
walk(def_node, owner, kind, {}.compare_by_identity)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
attr_reader :scope, :environment
|
|
94
|
+
|
|
95
|
+
# Memo + cycle-guarded computation. A def already on the current chain rejects WITHOUT memoising
|
|
96
|
+
# (it is mid-computation; its true result is memoised when its own frame completes).
|
|
97
|
+
def walk(def_node, owner, kind, visited)
|
|
98
|
+
return nil if def_node.nil?
|
|
99
|
+
return nil if visited.key?(def_node)
|
|
100
|
+
|
|
101
|
+
memo = memo_bucket
|
|
102
|
+
return memo[def_node] if memo.key?(def_node)
|
|
103
|
+
|
|
104
|
+
visited[def_node] = true
|
|
105
|
+
memo[def_node] = compute(def_node, owner, kind, visited)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def compute(def_node, owner, kind, visited)
|
|
109
|
+
return nil unless own_signature_admits?(owner, def_node.name, kind) # admission 1
|
|
110
|
+
|
|
111
|
+
tail = admissible_tail(def_node) # admissions 2 + 3
|
|
112
|
+
return nil if tail.nil?
|
|
113
|
+
|
|
114
|
+
resolve_tail(tail.name, owner, kind, visited) # admission 4
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Admission 1 — absent own signature, or every overload `untyped`. `untyped` is read as "no claim"
|
|
118
|
+
# per RBS's own semantics, so a hand-written `-> untyped` intermediate still admits: the provenance
|
|
119
|
+
# fact (this value was produced by an author-declared `-> void` return) remains true of it.
|
|
120
|
+
def own_signature_admits?(owner, method_name, kind)
|
|
121
|
+
definition = method_definition(owner, method_name, kind)
|
|
122
|
+
return true if definition.nil?
|
|
123
|
+
|
|
124
|
+
every_overload_return?(definition, RBS::Types::Bases::Any)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Admissions 2 + 3 — the tail expression when the body is a plain statements body with no
|
|
128
|
+
# `ReturnNode` anywhere and the tail is an implicit-self call; nil (reject) otherwise.
|
|
129
|
+
def admissible_tail(def_node)
|
|
130
|
+
body = def_node.body
|
|
131
|
+
return nil if body.nil?
|
|
132
|
+
return nil if body.is_a?(Prism::BeginNode) # def-level rescue / else / ensure
|
|
133
|
+
return nil if contains_return?(body)
|
|
134
|
+
|
|
135
|
+
tail = tail_expression(body)
|
|
136
|
+
return nil unless tail.is_a?(Prism::CallNode)
|
|
137
|
+
return nil unless implicit_self_receiver?(tail.receiver)
|
|
138
|
+
|
|
139
|
+
tail
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A `Prism::StatementsNode` (the plain and endless-def shapes both parse to one) tails on its last
|
|
143
|
+
# statement; any other surviving shape is treated as the tail expression itself.
|
|
144
|
+
def tail_expression(body)
|
|
145
|
+
case body
|
|
146
|
+
when Prism::StatementsNode then body.body.last
|
|
147
|
+
else body
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def implicit_self_receiver?(receiver)
|
|
152
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# True when a `Prism::ReturnNode` appears anywhere in the subtree — nested blocks and defs
|
|
156
|
+
# included (over-conservative is correct: a method that can return anything other than the void
|
|
157
|
+
# tail must never enter the table).
|
|
158
|
+
def contains_return?(node)
|
|
159
|
+
return false unless node.is_a?(Prism::Node)
|
|
160
|
+
return true if node.is_a?(Prism::ReturnNode)
|
|
161
|
+
|
|
162
|
+
node.rigor_each_child { |child| return true if contains_return?(child) }
|
|
163
|
+
false
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Admission 4 — the void leaf (RBS every-overload `void`) wins over the discovered-def recursion,
|
|
167
|
+
# because under ADR-93 auto-wire the annotated leaf is both.
|
|
168
|
+
def resolve_tail(tail_name, owner, kind, visited)
|
|
169
|
+
definition = method_definition(owner, tail_name, kind)
|
|
170
|
+
if definition && every_overload_return?(definition, RBS::Types::Bases::Void)
|
|
171
|
+
return VoidOrigin.new(class_name: owner, method_name: tail_name, kind: kind)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
next_def = discovered_def(owner, tail_name, kind)
|
|
175
|
+
return nil if next_def.nil?
|
|
176
|
+
|
|
177
|
+
walk(next_def, owner, kind, visited)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def method_definition(owner, method_name, kind)
|
|
181
|
+
if kind == :singleton
|
|
182
|
+
Reflection.singleton_method_definition(owner, method_name, environment: environment)
|
|
183
|
+
else
|
|
184
|
+
Reflection.instance_method_definition(owner, method_name, environment: environment)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def discovered_def(owner, method_name, kind)
|
|
189
|
+
if kind == :singleton
|
|
190
|
+
scope.singleton_def_for(owner, method_name)
|
|
191
|
+
else
|
|
192
|
+
scope.user_def_for(owner, method_name)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# True when the definition declares at least one overload and EVERY overload's return type is an
|
|
197
|
+
# instance of `base_klass` (`RBS::Types::Bases::Any` for admission 1, `...::Void` for the leaf).
|
|
198
|
+
# An `UntypedFunction` (`(?) -> T`) still responds to `return_type`, so it is handled the same way.
|
|
199
|
+
def every_overload_return?(definition, base_klass)
|
|
200
|
+
method_types = definition.method_types
|
|
201
|
+
return false if method_types.empty?
|
|
202
|
+
|
|
203
|
+
method_types.all? do |method_type|
|
|
204
|
+
fun = method_type.type
|
|
205
|
+
fun.respond_to?(:return_type) && fun.return_type.is_a?(base_klass)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def memo_bucket
|
|
210
|
+
generation = scope.run_generation || scope.discovered_def_nodes
|
|
211
|
+
slot = Thread.current[MEMO_KEY]
|
|
212
|
+
unless slot && slot[0].equal?(generation)
|
|
213
|
+
slot = [generation, {}.compare_by_identity]
|
|
214
|
+
Thread.current[MEMO_KEY] = slot
|
|
215
|
+
end
|
|
216
|
+
slot[1]
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -8,6 +8,7 @@ require_relative "../environment"
|
|
|
8
8
|
require_relative "../reflection"
|
|
9
9
|
require_relative "../scope"
|
|
10
10
|
require_relative "../source/node_locator"
|
|
11
|
+
require_relative "../source/node_children"
|
|
11
12
|
require_relative "../inference/scope_indexer"
|
|
12
13
|
require_relative "../type/nominal"
|
|
13
14
|
require_relative "../type/singleton"
|
|
@@ -187,7 +188,7 @@ module Rigor
|
|
|
187
188
|
n.location.start_offset <= symbol_offset && symbol_offset <= n.location.end_offset
|
|
188
189
|
result = n
|
|
189
190
|
end
|
|
190
|
-
n.
|
|
191
|
+
n.rigor_each_child(&walk)
|
|
191
192
|
end
|
|
192
193
|
walk.call(root)
|
|
193
194
|
result
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
6
|
require_relative "buffer_resolution"
|
|
7
|
+
require_relative "../source/node_children"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module LanguageServer
|
|
@@ -67,7 +68,7 @@ module Rigor
|
|
|
67
68
|
when Prism::DefNode
|
|
68
69
|
block.call(def_symbol(node, in_namespace: in_namespace))
|
|
69
70
|
else
|
|
70
|
-
node.
|
|
71
|
+
node.rigor_each_child do |child|
|
|
71
72
|
each_decl(child, in_namespace: in_namespace, &block)
|
|
72
73
|
end
|
|
73
74
|
end
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
6
|
require_relative "buffer_resolution"
|
|
7
|
+
require_relative "../source/node_children"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module LanguageServer
|
|
@@ -48,7 +49,7 @@ module Rigor
|
|
|
48
49
|
Prism::BlockNode
|
|
49
50
|
add_range(node, ranges)
|
|
50
51
|
end
|
|
51
|
-
node.
|
|
52
|
+
node.rigor_each_child { |child| walk(child, ranges) }
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def add_range(node, ranges)
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "uri"
|
|
6
6
|
require_relative "buffer_resolution"
|
|
7
|
+
require_relative "../source/node_children"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module LanguageServer
|
|
@@ -48,7 +49,7 @@ module Rigor
|
|
|
48
49
|
return chain unless node.location && offset_in?(node.location, offset)
|
|
49
50
|
|
|
50
51
|
chain << node
|
|
51
|
-
node.
|
|
52
|
+
node.rigor_each_child { |child| ancestor_chain(child, offset, chain) }
|
|
52
53
|
chain
|
|
53
54
|
end
|
|
54
55
|
|
|
@@ -8,6 +8,7 @@ require_relative "../environment"
|
|
|
8
8
|
require_relative "../reflection"
|
|
9
9
|
require_relative "../scope"
|
|
10
10
|
require_relative "../source/node_locator"
|
|
11
|
+
require_relative "../source/node_children"
|
|
11
12
|
require_relative "../inference/scope_indexer"
|
|
12
13
|
require_relative "../type/nominal"
|
|
13
14
|
require_relative "../type/singleton"
|
|
@@ -99,7 +100,7 @@ module Rigor
|
|
|
99
100
|
if n.is_a?(Prism::CallNode) && n.arguments && offset_in?(n.arguments.location, cursor_offset)
|
|
100
101
|
result = n # Innermost-wins because we keep walking children.
|
|
101
102
|
end
|
|
102
|
-
n.
|
|
103
|
+
n.rigor_each_child(&walk)
|
|
103
104
|
end
|
|
104
105
|
walk.call(root)
|
|
105
106
|
result
|
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -390,8 +390,8 @@ module Rigor
|
|
|
390
390
|
# the fact slot.
|
|
391
391
|
#
|
|
392
392
|
# Renamed from `type_specifier` (ADR-80): the old name read as a parallel to `dynamic_return` (a
|
|
393
|
-
# type) when it actually returns facts.
|
|
394
|
-
# 0.
|
|
393
|
+
# type) when it actually returns facts. The old verb was a deprecating alias through 0.2.x and is
|
|
394
|
+
# gone in 0.3.0 — `narrowing_facts` is the only spelling.
|
|
395
395
|
def narrowing_facts(methods:, &block)
|
|
396
396
|
raise ArgumentError, "Plugin::Base.narrowing_facts requires a block body" if block.nil?
|
|
397
397
|
unless methods.is_a?(Array) && !methods.empty? &&
|
|
@@ -401,30 +401,17 @@ module Rigor
|
|
|
401
401
|
"got #{methods.inspect}"
|
|
402
402
|
end
|
|
403
403
|
|
|
404
|
-
@
|
|
405
|
-
@
|
|
404
|
+
@narrowing_facts_rules ||= []
|
|
405
|
+
@narrowing_facts_rules << { methods: methods.map(&:to_sym).freeze, block: block }.freeze
|
|
406
406
|
nil
|
|
407
407
|
end
|
|
408
408
|
|
|
409
|
-
#
|
|
410
|
-
# *facts*, not a type; the old name misleads by parallel with {.dynamic_return}. Retained as a
|
|
411
|
-
# warning-emitting alias through 0.2.x; REMOVED in 0.3.0. Migrate `type_specifier methods: …` →
|
|
412
|
-
# `narrowing_facts methods: …`.
|
|
413
|
-
def type_specifier(methods:, &)
|
|
414
|
-
unless @type_specifier_deprecation_warned
|
|
415
|
-
@type_specifier_deprecation_warned = true
|
|
416
|
-
warn("[rigor] Plugin::Base.type_specifier is deprecated (ADR-80) and will be " \
|
|
417
|
-
"removed in Rigor 0.3.0; rename it to `narrowing_facts`. (#{name})")
|
|
418
|
-
end
|
|
419
|
-
narrowing_facts(methods:, &)
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
# Frozen snapshot of the declared type-specifier rules. Memoised for the same reason as
|
|
409
|
+
# Frozen snapshot of the declared narrowing-facts rules. Memoised for the same reason as
|
|
423
410
|
# {dynamic_returns} — consulted per plugin per dispatch, over an array fixed at class-definition
|
|
424
411
|
# time.
|
|
425
412
|
# rubocop:disable Naming/MemoizedInstanceVariableName -- see dynamic_returns
|
|
426
|
-
def
|
|
427
|
-
@
|
|
413
|
+
def narrowing_facts_rules
|
|
414
|
+
@narrowing_facts_rules_snapshot ||= (@narrowing_facts_rules || []).dup.freeze
|
|
428
415
|
end
|
|
429
416
|
# rubocop:enable Naming/MemoizedInstanceVariableName
|
|
430
417
|
end
|
|
@@ -558,8 +545,8 @@ module Rigor
|
|
|
558
545
|
# ADR-37 slice 2 — the post-return narrowing facts contributed by this plugin's {.narrowing_facts}
|
|
559
546
|
# rules for a call. The engine calls this from `StatementEvaluator`; a rule fires only when
|
|
560
547
|
# `call_node.name` is one of its declared `methods:`. Failures isolate to [].
|
|
561
|
-
def
|
|
562
|
-
rules = self.class.
|
|
548
|
+
def narrowing_facts_for(call_node:, scope:)
|
|
549
|
+
rules = self.class.narrowing_facts_rules
|
|
563
550
|
return [] if rules.empty? || !call_node.respond_to?(:name)
|
|
564
551
|
|
|
565
552
|
name = call_node.name
|
|
@@ -18,9 +18,12 @@ module Rigor
|
|
|
18
18
|
# rules are ingested by *statically parsing* `config/initializers/inflections.rb` in a later slice, never
|
|
19
19
|
# by executing it.)
|
|
20
20
|
#
|
|
21
|
-
# **Absence is silence, never a guess.**
|
|
22
|
-
#
|
|
23
|
-
#
|
|
21
|
+
# **Absence is silence, never a guess.** ActiveSupport is resolved from Rigor's host gem environment
|
|
22
|
+
# first, then from the analyzed project's own bundler install tree ({Isolation.target_bundle_root},
|
|
23
|
+
# ADR-90) — a Rails project always carries its locked activesupport on disk, and that copy is the
|
|
24
|
+
# higher-fidelity source of inflection rules anyway. When neither resolves (a standalone install
|
|
25
|
+
# analyzing a project whose bundle is not installed), the inflection methods raise {Unavailable}
|
|
26
|
+
# rather than approximate. That raise propagates to the caller's
|
|
24
27
|
# per-plugin rescue boundary, so the inflection-dependent check degrades to **no diagnostics** — reduced
|
|
25
28
|
# coverage, never a wrong fact. A consumer that wants to fail cleanly up front can gate on {available?}
|
|
26
29
|
# and emit a single load-error.
|
|
@@ -39,6 +42,12 @@ module Rigor
|
|
|
39
42
|
FEATURE = "active_support/inflector"
|
|
40
43
|
RECEIVER = "ActiveSupport::Inflector"
|
|
41
44
|
|
|
45
|
+
# The bundled plugins whose checks consume this helper (each README documents the dependency). Used
|
|
46
|
+
# by `rigor plugins` (ADR-90) to probe {available?} at activation time — so a standalone install
|
|
47
|
+
# where inflection silently degrades reports the degradation instead of an unqualified `[OK]`. When
|
|
48
|
+
# a new plugin adopts the helper, add its manifest id here.
|
|
49
|
+
CONSUMER_PLUGIN_IDS = %w[actionmailer actionpack activerecord factorybot rails-routes].freeze
|
|
50
|
+
|
|
42
51
|
# Raised when `ActiveSupport::Inflector` is required for an inflection but cannot be loaded. Caught by
|
|
43
52
|
# the per-plugin isolation boundary, so it surfaces as "this plugin produced no diagnostics" rather than
|
|
44
53
|
# a wrong inflection.
|
|
@@ -97,8 +97,12 @@ module Rigor
|
|
|
97
97
|
private
|
|
98
98
|
|
|
99
99
|
def record_file_entry(path, contents)
|
|
100
|
+
# ADR-87 WD1 — the boundary descriptor is validation-only (it never keys a cache), so a plugin-read
|
|
101
|
+
# file rides the stat-then-digest `:stat` tier: a warm run stat-checks the (on a Rails monorepo,
|
|
102
|
+
# 30k+ file) plugin-read set instead of re-hashing it. The digest is taken from the in-memory content
|
|
103
|
+
# we just read; `FileEntry.stat` stats the same path to pack the tuple.
|
|
100
104
|
digest = Digest::SHA256.hexdigest(contents)
|
|
101
|
-
entry = Cache::Descriptor::FileEntry.
|
|
105
|
+
entry = Cache::Descriptor::FileEntry.stat(path: path, digest: digest)
|
|
102
106
|
@mutex.synchronize { @file_entries[path] = entry }
|
|
103
107
|
end
|
|
104
108
|
|
|
@@ -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
|