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
|
@@ -8,6 +8,14 @@ module Rigor
|
|
|
8
8
|
# docs/type-specification/inference-budgets.md is not yet wired); counting how often each fires on a real
|
|
9
9
|
# project is the only way to see where inference actually stops.
|
|
10
10
|
#
|
|
11
|
+
# A second family of counters (the `MEMO_*` categories) profiles the ADR-57 user-method return memo
|
|
12
|
+
# (`ExpressionTyper#infer_user_method_return`) rather than a cutoff — inference entries, memo hits/misses,
|
|
13
|
+
# the split of non-stored results by reason, and body-evaluation (compute) entries — plus two
|
|
14
|
+
# per-signature distributions (body-eval count and distinct-memo-key count). This is the evidence for the
|
|
15
|
+
# next return-memo design slice: a finalization-aware taint gate (recover the `consult-tainted`
|
|
16
|
+
# non-stores) vs memo-key normalization (collapse arg-granularity thrash — a signature whose distinct-key
|
|
17
|
+
# count far exceeds its body-eval savings).
|
|
18
|
+
#
|
|
11
19
|
# Three categories, one per guard site:
|
|
12
20
|
#
|
|
13
21
|
# - {RECURSION_GUARD} — `ExpressionTyper#infer_user_method_return` detected a `(receiver, method)` cycle
|
|
@@ -44,9 +52,42 @@ module Rigor
|
|
|
44
52
|
# `Dynamic[top]` (the escaping-block floor). Shared by slice B's loop-body fixpoint.
|
|
45
53
|
BLOCK_WRITEBACK_CAP = :block_writeback_cap
|
|
46
54
|
|
|
55
|
+
# ADR-57 return-memo profile counters (not cutoffs — see the module doc). All bumped by
|
|
56
|
+
# `ExpressionTyper#infer_user_method_return` / `#compute_user_method_return`.
|
|
57
|
+
# - {MEMO_ENTRIES} — every `infer_user_method_return` entry (a user-method return inference).
|
|
58
|
+
# - {MEMO_HITS} / {MEMO_MISSES} — the memo was consulted (candidate frame) and the key was present /
|
|
59
|
+
# absent. Consults = hits + misses.
|
|
60
|
+
# - {MEMO_BODY_EVALS} — every `compute_user_method_return` entry (a body-evaluation compute; some are
|
|
61
|
+
# in-cycle re-entries that consult an ADR-55 summary rather than walk the body — those coincide with a
|
|
62
|
+
# `RECURSION_GUARD` hit). Body evals = misses + on-stack refusals (ADR-84 WD3 reduced candidacy to the
|
|
63
|
+
# on-stack check, so unroll-in-flight refusals are structurally gone).
|
|
64
|
+
# - {MEMO_REFUSE_ON_STACK} — the frame was not a memo candidate (bypassed the memo and computed): its
|
|
65
|
+
# plain `(receiver, method)` signature was already on the recursion guard stack.
|
|
66
|
+
# - {MEMO_REFUSE_UNROLL} — pre-ADR-84 blanket refusal (constant-arg unroll in flight). Kept in the table
|
|
67
|
+
# as the ADR-84 WD3 gate pin: structurally unreachable, must read ~0.
|
|
68
|
+
# - {MEMO_REFUSE_CONSULT_TAINTED} — a candidate frame computed a result but an ADR-55 fixpoint summary
|
|
69
|
+
# was *consulted* during the compute, so the result is a transient Kleene iterate and was NOT stored.
|
|
70
|
+
# - {MEMO_REFUSE_TRANSIENT} — ADR-84 WD3: a candidate frame's compute bracket saw a
|
|
71
|
+
# transient-machinery event (recursion-guard hit / unroll-fuel exhaustion / ADR-55 WD1 clamp /
|
|
72
|
+
# fixpoint-cap collapse — `ExpressionTyper#note_transient_fallback`) that referenced a stack frame
|
|
73
|
+
# BELOW the bracket's entry depth, so the ancestor context influenced the result and it was NOT
|
|
74
|
+
# stored. Own-machinery events (a recursive method's own fixpoint) do not refuse; top-of-stack
|
|
75
|
+
# computes are standalone by construction. Replaces the blanket unroll-in-flight exclusion.
|
|
76
|
+
MEMO_ENTRIES = :memo_entries
|
|
77
|
+
MEMO_HITS = :memo_hits
|
|
78
|
+
MEMO_MISSES = :memo_misses
|
|
79
|
+
MEMO_BODY_EVALS = :memo_body_evals
|
|
80
|
+
MEMO_REFUSE_ON_STACK = :memo_refuse_on_stack
|
|
81
|
+
MEMO_REFUSE_UNROLL = :memo_refuse_unroll
|
|
82
|
+
MEMO_REFUSE_CONSULT_TAINTED = :memo_refuse_consult_tainted
|
|
83
|
+
MEMO_REFUSE_TRANSIENT = :memo_refuse_transient
|
|
84
|
+
|
|
47
85
|
CATEGORIES = [
|
|
48
86
|
RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED, RECURSION_UNROLL_FUEL,
|
|
49
|
-
RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP
|
|
87
|
+
RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP,
|
|
88
|
+
MEMO_ENTRIES, MEMO_HITS, MEMO_MISSES, MEMO_BODY_EVALS,
|
|
89
|
+
MEMO_REFUSE_ON_STACK, MEMO_REFUSE_UNROLL, MEMO_REFUSE_CONSULT_TAINTED,
|
|
90
|
+
MEMO_REFUSE_TRANSIENT
|
|
50
91
|
].freeze
|
|
51
92
|
|
|
52
93
|
# Distribution (histogram) categories — read-only observations of a value's size at a site, used to
|
|
@@ -55,12 +96,28 @@ module Rigor
|
|
|
55
96
|
# `Combinator.union` produces — the distribution the `union_size` budget default should be set from.
|
|
56
97
|
UNION_ARITY = :union_arity
|
|
57
98
|
|
|
58
|
-
|
|
99
|
+
# ADR-57 return-memo per-signature distributions — `{signature => count}` maps, NOT integer-size
|
|
100
|
+
# histograms, so `summarize` (percentiles over integer values) does not apply; read them with
|
|
101
|
+
# {distribution}. Both keyed by the `"Receiver#method"` plain signature.
|
|
102
|
+
# - {MEMO_BODY_EVAL_BY_SIGNATURE} — body-eval (compute) count per signature (via {observe}).
|
|
103
|
+
# - {MEMO_DISTINCT_KEY_BY_SIGNATURE} — distinct memo keys per signature (via {observe_distinct}), i.e.
|
|
104
|
+
# how many `(receiver, arg-type)` variants a signature was memoised under. A count far above the
|
|
105
|
+
# signature's body-eval savings is arg-granularity thrash — the key-normalization candidate.
|
|
106
|
+
MEMO_BODY_EVAL_BY_SIGNATURE = :memo_body_eval_by_signature
|
|
107
|
+
MEMO_DISTINCT_KEY_BY_SIGNATURE = :memo_distinct_key_by_signature
|
|
108
|
+
|
|
109
|
+
DISTRIBUTION_CATEGORIES = [
|
|
110
|
+
UNION_ARITY, MEMO_BODY_EVAL_BY_SIGNATURE, MEMO_DISTINCT_KEY_BY_SIGNATURE
|
|
111
|
+
].freeze
|
|
59
112
|
|
|
60
113
|
@enabled = !ENV["RIGOR_BUDGET_TRACE"].to_s.empty?
|
|
61
114
|
@mutex = Mutex.new
|
|
62
115
|
@counts = Hash.new(0)
|
|
63
116
|
@distributions = Hash.new { |h, k| h[k] = Hash.new(0) }
|
|
117
|
+
# Auxiliary de-duplication state for {observe_distinct}: category → Set of the `[bucket, member]`
|
|
118
|
+
# pairs already counted, so a repeated `(signature, memo-key)` observation is counted once. Only ever
|
|
119
|
+
# written when tracing is enabled; not part of the reported surface.
|
|
120
|
+
@distinct_seen = Hash.new { |h, k| h[k] = Set.new }
|
|
64
121
|
|
|
65
122
|
module_function
|
|
66
123
|
|
|
@@ -101,6 +158,23 @@ module Rigor
|
|
|
101
158
|
@mutex.synchronize { @distributions[category][value] += 1 }
|
|
102
159
|
end
|
|
103
160
|
|
|
161
|
+
# Records one observation of `member` into `category`'s `{bucket => count}` map, but only the FIRST time
|
|
162
|
+
# a given `(bucket, member)` pair is seen — so the resulting count per bucket is the number of DISTINCT
|
|
163
|
+
# members, not the number of observations. Used for the distinct-memo-key-per-signature distribution.
|
|
164
|
+
# No-op (one boolean check) when disabled.
|
|
165
|
+
def observe_distinct(category, bucket, member)
|
|
166
|
+
return unless @enabled
|
|
167
|
+
|
|
168
|
+
@mutex.synchronize do
|
|
169
|
+
seen = @distinct_seen[category]
|
|
170
|
+
pair = [bucket, member]
|
|
171
|
+
unless seen.include?(pair)
|
|
172
|
+
seen << pair
|
|
173
|
+
@distributions[category][bucket] += 1
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
104
178
|
# Frozen `{value => count}` histogram for a distribution category.
|
|
105
179
|
def distribution(category)
|
|
106
180
|
@mutex.synchronize { @distributions[category].dup.freeze }
|
|
@@ -137,6 +211,7 @@ module Rigor
|
|
|
137
211
|
@mutex.synchronize do
|
|
138
212
|
@counts.clear
|
|
139
213
|
@distributions.clear
|
|
214
|
+
@distinct_seen.clear
|
|
140
215
|
end
|
|
141
216
|
end
|
|
142
217
|
end
|
|
@@ -144,6 +144,16 @@ module Rigor
|
|
|
144
144
|
RANGE_EXTRA = %i[step].freeze
|
|
145
145
|
INTEGER_EXTRA = %i[times upto downto].freeze
|
|
146
146
|
|
|
147
|
+
# IO / File / StringIO line- and byte-iteration methods invoke the block immediately, once per line /
|
|
148
|
+
# byte / char, and never retain it — the same immediate-invocation contract as `Array#each`. Both the
|
|
149
|
+
# singleton `File.foreach` / `IO.foreach` (receiver `singleton(File)` → class name "File") and the
|
|
150
|
+
# instance `io.each_line` / `each_char` … forms resolve through the same class-name key. Without these,
|
|
151
|
+
# `File.foreach(path) { case … when … then flag = true when … then return true if flag end }` classifies
|
|
152
|
+
# `:unknown` and misses the loop-body re-narrowing, so a local written in one `when` arm reads its
|
|
153
|
+
# pre-loop value in a sibling arm and a guarding condition folds to a spurious constant.
|
|
154
|
+
IO_ITERATION = %i[each_line each each_byte each_char each_codepoint].freeze
|
|
155
|
+
IO_SINGLETON_ITERATION = %i[foreach].freeze
|
|
156
|
+
|
|
147
157
|
NON_ESCAPING = {
|
|
148
158
|
"Array" => (ENUMERABLE_NON_ESCAPING + ARRAY_EXTRA).freeze,
|
|
149
159
|
"Hash" => (ENUMERABLE_NON_ESCAPING + HASH_EXTRA).freeze,
|
|
@@ -151,7 +161,10 @@ module Rigor
|
|
|
151
161
|
"Set" => ENUMERABLE_NON_ESCAPING,
|
|
152
162
|
"Integer" => INTEGER_EXTRA,
|
|
153
163
|
"Enumerator" => ENUMERABLE_NON_ESCAPING,
|
|
154
|
-
"Enumerator::Lazy" => ENUMERABLE_NON_ESCAPING
|
|
164
|
+
"Enumerator::Lazy" => ENUMERABLE_NON_ESCAPING,
|
|
165
|
+
"IO" => (IO_ITERATION + IO_SINGLETON_ITERATION).freeze,
|
|
166
|
+
"File" => (IO_ITERATION + IO_SINGLETON_ITERATION).freeze,
|
|
167
|
+
"StringIO" => IO_ITERATION
|
|
155
168
|
}.freeze
|
|
156
169
|
|
|
157
170
|
# Methods that are documented to **retain** the block past the call. The block is stored or scheduled,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# ADR-85 WD3 — a lazy reference to a `Prism::DefNode` stored in a cross-file discovery table, in place of
|
|
6
|
+
# the live node itself. When an incremental recheck rebuilds the discovery index from cached per-file seed
|
|
7
|
+
# bundles (ADR-85 WD2), the `def_nodes` / `singleton_def_nodes` tables carry handles for every unchanged
|
|
8
|
+
# file's methods: the file was never parsed this run, so there is no live node to store. A handle records
|
|
9
|
+
# the three things needed to resolve or fingerprint it without the node:
|
|
10
|
+
#
|
|
11
|
+
# - `path` + `node_id` — the resolution key. The per-run parse memo ({Scope#resolve_def_handle}) parses the
|
|
12
|
+
# file once and returns the `Prism::DefNode` whose `node_id` matches (Prism `node_id` is stable across
|
|
13
|
+
# repeated parses of identical bytes). `name` is the cross-check: a resolved node whose `name` differs
|
|
14
|
+
# from the handle's forces a fresh walk (bundle/source skew that the digest gate should make impossible —
|
|
15
|
+
# conservative, never silent).
|
|
16
|
+
# - `fingerprint` — the SHA-256 of the def's source slice, captured when the bundle was built. This lets
|
|
17
|
+
# `Runner#symbol_fingerprints` read the change-detection fingerprint (ADR-46 slice 4) off the handle
|
|
18
|
+
# without re-parsing the file — the one value-deref consumer besides the three accessor choke points.
|
|
19
|
+
#
|
|
20
|
+
# Marshal-clean by construction (Integer + Strings), so it rides the `IncrementalSnapshot` blob directly.
|
|
21
|
+
DefHandle = Data.define(:path, :node_id, :name, :fingerprint)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "def_handle"
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# ADR-85 WD3 — resolves a {DefHandle} (stored in a bundle-rebuilt discovery table) to a live
|
|
10
|
+
# `Prism::DefNode`, through a per-run parse memo that yields ONE stable node object per `(path, node_id)`
|
|
11
|
+
# per run. Identity stability is a correctness constraint, not an optimisation: the ADR-84 return memo keys
|
|
12
|
+
# on the resolved def-node's object identity, so a fresh parse per resolution would mint a new identity
|
|
13
|
+
# each time and fragment that memo. The memo parses each demanded file at most once (recon Q5: an
|
|
14
|
+
# incremental recheck demands 0–6 files' bodies), builds a `node_id → DefNode` index over its DefNodes, and
|
|
15
|
+
# returns the same object on every later lookup.
|
|
16
|
+
#
|
|
17
|
+
# `node_id` is the primary key; `name` is a cross-check. A miss (the node_id is absent, or its node's name
|
|
18
|
+
# differs — a bundle/source skew the digest gate should make impossible, since identical bytes under the
|
|
19
|
+
# same Prism yield identical node_ids) falls back to the first same-named DefNode in a fresh walk of the
|
|
20
|
+
# file, and finally nil — conservative (the accessor then reports no user def, losing precision but never
|
|
21
|
+
# firing a wrong diagnostic), never silent.
|
|
22
|
+
#
|
|
23
|
+
# The memo is a thread-local installed for the duration of a run (the {Cache::FileDigest} pattern:
|
|
24
|
+
# process-local, Ractor-safe, never crosses a fork boundary, reset per run). Outside a run scope every
|
|
25
|
+
# resolution parses directly — a correct if un-memoised fallback for runner-less probes.
|
|
26
|
+
module DefNodeResolver
|
|
27
|
+
MEMO_KEY = :rigor_def_node_resolver_memo
|
|
28
|
+
private_constant :MEMO_KEY
|
|
29
|
+
|
|
30
|
+
# Installs a fresh per-run resolution memo, restoring the previous one on exit (always, even on a raise).
|
|
31
|
+
# `nodes` caches the resolved node per `(path, node_id)`; `indexes` caches each file's parse index.
|
|
32
|
+
def self.with_run
|
|
33
|
+
previous = Thread.current[MEMO_KEY]
|
|
34
|
+
Thread.current[MEMO_KEY] = { nodes: {}, indexes: {} }
|
|
35
|
+
yield
|
|
36
|
+
ensure
|
|
37
|
+
Thread.current[MEMO_KEY] = previous
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Resolves `handle` to a `Prism::DefNode` (the same object across the run for a given (path, node_id)), or
|
|
41
|
+
# nil. A non-handle argument is returned unchanged, so callers can pass a table value that is either a live
|
|
42
|
+
# node (cold / re-walked file) or a handle (unchanged file) without branching.
|
|
43
|
+
def self.resolve(handle)
|
|
44
|
+
return handle unless handle.is_a?(DefHandle)
|
|
45
|
+
|
|
46
|
+
memo = Thread.current[MEMO_KEY]
|
|
47
|
+
return locate(handle, {}) if memo.nil?
|
|
48
|
+
|
|
49
|
+
key = [handle.path, handle.node_id].freeze
|
|
50
|
+
nodes = memo[:nodes]
|
|
51
|
+
return nodes[key] if nodes.key?(key)
|
|
52
|
+
|
|
53
|
+
nodes[key] = locate(handle, memo[:indexes])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Finds the node for `handle` using a per-file `{node_id => DefNode}` + `{name => DefNode}` index cache.
|
|
57
|
+
def self.locate(handle, index_cache)
|
|
58
|
+
index = index_cache[handle.path] ||= build_file_index(handle.path)
|
|
59
|
+
node = index[:by_id][handle.node_id]
|
|
60
|
+
return node if node && node.name.to_s == handle.name
|
|
61
|
+
|
|
62
|
+
index[:by_name][handle.name]
|
|
63
|
+
rescue StandardError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
private_class_method :locate
|
|
67
|
+
|
|
68
|
+
def self.build_file_index(path)
|
|
69
|
+
root = Prism.parse(File.read(path)).value
|
|
70
|
+
by_id = {}
|
|
71
|
+
by_name = {}
|
|
72
|
+
collect_def_nodes(root, by_id, by_name)
|
|
73
|
+
{ by_id: by_id, by_name: by_name }
|
|
74
|
+
end
|
|
75
|
+
private_class_method :build_file_index
|
|
76
|
+
|
|
77
|
+
# Walks the tree once, indexing every DefNode by node_id (primary) and by name (first-wins fallback).
|
|
78
|
+
def self.collect_def_nodes(node, by_id, by_name)
|
|
79
|
+
return unless node.is_a?(Prism::Node)
|
|
80
|
+
|
|
81
|
+
if node.is_a?(Prism::DefNode)
|
|
82
|
+
by_id[node.node_id] = node
|
|
83
|
+
by_name[node.name.to_s] ||= node
|
|
84
|
+
end
|
|
85
|
+
node.rigor_each_child { |child| collect_def_nodes(child, by_id, by_name) }
|
|
86
|
+
end
|
|
87
|
+
private_class_method :collect_def_nodes
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
|
+
require_relative "../source/node_children"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Inference
|
|
@@ -62,7 +63,7 @@ module Rigor
|
|
|
62
63
|
return if RETURN_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
63
64
|
|
|
64
65
|
type_return_node(node, scope_index, out) if node.is_a?(Prism::ReturnNode)
|
|
65
|
-
node.
|
|
66
|
+
node.rigor_each_child { |c| collect_return_types(c, scope_index, out) }
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
def type_return_node(return_node, scope_index, out)
|