rigortype 0.4.7 → 0.4.8
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 +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "typing_state"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
# ADR-127 WD3 — the memo audit. A memo hit is correct only when the key holds every input the value
|
|
7
|
+
# was computed from, and a key that misses one answers a caller with another caller's value (#2516, #2524). With the
|
|
8
|
+
# audit on, every hit in the callee floor's memos (`resolved_census`, the unrecorded hand-off memo, the successor
|
|
9
|
+
# store and `def_census`) recomputes the value and raises {Mismatch} when it differs from the stored one, so a key
|
|
10
|
+
# defect fails on the first hit instead of waiting for an order that exposes it.
|
|
11
|
+
#
|
|
12
|
+
# Like {BudgetTrace}, a switch for specs and CI (`RIGOR_MEMO_AUDIT=1` in the environment, read once at load, or
|
|
13
|
+
# {enable!}): off by default, a single boolean check when off. A recomputation
|
|
14
|
+
# runs with the audit suspended on its thread and the memos bypassed ({suspended?}), so it reads nothing stored.
|
|
15
|
+
#
|
|
16
|
+
# A mismatch raises, and is also kept on this thread's list until {drain} takes it (ADR-129 WD5, #2785): the
|
|
17
|
+
# typing path rescues `StandardError` in many places, and a raise one of them swallows would turn the defect into
|
|
18
|
+
# an ordinary fallback answer. The runner drains the list after each file and reports what it holds the way it
|
|
19
|
+
# reports a mismatch no rescue caught, and {raise_undrained!} fails the run on what a file never drained.
|
|
20
|
+
#
|
|
21
|
+
# **The key audit** (ADR-129 WD1) checks the other half at every miss: a Memo, or a Table a computation fills (the
|
|
22
|
+
# ADR-84 return memo), whose miss computation reads a Frame must key on the state id or on that Frame's value.
|
|
23
|
+
# {key_audit} runs a miss computation with its Frame reads noted ({TypingState::KeyAudit}) and compares them with
|
|
24
|
+
# the Frame terms the key read through {TypingState::State#key_frame} when {key} built it. A read Frame the key
|
|
25
|
+
# does not carry, and that neither the computation installed itself nor the site declared ({key_declaration}), is a
|
|
26
|
+
# {KeyMismatch}: kept on this thread's list like a raised mismatch, and not raised, since the computation's answer
|
|
27
|
+
# is correct for this caller.
|
|
28
|
+
module MemoAudit
|
|
29
|
+
# A memo hit whose stored value differs from a fresh computation.
|
|
30
|
+
class Mismatch < StandardError; end
|
|
31
|
+
|
|
32
|
+
# A memo miss whose computation read a Frame its key does not carry (ADR-129 WD1's key audit).
|
|
33
|
+
class KeyMismatch < Mismatch; end
|
|
34
|
+
|
|
35
|
+
# What a miss site declares beside its key terms (ADR-129 WD1), by Frame slot name:
|
|
36
|
+
# - `forced`: Frames the memo installs at a constant (or a value derived from its key) before the key is built,
|
|
37
|
+
# so the computation reads that form rather than the caller's;
|
|
38
|
+
# - `gaps`: `slot name => reason` naming the issue of a known key gap, read without a key term and reported as a
|
|
39
|
+
# named exception rather than a mismatch;
|
|
40
|
+
# - `carried`: `slot name => reason` for Frames another key term carries (a run-level flag, fixed while the run
|
|
41
|
+
# generation the memo's bucket keys on is), counted as key terms.
|
|
42
|
+
KeyDeclaration = Data.define(:forced, :gaps, :carried)
|
|
43
|
+
|
|
44
|
+
# ADR-129 WD1 — a Frame, installed around a recomputation. `@enabled` is a process switch, outside the typing
|
|
45
|
+
# state (WD5). G1′ witness: a recomputation types a callee body, and the memo hits inside it read the flag.
|
|
46
|
+
SUSPENDED = TypingState.frame(
|
|
47
|
+
:memo_audit_suspended, reason: "set around an audit recomputation, which bypasses every memo",
|
|
48
|
+
witness: "spec/fixtures/typing_state_witnesses/memo_audit_suspended.rb"
|
|
49
|
+
)
|
|
50
|
+
private_constant :SUSPENDED
|
|
51
|
+
|
|
52
|
+
# ADR-129 WD1 — a Resource: the mismatches raised on this thread and not drained yet. Typing never reads it; an
|
|
53
|
+
# append refuses every open capture, which only an audited run reaches.
|
|
54
|
+
MISMATCHES = TypingState.resource(
|
|
55
|
+
:memo_audit_mismatches,
|
|
56
|
+
run_scoped: true,
|
|
57
|
+
reason: "the audit's mismatches on this thread, appended where raised, drained per file, never read by typing"
|
|
58
|
+
)
|
|
59
|
+
private_constant :MISMATCHES
|
|
60
|
+
|
|
61
|
+
EMPTY = [].freeze
|
|
62
|
+
private_constant :EMPTY
|
|
63
|
+
|
|
64
|
+
# ADR-129 WD1 — the Frames every key audit names as exceptions rather than mismatches, with their reasons: the
|
|
65
|
+
# audit's own switch and the process switches whose installed recorders a computation may file into.
|
|
66
|
+
KEY_AUDIT_SWITCHES = {
|
|
67
|
+
memo_audit_suspended: "the audit's own switch: a nested hit's check reads it",
|
|
68
|
+
flow_tracer: "a process switch (WD5): the tracer is an output, and the type_of memo is shut under one (WD4)",
|
|
69
|
+
effects_collector: "a process switch (WD5): recording into its contents touches a Resource",
|
|
70
|
+
dependency_recorder: "a process switch (WD5): the recorder files ADR-46 dependency edges, which no type reads",
|
|
71
|
+
self_call_resolution_recorder: "a process switch (WD5): its records are a journaled Table, never read back"
|
|
72
|
+
}.freeze
|
|
73
|
+
|
|
74
|
+
# ADR-129 WD1 — a Resource: the named exceptions key audits on this thread met, `[label, slot name] => reason`.
|
|
75
|
+
# Typing never reads it.
|
|
76
|
+
KEY_EXCEPTIONS = TypingState.resource(
|
|
77
|
+
:memo_audit_key_exceptions,
|
|
78
|
+
run_scoped: true,
|
|
79
|
+
reason: "the key audit's named exceptions met on this thread, for specs and reports; never read by typing"
|
|
80
|
+
)
|
|
81
|
+
private_constant :KEY_EXCEPTIONS
|
|
82
|
+
|
|
83
|
+
NO_DECLARATION = KeyDeclaration.new(forced: EMPTY, gaps: {}.freeze, carried: {}.freeze)
|
|
84
|
+
private_constant :NO_DECLARATION
|
|
85
|
+
|
|
86
|
+
@enabled = %w[1 true].include?(ENV["RIGOR_MEMO_AUDIT"].to_s.downcase)
|
|
87
|
+
|
|
88
|
+
module_function
|
|
89
|
+
|
|
90
|
+
def enabled? = @enabled
|
|
91
|
+
|
|
92
|
+
# True while an audit recomputation runs on this thread. The callee floor's memos are then bypassed (read and
|
|
93
|
+
# write), so the recomputation reads no stored entry of another memo: a defect shared across a table (a stale
|
|
94
|
+
# `resolved_census(:edges)` entry) would otherwise make the recomputed value match the stored one.
|
|
95
|
+
def suspended? = TypingState.current[SUSPENDED]
|
|
96
|
+
|
|
97
|
+
def enable! = (@enabled = true)
|
|
98
|
+
|
|
99
|
+
def disable! = (@enabled = false)
|
|
100
|
+
|
|
101
|
+
# `stored`, after checking it against `yield` when the audit is on. `label` and the key parts (`key`, `key2`) name
|
|
102
|
+
# the memo entry in the failure; they are passed apart so an audit that is off allocates nothing.
|
|
103
|
+
def check(label, stored, key, key2 = nil, &)
|
|
104
|
+
return stored unless @enabled && !TypingState.current[SUSPENDED]
|
|
105
|
+
|
|
106
|
+
fresh = suspended(&)
|
|
107
|
+
return stored if fresh == stored
|
|
108
|
+
|
|
109
|
+
mismatch = Mismatch.new(
|
|
110
|
+
"#{label} memo hit differs from a fresh computation for #{[key, key2].inspect[0, 120]}: " \
|
|
111
|
+
"stored #{stored.inspect[0, 200]}, fresh #{fresh.inspect[0, 200]}"
|
|
112
|
+
)
|
|
113
|
+
state = TypingState.current
|
|
114
|
+
state.append(MISMATCHES, mismatch)
|
|
115
|
+
raise mismatch
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# ADR-129 G1′ — a `type_of` begins on this thread, for the matching {leave_type_of}; the caller guards on
|
|
119
|
+
# {enabled?}, so an audit that is off costs one boolean check. Fails (as a {Mismatch} on the list, not raised) on an
|
|
120
|
+
# installed `typing_unreachable` Frame ({TypingState::FrameAudit}).
|
|
121
|
+
def enter_type_of
|
|
122
|
+
state = TypingState.current
|
|
123
|
+
state.extend(TypingState::FrameAudit) unless state.is_a?(TypingState::FrameAudit)
|
|
124
|
+
state.enter_type_of
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# ADR-129 G1′ — the `type_of` memo's shadow reached its table on `state`; the caller guards on {enabled?}. Fails (as
|
|
128
|
+
# a {Mismatch} on the list, not raised) on an installed `memo_shut_while_installed` Frame.
|
|
129
|
+
def note_memo_lookup(state)
|
|
130
|
+
state.extend(TypingState::FrameAudit) unless state.is_a?(TypingState::FrameAudit)
|
|
131
|
+
state.note_memo_lookup
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# ADR-129 G1′ — the `type_of` {enter_type_of} tracked ends.
|
|
135
|
+
def leave_type_of
|
|
136
|
+
TypingState.current.leave_type_of
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Keeps one {Mismatch} per message on the list until it is drained: a Frame declaration that does not hold is
|
|
140
|
+
# reported at every `type_of` it meets.
|
|
141
|
+
def report_frame_declaration(state, message)
|
|
142
|
+
pending = state.read(MISMATCHES)
|
|
143
|
+
return if pending&.any? { |mismatch| mismatch.message == message }
|
|
144
|
+
|
|
145
|
+
mismatch = Mismatch.new(message)
|
|
146
|
+
mismatch.set_backtrace(caller.drop_while { |frame| frame.include?("/typing_state.rb:") })
|
|
147
|
+
state.append(MISMATCHES, mismatch)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# The mismatches raised on this thread since the last drain, oldest first, and an empty list for the next file.
|
|
151
|
+
# Each has been raised, so it carries the backtrace a crash row is built from.
|
|
152
|
+
def drain
|
|
153
|
+
state = TypingState.current
|
|
154
|
+
mismatches = state.read(MISMATCHES)
|
|
155
|
+
return EMPTY if mismatches.nil? || mismatches.empty?
|
|
156
|
+
|
|
157
|
+
state.write(MISMATCHES, nil)
|
|
158
|
+
mismatches
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# ADR-129 WD1 — checks a `carried:` claim at the install of a run-level flag (`flag`, ADR-130's erasure or
|
|
162
|
+
# dispatch-off frame). The return memo counts the flag as carried by its bucket's run-generation key, which holds
|
|
163
|
+
# only while the flag is installed around a whole run: with the audit on, an install that finds the return memo
|
|
164
|
+
# holding entries (a run in progress, or one that was not released) raises instead of silently mixing answers
|
|
165
|
+
# computed under both values.
|
|
166
|
+
def check_run_level_install(flag)
|
|
167
|
+
return unless @enabled
|
|
168
|
+
|
|
169
|
+
held = TypingState.current[TypingState.slot(:user_method_return_memo).index]
|
|
170
|
+
return if held.nil? || held[1].empty?
|
|
171
|
+
|
|
172
|
+
raise Mismatch, "#{flag} installed while the return memo holds entries: the memo's key carries it only as a " \
|
|
173
|
+
"flag installed around a whole run (ADR-129 WD1 key audit)"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Raises the first mismatch no file drained (one raised outside every file's analysis), so a run that ends with
|
|
177
|
+
# one fails instead of releasing it with the run's other slots.
|
|
178
|
+
def raise_undrained!
|
|
179
|
+
return unless @enabled
|
|
180
|
+
|
|
181
|
+
mismatches = drain
|
|
182
|
+
raise mismatches.first unless mismatches.empty?
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def suspended(&)
|
|
186
|
+
TypingState.current.with(SUSPENDED, true, &)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# A frozen {KeyDeclaration} for a miss site, built once into a constant so an audit that is off allocates nothing.
|
|
190
|
+
def key_declaration(forced: EMPTY, gaps: {}, carried: {})
|
|
191
|
+
KeyDeclaration.new(forced: forced.map(&:to_sym).freeze, gaps: gaps.transform_keys(&:to_sym).freeze,
|
|
192
|
+
carried: carried.transform_keys(&:to_sym).freeze)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# The key the block builds. When the audit is on, the Frames the block reads through
|
|
196
|
+
# {TypingState::State#key_frame} are remembered as the key's terms for the {key_audit} of the miss that follows.
|
|
197
|
+
def key(&)
|
|
198
|
+
return yield unless @enabled
|
|
199
|
+
|
|
200
|
+
auditing_state.collect_key_terms(&)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# The block's value: a memo's miss computation, run under ADR-129 WD1's Memo rule
|
|
204
|
+
# ({TypingState::State#memo_miss}: a miss that writes a Table while a capture is open touches), keyed by `key`
|
|
205
|
+
# ({key}, or nil for a key that reads no Frame) and declared by `declaration` ({key_declaration}). When the audit
|
|
206
|
+
# is on, a Frame the computation reads that the key does not carry is a {KeyMismatch} on this thread's list, named
|
|
207
|
+
# by `label` (the slot's `audit:` or `computed:` label), unless it is internal, forced, carried, a declared gap or
|
|
208
|
+
# a {KEY_AUDIT_SWITCHES} switch.
|
|
209
|
+
#
|
|
210
|
+
# A computation that raises or throws is not judged, and its audit is closed on the way out. A computation that
|
|
211
|
+
# returns is judged whether or not its memo then stores it (the return memo's WD3 bracket may refuse it), which
|
|
212
|
+
# only reports more. An audit recomputation ({suspended?}) is not judged either, but it still opens the site's
|
|
213
|
+
# declaration, so a gap it declares reaches the audits around it as it does on a miss: the verdict of an enclosing
|
|
214
|
+
# miss does not depend on whether this memo hit.
|
|
215
|
+
def key_audit(label, declaration = nil, key = nil, &)
|
|
216
|
+
return TypingState.current.memo_miss(&) unless @enabled
|
|
217
|
+
|
|
218
|
+
state = auditing_state
|
|
219
|
+
declaration ||= NO_DECLARATION
|
|
220
|
+
judged = !state[SUSPENDED]
|
|
221
|
+
terms = state.key_terms_for(key) if judged
|
|
222
|
+
state.open_key_audit(label, declaration.gaps)
|
|
223
|
+
begin
|
|
224
|
+
value = state.memo_miss(&)
|
|
225
|
+
ensure
|
|
226
|
+
reads = state.close_key_audit
|
|
227
|
+
end
|
|
228
|
+
judge_key_audit(state, label, declaration, terms, reads) if judged
|
|
229
|
+
value
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# The named exceptions key audits on this thread have met since the run began, `[label, slot name] => reason`.
|
|
233
|
+
def key_exceptions
|
|
234
|
+
TypingState.current.read(KEY_EXCEPTIONS) || {}
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def auditing_state
|
|
238
|
+
state = TypingState.current
|
|
239
|
+
state.extend(TypingState::KeyAudit) unless state.is_a?(TypingState::KeyAudit)
|
|
240
|
+
state
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def judge_key_audit(state, label, declaration, terms, reads)
|
|
244
|
+
return if reads.empty? || terms.include?(TypingState::KeyAudit::STATE_ID)
|
|
245
|
+
|
|
246
|
+
reads.each do |slot, via|
|
|
247
|
+
name = TypingState.slots[slot].name
|
|
248
|
+
next note_key_exception(state, label, name, "through the #{via[0]} miss: #{via[1]}") unless via == true
|
|
249
|
+
next if terms.include?(slot) || declaration.forced.include?(name) || declaration.carried.key?(name)
|
|
250
|
+
|
|
251
|
+
reason = key_exception_reason(declaration, name)
|
|
252
|
+
reason ? note_key_exception(state, label, name, reason) : report_key_mismatch(state, label, name)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Why a miss may read the Frame `name` without a key term, or nil: a gap its site declares, or a switch.
|
|
257
|
+
def key_exception_reason(declaration, name) = declaration.gaps[name] || KEY_AUDIT_SWITCHES[name]
|
|
258
|
+
|
|
259
|
+
def note_key_exception(state, label, name, reason)
|
|
260
|
+
seen = state.acquire(KEY_EXCEPTIONS) { {} }
|
|
261
|
+
seen[[label, name]] ||= reason
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Keeps one {KeyMismatch} per message on the list until it is drained: a key defect repeats at every miss.
|
|
265
|
+
def report_key_mismatch(state, label, name)
|
|
266
|
+
message = "key audit: the #{label} miss read the Frame #{name}, which its key does not carry"
|
|
267
|
+
pending = state.read(MISMATCHES)
|
|
268
|
+
return if pending&.any? { |mismatch| mismatch.message == message }
|
|
269
|
+
|
|
270
|
+
mismatch = KeyMismatch.new(message)
|
|
271
|
+
mismatch.set_backtrace(caller.drop_while { |frame| frame.include?("/memo_audit.rb:") })
|
|
272
|
+
state.append(MISMATCHES, mismatch)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# The engine reaches the audit as `Inference::MemoAudit`; it lives at `lib/rigor/memo_audit.rb` so the value layer
|
|
277
|
+
# (`Reflection`, `Scope`) can load it without loading an `inference/` file (ADR-104's warm effects probe budget), as
|
|
278
|
+
# `TypingState` does (#2796).
|
|
279
|
+
module Inference
|
|
280
|
+
MemoAudit = Rigor::MemoAudit
|
|
281
|
+
end
|
|
282
|
+
end
|
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -945,6 +945,14 @@ module Rigor
|
|
|
945
945
|
@io_boundary ||= services.io_boundary_for(manifest.id)
|
|
946
946
|
end
|
|
947
947
|
|
|
948
|
+
# The boundary {#io_boundary} has built, or nil when this plugin never asked for one, without building it: a
|
|
949
|
+
# plugin that never built one read nothing through it. Engine-internal ({Analysis::RunDisclosureCredit} sends it,
|
|
950
|
+
# being outside the class), so it is private and part of no plugin-facing contract.
|
|
951
|
+
#
|
|
952
|
+
# @api private
|
|
953
|
+
def built_io_boundary = @io_boundary
|
|
954
|
+
private :built_io_boundary
|
|
955
|
+
|
|
948
956
|
# ADR-7 § "Slice 6-A" / ADR-60 WD3 — returns a callable that performs a `Cache::Store#fetch_or_validate`
|
|
949
957
|
# round-trip for the named producer (the ADR-45 record-and-validate path). The entry is KEYED on the
|
|
950
958
|
# stable identity inputs — the plugin's `PluginEntry` template (id, version, config_hash) composed with
|
|
@@ -1064,14 +1072,19 @@ module Rigor
|
|
|
1064
1072
|
# The gate for one `dynamic_return` rule. Method-name gate first — a Symbol-array probe vs the receiver
|
|
1065
1073
|
# ancestry resolution below (ADR-52 WD1); both are pure predicates, so order only affects cost. A
|
|
1066
1074
|
# receiver-less rule (ADR-52 WD2) skips the ancestry check entirely and fires on the method name alone.
|
|
1075
|
+
# The engine also offers the nameless nodes of #2717 (the `&:sym` BlockArgumentNode, the compound
|
|
1076
|
+
# write nodes) to a plugin `build_name_gates` left ungated; only `CallNode` answers `name`, so a
|
|
1077
|
+
# name-gated rule declines them (`include?(nil)` is false) rather than raising past the later rules
|
|
1078
|
+
# (#2764).
|
|
1067
1079
|
def dynamic_return_rule_applies?(rule, call_node, receiver_type, environment, scope)
|
|
1068
|
-
|
|
1080
|
+
name = call_node.respond_to?(:name) ? call_node.name : nil
|
|
1081
|
+
return false if rule[:methods] && !resolved_dynamic_return_methods(rule).include?(name)
|
|
1069
1082
|
|
|
1070
1083
|
if rule[:file_methods]
|
|
1071
1084
|
# The path is read here, not in `dynamic_return_type`, so a spec-double scope without
|
|
1072
1085
|
# `source_path` only affects `file_methods:` rules (other gate forms never touch it).
|
|
1073
1086
|
path = scope.respond_to?(:source_path) ? scope.source_path : nil
|
|
1074
|
-
return false unless resolved_dynamic_return_file_methods(rule, path).include?(
|
|
1087
|
+
return false unless resolved_dynamic_return_file_methods(rule, path).include?(name)
|
|
1075
1088
|
end
|
|
1076
1089
|
|
|
1077
1090
|
entries = resolved_dynamic_return_receiver_entries(rule)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
|
+
require_relative "../typing_state"
|
|
4
5
|
|
|
5
6
|
require_relative "access_denied_error"
|
|
6
7
|
require_relative "../rooted_glob"
|
|
@@ -52,11 +53,20 @@ module Rigor
|
|
|
52
53
|
# descriptor and any enclosing producer's descriptor then carry the served value's inputs.
|
|
53
54
|
class IoBoundary
|
|
54
55
|
URL_TIMEOUT_SECONDS = 10
|
|
55
|
-
URL_MAX_BYTES =
|
|
56
|
+
URL_MAX_BYTES = 10_485_760 # 10 MiB
|
|
56
57
|
|
|
57
58
|
ATTRIBUTION_KEY = :rigor_io_boundary_attribution
|
|
58
59
|
private_constant :ATTRIBUTION_KEY
|
|
59
60
|
|
|
61
|
+
# ADR-129 WD3 / #2814 — the attribution sink is a Resource: a `type_of` memo hit that served an answer without
|
|
62
|
+
# re-running the plugin read would not append what the miss appended, so every {.attribute} refuses each open
|
|
63
|
+
# capture. Its storage stays the thread variable above (ADR-129 WD5 shape 2); this slot declares its kind.
|
|
64
|
+
SINK = TypingState.resource(
|
|
65
|
+
:io_boundary_attribution,
|
|
66
|
+
reason: "every plugin boundary read appends to the attributing sink, which a memo hit would not replay"
|
|
67
|
+
)
|
|
68
|
+
private_constant :SINK
|
|
69
|
+
|
|
60
70
|
# ADR-45 WD2 (#1507) — runs the block with `sink` (an Array) receiving every file and listing row ANY
|
|
61
71
|
# boundary records on this thread meanwhile, in addition to the boundary's own table, and returns the
|
|
62
72
|
# block's value. `Analysis::Runner` wraps each file's analysis in one when it records dependencies, so the
|
|
@@ -67,16 +77,28 @@ module Rigor
|
|
|
67
77
|
# lands in the sink. The boundary's own table is unchanged by it, including its first-existence-row-stands
|
|
68
78
|
# rule: the sink records what each read observed. Only reads are credited: a value a plugin memoised reaches
|
|
69
79
|
# later files without one, so the first file to trigger the read is the only one credited with it.
|
|
80
|
+
#
|
|
81
|
+
# The installing fiber's typing state rides with the sink, so a read from inside an `Enumerator` refuses the
|
|
82
|
+
# captures open where the analysis runs, not only those of the `Enumerator`'s own fiber ({.attribute}).
|
|
70
83
|
def self.attributing(sink)
|
|
71
84
|
previous = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
|
|
72
|
-
Thread.current.thread_variable_set(ATTRIBUTION_KEY, sink)
|
|
85
|
+
Thread.current.thread_variable_set(ATTRIBUTION_KEY, [sink, TypingState.current].freeze)
|
|
73
86
|
yield
|
|
74
87
|
ensure
|
|
75
88
|
Thread.current.thread_variable_set(ATTRIBUTION_KEY, previous)
|
|
76
89
|
end
|
|
77
90
|
|
|
91
|
+
# Records `entry` in the attributing sink, if one is installed, and touches {SINK} on the calling fiber's typing
|
|
92
|
+
# state and on the installing fiber's (ADR-129 WD3, #2814).
|
|
78
93
|
def self.attribute(entry)
|
|
79
|
-
|
|
94
|
+
state = TypingState.current
|
|
95
|
+
state.touch(SINK)
|
|
96
|
+
sink, owner = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
|
|
97
|
+
return nil if sink.nil?
|
|
98
|
+
|
|
99
|
+
owner.touch(SINK)
|
|
100
|
+
sink.push(entry)
|
|
101
|
+
nil
|
|
80
102
|
end
|
|
81
103
|
|
|
82
104
|
attr_reader :policy, :plugin_id
|
|
@@ -304,11 +304,15 @@ module Rigor
|
|
|
304
304
|
FirstParty.bundled?(id)
|
|
305
305
|
end
|
|
306
306
|
|
|
307
|
-
# Whether the
|
|
308
|
-
#
|
|
307
|
+
# Whether the manifest declares anything for the effect surfaces — a convenience predicate over the
|
|
308
|
+
# manifest-declared fields only, true exactly when a `Registry::Contribution` built from these
|
|
309
|
+
# declarations would not be empty (`Contribution#empty?` reads the same five fields,
|
|
310
|
+
# `effect_ancestry:` included). A plugin that overrides the `Base#effect_*` readers instead of
|
|
311
|
+
# declaring rows here (rigor-sidekiq) can contribute while this answers false. The compiled
|
|
312
|
+
# per-run tables filter on `Contribution#empty?` themselves; nothing consults this predicate.
|
|
309
313
|
def effects?
|
|
310
314
|
!effect_labels.empty? || !effect_attributions.empty? || !effect_edges.empty? ||
|
|
311
|
-
!effect_entry_points.empty?
|
|
315
|
+
!effect_entry_points.empty? || !effect_ancestry.empty?
|
|
312
316
|
end
|
|
313
317
|
|
|
314
318
|
def ==(other)
|
|
@@ -4,6 +4,7 @@ require_relative "blueprint"
|
|
|
4
4
|
# Issue #806 — {Registry#guarded_manifest} constructs one. `Rigor::Plugin::LoadError` MUST be defined by
|
|
5
5
|
# then: an unresolved bare `LoadError` in this lexical scope silently falls back to Ruby's own ::LoadError.
|
|
6
6
|
require_relative "load_error"
|
|
7
|
+
require_relative "../typing_state"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Plugin
|
|
@@ -20,6 +21,10 @@ module Rigor
|
|
|
20
21
|
# exactly the order the ungated walk produced — diagnostics stay byte-identical. The receiver-class
|
|
21
22
|
# ancestry match for `dynamic_return` still happens per-dispatch inside `Plugin::Base#dynamic_return_type`.
|
|
22
23
|
class ContributionIndex
|
|
24
|
+
TypingState.memo_field(self, :@owns_receiver_memo,
|
|
25
|
+
reason: "keyed on the environment's identity and the class name; the plugins' " \
|
|
26
|
+
"owns_receivers set is fixed at construction")
|
|
27
|
+
|
|
23
28
|
# Ordered (registry-order) subsets relevant to each collector, and the membership sets used to gate the
|
|
24
29
|
# paths within a plugin. `for_file_diagnostics` is the subset the runner's per-file diagnostic loop
|
|
25
30
|
# visits: plugins overriding `#diagnostics_for_file` or declaring at least one `node_rule`.
|
|
@@ -103,8 +108,8 @@ module Rigor
|
|
|
103
108
|
# The categorisation sets + the ordered per-collector subsets.
|
|
104
109
|
def compile_memberships(plugins)
|
|
105
110
|
plugins.each { |p| reject_legacy_flow_hook!(p) }
|
|
106
|
-
@dynamic = plugins.reject { |p| p.class.dynamic_returns.empty? }.to_set
|
|
107
|
-
@narrowing_facts = plugins.reject { |p| p.class.narrowing_facts_rules.empty? }.to_set
|
|
111
|
+
@dynamic = plugins.reject { |p| p.class.dynamic_returns.empty? }.to_set.freeze
|
|
112
|
+
@narrowing_facts = plugins.reject { |p| p.class.narrowing_facts_rules.empty? }.to_set.freeze
|
|
108
113
|
compile_collector_subsets(plugins)
|
|
109
114
|
end
|
|
110
115
|
|
|
@@ -218,6 +223,12 @@ module Rigor
|
|
|
218
223
|
# Ractor. The eventual Phase 4 pool ships `blueprints` across the boundary and calls {.materialize}
|
|
219
224
|
# per-Ractor; the live `plugins` carriage on the coordinator registry stays unchanged.
|
|
220
225
|
class Registry
|
|
226
|
+
TypingState.memo_field(self, :@contracts_by_path, :@effect_memo,
|
|
227
|
+
reason: "keyed on the path, and on the one effect-contribution compile; the contract " \
|
|
228
|
+
"set, glob semantics and plugin set are fixed for the run (ADR-52 WD1), and the " \
|
|
229
|
+
"compile, which may read project facts across the I/O boundary, rests on the " \
|
|
230
|
+
"run's stable-filesystem premise, as FileDigest's run tables do")
|
|
231
|
+
|
|
221
232
|
attr_reader :plugins, :load_errors, :blueprints, :contribution_index, :resolved_gem_paths
|
|
222
233
|
|
|
223
234
|
# @param plugins — instantiated plugin instances in deterministic order.
|
|
@@ -45,6 +45,52 @@ module Rigor
|
|
|
45
45
|
freeze
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Builds the run's policy from the project configuration — the one derivation `rigor check`
|
|
49
|
+
# ({Analysis::Runner}), pooled workers ({Analysis::WorkerSession}) and `rigor unused`'s plugin roots
|
|
50
|
+
# ({Analysis::Reachability::PluginRoots}) share, so every `Plugin::Services` container enforces the
|
|
51
|
+
# same scope (#2586).
|
|
52
|
+
#
|
|
53
|
+
# `trusted_gems` is the gem-name half of every `Configuration#plugins` entry. `allowed_read_roots`
|
|
54
|
+
# is the project root (CWD), every `signature_paths` entry, each trusted gem's
|
|
55
|
+
# `Gem.loaded_specs` `full_gem_path` (silently skipped when the gem has no loaded spec), and every
|
|
56
|
+
# `plugins_io.allowed_paths` entry. `network_policy` / `allowed_url_hosts` come from
|
|
57
|
+
# `plugins_io.network` / `plugins_io.allowed_url_hosts`.
|
|
58
|
+
def self.for_configuration(configuration)
|
|
59
|
+
trusted_gems = configuration.plugins.map { |entry| trusted_gem_name(entry) }.uniq
|
|
60
|
+
roots = [Dir.pwd]
|
|
61
|
+
Array(configuration.signature_paths).each { |sp| roots << File.absolute_path(sp) }
|
|
62
|
+
trusted_gems.each do |gem_name|
|
|
63
|
+
path = trusted_gem_root(gem_name)
|
|
64
|
+
roots << path if path
|
|
65
|
+
end
|
|
66
|
+
configuration.plugins_io_allowed_paths.each { |p| roots << File.absolute_path(p) }
|
|
67
|
+
|
|
68
|
+
new(
|
|
69
|
+
trusted_gems: trusted_gems,
|
|
70
|
+
allowed_read_roots: roots,
|
|
71
|
+
network_policy: configuration.plugins_io_network,
|
|
72
|
+
allowed_url_hosts: configuration.plugins_io_allowed_url_hosts
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.trusted_gem_name(entry)
|
|
77
|
+
case entry
|
|
78
|
+
when String then entry
|
|
79
|
+
when Hash then entry["gem"] || entry["id"]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
private_class_method :trusted_gem_name
|
|
83
|
+
|
|
84
|
+
def self.trusted_gem_root(gem_name)
|
|
85
|
+
return nil if gem_name.nil? || gem_name.empty?
|
|
86
|
+
|
|
87
|
+
spec = Gem.loaded_specs[gem_name]
|
|
88
|
+
spec&.full_gem_path
|
|
89
|
+
rescue StandardError
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
private_class_method :trusted_gem_root
|
|
93
|
+
|
|
48
94
|
# @return true when the absolute path falls inside any allowed read root. Symlinks are
|
|
49
95
|
# resolved through `File.absolute_path` only (no `realpath`); plugins with adversarial intent are out of
|
|
50
96
|
# scope per ADR-2.
|
|
@@ -215,7 +215,10 @@ module Rigor
|
|
|
215
215
|
end,
|
|
216
216
|
"patched_overrides" => project_scan.project_patched_methods.overrides.map do |key, visibility|
|
|
217
217
|
[*key, visibility].map(&:to_s)
|
|
218
|
-
end
|
|
218
|
+
end,
|
|
219
|
+
"patched_singleton_copies" => project_scan.project_patched_methods.singleton_copies.map do |owner, name|
|
|
220
|
+
[owner.to_s, name.to_s]
|
|
221
|
+
end.sort
|
|
219
222
|
}
|
|
220
223
|
Digest::SHA256.hexdigest(JSON.generate(payload))
|
|
221
224
|
rescue StandardError
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../
|
|
3
|
+
require_relative "../typing_state"
|
|
4
|
+
require_relative "../memo_audit"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
# {Reflection}'s constant-ancestor walk ([#354](https://github.com/rigortype/rigor/issues/354)), split out for
|
|
@@ -9,13 +10,23 @@ module Rigor
|
|
|
9
10
|
# name's owner inherits constants from, and this file answers that once for both. Reopening the module
|
|
10
11
|
# keeps the walk behind the facade its callers already read.
|
|
11
12
|
module Reflection
|
|
12
|
-
# #354 —
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
# #354 — the per-run ancestor-scope memo. See {.ancestor_constant_scopes}. ADR-129 WD1 — a run-scoped Memo.
|
|
14
|
+
ANCESTOR_SCOPES = TypingState.memo(
|
|
15
|
+
:ancestor_constant_scopes,
|
|
16
|
+
run_scoped: true, audit: "ancestor constant scopes",
|
|
17
|
+
reason: "one [discovery, scopes by class name] slot keyed on the discovery index's identity, as the " \
|
|
18
|
+
"resolution chain the scopes are read from is (#2848)"
|
|
19
|
+
)
|
|
20
|
+
private_constant :ANCESTOR_SCOPES
|
|
21
|
+
|
|
22
|
+
# The recording run's slot. See {.recorded_ancestor_constant_scopes}. ADR-129 WD1 — a run-scoped Memo.
|
|
23
|
+
RECORDED_ANCESTOR_SCOPES = TypingState.memo(
|
|
24
|
+
:recorded_ancestor_constant_scopes,
|
|
25
|
+
run_scoped: true, audit: "recorded ancestor constant scopes",
|
|
26
|
+
reason: "one [discovery, scopes by class name] slot keyed on the discovery index's identity; a hit replays " \
|
|
27
|
+
"the chain's edges, so the key is exact"
|
|
28
|
+
)
|
|
29
|
+
private_constant :RECORDED_ANCESTOR_SCOPES
|
|
19
30
|
|
|
20
31
|
module_function
|
|
21
32
|
|
|
@@ -37,11 +48,13 @@ module Rigor
|
|
|
37
48
|
# constant it owns resolves only if the bare name reaches it at step 3: widening to the superclass's
|
|
38
49
|
# RBS graph is a separate question with its own FP surface. See {.external_constant_owner}.
|
|
39
50
|
#
|
|
40
|
-
# Memoised
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
51
|
+
# Memoised because step 2 runs on every constant reference whose lexical candidates all miss — which
|
|
52
|
+
# is the common case for a core-class reference (`String` inside `class Foo`). The bucket keys on the
|
|
53
|
+
# identity of the discovery index, as the `Scope::ResolutionChain` memo it reads does: the chain's
|
|
54
|
+
# order depends on the analysing file's own view of the include order (a class declared in two files
|
|
55
|
+
# orders its `include`s from the reader's file first), so a run-generation key served one file's
|
|
56
|
+
# order to another (#2848). A re-run in one process (LSP, ADR-62 warm loop) builds a new index, so it
|
|
57
|
+
# cannot hit stale entries either.
|
|
45
58
|
def ancestor_constant_scopes(class_name, scope)
|
|
46
59
|
ancestor_constant_worlds(class_name, scope).first
|
|
47
60
|
end
|
|
@@ -87,32 +100,76 @@ module Rigor
|
|
|
87
100
|
# every call.
|
|
88
101
|
return recorded_ancestor_constant_scopes(class_name, scope) if Analysis::DependencyRecorder.active?
|
|
89
102
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
103
|
+
# ADR-129 G2 — an audit recomputation bypasses the memo (neither read nor written).
|
|
104
|
+
if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
|
|
105
|
+
return compute_ancestor_constant_scopes(class_name, scope)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
discovery = scope.discovery
|
|
109
|
+
state = TypingState.current
|
|
110
|
+
slot = state[ANCESTOR_SCOPES]
|
|
111
|
+
unless slot && slot[0].equal?(discovery)
|
|
112
|
+
slot = [discovery, {}]
|
|
113
|
+
state[ANCESTOR_SCOPES] = slot
|
|
95
114
|
end
|
|
96
115
|
bucket = slot[1]
|
|
97
|
-
|
|
116
|
+
worlds = bucket[class_name]
|
|
117
|
+
if worlds.nil?
|
|
118
|
+
return bucket[class_name] = Inference::MemoAudit.key_audit("ancestor constant scopes") do
|
|
119
|
+
compute_ancestor_constant_scopes(class_name, scope)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
return worlds unless Inference::MemoAudit.enabled?
|
|
123
|
+
|
|
124
|
+
Inference::MemoAudit.check("ancestor constant scopes", audit_view(worlds), class_name) do
|
|
125
|
+
audit_view(compute_ancestor_constant_scopes(class_name, scope))
|
|
126
|
+
end
|
|
127
|
+
worlds
|
|
98
128
|
end
|
|
99
129
|
private_class_method :ancestor_constant_worlds
|
|
100
130
|
|
|
131
|
+
# `[scopes, chain]` compared by content: a recomputation builds a new chain object, equal to the stored one
|
|
132
|
+
# entry for entry when the key holds every input.
|
|
133
|
+
def audit_view(worlds) = [worlds[0], worlds[1].audit_signature]
|
|
134
|
+
private_class_method :audit_view
|
|
135
|
+
|
|
136
|
+
# A recomputation for the audit, its dependency reads withheld: the hit it checks files its own edges, so the
|
|
137
|
+
# recorded set stays what an unaudited run records.
|
|
138
|
+
def fresh_ancestor_constant_scopes(class_name, scope)
|
|
139
|
+
Analysis::DependencyRecorder.withhold { compute_ancestor_constant_scopes(class_name, scope) }.first
|
|
140
|
+
end
|
|
141
|
+
private_class_method :fresh_ancestor_constant_scopes
|
|
142
|
+
|
|
101
143
|
# The recording run's memo. The answer is every project entry of the chain, so its edges are the
|
|
102
144
|
# root's and every entry's — what `Scope::ResolutionChain#record` files, which a computation does and a
|
|
103
145
|
# hit replays, in the same order. One slot keyed on the discovery index: the answer and the edges are
|
|
104
146
|
# both read from it, so the key is exact, and the slot never pins more than the index it serves.
|
|
105
147
|
def recorded_ancestor_constant_scopes(class_name, scope)
|
|
148
|
+
# ADR-129 G2 — an audit recomputation bypasses the memo (neither read nor written).
|
|
149
|
+
if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
|
|
150
|
+
return compute_ancestor_constant_scopes(class_name, scope)
|
|
151
|
+
end
|
|
152
|
+
|
|
106
153
|
discovery = scope.discovery
|
|
107
|
-
|
|
154
|
+
state = TypingState.current
|
|
155
|
+
slot = state[RECORDED_ANCESTOR_SCOPES]
|
|
108
156
|
unless slot && slot[0].equal?(discovery)
|
|
109
157
|
slot = [discovery, {}]
|
|
110
|
-
|
|
158
|
+
state[RECORDED_ANCESTOR_SCOPES] = slot
|
|
111
159
|
end
|
|
112
160
|
bucket = slot[1]
|
|
113
161
|
worlds = bucket[class_name]
|
|
114
|
-
|
|
162
|
+
if worlds.nil?
|
|
163
|
+
return bucket[class_name] = Inference::MemoAudit.key_audit("recorded ancestor constant scopes") do
|
|
164
|
+
compute_ancestor_constant_scopes(class_name, scope)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
115
167
|
|
|
168
|
+
if Inference::MemoAudit.enabled?
|
|
169
|
+
Inference::MemoAudit.check("recorded ancestor constant scopes", audit_view(worlds), class_name) do
|
|
170
|
+
audit_view(fresh_ancestor_constant_scopes(class_name, scope))
|
|
171
|
+
end
|
|
172
|
+
end
|
|
116
173
|
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
|
|
117
174
|
chain.record(scope)
|
|
118
175
|
record_external_owner_edges(chain)
|