rigortype 0.4.6 → 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 +4 -2
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
- data/docs/handbook/01-getting-started.md +6 -3
- data/docs/handbook/11-sig-gen.md +85 -3
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +455 -113
- data/docs/manual/03-configuration.md +76 -5
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +21 -2
- data/docs/manual/10-mcp-server.md +38 -2
- data/docs/manual/12-caching.md +4 -3
- 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 +32 -15
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
- data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules.rb +720 -141
- data/lib/rigor/analysis/crash_signature.rb +45 -3
- data/lib/rigor/analysis/dependency_recorder.rb +237 -80
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/edit_replay.rb +93 -6
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +306 -53
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +39 -3
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
- data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
- data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
- data/lib/rigor/analysis/runner.rb +298 -109
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
- data/lib/rigor/analysis/template_unit_positions.rb +2 -3
- data/lib/rigor/analysis/template_units.rb +9 -1
- data/lib/rigor/analysis/worker_session.rb +65 -71
- data/lib/rigor/bleeding_edge.rb +22 -10
- data/lib/rigor/builtins/imported_refinements.rb +6 -8
- data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
- data/lib/rigor/builtins/regex_refinement.rb +4 -6
- data/lib/rigor/cache/descriptor.rb +6 -5
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +95 -17
- data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
- data/lib/rigor/cache/rbs_constant_table.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +90 -4
- data/lib/rigor/cache/rbs_environment.rb +8 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
- data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
- data/lib/rigor/cache/store.rb +11 -10
- data/lib/rigor/ci_detector.rb +1 -1
- data/lib/rigor/cli/annotate_command.rb +24 -19
- data/lib/rigor/cli/baseline_command.rb +160 -57
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +112 -104
- data/lib/rigor/cli/check_runner_factory.rb +2 -2
- data/lib/rigor/cli/command.rb +15 -1
- data/lib/rigor/cli/coverage_command.rb +21 -8
- data/lib/rigor/cli/coverage_mutation.rb +5 -7
- data/lib/rigor/cli/coverage_renderer.rb +2 -1
- data/lib/rigor/cli/coverage_scan.rb +3 -5
- data/lib/rigor/cli/diff_command.rb +25 -20
- data/lib/rigor/cli/docs_command.rb +1 -2
- data/lib/rigor/cli/doctor_command.rb +12 -12
- data/lib/rigor/cli/effects_command.rb +50 -40
- data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
- data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
- data/lib/rigor/cli/effects_renderer.rb +2 -1
- data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
- data/lib/rigor/cli/explain_command.rb +18 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
- data/lib/rigor/cli/json_document.rb +41 -0
- data/lib/rigor/cli/lens_command.rb +29 -10
- data/lib/rigor/cli/lsp_command.rb +250 -23
- data/lib/rigor/cli/mcp_command.rb +12 -7
- data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
- data/lib/rigor/cli/options.rb +40 -1
- data/lib/rigor/cli/plugin_command.rb +93 -46
- data/lib/rigor/cli/plugins_command.rb +17 -18
- data/lib/rigor/cli/plugins_renderer.rb +10 -6
- data/lib/rigor/cli/protection_fork_scan.rb +2 -2
- data/lib/rigor/cli/protection_renderer.rb +2 -1
- data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
- data/lib/rigor/cli/sig_gen_command.rb +92 -27
- data/lib/rigor/cli/skill_command.rb +32 -3
- data/lib/rigor/cli/skill_deep_probe.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +14 -9
- data/lib/rigor/cli/triage_command.rb +10 -11
- data/lib/rigor/cli/triage_renderer.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +16 -11
- data/lib/rigor/cli/type_of_project_scope.rb +1 -4
- data/lib/rigor/cli/type_of_renderer.rb +4 -1
- data/lib/rigor/cli/type_scan_command.rb +14 -9
- data/lib/rigor/cli/type_scan_renderer.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +27 -17
- data/lib/rigor/cli/upgrade_command.rb +5 -0
- data/lib/rigor/cli.rb +214 -56
- data/lib/rigor/config_audit.rb +33 -19
- data/lib/rigor/configuration/severity_profile.rb +14 -5
- data/lib/rigor/configuration.rb +104 -35
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/effects/definition_context.rb +0 -2
- data/lib/rigor/effects/envelope.rb +1 -1
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/mutation_classifier.rb +4 -7
- data/lib/rigor/effects/propagator.rb +1 -1
- data/lib/rigor/effects/registry.rb +2 -2
- data/lib/rigor/effects/snapshot.rb +1 -1
- data/lib/rigor/effects/taint_cause.rb +0 -2
- data/lib/rigor/effects/unit_scan.rb +9 -13
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +18 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
- data/lib/rigor/environment/gated_signature_guard.rb +1 -1
- data/lib/rigor/environment/gated_signature_plan.rb +5 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/lockfile_resolver.rb +28 -12
- data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
- data/lib/rigor/environment/member_consistency.rb +56 -7
- data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
- data/lib/rigor/environment/project_path_gems.rb +124 -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 +12 -15
- data/lib/rigor/environment/rbs_loader.rb +772 -110
- data/lib/rigor/environment/reflection.rb +7 -7
- data/lib/rigor/environment/required_features.rb +0 -3
- data/lib/rigor/environment/ruby_library_roots.rb +1 -1
- data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
- data/lib/rigor/environment.rb +169 -42
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +136 -40
- 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_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +185 -0
- data/lib/rigor/inference/budget_trace.rb +8 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- 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 +258 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/deep_erasure.rb +39 -7
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +33 -15
- data/lib/rigor/inference/def_return_typer.rb +47 -5
- data/lib/rigor/inference/definer_resolution.rb +19 -9
- data/lib/rigor/inference/definers_index.rb +162 -0
- data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
- data/lib/rigor/inference/element_read_widening.rb +49 -3
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1559 -320
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- 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 +81 -5
- data/lib/rigor/inference/global_writer_reach.rb +260 -0
- data/lib/rigor/inference/hkt_body.rb +1 -2
- data/lib/rigor/inference/hkt_body_parser.rb +3 -5
- data/lib/rigor/inference/hkt_reducer.rb +2 -2
- data/lib/rigor/inference/hkt_registry.rb +3 -5
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +61 -15
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +154 -8
- data/lib/rigor/inference/key_presence_guard.rb +51 -24
- 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 +66 -14
- data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
- data/lib/rigor/inference/match_rebinding.rb +28 -5
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -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/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +123 -25
- 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 +24 -3
- data/lib/rigor/inference/multi_target_binder.rb +2 -2
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +199 -29
- data/lib/rigor/inference/narrowing.rb +238 -50
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +134 -9
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +30 -9
- 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/project_yielder.rb +11 -17
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- 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 +57 -17
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
- 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 +287 -0
- data/lib/rigor/inference/remover_narrowing.rb +224 -0
- data/lib/rigor/inference/rewrite_mutation.rb +53 -2
- data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
- data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
- data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2213 -226
- data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +3268 -358
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
- data/lib/rigor/inference/toplevel_def_census.rb +67 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -9
- data/lib/rigor/language_server/buffer_table.rb +3 -3
- data/lib/rigor/language_server/completion_provider.rb +26 -20
- data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -2
- data/lib/rigor/language_server/hover_provider.rb +1 -7
- data/lib/rigor/language_server/hover_renderer.rb +6 -8
- data/lib/rigor/language_server/project_context.rb +39 -1
- data/lib/rigor/language_server/publish_batcher.rb +3 -4
- data/lib/rigor/language_server/selection_range_provider.rb +2 -4
- data/lib/rigor/language_server/server.rb +7 -3
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- 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/plugins.rb +1 -1
- 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/mcp/server.rb +73 -2
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +26 -4
- data/lib/rigor/plugin/loader.rb +25 -8
- data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
- data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
- data/lib/rigor/plugin/manifest.rb +16 -16
- data/lib/rigor/plugin/registry.rb +16 -5
- data/lib/rigor/plugin/services.rb +2 -3
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
- data/lib/rigor/plugin/trust_policy.rb +48 -2
- data/lib/rigor/plugin/type_node_resolver.rb +2 -4
- data/lib/rigor/plugin.rb +6 -8
- data/lib/rigor/plugin_gap_advisory.rb +5 -1
- data/lib/rigor/project_environment.rb +16 -2
- data/lib/rigor/protection/analysis_guard.rb +1 -1
- data/lib/rigor/protection/discovery_seed.rb +44 -5
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
- data/lib/rigor/rbs_extended/reporter.rb +27 -3
- data/lib/rigor/rbs_extended.rb +2 -3
- data/lib/rigor/reflection/constant_ancestors.rb +81 -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 +32 -1
- data/lib/rigor/run_scoped_slots.rb +38 -0
- data/lib/rigor/runtime/jit.rb +1 -1
- data/lib/rigor/scope/discovery_index.rb +168 -8
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +901 -54
- data/lib/rigor/scope.rb +644 -37
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +18 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +588 -66
- data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
- data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
- data/lib/rigor/sig_gen/layout_index.rb +3 -3
- data/lib/rigor/sig_gen/method_candidate.rb +37 -4
- data/lib/rigor/sig_gen/observation_collector.rb +14 -7
- data/lib/rigor/sig_gen/observed_call.rb +1 -1
- data/lib/rigor/sig_gen/path_mapper.rb +6 -0
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/renderer.rb +15 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
- data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
- data/lib/rigor/sig_gen/write_reporter.rb +63 -14
- data/lib/rigor/sig_gen/write_result.rb +23 -12
- data/lib/rigor/sig_gen/writer.rb +238 -37
- 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/acceptance_router.rb +2 -2
- data/lib/rigor/type/accepts_result.rb +1 -1
- data/lib/rigor/type/app.rb +4 -6
- data/lib/rigor/type/combinator.rb +173 -21
- data/lib/rigor/type/constant.rb +4 -5
- data/lib/rigor/type/hash_shape.rb +21 -40
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type/union.rb +1 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/identifier.rb +1 -2
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node/string_literal.rb +1 -2
- data/lib/rigor/type_node/symbol_literal.rb +1 -2
- data/lib/rigor/type_node/union.rb +3 -6
- 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/value_semantics.rb +1 -10
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
- 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/effects.rb +1 -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-activejob/lib/rigor/plugin/activejob.rb +4 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
- 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-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -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 +4 -8
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/sig/rigor/analysis/baseline.rbs +0 -3
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -2
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- data/sig/rigor/effects/registry.rbs +2 -2
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +53 -18
- data/sig/rigor/plugin/effect_attribution.rbs +3 -3
- data/sig/rigor/plugin/effect_edge.rbs +1 -1
- data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +9 -9
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/plugin/protocol_contract.rbs +3 -7
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +43 -4
- 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-plugin-author/SKILL.md +4 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
- 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 +82 -3
data/lib/rigor/scope.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "type"
|
|
4
4
|
require_relative "environment"
|
|
5
5
|
require_relative "scope/discovery_index"
|
|
6
|
+
require_relative "typing_state"
|
|
7
|
+
require_relative "memo_audit"
|
|
6
8
|
require_relative "scope/resolution_chain"
|
|
7
9
|
require_relative "inference/definer_resolution"
|
|
8
10
|
require_relative "inference/in_effect_refinements"
|
|
@@ -13,6 +15,10 @@ require_relative "inference/flow_tracer"
|
|
|
13
15
|
require_relative "inference/optimistic_origin"
|
|
14
16
|
require_relative "inference/statement_evaluator"
|
|
15
17
|
require_relative "inference/def_node_resolver"
|
|
18
|
+
# Issue #2608 — read by `MutationWidening` at a remover call, which only an analysis makes; loaded with the engine
|
|
19
|
+
# so the engine-free value layer that requires `MutationWidening` stays as small as it was.
|
|
20
|
+
require_relative "inference/remover_narrowing"
|
|
21
|
+
require_relative "inference/alias_mutation_floor"
|
|
16
22
|
|
|
17
23
|
module Rigor
|
|
18
24
|
# Immutable analyzer scope: holds local-variable bindings and a reference to the surrounding Environment. State
|
|
@@ -30,7 +36,7 @@ module Rigor
|
|
|
30
36
|
:opaque_block_self, :block_self_unknown, :singleton_class_body, :lexical_nesting,
|
|
31
37
|
:dynamic_origins, :local_origins, :ivar_origins,
|
|
32
38
|
:void_origins, :plugin_typed_calls,
|
|
33
|
-
:optimistic_origins, :optimistic_locals, :optimistic_ivars,
|
|
39
|
+
:optimistic_origins, :optimistic_locals, :optimistic_ivars, :construction_facts,
|
|
34
40
|
:repeated_or_writes, :match_frame,
|
|
35
41
|
:constant_narrowings, :guard_records, :bot_guard_classes, :declared_refinements
|
|
36
42
|
|
|
@@ -89,7 +95,7 @@ module Rigor
|
|
|
89
95
|
def discovered_prepends = @discovery.discovered_prepends.transform_values { |names| names.uniq.freeze }.freeze
|
|
90
96
|
def discovered_extends = @discovery.discovered_extends
|
|
91
97
|
def discovered_class_sources = @discovery.discovered_class_sources
|
|
92
|
-
# Issue #644 — `{qualified constant name =>
|
|
98
|
+
# Issue #644 — `{qualified constant name => declaring files}`; seeded only on an ADR-46 recording run.
|
|
93
99
|
def constant_sources = @discovery.constant_sources
|
|
94
100
|
def published_constant_names = @discovery.published_constant_names
|
|
95
101
|
def local_constant_names = @discovery.local_constant_names
|
|
@@ -153,7 +159,13 @@ module Rigor
|
|
|
153
159
|
# renames the foreign declaration. `published_constant_alias_names` holds the last segments of exactly
|
|
154
160
|
# those aliases, and matching on the last segment over-answers in the WITHHOLDING direction, which is the
|
|
155
161
|
# safe one.
|
|
162
|
+
#
|
|
163
|
+
# Issue #3199 — the answer turns on another file's publication of the last segment, so the question records
|
|
164
|
+
# the ADR-46 `constant:<segment>` name edge itself, whatever it answers. The guard asks it about spellings no
|
|
165
|
+
# typing step reads (a `defined?(B::X)` probe, a callee body the one hop scans), and an edge left to the read
|
|
166
|
+
# would let an `--incremental` run keep a verdict the moved publication no longer earns.
|
|
156
167
|
def published_constant?(name)
|
|
168
|
+
Analysis::DependencyRecorder.read_last_segment(:constant, name) if Analysis::DependencyRecorder.active?
|
|
157
169
|
names = @discovery.published_constant_names
|
|
158
170
|
return true if published_constant_alias?(name)
|
|
159
171
|
return false if names.empty?
|
|
@@ -202,9 +214,9 @@ module Rigor
|
|
|
202
214
|
# `:const` allowed; its entry marks presence only.
|
|
203
215
|
IndexedKey = Data.define(:receiver_kind, :receiver_name, :key)
|
|
204
216
|
|
|
205
|
-
# Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain
|
|
206
|
-
#
|
|
207
|
-
#
|
|
217
|
+
# Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain).
|
|
218
|
+
# The value of the map at this key is the narrowed type the next read of the same chain MUST observe —
|
|
219
|
+
# typically the post-`is_a?(C)` narrowing established on a predicate edge.
|
|
208
220
|
#
|
|
209
221
|
# - `receiver_kind` ∈ `{:local, :ivar}` — the analyzer only tracks chains rooted at a local or instance
|
|
210
222
|
# variable today (Law-of-Demeter-style single-hop).
|
|
@@ -232,7 +244,7 @@ module Rigor
|
|
|
232
244
|
# through flow transitions and ignored by `==` / `hash`.
|
|
233
245
|
EMPTY_FOLD_SAFE = Set.new.freeze
|
|
234
246
|
# ADR-82 WD1 — provenance propagation across the receiver-node lookup. `local_origins` / `ivar_origins`
|
|
235
|
-
# map a `name`
|
|
247
|
+
# map a `name` to the {Inference::DynamicOrigin} cause of the `Dynamic` value currently bound to
|
|
236
248
|
# it, so a downstream dispatch whose receiver is a bare `x` / `@x` read resolves to why that value is
|
|
237
249
|
# dynamic (the cause was recorded on the *assignment*'s rhs node, which the receiver-read node is not).
|
|
238
250
|
# Advisory metadata like {#dynamic_origins}: ignored by `==` / `hash` (never varies a flow decision) and
|
|
@@ -283,6 +295,32 @@ module Rigor
|
|
|
283
295
|
:EMPTY_PUBLISHED_CONSTANT_IVARS, :EMPTY_REPEATED_OR_WRITES,
|
|
284
296
|
:EMPTY_CONSTANT_NARROWINGS, :EMPTY_GUARD_RECORDS, :EMPTY_BOT_GUARD_CLASSES
|
|
285
297
|
|
|
298
|
+
# ADR-129 WD1 — the four advisory tables are bound Tables: each is an identity Hash one file's scopes share by
|
|
299
|
+
# reference (a fresh body scope may start its own; {#with_isolated_side_tables} copies them), so a journal record
|
|
300
|
+
# names the store beside the node. Every entry read and write goes through the methods below, which log it while a
|
|
301
|
+
# capture is open; handing a store to a new Scope is not an entry read.
|
|
302
|
+
DYNAMIC_ORIGINS = TypingState.table(
|
|
303
|
+
:dynamic_origins,
|
|
304
|
+
bound: true,
|
|
305
|
+
reason: "node => DynamicOrigin cause; typing records it and reads it back for receiver provenance"
|
|
306
|
+
)
|
|
307
|
+
VOID_ORIGINS = TypingState.table(
|
|
308
|
+
:void_origins, bound: true, reason: "node => VoidOrigin site; recorded by dispatch, read by the value-use rule"
|
|
309
|
+
)
|
|
310
|
+
PLUGIN_TYPED_CALLS = TypingState.table(
|
|
311
|
+
:plugin_typed_calls, bound: true, reason: "node => true where a plugin's dynamic_return answered the call"
|
|
312
|
+
)
|
|
313
|
+
OPTIMISTIC_ORIGINS = TypingState.table(
|
|
314
|
+
:optimistic_origins,
|
|
315
|
+
bound: true,
|
|
316
|
+
reason: "node => OptimisticOrigin cause; typing records it and reads it back to keep a value nil-free"
|
|
317
|
+
)
|
|
318
|
+
private_constant :DYNAMIC_ORIGINS, :VOID_ORIGINS, :PLUGIN_TYPED_CALLS, :OPTIMISTIC_ORIGINS
|
|
319
|
+
TypingState.store_field(self, :@dynamic_origins, DYNAMIC_ORIGINS)
|
|
320
|
+
TypingState.store_field(self, :@void_origins, VOID_ORIGINS)
|
|
321
|
+
TypingState.store_field(self, :@plugin_typed_calls, PLUGIN_TYPED_CALLS)
|
|
322
|
+
TypingState.store_field(self, :@optimistic_origins, OPTIMISTIC_ORIGINS)
|
|
323
|
+
|
|
286
324
|
class << self
|
|
287
325
|
def empty(environment: Environment.default, source_path: nil)
|
|
288
326
|
new(environment: environment, locals: {}.freeze,
|
|
@@ -291,16 +329,33 @@ module Rigor
|
|
|
291
329
|
end
|
|
292
330
|
|
|
293
331
|
def record_dynamic_origin(node, cause)
|
|
332
|
+
TypingState.current.log_bound_write(DYNAMIC_ORIGINS, @dynamic_origins, node, cause)
|
|
294
333
|
@dynamic_origins[node] = cause
|
|
295
334
|
self
|
|
296
335
|
end
|
|
297
336
|
|
|
337
|
+
# The {Inference::DynamicOrigin} cause recorded at `node`, or nil (ADR-129: a logged Table read).
|
|
338
|
+
def dynamic_origin(node)
|
|
339
|
+
log_advisory_read(DYNAMIC_ORIGINS, @dynamic_origins, node)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# The {Inference::VoidOrigin} recorded at `node`, or nil (ADR-129: a logged Table read).
|
|
343
|
+
def void_origin(node)
|
|
344
|
+
log_advisory_read(VOID_ORIGINS, @void_origins, node)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# The {Inference::OptimisticOrigin} cause recorded at `node`, or nil (ADR-129: a logged Table read).
|
|
348
|
+
def optimistic_origin(node)
|
|
349
|
+
log_advisory_read(OPTIMISTIC_ORIGINS, @optimistic_origins, node)
|
|
350
|
+
end
|
|
351
|
+
|
|
298
352
|
# ADR-100 WD3 — records that the value introduced at `node` (a call node) is a `top` recovered from an
|
|
299
353
|
# author-declared `-> void` return, keyed by the origin site (`origin`, an {Inference::VoidOrigin}). The
|
|
300
354
|
# value-context check rule `static.value-use.void` consumes this table. Mirrors {#record_dynamic_origin}
|
|
301
355
|
# exactly: identity-keyed advisory metadata, mutated in place on the shared table (threaded by reference
|
|
302
356
|
# through `#join` / `#rebuild`), excluded from `==` / `hash`, so it never forks a flow-dedup or cache key.
|
|
303
357
|
def record_void_origin(node, origin)
|
|
358
|
+
TypingState.current.log_bound_write(VOID_ORIGINS, @void_origins, node, origin)
|
|
304
359
|
@void_origins[node] = origin
|
|
305
360
|
self
|
|
306
361
|
end
|
|
@@ -313,15 +368,22 @@ module Rigor
|
|
|
313
368
|
# identity-keyed advisory metadata on a table threaded by reference through `#join` / `#rebuild`,
|
|
314
369
|
# excluded from `==` / `hash`, so it never forks a flow-dedup or cache key.
|
|
315
370
|
def record_plugin_typed_call(node)
|
|
371
|
+
TypingState.current.log_bound_write(PLUGIN_TYPED_CALLS, @plugin_typed_calls, node, true)
|
|
316
372
|
@plugin_typed_calls[node] = true
|
|
317
373
|
self
|
|
318
374
|
end
|
|
319
375
|
|
|
320
376
|
# Issue #653 — whether a plugin answered the return type for `node` on this run.
|
|
321
377
|
def plugin_typed_call?(node)
|
|
322
|
-
@plugin_typed_calls.
|
|
378
|
+
!log_advisory_read(PLUGIN_TYPED_CALLS, @plugin_typed_calls, node).nil?
|
|
323
379
|
end
|
|
324
380
|
|
|
381
|
+
# Every keyword past the 31st (`opaque_block_self`, `block_self_unknown`) must keep a literal default. A keyword at
|
|
382
|
+
# that index with a non-literal default makes a call that omits it allocate CRuby's unspecified-keywords record
|
|
383
|
+
# during argument setup, and on CRuby 4.0 a GC started by that allocation frees the values already moved into the
|
|
384
|
+
# keyword locals (ruby/ruby Bug #22500, issue #2850): `Scope.new` stored a freed `match_frame`.
|
|
385
|
+
# `spec/rigor/keyword_gc_safety_spec.rb` holds every method under `lib/` and `plugins/*/lib` to this. See the note
|
|
386
|
+
# at {#rebuild} before adding a field.
|
|
325
387
|
def initialize( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- one keyword and assignment per field
|
|
326
388
|
environment:, locals:,
|
|
327
389
|
fact_store: Analysis::FactStore.empty,
|
|
@@ -336,8 +398,6 @@ module Rigor
|
|
|
336
398
|
published_constant_sourced: EMPTY_PUBLISHED_CONSTANT_SOURCED,
|
|
337
399
|
source_path: nil,
|
|
338
400
|
struct_fold_safe_locals: EMPTY_FOLD_SAFE,
|
|
339
|
-
opaque_block_self: false,
|
|
340
|
-
block_self_unknown: false,
|
|
341
401
|
singleton_class_body: false,
|
|
342
402
|
lexical_nesting: nil,
|
|
343
403
|
dynamic_origins: {}.compare_by_identity,
|
|
@@ -348,12 +408,16 @@ module Rigor
|
|
|
348
408
|
optimistic_origins: {}.compare_by_identity,
|
|
349
409
|
optimistic_locals: EMPTY_ORIGINS,
|
|
350
410
|
optimistic_ivars: EMPTY_ORIGINS,
|
|
411
|
+
construction_facts: EMPTY_ORIGINS,
|
|
351
412
|
repeated_or_writes: EMPTY_REPEATED_OR_WRITES,
|
|
352
413
|
match_frame: nil,
|
|
353
414
|
constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
|
|
354
415
|
guard_records: EMPTY_GUARD_RECORDS,
|
|
355
416
|
bot_guard_classes: EMPTY_BOT_GUARD_CLASSES,
|
|
356
|
-
declared_refinements: Inference::InEffectRefinements::EMPTY
|
|
417
|
+
declared_refinements: Inference::InEffectRefinements::EMPTY,
|
|
418
|
+
# Literal defaults only from here on; see the note above.
|
|
419
|
+
opaque_block_self: false,
|
|
420
|
+
block_self_unknown: false
|
|
357
421
|
)
|
|
358
422
|
@environment = environment
|
|
359
423
|
@locals = locals
|
|
@@ -381,6 +445,7 @@ module Rigor
|
|
|
381
445
|
@optimistic_origins = optimistic_origins
|
|
382
446
|
@optimistic_locals = optimistic_locals
|
|
383
447
|
@optimistic_ivars = optimistic_ivars
|
|
448
|
+
@construction_facts = construction_facts
|
|
384
449
|
@repeated_or_writes = repeated_or_writes
|
|
385
450
|
@match_frame = match_frame
|
|
386
451
|
@constant_narrowings = constant_narrowings
|
|
@@ -395,10 +460,20 @@ module Rigor
|
|
|
395
460
|
# nil-freeness is a property of its class. Mirrors {#dynamic_origins} / {#void_origins}: advisory
|
|
396
461
|
# metadata, ignored by `==` / `hash`, and never varying a flow decision on its own.
|
|
397
462
|
def record_optimistic_origin(node, cause)
|
|
463
|
+
TypingState.current.log_bound_write(OPTIMISTIC_ORIGINS, @optimistic_origins, node, cause)
|
|
398
464
|
@optimistic_origins[node] = cause
|
|
399
465
|
self
|
|
400
466
|
end
|
|
401
467
|
|
|
468
|
+
# The entry of the advisory table `store` (the bound Table `slot`'s) at `node`, or nil, logged as a Table read: a
|
|
469
|
+
# key the store does not hold logs {TypingState::ABSENT}, so a recorded nil and a miss differ.
|
|
470
|
+
def log_advisory_read(slot, store, node)
|
|
471
|
+
value = store.fetch(node, TypingState::ABSENT)
|
|
472
|
+
TypingState.current.log_bound_read(slot, store, node, value)
|
|
473
|
+
TypingState::ABSENT.equal?(value) ? nil : value
|
|
474
|
+
end
|
|
475
|
+
private :log_advisory_read
|
|
476
|
+
|
|
402
477
|
# Issue #1703 — this scope with private copies of the identity-keyed side tables (dynamic, void and optimistic
|
|
403
478
|
# origins, plugin-typed calls), so a second walk from it — the `key?`-guarded re-walk — records into its own
|
|
404
479
|
# tables and never into the ones the file's analysis reads.
|
|
@@ -433,6 +508,41 @@ module Rigor
|
|
|
433
508
|
rebuild(optimistic_ivars: @optimistic_ivars.merge(name.to_sym => mark).freeze)
|
|
434
509
|
end
|
|
435
510
|
|
|
511
|
+
# Issue #2555 L3 — the construction facts a LOCAL's binding carries: what the engine knows about how the object
|
|
512
|
+
# bound to it was made, as opposed to what its type says about its contents. `:synthesised` means the engine
|
|
513
|
+
# built the object itself (a collection literal, or an `Array.new` / `Hash.new` / `Set.new` / `X[...]`
|
|
514
|
+
# constructor on the core class), so its element types are this engine's own inference and no declaration's
|
|
515
|
+
# claim: the mutation seams may re-grow such a carrier precisely, where a declared one (a parameter, an RBS
|
|
516
|
+
# return) keeps the issue #561 boundary. The table is `{ name => frozen Set[Symbol] }`, so a second fact joins
|
|
517
|
+
# an existing entry without reshaping it.
|
|
518
|
+
#
|
|
519
|
+
# Locals only. An instance variable's binding is seeded across methods from the class-ivar index, whose
|
|
520
|
+
# construction no single body witnesses, so an ivar never carries a fact. Like {#optimistic_locals} the facts
|
|
521
|
+
# are kept by {#with_mutated_local} (the binding still names the same object), dropped by {#with_local} (a
|
|
522
|
+
# source-level rebind names a new one), intersected at a merge, and reset with the locals at a method body's
|
|
523
|
+
# entry. No fact is copied by an alias write (`b = a`): the type is shared, the provenance is not tracked
|
|
524
|
+
# through a second name (issue #2335), and a local that may escape anywhere in its body is never stamped
|
|
525
|
+
# ({Inference::ConstructionEscape}). Within an entry `:synthesised` intersects at a merge and `:grown` (a store
|
|
526
|
+
# has grown the carrier since construction) unions.
|
|
527
|
+
CONSTRUCTION_FACT_SETS = Ractor.make_shareable({ synthesised: Set[:synthesised] })
|
|
528
|
+
NO_CONSTRUCTION_FACTS = Set.new.freeze
|
|
529
|
+
private_constant :CONSTRUCTION_FACT_SETS, :NO_CONSTRUCTION_FACTS
|
|
530
|
+
|
|
531
|
+
def construction_fact?(name, fact = :synthesised)
|
|
532
|
+
@construction_facts.fetch(name.to_sym, NO_CONSTRUCTION_FACTS).include?(fact)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# Records `fact` on the local `name`. Applied AFTER the {#with_local} that binds the value, which drops any
|
|
536
|
+
# fact the old binding held.
|
|
537
|
+
def with_construction_fact(name, fact)
|
|
538
|
+
key = name.to_sym
|
|
539
|
+
existing = @construction_facts[key]
|
|
540
|
+
return self if existing&.include?(fact)
|
|
541
|
+
|
|
542
|
+
facts = existing ? (existing | [fact]).freeze : (CONSTRUCTION_FACT_SETS[fact] || Set[fact].freeze)
|
|
543
|
+
rebuild(@construction_facts.merge(key => facts).freeze)
|
|
544
|
+
end
|
|
545
|
+
|
|
436
546
|
# ADR-82 WD1 — the propagated origin of the `Dynamic` value currently bound to a local / instance
|
|
437
547
|
# variable, or `nil` when none is tracked. Consulted by `Inference::ProtectionScanner` when a dispatch's
|
|
438
548
|
# receiver is a bare `x` / `@x` read whose own node carries no origin.
|
|
@@ -487,7 +597,8 @@ module Rigor
|
|
|
487
597
|
# longer holds (the new value may carry a method-local nil). `with_declaration_sourced_local` re-establishes
|
|
488
598
|
# the mark afterward when the RHS is a pure copy of a declaration-sourced ivar read; the default is to drop
|
|
489
599
|
# it.
|
|
490
|
-
rebuild(
|
|
600
|
+
rebuild(keep_marks ? @construction_facts : drop_origin(@construction_facts, name),
|
|
601
|
+
locals: new_locals, fact_store: new_fact_store,
|
|
491
602
|
indexed_narrowings: new_indexed_narrowings,
|
|
492
603
|
method_chain_narrowings: new_chain_narrowings,
|
|
493
604
|
declaration_sourced: keep_marks ? @declaration_sourced : drop_local_declaration_marks(name),
|
|
@@ -595,9 +706,36 @@ module Rigor
|
|
|
595
706
|
rebuild(opaque_block_self: true, block_self_unknown: true)
|
|
596
707
|
end
|
|
597
708
|
|
|
709
|
+
# Issue #2861 — the {#block_self_unknown?} value a block whose yielding call provably runs it under the
|
|
710
|
+
# caller's own `self` ({Inference::SelfPreservingBlock}) is entered with, when the caller's `self` is not
|
|
711
|
+
# already unmodelled. It reads as unknown everywhere `true` does, so issue #1717's explicit-`self` exemption is
|
|
712
|
+
# unchanged; what it withholds is the #316 opaque mark, and it tells a block nested beneath it that it does
|
|
713
|
+
# not sit under an index walk ({#walked_block_self_unknown?}).
|
|
714
|
+
PRESERVED_BLOCK_SELF = :preserved
|
|
715
|
+
|
|
716
|
+
# Issue #2861 — the entry for a block {Inference::SelfPreservingBlock} proves runs under the caller's `self`
|
|
717
|
+
# (`rows.map { … }`, `5.times { … }`). Ruby gives the block the `self` the call site has, so the block is as
|
|
718
|
+
# opaque as the scope around it and no more: it keeps whatever #316 mark the caller's scope carries and adds
|
|
719
|
+
# none, so a cross-file top-level `def` binds there exactly where it binds beside the call
|
|
720
|
+
# ({#bindable_top_level_def_for}). A caller's unknown-`self` mark of `true` or {PRESERVED_BLOCK_SELF} is kept as
|
|
721
|
+
# it is (no rebuild); a {REFINEMENT_BLOCK_SELF} one is not, since the block's `self` is not the refinement module
|
|
722
|
+
# the mark describes.
|
|
723
|
+
def entering_self_preserving_block
|
|
724
|
+
return self if @block_self_unknown == true || @block_self_unknown == PRESERVED_BLOCK_SELF
|
|
725
|
+
|
|
726
|
+
rebuild(block_self_unknown: PRESERVED_BLOCK_SELF)
|
|
727
|
+
end
|
|
728
|
+
|
|
598
729
|
# True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
|
|
599
730
|
def opaque_block_self? = @opaque_block_self
|
|
600
731
|
|
|
732
|
+
# Issue #2861 — true where the unknown-`self` mark came from the index walk or a value-position block
|
|
733
|
+
# ({#with_block_self_unknown}) rather than a block entry: the mark is set, the #316 one is not, and it is not
|
|
734
|
+
# a {PRESERVED_BLOCK_SELF} entry's. A block nested in such a scope keeps the #316 mark off (issue #1717).
|
|
735
|
+
def walked_block_self_unknown?
|
|
736
|
+
!!@block_self_unknown && !@opaque_block_self && @block_self_unknown != PRESERVED_BLOCK_SELF
|
|
737
|
+
end
|
|
738
|
+
|
|
601
739
|
# Issue #1717 — true when this scope sits in a block whose `self` the engine does not know, so an explicit
|
|
602
740
|
# `self.m` there is no evidence `m` is missing: the yielding method may `instance_exec` the block on another
|
|
603
741
|
# object. Read ONLY by `call.undefined-method`'s explicit-`self` exemption, and kept apart from
|
|
@@ -681,9 +819,8 @@ module Rigor
|
|
|
681
819
|
|
|
682
820
|
# Slice 7 phase 1 — instance/class/global variable bindings. `ivar(name)` / `cvar(name)` / `global(name)`
|
|
683
821
|
# return the type currently bound for the named variable, or `nil` when the variable has not been written in
|
|
684
|
-
# the analyzed slice of the program.
|
|
685
|
-
#
|
|
686
|
-
# `Dynamic[Top]`. Cross-method ivar/cvar inference is a follow-up slice.
|
|
822
|
+
# the analyzed slice of the program. Each `def` enters with a fresh binding map seeded from the class-level
|
|
823
|
+
# accumulators ({#class_ivars_for}, {#class_cvars_for}).
|
|
687
824
|
def ivar(name)
|
|
688
825
|
@ivars[name.to_sym]
|
|
689
826
|
end
|
|
@@ -854,6 +991,56 @@ module Rigor
|
|
|
854
991
|
@declaration_sourced.include?([:inferred_param, name.to_sym])
|
|
855
992
|
end
|
|
856
993
|
|
|
994
|
+
# Issue #2656 — stamp the gradual-narrowing mark on the local `name`: a class guard narrowed it from a `Dynamic`
|
|
995
|
+
# value, so its `Nominal` reading is the guard's, not a contract the value was checked against. Rides the ADR-58
|
|
996
|
+
# side-mark machinery under a distinct `:gradual_narrowing` kind, as `:inferred_param` does: the displayed type
|
|
997
|
+
# stays the narrowed `Nominal` (dispatch, `call.undefined-method` and `call.wrong-arity` read it), and only
|
|
998
|
+
# `call.argument-type-mismatch` consults the mark ({Analysis::CheckRules::DeclarationSourcedGuard.gradual_narrowed?}).
|
|
999
|
+
# Like the `:local` mark it is dropped by `with_local` and intersected by `join`. Applied after the binding.
|
|
1000
|
+
def with_gradual_narrowing_mark(name)
|
|
1001
|
+
marks = add_declaration_sourced(:gradual_narrowing, name)
|
|
1002
|
+
return self if marks.equal?(@declaration_sourced)
|
|
1003
|
+
|
|
1004
|
+
marks = (marks.dup << GRADUAL_HINT).freeze unless marks.include?(GRADUAL_HINT)
|
|
1005
|
+
rebuild(declaration_sourced: marks)
|
|
1006
|
+
end
|
|
1007
|
+
|
|
1008
|
+
# Issue #2656 — a ref stamped beside every gradual-narrowing mark, so {#gradual_narrowing_names} can answer "none"
|
|
1009
|
+
# with one hash lookup instead of a walk of the set, which the ADR-67 marks keep non-empty in most bodies. It is a
|
|
1010
|
+
# hint: a drop leaves it, and the walk then finds no mark; a join keeps it only where both arms carry it.
|
|
1011
|
+
GRADUAL_HINT = [:gradual_narrowing_hint].freeze
|
|
1012
|
+
private_constant :GRADUAL_HINT
|
|
1013
|
+
|
|
1014
|
+
# Issue #2656 — this scope without the gradual-narrowing mark of every local `block_node` writes: a block may run
|
|
1015
|
+
# again after its own write (`loop do mode(x); x = "zz".dup; end`), and its entry does not see that back edge, so
|
|
1016
|
+
# a mark from before the block must not reach a read the write may precede. Zero-alloc when no local is marked.
|
|
1017
|
+
def entering_block_without_gradual_marks(block_node)
|
|
1018
|
+
names = gradual_narrowing_names
|
|
1019
|
+
return self if names.empty?
|
|
1020
|
+
|
|
1021
|
+
written = Inference::OperandEffects.written_variables(block_node).map(&:to_sym)
|
|
1022
|
+
dropped = names.reduce(@declaration_sourced) do |marks, name|
|
|
1023
|
+
written.include?(name) ? drop_gradual_narrowing_mark(marks, name) : marks
|
|
1024
|
+
end
|
|
1025
|
+
dropped.equal?(@declaration_sourced) ? self : rebuild(declaration_sourced: dropped)
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
# Issue #2656 — the locals carrying the gradual-narrowing mark, empty when none does.
|
|
1029
|
+
def gradual_narrowing_names
|
|
1030
|
+
return EMPTY_GRADUAL_NAMES unless @declaration_sourced.include?(GRADUAL_HINT)
|
|
1031
|
+
|
|
1032
|
+
@declaration_sourced.filter_map { |ref| ref[1] if ref[0] == :gradual_narrowing }
|
|
1033
|
+
end
|
|
1034
|
+
EMPTY_GRADUAL_NAMES = [].freeze
|
|
1035
|
+
private_constant :EMPTY_GRADUAL_NAMES
|
|
1036
|
+
|
|
1037
|
+
# Issue #2656 — true when the local `name`'s binding is a class guard's narrowing of a `Dynamic` value.
|
|
1038
|
+
def gradual_narrowing?(name)
|
|
1039
|
+
return false if @declaration_sourced.empty?
|
|
1040
|
+
|
|
1041
|
+
@declaration_sourced.include?([:gradual_narrowing, name.to_sym])
|
|
1042
|
+
end
|
|
1043
|
+
|
|
857
1044
|
def with_cvar(name, type)
|
|
858
1045
|
rebuild(cvars: @cvars.merge(name.to_sym => type).freeze)
|
|
859
1046
|
end
|
|
@@ -1186,8 +1373,8 @@ module Rigor
|
|
|
1186
1373
|
end
|
|
1187
1374
|
|
|
1188
1375
|
# Slice 7 phase 2 — class-level ivar accumulator. Keyed by the qualified class name (e.g. `"Rigor::Scope"`);
|
|
1189
|
-
# the value
|
|
1190
|
-
#
|
|
1376
|
+
# the value maps every ivar that appears as a write target inside any def body of that class to its type.
|
|
1377
|
+
# `StatementEvaluator#build_method_entry_scope` seeds the method body's `ivars` map from this
|
|
1191
1378
|
# table so a `def get; @x; end` reads the type written in a sibling `def init; @x = 1; end`.
|
|
1192
1379
|
#
|
|
1193
1380
|
# `ScopeIndexer` populates the table once at index time through a separate pre-pass over the program. The map
|
|
@@ -1207,7 +1394,7 @@ module Rigor
|
|
|
1207
1394
|
@discovery.class_cvars[class_name.to_s] || EMPTY_VAR_BINDINGS
|
|
1208
1395
|
end
|
|
1209
1396
|
|
|
1210
|
-
# Slice 7 phase 12 — in-source method discovery. Maps a qualified class name to a
|
|
1397
|
+
# Slice 7 phase 12 — in-source method discovery. Maps a qualified class name to a table of
|
|
1211
1398
|
# `method_name => :instance | :singleton`. Populated by `ScopeIndexer` from every `Prism::DefNode` and
|
|
1212
1399
|
# recognised `define_method` invocation inside class/module bodies. The `rigor check` undefined-method and
|
|
1213
1400
|
# wrong-arity rules consult this map to suppress diagnostics for methods the user has defined dynamically,
|
|
@@ -1230,6 +1417,58 @@ module Rigor
|
|
|
1230
1417
|
true
|
|
1231
1418
|
end
|
|
1232
1419
|
|
|
1420
|
+
# Issue #2555 — whether the project defines a method named `method_name` anywhere: on any class or module,
|
|
1421
|
+
# instance or singleton side (the def-node tables, which hold a plain cross-file `def` on an RBS-declared class
|
|
1422
|
+
# that `discovered_methods` withholds, and `discovered_methods`), or in a `refine` block. The name set is built
|
|
1423
|
+
# once per discovery index. The answer depends on every file, so an incremental run records the name either way:
|
|
1424
|
+
# a file that starts or stops defining it re-analyses the reader (issue #1459's `defines:` edge).
|
|
1425
|
+
def project_method_name?(method_name)
|
|
1426
|
+
Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
|
|
1427
|
+
return true if project_method_names.include?(method_name.to_sym)
|
|
1428
|
+
|
|
1429
|
+
false
|
|
1430
|
+
end
|
|
1431
|
+
|
|
1432
|
+
# The name set {#project_method_name?} reads, held per discovery index. ADR-129 G2: a hit is checked against a
|
|
1433
|
+
# fresh collection under `RIGOR_MEMO_AUDIT=1`, and an audit recomputation bypasses the table.
|
|
1434
|
+
def project_method_names
|
|
1435
|
+
return collect_project_method_names if memo_audit_bypass?
|
|
1436
|
+
|
|
1437
|
+
table = TypingState.current.weak_key_map(PROJECT_METHOD_NAMES)
|
|
1438
|
+
names = table[@discovery]
|
|
1439
|
+
if names.nil?
|
|
1440
|
+
return table[@discovery] = Inference::MemoAudit.key_audit("project method names") do
|
|
1441
|
+
collect_project_method_names
|
|
1442
|
+
end
|
|
1443
|
+
end
|
|
1444
|
+
|
|
1445
|
+
Inference::MemoAudit.check("project method names", names, :discovery) { collect_project_method_names }
|
|
1446
|
+
end
|
|
1447
|
+
private :project_method_names
|
|
1448
|
+
|
|
1449
|
+
# ADR-129 G2 — true while a MemoAudit recomputation runs: the memos wired here are then bypassed, neither read
|
|
1450
|
+
# nor written. One boolean check when the audit is off.
|
|
1451
|
+
def memo_audit_bypass? = Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
|
|
1452
|
+
private :memo_audit_bypass?
|
|
1453
|
+
|
|
1454
|
+
def collect_project_method_names
|
|
1455
|
+
names = Set.new
|
|
1456
|
+
[@discovery.discovered_methods, @discovery.discovered_def_nodes, @discovery.discovered_singleton_def_nodes,
|
|
1457
|
+
@discovery.discovered_refinements].each do |by_owner|
|
|
1458
|
+
by_owner.each_value { |methods| methods.each_key { |method_name| names << method_name.to_sym } }
|
|
1459
|
+
end
|
|
1460
|
+
names.freeze
|
|
1461
|
+
end
|
|
1462
|
+
private :collect_project_method_names
|
|
1463
|
+
|
|
1464
|
+
# ADR-129 WD1 — a run-scoped Memo ({TypingState}): emptied when a run ends, so an edited project is read afresh.
|
|
1465
|
+
PROJECT_METHOD_NAMES = TypingState.memo(
|
|
1466
|
+
:project_method_names,
|
|
1467
|
+
run_scoped: true, audit: "project method names",
|
|
1468
|
+
reason: "a WeakKeyMap keyed on the discovery index; the name set reads only its tables"
|
|
1469
|
+
)
|
|
1470
|
+
private_constant :PROJECT_METHOD_NAMES
|
|
1471
|
+
|
|
1233
1472
|
def record_discovered_method(class_name, method_name)
|
|
1234
1473
|
record_class_dependency(class_name)
|
|
1235
1474
|
Analysis::DependencyRecorder.read_name(:defines, method_name)
|
|
@@ -1258,7 +1497,7 @@ module Rigor
|
|
|
1258
1497
|
per_class && per_class[method_name]
|
|
1259
1498
|
end
|
|
1260
1499
|
|
|
1261
|
-
# v0.0.2 #5 — per-class table mapping `method_name
|
|
1500
|
+
# v0.0.2 #5 — per-class table mapping `method_name → Prism::DefNode`. Populated by `ScopeIndexer`
|
|
1262
1501
|
# alongside `discovered_methods` for instance-side defs only (singleton-side and `define_method`-introduced
|
|
1263
1502
|
# methods do not contribute a static body the engine can re-type). Consumed by `ExpressionTyper` to do
|
|
1264
1503
|
# inter-procedural return-type inference when the receiver class is user-defined and has no RBS sig.
|
|
@@ -1272,6 +1511,89 @@ module Rigor
|
|
|
1272
1511
|
Inference::DefNodeResolver.resolve(entry)
|
|
1273
1512
|
end
|
|
1274
1513
|
|
|
1514
|
+
# Issue #2515 — `[class_name, open]` for each class or module whose instance def table holds `method_name`, in
|
|
1515
|
+
# table order (frozen; empty when no user def has the name). The callee floor's hand-off on a receiver it cannot
|
|
1516
|
+
# type reads every def of the name, so the answer depends on the whole project's defs of that name: a recorder
|
|
1517
|
+
# files the `defines:<name>` name edge, which fires when a file starts or stops defining it. `open` is true when
|
|
1518
|
+
# the real receiver may not be the class the def sits on: the owner is a module (an includer's methods are
|
|
1519
|
+
# called from it), or a project class names it as its superclass (a subclass may override what the def calls).
|
|
1520
|
+
# A recorder files `subclass_of:<owner>` for the second.
|
|
1521
|
+
def user_instance_def_owners(method_name)
|
|
1522
|
+
Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
|
|
1523
|
+
name = method_name.to_sym
|
|
1524
|
+
# `each` with a lazily built array, not `filter_map`: this runs once per call on every discovered class, and
|
|
1525
|
+
# `Hash#filter_map` allocates a `[key, value]` pair per entry (#2895 cost 3.35% of the release gate's
|
|
1526
|
+
# allocations that way).
|
|
1527
|
+
owners = nil
|
|
1528
|
+
@discovery.discovered_def_nodes.each do |class_name, defs|
|
|
1529
|
+
next unless defs.key?(name) && class_name != Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
1530
|
+
|
|
1531
|
+
(owners ||= []) << [class_name, open_to_override?(class_name)].freeze
|
|
1532
|
+
end
|
|
1533
|
+
owners.nil? ? EMPTY_OWNERS : owners.freeze
|
|
1534
|
+
end
|
|
1535
|
+
|
|
1536
|
+
EMPTY_OWNERS = [].freeze
|
|
1537
|
+
private_constant :EMPTY_OWNERS
|
|
1538
|
+
|
|
1539
|
+
# Issue #2515 — `[opaque, keys]`: whether the project holds a subclass relation the superclass table does not
|
|
1540
|
+
# spell as `class X < Name`, so that no owner can be called free of an override. It holds when a class takes
|
|
1541
|
+
# `Class.new(Parent)` ({DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK}), when a class's superclass expression is
|
|
1542
|
+
# unreadable (`nil` in the table), or when a written superclass is not a declared class and some file assigns a
|
|
1543
|
+
# constant of that name (`H2 = Helper; class Sub < H2`; the writer census {#bound_constant_names} or this file's
|
|
1544
|
+
# constant table).
|
|
1545
|
+
# `keys` are the frozen ADR-46 name keys a recorder files for the answer; the caller files them, since the answer
|
|
1546
|
+
# is memoised per discovery.
|
|
1547
|
+
def subclassing_opacity
|
|
1548
|
+
keys = OPACITY_KEYS.dup
|
|
1549
|
+
opaque = @discovery.discovered_parameter_envelopes.each_value.any? do |bucket|
|
|
1550
|
+
bucket.key?(DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
|
|
1551
|
+
end
|
|
1552
|
+
segments = {}
|
|
1553
|
+
@discovery.discovered_superclasses.each_value do |parent|
|
|
1554
|
+
opaque = true if parent.nil? || alias_superclass?(parent, segments, keys)
|
|
1555
|
+
end
|
|
1556
|
+
[opaque ? true : false, keys.each(&:freeze).freeze].freeze
|
|
1557
|
+
end
|
|
1558
|
+
|
|
1559
|
+
# True when `parent`, a written superclass that is no declared class, names a constant some file assigns. Files
|
|
1560
|
+
# the `constant:<segment>` key for it once per segment.
|
|
1561
|
+
def alias_superclass?(parent, segments, keys)
|
|
1562
|
+
return false if @discovery.discovered_classes.key?(parent)
|
|
1563
|
+
|
|
1564
|
+
cut = parent.rindex("::")
|
|
1565
|
+
segment = cut ? parent.byteslice(cut + 2, parent.bytesize - cut - 2) : parent
|
|
1566
|
+
return false if segments.key?(segment)
|
|
1567
|
+
|
|
1568
|
+
segments[segment] = true
|
|
1569
|
+
keys << "constant:#{segment}"
|
|
1570
|
+
@discovery.constant_writers.key?(segment) || @discovery.in_source_constants.key?(parent)
|
|
1571
|
+
end
|
|
1572
|
+
private :alias_superclass?
|
|
1573
|
+
|
|
1574
|
+
OPACITY_KEYS = %w[subclass_of:* mark:*].freeze
|
|
1575
|
+
private_constant :OPACITY_KEYS
|
|
1576
|
+
|
|
1577
|
+
def open_to_override?(class_name)
|
|
1578
|
+
Analysis::DependencyRecorder.read_last_segment(:subclass_of, class_name) if Analysis::DependencyRecorder.active?
|
|
1579
|
+
return true if @discovery.discovered_parameter_envelopes[class_name]&.key?(DiscoveryIndex::ENVELOPE_MODULE_MARK)
|
|
1580
|
+
|
|
1581
|
+
@discovery.discovered_superclasses.any? do |child, parent|
|
|
1582
|
+
child != class_name && parent && names_class?(parent, class_name)
|
|
1583
|
+
end
|
|
1584
|
+
end
|
|
1585
|
+
private :open_to_override?
|
|
1586
|
+
|
|
1587
|
+
# True when a superclass written as `written` (`Helper`, `A::Helper`, `::Helper`) may name `qualified`.
|
|
1588
|
+
def names_class?(written, qualified)
|
|
1589
|
+
written = written[2..] if written.start_with?("::")
|
|
1590
|
+
return true if written == qualified
|
|
1591
|
+
return false unless qualified.end_with?(written)
|
|
1592
|
+
|
|
1593
|
+
qualified.getbyte(qualified.bytesize - written.bytesize - 1) == 58
|
|
1594
|
+
end
|
|
1595
|
+
private :names_class?
|
|
1596
|
+
|
|
1275
1597
|
# Module-singleton call resolution (ADR-57 follow-up) — companion of {#user_def_for} for SINGLETON-side defs
|
|
1276
1598
|
# (`def self.x`, `def Foo.x`, `class << self` bodies, and `module_function` defs). Returns the
|
|
1277
1599
|
# `Prism::DefNode` for `class_name.method_name` invoked on the module/class constant itself, or nil. The
|
|
@@ -1317,6 +1639,199 @@ module Rigor
|
|
|
1317
1639
|
Inference::DefNodeResolver.resolve(entry)
|
|
1318
1640
|
end
|
|
1319
1641
|
|
|
1642
|
+
# Issue #2515 — why the `<toplevel>` table's def for `method_name` may not be the method a receiverless call of that
|
|
1643
|
+
# name runs, or nil when the project's own tables show nothing else that could be. The table keeps one def per name
|
|
1644
|
+
# (the last writer's) and also files the defs of an anonymous `Class.new` body, so:
|
|
1645
|
+
#
|
|
1646
|
+
# - `:contested` — a def in a block or under a condition wrote the slot last;
|
|
1647
|
+
# - `:several` — the project holds more than one ownerless definition row of the name: two top-level defs (across
|
|
1648
|
+
# files, or redefined), a def in a block or in another def, `define_method` or `alias_method` of the name;
|
|
1649
|
+
# - `:main_singleton` — a `def self.name` (or a `class << self` def) at the top level defines it on `main`;
|
|
1650
|
+
# - `:aliased` — an `alias` statement or `alias_method` call names it as the new name;
|
|
1651
|
+
# - `:anonymous` — an anonymous class body defines it;
|
|
1652
|
+
# - `:object` — `Object` itself defines it (`class Object; def name`, `define_method`) or has a module prepended,
|
|
1653
|
+
# which Ruby searches ahead of the top-level def. A module included at the top level is searched after `Object`'s
|
|
1654
|
+
# own methods, so it is no reason.
|
|
1655
|
+
#
|
|
1656
|
+
# A name with no row at all (a scope built without the project pre-pass) is not `:several`: the rows say nothing.
|
|
1657
|
+
# The answer depends on every file, so an incremental run records the `defines:<name>` edge and the census edge of
|
|
1658
|
+
# an alias either way. The two project-wide scans are built once per table, not per call.
|
|
1659
|
+
def toplevel_def_ambiguity(method_name)
|
|
1660
|
+
name = method_name.to_sym
|
|
1661
|
+
record_toplevel_ambiguity_reads(name) if Analysis::DependencyRecorder.active?
|
|
1662
|
+
key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
1663
|
+
return :contested if @discovery.contested_discovered_def_nodes.include?([key, name])
|
|
1664
|
+
|
|
1665
|
+
rows = toplevel_definition_rows[name]
|
|
1666
|
+
return :main_singleton if rows == MAIN_SINGLETON_ROWS
|
|
1667
|
+
return :several if rows > 1
|
|
1668
|
+
return :aliased if method_alias_names.include?(name)
|
|
1669
|
+
|
|
1670
|
+
return :anonymous if @discovery.discovered_def_nodes.any? do |class_name, defs|
|
|
1671
|
+
defs.key?(name) && class_name.start_with?(Type::AnonymousClassName::PREFIX)
|
|
1672
|
+
end
|
|
1673
|
+
|
|
1674
|
+
:object if object_answers_first?(name)
|
|
1675
|
+
end
|
|
1676
|
+
|
|
1677
|
+
# True when `Object` defines `name` itself or has a module prepended (see {#toplevel_def_ambiguity}).
|
|
1678
|
+
def object_answers_first?(name)
|
|
1679
|
+
!user_def_for("Object", name).nil? || discovered_method?("Object", name, :instance) ||
|
|
1680
|
+
!(@discovery.discovered_prepends["Object"] || EMPTY_PREPENDS).empty?
|
|
1681
|
+
end
|
|
1682
|
+
private :object_answers_first?
|
|
1683
|
+
|
|
1684
|
+
EMPTY_PREPENDS = [].freeze
|
|
1685
|
+
private_constant :EMPTY_PREPENDS
|
|
1686
|
+
|
|
1687
|
+
# True for a class or module the project declares, even one with no method of its own.
|
|
1688
|
+
def declared_class?(class_name) = @discovery.discovered_classes.key?(class_name.to_s)
|
|
1689
|
+
|
|
1690
|
+
def record_toplevel_ambiguity_reads(name)
|
|
1691
|
+
Analysis::DependencyRecorder.read_name(:defines, name)
|
|
1692
|
+
Analysis::DependencyRecorder.read_name(:global_census, "method_alias:#{name}")
|
|
1693
|
+
Analysis::DependencyRecorder.read_last_segment(:class, "Object")
|
|
1694
|
+
record_class_dependency("Object")
|
|
1695
|
+
end
|
|
1696
|
+
private :record_toplevel_ambiguity_reads
|
|
1697
|
+
|
|
1698
|
+
# Issue #2336 — whether the project gives `method_name` a body through an ownerless definition row
|
|
1699
|
+
# ({#toplevel_definition_rows}): a `define_method` / `define_singleton_method` / `alias_method` call with that
|
|
1700
|
+
# literal name on any receiver, a def in a block, or (with `top_level:`) a def outside every class body. The
|
|
1701
|
+
# callee mutation floor reads a call no def table answers as one such body may run. The answer depends on every
|
|
1702
|
+
# file, so an incremental run records the `defines:<name>` edge either way.
|
|
1703
|
+
def ownerless_definition?(method_name, top_level: true)
|
|
1704
|
+
Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
|
|
1705
|
+
name = method_name.to_sym
|
|
1706
|
+
return true if top_level ? toplevel_definition_rows.key?(name) : deferred_definition_names.include?(name)
|
|
1707
|
+
|
|
1708
|
+
false
|
|
1709
|
+
end
|
|
1710
|
+
|
|
1711
|
+
# The names of the ownerless rows that claim no side ({#ownerless_definition?} without `top_level:`): a definer
|
|
1712
|
+
# call's literal name and a def in a block. Held by the table's identity, as {#toplevel_definition_rows} is.
|
|
1713
|
+
def deferred_definition_names
|
|
1714
|
+
ranges = @discovery.discovered_deferred_ranges
|
|
1715
|
+
return collect_deferred_definition_names(ranges) if memo_audit_bypass?
|
|
1716
|
+
|
|
1717
|
+
state = TypingState.current
|
|
1718
|
+
slots = (state[DEFERRED_NAMES] ||= [])
|
|
1719
|
+
slot = slots.find { |entry| entry[0].equal?(ranges) }
|
|
1720
|
+
if slot
|
|
1721
|
+
return Inference::MemoAudit.check("deferred definition names", slot[1], :ranges) do
|
|
1722
|
+
collect_deferred_definition_names(ranges)
|
|
1723
|
+
end
|
|
1724
|
+
end
|
|
1725
|
+
|
|
1726
|
+
names = Inference::MemoAudit.key_audit("deferred definition names") { collect_deferred_definition_names(ranges) }
|
|
1727
|
+
slots.unshift([ranges, names]).slice!(2..)
|
|
1728
|
+
names
|
|
1729
|
+
end
|
|
1730
|
+
private :deferred_definition_names
|
|
1731
|
+
|
|
1732
|
+
def collect_deferred_definition_names(ranges)
|
|
1733
|
+
names = Set.new
|
|
1734
|
+
ranges.each_value do |entries|
|
|
1735
|
+
entries.each { |(_start, _finish, name, kind, owner)| names << name if name && kind.nil? && owner.nil? }
|
|
1736
|
+
end
|
|
1737
|
+
names.freeze
|
|
1738
|
+
end
|
|
1739
|
+
private :collect_deferred_definition_names
|
|
1740
|
+
|
|
1741
|
+
# Issue #2336 — {#module_function_def?}'s `:both` reading for a def in any file: whether `def_node`, an instance
|
|
1742
|
+
# def of `owner`, is a module function by a deferred-range row over it (`module_function def x`, or a bare
|
|
1743
|
+
# `module_function` before it), which the singleton def table does not mirror for the argument form.
|
|
1744
|
+
def module_function_row_def?(def_node, owner)
|
|
1745
|
+
start = def_node.location.start_offset
|
|
1746
|
+
name = def_node.name
|
|
1747
|
+
owner = owner.to_s
|
|
1748
|
+
discovered_deferred_ranges.each_value.any? do |entries|
|
|
1749
|
+
entries.any? do |(row_start, _finish, row_name, kind, row_owner)|
|
|
1750
|
+
row_start == start && kind == :both && row_name == name && row_owner == owner
|
|
1751
|
+
end
|
|
1752
|
+
end
|
|
1753
|
+
end
|
|
1754
|
+
|
|
1755
|
+
# `{ name => ownerless definition rows }` over `discovered_deferred_ranges`, {MAIN_SINGLETON_ROWS} for a name with
|
|
1756
|
+
# a singleton row. Held by the table's identity (a file's merged copy and the seed), released with the run.
|
|
1757
|
+
def toplevel_definition_rows
|
|
1758
|
+
ranges = @discovery.discovered_deferred_ranges
|
|
1759
|
+
return collect_toplevel_definition_rows(ranges) if memo_audit_bypass?
|
|
1760
|
+
|
|
1761
|
+
state = TypingState.current
|
|
1762
|
+
slots = (state[TOPLEVEL_ROWS] ||= [])
|
|
1763
|
+
slot = slots.find { |entry| entry[0].equal?(ranges) }
|
|
1764
|
+
if slot
|
|
1765
|
+
return Inference::MemoAudit.check("toplevel definition rows", slot[1], :ranges) do
|
|
1766
|
+
collect_toplevel_definition_rows(ranges)
|
|
1767
|
+
end
|
|
1768
|
+
end
|
|
1769
|
+
|
|
1770
|
+
rows = Inference::MemoAudit.key_audit("toplevel definition rows") { collect_toplevel_definition_rows(ranges) }
|
|
1771
|
+
slots.unshift([ranges, rows]).slice!(2..)
|
|
1772
|
+
rows
|
|
1773
|
+
end
|
|
1774
|
+
private :toplevel_definition_rows
|
|
1775
|
+
|
|
1776
|
+
def collect_toplevel_definition_rows(ranges)
|
|
1777
|
+
rows = Hash.new(0)
|
|
1778
|
+
ranges.each_value do |entries|
|
|
1779
|
+
entries.each do |(_start, _finish, name, kind, owner)|
|
|
1780
|
+
next if name.nil? || !owner.nil?
|
|
1781
|
+
|
|
1782
|
+
rows[name] = kind == :singleton || rows[name] == MAIN_SINGLETON_ROWS ? MAIN_SINGLETON_ROWS : rows[name] + 1
|
|
1783
|
+
end
|
|
1784
|
+
end
|
|
1785
|
+
rows.freeze
|
|
1786
|
+
end
|
|
1787
|
+
private :collect_toplevel_definition_rows
|
|
1788
|
+
|
|
1789
|
+
# The names an `alias` statement or `alias_method` call introduces, from the global write census; held by the
|
|
1790
|
+
# census's identity as {#toplevel_definition_rows} is.
|
|
1791
|
+
def method_alias_names
|
|
1792
|
+
census = @discovery.discovered_global_write_census
|
|
1793
|
+
return collect_method_alias_names(census) if memo_audit_bypass?
|
|
1794
|
+
|
|
1795
|
+
state = TypingState.current
|
|
1796
|
+
slots = (state[METHOD_ALIASES] ||= [])
|
|
1797
|
+
slot = slots.find { |entry| entry[0].equal?(census) }
|
|
1798
|
+
if slot
|
|
1799
|
+
return Inference::MemoAudit.check("method alias names", slot[1], :census) { collect_method_alias_names(census) }
|
|
1800
|
+
end
|
|
1801
|
+
|
|
1802
|
+
names = Inference::MemoAudit.key_audit("method alias names") { collect_method_alias_names(census) }
|
|
1803
|
+
slots.unshift([census, names]).slice!(2..)
|
|
1804
|
+
names
|
|
1805
|
+
end
|
|
1806
|
+
private :method_alias_names
|
|
1807
|
+
|
|
1808
|
+
def collect_method_alias_names(census)
|
|
1809
|
+
census.each_with_object(Set.new) { |entry, acc| acc << entry[1] if entry[0] == :method_alias }.freeze
|
|
1810
|
+
end
|
|
1811
|
+
private :collect_method_alias_names
|
|
1812
|
+
|
|
1813
|
+
MAIN_SINGLETON_ROWS = -1
|
|
1814
|
+
# ADR-129 WD1 — three run-scoped Memos, each a two-entry list of `[table, derived]` held by the identity of the
|
|
1815
|
+
# table the derived value reads (the deferred ranges, or the global write census).
|
|
1816
|
+
TOPLEVEL_ROWS = TypingState.memo(
|
|
1817
|
+
:toplevel_definition_rows,
|
|
1818
|
+
run_scoped: true, audit: "toplevel definition rows",
|
|
1819
|
+
reason: "[deferred ranges, rows] entries keyed on the identity of the ranges table, the only input the rows read"
|
|
1820
|
+
)
|
|
1821
|
+
METHOD_ALIASES = TypingState.memo(
|
|
1822
|
+
:method_alias_names,
|
|
1823
|
+
run_scoped: true, audit: "method alias names",
|
|
1824
|
+
reason: "[census, names] entries keyed on the identity of the global write census, the only input the names " \
|
|
1825
|
+
"read"
|
|
1826
|
+
)
|
|
1827
|
+
DEFERRED_NAMES = TypingState.memo(
|
|
1828
|
+
:deferred_definition_names,
|
|
1829
|
+
run_scoped: true, audit: "deferred definition names",
|
|
1830
|
+
reason: "[deferred ranges, names] entries keyed on the identity of the ranges table, the only input the names " \
|
|
1831
|
+
"read"
|
|
1832
|
+
)
|
|
1833
|
+
private_constant :MAIN_SINGLETON_ROWS, :TOPLEVEL_ROWS, :METHOD_ALIASES, :DEFERRED_NAMES
|
|
1834
|
+
|
|
1320
1835
|
# Issue #316 — the CONFIDENCE-GATED companion of {#top_level_def_for}, and the only accessor the type
|
|
1321
1836
|
# inference may bind through. {#top_level_def_for} stays unrestricted because it also serves the
|
|
1322
1837
|
# *suppression* side (`call.unresolved-toplevel`, `call.undefined-method`): a name the project defines at
|
|
@@ -1556,6 +2071,14 @@ module Rigor
|
|
|
1556
2071
|
@discovery.discovered_includes[class_name.to_s] || []
|
|
1557
2072
|
end
|
|
1558
2073
|
|
|
2074
|
+
# Issue #2986 — the modules `class_name` prepends, as written, in search order: the prepended part of
|
|
2075
|
+
# {#includes_of}, which lists them ahead of the included ones. An ActiveSupport::Concern's `prepended do … end`
|
|
2076
|
+
# block runs for a prepend edge only, and its `included do … end` block for an include edge only.
|
|
2077
|
+
def prepends_of(class_name)
|
|
2078
|
+
record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
|
|
2079
|
+
@discovery.discovered_prepends[class_name.to_s] || EMPTY_PREPENDS
|
|
2080
|
+
end
|
|
2081
|
+
|
|
1559
2082
|
# Issue #898 — the module names `extend`ed onto `class_name`'s SINGLETON, as written, gathered up the
|
|
1560
2083
|
# as-written superclass chain because a singleton class inherits its superclass's singleton class
|
|
1561
2084
|
# (`class Base; extend Comparable; end; class Widget < Base; end` leaves `Widget.is_a?(Comparable)`
|
|
@@ -1622,6 +2145,7 @@ module Rigor
|
|
|
1622
2145
|
end
|
|
1623
2146
|
return found if found
|
|
1624
2147
|
|
|
2148
|
+
chain.record_external_absence
|
|
1625
2149
|
chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
|
|
1626
2150
|
end
|
|
1627
2151
|
|
|
@@ -1661,6 +2185,7 @@ module Rigor
|
|
|
1661
2185
|
end
|
|
1662
2186
|
return found if found
|
|
1663
2187
|
|
|
2188
|
+
chain.record_external_absence
|
|
1664
2189
|
chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
|
|
1665
2190
|
end
|
|
1666
2191
|
|
|
@@ -1744,6 +2269,8 @@ module Rigor
|
|
|
1744
2269
|
discovered_method?(entry.name, method_name, entry.side == :singleton ? :singleton : kind_for(kind))
|
|
1745
2270
|
end
|
|
1746
2271
|
return true if found
|
|
2272
|
+
|
|
2273
|
+
record_discovered_miss(chain, method_name) if Analysis::DependencyRecorder.active?
|
|
1747
2274
|
# Budget exhaustion is uncertainty, not absence: answering "not declared" here would hand a
|
|
1748
2275
|
# `call.undefined-method` a fired verdict it has no evidence for. Suppress and record the hit.
|
|
1749
2276
|
return false unless chain.truncated?
|
|
@@ -1752,6 +2279,28 @@ module Rigor
|
|
|
1752
2279
|
true
|
|
1753
2280
|
end
|
|
1754
2281
|
|
|
2282
|
+
# Issue #2689 — the ADR-46 negative edges of a discovery-walk miss: the answer is "no project entry on the chain
|
|
2283
|
+
# defines it", so a later edit that defines it on any of them, or a new file that reopens one of them (and may
|
|
2284
|
+
# change the chain, `include` a module that defines it), turns it. Each project entry files the `method:` key
|
|
2285
|
+
# `Incremental.appeared_symbols` emits for that definition (`Name.method` for a class object's own side,
|
|
2286
|
+
# `Name#method` for an instance-chain entry and for a module on the singleton chain, which answers with its
|
|
2287
|
+
# instance methods) and the `class:` key a declaring file appearing fires — the edges the def walk files for the
|
|
2288
|
+
# same miss (`#user_def_for` per entry, `ResolutionChain#settle` for the classes). A hit files none: existence is
|
|
2289
|
+
# a union, so no other entry gaining the method can turn a found answer.
|
|
2290
|
+
# An external entry (an ancestor declared with no methods, or not yet) files its `class:` key
|
|
2291
|
+
# ({ResolutionChain#record_external_absence}), as the def walks do on a miss.
|
|
2292
|
+
def record_discovered_miss(chain, method_name)
|
|
2293
|
+
chain.record_external_absence
|
|
2294
|
+
chain.entries.each do |entry|
|
|
2295
|
+
next if entry.external?
|
|
2296
|
+
|
|
2297
|
+
separator = entry.side == :singleton ? "." : "#"
|
|
2298
|
+
Analysis::DependencyRecorder.read_missing(:method, "#{entry.name}#{separator}#{method_name}")
|
|
2299
|
+
Analysis::DependencyRecorder.read_missing(:class, entry.last_segment)
|
|
2300
|
+
end
|
|
2301
|
+
end
|
|
2302
|
+
private :record_discovered_miss
|
|
2303
|
+
|
|
1755
2304
|
# A module entry on the singleton chain answers with its INSTANCE methods; on the instance chain, the
|
|
1756
2305
|
# caller's kind stands.
|
|
1757
2306
|
def kind_for(kind) = kind == :singleton ? :instance : kind
|
|
@@ -1970,7 +2519,7 @@ module Rigor
|
|
|
1970
2519
|
EMPTY_PARAMETER_ENVELOPES = {}.freeze
|
|
1971
2520
|
private_constant :EMPTY_PARAMETER_ENVELOPES
|
|
1972
2521
|
|
|
1973
|
-
# v0.1.2 — per-class table mapping `method_name
|
|
2522
|
+
# v0.1.2 — per-class table mapping `method_name → :public | :private | :protected`. Populated by
|
|
1974
2523
|
# `ScopeIndexer` for every `def` and `attr_*` member it sees inside a class body, with the visibility taken from
|
|
1975
2524
|
# a wrapping `private def` / `private attr_reader` call, else the surrounding `private` / `protected` / `public`
|
|
1976
2525
|
# modifier state, plus any post-hoc `private :name, ...` named-argument calls. Consumed by the
|
|
@@ -1989,13 +2538,14 @@ module Rigor
|
|
|
1989
2538
|
table[singleton ? [:singleton, method_name.to_sym] : method_name.to_sym]
|
|
1990
2539
|
end
|
|
1991
2540
|
|
|
1992
|
-
# Closes the "`params[:f] ||= []; params[:f] << x`" precision gap
|
|
1993
|
-
# indexed-collection narrowing through `Hash[k] ||= default`). After `receiver[key] ||= default`, the next
|
|
2541
|
+
# Closes the "`params[:f] ||= []; params[:f] << x`" precision gap. After `receiver[key] ||= default`, the next
|
|
1994
2542
|
# read at `receiver[key]` is known non-nil; recording the post-`||=` type keyed on
|
|
1995
2543
|
# `(receiver_kind, receiver_name, literal_key)` lets the ExpressionTyper's `[]` dispatch hand back the
|
|
1996
2544
|
# narrowed type. Receiver-rebind and `[]=`/mutator invalidation rules are documented at the call sites in
|
|
1997
2545
|
# `Inference::StatementEvaluator`.
|
|
1998
2546
|
def indexed_narrowing(receiver_kind, receiver_name, key)
|
|
2547
|
+
return nil if @indexed_narrowings.empty?
|
|
2548
|
+
|
|
1999
2549
|
@indexed_narrowings[indexed_key(receiver_kind, receiver_name, key)]
|
|
2000
2550
|
end
|
|
2001
2551
|
|
|
@@ -2007,6 +2557,8 @@ module Rigor
|
|
|
2007
2557
|
end
|
|
2008
2558
|
|
|
2009
2559
|
def without_indexed_narrowing(receiver_kind, receiver_name, key)
|
|
2560
|
+
return self if @indexed_narrowings.empty?
|
|
2561
|
+
|
|
2010
2562
|
lookup = indexed_key(receiver_kind, receiver_name, key)
|
|
2011
2563
|
return self unless @indexed_narrowings.key?(lookup)
|
|
2012
2564
|
|
|
@@ -2021,14 +2573,15 @@ module Rigor
|
|
|
2021
2573
|
rebuild(indexed_narrowings: new_table)
|
|
2022
2574
|
end
|
|
2023
2575
|
|
|
2024
|
-
# Closes the "stable receiver method-chain narrowing" gap (
|
|
2025
|
-
# "Method-call receiver narrowing across stable receivers"; 2026-05-28 Redmine survey). After
|
|
2576
|
+
# Closes the "stable receiver method-chain narrowing" gap (2026-05-28 Redmine survey). After
|
|
2026
2577
|
# `if x.last.is_a?(Array)` the dominated body's `x.last` reads MUST observe the truthy-narrowed type; the same
|
|
2027
2578
|
# chain reaching the falsey edge observes the negative narrowing.
|
|
2028
2579
|
#
|
|
2029
2580
|
# Address shape mirrors {.indexed_narrowing}: stable root variable + no-arg single-hop method name. See
|
|
2030
2581
|
# {ChainKey} for the precise contract.
|
|
2031
2582
|
def method_chain_narrowing(receiver_kind, receiver_name, method_name)
|
|
2583
|
+
return nil if @method_chain_narrowings.empty?
|
|
2584
|
+
|
|
2032
2585
|
@method_chain_narrowings[chain_key(receiver_kind, receiver_name, method_name)]
|
|
2033
2586
|
end
|
|
2034
2587
|
|
|
@@ -2040,6 +2593,8 @@ module Rigor
|
|
|
2040
2593
|
end
|
|
2041
2594
|
|
|
2042
2595
|
def without_method_chain_narrowing(receiver_kind, receiver_name, method_name)
|
|
2596
|
+
return self if @method_chain_narrowings.empty?
|
|
2597
|
+
|
|
2043
2598
|
lookup = chain_key(receiver_kind, receiver_name, method_name)
|
|
2044
2599
|
return self unless @method_chain_narrowings.key?(lookup)
|
|
2045
2600
|
|
|
@@ -2070,8 +2625,9 @@ module Rigor
|
|
|
2070
2625
|
# against THIS scope's discovery index (so cross-file dispatches in the body resolve). The incremental
|
|
2071
2626
|
# session re-drives a declaration-stable changed callee at each previously-observed call key to prove its
|
|
2072
2627
|
# return is unchanged before skipping its symbol dependents.
|
|
2073
|
-
def user_method_return(def_node, receiver, arg_types)
|
|
2074
|
-
Inference::ExpressionTyper.new(scope: self)
|
|
2628
|
+
def user_method_return(def_node, receiver, arg_types, positionals_unknown: false)
|
|
2629
|
+
typer = Inference::ExpressionTyper.new(scope: self)
|
|
2630
|
+
typer.return_type_for(def_node, receiver, arg_types, positionals_unknown: positionals_unknown)
|
|
2075
2631
|
end
|
|
2076
2632
|
|
|
2077
2633
|
# Statement-level evaluation: returns the pair `[type, scope']` where `type` is what the node produces and
|
|
@@ -2149,11 +2705,13 @@ module Rigor
|
|
|
2149
2705
|
|
|
2150
2706
|
private
|
|
2151
2707
|
|
|
2152
|
-
# The marks {#==} compares: ADR-58's, issue #667's
|
|
2708
|
+
# The marks {#==} compares: ADR-58's, issue #667's, the repeated `||=` sites, #2555's construction facts (they
|
|
2709
|
+
# change what a mutation seam answers), and #1429's guard state.
|
|
2153
2710
|
def same_marks?(other)
|
|
2154
2711
|
@declaration_sourced == other.declaration_sourced &&
|
|
2155
2712
|
@published_constant_sourced == other.published_constant_sourced &&
|
|
2156
2713
|
@repeated_or_writes == other.repeated_or_writes &&
|
|
2714
|
+
@construction_facts == other.construction_facts &&
|
|
2157
2715
|
same_guard_state?(other)
|
|
2158
2716
|
end
|
|
2159
2717
|
|
|
@@ -2162,7 +2720,16 @@ module Rigor
|
|
|
2162
2720
|
@bot_guard_classes == other.bot_guard_classes
|
|
2163
2721
|
end
|
|
2164
2722
|
|
|
2723
|
+
# `construction_facts` is the one positional parameter. CRuby records which keywords a call left out in a bit
|
|
2724
|
+
# field indexed by each keyword's position among ALL the method's keywords, required and optional
|
|
2725
|
+
# (`VM_KW_SPECIFIED_BITS_MAX`, 31); a keyword past that index whose default is not a literal makes every call that
|
|
2726
|
+
# omits it allocate the record instead. A 32nd keyword here did that on every scope transition, about 1% of an
|
|
2727
|
+
# analysis's allocations (issue #2555). On CRuby 4.0 that allocation is also unsafe: a GC it starts frees the
|
|
2728
|
+
# values already moved into the keyword locals (ruby/ruby Bug #22500, issue #2850). `#initialize` is past the index
|
|
2729
|
+
# and keeps literal defaults there for that reason. The next Scope field crosses it in this method again, so it
|
|
2730
|
+
# needs a positional slot or a restructuring, not a 32nd keyword.
|
|
2165
2731
|
def rebuild(
|
|
2732
|
+
construction_facts = @construction_facts,
|
|
2166
2733
|
locals: @locals, fact_store: @fact_store, self_type: @self_type,
|
|
2167
2734
|
ivars: @ivars, cvars: @cvars, globals: @globals,
|
|
2168
2735
|
discovery: @discovery,
|
|
@@ -2213,7 +2780,7 @@ module Rigor
|
|
|
2213
2780
|
plugin_typed_calls: plugin_typed_calls,
|
|
2214
2781
|
optimistic_origins: optimistic_origins,
|
|
2215
2782
|
optimistic_locals: optimistic_locals,
|
|
2216
|
-
optimistic_ivars: optimistic_ivars,
|
|
2783
|
+
optimistic_ivars: optimistic_ivars, construction_facts: construction_facts,
|
|
2217
2784
|
repeated_or_writes: repeated_or_writes,
|
|
2218
2785
|
match_frame: match_frame,
|
|
2219
2786
|
constant_narrowings: constant_narrowings,
|
|
@@ -2223,11 +2790,18 @@ module Rigor
|
|
|
2223
2790
|
)
|
|
2224
2791
|
end
|
|
2225
2792
|
|
|
2793
|
+
# A join with an empty side has no key in common (usually both are empty, the common case at a branch merge), so
|
|
2794
|
+
# its answer is this one frozen hash rather than a fresh one.
|
|
2795
|
+
EMPTY_BINDINGS = {}.freeze
|
|
2796
|
+
private_constant :EMPTY_BINDINGS
|
|
2797
|
+
|
|
2226
2798
|
def join_bindings(left, right)
|
|
2227
2799
|
# Keys present in both, unioned. Iterating `left` and probing `right.key?` yields the same keys in the same
|
|
2228
2800
|
# order as the prior `(left.keys & right.keys)` while avoiding the two key arrays and the intersection
|
|
2229
2801
|
# array — this is the control-flow join, run at every branch merge, and was a top allocation site (~75% of
|
|
2230
2802
|
# `Hash#keys`).
|
|
2803
|
+
return EMPTY_BINDINGS if left.empty? || right.empty?
|
|
2804
|
+
|
|
2231
2805
|
result = {}
|
|
2232
2806
|
left.each do |name, ltype|
|
|
2233
2807
|
next unless right.key?(name)
|
|
@@ -2306,6 +2880,8 @@ module Rigor
|
|
|
2306
2880
|
optimistic_origins: @optimistic_origins,
|
|
2307
2881
|
optimistic_locals: join_optimistic_marks(@optimistic_locals, other.optimistic_locals),
|
|
2308
2882
|
optimistic_ivars: join_optimistic_marks(@optimistic_ivars, other.optimistic_ivars),
|
|
2883
|
+
# Issue #2555 L3 — INTERSECTION: a construction fact holds after a merge only when both arms established it.
|
|
2884
|
+
construction_facts: join_construction_facts(@construction_facts, other.construction_facts),
|
|
2309
2885
|
# UNION, the published-constant mark's direction: the mark only withholds the memoizing `||=`
|
|
2310
2886
|
# reading, so keeping a site either arm holds is the wider answer.
|
|
2311
2887
|
repeated_or_writes: join_repeated_or_writes(other),
|
|
@@ -2403,6 +2979,31 @@ module Rigor
|
|
|
2403
2979
|
.freeze
|
|
2404
2980
|
end
|
|
2405
2981
|
|
|
2982
|
+
# Issue #2555 L3 — a fact survives a merge only for a name both arms hold it for, and only the facts both agree
|
|
2983
|
+
# on. Zero-alloc when either side is empty or both are the same table.
|
|
2984
|
+
def join_construction_facts(mine, theirs)
|
|
2985
|
+
return mine if mine.equal?(theirs)
|
|
2986
|
+
return EMPTY_ORIGINS if mine.empty? || theirs.empty?
|
|
2987
|
+
|
|
2988
|
+
joined = {}
|
|
2989
|
+
mine.each do |name, facts|
|
|
2990
|
+
their_facts = theirs[name]
|
|
2991
|
+
next if their_facts.nil?
|
|
2992
|
+
|
|
2993
|
+
common = facts == their_facts ? facts : join_fact_sets(facts, their_facts)
|
|
2994
|
+
joined[name] = common unless common.empty?
|
|
2995
|
+
end
|
|
2996
|
+
joined.empty? ? EMPTY_ORIGINS : joined.freeze
|
|
2997
|
+
end
|
|
2998
|
+
|
|
2999
|
+
# `:grown` joins by UNION among the names both arms keep: a store on either arm may reach the carrier, and the
|
|
3000
|
+
# fact only ever floors it ({Inference::MutationRejoin.floors_grown_carrier?}). Every other fact intersects.
|
|
3001
|
+
def join_fact_sets(facts, their_facts)
|
|
3002
|
+
common = facts & their_facts
|
|
3003
|
+
common << :grown if facts.include?(:grown) || their_facts.include?(:grown)
|
|
3004
|
+
common.freeze
|
|
3005
|
+
end
|
|
3006
|
+
|
|
2406
3007
|
# ADR-82 WD1 — rebinding drops any propagated origin for the name (the new value's provenance is set
|
|
2407
3008
|
# afterward by `with_local_origin` when it is a `Dynamic` with a recorded cause). Zero-alloc when the name
|
|
2408
3009
|
# carries no origin — the overwhelming common case, so this stays off the with_local hot-path budget.
|
|
@@ -2464,19 +3065,12 @@ module Rigor
|
|
|
2464
3065
|
end
|
|
2465
3066
|
|
|
2466
3067
|
def indexed_key(receiver_kind, receiver_name, key)
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
receiver_name: receiver_name.to_sym,
|
|
2470
|
-
key: key
|
|
2471
|
-
)
|
|
3068
|
+
# Positional `new` (field-definition order) skips the keyword hash a keyword `new` allocates.
|
|
3069
|
+
IndexedKey.new(receiver_kind.to_sym, receiver_name.to_sym, key)
|
|
2472
3070
|
end
|
|
2473
3071
|
|
|
2474
3072
|
def chain_key(receiver_kind, receiver_name, method_name)
|
|
2475
|
-
ChainKey.new(
|
|
2476
|
-
receiver_kind: receiver_kind.to_sym,
|
|
2477
|
-
receiver_name: receiver_name.to_sym,
|
|
2478
|
-
method_name: method_name.to_sym
|
|
2479
|
-
)
|
|
3073
|
+
ChainKey.new(receiver_kind.to_sym, receiver_name.to_sym, method_name.to_sym)
|
|
2480
3074
|
end
|
|
2481
3075
|
|
|
2482
3076
|
def drop_indexed_narrowings_for(receiver_kind, receiver_name)
|
|
@@ -2558,6 +3152,7 @@ module Rigor
|
|
|
2558
3152
|
# beside it ({#with_global_copy_marks}).
|
|
2559
3153
|
def drop_local_declaration_marks(name)
|
|
2560
3154
|
dropped = drop_declaration_sourced_for(:local, name)
|
|
3155
|
+
dropped = drop_gradual_narrowing_mark(dropped, name)
|
|
2561
3156
|
return dropped if dropped.equal?(@declaration_sourced)
|
|
2562
3157
|
|
|
2563
3158
|
name = name.to_sym
|
|
@@ -2566,6 +3161,18 @@ module Rigor
|
|
|
2566
3161
|
dropped.dup.delete_if { |ref| ref[0] == :global_copy && ref[1] == name }.freeze
|
|
2567
3162
|
end
|
|
2568
3163
|
|
|
3164
|
+
# `marks` without the local `name`'s `:gradual_narrowing` mark (issue #2656); `marks` itself when it has none.
|
|
3165
|
+
def drop_gradual_narrowing_mark(marks, name)
|
|
3166
|
+
return marks if marks.empty?
|
|
3167
|
+
|
|
3168
|
+
ref = [:gradual_narrowing, name.to_sym]
|
|
3169
|
+
return marks unless marks.include?(ref)
|
|
3170
|
+
|
|
3171
|
+
dropped = marks.dup
|
|
3172
|
+
dropped.delete(ref)
|
|
3173
|
+
dropped.freeze
|
|
3174
|
+
end
|
|
3175
|
+
|
|
2569
3176
|
def add_guard_record(key, pre_guard)
|
|
2570
3177
|
return @guard_records if @guard_records.key?(key)
|
|
2571
3178
|
|