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
|
@@ -64,8 +64,10 @@ module Rigor
|
|
|
64
64
|
|
|
65
65
|
# ADR-89 WD2 — bounds on the persisted return summaries (per-def observed call keys, and total defs
|
|
66
66
|
# summarised) so the incremental snapshot stays small. A def observed under more keys than the per-def
|
|
67
|
-
# cap keeps
|
|
68
|
-
#
|
|
67
|
+
# cap keeps none of them (issue #3116): the keys past the cap are unknown, so equal returns at the first
|
|
68
|
+
# eight prove nothing about the ninth key's reader. A def past the total cap keeps none either. Both are
|
|
69
|
+
# harvested as nil ({#return_summaries}), which the session stores as no summary at all, so the def's symbol
|
|
70
|
+
# dependents always re-check — the conservative direction.
|
|
69
71
|
RETURN_SUMMARY_KEYS_PER_DEF = 8
|
|
70
72
|
RETURN_SUMMARY_TOTAL_CAP = 4000
|
|
71
73
|
|
|
@@ -515,6 +517,10 @@ module Rigor
|
|
|
515
517
|
# The return memo bucket the last `run` filled, kept for {#return_summaries} once the run has released the
|
|
516
518
|
# thread's slot; nil before the first run.
|
|
517
519
|
@return_memo = nil
|
|
520
|
+
# The `[path, node_id] => Prism::DefNode` table the last `run`'s {Inference::DefNodeResolver} filled, kept for
|
|
521
|
+
# the same reader.
|
|
522
|
+
@resolved_def_nodes = nil
|
|
523
|
+
@probe_reads = {}.freeze
|
|
518
524
|
build_collaborators
|
|
519
525
|
end
|
|
520
526
|
|
|
@@ -528,6 +534,9 @@ module Rigor
|
|
|
528
534
|
# errors. The Environment is built once at run start through `Environment.for_project` so all files
|
|
529
535
|
# share the same RBS load.
|
|
530
536
|
def run(paths = @configuration.paths)
|
|
537
|
+
# ADR-129 WD5 (#2785) — a mismatch left on the thread by typing after an earlier run (its return summaries, a
|
|
538
|
+
# language-server hover) fails here rather than reaching this run's first file as a crash row.
|
|
539
|
+
Inference::MemoAudit.raise_undrained!
|
|
531
540
|
# One per-run file-digest memo spans the whole run, so a path is SHA-256'd at most once across the
|
|
532
541
|
# run-diagnostics dependency descriptor, its `fresh?` validation, the RBS signature tree, and every
|
|
533
542
|
# plugin producer's watched-glob validation (they overlap heavily on the warm path). The nested ADR-85
|
|
@@ -536,7 +545,12 @@ module Rigor
|
|
|
536
545
|
# ADR-87 WD1 — `cache.validation` (or the RIGOR_STRICT_VALIDATION env, which wins) selects the
|
|
537
546
|
# freshness path for this run; the `auto` default resolves strict in CI (#190), stat-first elsewhere.
|
|
538
547
|
Cache::FileDigest.with_run(strict: @configuration.cache_validation_strict?) do
|
|
539
|
-
Inference::DefNodeResolver.with_run
|
|
548
|
+
Inference::DefNodeResolver.with_run do
|
|
549
|
+
run_analysis(paths)
|
|
550
|
+
ensure
|
|
551
|
+
# Issue #3160 — the memo keys an unchanged file's def on the node the resolver minted for its handle.
|
|
552
|
+
@resolved_def_nodes = Inference::DefNodeResolver.resolved_nodes
|
|
553
|
+
end
|
|
540
554
|
ensure
|
|
541
555
|
# ADR-89 WD2 — {#return_summaries} reads the return memo after the run, so the runner keeps the bucket the
|
|
542
556
|
# run filled before the thread's run-scoped slots are released (issues #2412, #2399).
|
|
@@ -788,15 +802,28 @@ module Rigor
|
|
|
788
802
|
end
|
|
789
803
|
|
|
790
804
|
# ADR-89 WD2 — the per-method observed-key return summaries the run's ADR-84 return memo just captured.
|
|
791
|
-
# For each project method
|
|
792
|
-
# incremental session persists: `keys` the observed `[receiver, arg_types]` type tuples, `returns` their
|
|
805
|
+
# For each project method the run observed, a bounded `{ keys:, returns:, effects:, guard_constants: }` summary
|
|
806
|
+
# the incremental session persists: `keys` the observed `[receiver, arg_types]` type tuples, `returns` their
|
|
793
807
|
# `describe(:short)` descriptors, `effects` the content-mutated parameter positions (a caller-visible
|
|
794
|
-
# arg-flooring surface)
|
|
808
|
+
# arg-flooring surface), `guard_constants` the published-constant spellings its body reads ({#guard_constants}).
|
|
809
|
+
# Only meaningful right after a run populated the memo (baseline / recheck),
|
|
795
810
|
# before the bucket rolls. Keys are held live here; the session Marshals them for the snapshot.
|
|
796
811
|
#
|
|
812
|
+
# Issue #3160 — the session reads the harvest as EVERY key the files this run analysed observed each def
|
|
813
|
+
# under, so whatever cannot be that says so instead of looking like a short key list:
|
|
814
|
+
#
|
|
815
|
+
# - a def observed under keys this run cannot list maps to nil: more keys than
|
|
816
|
+
# {RETURN_SUMMARY_KEYS_PER_DEF} (issue #3116), a call whose result the memo refused to store, or a def
|
|
817
|
+
# past {RETURN_SUMMARY_TOTAL_CAP};
|
|
818
|
+
# - a def absent from the result was observed by no analysed file, wherever it is defined: an unchanged
|
|
819
|
+
# file's def is found through the node its {Inference::DefHandle} resolved to;
|
|
820
|
+
# - the whole result is nil for a pool run, whose workers typed the calls in memos this process never sees.
|
|
821
|
+
#
|
|
797
822
|
# @return the summaries keyed by `[path, "Class#method"]` (or `"Class.method"`), each holding `keys:`,
|
|
798
|
-
# `returns:` and `effects
|
|
823
|
+
# `returns:` and `effects:`, or nil; nil for a pool run.
|
|
799
824
|
def return_summaries
|
|
825
|
+
return nil if pool_mode?
|
|
826
|
+
|
|
800
827
|
memo = Inference::ExpressionTyper.harvest_return_memo(@return_memo)
|
|
801
828
|
return {} if memo.empty?
|
|
802
829
|
|
|
@@ -806,37 +833,89 @@ module Rigor
|
|
|
806
833
|
"#", memo, effects_evaluator)
|
|
807
834
|
collect_return_summaries(result, @project_discovered_singleton_def_nodes,
|
|
808
835
|
@project_discovered_singleton_def_sources, ".", memo, effects_evaluator)
|
|
836
|
+
# ADR-129 WD5 (#2785) — typing after the run, under `content_effects`' rescue.
|
|
837
|
+
Inference::MemoAudit.raise_undrained!
|
|
809
838
|
result
|
|
810
839
|
end
|
|
811
840
|
|
|
812
841
|
# Folds one def-node/def-source table pair's memo entries into the return-summary result under
|
|
813
|
-
# `separator`.
|
|
814
|
-
#
|
|
815
|
-
#
|
|
816
|
-
# the snapshot). Bounded per def and in total so the snapshot stays small.
|
|
842
|
+
# `separator`. The node the memo keyed is the identity every cross-file caller resolved through: the live
|
|
843
|
+
# def node of a cold / re-walked file, or the one the resolver minted for an unchanged file's
|
|
844
|
+
# {Inference::DefHandle} ({#observed_def_node}).
|
|
817
845
|
def collect_return_summaries(result, nodes, sources, separator, memo, effects_evaluator)
|
|
846
|
+
summarised = result.count { |_pair, summary| summary }
|
|
818
847
|
nodes.each do |class_name, methods|
|
|
819
|
-
methods.each do |method_name,
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
entries = memo[node]
|
|
823
|
-
next if entries.nil? || entries.empty?
|
|
848
|
+
methods.each do |method_name, slot|
|
|
849
|
+
node = observed_def_node(slot)
|
|
850
|
+
next unless memo.key?(node)
|
|
824
851
|
|
|
825
852
|
path_line = sources.dig(class_name, method_name)
|
|
826
853
|
next if path_line.nil?
|
|
827
854
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
result[[path_line.split(":", 2).first, "#{class_name}#{separator}#{method_name}"]] = {
|
|
832
|
-
keys: capped.map { |entry| [entry.receiver, entry.arg_types] },
|
|
833
|
-
returns: capped.map { |entry| entry.result.describe(:short) },
|
|
834
|
-
effects: content_effects(effects_evaluator, node)
|
|
835
|
-
}
|
|
855
|
+
summary = summarised < RETURN_SUMMARY_TOTAL_CAP ? return_summary(memo[node], effects_evaluator, node) : nil
|
|
856
|
+
summarised += 1 if summary
|
|
857
|
+
result[[path_line.split(":", 2).first, "#{class_name}#{separator}#{method_name}"]] = summary
|
|
836
858
|
end
|
|
837
859
|
end
|
|
838
860
|
end
|
|
839
861
|
|
|
862
|
+
# The node the run's return memo keyed `slot`'s def on, or nil when no call resolved it: the live node, or the
|
|
863
|
+
# one the run's resolver minted for a {Inference::DefHandle}.
|
|
864
|
+
def observed_def_node(slot)
|
|
865
|
+
return slot unless slot.is_a?(Inference::DefHandle)
|
|
866
|
+
|
|
867
|
+
@resolved_def_nodes&.[]([slot.path, slot.node_id])
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
# One def's summary, or nil when `entries` are not every key it was observed under: the memo refused to store
|
|
871
|
+
# a call of it (so `entries` may be empty), or there are more than {RETURN_SUMMARY_KEYS_PER_DEF} (issue
|
|
872
|
+
# #3116). A splat call's key (issue #1237) is an entry like any other and counts toward the cap.
|
|
873
|
+
def return_summary(entries, effects_evaluator, node)
|
|
874
|
+
return nil if entries.empty? || entries.size > RETURN_SUMMARY_KEYS_PER_DEF
|
|
875
|
+
return nil if Inference::ExpressionTyper.return_memo_refused?(@return_memo, node)
|
|
876
|
+
|
|
877
|
+
{ keys: entries.map { |entry| return_summary_key(entry) },
|
|
878
|
+
returns: entries.map { |entry| entry.result.describe(:short) },
|
|
879
|
+
effects: content_effects(effects_evaluator, node),
|
|
880
|
+
guard_constants: guard_constants(node) }
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
# Issue #3186 — the constant spellings in `node`'s body that the truthiness rule's one interprocedural hop
|
|
884
|
+
# ({CheckRules::PublishedConstantGuard}) could answer true for, or nil when they cannot be listed. A reader's
|
|
885
|
+
# `flow.always-truthy-condition` withholds when a def it calls reads a constant another file published,
|
|
886
|
+
# whatever the def returns, so the ADR-89 WD2 gate compares these as well as the returns; nil never compares
|
|
887
|
+
# equal, so it keeps the def's readers.
|
|
888
|
+
#
|
|
889
|
+
# The hop asks `Scope#published_constant?`, which answers true only for a spelling whose last segment the
|
|
890
|
+
# project publishes or the reader's own file assigns from a published constant, and each of those is a segment
|
|
891
|
+
# some project file writes. So the spellings whose last segment no file writes (a class name, as a rule) are
|
|
892
|
+
# left out: they answer false for every reader, and keeping them would fail the gate on every edit that adds a
|
|
893
|
+
# callee. The filter is this run's write census, so a census that moved fails the comparison.
|
|
894
|
+
def guard_constants(node)
|
|
895
|
+
written = constant_write_segments
|
|
896
|
+
CheckRules::PublishedConstantGuard.body_constant_spellings(node.body).select do |spelling|
|
|
897
|
+
written.include?(spelling.split("::").last)
|
|
898
|
+
end
|
|
899
|
+
rescue StandardError
|
|
900
|
+
nil
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
# The last segments of every constant name the run's write census holds, a wildcard key's included.
|
|
904
|
+
def constant_write_segments
|
|
905
|
+
unless @constant_write_segments_of.equal?(@project_constant_writes)
|
|
906
|
+
@constant_write_segments_of = @project_constant_writes
|
|
907
|
+
@constant_write_segments = @project_constant_writes.each_key.to_set { |name| name.split("::").last }
|
|
908
|
+
end
|
|
909
|
+
@constant_write_segments
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
# One observed call key: `[receiver, arg_types]`, or, for an entry a splat call stored (issue #1237),
|
|
913
|
+
# `[receiver, arg_types, true]`, which {#evaluate_spec_returns} re-drives with no positional correspondence.
|
|
914
|
+
def return_summary_key(entry)
|
|
915
|
+
key = [entry.receiver, entry.arg_types]
|
|
916
|
+
entry.positionals_unknown ? key << true : key
|
|
917
|
+
end
|
|
918
|
+
|
|
840
919
|
# The content-mutated parameter positions of `node`, or `[]` if the computation raises (defensive: the
|
|
841
920
|
# effects surface only routes attention, never soundness — a missing set reads as "no floor").
|
|
842
921
|
def content_effects(effects_evaluator, node)
|
|
@@ -859,13 +938,45 @@ module Rigor
|
|
|
859
938
|
def evaluate_return_types(paths, specs)
|
|
860
939
|
return {} if specs.empty?
|
|
861
940
|
|
|
941
|
+
@probe_reads = {}
|
|
942
|
+
@probe_plugin_reads = {}
|
|
943
|
+
@probe_disclosures = {}
|
|
944
|
+
@probe_guard_constants = {}
|
|
862
945
|
Cache::FileDigest.with_run(strict: @configuration.cache_validation_strict?) do
|
|
863
|
-
Inference::DefNodeResolver.with_run
|
|
946
|
+
Inference::DefNodeResolver.with_run do
|
|
947
|
+
evaluate_return_types_setup(paths, specs)
|
|
948
|
+
ensure
|
|
949
|
+
@resolved_def_nodes = Inference::DefNodeResolver.resolved_nodes
|
|
950
|
+
end
|
|
864
951
|
end
|
|
865
952
|
ensure
|
|
953
|
+
# Issue #3180 — as {#run} does: {#return_summaries} then answers for what the probe observed.
|
|
954
|
+
@return_memo = Inference::ExpressionTyper.current_return_memo
|
|
866
955
|
RunScopedSlots.release
|
|
867
956
|
end
|
|
868
957
|
|
|
958
|
+
# Issue #3180 — what the last {#evaluate_return_types} read while re-evaluating each spec's def at its keys: the
|
|
959
|
+
# spec triple => a {DependencyRecorder::Record} of every def, class and name the evaluation read, the def's own
|
|
960
|
+
# file included (nothing is filtered as a self-read). The incremental session gives these edges to the readers
|
|
961
|
+
# a passing gate skips, since they now reach whatever the edited def reaches. Empty before the first probe.
|
|
962
|
+
attr_reader :probe_reads
|
|
963
|
+
|
|
964
|
+
# Issue #3187 — what the plugins did while the last {#evaluate_return_types} re-evaluated each spec's def, as
|
|
965
|
+
# {#analysis_reads} and {#analysis_disclosures} hold for an analysed file: the spec triple => the boundary rows
|
|
966
|
+
# its evaluation read ({Plugin::IoBoundary.attributing}), and => the run-scoped rows it registered
|
|
967
|
+
# ({RunDisclosureCredit}). A triple whose evaluation read or registered nothing has no key. The incremental
|
|
968
|
+
# session credits the reads to the readers a passing gate skips, as it does {#probe_reads}; a registration
|
|
969
|
+
# keeps the def's readers instead, since no skipped reader could be credited with it, or have it retracted.
|
|
970
|
+
attr_reader :probe_plugin_reads, :probe_disclosures
|
|
971
|
+
|
|
972
|
+
# Issue #3186 — the spec triple => {#guard_constants} of the def the last {#evaluate_return_types} re-evaluated,
|
|
973
|
+
# under that probe's write census. A triple whose def the probe did not find has no key.
|
|
974
|
+
attr_reader :probe_guard_constants
|
|
975
|
+
|
|
976
|
+
# The consumer {#evaluate_spec_returns} records under. It is no file's path, so no read is dropped as a self-read.
|
|
977
|
+
PROBE_CONSUMER = ""
|
|
978
|
+
private_constant :PROBE_CONSUMER
|
|
979
|
+
|
|
869
980
|
# Builds the discovery index + environment (a real run's prologue, minus per-file analysis) and
|
|
870
981
|
# re-evaluates each spec's def. Extracted so {#evaluate_return_types}'s `with_run` wrappers stay thin.
|
|
871
982
|
def evaluate_return_types_setup(paths, specs)
|
|
@@ -901,10 +1012,43 @@ module Rigor
|
|
|
901
1012
|
return spec[:keys].map { nil } if node.nil?
|
|
902
1013
|
|
|
903
1014
|
scope = seed_project_scope(Scope.empty(environment: environment, source_path: spec[:path]))
|
|
904
|
-
spec[:
|
|
905
|
-
|
|
906
|
-
|
|
1015
|
+
triple = [spec[:class_name], spec[:method_name], spec[:singleton]]
|
|
1016
|
+
@probe_guard_constants[triple] = guard_constants(node)
|
|
1017
|
+
returns = nil
|
|
1018
|
+
attributed_probe(triple) do
|
|
1019
|
+
record_guard_spelling_edges(node)
|
|
1020
|
+
returns = spec[:keys].map do |(receiver, arg_types, positionals_unknown)|
|
|
1021
|
+
result = scope.user_method_return(node, receiver, arg_types,
|
|
1022
|
+
positionals_unknown: positionals_unknown || false)
|
|
1023
|
+
result&.describe(:short)
|
|
1024
|
+
end
|
|
1025
|
+
end
|
|
1026
|
+
returns
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
# Issue #3199 — the `constant:` edge for every spelling the truthiness guard's hop would scan in `node`'s body,
|
|
1030
|
+
# recorded into the probe's reads, which every reader the gate skips receives. {#guard_constants} leaves out a
|
|
1031
|
+
# spelling whose last segment no file writes, so an edit that starts probing one (`defined?(B::X)` before any
|
|
1032
|
+
# file assigns `B::X`) passes the gate; the skipped reader's guard never asks about it, and without this edge a
|
|
1033
|
+
# file that later publishes the constant would re-check nobody.
|
|
1034
|
+
def record_guard_spelling_edges(node)
|
|
1035
|
+
CheckRules::PublishedConstantGuard.body_constant_spellings(node.body).each do |spelling|
|
|
1036
|
+
DependencyRecorder.read_last_segment(:constant, spelling)
|
|
1037
|
+
end
|
|
1038
|
+
end
|
|
1039
|
+
|
|
1040
|
+
# Runs one spec's evaluation with its dependency reads recorded ({#probe_reads}) and its plugin reads and
|
|
1041
|
+
# run-scoped registrations attributed to it ({#probe_plugin_reads}, {#probe_disclosures}), as
|
|
1042
|
+
# {#analyze_file_attributed} does for a file.
|
|
1043
|
+
def attributed_probe(triple, &)
|
|
1044
|
+
reads = []
|
|
1045
|
+
disclosures = RunDisclosureCredit.collect do
|
|
1046
|
+
@probe_reads[triple] = Plugin::IoBoundary.attributing(reads) do
|
|
1047
|
+
DependencyRecorder.record_for(PROBE_CONSUMER, &)
|
|
1048
|
+
end
|
|
907
1049
|
end
|
|
1050
|
+
@probe_plugin_reads[triple] = reads.uniq.freeze unless reads.empty?
|
|
1051
|
+
@probe_disclosures[triple] = disclosures unless disclosures.empty?
|
|
908
1052
|
end
|
|
909
1053
|
|
|
910
1054
|
# ADR-45 — unchanged-project fast path. Serves the whole run's (pre-severity-profile) diagnostics
|
|
@@ -920,7 +1064,10 @@ module Rigor
|
|
|
920
1064
|
# #392 — `source_files:` is what a plugin's `source_rbs_synthesizer` is offered at env-build time,
|
|
921
1065
|
# and a template unit is not Ruby the synthesiser can read (rbs-inline would be handed `.rbx`
|
|
922
1066
|
# bytes). The `.rb` expansion is the right set here; the units join the ANALYSED set only.
|
|
923
|
-
|
|
1067
|
+
# Issue #2883 — not eager: a warm result-cache HIT never types, so it must not build the RBS environment;
|
|
1068
|
+
# {#assemble_run_diagnostics} builds it on the analysis path.
|
|
1069
|
+
environment = @pool_coordinator.resolve_sequential_environment(source_files: expansion.fetch(:files),
|
|
1070
|
+
eager: false)
|
|
924
1071
|
# Lazy-files descriptor: the cache KEY reads only `gems` + `configs`; the RBS signature-tree `files`
|
|
925
1072
|
# are digested solely by `run_dependency_descriptor` on a MISS, so a warm HIT never walks the tree.
|
|
926
1073
|
rbs_descriptor = if environment&.rbs_loader
|
|
@@ -1194,6 +1341,8 @@ module Rigor
|
|
|
1194
1341
|
end
|
|
1195
1342
|
|
|
1196
1343
|
def assemble_run_diagnostics(expansion, environment: nil)
|
|
1344
|
+
# Issue #2883 — the analysis path builds the RBS environment before anything types (ADR-129 G1').
|
|
1345
|
+
environment&.prepare_for_typing!
|
|
1197
1346
|
# Force the deferred cross-file discovery pre-pass on the analysis (miss) path. Memoised, so the
|
|
1198
1347
|
# eager force in `#run` (recording / subset modes) makes this a no-op. A warm cache HIT never calls
|
|
1199
1348
|
# `assemble_run_diagnostics`, so it never runs the two whole-project parse passes. Runs over the FULL
|
|
@@ -1442,7 +1591,7 @@ module Rigor
|
|
|
1442
1591
|
# #979 — the signature ROOTS' listings, one glob row each. `rbs_descriptor.files` covers only the
|
|
1443
1592
|
# `.rbs` files that existed while the run read them, so without these a signature file written after
|
|
1444
1593
|
# the run left the slot validating fresh and the warm run answered without it.
|
|
1445
|
-
globs = rbs_descriptor.globs + template_unit_glob_entries + discovery_glob_entries
|
|
1594
|
+
globs = rbs_descriptor.globs + template_unit_glob_entries + discovery_glob_entries + bundle_sig_glob_entries
|
|
1446
1595
|
boundary = plugin_boundary_rows
|
|
1447
1596
|
entries.concat(boundary.files)
|
|
1448
1597
|
globs.concat(boundary.globs).concat(producer_watch_glob_entries)
|
|
@@ -1590,6 +1739,9 @@ module Rigor
|
|
|
1590
1739
|
return nil if pool_mode? || !@collect_stats || @analyzed_files.empty?
|
|
1591
1740
|
|
|
1592
1741
|
own, pinned = signature_rows
|
|
1742
|
+
# Issue #2987 — the bundle's `sig/` listing is identified by the gems' directory, outside the project, so a
|
|
1743
|
+
# `pinned` row: the write guard passes over it where it would refuse an `owned` one it cannot stat.
|
|
1744
|
+
pinned = Cache::Descriptor.new(files: pinned.files, globs: (pinned.globs + bundle_sig_glob_entries).uniq)
|
|
1593
1745
|
BaselineRows.new(
|
|
1594
1746
|
owned: Cache::Descriptor.new(
|
|
1595
1747
|
files: own.files + kind_entries(signature_root_candidates) +
|
|
@@ -1618,12 +1770,12 @@ module Rigor
|
|
|
1618
1770
|
roots.any? { |root| directory.start_with?(root) }
|
|
1619
1771
|
end
|
|
1620
1772
|
|
|
1621
|
-
# The project's own signature roots, whether or not they exist:
|
|
1622
|
-
#
|
|
1623
|
-
#
|
|
1624
|
-
# itself covers it appearing.
|
|
1773
|
+
# The project's own signature roots, whether or not they exist:
|
|
1774
|
+
# {Environment.project_signature_root_candidates} — the configured `signature_paths:`, or the `sig`
|
|
1775
|
+
# directory it defaults to when none are configured (issue #1619). The signature rows' listing rows
|
|
1776
|
+
# cover a file appearing under a root that existed; only a row for the root itself covers it appearing.
|
|
1625
1777
|
def signature_root_candidates
|
|
1626
|
-
@configuration.signature_paths
|
|
1778
|
+
Environment.project_signature_root_candidates(@configuration.signature_paths)
|
|
1627
1779
|
end
|
|
1628
1780
|
|
|
1629
1781
|
# One names-mode listing row per directory the discovery widening globbed, so a file appearing there is seen
|
|
@@ -1638,6 +1790,22 @@ module Rigor
|
|
|
1638
1790
|
end
|
|
1639
1791
|
end
|
|
1640
1792
|
|
|
1793
|
+
# Issue #2987 — one names-mode listing row per bundle `sig/` layout ({Environment::BundleSigDiscovery.discover}'s
|
|
1794
|
+
# two globs). The signature rows list only the directories the loader read, so a gem that GAINS a `sig/`
|
|
1795
|
+
# (or loses it) while its version and lock entry stand still sits in none of them, and the run-result
|
|
1796
|
+
# slot kept serving the environment built without (or with) it.
|
|
1797
|
+
def bundle_sig_glob_entries
|
|
1798
|
+
root = Environment::BundleSigDiscovery.resolve_bundle_path(
|
|
1799
|
+
bundle_path: @configuration.bundler_bundle_path, project_root: Dir.pwd,
|
|
1800
|
+
auto_detect: @configuration.bundler_auto_detect
|
|
1801
|
+
)
|
|
1802
|
+
return [] if root.nil?
|
|
1803
|
+
|
|
1804
|
+
Environment::BundleSigDiscovery::SIG_GLOBS.map do |pattern|
|
|
1805
|
+
Cache::Descriptor::GlobEntry.compute(root: root.to_s, pattern: pattern, mode: :names)
|
|
1806
|
+
end
|
|
1807
|
+
end
|
|
1808
|
+
|
|
1641
1809
|
def unanalysed_pre_eval_entries(files)
|
|
1642
1810
|
analysed = files.to_set { |path| File.absolute_path(path) }
|
|
1643
1811
|
outside = @configuration.pre_eval.reject { |path| analysed.include?(File.absolute_path(path)) }
|
|
@@ -1658,6 +1826,7 @@ module Rigor
|
|
|
1658
1826
|
end
|
|
1659
1827
|
private :plugin_boundary_rows, :producer_watch_glob_entries, :kind_entries, :root_presence_entries,
|
|
1660
1828
|
:analysis_reads_attributed?, :signature_root_candidates, :discovery_glob_entries,
|
|
1829
|
+
:bundle_sig_glob_entries,
|
|
1661
1830
|
:unanalysed_pre_eval_entries, :signature_rows, :within?
|
|
1662
1831
|
|
|
1663
1832
|
# Issue #684 — the project files this run DISCOVERED but did not analyse. A widened run's diagnostics
|
|
@@ -2031,7 +2200,7 @@ module Rigor
|
|
|
2031
2200
|
rescue ArgumentError => e
|
|
2032
2201
|
floor = self.class.prism_supported_floor
|
|
2033
2202
|
Diagnostic.new(
|
|
2034
|
-
path:
|
|
2203
|
+
path: @configuration.config_display_path, line: 1, column: 1,
|
|
2035
2204
|
message: "target_ruby #{@configuration.target_ruby.inspect} is not supported by this Rigor build " \
|
|
2036
2205
|
"(Prism accepts #{floor} and newer). Set target_ruby to your project's Ruby version " \
|
|
2037
2206
|
"(>= #{floor}) — read it from Gemfile.lock's `RUBY VERSION` or .ruby-version. " \
|
|
@@ -2661,6 +2830,14 @@ module Rigor
|
|
|
2661
2830
|
# `DependencyRecorder.active? == false`. Recording is observational, so diagnostics are byte-identical
|
|
2662
2831
|
# either way.
|
|
2663
2832
|
def analyze_file(path, environment)
|
|
2833
|
+
diagnostics = analyze_file_attributed(path, environment)
|
|
2834
|
+
return diagnostics unless Inference::MemoAudit.enabled?
|
|
2835
|
+
|
|
2836
|
+
# ADR-129 WD5 (#2785) — the audit's mismatches this file raised, a swallowed one included.
|
|
2837
|
+
CrashSignature.with_swallowed(path, diagnostics, Inference::MemoAudit.drain)
|
|
2838
|
+
end
|
|
2839
|
+
|
|
2840
|
+
def analyze_file_attributed(path, environment)
|
|
2664
2841
|
return analyze_with_effects(path, environment) unless @record_dependencies
|
|
2665
2842
|
|
|
2666
2843
|
# ADR-45 WD2 (#1507) — every plugin read this file's analysis makes is attributed to it, so the
|
|
@@ -2741,9 +2918,11 @@ module Rigor
|
|
|
2741
2918
|
# `self_call_misses` feeds the `call.self-undefined-method` collector; the recorder is inert unless
|
|
2742
2919
|
# the rule is active or `record_self_calls:` opted in.
|
|
2743
2920
|
index = nil
|
|
2921
|
+
TypeOfMemo.begin_file
|
|
2744
2922
|
self_call_record = with_self_call_recording(path) do
|
|
2745
2923
|
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope, widen_clauses: true)
|
|
2746
2924
|
end
|
|
2925
|
+
TypeOfMemo.phase!(:rule)
|
|
2747
2926
|
self_call_misses = self_call_record ? self_call_record.calls : []
|
|
2748
2927
|
diagnostics = rule_and_plugin_diagnostics(path, parse_result, scope, index, self_call_misses)
|
|
2749
2928
|
notify_file_indexed(path, parse_result, scope, index)
|
|
@@ -2758,16 +2937,18 @@ module Rigor
|
|
|
2758
2937
|
severity: :error
|
|
2759
2938
|
)
|
|
2760
2939
|
]
|
|
2761
|
-
rescue StandardError => e
|
|
2940
|
+
rescue StandardError, SystemStackError => e
|
|
2762
2941
|
[
|
|
2763
2942
|
Diagnostic.new(
|
|
2764
2943
|
path: path,
|
|
2765
2944
|
line: 1,
|
|
2766
2945
|
column: 1,
|
|
2767
|
-
message: CrashSignature.
|
|
2946
|
+
message: CrashSignature.contained_message!(e, parse_result),
|
|
2768
2947
|
severity: :error
|
|
2769
2948
|
)
|
|
2770
2949
|
]
|
|
2950
|
+
ensure
|
|
2951
|
+
TypeOfMemo.end_file
|
|
2771
2952
|
end
|
|
2772
2953
|
|
|
2773
2954
|
# ADR-113 WD2 — the `on_file_indexed` observer. It reads; an error inside it never changes this file's
|
|
@@ -2784,7 +2965,9 @@ module Rigor
|
|
|
2784
2965
|
# output and the plugin diagnostics are then built from that single walk's results.
|
|
2785
2966
|
def rule_and_plugin_diagnostics(path, parse_result, scope, index, self_call_misses)
|
|
2786
2967
|
root = parse_result.value
|
|
2787
|
-
node_collectors = CheckRules.build_node_collectors(
|
|
2968
|
+
node_collectors = CheckRules.build_node_collectors(
|
|
2969
|
+
path, index, root, plugin_api_surface: CheckRules::PluginApiSurface.active?(@configuration)
|
|
2970
|
+
)
|
|
2788
2971
|
node_results = node_rule_results_by_plugin(path, root, scope, node_collectors, index)
|
|
2789
2972
|
diagnostics = CheckRules.diagnose(
|
|
2790
2973
|
path: path,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../typing_state"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Analysis
|
|
5
7
|
# ADR-24 slice 4a — records every *implicit-self* method call the inference engine could not resolve to any
|
|
@@ -25,8 +27,25 @@ module Rigor
|
|
|
25
27
|
# Modelled on {DependencyRecorder}: process-thread-local accumulator, a cheap disabled fast path, and a
|
|
26
28
|
# frozen snapshot for consumers.
|
|
27
29
|
module SelfCallResolutionRecorder
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
# ADR-129 WD1 — the installed accumulator is a Frame; its contents are the bound Table {CALLS}. The activation
|
|
31
|
+
# count below is a process switch, outside the typing state (WD5).
|
|
32
|
+
RECORDER = TypingState.frame(
|
|
33
|
+
:self_call_resolution_recorder,
|
|
34
|
+
contents: :table, reason: "the accumulator a recording installs; each unresolved call records into it",
|
|
35
|
+
# G1′ witness: the memo journals and replays the records, so it is not a shut switch (unlike the tracers).
|
|
36
|
+
witness: "spec/fixtures/typing_state_witnesses/self_call_resolution_recorder.rb"
|
|
37
|
+
)
|
|
38
|
+
private_constant :RECORDER
|
|
39
|
+
|
|
40
|
+
# ADR-129 WD1 — the recorded misses are a bound Table: each installed accumulator is one of its stores, keyed by
|
|
41
|
+
# the call tuple (deduplicated, so the first record of a tuple is its only write), journaled while a capture is
|
|
42
|
+
# open so a later `type_of` memo can validate the reads and replay the writes in order.
|
|
43
|
+
CALLS = TypingState.table(
|
|
44
|
+
:self_call_misses,
|
|
45
|
+
bound: true,
|
|
46
|
+
reason: "UnresolvedSelfCall => true per consumer accumulator, appended in first-record order"
|
|
47
|
+
)
|
|
48
|
+
private_constant :CALLS
|
|
30
49
|
|
|
31
50
|
# One unresolved implicit-self call. `class_name` is the receiver's statically known class (the
|
|
32
51
|
# enclosing `self` type); `method_name` the called name; `node` the Prism `CallNode` (held in-memory so
|
|
@@ -45,10 +64,28 @@ module Rigor
|
|
|
45
64
|
@seen = Set.new
|
|
46
65
|
end
|
|
47
66
|
|
|
48
|
-
def add(call)
|
|
49
|
-
return unless @seen.add?(call)
|
|
67
|
+
def add?(call)
|
|
68
|
+
return false unless @seen.add?(call)
|
|
50
69
|
|
|
51
70
|
@calls << call
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Whether `call` is already recorded.
|
|
75
|
+
def recorded?(call) = @seen.include?(call)
|
|
76
|
+
|
|
77
|
+
# The bound Table's store protocol ({CALLS}): `true` for a recorded call, else `default`.
|
|
78
|
+
def fetch(call, default) = @seen.include?(call) || default
|
|
79
|
+
|
|
80
|
+
# The bound Table's store protocol: an undone write (G1's stage-4 form) forgets `call`.
|
|
81
|
+
def delete(call)
|
|
82
|
+
@calls.delete(call) if @seen.delete?(call)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The bound Table's store protocol: a replayed write records `call` as {#add?} does, so the replay order of a
|
|
86
|
+
# write slice is the order of {#calls}.
|
|
87
|
+
def []=(call, _value)
|
|
88
|
+
add?(call)
|
|
52
89
|
end
|
|
53
90
|
|
|
54
91
|
def snapshot
|
|
@@ -69,15 +106,16 @@ module Rigor
|
|
|
69
106
|
# Activates recording for `consumer` (the path being analyzed) for the duration of the block and returns
|
|
70
107
|
# the frozen {Record}. Nests safely; restores the previous recorder on exit.
|
|
71
108
|
def record_for(consumer)
|
|
72
|
-
previous = Thread.current[KEY]
|
|
73
109
|
accumulator = Accumulator.new(consumer.to_s)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
110
|
+
TypingState.current.with(RECORDER, accumulator) do
|
|
111
|
+
@mutex.synchronize { @active_count += 1 }
|
|
112
|
+
begin
|
|
113
|
+
yield
|
|
114
|
+
accumulator.snapshot
|
|
115
|
+
ensure
|
|
116
|
+
@mutex.synchronize { @active_count -= 1 }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
81
119
|
end
|
|
82
120
|
|
|
83
121
|
# Plain integer read (GVL-atomic) — no `Thread.current` lookup on the disabled fast path.
|
|
@@ -88,19 +126,20 @@ module Rigor
|
|
|
88
126
|
# Records one unresolved implicit-self call. No-op when no consumer is active on this thread (another
|
|
89
127
|
# thread may have flipped {active?}).
|
|
90
128
|
def record(class_name:, method_name:, node:, path:, line:, column:)
|
|
91
|
-
|
|
129
|
+
state = TypingState.current
|
|
130
|
+
accumulator = state[RECORDER]
|
|
92
131
|
return if accumulator.nil?
|
|
93
132
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
column: column
|
|
102
|
-
)
|
|
133
|
+
call = UnresolvedSelfCall.new(
|
|
134
|
+
class_name: class_name.to_s,
|
|
135
|
+
method_name: method_name.to_sym,
|
|
136
|
+
node: node,
|
|
137
|
+
path: path,
|
|
138
|
+
line: line,
|
|
139
|
+
column: column
|
|
103
140
|
)
|
|
141
|
+
state.log_bound_read(CALLS, accumulator, call, accumulator.fetch(call, TypingState::ABSENT)) if state.capturing?
|
|
142
|
+
state.log_bound_write(CALLS, accumulator, call, true, TypingState::ABSENT) if accumulator.add?(call)
|
|
104
143
|
end
|
|
105
144
|
end
|
|
106
145
|
end
|