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
|
@@ -124,9 +124,13 @@ module Rigor
|
|
|
124
124
|
# Diffed on a recheck to find every constant whose PUBLISHED answer could have moved, which satisfies
|
|
125
125
|
# the `constant:` edges a reader records at every reference, resolved or not.
|
|
126
126
|
@constant_decls = {}
|
|
127
|
-
# ADR-89 WD2 — per-def observed-key return summaries: [path, symbol] => { keys:, returns:, effects
|
|
127
|
+
# ADR-89 WD2 — per-def observed-key return summaries: [path, symbol] => { keys:, returns:, effects:,
|
|
128
|
+
# guard_constants: }.
|
|
128
129
|
# Harvested from the ADR-84 return memo after each run; drives the behavioural-stability gate.
|
|
129
130
|
@return_summaries = {}
|
|
131
|
+
# Issue #3180 — what the ADR-89 WD2 gate's probe read for the pairs it passed, from the closure decision
|
|
132
|
+
# until {#absorb} folds it into the skipped readers; nil when no pair passed.
|
|
133
|
+
@gate_observations = nil
|
|
130
134
|
# ADR-67 WD6c lift — the `parameter_inference:` seed table the cached diagnostics were computed
|
|
131
135
|
# under (`{}` when the gate is off — the gate state is constant across a snapshot's lifetime because
|
|
132
136
|
# the configuration is part of the global fingerprint). A recheck recomputes the table fresh and
|
|
@@ -263,7 +267,7 @@ module Rigor
|
|
|
263
267
|
# digest (issue #1536).
|
|
264
268
|
@seed_bundles = source_rbs_gate.stamp(runner.seed_bundles, whole_project: true)
|
|
265
269
|
absorb_dependency_graph(runner)
|
|
266
|
-
@return_summaries = runner
|
|
270
|
+
@return_summaries = baseline_return_summaries(runner)
|
|
267
271
|
# ADR-67 WD6c lift — the seed table the runner's own pre-pass computed ({} when the gate is off).
|
|
268
272
|
# Reading it back, rather than computing it here, keeps the baseline single-collect.
|
|
269
273
|
@param_table = runner.param_inferred_types
|
|
@@ -391,6 +395,8 @@ module Rigor
|
|
|
391
395
|
# whole project — what a `sig/` edit already costs through the snapshot fingerprint.
|
|
392
396
|
def affected_closure(changed, added, removed, param_files = Set.new, param_pairs = Set.new)
|
|
393
397
|
@stale_plugin_readers = plugin_read_affected | disclosure_refreshers(removed)
|
|
398
|
+
# Issue #3180 — a recheck that passed the gate and then re-baselined absorbed nothing: start without it.
|
|
399
|
+
@gate_observations = nil
|
|
394
400
|
return whole_project_closure(changed, added) if source_rbs_gate.moved?(@seed_bundles, changed, added, removed)
|
|
395
401
|
|
|
396
402
|
(gated_closure(changed, added, removed, param_files, param_pairs) | @stale_plugin_readers).freeze
|
|
@@ -577,8 +583,10 @@ module Rigor
|
|
|
577
583
|
# surfaces are its return type and its content-mutation effects (no instance/class variable write, no
|
|
578
584
|
# `yield`, no implicit-self call whose transitive effect a caller could observe). For a candidate the
|
|
579
585
|
# session compares its content-mutation effects (pure AST) then re-evaluates its return at every
|
|
580
|
-
# persisted key (a runner probe)
|
|
581
|
-
#
|
|
586
|
+
# persisted key (a runner probe), with the published-constant spellings its body reads (issue #3186,
|
|
587
|
+
# {#guard_constants_unmoved?}); a pair passes only when all three are unchanged. Any missing summary (which is
|
|
588
|
+
# also how a def whose keys could not all be recorded is stored, issues #3116 and #3160), ineligible def,
|
|
589
|
+
# changed effect, changed return, or uncomputable key keeps the pair — the conservative
|
|
582
590
|
# direction the `--verify-incremental` and byte-identical recheck specs backstop.
|
|
583
591
|
def behaviourally_unstable_pairs(changed_pairs, unstable, scan_index)
|
|
584
592
|
return changed_pairs if @return_summaries.empty? || scan_index.nil?
|
|
@@ -1236,10 +1244,79 @@ module Rigor
|
|
|
1236
1244
|
# with anything its closure demanded for itself, so this is the same union the next run replays.
|
|
1237
1245
|
@run_level_rows = runner.run_level_rows
|
|
1238
1246
|
@synthetic_surface_digest = synthetic_surface_digest_of(runner)
|
|
1239
|
-
|
|
1247
|
+
refresh_gated_state(runner, analyze_set)
|
|
1240
1248
|
refresh_effect_collections(runner, analyze_set)
|
|
1241
1249
|
end
|
|
1242
1250
|
|
|
1251
|
+
# The summaries, then the skipped readers' edges, in that order ({#refresh_return_summaries}), with what the
|
|
1252
|
+
# gate's probe observed for this recheck taken and cleared. The clear is a guard no spec pins:
|
|
1253
|
+
# {#affected_closure} resets the slot before every {#absorb}, so nothing reads it again. It only keeps a
|
|
1254
|
+
# consumed observation from outliving its recheck, should absorb gain another caller.
|
|
1255
|
+
def refresh_gated_state(runner, analyze_set)
|
|
1256
|
+
observations = @gate_observations
|
|
1257
|
+
@gate_observations = nil
|
|
1258
|
+
refresh_return_summaries(runner, analyze_set, observations)
|
|
1259
|
+
absorb_gate_reads(observations, analyze_set)
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1262
|
+
# Issue #3180 — give every reader the ADR-89 WD2 gate skipped the edges its probe recorded for the pair it
|
|
1263
|
+
# passed ({#gate_observations}): the reader reaches those defs, classes and names through the edited def now,
|
|
1264
|
+
# and no analysis of the reader will record that until something else re-analyses it. The probe evaluated the
|
|
1265
|
+
# def at all of its stored keys and a reader observes it under some of them, so the reader may gain an edge it
|
|
1266
|
+
# does not need; that costs a re-analysis, never an answer. Runs after {#refresh_return_summaries}, which reads
|
|
1267
|
+
# the dependents as they stood before these edges. Independent of the summaries: a pooled recheck over
|
|
1268
|
+
# summaries a sequential run stored can pass the gate too, and its skipped readers need the edges as much.
|
|
1269
|
+
def absorb_gate_reads(observations, analyze_set)
|
|
1270
|
+
return if observations.nil?
|
|
1271
|
+
|
|
1272
|
+
analyzed = analyze_set.to_set
|
|
1273
|
+
skipped = observations.reads.keys.to_h do |pair|
|
|
1274
|
+
[pair, ((@symbol_dependents[pair] || Set.new) - analyzed).to_a]
|
|
1275
|
+
end
|
|
1276
|
+
return if skipped.each_value.all?(&:empty?)
|
|
1277
|
+
|
|
1278
|
+
credit_probe_plugin_rows(observations, skipped)
|
|
1279
|
+
skipped.each { |pair, readers| readers.each { |reader| add_recorded_reads(reader, observations.reads[pair]) } }
|
|
1280
|
+
invert_dependency_graph
|
|
1281
|
+
end
|
|
1282
|
+
|
|
1283
|
+
# Issue #3187 — the plugin reads the probe made while evaluating a passed pair's def, credited to each reader of
|
|
1284
|
+
# the pair it skipped, as an analysis of that reader would have credited them: a later edit of what the plugin
|
|
1285
|
+
# read re-analyses the reader ({#plugin_read_affected}). Like the edges, a reader may gain a row it does not
|
|
1286
|
+
# need, which costs a re-analysis. Run-scoped registrations are not credited: a pair whose probe registered one
|
|
1287
|
+
# does not pass ({#readers_hold_disclosures?}).
|
|
1288
|
+
def credit_probe_plugin_rows(observations, skipped)
|
|
1289
|
+
skipped.each do |pair, readers|
|
|
1290
|
+
rows = observations.plugin_reads[pair]
|
|
1291
|
+
next if rows.nil?
|
|
1292
|
+
|
|
1293
|
+
readers.each { |reader| @plugin_reads[reader] = ((@plugin_reads[reader] || []) | rows).freeze }
|
|
1294
|
+
end
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
# `record`'s reads added to `reader`'s, less the reads of `reader`'s own file (a file records no edge to
|
|
1298
|
+
# itself). Builds new sets: a restored snapshot's are not this session's to mutate. The self-filter on the
|
|
1299
|
+
# ancestry edges is a guard no spec pins: it mirrors the two that are pinned, and no row was built in which
|
|
1300
|
+
# the edited def comes to read the ancestry of a class its skipped reader's file declares.
|
|
1301
|
+
def add_recorded_reads(reader, record)
|
|
1302
|
+
symbols = (@symbol_sources[reader] || {}).dup
|
|
1303
|
+
record.symbol_sources.each do |path, names|
|
|
1304
|
+
symbols[path] = (symbols[path] || Set.new) | names unless path == reader
|
|
1305
|
+
end
|
|
1306
|
+
@symbol_sources[reader] = symbols
|
|
1307
|
+
@ancestry_sources[reader] = (@ancestry_sources[reader] || Set.new) | (record.ancestry_sources - [reader])
|
|
1308
|
+
@sources[reader] = (@sources[reader] || Set.new) | (record.sources - [reader])
|
|
1309
|
+
@missing[reader] = (@missing[reader] || Set.new) | record.missing
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1312
|
+
# The inverted indexes of the four per-consumer edge tables.
|
|
1313
|
+
def invert_dependency_graph
|
|
1314
|
+
@dependents = Incremental.invert(@sources)
|
|
1315
|
+
@symbol_dependents = Incremental.invert_symbols(@symbol_sources)
|
|
1316
|
+
@ancestry_dependents = Incremental.invert(@ancestry_sources)
|
|
1317
|
+
@negative_dependents = Incremental.invert(@missing)
|
|
1318
|
+
end
|
|
1319
|
+
|
|
1243
1320
|
# ADR-103 WD13 / #382 — the effects analogue of {#refresh_return_summaries}, and the same soundness
|
|
1244
1321
|
# order: drop every re-analyzed file's collection first, then fold this run's harvest in. A file the
|
|
1245
1322
|
# recheck did NOT analyze keeps the collection the snapshot carried, which is exactly the reuse the
|
|
@@ -1255,21 +1332,97 @@ module Rigor
|
|
|
1255
1332
|
@effect_plugin_digest = effect_plugin_digest_of(runner)
|
|
1256
1333
|
end
|
|
1257
1334
|
|
|
1258
|
-
# ADR-89 WD2 —
|
|
1259
|
-
# the
|
|
1260
|
-
#
|
|
1261
|
-
#
|
|
1262
|
-
#
|
|
1263
|
-
#
|
|
1264
|
-
#
|
|
1265
|
-
#
|
|
1266
|
-
#
|
|
1267
|
-
#
|
|
1268
|
-
#
|
|
1269
|
-
|
|
1335
|
+
# ADR-89 WD2 / issues #3160, #3180 — bring the behavioural summaries up to date with THIS run. The invariant
|
|
1336
|
+
# the gate reads them under: a pair's summary holds every key under which a CURRENT symbol dependent of the
|
|
1337
|
+
# def observes it, directly or through other defs, each with the def's current return; and every file that
|
|
1338
|
+
# observes the def is one of its symbol dependents. A summary that cannot be shown to is deleted, and a pair
|
|
1339
|
+
# with no summary keeps its dependents, so every doubt below resolves to "no summary".
|
|
1340
|
+
#
|
|
1341
|
+
# A run's harvest ({Runner#return_summaries}) is what the files it analysed observed. The keys the gate's
|
|
1342
|
+
# probe observed for the pairs it passed (`observations`, {#gate_observations}) count as this run's too: they
|
|
1343
|
+
# are what the readers the gate skipped observe through the edited def. So per pair:
|
|
1344
|
+
#
|
|
1345
|
+
# - the run cannot list the keys it observed (a pool run, a def past the per-def or total cap, a call the
|
|
1346
|
+
# memo refused to store): delete;
|
|
1347
|
+
# - every dependent was analysed: the harvest is the whole key set, and replaces the summary (deleting it
|
|
1348
|
+
# when no analysed file observed the def, which is where a def with no dependent left ends up);
|
|
1349
|
+
# - otherwise the summary keeps the keys of the dependents that were not analysed, and gains this run's
|
|
1350
|
+
# ({#merged_return_summary}), which are none when no dependent was analysed. The kept returns are current:
|
|
1351
|
+
# a dependent stays out of the closure of its callee's edit only when the gate proved the returns unmoved
|
|
1352
|
+
# at every kept key, or when the def's own text did not change. With no summary to keep, the other
|
|
1353
|
+
# dependents' keys are unknown.
|
|
1354
|
+
#
|
|
1355
|
+
# Fewer keys are never the conservative direction: each key protects the readers that observe the def under
|
|
1356
|
+
# it, so a summary missing one skips a reader whose answer the edit moved.
|
|
1357
|
+
#
|
|
1358
|
+
# "Every dependent" is read off the edges as they stood BEFORE {#absorb_gate_reads} adds the probe's. A
|
|
1359
|
+
# skipped reader that gains its first edge to a def there observes it only through the pair the gate passed,
|
|
1360
|
+
# so the probe's keys are all of its keys; one that already held an edge was not analysed, and the pair keeps
|
|
1361
|
+
# its stored keys.
|
|
1362
|
+
def refresh_return_summaries(runner, analyze_set, observations = nil)
|
|
1363
|
+
harvest = harvest_with_gate_summaries(runner.return_summaries, observations&.summaries)
|
|
1270
1364
|
analyzed = analyze_set.to_set
|
|
1271
|
-
@return_summaries.
|
|
1272
|
-
|
|
1365
|
+
(@return_summaries.keys | (harvest || {}).keys).each do |pair|
|
|
1366
|
+
dependents = @symbol_dependents[pair] || Set.new
|
|
1367
|
+
summary = refreshed_return_summary(pair, harvest, dependents.subset?(analyzed))
|
|
1368
|
+
summary.nil? ? @return_summaries.delete(pair) : @return_summaries[pair] = summary
|
|
1369
|
+
end
|
|
1370
|
+
@return_summaries = capped_return_summaries(@return_summaries)
|
|
1371
|
+
end
|
|
1372
|
+
|
|
1373
|
+
# Issue #3180 — `harvest` (this run's) with the keys the gate's probe observed (`probed`, see
|
|
1374
|
+
# {#gate_observations}) counted as observed by this run: they are what the readers the gate skipped observe
|
|
1375
|
+
# through the def it passed, each with the callee's current return. A def either side cannot list the keys
|
|
1376
|
+
# of has none, and so has one whose keys together pass the per-def cap. A run with no harvest stays without.
|
|
1377
|
+
def harvest_with_gate_summaries(harvest, probed)
|
|
1378
|
+
return harvest if harvest.nil? || probed.nil? || probed.empty?
|
|
1379
|
+
|
|
1380
|
+
probed.each_with_object(harvest.dup) do |(pair, summary), merged|
|
|
1381
|
+
mine = merged.fetch(pair, summary)
|
|
1382
|
+
merged[pair] = mine.nil? || summary.nil? ? nil : merged_return_summary(mine, summary)
|
|
1383
|
+
end
|
|
1384
|
+
end
|
|
1385
|
+
|
|
1386
|
+
# ADR-89 WD2 — the full-run behavioural surface: every file was analysed, so the harvest is every reader's.
|
|
1387
|
+
# A def the run could not list the keys of (nil in the harvest) and a pool run (no harvest) record nothing.
|
|
1388
|
+
def baseline_return_summaries(runner)
|
|
1389
|
+
capped_return_summaries((runner.return_summaries || {}).compact)
|
|
1390
|
+
end
|
|
1391
|
+
|
|
1392
|
+
# `pair`'s summary after a run whose harvest is `harvest` (nil for a run that has none), or nil for none.
|
|
1393
|
+
# `complete` says every dependent of the def was analysed by that run.
|
|
1394
|
+
def refreshed_return_summary(pair, harvest, complete)
|
|
1395
|
+
return nil if harvest.nil?
|
|
1396
|
+
|
|
1397
|
+
fresh = harvest[pair]
|
|
1398
|
+
return nil if fresh.nil? && harvest.key?(pair)
|
|
1399
|
+
return fresh if complete
|
|
1400
|
+
|
|
1401
|
+
merged_return_summary(@return_summaries[pair], fresh)
|
|
1402
|
+
end
|
|
1403
|
+
|
|
1404
|
+
# Issue #3160 — `kept` (the summary of a def some of whose dependents this run did not analyse) with the
|
|
1405
|
+
# keys `fresh` (this run's harvest for the def, or nil) adds. A kept key stays unless `fresh` holds the same
|
|
1406
|
+
# key with the same return; one whose return differs stays beside it, which no re-evaluation can satisfy, so
|
|
1407
|
+
# the gate keeps the def's dependents. nil when there is nothing to keep or the keys pass the per-def cap.
|
|
1408
|
+
def merged_return_summary(kept, fresh)
|
|
1409
|
+
return nil if kept.nil?
|
|
1410
|
+
return kept if fresh.nil?
|
|
1411
|
+
|
|
1412
|
+
rows = fresh[:keys].zip(fresh[:returns])
|
|
1413
|
+
rows += kept[:keys].zip(kept[:returns]).reject { |row| rows.include?(row) }
|
|
1414
|
+
return nil if rows.size > Runner::RETURN_SUMMARY_KEYS_PER_DEF
|
|
1415
|
+
|
|
1416
|
+
{ keys: rows.map(&:first), returns: rows.map(&:last), effects: fresh[:effects],
|
|
1417
|
+
guard_constants: fresh[:guard_constants] }
|
|
1418
|
+
end
|
|
1419
|
+
|
|
1420
|
+
# The first {Runner::RETURN_SUMMARY_TOTAL_CAP} of `summaries`, so the snapshot stays small however many runs
|
|
1421
|
+
# fed it. A def past the cap has no summary, which keeps its dependents.
|
|
1422
|
+
def capped_return_summaries(summaries)
|
|
1423
|
+
return summaries if summaries.size <= Runner::RETURN_SUMMARY_TOTAL_CAP
|
|
1424
|
+
|
|
1425
|
+
summaries.first(Runner::RETURN_SUMMARY_TOTAL_CAP).to_h
|
|
1273
1426
|
end
|
|
1274
1427
|
|
|
1275
1428
|
# Evict a removed file from every per-file map so its stale diagnostics are never served and it drops
|
|
@@ -1302,10 +1455,7 @@ module Rigor
|
|
|
1302
1455
|
@ancestry_sources[path] = record.ancestry_sources.dup
|
|
1303
1456
|
@missing[path] = record.missing.dup
|
|
1304
1457
|
end
|
|
1305
|
-
|
|
1306
|
-
@symbol_dependents = Incremental.invert_symbols(@symbol_sources)
|
|
1307
|
-
@ancestry_dependents = Incremental.invert(@ancestry_sources)
|
|
1308
|
-
@negative_dependents = Incremental.invert(@missing)
|
|
1458
|
+
invert_dependency_graph
|
|
1309
1459
|
@symbol_fingerprints.merge!(runner.symbol_fingerprints)
|
|
1310
1460
|
# Wholesale replace (the subset runner's pre-pass is complete): a file that lost its last class must
|
|
1311
1461
|
# drop out of the map so a later re-add registers as an appearance.
|
|
@@ -1394,6 +1544,11 @@ module Rigor
|
|
|
1394
1544
|
# (`ResolutionChain#record_hooks`), so a new file that taints `Comparable` with `include(*MODS)` re-checks the
|
|
1395
1545
|
# readers of a chain through it. It is not `mark:`, which a plain reopening of a module moves (the bucket's
|
|
1396
1546
|
# module mark) and which would re-check every reader of `Kernel` on any `module Kernel` reopening.
|
|
1547
|
+
#
|
|
1548
|
+
# Issue #3005 — a moved {Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK} fires the owner's `mixin:` keys too: a new
|
|
1549
|
+
# file whose body-level `send(:then)` marks an external `Comparable` makes a reader of a chain through it decline,
|
|
1550
|
+
# and that reader filed `mixin:Comparable`, not `mark:Comparable`. The mark moves only with such a call, not on a
|
|
1551
|
+
# plain reopening.
|
|
1397
1552
|
def mark_affected(paths, summary)
|
|
1398
1553
|
def_index = summary && summary[:def_index]
|
|
1399
1554
|
before = paths.map { |path| @seed_bundles.dig(path, :parameter_envelopes) }
|
|
@@ -1401,7 +1556,7 @@ module Rigor
|
|
|
1401
1556
|
tainted = Incremental.changed_unpositioned_owners(
|
|
1402
1557
|
paths.map { |path| @seed_bundles.dig(path, :unpositioned_mixins) },
|
|
1403
1558
|
[def_index && def_index[:unpositioned_mixins]]
|
|
1404
|
-
)
|
|
1559
|
+
) | dynamic_owners(moved)
|
|
1405
1560
|
return Set.new if moved.empty? && tainted.empty?
|
|
1406
1561
|
|
|
1407
1562
|
keys = moved.map { |(class_name, _mark)| "mark:#{class_name.split('::').last}" }.uniq
|
|
@@ -1412,6 +1567,11 @@ module Rigor
|
|
|
1412
1567
|
Incremental.negative_closure(keys, @negative_dependents)
|
|
1413
1568
|
end
|
|
1414
1569
|
|
|
1570
|
+
def dynamic_owners(moved)
|
|
1571
|
+
dynamic = Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK
|
|
1572
|
+
moved.filter_map { |(class_name, mark)| class_name if mark == dynamic }
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1415
1575
|
# Issue #1661 — the `mixin:` keys an owner of a moved unpositioned edge fires. The table keys a receiver-form
|
|
1416
1576
|
# owner by the spelling written (`class File; Constants.include(*MODS); end` is `"Constants"`), not the constant
|
|
1417
1577
|
# Ruby resolves it to, while the chain files the qualified candidate (`mixin:File::Constants`). The table does
|
|
@@ -1556,7 +1716,9 @@ module Rigor
|
|
|
1556
1716
|
|
|
1557
1717
|
# ADR-89 WD2 — true when the ONLY cross-file body surfaces of `node` a symbol dependent can consume are
|
|
1558
1718
|
# its return type and its content-mutation effects — so comparing exactly those two is a COMPLETE
|
|
1559
|
-
# behavioural check.
|
|
1719
|
+
# behavioural check. (Issue #3186: the truthiness rule's one-hop body scan reads a third, the published
|
|
1720
|
+
# constants the body names, which the probe compares too; no node kind makes a def ineligible for it.)
|
|
1721
|
+
# A def qualifies when its body writes no instance / class variable (an ivar
|
|
1560
1722
|
# definite-assignment a same-class caller consumes), does not `yield` (a block-parameter surface a
|
|
1561
1723
|
# caller passing a block consumes), and makes no implicit-self call (which could carry a transitive
|
|
1562
1724
|
# shared-state write). Deliberately conservative + purely syntactic: a def with a self-call or a
|
|
@@ -1608,29 +1770,104 @@ module Rigor
|
|
|
1608
1770
|
end
|
|
1609
1771
|
|
|
1610
1772
|
def content_effects_for(node)
|
|
1611
|
-
|
|
1612
|
-
.content_mutated_parameter_positions(node)
|
|
1773
|
+
effects_evaluator.content_mutated_parameter_positions(node)
|
|
1613
1774
|
rescue StandardError
|
|
1614
1775
|
nil
|
|
1615
1776
|
end
|
|
1616
1777
|
|
|
1778
|
+
# The evaluator {#content_effects_for} asks, kept for as long as the calling thread's {TypingState} is the one it
|
|
1779
|
+
# was built under. An evaluator caches that state, and the owner evaluators it builds cache theirs, so one kept
|
|
1780
|
+
# across the language server's save rounds, each on a thread of its own, would keep writing its run-scoped memos
|
|
1781
|
+
# into a finished round's state, which that round's release has already emptied (ADR-129).
|
|
1782
|
+
def effects_evaluator
|
|
1783
|
+
state = TypingState.current
|
|
1784
|
+
unless @effects_state.equal?(state)
|
|
1785
|
+
@effects_state = state
|
|
1786
|
+
@effects_evaluator = Inference::StatementEvaluator.new(scope: Scope.empty)
|
|
1787
|
+
end
|
|
1788
|
+
@effects_evaluator
|
|
1789
|
+
end
|
|
1790
|
+
|
|
1617
1791
|
# ADR-89 WD2 — the subset of `pairs` whose (edited) def re-evaluates to the SAME return descriptor at
|
|
1618
1792
|
# every persisted key. Runs one runner probe (`evaluate_return_types` — builds the discovery + env once,
|
|
1619
1793
|
# no file analysis) over all candidate defs, then keeps a pair only when every key's re-evaluated
|
|
1620
1794
|
# descriptor is present (not a memo refusal) and equal to the persisted one. A probe failure yields an
|
|
1621
1795
|
# empty set (keep everything).
|
|
1622
1796
|
def return_stable_pairs(pairs, scan_index)
|
|
1623
|
-
specs = pairs.
|
|
1797
|
+
specs = pairs.reject { |pair| readers_hold_disclosures?(pair) }.filter_map do |pair|
|
|
1798
|
+
return_spec_for(pair, scan_index)
|
|
1799
|
+
end
|
|
1624
1800
|
return Set.new if specs.empty?
|
|
1625
1801
|
|
|
1626
|
-
results = probe_return_types(specs)
|
|
1627
|
-
specs.each_with_object(
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
next
|
|
1631
|
-
|
|
1802
|
+
runner, results = probe_return_types(specs)
|
|
1803
|
+
passed = specs.each_with_object({}) do |spec, pairs|
|
|
1804
|
+
triple = [spec[:class_name], spec[:method_name], spec[:singleton]]
|
|
1805
|
+
next unless returns_unmoved?(spec[:pair], results[triple])
|
|
1806
|
+
next unless guard_constants_unmoved?(spec[:pair], runner.probe_guard_constants[triple])
|
|
1807
|
+
# Issue #3187 — a run-scoped row the probe registered keeps the readers ({#readers_hold_disclosures?}).
|
|
1808
|
+
next if runner.probe_disclosures.key?(triple)
|
|
1632
1809
|
|
|
1633
|
-
|
|
1810
|
+
pairs[spec[:pair]] = triple
|
|
1811
|
+
end
|
|
1812
|
+
@gate_observations = gate_observations(runner, passed) unless passed.empty?
|
|
1813
|
+
passed.keys.to_set
|
|
1814
|
+
end
|
|
1815
|
+
|
|
1816
|
+
# Issue #3187 — whether a symbol dependent of `pair` holds a run-scoped plugin row ({RunDisclosureLedger}), which
|
|
1817
|
+
# keeps the pair's readers, as a probe that registers one does. A skipped reader replays the rows its last
|
|
1818
|
+
# analysis registered, and which of them came through the edited def, under which key, is not recorded; nor
|
|
1819
|
+
# can rows the probe registers be credited to a reader without that key, or ever be retracted once its def
|
|
1820
|
+
# stops registering them. So a reader holding any row is re-analysed, which settles its rows afresh.
|
|
1821
|
+
def readers_hold_disclosures?(pair)
|
|
1822
|
+
(@symbol_dependents[pair] || Set.new).any? { |reader| @plugin_disclosures.files.key?(reader) }
|
|
1823
|
+
end
|
|
1824
|
+
|
|
1825
|
+
# Issue #3186 — whether the probe's `current` constant spellings ({Runner#guard_constants}) are `pair`'s stored
|
|
1826
|
+
# ones. A reader's `flow.always-truthy-condition` withholds when a def it calls reads a constant another file
|
|
1827
|
+
# published, whatever the def returns, so a return-preserving edit that adds or drops such a read moves the
|
|
1828
|
+
# reader's diagnostics. nil on either side (a list the runner could not make, a summary stored before the list
|
|
1829
|
+
# existed) reads as moved.
|
|
1830
|
+
def guard_constants_unmoved?(pair, current)
|
|
1831
|
+
stored = @return_summaries[pair][:guard_constants]
|
|
1832
|
+
!current.nil? && !stored.nil? && current == stored
|
|
1833
|
+
end
|
|
1834
|
+
|
|
1835
|
+
# Whether the probe's `descriptors` are `pair`'s stored returns, key for key, none of them a memo refusal.
|
|
1836
|
+
def returns_unmoved?(pair, descriptors)
|
|
1837
|
+
expected = @return_summaries[pair][:returns]
|
|
1838
|
+
return false if descriptors.nil? || descriptors.length != expected.length
|
|
1839
|
+
|
|
1840
|
+
descriptors.each_with_index.all? { |descriptor, index| !descriptor.nil? && descriptor == expected[index] }
|
|
1841
|
+
end
|
|
1842
|
+
|
|
1843
|
+
# Issue #3180 — what the gate's probe observed for the pairs it passed. A reader the gate skips is not
|
|
1844
|
+
# re-analysed, yet it now reaches whatever the edited def reaches: a callee the def did not call before, or
|
|
1845
|
+
# one it calls under another key. The probe evaluated the def at every key its readers observe it under, so
|
|
1846
|
+
# its reads are a superset of theirs through this def, and its memo holds every key those evaluations put to
|
|
1847
|
+
# another def. `reads` is each passing pair's {DependencyRecorder::Record}; `summaries` is the probe's own
|
|
1848
|
+
# harvest ({Runner#return_summaries}: nil for a def it cannot list the keys of, nil as a whole in pool mode),
|
|
1849
|
+
# cut down to the defs a passing pair read. A failing pair's observations are left out: its readers are all
|
|
1850
|
+
# re-analysed, and the run's harvest speaks for them. Issue #3187 — `plugin_reads` is each passing pair's plugin
|
|
1851
|
+
# boundary rows ({Runner#probe_plugin_reads}); a pair whose evaluation read none has no key.
|
|
1852
|
+
GateObservations = Data.define(:reads, :summaries, :plugin_reads)
|
|
1853
|
+
private_constant :GateObservations
|
|
1854
|
+
|
|
1855
|
+
# `passed` maps each passing pair to its spec triple. The probe recorded reads for every def it evaluated, and
|
|
1856
|
+
# it evaluated each of these: a summary holds at least one key, so a def the probe could not find answered nil
|
|
1857
|
+
# and did not pass.
|
|
1858
|
+
def gate_observations(runner, passed)
|
|
1859
|
+
reads = passed.transform_values { |triple| runner.probe_reads[triple] }
|
|
1860
|
+
observed = reads.each_value.with_object(Set.new) do |record, pairs|
|
|
1861
|
+
record.symbol_sources.each { |path, symbols| symbols.each { |symbol| pairs << [path, symbol] } }
|
|
1862
|
+
end
|
|
1863
|
+
GateObservations.new(reads: reads, summaries: runner.return_summaries&.slice(*observed),
|
|
1864
|
+
plugin_reads: probe_rows(passed, runner.probe_plugin_reads))
|
|
1865
|
+
end
|
|
1866
|
+
|
|
1867
|
+
# `table` (spec triple => rows) re-keyed by each passing pair, leaving out a pair with no rows.
|
|
1868
|
+
def probe_rows(passed, table)
|
|
1869
|
+
passed.each_with_object({}) do |(pair, triple), rows|
|
|
1870
|
+
rows[pair] = table[triple] if table.key?(triple)
|
|
1634
1871
|
end
|
|
1635
1872
|
end
|
|
1636
1873
|
|
|
@@ -1645,10 +1882,15 @@ module Rigor
|
|
|
1645
1882
|
singleton: singleton, keys: @return_summaries[pair][:keys] }
|
|
1646
1883
|
end
|
|
1647
1884
|
|
|
1885
|
+
# The probe's runner and its `spec triple => descriptors` answer ({} when the probe failed).
|
|
1648
1886
|
def probe_return_types(specs)
|
|
1649
|
-
build_runner
|
|
1887
|
+
runner = build_runner
|
|
1888
|
+
[runner, runner.evaluate_return_types(@paths, specs)]
|
|
1889
|
+
rescue Inference::MemoAudit::Mismatch
|
|
1890
|
+
# ADR-129 WD5 (#2785) — the audit's mismatch (raised by the run's release) fails the session, not the probe.
|
|
1891
|
+
raise
|
|
1650
1892
|
rescue StandardError
|
|
1651
|
-
{}
|
|
1893
|
+
[runner, {}]
|
|
1652
1894
|
end
|
|
1653
1895
|
|
|
1654
1896
|
TOP_LEVEL_KEY = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../../plugin"
|
|
4
4
|
require_relative "../../plugin/loader"
|
|
5
5
|
require_relative "../../plugin/services"
|
|
6
|
+
require_relative "../../plugin/trust_policy"
|
|
6
7
|
require_relative "../../reflection"
|
|
7
8
|
require_relative "../../type/combinator"
|
|
8
9
|
|
|
@@ -107,14 +108,16 @@ module Rigor
|
|
|
107
108
|
end
|
|
108
109
|
|
|
109
110
|
# Mirrors `Analysis::WorkerSession`'s services: the shared fact store plus whatever cache store the
|
|
110
|
-
# caller holds
|
|
111
|
-
#
|
|
111
|
+
# caller holds, and the run's trust policy — `rigor unused`'s plugin roots read the same scope
|
|
112
|
+
# `rigor check` does (#2586). Holding the `Services` is what gives access to the fact store the
|
|
113
|
+
# loaded plugins share — the loader hands the same instance to every plugin.
|
|
112
114
|
def build_services(configuration, cache_store)
|
|
113
115
|
Plugin::Services.new(
|
|
114
116
|
reflection: Reflection,
|
|
115
117
|
type: Type::Combinator,
|
|
116
118
|
configuration: configuration,
|
|
117
|
-
cache_store: cache_store
|
|
119
|
+
cache_store: cache_store,
|
|
120
|
+
trust_policy: Plugin::TrustPolicy.for_configuration(configuration)
|
|
118
121
|
)
|
|
119
122
|
end
|
|
120
123
|
|
|
@@ -101,7 +101,9 @@ module Rigor
|
|
|
101
101
|
"of the class are open, and an inherited Object / Kernel signature no longer decides " \
|
|
102
102
|
"`call.wrong-arity` or `call.argument-type-mismatch` on it.",
|
|
103
103
|
"The program may answer the call another way (#1722): a `method_missing` on the receiver's class or an " \
|
|
104
|
-
"ancestor
|
|
104
|
+
"ancestor, in source or in RBS (#2792; an RBS `method_missing` on `BasicObject`, `Object` or " \
|
|
105
|
+
"`Kernel` does not count); " \
|
|
106
|
+
"a module mixed into either that defines the method (`Integer.include(Ext)` counts wherever " \
|
|
105
107
|
"it is written); singleton methods given to the receiver local (`s.extend(M)`, " \
|
|
106
108
|
"`define_singleton_method`, `class << s`); or, for a receiver typed by its class's name rather than a " \
|
|
107
109
|
"literal, a project subclass that defines the method."
|
|
@@ -1087,6 +1089,40 @@ module Rigor
|
|
|
1087
1089
|
since: "0.4.0"
|
|
1088
1090
|
),
|
|
1089
1091
|
|
|
1092
|
+
CheckRules::RULE_PLUGIN_API_SURFACE => Entry.new(
|
|
1093
|
+
id: CheckRules::RULE_PLUGIN_API_SURFACE,
|
|
1094
|
+
summary: "Plugin code calls a member a `Rigor::Plugin` interface does not declare.",
|
|
1095
|
+
fires_when: [
|
|
1096
|
+
"The call's receiver is typed by a `Rigor::Plugin` interface (`_Scope`, `_Environment`, " \
|
|
1097
|
+
"`_Services`, `_FactStore`, `_Configuration`), possibly with `nil`: a hook's `scope`, " \
|
|
1098
|
+
"`scope.environment`, `services.fact_store`, and so on.",
|
|
1099
|
+
"The interface does not declare the called method, and neither does `Object` (with `Kernel` and " \
|
|
1100
|
+
"`BasicObject`).",
|
|
1101
|
+
"The call sits inside a class whose superclass chain reaches `Rigor::Plugin::Base`.",
|
|
1102
|
+
"The `plugin-api-surface` bleeding-edge feature is enabled, or a `severity_overrides:` key names " \
|
|
1103
|
+
"`plugin.api-surface` (the rule resolves `:off` otherwise; a dot-free `plugin` key does not reach it)."
|
|
1104
|
+
],
|
|
1105
|
+
does_not_fire_when: [
|
|
1106
|
+
"The interface declares the method (`scope.type_of(node)`), or every object answers it " \
|
|
1107
|
+
"(`scope.nil?`, `scope.respond_to?(...)`). A call that a `respond_to?` guard protects still fires.",
|
|
1108
|
+
"The value lost its interface type: a join with an untyped value, a helper parameter with no " \
|
|
1109
|
+
"signature, or the result of a `&.` call on a value that may be nil. It reads as `untyped` there.",
|
|
1110
|
+
"The call is outside a `Rigor::Plugin::Base` subclass, or inside `Rigor::Plugin::Base` itself.",
|
|
1111
|
+
"The `plugin-api-surface` bleeding-edge feature is not enabled (the default)."
|
|
1112
|
+
],
|
|
1113
|
+
suppression: "`# rigor:disable plugin.api-surface` on the call line, or " \
|
|
1114
|
+
"`disable: [\"plugin.api-surface\"]` in `.rigor.yml`. The fix is to call a member the " \
|
|
1115
|
+
"interface declares or a public namespace (`Rigor::Reflection`, ...).",
|
|
1116
|
+
severity_authored: :warning,
|
|
1117
|
+
severity_by_profile: { lenient: :off, balanced: :off, strict: :off },
|
|
1118
|
+
# The receiver's interface type comes from Rigor's own signatures and the member list is the
|
|
1119
|
+
# interface's declaration, so a firing is a call outside the declared surface. It ships `:off`
|
|
1120
|
+
# behind `bleeding_edge:` because it is a new discipline (ADR-50 Decision 3), not for lack of
|
|
1121
|
+
# confidence.
|
|
1122
|
+
evidence_tier: :high,
|
|
1123
|
+
since: "0.4.8"
|
|
1124
|
+
),
|
|
1125
|
+
|
|
1090
1126
|
CheckRules::RULE_SUPPRESSION_UNKNOWN_MARKER => Entry.new(
|
|
1091
1127
|
id: CheckRules::RULE_SUPPRESSION_UNKNOWN_MARKER,
|
|
1092
1128
|
summary: "A comment uses a suppression marker Rigor does not recognise " \
|
|
@@ -99,7 +99,8 @@ module Rigor
|
|
|
99
99
|
gems: [Cache::RbsDescriptor.rbs_gem_entry],
|
|
100
100
|
dependencies: dependency_source_entries(configuration),
|
|
101
101
|
configs: rbs_config_entries + engine_source_entries + lockfile_entries(configuration) +
|
|
102
|
-
|
|
102
|
+
collection_setup_hint_entries(configuration) + template_unit_entries(template_units_digest) +
|
|
103
|
+
shadow_harness_entries + [
|
|
103
104
|
config_entry("configuration", Marshal.dump(configuration.to_h)),
|
|
104
105
|
config_entry("engine",
|
|
105
106
|
"#{Rigor::VERSION}:#{Cache::Descriptor::SCHEMA_VERSION}:#{explain}"),
|
|
@@ -179,6 +180,24 @@ module Rigor
|
|
|
179
180
|
[lockfile_entry("bundler.lockfile", bundler), lockfile_entry("rbs_collection.lockfile", collection)]
|
|
180
181
|
end
|
|
181
182
|
|
|
183
|
+
# Issue #3030 — the `rbs.coverage.collection-not-installed` hint is a run-level row read off the working
|
|
184
|
+
# directory (an `rbs_collection.yaml`, a gem_rbs_collection `_test/` layout), which neither the lockfile slot
|
|
185
|
+
# nor the configuration covers: without this slot a warm run replayed the hint after the file went, or
|
|
186
|
+
# missed it after the file came. No slot when there is no hint, so every other project keeps its key.
|
|
187
|
+
def collection_setup_hint_entries(configuration)
|
|
188
|
+
hint = collection_setup_hint(configuration)
|
|
189
|
+
hint.nil? ? [] : [config_entry("rbs_collection.setup_hint", hint.to_s)]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The hint {Analysis::Runner::DiagnosticAggregator} reports, read the same way. Shared with
|
|
193
|
+
# {Cache::IncrementalSnapshot.fingerprint}.
|
|
194
|
+
def collection_setup_hint(configuration)
|
|
195
|
+
Environment::RbsCollectionDiscovery.missing_lockfile_hint(
|
|
196
|
+
lockfile_path: configuration.rbs_collection_lockfile,
|
|
197
|
+
auto_detect: configuration.rbs_collection_auto_detect
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
182
201
|
# ADR-10 slice 3, issue #1814 — the `dependencies.source_inference` gems' `(name, version, mode)` rows, which the
|
|
183
202
|
# walker's output depends on and no lockfile digest covers when the project has none or the gem sits beside the
|
|
184
203
|
# bundle. Shared by this key and by {Cache::IncrementalSnapshot.fingerprint}, as the lockfiles are. A project
|
|
@@ -216,12 +235,17 @@ module Rigor
|
|
|
216
235
|
|
|
217
236
|
# The `rbs.libraries` config slot reconstructed from configuration alone — byte-identical to the
|
|
218
237
|
# loader's `RbsDescriptor.libraries_entry(loader.libraries)` because `Environment.for_project` merges
|
|
219
|
-
# exactly `DEFAULT_LIBRARIES + config.libraries`
|
|
238
|
+
# exactly {Environment::BaseLibraries.resolve} — `DEFAULT_LIBRARIES + config.libraries` + the collection lockfile
|
|
239
|
+
# stdlib entries (issue #2946), uniq — into `loader.libraries`, plus the
|
|
220
240
|
# `Environment::RequiredFeatures` tokens the run's source activates (issue #1700). Those are left out
|
|
221
241
|
# here: reading every source file would cost the boot-slimming probe the time it exists to save, and a
|
|
222
242
|
# project that requires a gated feature then only misses this probe, never hits a wrong slot.
|
|
223
243
|
def libraries_config_entries(configuration)
|
|
224
|
-
merged =
|
|
244
|
+
merged = Environment::BaseLibraries.resolve(
|
|
245
|
+
configuration.libraries,
|
|
246
|
+
rbs_collection_lockfile: configuration.rbs_collection_lockfile,
|
|
247
|
+
rbs_collection_auto_detect: configuration.rbs_collection_auto_detect
|
|
248
|
+
)
|
|
225
249
|
[Cache::RbsDescriptor.libraries_entry(merged)]
|
|
226
250
|
end
|
|
227
251
|
end
|
|
@@ -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
|
# Issue #2020 — what one file's analysis contributed to the run-scoped plugin tables ({Plugin::Base#disclose_once}
|
|
@@ -17,16 +19,27 @@ module Rigor
|
|
|
17
19
|
ATTRIBUTION_KEY = :rigor_run_disclosure_attribution
|
|
18
20
|
private_constant :ATTRIBUTION_KEY
|
|
19
21
|
|
|
22
|
+
# ADR-129 WD3 / #2814 — the registrations sink is a Resource: a `type_of` memo hit that served an answer without
|
|
23
|
+
# re-running the plugin hook would not append the registration the miss appended, so every {.attribute} refuses
|
|
24
|
+
# each open capture. Its storage stays the thread variable above (ADR-129 WD5 shape 2); this slot declares its
|
|
25
|
+
# kind.
|
|
26
|
+
SINK = TypingState.resource(
|
|
27
|
+
:run_disclosure_attribution,
|
|
28
|
+
reason: "every run-scoped plugin registration appends to the collecting sink, which a memo hit would not replay"
|
|
29
|
+
)
|
|
30
|
+
private_constant :SINK
|
|
31
|
+
|
|
20
32
|
module_function
|
|
21
33
|
|
|
22
34
|
# Runs the block with run-scoped registrations attributed and returns the file's records — each a run-disclosure
|
|
23
35
|
# record tagged with its `plugin_id` and carrying the registering plugin's boundary rows as `:reads`, one per
|
|
24
36
|
# `[plugin id, key]` — frozen and Marshal-clean. A thread variable, as {Plugin::IoBoundary.attributing} uses, so a
|
|
25
|
-
# registration from inside an `Enumerator` still lands
|
|
37
|
+
# registration from inside an `Enumerator` still lands, carrying the installing fiber's typing state as
|
|
38
|
+
# {Plugin::IoBoundary.attributing} does.
|
|
26
39
|
def collect
|
|
27
40
|
registrations = []
|
|
28
41
|
previous = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
|
|
29
|
-
Thread.current.thread_variable_set(ATTRIBUTION_KEY, registrations)
|
|
42
|
+
Thread.current.thread_variable_set(ATTRIBUTION_KEY, [registrations, TypingState.current].freeze)
|
|
30
43
|
begin
|
|
31
44
|
yield
|
|
32
45
|
ensure
|
|
@@ -35,9 +48,16 @@ module Rigor
|
|
|
35
48
|
credit(registrations)
|
|
36
49
|
end
|
|
37
50
|
|
|
38
|
-
# Called by {Plugin::Base#disclose_once} and {Plugin::Base#emit_once} for every call, a repeat included.
|
|
51
|
+
# Called by {Plugin::Base#disclose_once} and {Plugin::Base#emit_once} for every call, a repeat included. Touches
|
|
52
|
+
# {SINK} on the calling fiber's typing state and on the installing fiber's (ADR-129 WD3, #2814).
|
|
39
53
|
def attribute(plugin, record)
|
|
40
|
-
|
|
54
|
+
state = TypingState.current
|
|
55
|
+
state.touch(SINK)
|
|
56
|
+
registrations, owner = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
|
|
57
|
+
return nil if registrations.nil?
|
|
58
|
+
|
|
59
|
+
owner.touch(SINK)
|
|
60
|
+
registrations.push([plugin, record])
|
|
41
61
|
nil
|
|
42
62
|
end
|
|
43
63
|
|
|
@@ -63,9 +83,9 @@ module Rigor
|
|
|
63
83
|
end
|
|
64
84
|
|
|
65
85
|
# The boundary's file and listing rows, read without triggering its lazy initializer: a plugin that never
|
|
66
|
-
# built one read nothing through it.
|
|
86
|
+
# built one read nothing through it. {Plugin::Base#built_io_boundary} is private to the engine, so it is sent.
|
|
67
87
|
def boundary_rows(plugin)
|
|
68
|
-
boundary = plugin.
|
|
88
|
+
boundary = plugin.__send__(:built_io_boundary) or return []
|
|
69
89
|
descriptor = boundary.cache_descriptor
|
|
70
90
|
descriptor.files + descriptor.globs
|
|
71
91
|
end
|