rigortype 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
- 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 +5 -2
- data/docs/handbook/11-sig-gen.md +61 -5
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/manual/02-cli-reference.md +192 -27
- data/docs/manual/03-configuration.md +38 -4
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/09-editor-integration.md +13 -2
- data/docs/manual/10-mcp-server.md +47 -3
- data/docs/manual/12-caching.md +9 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +49 -11
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
- 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 +53 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +217 -52
- data/lib/rigor/analysis/crash_signature.rb +2 -2
- data/lib/rigor/analysis/dependency_recorder.rb +15 -4
- 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 +154 -0
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +202 -43
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +7 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
- data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
- 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/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
- data/lib/rigor/analysis/runner.rb +233 -120
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
- 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 +38 -33
- data/lib/rigor/bleeding_edge.rb +3 -6
- 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/incremental_snapshot.rb +60 -18
- 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 +54 -3
- data/lib/rigor/cache/rbs_environment.rb +4 -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 +145 -55
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +188 -93
- 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 +27 -21
- 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 +18 -17
- 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 +17 -13
- data/lib/rigor/cli/lsp_command.rb +250 -23
- data/lib/rigor/cli/mcp_command.rb +14 -8
- 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 +60 -4
- data/lib/rigor/cli/plugin_command.rb +93 -46
- data/lib/rigor/cli/plugins_command.rb +22 -18
- data/lib/rigor/cli/plugins_renderer.rb +34 -8
- 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 +23 -16
- data/lib/rigor/cli/sig_gen_command.rb +130 -34
- 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 +1 -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 +18 -12
- data/lib/rigor/cli/type_of_project_scope.rb +24 -0
- 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 +209 -54
- data/lib/rigor/config_audit.rb +17 -6
- data/lib/rigor/configuration/severity_profile.rb +5 -5
- data/lib/rigor/configuration.rb +86 -34
- 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 +5 -9
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +1 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +283 -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/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_hierarchy.rb +6 -15
- data/lib/rigor/environment/rbs_loader.rb +592 -70
- data/lib/rigor/environment/reflection.rb +7 -7
- data/lib/rigor/environment/required_features.rb +136 -32
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
- data/lib/rigor/environment.rb +44 -11
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +93 -32
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +178 -0
- data/lib/rigor/inference/budget_trace.rb +8 -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 +126 -0
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- data/lib/rigor/inference/construction_escape.rb +217 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/deep_erasure.rb +170 -0
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +1 -1
- data/lib/rigor/inference/def_return_typer.rb +204 -12
- data/lib/rigor/inference/definer_resolution.rb +21 -5
- data/lib/rigor/inference/definers_index.rb +106 -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 +17 -3
- data/lib/rigor/inference/expression_typer.rb +173 -57
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/global_write_census.rb +110 -1
- 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/indexed_narrowing.rb +4 -7
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +246 -0
- data/lib/rigor/inference/key_presence_guard.rb +2 -1
- data/lib/rigor/inference/loop_value_record.rb +57 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
- data/lib/rigor/inference/match_rebinding.rb +11 -1
- data/lib/rigor/inference/memo_audit.rb +55 -0
- 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 +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
- 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 +2 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
- 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/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 +109 -29
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +23 -1
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +162 -29
- data/lib/rigor/inference/narrowing.rb +113 -38
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/optimistic_origin.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/project_method_ownership.rb +6 -3
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +122 -0
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
- data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
- data/lib/rigor/inference/remover_holder_scan.rb +233 -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 +9 -1
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
- 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 +135 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +614 -146
- data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
- data/lib/rigor/inference/statement_evaluator.rb +3036 -278
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
- data/lib/rigor/inference/toplevel_def_census.rb +59 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -1
- data/lib/rigor/language_server/buffer_table.rb +9 -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 +28 -9
- 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 +98 -4
- 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 +12 -6
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/lens/plugins.rb +1 -1
- data/lib/rigor/mcp/server.rb +82 -3
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +1 -1
- 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 +38 -16
- data/lib/rigor/plugin/registry.rb +41 -3
- 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 +2 -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 +96 -2
- 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 +5 -3
- data/lib/rigor/rooted_glob.rb +25 -2
- data/lib/rigor/run_scoped_slots.rb +38 -0
- data/lib/rigor/runtime/jit.rb +1 -1
- data/lib/rigor/scope/discovery_index.rb +19 -6
- data/lib/rigor/scope/resolution_chain.rb +3 -1
- data/lib/rigor/scope.rb +379 -36
- data/lib/rigor/sig_gen/classification.rb +4 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +668 -84
- 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 +23 -4
- data/lib/rigor/sig_gen/observation_collector.rb +7 -6
- 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/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +21 -12
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- 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 +217 -37
- 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 +13 -16
- data/lib/rigor/type/constant.rb +48 -7
- data/lib/rigor/type/hash_shape.rb +26 -40
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/type/union.rb +1 -1
- 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/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 +89 -70
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -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 +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
- 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 +2 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
- 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 +7 -12
- 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 +19 -6
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -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 +7 -6
- 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 +4 -5
- 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-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 +2 -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 +1 -3
- 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 +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- 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 +5 -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 +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +0 -2
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +10 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +1 -0
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +6 -5
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +6 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +51 -50
- data/sig/rigor/type.rbs +0 -6
- 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/03-baseline-and-bugs.md +12 -1
- metadata +55 -3
data/lib/rigor/scope.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "type"
|
|
4
4
|
require_relative "environment"
|
|
5
5
|
require_relative "scope/discovery_index"
|
|
6
|
+
require_relative "run_scoped_slots"
|
|
6
7
|
require_relative "scope/resolution_chain"
|
|
7
8
|
require_relative "inference/definer_resolution"
|
|
8
9
|
require_relative "inference/in_effect_refinements"
|
|
@@ -13,6 +14,9 @@ require_relative "inference/flow_tracer"
|
|
|
13
14
|
require_relative "inference/optimistic_origin"
|
|
14
15
|
require_relative "inference/statement_evaluator"
|
|
15
16
|
require_relative "inference/def_node_resolver"
|
|
17
|
+
# Issue #2608 — read by `MutationWidening` at a remover call, which only an analysis makes; loaded with the engine
|
|
18
|
+
# so the engine-free value layer that requires `MutationWidening` stays as small as it was.
|
|
19
|
+
require_relative "inference/remover_narrowing"
|
|
16
20
|
|
|
17
21
|
module Rigor
|
|
18
22
|
# Immutable analyzer scope: holds local-variable bindings and a reference to the surrounding Environment. State
|
|
@@ -30,7 +34,7 @@ module Rigor
|
|
|
30
34
|
:opaque_block_self, :block_self_unknown, :singleton_class_body, :lexical_nesting,
|
|
31
35
|
:dynamic_origins, :local_origins, :ivar_origins,
|
|
32
36
|
:void_origins, :plugin_typed_calls,
|
|
33
|
-
:optimistic_origins, :optimistic_locals, :optimistic_ivars,
|
|
37
|
+
:optimistic_origins, :optimistic_locals, :optimistic_ivars, :construction_facts,
|
|
34
38
|
:repeated_or_writes, :match_frame,
|
|
35
39
|
:constant_narrowings, :guard_records, :bot_guard_classes, :declared_refinements
|
|
36
40
|
|
|
@@ -89,7 +93,7 @@ module Rigor
|
|
|
89
93
|
def discovered_prepends = @discovery.discovered_prepends.transform_values { |names| names.uniq.freeze }.freeze
|
|
90
94
|
def discovered_extends = @discovery.discovered_extends
|
|
91
95
|
def discovered_class_sources = @discovery.discovered_class_sources
|
|
92
|
-
# Issue #644 — `{qualified constant name =>
|
|
96
|
+
# Issue #644 — `{qualified constant name => declaring files}`; seeded only on an ADR-46 recording run.
|
|
93
97
|
def constant_sources = @discovery.constant_sources
|
|
94
98
|
def published_constant_names = @discovery.published_constant_names
|
|
95
99
|
def local_constant_names = @discovery.local_constant_names
|
|
@@ -202,9 +206,9 @@ module Rigor
|
|
|
202
206
|
# `:const` allowed; its entry marks presence only.
|
|
203
207
|
IndexedKey = Data.define(:receiver_kind, :receiver_name, :key)
|
|
204
208
|
|
|
205
|
-
# Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain
|
|
206
|
-
#
|
|
207
|
-
#
|
|
209
|
+
# Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain).
|
|
210
|
+
# The value of the map at this key is the narrowed type the next read of the same chain MUST observe —
|
|
211
|
+
# typically the post-`is_a?(C)` narrowing established on a predicate edge.
|
|
208
212
|
#
|
|
209
213
|
# - `receiver_kind` ∈ `{:local, :ivar}` — the analyzer only tracks chains rooted at a local or instance
|
|
210
214
|
# variable today (Law-of-Demeter-style single-hop).
|
|
@@ -232,7 +236,7 @@ module Rigor
|
|
|
232
236
|
# through flow transitions and ignored by `==` / `hash`.
|
|
233
237
|
EMPTY_FOLD_SAFE = Set.new.freeze
|
|
234
238
|
# ADR-82 WD1 — provenance propagation across the receiver-node lookup. `local_origins` / `ivar_origins`
|
|
235
|
-
# map a `name`
|
|
239
|
+
# map a `name` to the {Inference::DynamicOrigin} cause of the `Dynamic` value currently bound to
|
|
236
240
|
# it, so a downstream dispatch whose receiver is a bare `x` / `@x` read resolves to why that value is
|
|
237
241
|
# dynamic (the cause was recorded on the *assignment*'s rhs node, which the receiver-read node is not).
|
|
238
242
|
# Advisory metadata like {#dynamic_origins}: ignored by `==` / `hash` (never varies a flow decision) and
|
|
@@ -322,6 +326,8 @@ module Rigor
|
|
|
322
326
|
@plugin_typed_calls.key?(node)
|
|
323
327
|
end
|
|
324
328
|
|
|
329
|
+
# Every keyword past the 31st takes CRuby's allocating unspecified-keywords path when omitted with a non-literal
|
|
330
|
+
# default; see the note at {#rebuild} before adding a field.
|
|
325
331
|
def initialize( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- one keyword and assignment per field
|
|
326
332
|
environment:, locals:,
|
|
327
333
|
fact_store: Analysis::FactStore.empty,
|
|
@@ -348,6 +354,7 @@ module Rigor
|
|
|
348
354
|
optimistic_origins: {}.compare_by_identity,
|
|
349
355
|
optimistic_locals: EMPTY_ORIGINS,
|
|
350
356
|
optimistic_ivars: EMPTY_ORIGINS,
|
|
357
|
+
construction_facts: EMPTY_ORIGINS,
|
|
351
358
|
repeated_or_writes: EMPTY_REPEATED_OR_WRITES,
|
|
352
359
|
match_frame: nil,
|
|
353
360
|
constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
|
|
@@ -381,6 +388,7 @@ module Rigor
|
|
|
381
388
|
@optimistic_origins = optimistic_origins
|
|
382
389
|
@optimistic_locals = optimistic_locals
|
|
383
390
|
@optimistic_ivars = optimistic_ivars
|
|
391
|
+
@construction_facts = construction_facts
|
|
384
392
|
@repeated_or_writes = repeated_or_writes
|
|
385
393
|
@match_frame = match_frame
|
|
386
394
|
@constant_narrowings = constant_narrowings
|
|
@@ -433,6 +441,41 @@ module Rigor
|
|
|
433
441
|
rebuild(optimistic_ivars: @optimistic_ivars.merge(name.to_sym => mark).freeze)
|
|
434
442
|
end
|
|
435
443
|
|
|
444
|
+
# Issue #2555 L3 — the construction facts a LOCAL's binding carries: what the engine knows about how the object
|
|
445
|
+
# bound to it was made, as opposed to what its type says about its contents. `:synthesised` means the engine
|
|
446
|
+
# built the object itself (a collection literal, or an `Array.new` / `Hash.new` / `Set.new` / `X[...]`
|
|
447
|
+
# constructor on the core class), so its element types are this engine's own inference and no declaration's
|
|
448
|
+
# claim: the mutation seams may re-grow such a carrier precisely, where a declared one (a parameter, an RBS
|
|
449
|
+
# return) keeps the issue #561 boundary. The table is `{ name => frozen Set[Symbol] }`, so a second fact joins
|
|
450
|
+
# an existing entry without reshaping it.
|
|
451
|
+
#
|
|
452
|
+
# Locals only. An instance variable's binding is seeded across methods from the class-ivar index, whose
|
|
453
|
+
# construction no single body witnesses, so an ivar never carries a fact. Like {#optimistic_locals} the facts
|
|
454
|
+
# are kept by {#with_mutated_local} (the binding still names the same object), dropped by {#with_local} (a
|
|
455
|
+
# source-level rebind names a new one), intersected at a merge, and reset with the locals at a method body's
|
|
456
|
+
# entry. No fact is copied by an alias write (`b = a`): the type is shared, the provenance is not tracked
|
|
457
|
+
# through a second name (issue #2335), and a local that may escape anywhere in its body is never stamped
|
|
458
|
+
# ({Inference::ConstructionEscape}). Within an entry `:synthesised` intersects at a merge and `:grown` (a store
|
|
459
|
+
# has grown the carrier since construction) unions.
|
|
460
|
+
CONSTRUCTION_FACT_SETS = Ractor.make_shareable({ synthesised: Set[:synthesised] })
|
|
461
|
+
NO_CONSTRUCTION_FACTS = Set.new.freeze
|
|
462
|
+
private_constant :CONSTRUCTION_FACT_SETS, :NO_CONSTRUCTION_FACTS
|
|
463
|
+
|
|
464
|
+
def construction_fact?(name, fact = :synthesised)
|
|
465
|
+
@construction_facts.fetch(name.to_sym, NO_CONSTRUCTION_FACTS).include?(fact)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Records `fact` on the local `name`. Applied AFTER the {#with_local} that binds the value, which drops any
|
|
469
|
+
# fact the old binding held.
|
|
470
|
+
def with_construction_fact(name, fact)
|
|
471
|
+
key = name.to_sym
|
|
472
|
+
existing = @construction_facts[key]
|
|
473
|
+
return self if existing&.include?(fact)
|
|
474
|
+
|
|
475
|
+
facts = existing ? (existing | [fact]).freeze : (CONSTRUCTION_FACT_SETS[fact] || Set[fact].freeze)
|
|
476
|
+
rebuild(@construction_facts.merge(key => facts).freeze)
|
|
477
|
+
end
|
|
478
|
+
|
|
436
479
|
# ADR-82 WD1 — the propagated origin of the `Dynamic` value currently bound to a local / instance
|
|
437
480
|
# variable, or `nil` when none is tracked. Consulted by `Inference::ProtectionScanner` when a dispatch's
|
|
438
481
|
# receiver is a bare `x` / `@x` read whose own node carries no origin.
|
|
@@ -487,7 +530,8 @@ module Rigor
|
|
|
487
530
|
# longer holds (the new value may carry a method-local nil). `with_declaration_sourced_local` re-establishes
|
|
488
531
|
# the mark afterward when the RHS is a pure copy of a declaration-sourced ivar read; the default is to drop
|
|
489
532
|
# it.
|
|
490
|
-
rebuild(
|
|
533
|
+
rebuild(keep_marks ? @construction_facts : drop_origin(@construction_facts, name),
|
|
534
|
+
locals: new_locals, fact_store: new_fact_store,
|
|
491
535
|
indexed_narrowings: new_indexed_narrowings,
|
|
492
536
|
method_chain_narrowings: new_chain_narrowings,
|
|
493
537
|
declaration_sourced: keep_marks ? @declaration_sourced : drop_local_declaration_marks(name),
|
|
@@ -681,9 +725,8 @@ module Rigor
|
|
|
681
725
|
|
|
682
726
|
# Slice 7 phase 1 — instance/class/global variable bindings. `ivar(name)` / `cvar(name)` / `global(name)`
|
|
683
727
|
# 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.
|
|
728
|
+
# the analyzed slice of the program. Each `def` enters with a fresh binding map seeded from the class-level
|
|
729
|
+
# accumulators ({#class_ivars_for}, {#class_cvars_for}).
|
|
687
730
|
def ivar(name)
|
|
688
731
|
@ivars[name.to_sym]
|
|
689
732
|
end
|
|
@@ -1186,8 +1229,8 @@ module Rigor
|
|
|
1186
1229
|
end
|
|
1187
1230
|
|
|
1188
1231
|
# Slice 7 phase 2 — class-level ivar accumulator. Keyed by the qualified class name (e.g. `"Rigor::Scope"`);
|
|
1189
|
-
# the value
|
|
1190
|
-
#
|
|
1232
|
+
# the value maps every ivar that appears as a write target inside any def body of that class to its type.
|
|
1233
|
+
# `StatementEvaluator#build_method_entry_scope` seeds the method body's `ivars` map from this
|
|
1191
1234
|
# table so a `def get; @x; end` reads the type written in a sibling `def init; @x = 1; end`.
|
|
1192
1235
|
#
|
|
1193
1236
|
# `ScopeIndexer` populates the table once at index time through a separate pre-pass over the program. The map
|
|
@@ -1207,7 +1250,7 @@ module Rigor
|
|
|
1207
1250
|
@discovery.class_cvars[class_name.to_s] || EMPTY_VAR_BINDINGS
|
|
1208
1251
|
end
|
|
1209
1252
|
|
|
1210
|
-
# Slice 7 phase 12 — in-source method discovery. Maps a qualified class name to a
|
|
1253
|
+
# Slice 7 phase 12 — in-source method discovery. Maps a qualified class name to a table of
|
|
1211
1254
|
# `method_name => :instance | :singleton`. Populated by `ScopeIndexer` from every `Prism::DefNode` and
|
|
1212
1255
|
# recognised `define_method` invocation inside class/module bodies. The `rigor check` undefined-method and
|
|
1213
1256
|
# wrong-arity rules consult this map to suppress diagnostics for methods the user has defined dynamically,
|
|
@@ -1230,6 +1273,33 @@ module Rigor
|
|
|
1230
1273
|
true
|
|
1231
1274
|
end
|
|
1232
1275
|
|
|
1276
|
+
# Issue #2555 — whether the project defines a method named `method_name` anywhere: on any class or module,
|
|
1277
|
+
# instance or singleton side (the def-node tables, which hold a plain cross-file `def` on an RBS-declared class
|
|
1278
|
+
# that `discovered_methods` withholds, and `discovered_methods`), or in a `refine` block. The name set is built
|
|
1279
|
+
# once per discovery index. The answer depends on every file, so an incremental run records the name either way:
|
|
1280
|
+
# a file that starts or stops defining it re-analyses the reader (issue #1459's `defines:` edge).
|
|
1281
|
+
def project_method_name?(method_name)
|
|
1282
|
+
Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
|
|
1283
|
+
table = (Thread.current[PROJECT_METHOD_NAMES_KEY] ||= ObjectSpace::WeakKeyMap.new)
|
|
1284
|
+
return true if (table[@discovery] ||= collect_project_method_names).include?(method_name.to_sym)
|
|
1285
|
+
|
|
1286
|
+
false
|
|
1287
|
+
end
|
|
1288
|
+
|
|
1289
|
+
def collect_project_method_names
|
|
1290
|
+
names = Set.new
|
|
1291
|
+
[@discovery.discovered_methods, @discovery.discovered_def_nodes, @discovery.discovered_singleton_def_nodes,
|
|
1292
|
+
@discovery.discovered_refinements].each do |by_owner|
|
|
1293
|
+
by_owner.each_value { |methods| methods.each_key { |method_name| names << method_name.to_sym } }
|
|
1294
|
+
end
|
|
1295
|
+
names.freeze
|
|
1296
|
+
end
|
|
1297
|
+
private :collect_project_method_names
|
|
1298
|
+
|
|
1299
|
+
# A run-scoped slot ({RunScopedSlots}): emptied when a run ends, so an edited project is read afresh.
|
|
1300
|
+
PROJECT_METHOD_NAMES_KEY = RunScopedSlots.register(:__rigor_project_method_names)
|
|
1301
|
+
private_constant :PROJECT_METHOD_NAMES_KEY
|
|
1302
|
+
|
|
1233
1303
|
def record_discovered_method(class_name, method_name)
|
|
1234
1304
|
record_class_dependency(class_name)
|
|
1235
1305
|
Analysis::DependencyRecorder.read_name(:defines, method_name)
|
|
@@ -1258,7 +1328,7 @@ module Rigor
|
|
|
1258
1328
|
per_class && per_class[method_name]
|
|
1259
1329
|
end
|
|
1260
1330
|
|
|
1261
|
-
# v0.0.2 #5 — per-class table mapping `method_name
|
|
1331
|
+
# v0.0.2 #5 — per-class table mapping `method_name → Prism::DefNode`. Populated by `ScopeIndexer`
|
|
1262
1332
|
# alongside `discovered_methods` for instance-side defs only (singleton-side and `define_method`-introduced
|
|
1263
1333
|
# methods do not contribute a static body the engine can re-type). Consumed by `ExpressionTyper` to do
|
|
1264
1334
|
# inter-procedural return-type inference when the receiver class is user-defined and has no RBS sig.
|
|
@@ -1272,6 +1342,86 @@ module Rigor
|
|
|
1272
1342
|
Inference::DefNodeResolver.resolve(entry)
|
|
1273
1343
|
end
|
|
1274
1344
|
|
|
1345
|
+
# Issue #2515 — `[class_name, open]` for each class or module whose instance def table holds `method_name`, in
|
|
1346
|
+
# table order (frozen; empty when no user def has the name). The callee floor's hand-off on a receiver it cannot
|
|
1347
|
+
# type reads every def of the name, so the answer depends on the whole project's defs of that name: a recorder
|
|
1348
|
+
# files the `defines:<name>` name edge, which fires when a file starts or stops defining it. `open` is true when
|
|
1349
|
+
# the real receiver may not be the class the def sits on: the owner is a module (an includer's methods are
|
|
1350
|
+
# called from it), or a project class names it as its superclass (a subclass may override what the def calls).
|
|
1351
|
+
# A recorder files `subclass_of:<owner>` for the second.
|
|
1352
|
+
def user_instance_def_owners(method_name)
|
|
1353
|
+
Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
|
|
1354
|
+
name = method_name.to_sym
|
|
1355
|
+
owners = nil
|
|
1356
|
+
@discovery.discovered_def_nodes.each do |class_name, defs|
|
|
1357
|
+
next unless defs.key?(name) && class_name != Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
1358
|
+
|
|
1359
|
+
(owners ||= []) << [class_name, open_to_override?(class_name)].freeze
|
|
1360
|
+
end
|
|
1361
|
+
owners.nil? ? EMPTY_OWNERS : owners.freeze
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
EMPTY_OWNERS = [].freeze
|
|
1365
|
+
private_constant :EMPTY_OWNERS
|
|
1366
|
+
|
|
1367
|
+
# Issue #2515 — `[opaque, keys]`: whether the project holds a subclass relation the superclass table does not
|
|
1368
|
+
# spell as `class X < Name`, so that no owner can be called free of an override. It holds when a class takes
|
|
1369
|
+
# `Class.new(Parent)` ({DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK}), when a class's superclass expression is
|
|
1370
|
+
# unreadable (`nil` in the table), or when a written superclass is not a declared class and some file assigns a
|
|
1371
|
+
# constant of that name (`H2 = Helper; class Sub < H2`; the writer census {#bound_constant_names} or this file's
|
|
1372
|
+
# constant table).
|
|
1373
|
+
# `keys` are the frozen ADR-46 name keys a recorder files for the answer; the caller files them, since the answer
|
|
1374
|
+
# is memoised per discovery.
|
|
1375
|
+
def subclassing_opacity
|
|
1376
|
+
keys = OPACITY_KEYS.dup
|
|
1377
|
+
opaque = @discovery.discovered_parameter_envelopes.each_value.any? do |bucket|
|
|
1378
|
+
bucket.key?(DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
|
|
1379
|
+
end
|
|
1380
|
+
segments = {}
|
|
1381
|
+
@discovery.discovered_superclasses.each_value do |parent|
|
|
1382
|
+
opaque = true if parent.nil? || alias_superclass?(parent, segments, keys)
|
|
1383
|
+
end
|
|
1384
|
+
[opaque ? true : false, keys.each(&:freeze).freeze].freeze
|
|
1385
|
+
end
|
|
1386
|
+
|
|
1387
|
+
# True when `parent`, a written superclass that is no declared class, names a constant some file assigns. Files
|
|
1388
|
+
# the `constant:<segment>` key for it once per segment.
|
|
1389
|
+
def alias_superclass?(parent, segments, keys)
|
|
1390
|
+
return false if @discovery.discovered_classes.key?(parent)
|
|
1391
|
+
|
|
1392
|
+
cut = parent.rindex("::")
|
|
1393
|
+
segment = cut ? parent.byteslice(cut + 2, parent.bytesize - cut - 2) : parent
|
|
1394
|
+
return false if segments.key?(segment)
|
|
1395
|
+
|
|
1396
|
+
segments[segment] = true
|
|
1397
|
+
keys << "constant:#{segment}"
|
|
1398
|
+
@discovery.constant_writers.key?(segment) || @discovery.in_source_constants.key?(parent)
|
|
1399
|
+
end
|
|
1400
|
+
private :alias_superclass?
|
|
1401
|
+
|
|
1402
|
+
OPACITY_KEYS = %w[subclass_of:* mark:*].freeze
|
|
1403
|
+
private_constant :OPACITY_KEYS
|
|
1404
|
+
|
|
1405
|
+
def open_to_override?(class_name)
|
|
1406
|
+
Analysis::DependencyRecorder.read_last_segment(:subclass_of, class_name) if Analysis::DependencyRecorder.active?
|
|
1407
|
+
return true if @discovery.discovered_parameter_envelopes[class_name]&.key?(DiscoveryIndex::ENVELOPE_MODULE_MARK)
|
|
1408
|
+
|
|
1409
|
+
@discovery.discovered_superclasses.any? do |child, parent|
|
|
1410
|
+
child != class_name && parent && names_class?(parent, class_name)
|
|
1411
|
+
end
|
|
1412
|
+
end
|
|
1413
|
+
private :open_to_override?
|
|
1414
|
+
|
|
1415
|
+
# True when a superclass written as `written` (`Helper`, `A::Helper`, `::Helper`) may name `qualified`.
|
|
1416
|
+
def names_class?(written, qualified)
|
|
1417
|
+
written = written[2..] if written.start_with?("::")
|
|
1418
|
+
return true if written == qualified
|
|
1419
|
+
return false unless qualified.end_with?(written)
|
|
1420
|
+
|
|
1421
|
+
qualified.getbyte(qualified.bytesize - written.bytesize - 1) == 58
|
|
1422
|
+
end
|
|
1423
|
+
private :names_class?
|
|
1424
|
+
|
|
1275
1425
|
# Module-singleton call resolution (ADR-57 follow-up) — companion of {#user_def_for} for SINGLETON-side defs
|
|
1276
1426
|
# (`def self.x`, `def Foo.x`, `class << self` bodies, and `module_function` defs). Returns the
|
|
1277
1427
|
# `Prism::DefNode` for `class_name.method_name` invoked on the module/class constant itself, or nil. The
|
|
@@ -1317,6 +1467,147 @@ module Rigor
|
|
|
1317
1467
|
Inference::DefNodeResolver.resolve(entry)
|
|
1318
1468
|
end
|
|
1319
1469
|
|
|
1470
|
+
# Issue #2515 — why the `<toplevel>` table's def for `method_name` may not be the method a receiverless call of that
|
|
1471
|
+
# name runs, or nil when the project's own tables show nothing else that could be. The table keeps one def per name
|
|
1472
|
+
# (the last writer's) and also files the defs of an anonymous `Class.new` body, so:
|
|
1473
|
+
#
|
|
1474
|
+
# - `:contested` — a def in a block or under a condition wrote the slot last;
|
|
1475
|
+
# - `:several` — the project holds more than one ownerless definition row of the name: two top-level defs (across
|
|
1476
|
+
# files, or redefined), a def in a block or in another def, `define_method` or `alias_method` of the name;
|
|
1477
|
+
# - `:main_singleton` — a `def self.name` (or a `class << self` def) at the top level defines it on `main`;
|
|
1478
|
+
# - `:aliased` — an `alias` statement or `alias_method` call names it as the new name;
|
|
1479
|
+
# - `:anonymous` — an anonymous class body defines it;
|
|
1480
|
+
# - `:object` — `Object` itself defines it (`class Object; def name`, `define_method`) or has a module prepended,
|
|
1481
|
+
# which Ruby searches ahead of the top-level def. A module included at the top level is searched after `Object`'s
|
|
1482
|
+
# own methods, so it is no reason.
|
|
1483
|
+
#
|
|
1484
|
+
# A name with no row at all (a scope built without the project pre-pass) is not `:several`: the rows say nothing.
|
|
1485
|
+
# The answer depends on every file, so an incremental run records the `defines:<name>` edge and the census edge of
|
|
1486
|
+
# an alias either way. The two project-wide scans are built once per table, not per call.
|
|
1487
|
+
def toplevel_def_ambiguity(method_name)
|
|
1488
|
+
name = method_name.to_sym
|
|
1489
|
+
record_toplevel_ambiguity_reads(name) if Analysis::DependencyRecorder.active?
|
|
1490
|
+
key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
1491
|
+
return :contested if @discovery.contested_discovered_def_nodes.include?([key, name])
|
|
1492
|
+
|
|
1493
|
+
rows = toplevel_definition_rows[name]
|
|
1494
|
+
return :main_singleton if rows == MAIN_SINGLETON_ROWS
|
|
1495
|
+
return :several if rows > 1
|
|
1496
|
+
return :aliased if method_alias_names.include?(name)
|
|
1497
|
+
|
|
1498
|
+
return :anonymous if @discovery.discovered_def_nodes.any? do |class_name, defs|
|
|
1499
|
+
defs.key?(name) && class_name.start_with?(Type::AnonymousClassName::PREFIX)
|
|
1500
|
+
end
|
|
1501
|
+
|
|
1502
|
+
:object if object_answers_first?(name)
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
# True when `Object` defines `name` itself or has a module prepended (see {#toplevel_def_ambiguity}).
|
|
1506
|
+
def object_answers_first?(name)
|
|
1507
|
+
!user_def_for("Object", name).nil? || discovered_method?("Object", name, :instance) ||
|
|
1508
|
+
!(@discovery.discovered_prepends["Object"] || EMPTY_PREPENDS).empty?
|
|
1509
|
+
end
|
|
1510
|
+
private :object_answers_first?
|
|
1511
|
+
|
|
1512
|
+
EMPTY_PREPENDS = [].freeze
|
|
1513
|
+
private_constant :EMPTY_PREPENDS
|
|
1514
|
+
|
|
1515
|
+
# True for a class or module the project declares, even one with no method of its own.
|
|
1516
|
+
def declared_class?(class_name) = @discovery.discovered_classes.key?(class_name.to_s)
|
|
1517
|
+
|
|
1518
|
+
def record_toplevel_ambiguity_reads(name)
|
|
1519
|
+
Analysis::DependencyRecorder.read_name(:defines, name)
|
|
1520
|
+
Analysis::DependencyRecorder.read_name(:global_census, "method_alias:#{name}")
|
|
1521
|
+
Analysis::DependencyRecorder.read_last_segment(:class, "Object")
|
|
1522
|
+
record_class_dependency("Object")
|
|
1523
|
+
end
|
|
1524
|
+
private :record_toplevel_ambiguity_reads
|
|
1525
|
+
|
|
1526
|
+
# Issue #2336 — whether the project gives `method_name` a body through an ownerless definition row
|
|
1527
|
+
# ({#toplevel_definition_rows}): a `define_method` / `define_singleton_method` / `alias_method` call with that
|
|
1528
|
+
# literal name on any receiver, a def in a block, or (with `top_level:`) a def outside every class body. The
|
|
1529
|
+
# callee mutation floor reads a call no def table answers as one such body may run. The answer depends on every
|
|
1530
|
+
# file, so an incremental run records the `defines:<name>` edge either way.
|
|
1531
|
+
def ownerless_definition?(method_name, top_level: true)
|
|
1532
|
+
Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
|
|
1533
|
+
name = method_name.to_sym
|
|
1534
|
+
return true if top_level ? toplevel_definition_rows.key?(name) : deferred_definition_names.include?(name)
|
|
1535
|
+
|
|
1536
|
+
false
|
|
1537
|
+
end
|
|
1538
|
+
|
|
1539
|
+
# The names of the ownerless rows that claim no side ({#ownerless_definition?} without `top_level:`): a definer
|
|
1540
|
+
# call's literal name and a def in a block. Held by the table's identity, as {#toplevel_definition_rows} is.
|
|
1541
|
+
def deferred_definition_names
|
|
1542
|
+
ranges = @discovery.discovered_deferred_ranges
|
|
1543
|
+
slots = (Thread.current[DEFERRED_NAMES_KEY] ||= [])
|
|
1544
|
+
slot = slots.find { |entry| entry[0].equal?(ranges) }
|
|
1545
|
+
return slot[1] if slot
|
|
1546
|
+
|
|
1547
|
+
names = Set.new
|
|
1548
|
+
ranges.each_value do |entries|
|
|
1549
|
+
entries.each { |(_start, _finish, name, kind, owner)| names << name if name && kind.nil? && owner.nil? }
|
|
1550
|
+
end
|
|
1551
|
+
slots.unshift([ranges, names.freeze]).slice!(2..)
|
|
1552
|
+
names
|
|
1553
|
+
end
|
|
1554
|
+
private :deferred_definition_names
|
|
1555
|
+
|
|
1556
|
+
# Issue #2336 — {#module_function_def?}'s `:both` reading for a def in any file: whether `def_node`, an instance
|
|
1557
|
+
# def of `owner`, is a module function by a deferred-range row over it (`module_function def x`, or a bare
|
|
1558
|
+
# `module_function` before it), which the singleton def table does not mirror for the argument form.
|
|
1559
|
+
def module_function_row_def?(def_node, owner)
|
|
1560
|
+
start = def_node.location.start_offset
|
|
1561
|
+
name = def_node.name
|
|
1562
|
+
owner = owner.to_s
|
|
1563
|
+
discovered_deferred_ranges.each_value.any? do |entries|
|
|
1564
|
+
entries.any? do |(row_start, _finish, row_name, kind, row_owner)|
|
|
1565
|
+
row_start == start && kind == :both && row_name == name && row_owner == owner
|
|
1566
|
+
end
|
|
1567
|
+
end
|
|
1568
|
+
end
|
|
1569
|
+
|
|
1570
|
+
# `{ name => ownerless definition rows }` over `discovered_deferred_ranges`, {MAIN_SINGLETON_ROWS} for a name with
|
|
1571
|
+
# a singleton row. Held by the table's identity (a file's merged copy and the seed), released with the run.
|
|
1572
|
+
def toplevel_definition_rows
|
|
1573
|
+
ranges = @discovery.discovered_deferred_ranges
|
|
1574
|
+
slots = (Thread.current[TOPLEVEL_ROWS_KEY] ||= [])
|
|
1575
|
+
slot = slots.find { |entry| entry[0].equal?(ranges) }
|
|
1576
|
+
return slot[1] if slot
|
|
1577
|
+
|
|
1578
|
+
rows = Hash.new(0)
|
|
1579
|
+
ranges.each_value do |entries|
|
|
1580
|
+
entries.each do |(_start, _finish, name, kind, owner)|
|
|
1581
|
+
next if name.nil? || !owner.nil?
|
|
1582
|
+
|
|
1583
|
+
rows[name] = kind == :singleton || rows[name] == MAIN_SINGLETON_ROWS ? MAIN_SINGLETON_ROWS : rows[name] + 1
|
|
1584
|
+
end
|
|
1585
|
+
end
|
|
1586
|
+
slots.unshift([ranges, rows.freeze]).slice!(2..)
|
|
1587
|
+
rows
|
|
1588
|
+
end
|
|
1589
|
+
private :toplevel_definition_rows
|
|
1590
|
+
|
|
1591
|
+
# The names an `alias` statement or `alias_method` call introduces, from the global write census; held by the
|
|
1592
|
+
# census's identity as {#toplevel_definition_rows} is.
|
|
1593
|
+
def method_alias_names
|
|
1594
|
+
census = @discovery.discovered_global_write_census
|
|
1595
|
+
slots = (Thread.current[METHOD_ALIASES_KEY] ||= [])
|
|
1596
|
+
slot = slots.find { |entry| entry[0].equal?(census) }
|
|
1597
|
+
return slot[1] if slot
|
|
1598
|
+
|
|
1599
|
+
names = census.each_with_object(Set.new) { |entry, acc| acc << entry[1] if entry[0] == :method_alias }.freeze
|
|
1600
|
+
slots.unshift([census, names]).slice!(2..)
|
|
1601
|
+
names
|
|
1602
|
+
end
|
|
1603
|
+
private :method_alias_names
|
|
1604
|
+
|
|
1605
|
+
MAIN_SINGLETON_ROWS = -1
|
|
1606
|
+
TOPLEVEL_ROWS_KEY = RunScopedSlots.register(:__rigor_toplevel_definition_rows)
|
|
1607
|
+
METHOD_ALIASES_KEY = RunScopedSlots.register(:__rigor_method_alias_names)
|
|
1608
|
+
DEFERRED_NAMES_KEY = RunScopedSlots.register(:__rigor_deferred_definition_names)
|
|
1609
|
+
private_constant :MAIN_SINGLETON_ROWS, :TOPLEVEL_ROWS_KEY, :METHOD_ALIASES_KEY, :DEFERRED_NAMES_KEY
|
|
1610
|
+
|
|
1320
1611
|
# Issue #316 — the CONFIDENCE-GATED companion of {#top_level_def_for}, and the only accessor the type
|
|
1321
1612
|
# inference may bind through. {#top_level_def_for} stays unrestricted because it also serves the
|
|
1322
1613
|
# *suppression* side (`call.unresolved-toplevel`, `call.undefined-method`): a name the project defines at
|
|
@@ -1563,7 +1854,8 @@ module Rigor
|
|
|
1563
1854
|
#
|
|
1564
1855
|
# This is EVIDENCE FOR a singleton ancestor, never against one: the walk sees only what a constant
|
|
1565
1856
|
# argument to a receiverless `extend` (or, since #915, a receiverless `include` / `prepend` inside a
|
|
1566
|
-
# `class << self` body) written inside a declaration body spells,
|
|
1857
|
+
# `class << self` body) written inside a declaration body spells, or, since #2094, `Widget.extend(M)` and
|
|
1858
|
+
# `Widget.singleton_class.include(M)` with a constant receiver, so a runtime `Widget.extend(m)` and an
|
|
1567
1859
|
# `extend` in a file outside the analysed set are both absent from it, as is an `extend` declared only
|
|
1568
1860
|
# in RBS — that one reaches the same consumer through `Environment#singleton_extended_modules` instead.
|
|
1569
1861
|
# Its one consumer ({Inference::Narrowing.narrow_class}) therefore reads it
|
|
@@ -1941,8 +2233,9 @@ module Rigor
|
|
|
1941
2233
|
# declaration-level fact of the whole file, which is also the granularity
|
|
1942
2234
|
# `ScopeIndexer#append_constant_signature` moves the declaration signature at.
|
|
1943
2235
|
#
|
|
1944
|
-
# `constant_sources` is seeded
|
|
1945
|
-
# is empty and this is one Hash miss
|
|
2236
|
+
# `constant_sources` is seeded when dependency recording is on and in the editor publish's discovery seed
|
|
2237
|
+
# (ADR-124), where the recorder is inactive; elsewhere the table is empty and this is one Hash miss. The caller
|
|
2238
|
+
# gates on the recorder being active.
|
|
1946
2239
|
def record_constant_dependency(name)
|
|
1947
2240
|
sites = @discovery.constant_sources[name]
|
|
1948
2241
|
return if sites.nil?
|
|
@@ -1968,25 +2261,33 @@ module Rigor
|
|
|
1968
2261
|
EMPTY_PARAMETER_ENVELOPES = {}.freeze
|
|
1969
2262
|
private_constant :EMPTY_PARAMETER_ENVELOPES
|
|
1970
2263
|
|
|
1971
|
-
# v0.1.2 — per-class table mapping `method_name
|
|
2264
|
+
# v0.1.2 — per-class table mapping `method_name → :public | :private | :protected`. Populated by
|
|
1972
2265
|
# `ScopeIndexer` for every `def` and `attr_*` member it sees inside a class body, with the visibility taken from
|
|
1973
2266
|
# a wrapping `private def` / `private attr_reader` call, else the surrounding `private` / `protected` / `public`
|
|
1974
2267
|
# modifier state, plus any post-hoc `private :name, ...` named-argument calls. Consumed by the
|
|
1975
2268
|
# `def.method-visibility-mismatch` rule so explicit-non-self calls to a private method surface a diagnostic.
|
|
1976
|
-
|
|
2269
|
+
# `singleton: true` reads the class's singleton slot for the name (`[:singleton, name]`).
|
|
2270
|
+
#
|
|
2271
|
+
# Issue #2134 — every answer is an ADR-46 read of the slot, so it records `visibility:<class>#<method_name>`
|
|
2272
|
+
# whichever way it answers ({Analysis::DependencyRecorder.read_visibility}). A `private :foo` can be written from
|
|
2273
|
+
# a reopening or a `class_eval` block in a file the reader has no other edge to, and adding or removing it must
|
|
2274
|
+
# re-check the reader.
|
|
2275
|
+
def discovered_method_visibility(class_name, method_name, singleton: false)
|
|
2276
|
+
Analysis::DependencyRecorder.read_visibility(class_name, method_name) if Analysis::DependencyRecorder.active?
|
|
1977
2277
|
table = @discovery.discovered_method_visibilities[class_name.to_s]
|
|
1978
2278
|
return nil unless table
|
|
1979
2279
|
|
|
1980
|
-
table[method_name.to_sym]
|
|
2280
|
+
table[singleton ? [:singleton, method_name.to_sym] : method_name.to_sym]
|
|
1981
2281
|
end
|
|
1982
2282
|
|
|
1983
|
-
# Closes the "`params[:f] ||= []; params[:f] << x`" precision gap
|
|
1984
|
-
# indexed-collection narrowing through `Hash[k] ||= default`). After `receiver[key] ||= default`, the next
|
|
2283
|
+
# Closes the "`params[:f] ||= []; params[:f] << x`" precision gap. After `receiver[key] ||= default`, the next
|
|
1985
2284
|
# read at `receiver[key]` is known non-nil; recording the post-`||=` type keyed on
|
|
1986
2285
|
# `(receiver_kind, receiver_name, literal_key)` lets the ExpressionTyper's `[]` dispatch hand back the
|
|
1987
2286
|
# narrowed type. Receiver-rebind and `[]=`/mutator invalidation rules are documented at the call sites in
|
|
1988
2287
|
# `Inference::StatementEvaluator`.
|
|
1989
2288
|
def indexed_narrowing(receiver_kind, receiver_name, key)
|
|
2289
|
+
return nil if @indexed_narrowings.empty?
|
|
2290
|
+
|
|
1990
2291
|
@indexed_narrowings[indexed_key(receiver_kind, receiver_name, key)]
|
|
1991
2292
|
end
|
|
1992
2293
|
|
|
@@ -1998,6 +2299,8 @@ module Rigor
|
|
|
1998
2299
|
end
|
|
1999
2300
|
|
|
2000
2301
|
def without_indexed_narrowing(receiver_kind, receiver_name, key)
|
|
2302
|
+
return self if @indexed_narrowings.empty?
|
|
2303
|
+
|
|
2001
2304
|
lookup = indexed_key(receiver_kind, receiver_name, key)
|
|
2002
2305
|
return self unless @indexed_narrowings.key?(lookup)
|
|
2003
2306
|
|
|
@@ -2012,14 +2315,15 @@ module Rigor
|
|
|
2012
2315
|
rebuild(indexed_narrowings: new_table)
|
|
2013
2316
|
end
|
|
2014
2317
|
|
|
2015
|
-
# Closes the "stable receiver method-chain narrowing" gap (
|
|
2016
|
-
# "Method-call receiver narrowing across stable receivers"; 2026-05-28 Redmine survey). After
|
|
2318
|
+
# Closes the "stable receiver method-chain narrowing" gap (2026-05-28 Redmine survey). After
|
|
2017
2319
|
# `if x.last.is_a?(Array)` the dominated body's `x.last` reads MUST observe the truthy-narrowed type; the same
|
|
2018
2320
|
# chain reaching the falsey edge observes the negative narrowing.
|
|
2019
2321
|
#
|
|
2020
2322
|
# Address shape mirrors {.indexed_narrowing}: stable root variable + no-arg single-hop method name. See
|
|
2021
2323
|
# {ChainKey} for the precise contract.
|
|
2022
2324
|
def method_chain_narrowing(receiver_kind, receiver_name, method_name)
|
|
2325
|
+
return nil if @method_chain_narrowings.empty?
|
|
2326
|
+
|
|
2023
2327
|
@method_chain_narrowings[chain_key(receiver_kind, receiver_name, method_name)]
|
|
2024
2328
|
end
|
|
2025
2329
|
|
|
@@ -2031,6 +2335,8 @@ module Rigor
|
|
|
2031
2335
|
end
|
|
2032
2336
|
|
|
2033
2337
|
def without_method_chain_narrowing(receiver_kind, receiver_name, method_name)
|
|
2338
|
+
return self if @method_chain_narrowings.empty?
|
|
2339
|
+
|
|
2034
2340
|
lookup = chain_key(receiver_kind, receiver_name, method_name)
|
|
2035
2341
|
return self unless @method_chain_narrowings.key?(lookup)
|
|
2036
2342
|
|
|
@@ -2140,11 +2446,13 @@ module Rigor
|
|
|
2140
2446
|
|
|
2141
2447
|
private
|
|
2142
2448
|
|
|
2143
|
-
# The marks {#==} compares: ADR-58's, issue #667's
|
|
2449
|
+
# The marks {#==} compares: ADR-58's, issue #667's, the repeated `||=` sites, #2555's construction facts (they
|
|
2450
|
+
# change what a mutation seam answers), and #1429's guard state.
|
|
2144
2451
|
def same_marks?(other)
|
|
2145
2452
|
@declaration_sourced == other.declaration_sourced &&
|
|
2146
2453
|
@published_constant_sourced == other.published_constant_sourced &&
|
|
2147
2454
|
@repeated_or_writes == other.repeated_or_writes &&
|
|
2455
|
+
@construction_facts == other.construction_facts &&
|
|
2148
2456
|
same_guard_state?(other)
|
|
2149
2457
|
end
|
|
2150
2458
|
|
|
@@ -2153,7 +2461,15 @@ module Rigor
|
|
|
2153
2461
|
@bot_guard_classes == other.bot_guard_classes
|
|
2154
2462
|
end
|
|
2155
2463
|
|
|
2464
|
+
# `construction_facts` is the one positional parameter. CRuby records which keywords a call left out in a bit
|
|
2465
|
+
# field indexed by each keyword's position among ALL the method's keywords, required and optional
|
|
2466
|
+
# (`VM_KW_SPECIFIED_BITS_MAX`, 31); a keyword past that index whose default is not a literal makes every call that
|
|
2467
|
+
# omits it allocate the record instead. A 32nd keyword here did that on every scope transition, about 1% of an
|
|
2468
|
+
# analysis's allocations (issue #2555). `#initialize` is already past the index: `bot_guard_classes` and
|
|
2469
|
+
# `declared_refinements` take the allocating path whenever a caller omits them. The next Scope field crosses it in
|
|
2470
|
+
# this method again, so it needs a positional slot or a restructuring, not a 32nd keyword.
|
|
2156
2471
|
def rebuild(
|
|
2472
|
+
construction_facts = @construction_facts,
|
|
2157
2473
|
locals: @locals, fact_store: @fact_store, self_type: @self_type,
|
|
2158
2474
|
ivars: @ivars, cvars: @cvars, globals: @globals,
|
|
2159
2475
|
discovery: @discovery,
|
|
@@ -2204,7 +2520,7 @@ module Rigor
|
|
|
2204
2520
|
plugin_typed_calls: plugin_typed_calls,
|
|
2205
2521
|
optimistic_origins: optimistic_origins,
|
|
2206
2522
|
optimistic_locals: optimistic_locals,
|
|
2207
|
-
optimistic_ivars: optimistic_ivars,
|
|
2523
|
+
optimistic_ivars: optimistic_ivars, construction_facts: construction_facts,
|
|
2208
2524
|
repeated_or_writes: repeated_or_writes,
|
|
2209
2525
|
match_frame: match_frame,
|
|
2210
2526
|
constant_narrowings: constant_narrowings,
|
|
@@ -2214,11 +2530,18 @@ module Rigor
|
|
|
2214
2530
|
)
|
|
2215
2531
|
end
|
|
2216
2532
|
|
|
2533
|
+
# A join with an empty side has no key in common (usually both are empty, the common case at a branch merge), so
|
|
2534
|
+
# its answer is this one frozen hash rather than a fresh one.
|
|
2535
|
+
EMPTY_BINDINGS = {}.freeze
|
|
2536
|
+
private_constant :EMPTY_BINDINGS
|
|
2537
|
+
|
|
2217
2538
|
def join_bindings(left, right)
|
|
2218
2539
|
# Keys present in both, unioned. Iterating `left` and probing `right.key?` yields the same keys in the same
|
|
2219
2540
|
# order as the prior `(left.keys & right.keys)` while avoiding the two key arrays and the intersection
|
|
2220
2541
|
# array — this is the control-flow join, run at every branch merge, and was a top allocation site (~75% of
|
|
2221
2542
|
# `Hash#keys`).
|
|
2543
|
+
return EMPTY_BINDINGS if left.empty? || right.empty?
|
|
2544
|
+
|
|
2222
2545
|
result = {}
|
|
2223
2546
|
left.each do |name, ltype|
|
|
2224
2547
|
next unless right.key?(name)
|
|
@@ -2297,6 +2620,8 @@ module Rigor
|
|
|
2297
2620
|
optimistic_origins: @optimistic_origins,
|
|
2298
2621
|
optimistic_locals: join_optimistic_marks(@optimistic_locals, other.optimistic_locals),
|
|
2299
2622
|
optimistic_ivars: join_optimistic_marks(@optimistic_ivars, other.optimistic_ivars),
|
|
2623
|
+
# Issue #2555 L3 — INTERSECTION: a construction fact holds after a merge only when both arms established it.
|
|
2624
|
+
construction_facts: join_construction_facts(@construction_facts, other.construction_facts),
|
|
2300
2625
|
# UNION, the published-constant mark's direction: the mark only withholds the memoizing `||=`
|
|
2301
2626
|
# reading, so keeping a site either arm holds is the wider answer.
|
|
2302
2627
|
repeated_or_writes: join_repeated_or_writes(other),
|
|
@@ -2394,6 +2719,31 @@ module Rigor
|
|
|
2394
2719
|
.freeze
|
|
2395
2720
|
end
|
|
2396
2721
|
|
|
2722
|
+
# Issue #2555 L3 — a fact survives a merge only for a name both arms hold it for, and only the facts both agree
|
|
2723
|
+
# on. Zero-alloc when either side is empty or both are the same table.
|
|
2724
|
+
def join_construction_facts(mine, theirs)
|
|
2725
|
+
return mine if mine.equal?(theirs)
|
|
2726
|
+
return EMPTY_ORIGINS if mine.empty? || theirs.empty?
|
|
2727
|
+
|
|
2728
|
+
joined = {}
|
|
2729
|
+
mine.each do |name, facts|
|
|
2730
|
+
their_facts = theirs[name]
|
|
2731
|
+
next if their_facts.nil?
|
|
2732
|
+
|
|
2733
|
+
common = facts == their_facts ? facts : join_fact_sets(facts, their_facts)
|
|
2734
|
+
joined[name] = common unless common.empty?
|
|
2735
|
+
end
|
|
2736
|
+
joined.empty? ? EMPTY_ORIGINS : joined.freeze
|
|
2737
|
+
end
|
|
2738
|
+
|
|
2739
|
+
# `:grown` joins by UNION among the names both arms keep: a store on either arm may reach the carrier, and the
|
|
2740
|
+
# fact only ever floors it ({Inference::MutationRejoin.floors_grown_carrier?}). Every other fact intersects.
|
|
2741
|
+
def join_fact_sets(facts, their_facts)
|
|
2742
|
+
common = facts & their_facts
|
|
2743
|
+
common << :grown if facts.include?(:grown) || their_facts.include?(:grown)
|
|
2744
|
+
common.freeze
|
|
2745
|
+
end
|
|
2746
|
+
|
|
2397
2747
|
# ADR-82 WD1 — rebinding drops any propagated origin for the name (the new value's provenance is set
|
|
2398
2748
|
# afterward by `with_local_origin` when it is a `Dynamic` with a recorded cause). Zero-alloc when the name
|
|
2399
2749
|
# carries no origin — the overwhelming common case, so this stays off the with_local hot-path budget.
|
|
@@ -2455,19 +2805,12 @@ module Rigor
|
|
|
2455
2805
|
end
|
|
2456
2806
|
|
|
2457
2807
|
def indexed_key(receiver_kind, receiver_name, key)
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
receiver_name: receiver_name.to_sym,
|
|
2461
|
-
key: key
|
|
2462
|
-
)
|
|
2808
|
+
# Positional `new` (field-definition order) skips the keyword hash a keyword `new` allocates.
|
|
2809
|
+
IndexedKey.new(receiver_kind.to_sym, receiver_name.to_sym, key)
|
|
2463
2810
|
end
|
|
2464
2811
|
|
|
2465
2812
|
def chain_key(receiver_kind, receiver_name, method_name)
|
|
2466
|
-
ChainKey.new(
|
|
2467
|
-
receiver_kind: receiver_kind.to_sym,
|
|
2468
|
-
receiver_name: receiver_name.to_sym,
|
|
2469
|
-
method_name: method_name.to_sym
|
|
2470
|
-
)
|
|
2813
|
+
ChainKey.new(receiver_kind.to_sym, receiver_name.to_sym, method_name.to_sym)
|
|
2471
2814
|
end
|
|
2472
2815
|
|
|
2473
2816
|
def drop_indexed_narrowings_for(receiver_kind, receiver_name)
|