rigortype 0.4.6 → 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 +50 -3
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/manual/02-cli-reference.md +152 -26
- data/docs/manual/03-configuration.md +36 -3
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/09-editor-integration.md +13 -2
- data/docs/manual/10-mcp-server.md +38 -2
- data/docs/manual/12-caching.md +4 -3
- data/docs/manual/plugins/rigor-actionpack.md +19 -11
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
- data/lib/rigor/analysis/baseline.rb +24 -27
- 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 +85 -13
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -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/rebinding_block_callees.rb +3 -3
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- 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 +2 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +124 -44
- data/lib/rigor/analysis/crash_signature.rb +2 -2
- data/lib/rigor/analysis/dependency_recorder.rb +4 -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 +93 -6
- data/lib/rigor/analysis/fact_store.rb +3 -3
- 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 +27 -16
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
- data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
- 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 +77 -71
- 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 +24 -31
- 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 +31 -12
- 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 +108 -102
- 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 +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 +12 -8
- 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 +84 -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 +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 +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 +209 -54
- data/lib/rigor/config_audit.rb +1 -1
- data/lib/rigor/configuration/severity_profile.rb +5 -5
- data/lib/rigor/configuration.rb +72 -30
- 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 +480 -67
- 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 +41 -10
- 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 +88 -77
- 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 +39 -7
- 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 +46 -4
- data/lib/rigor/inference/definer_resolution.rb +4 -2
- 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/expression_typer.rb +116 -49
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/global_write_census.rb +76 -2
- 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 +51 -0
- data/lib/rigor/inference/key_presence_guard.rb +2 -1
- data/lib/rigor/inference/loop_value_record.rb +8 -4
- data/lib/rigor/inference/match_rebinding/frame.rb +46 -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 +3 -3
- 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 +1 -1
- 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 +9 -7
- 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 +64 -14
- data/lib/rigor/inference/module_function_state.rb +11 -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 +157 -27
- data/lib/rigor/inference/narrowing.rb +47 -33
- 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_yielder.rb +11 -17
- 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 +0 -1
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
- 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.rb +137 -46
- data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
- 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 +2328 -138
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
- 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 +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/plugins.rb +1 -1
- data/lib/rigor/mcp/server.rb +73 -2
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- 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 +9 -13
- data/lib/rigor/plugin/registry.rb +3 -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 +44 -5
- 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 +15 -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 +19 -6
- data/lib/rigor/scope/resolution_chain.rb +3 -1
- data/lib/rigor/scope.rb +365 -31
- 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 +371 -58
- 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 +3 -5
- 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/renderer.rb +15 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -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 +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 +9 -13
- data/lib/rigor/type/constant.rb +4 -5
- data/lib/rigor/type/hash_shape.rb +21 -40
- 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 +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 +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -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-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 +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 +1 -1
- 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 +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/sig/rigor/analysis/baseline.rbs +0 -3
- 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/registry.rbs +2 -2
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +1 -0
- 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/io_boundary.rbs +2 -1
- data/sig/rigor/plugin/manifest.rbs +9 -9
- data/sig/rigor/plugin/protocol_contract.rbs +3 -7
- data/sig/rigor/scope.rbs +23 -3
- 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 +36 -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
|
|
@@ -1970,7 +2261,7 @@ module Rigor
|
|
|
1970
2261
|
EMPTY_PARAMETER_ENVELOPES = {}.freeze
|
|
1971
2262
|
private_constant :EMPTY_PARAMETER_ENVELOPES
|
|
1972
2263
|
|
|
1973
|
-
# v0.1.2 — per-class table mapping `method_name
|
|
2264
|
+
# v0.1.2 — per-class table mapping `method_name → :public | :private | :protected`. Populated by
|
|
1974
2265
|
# `ScopeIndexer` for every `def` and `attr_*` member it sees inside a class body, with the visibility taken from
|
|
1975
2266
|
# a wrapping `private def` / `private attr_reader` call, else the surrounding `private` / `protected` / `public`
|
|
1976
2267
|
# modifier state, plus any post-hoc `private :name, ...` named-argument calls. Consumed by the
|
|
@@ -1989,13 +2280,14 @@ module Rigor
|
|
|
1989
2280
|
table[singleton ? [:singleton, method_name.to_sym] : method_name.to_sym]
|
|
1990
2281
|
end
|
|
1991
2282
|
|
|
1992
|
-
# Closes the "`params[:f] ||= []; params[:f] << x`" precision gap
|
|
1993
|
-
# 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
|
|
1994
2284
|
# read at `receiver[key]` is known non-nil; recording the post-`||=` type keyed on
|
|
1995
2285
|
# `(receiver_kind, receiver_name, literal_key)` lets the ExpressionTyper's `[]` dispatch hand back the
|
|
1996
2286
|
# narrowed type. Receiver-rebind and `[]=`/mutator invalidation rules are documented at the call sites in
|
|
1997
2287
|
# `Inference::StatementEvaluator`.
|
|
1998
2288
|
def indexed_narrowing(receiver_kind, receiver_name, key)
|
|
2289
|
+
return nil if @indexed_narrowings.empty?
|
|
2290
|
+
|
|
1999
2291
|
@indexed_narrowings[indexed_key(receiver_kind, receiver_name, key)]
|
|
2000
2292
|
end
|
|
2001
2293
|
|
|
@@ -2007,6 +2299,8 @@ module Rigor
|
|
|
2007
2299
|
end
|
|
2008
2300
|
|
|
2009
2301
|
def without_indexed_narrowing(receiver_kind, receiver_name, key)
|
|
2302
|
+
return self if @indexed_narrowings.empty?
|
|
2303
|
+
|
|
2010
2304
|
lookup = indexed_key(receiver_kind, receiver_name, key)
|
|
2011
2305
|
return self unless @indexed_narrowings.key?(lookup)
|
|
2012
2306
|
|
|
@@ -2021,14 +2315,15 @@ module Rigor
|
|
|
2021
2315
|
rebuild(indexed_narrowings: new_table)
|
|
2022
2316
|
end
|
|
2023
2317
|
|
|
2024
|
-
# Closes the "stable receiver method-chain narrowing" gap (
|
|
2025
|
-
# "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
|
|
2026
2319
|
# `if x.last.is_a?(Array)` the dominated body's `x.last` reads MUST observe the truthy-narrowed type; the same
|
|
2027
2320
|
# chain reaching the falsey edge observes the negative narrowing.
|
|
2028
2321
|
#
|
|
2029
2322
|
# Address shape mirrors {.indexed_narrowing}: stable root variable + no-arg single-hop method name. See
|
|
2030
2323
|
# {ChainKey} for the precise contract.
|
|
2031
2324
|
def method_chain_narrowing(receiver_kind, receiver_name, method_name)
|
|
2325
|
+
return nil if @method_chain_narrowings.empty?
|
|
2326
|
+
|
|
2032
2327
|
@method_chain_narrowings[chain_key(receiver_kind, receiver_name, method_name)]
|
|
2033
2328
|
end
|
|
2034
2329
|
|
|
@@ -2040,6 +2335,8 @@ module Rigor
|
|
|
2040
2335
|
end
|
|
2041
2336
|
|
|
2042
2337
|
def without_method_chain_narrowing(receiver_kind, receiver_name, method_name)
|
|
2338
|
+
return self if @method_chain_narrowings.empty?
|
|
2339
|
+
|
|
2043
2340
|
lookup = chain_key(receiver_kind, receiver_name, method_name)
|
|
2044
2341
|
return self unless @method_chain_narrowings.key?(lookup)
|
|
2045
2342
|
|
|
@@ -2149,11 +2446,13 @@ module Rigor
|
|
|
2149
2446
|
|
|
2150
2447
|
private
|
|
2151
2448
|
|
|
2152
|
-
# 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.
|
|
2153
2451
|
def same_marks?(other)
|
|
2154
2452
|
@declaration_sourced == other.declaration_sourced &&
|
|
2155
2453
|
@published_constant_sourced == other.published_constant_sourced &&
|
|
2156
2454
|
@repeated_or_writes == other.repeated_or_writes &&
|
|
2455
|
+
@construction_facts == other.construction_facts &&
|
|
2157
2456
|
same_guard_state?(other)
|
|
2158
2457
|
end
|
|
2159
2458
|
|
|
@@ -2162,7 +2461,15 @@ module Rigor
|
|
|
2162
2461
|
@bot_guard_classes == other.bot_guard_classes
|
|
2163
2462
|
end
|
|
2164
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.
|
|
2165
2471
|
def rebuild(
|
|
2472
|
+
construction_facts = @construction_facts,
|
|
2166
2473
|
locals: @locals, fact_store: @fact_store, self_type: @self_type,
|
|
2167
2474
|
ivars: @ivars, cvars: @cvars, globals: @globals,
|
|
2168
2475
|
discovery: @discovery,
|
|
@@ -2213,7 +2520,7 @@ module Rigor
|
|
|
2213
2520
|
plugin_typed_calls: plugin_typed_calls,
|
|
2214
2521
|
optimistic_origins: optimistic_origins,
|
|
2215
2522
|
optimistic_locals: optimistic_locals,
|
|
2216
|
-
optimistic_ivars: optimistic_ivars,
|
|
2523
|
+
optimistic_ivars: optimistic_ivars, construction_facts: construction_facts,
|
|
2217
2524
|
repeated_or_writes: repeated_or_writes,
|
|
2218
2525
|
match_frame: match_frame,
|
|
2219
2526
|
constant_narrowings: constant_narrowings,
|
|
@@ -2223,11 +2530,18 @@ module Rigor
|
|
|
2223
2530
|
)
|
|
2224
2531
|
end
|
|
2225
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
|
+
|
|
2226
2538
|
def join_bindings(left, right)
|
|
2227
2539
|
# Keys present in both, unioned. Iterating `left` and probing `right.key?` yields the same keys in the same
|
|
2228
2540
|
# order as the prior `(left.keys & right.keys)` while avoiding the two key arrays and the intersection
|
|
2229
2541
|
# array — this is the control-flow join, run at every branch merge, and was a top allocation site (~75% of
|
|
2230
2542
|
# `Hash#keys`).
|
|
2543
|
+
return EMPTY_BINDINGS if left.empty? || right.empty?
|
|
2544
|
+
|
|
2231
2545
|
result = {}
|
|
2232
2546
|
left.each do |name, ltype|
|
|
2233
2547
|
next unless right.key?(name)
|
|
@@ -2306,6 +2620,8 @@ module Rigor
|
|
|
2306
2620
|
optimistic_origins: @optimistic_origins,
|
|
2307
2621
|
optimistic_locals: join_optimistic_marks(@optimistic_locals, other.optimistic_locals),
|
|
2308
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),
|
|
2309
2625
|
# UNION, the published-constant mark's direction: the mark only withholds the memoizing `||=`
|
|
2310
2626
|
# reading, so keeping a site either arm holds is the wider answer.
|
|
2311
2627
|
repeated_or_writes: join_repeated_or_writes(other),
|
|
@@ -2403,6 +2719,31 @@ module Rigor
|
|
|
2403
2719
|
.freeze
|
|
2404
2720
|
end
|
|
2405
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
|
+
|
|
2406
2747
|
# ADR-82 WD1 — rebinding drops any propagated origin for the name (the new value's provenance is set
|
|
2407
2748
|
# afterward by `with_local_origin` when it is a `Dynamic` with a recorded cause). Zero-alloc when the name
|
|
2408
2749
|
# carries no origin — the overwhelming common case, so this stays off the with_local hot-path budget.
|
|
@@ -2464,19 +2805,12 @@ module Rigor
|
|
|
2464
2805
|
end
|
|
2465
2806
|
|
|
2466
2807
|
def indexed_key(receiver_kind, receiver_name, key)
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
receiver_name: receiver_name.to_sym,
|
|
2470
|
-
key: key
|
|
2471
|
-
)
|
|
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)
|
|
2472
2810
|
end
|
|
2473
2811
|
|
|
2474
2812
|
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
|
-
)
|
|
2813
|
+
ChainKey.new(receiver_kind.to_sym, receiver_name.to_sym, method_name.to_sym)
|
|
2480
2814
|
end
|
|
2481
2815
|
|
|
2482
2816
|
def drop_indexed_narrowings_for(receiver_kind, receiver_name)
|
|
@@ -55,6 +55,10 @@ module Rigor
|
|
|
55
55
|
# generic T`) and `sig/` does not declare it yet. sig-gen writes no class type parameters, and a header
|
|
56
56
|
# without them fails the class's definition build.
|
|
57
57
|
inline_generic_class: "sig.skipped.inline-generic-class",
|
|
58
|
+
# #2436 — the member's class, or one it is nested in, is declared generic by loaded RBS with a parameter
|
|
59
|
+
# that has a bound or a default, and `sig/` does not declare it. A header copied from that declaration
|
|
60
|
+
# could resolve the bound differently, and one without parameters fails the class's definition build.
|
|
61
|
+
generic_reopening: "sig.skipped.generic-reopening",
|
|
58
62
|
# ADR-112 WD4 — the member is declared inline and in `sig/`, and the two disagree. A refusal, not a
|
|
59
63
|
# skip: `--write` and `--check` exit 1 until a person reconciles them or passes `--overwrite`.
|
|
60
64
|
inline_differs: "sig.skipped.inline-differs",
|
|
@@ -82,8 +82,8 @@ module Rigor
|
|
|
82
82
|
#
|
|
83
83
|
# @param declared — whether the method already carries an authored bound of its own
|
|
84
84
|
# ({Effects::EnvelopeIndex}); see gate 0.
|
|
85
|
-
# @return
|
|
86
|
-
#
|
|
85
|
+
# @return the annotation lines to render, then the reason to report. An empty list with a `nil` reason
|
|
86
|
+
# is "nothing to say about this method".
|
|
87
87
|
def decide(entry, envelopes: false, declared: false)
|
|
88
88
|
return [[], :withheld_declared] if declared
|
|
89
89
|
return [[], nil] if entry.nil?
|
|
@@ -133,7 +133,7 @@ module Rigor
|
|
|
133
133
|
|
|
134
134
|
# @param path — the `.rb` file the def came from, for a `match:`-selected
|
|
135
135
|
# `effects.envelopes:` entry.
|
|
136
|
-
# @return
|
|
136
|
+
# @return the annotation lines and the reason, as {EffectAnnotation.decide} gives them.
|
|
137
137
|
def annotate(class_name:, method_name:, kind:, path: nil)
|
|
138
138
|
key = EffectAnnotation.key_for(class_name, method_name, kind)
|
|
139
139
|
return [[], nil] if key.nil?
|