rigortype 0.3.8 → 0.4.0
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/capability_roles/capability_roles.rbs +36 -0
- data/data/core_overlay/enumerable.rbs +51 -0
- data/data/core_overlay/enumerator.rbs +84 -0
- data/data/core_overlay/hash_rbs3.rbs +41 -0
- data/data/core_overlay/process.rbs +40 -0
- data/data/core_overlay/string_io.rbs +33 -0
- data/data/effects/core.yml +3 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
- data/docs/handbook/02-everyday-types.md +17 -14
- data/docs/handbook/03-narrowing.md +132 -14
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/05-methods-and-blocks.md +1 -1
- data/docs/handbook/06-classes.md +2 -2
- data/docs/handbook/07-rbs-and-extended.md +19 -1
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +525 -13
- data/docs/handbook/README.md +1 -1
- data/docs/handbook/appendix-elixir.md +2 -2
- data/docs/handbook/appendix-go.md +2 -2
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +2 -2
- data/docs/handbook/appendix-phpstan.md +1 -1
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +4 -4
- data/docs/handbook/appendix-typescript.md +1 -1
- data/docs/llms.txt +2 -0
- data/docs/manual/02-cli-reference.md +95 -6
- data/docs/manual/03-configuration.md +18 -0
- data/docs/manual/04-diagnostics.md +15 -0
- data/docs/manual/07-plugins.md +15 -5
- data/docs/manual/08-skills.md +23 -2
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/11-ci.md +9 -0
- data/docs/manual/15-type-protection-coverage.md +8 -0
- data/docs/manual/16-rbs-extended-annotations.md +46 -8
- data/docs/manual/18-removing-dead-code.md +10 -8
- data/docs/manual/19-effect-labels.md +10 -0
- data/docs/manual/plugins/README.md +7 -0
- data/docs/manual/plugins/rigor-actioncable.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +7 -0
- data/docs/manual/plugins/rigor-actionpack.md +261 -0
- data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
- data/docs/manual/plugins/rigor-activejob.md +7 -0
- data/docs/manual/plugins/rigor-activerecord.md +138 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
- data/docs/manual/plugins/rigor-grape.md +106 -0
- data/docs/manual/plugins/rigor-graphql.md +23 -2
- data/docs/manual/plugins/rigor-pundit.md +8 -1
- data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
- data/docs/manual/plugins/rigor-rails-routes.md +7 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
- data/docs/manual/plugins/rigor-sidekiq.md +8 -1
- data/docs/manual/plugins/rigor-sorbet.md +21 -5
- data/exe/rigor +19 -4
- data/lib/rigor/analysis/baseline.rb +3 -3
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
- data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
- data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
- data/lib/rigor/analysis/check_rules.rb +718 -79
- data/lib/rigor/analysis/crash_signature.rb +8 -24
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
- data/lib/rigor/analysis/erb_template_detector.rb +1 -2
- data/lib/rigor/analysis/fact_store.rb +15 -1
- data/lib/rigor/analysis/incremental.rb +42 -0
- data/lib/rigor/analysis/incremental_session.rb +117 -26
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/graph.rb +3 -5
- data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
- data/lib/rigor/analysis/reachability/project_files.rb +2 -2
- data/lib/rigor/analysis/reachability/scan.rb +5 -5
- data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
- data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
- data/lib/rigor/analysis/result.rb +1 -3
- data/lib/rigor/analysis/rule_catalog.rb +131 -1
- data/lib/rigor/analysis/run_cache_key.rb +21 -12
- data/lib/rigor/analysis/run_cache_probe.rb +3 -5
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
- data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
- data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +407 -51
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +91 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +399 -0
- data/lib/rigor/analysis/worker_session.rb +62 -20
- data/lib/rigor/bleeding_edge.rb +10 -24
- data/lib/rigor/builtins/hkt_builtins.rb +3 -2
- data/lib/rigor/builtins/imported_refinements.rb +159 -21
- data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
- data/lib/rigor/builtins/regex_refinement.rb +21 -13
- data/lib/rigor/builtins/static_return_refinements.rb +7 -6
- data/lib/rigor/cache/annotation_location.rb +2 -4
- data/lib/rigor/cache/descriptor.rb +63 -20
- data/lib/rigor/cache/engine_source.rb +33 -5
- data/lib/rigor/cache/file_digest.rb +9 -1
- data/lib/rigor/cache/incremental_snapshot.rb +72 -6
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
- data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
- data/lib/rigor/cache/rbs_constant_table.rb +0 -3
- data/lib/rigor/cache/rbs_descriptor.rb +112 -14
- data/lib/rigor/cache/rbs_environment.rb +8 -4
- data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
- data/lib/rigor/cache/store.rb +76 -32
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/annotate_command.rb +5 -6
- data/lib/rigor/cli/check_command.rb +57 -10
- data/lib/rigor/cli/check_invocation.rb +6 -11
- data/lib/rigor/cli/check_runner_factory.rb +1 -5
- data/lib/rigor/cli/coverage_command.rb +1 -1
- data/lib/rigor/cli/coverage_mutation.rb +12 -3
- data/lib/rigor/cli/coverage_scan.rb +1 -5
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/doc_links.rb +4 -4
- data/lib/rigor/cli/docs_command.rb +6 -6
- data/lib/rigor/cli/doctor_command.rb +85 -38
- data/lib/rigor/cli/effects_command.rb +2 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +34 -1
- data/lib/rigor/cli/fused_protection_report.rb +7 -1
- data/lib/rigor/cli/lsp_command.rb +1 -1
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
- data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
- data/lib/rigor/cli/mutation_protection_report.rb +7 -1
- data/lib/rigor/cli/plugin_command.rb +4 -4
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/prism_colorizer.rb +1 -2
- data/lib/rigor/cli/protection_fork_scan.rb +6 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +212 -31
- data/lib/rigor/cli/skill_command.rb +2 -2
- data/lib/rigor/cli/skill_deep_probe.rb +4 -4
- data/lib/rigor/cli/skill_describe.rb +35 -35
- data/lib/rigor/cli/trace_command.rb +4 -4
- data/lib/rigor/cli/trace_renderer.rb +5 -6
- data/lib/rigor/cli/triage_command.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +29 -11
- data/lib/rigor/cli/type_of_renderer.rb +20 -9
- data/lib/rigor/cli/type_of_template_probe.rb +189 -0
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli/unused_command.rb +11 -3
- data/lib/rigor/cli/upgrade_command.rb +1 -1
- data/lib/rigor/cli.rb +89 -9
- data/lib/rigor/config_audit.rb +30 -5
- data/lib/rigor/configuration/severity_profile.rb +25 -9
- data/lib/rigor/configuration.rb +109 -7
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +12 -5
- data/lib/rigor/effects/callee_rule.rb +368 -0
- data/lib/rigor/effects/catalog.rb +7 -4
- data/lib/rigor/effects/collector.rb +11 -5
- data/lib/rigor/effects/config_envelopes.rb +15 -10
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/definition_lines.rb +25 -6
- data/lib/rigor/effects/effect_table.rb +11 -4
- data/lib/rigor/effects/entry_points.rb +0 -2
- data/lib/rigor/effects/envelope_check.rb +9 -9
- data/lib/rigor/effects/envelope_index.rb +20 -8
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +5 -7
- data/lib/rigor/effects/identity.rb +20 -6
- data/lib/rigor/effects/inline_anchor.rb +7 -7
- data/lib/rigor/effects/label_intent.rb +3 -4
- data/lib/rigor/effects/liskov_check.rb +8 -8
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +22 -1
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +47 -37
- data/lib/rigor/effects/propagator.rb +297 -19
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +122 -73
- data/lib/rigor/effects/signature_sources.rb +4 -6
- data/lib/rigor/effects/snapshot.rb +12 -11
- data/lib/rigor/effects/snapshot_diff.rb +1 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +393 -38
- data/lib/rigor/effects/unknown_label_check.rb +3 -8
- data/lib/rigor/effects/unknown_label_report.rb +3 -4
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
- data/lib/rigor/environment/class_registry.rb +3 -1
- data/lib/rigor/environment/failure_slot.rb +2 -2
- data/lib/rigor/environment/installed_gem_set.rb +85 -0
- data/lib/rigor/environment/lockfile_resolver.rb +68 -4
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
- data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
- data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
- data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
- data/lib/rigor/environment/rbs_loader.rb +674 -79
- data/lib/rigor/environment.rb +174 -24
- data/lib/rigor/flow_contribution/merger.rb +0 -2
- data/lib/rigor/flow_contribution.rb +11 -13
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +292 -17
- data/lib/rigor/inference/block_auto_splat.rb +216 -0
- data/lib/rigor/inference/block_call_timing.rb +338 -0
- data/lib/rigor/inference/block_parameter_binder.rb +75 -30
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +7 -6
- data/lib/rigor/inference/budget_trace.rb +3 -6
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +389 -18
- data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/coverage_scanner.rb +4 -6
- data/lib/rigor/inference/def_return_typer.rb +11 -7
- data/lib/rigor/inference/define_method_block_self.rb +64 -0
- data/lib/rigor/inference/dynamic_origin.rb +1 -1
- data/lib/rigor/inference/element_read_widening.rb +180 -0
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +2124 -468
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fork_map.rb +5 -7
- data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
- data/lib/rigor/inference/global_write_census.rb +239 -0
- data/lib/rigor/inference/guard_rebinding.rb +447 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/hkt_reducer.rb +2 -3
- data/lib/rigor/inference/hkt_registry.rb +4 -7
- data/lib/rigor/inference/index_write_widening.rb +16 -7
- data/lib/rigor/inference/indexed_narrowing.rb +61 -9
- data/lib/rigor/inference/jump_targets.rb +82 -0
- data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
- data/lib/rigor/inference/last_line.rb +340 -0
- data/lib/rigor/inference/last_status.rb +144 -0
- data/lib/rigor/inference/macro_block_self_type.rb +168 -21
- data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
- data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
- data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
- data/lib/rigor/inference/match_rebinding.rb +392 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
- data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +225 -25
- data/lib/rigor/inference/method_parameter_binder.rb +12 -9
- data/lib/rigor/inference/multi_target_binder.rb +341 -55
- data/lib/rigor/inference/mutation_rejoin.rb +171 -0
- data/lib/rigor/inference/mutation_widening.rb +156 -85
- data/lib/rigor/inference/narrowing.rb +835 -162
- data/lib/rigor/inference/operand_effects.rb +167 -0
- data/lib/rigor/inference/operand_walk.rb +88 -0
- data/lib/rigor/inference/optimistic_origin.rb +153 -16
- data/lib/rigor/inference/origin_lookup.rb +2 -3
- data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
- data/lib/rigor/inference/pre_eval_constants.rb +6 -5
- data/lib/rigor/inference/precision_scanner.rb +3 -4
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +9 -4
- data/lib/rigor/inference/project_patched_scanner.rb +10 -6
- data/lib/rigor/inference/protection_scanner.rb +1 -2
- data/lib/rigor/inference/range_constant.rb +57 -0
- data/lib/rigor/inference/rbs_type_translator.rb +49 -11
- data/lib/rigor/inference/receiver_alias.rb +93 -4
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +76 -0
- data/lib/rigor/inference/repeated_or_writes.rb +463 -0
- data/lib/rigor/inference/return_barrier.rb +54 -0
- data/lib/rigor/inference/rewrite_mutation.rb +120 -0
- data/lib/rigor/inference/scope_indexer.rb +5228 -627
- data/lib/rigor/inference/statement_evaluator.rb +3452 -595
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +97 -0
- data/lib/rigor/inference/struct_fold_safety.rb +6 -7
- data/lib/rigor/inference/synthetic_method_index.rb +1 -2
- data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/inference/version_guard.rb +28 -21
- data/lib/rigor/inference/void_origin.rb +3 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -4
- data/lib/rigor/language_server/buffer_table.rb +4 -4
- data/lib/rigor/language_server/completion_provider.rb +1 -1
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -1
- data/lib/rigor/language_server/hover_provider.rb +1 -1
- data/lib/rigor/language_server/hover_renderer.rb +4 -4
- data/lib/rigor/language_server/incremental_sync.rb +6 -6
- data/lib/rigor/language_server/project_context.rb +7 -8
- data/lib/rigor/language_server/publish_batcher.rb +2 -2
- data/lib/rigor/language_server/selection_range_provider.rb +2 -2
- data/lib/rigor/language_server/server.rb +9 -9
- data/lib/rigor/language_server/signature_help_provider.rb +1 -1
- data/lib/rigor/language_server/uri.rb +1 -1
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +258 -26
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +167 -0
- data/lib/rigor/plugin/effect_attribution.rb +65 -11
- data/lib/rigor/plugin/effect_entry_points.rb +3 -3
- data/lib/rigor/plugin/fact_store.rb +6 -6
- data/lib/rigor/plugin/io_boundary.rb +93 -8
- data/lib/rigor/plugin/isolation.rb +29 -6
- data/lib/rigor/plugin/loader.rb +33 -13
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +94 -11
- data/lib/rigor/plugin/registry.rb +76 -12
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -4
- data/lib/rigor/plugin/type_node_resolver.rb +3 -3
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/plugin_gap_advisory.rb +96 -0
- data/lib/rigor/project_environment.rb +138 -0
- data/lib/rigor/protection/analysis_guard.rb +93 -14
- data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
- data/lib/rigor/protection/dependency_closure.rb +5 -8
- data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
- data/lib/rigor/protection/discovery_seed.rb +14 -12
- data/lib/rigor/protection/kill_signature.rb +1 -4
- data/lib/rigor/protection/measurement_integrity.rb +1 -3
- data/lib/rigor/protection/mutation_cache.rb +7 -11
- data/lib/rigor/protection/mutation_scanner.rb +12 -15
- data/lib/rigor/protection/mutator.rb +2 -1
- data/lib/rigor/protection/test_suite_oracle.rb +5 -5
- data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/reporter.rb +36 -10
- data/lib/rigor/rbs_extended.rb +88 -21
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +139 -0
- data/lib/rigor/reflection.rb +93 -110
- data/lib/rigor/runtime/jit.rb +6 -8
- data/lib/rigor/scope/discovery_index.rb +165 -2
- data/lib/rigor/scope.rb +1041 -32
- data/lib/rigor/sig_gen/alias_index.rb +289 -0
- data/lib/rigor/sig_gen/classification.rb +22 -5
- data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
- data/lib/rigor/sig_gen/generator.rb +673 -53
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/layout_index.rb +3 -4
- data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +18 -16
- data/lib/rigor/sig_gen/path_mapper.rb +4 -6
- data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
- data/lib/rigor/sig_gen/renderer.rb +128 -14
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
- data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +189 -41
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +153 -6
- data/lib/rigor/source/literals.rb +0 -23
- data/lib/rigor/source/node_children.rb +0 -2
- data/lib/rigor/source/node_locator.rb +5 -11
- data/lib/rigor/source/node_walker.rb +0 -6
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +1 -3
- data/lib/rigor/triage.rb +3 -5
- data/lib/rigor/type/accepts_result.rb +24 -6
- data/lib/rigor/type/combinator.rb +144 -5
- data/lib/rigor/type/data_class.rb +2 -2
- data/lib/rigor/type/data_instance.rb +4 -4
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/float_range.rb +128 -0
- data/lib/rigor/type/hash_shape.rb +6 -6
- data/lib/rigor/type/integer_range.rb +13 -8
- data/lib/rigor/type/nominal.rb +8 -2
- data/lib/rigor/type/refined.rb +4 -3
- data/lib/rigor/type/struct_class.rb +3 -3
- data/lib/rigor/type/struct_instance.rb +4 -4
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +1 -1
- data/lib/rigor/type_node/range_literal.rb +25 -0
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
- data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
- data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
- data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
- data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
- data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
- data/sig/rigor/analysis/baseline.rbs +69 -7
- data/sig/rigor/analysis/fact_store.rbs +1 -1
- data/sig/rigor/analysis/project_scan.rbs +74 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
- data/sig/rigor/cache.rbs +2 -3
- data/sig/rigor/effects/config_envelopes.rbs +100 -0
- data/sig/rigor/effects/effect_table.rbs +60 -0
- data/sig/rigor/effects/envelope.rbs +97 -0
- data/sig/rigor/effects/envelope_index.rbs +33 -0
- data/sig/rigor/effects/file_collection.rbs +81 -0
- data/sig/rigor/effects/label.rbs +26 -0
- data/sig/rigor/effects/label_set.rbs +46 -0
- data/sig/rigor/effects/method_key.rbs +24 -0
- data/sig/rigor/effects/origin.rbs +50 -0
- data/sig/rigor/effects/plugin_facts.rbs +141 -0
- data/sig/rigor/effects/registry.rbs +68 -0
- data/sig/rigor/effects/summary.rbs +50 -0
- data/sig/rigor/effects/taint_cause.rbs +12 -0
- data/sig/rigor/environment.rbs +17 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +9 -14
- data/sig/rigor/plugin/additional_initializer.rbs +36 -0
- data/sig/rigor/plugin/base.rbs +30 -7
- data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
- data/sig/rigor/plugin/effect_attribution.rbs +66 -0
- data/sig/rigor/plugin/effect_edge.rbs +33 -0
- data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +51 -11
- data/sig/rigor/plugin/protocol_contract.rbs +68 -0
- data/sig/rigor/plugin/registry.rbs +62 -1
- data/sig/rigor/plugin.rbs +1 -1
- data/sig/rigor/rbs_extended.rbs +1 -1
- data/sig/rigor/reflection.rbs +9 -6
- data/sig/rigor/scope.rbs +148 -12
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +33 -1
- data/sig/rigor.rbs +39 -20
- data/skills/rigor-ask/SKILL.md +16 -11
- data/skills/rigor-baseline-reduce/SKILL.md +4 -6
- data/skills/rigor-ci-setup/SKILL.md +17 -21
- data/skills/rigor-doctor/SKILL.md +24 -22
- data/skills/rigor-doctor/references/01-checks.md +97 -33
- data/skills/rigor-editor-setup/SKILL.md +6 -4
- data/skills/rigor-mcp-setup/SKILL.md +5 -4
- data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
- data/skills/rigor-next-steps/SKILL.md +4 -2
- data/skills/rigor-plugin-author/SKILL.md +19 -23
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
- data/skills/rigor-plugin-review/SKILL.md +6 -4
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
- data/skills/rigor-plugin-tune/SKILL.md +4 -2
- data/skills/rigor-project-init/SKILL.md +15 -10
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +43 -14
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
- data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
- data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
- data/skills/rigor-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +226 -0
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
- data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +133 -3
- data/lib/rigor/cli/probe_environment.rb +0 -85
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
require_relative "../../reflection"
|
|
4
4
|
require_relative "../../type"
|
|
5
5
|
require_relative "../../rbs_extended"
|
|
6
|
+
require_relative "../range_constant"
|
|
6
7
|
require_relative "../rbs_type_translator"
|
|
8
|
+
require_relative "../external_ancestor_resolution"
|
|
7
9
|
require_relative "../void_origin"
|
|
8
10
|
require_relative "../optimistic_origin"
|
|
9
11
|
require_relative "overload_selector"
|
|
12
|
+
require_relative "self_substitute"
|
|
10
13
|
|
|
11
14
|
module Rigor
|
|
12
15
|
module Inference
|
|
@@ -41,14 +44,16 @@ module Rigor
|
|
|
41
44
|
#
|
|
42
45
|
# Remaining limitations:
|
|
43
46
|
#
|
|
44
|
-
# *
|
|
45
|
-
#
|
|
46
|
-
# are skipped (they cannot match the empty kwargs we send).
|
|
47
|
+
# * Keyword arguments reach `args` only as one trailing hash entry and are not matched against keyword
|
|
48
|
+
# parameters, so overloads with required keywords are skipped.
|
|
47
49
|
# * Method-level type parameters bind only from two positions: the block return type (Slice 6 phase C)
|
|
48
50
|
# and a positional parameter whose declared type is EXACTLY a type variable (issue #303 —
|
|
49
51
|
# `def foo[T]: (T) -> T` binds `T` from the first argument, and carries it into a generic return
|
|
50
52
|
# such as `-> Array[T]`). A variable reachable only through a container position (`Array[T] arg`),
|
|
51
53
|
# a rest positional (`*T`), or a keyword parameter is still unbound and degrades to `Dynamic[Top]`.
|
|
54
|
+
# A block-return variable that a parameter also names takes the value class the argument and the
|
|
55
|
+
# block share once the call passes an argument, or stays unbound when they share none (see
|
|
56
|
+
# {compose_block_type_vars}).
|
|
52
57
|
#
|
|
53
58
|
# See docs/adr/4-type-inference-engine.md for the broader plan.
|
|
54
59
|
# rubocop:disable-next Metrics/ModuleLength
|
|
@@ -64,6 +69,86 @@ module Rigor
|
|
|
64
69
|
# objects answer to methods their RBS omits (`ActionController::Base`, `Hash`, …).
|
|
65
70
|
ALLOWED_RBS_COMPLETE_ANCESTORS = ["Rigor::Plugin::Base"].freeze
|
|
66
71
|
|
|
72
|
+
# Issue #1094 — the methods through which a value can answer Ruby's implicit `to_ary` conversion.
|
|
73
|
+
# Multiple assignment and block auto-splat convert through `rb_check_array_type`, which calls
|
|
74
|
+
# `to_ary` when defined and otherwise asks `respond_to_missing?`, dispatching to `method_missing`
|
|
75
|
+
# when that answers true — so a `Delegator` destructures its target without defining `to_ary`.
|
|
76
|
+
ARRAY_CONVERSION_HOOKS = %i[to_ary method_missing respond_to_missing?].freeze
|
|
77
|
+
|
|
78
|
+
# The owners whose declarations of those hooks are the defaults rather than an override:
|
|
79
|
+
# `BasicObject#method_missing` raises and `Kernel#respond_to_missing?` answers false.
|
|
80
|
+
ARRAY_CONVERSION_DEFAULT_OWNERS = %w[::BasicObject ::Object ::Kernel].freeze
|
|
81
|
+
|
|
82
|
+
# Core value classes whose instances never convert — the answer with no RBS environment to hand.
|
|
83
|
+
ARRAY_CONVERSION_FREE_CORE_CLASSES = %w[
|
|
84
|
+
Integer Float Symbol String Hash Range Regexp Proc NilClass TrueClass FalseClass
|
|
85
|
+
].freeze
|
|
86
|
+
|
|
87
|
+
# The classes `Array` itself inherits from. A `Nominal[Object]` is routinely an array at runtime, so
|
|
88
|
+
# the RBS walk below, which reads the named class's own ancestry, cannot vouch for them.
|
|
89
|
+
ARRAY_SUPERCLASSES = %w[Object BasicObject].freeze
|
|
90
|
+
|
|
91
|
+
# Issue #1094 — whether an instance of `class_name` provably has no implicit array conversion, so
|
|
92
|
+
# `a, b = value` binds `a` to the value and `b` to `nil` rather than splatting it. Shares ADR-43's
|
|
93
|
+
# rationale for when an RBS ancestry is closed: the RBS of a class the environment KNOWS is the method
|
|
94
|
+
# set every other negative rule already trusts (`call.undefined-method` fires on it), while a class the
|
|
95
|
+
# environment does not know — a Ruby-source class, whatever it inherits — is an open hierarchy and
|
|
96
|
+
# declines, exactly as ADR-43 keeps it on the Dynamic fallback. Modules and `Array`'s own superclasses
|
|
97
|
+
# decline because a value of those types is routinely something else. A project `def` of any hook on
|
|
98
|
+
# the class, its source ancestors, or the default owners declines too: the project's source outranks
|
|
99
|
+
# the RBS it did not write. Without a `scope` only the core list answers.
|
|
100
|
+
def array_conversion_free?(class_name, scope)
|
|
101
|
+
name = class_name.to_s.delete_prefix("::")
|
|
102
|
+
return false if project_defines_array_conversion?(name, scope)
|
|
103
|
+
return true if ARRAY_CONVERSION_FREE_CORE_CLASSES.include?(name)
|
|
104
|
+
return false if scope.nil? || ARRAY_SUPERCLASSES.include?(name)
|
|
105
|
+
|
|
106
|
+
rbs_ancestry_array_conversion_free?(name, scope.environment)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# ADR-17's boundary for what the project's source can add to a class: a `def` in the analysed file
|
|
110
|
+
# (`Scope#discovered_method*`, walked through the source ancestry) or in a `pre_eval:` file
|
|
111
|
+
# (`Environment#project_patched_methods`). Both are asked about the class, its RBS ancestors and the
|
|
112
|
+
# default owners, because a hook added to any of them reaches the instance.
|
|
113
|
+
def project_defines_array_conversion?(name, scope)
|
|
114
|
+
return false if scope.nil?
|
|
115
|
+
|
|
116
|
+
owners = [name, *rbs_instance_ancestor_names(name, scope.environment),
|
|
117
|
+
*ARRAY_CONVERSION_DEFAULT_OWNERS.map { |owner| owner.delete_prefix("::") }].uniq
|
|
118
|
+
patched = scope.environment&.project_patched_methods
|
|
119
|
+
patched = nil if patched && patched.empty?
|
|
120
|
+
ARRAY_CONVERSION_HOOKS.any? do |hook|
|
|
121
|
+
scope.discovered_method_through_ancestors?(name, hook, :instance) ||
|
|
122
|
+
owners.any? do |owner|
|
|
123
|
+
scope.discovered_method?(owner, hook, :instance) ||
|
|
124
|
+
!patched&.lookup(class_name: owner, method_name: hook, kind: :instance).nil?
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def rbs_instance_ancestor_names(name, environment)
|
|
130
|
+
return [] if environment.nil? || !Rigor::Reflection.rbs_class_known?(name, environment: environment)
|
|
131
|
+
|
|
132
|
+
definition = Rigor::Reflection.instance_definition(name, environment: environment)
|
|
133
|
+
return [] if definition.nil?
|
|
134
|
+
|
|
135
|
+
definition.ancestors.ancestors.map { |ancestor| ancestor.name.to_s.delete_prefix("::") }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def rbs_ancestry_array_conversion_free?(name, environment)
|
|
139
|
+
return false if environment.nil?
|
|
140
|
+
return false unless Rigor::Reflection.rbs_class_known?(name, environment: environment)
|
|
141
|
+
return false if environment.rbs_module?(name)
|
|
142
|
+
|
|
143
|
+
definition = Rigor::Reflection.instance_definition(name, environment: environment)
|
|
144
|
+
return false if definition.nil?
|
|
145
|
+
|
|
146
|
+
ARRAY_CONVERSION_HOOKS.none? do |hook|
|
|
147
|
+
method = definition.methods[hook]
|
|
148
|
+
method && !ARRAY_CONVERSION_DEFAULT_OWNERS.include?(method.defined_in.to_s)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
67
152
|
# Shared empty returns for the argument-position type-variable binding (issue #303). The
|
|
68
153
|
# no-candidate answer is by far the common case — every non-generic overload takes it — so it must
|
|
69
154
|
# not allocate.
|
|
@@ -71,31 +156,48 @@ module Rigor
|
|
|
71
156
|
private_constant :EMPTY_TYPE_VARS
|
|
72
157
|
EMPTY_TYPE_PARAM_NAMES = [].freeze
|
|
73
158
|
private_constant :EMPTY_TYPE_PARAM_NAMES
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
#
|
|
94
|
-
#
|
|
159
|
+
NO_BINDING = [nil, nil].freeze
|
|
160
|
+
private_constant :NO_BINDING
|
|
161
|
+
EMPTY_ARGUMENT_NODES = [].freeze
|
|
162
|
+
private_constant :EMPTY_ARGUMENT_NODES
|
|
163
|
+
|
|
164
|
+
# The classes {#shared_value_class} admits. Given an argument of the same class, each one's `+`
|
|
165
|
+
# answers that class for every receiver, subclass instances included (`String#+` answers a String;
|
|
166
|
+
# the numeric classes have no instances of a subclass). `class Name < String` is left out, since the
|
|
167
|
+
# `+` it inherits answers a plain String, and so is any class whose `coerce` may make `+` answer a
|
|
168
|
+
# third one.
|
|
169
|
+
CLOSED_VALUE_CLASSES = Set["Integer", "Float", "Rational", "Complex", "String"].freeze
|
|
170
|
+
private_constant :CLOSED_VALUE_CLASSES
|
|
171
|
+
|
|
172
|
+
# Both spellings a resolved `RBS::Types::ClassInstance#name` — or a `Nominal#class_name` built
|
|
173
|
+
# from one — may carry for `Range`; core signatures absolutise, but a plugin-contributed one
|
|
174
|
+
# need not.
|
|
175
|
+
RANGE_TYPE_NAMES = ["::Range", "Range"].freeze
|
|
176
|
+
private_constant :RANGE_TYPE_NAMES
|
|
177
|
+
|
|
178
|
+
# Four fields of `context` shape the answer beyond receiver, method name and arguments.
|
|
179
|
+
#
|
|
180
|
+
# `block_type` is the inferred block return type propagated from `MethodDispatcher.dispatch`; when
|
|
181
|
+
# non-nil, the selector prefers a block-bearing overload and binds the method-level type parameter
|
|
182
|
+
# that the block's return type references to it (Slice 6 phase C sub-phase 2).
|
|
183
|
+
#
|
|
184
|
+
# `self_type_override`, when set, is the substitution for `Bases::Self` in the method's return type.
|
|
185
|
+
# `MethodDispatcher#try_user_class_fallback` uses it to preserve the ORIGINAL receiver as the
|
|
186
|
+
# substitute for `self` even though the dispatch is routed through `Nominal[Object]` — so that
|
|
187
|
+
# `Bundler::URI::Generic.dup` (which resolves through the `Object` fallback because
|
|
188
|
+
# `Bundler::URI::Generic` lacks RBS) returns `Bundler::URI::Generic` per `Kernel#dup: () -> self`
|
|
189
|
+
# rather than `Object`. Nil computes self from the resolved class_name as before.
|
|
190
|
+
#
|
|
191
|
+
# `public_only`, when true, keeps a method whose RBS accessibility is `:private` from resolving (the
|
|
192
|
+
# call yields `nil`, i.e. "no rule"). Set by the explicit-non-`self`-receiver user-class fallback so
|
|
193
|
+
# a call like `Favourite.select(...)` does not adopt the private `Kernel#select` signature.
|
|
194
|
+
#
|
|
195
|
+
# `scope`, when supplied, enables ADR-43 RBS-complete-ancestor resolution against
|
|
196
|
+
# `ALLOWED_RBS_COMPLETE_ANCESTORS`; `nil` keeps inherited calls unresolved (`Dynamic[Top]`) — the
|
|
197
|
+
# FP-safe default for open hierarchies (`< ActionController::Base`, …).
|
|
198
|
+
#
|
|
199
|
+
# @return inferred return type, or `nil` when no rule resolves (no class name,
|
|
95
200
|
# no method, dispatch on a Top/Dynamic[Top] receiver, etc.).
|
|
96
|
-
# @param scope [Rigor::Scope, nil] when supplied, enables ADR-43 RBS-complete-ancestor resolution
|
|
97
|
-
# against `ALLOWED_RBS_COMPLETE_ANCESTORS`. `nil` keeps inherited calls unresolved
|
|
98
|
-
# (`Dynamic[Top]`) — the FP-safe default for open hierarchies (`< ActionController::Base`, …).
|
|
99
201
|
def try_dispatch(context)
|
|
100
202
|
environment = context.environment
|
|
101
203
|
return nil if environment.nil?
|
|
@@ -130,7 +232,7 @@ module Rigor
|
|
|
130
232
|
#
|
|
131
233
|
# This deliberately does NOT differentiate "no overload had a block" from "the block is untyped";
|
|
132
234
|
# the binder treats both the same way (every parameter defaults to `Dynamic[Top]`).
|
|
133
|
-
# @return
|
|
235
|
+
# @return positional block parameter types.
|
|
134
236
|
def block_param_types(context)
|
|
135
237
|
environment = context.environment
|
|
136
238
|
return [] if environment.nil?
|
|
@@ -140,7 +242,8 @@ module Rigor
|
|
|
140
242
|
receiver: context.receiver,
|
|
141
243
|
method_name: context.method_name,
|
|
142
244
|
args: context.args,
|
|
143
|
-
environment: environment
|
|
245
|
+
environment: environment,
|
|
246
|
+
scope: context.scope
|
|
144
247
|
)
|
|
145
248
|
end
|
|
146
249
|
|
|
@@ -178,9 +281,19 @@ module Rigor
|
|
|
178
281
|
return nil unless descriptor
|
|
179
282
|
|
|
180
283
|
class_name, kind, receiver_args = descriptor
|
|
181
|
-
method_definition = lookup_method(environment, class_name, kind, method_name, scope
|
|
284
|
+
method_definition = lookup_method(environment, class_name, kind, method_name, scope,
|
|
285
|
+
call_node: call_node)
|
|
182
286
|
return nil unless method_definition
|
|
183
287
|
return nil if public_only && method_private?(method_definition)
|
|
288
|
+
# Issue #823 — a declaration that states the member's presence and parameters but not its
|
|
289
|
+
# return (`%a{rigor:v1:inferred-return}`, written by `rigor-rbs-inline` on every type slot it
|
|
290
|
+
# DEFAULTED). Declining here is the whole mechanism: this tier's product IS the return type, so
|
|
291
|
+
# withholding it routes the call down the same tiers an undeclared method takes and the body
|
|
292
|
+
# gets typed. Everything the declaration DOES say survives, because the rules that read it —
|
|
293
|
+
# `call.undefined-method`, `call.wrong-arity`, argument-type checking — look the method up in
|
|
294
|
+
# the environment themselves rather than reading this tier's answer.
|
|
295
|
+
return nil if RbsExtended.inferred_return?(method_definition)
|
|
296
|
+
return SPACESHIP_ENVELOPE if inherited_identity_spaceship?(method_definition, class_name, kind, method_name)
|
|
184
297
|
|
|
185
298
|
type_vars = build_type_vars(environment, class_name, receiver_args)
|
|
186
299
|
translate_return_type(
|
|
@@ -192,7 +305,8 @@ module Rigor
|
|
|
192
305
|
type_vars: type_vars,
|
|
193
306
|
block_type: block_type,
|
|
194
307
|
environment: environment,
|
|
195
|
-
self_type_override: self_type_override
|
|
308
|
+
self_type_override: self_type_override ||
|
|
309
|
+
SelfSubstitute.for(receiver, receiver_args, method_name, args, block_type),
|
|
196
310
|
scope: scope,
|
|
197
311
|
call_node: call_node
|
|
198
312
|
)
|
|
@@ -202,6 +316,46 @@ module Rigor
|
|
|
202
316
|
nil
|
|
203
317
|
end
|
|
204
318
|
|
|
319
|
+
# Issue #661 — `Kernel#<=>: (untyped other) -> 0?` is the IDENTITY comparison: `0` when the two
|
|
320
|
+
# are the same object, `nil` otherwise, and correct as written for a bare `Object`. Reached by
|
|
321
|
+
# inheritance it stops being a statement about the call. `1.day <=> 2.days` typed as `0?`, and a
|
|
322
|
+
# `Money` whose signature says `include Comparable` — whose whole contract is that the includer
|
|
323
|
+
# defines `<=>` — typed the same, because neither declares `<=>` of its own and every class
|
|
324
|
+
# inherits Kernel's.
|
|
325
|
+
#
|
|
326
|
+
# `0?` is a VALUE claim, so the cost is not confined to the expression: narrowing
|
|
327
|
+
# `n = a <=> b; n.negative? if n` reads the truthy arm as the literal `0`, folds `0.negative?`
|
|
328
|
+
# to false, and answers `bot` for a branch the runtime takes on every ordered pair. That is one
|
|
329
|
+
# `clause.unreachable` away from a false positive on correct code.
|
|
330
|
+
#
|
|
331
|
+
# Widened rather than declined: `Integer?` is the envelope Ruby's own `<=>` convention states and
|
|
332
|
+
# the one upstream rbs writes wherever a class DOES declare the operator (`Array`, `Module`,
|
|
333
|
+
# `Complex`). It is a supertype of `0?`, so this only ever removes a conclusion — and it removes
|
|
334
|
+
# exactly the conclusions that rested on the receiver not overriding an operator its signature
|
|
335
|
+
# was never required to mention (ADR-5: a partially-declared class is not a closed world).
|
|
336
|
+
#
|
|
337
|
+
# Bounded to the three classes that OWN the identity comparison, where the claim is the truth
|
|
338
|
+
# about the receiver rather than an artifact of inheritance, and to `<=>` alone — the general
|
|
339
|
+
# question of what an inherited Object/Kernel signature may assert about a subclass is much
|
|
340
|
+
# larger, and `to_s` / `hash` / `inspect` do not carry a value-precise return to lose.
|
|
341
|
+
SPACESHIP_IDENTITY_OWNERS = %w[Kernel Object BasicObject].to_set.freeze
|
|
342
|
+
private_constant :SPACESHIP_IDENTITY_OWNERS
|
|
343
|
+
|
|
344
|
+
SPACESHIP_ENVELOPE = Type::Combinator.union(
|
|
345
|
+
Type::Combinator.nominal_of("Integer"),
|
|
346
|
+
Type::Combinator.constant_of(nil)
|
|
347
|
+
).freeze
|
|
348
|
+
private_constant :SPACESHIP_ENVELOPE
|
|
349
|
+
|
|
350
|
+
def inherited_identity_spaceship?(method_definition, class_name, kind, method_name)
|
|
351
|
+
return false unless method_name == :<=>
|
|
352
|
+
return false unless kind == :instance
|
|
353
|
+
return false if SPACESHIP_IDENTITY_OWNERS.include?(class_name.to_s.delete_prefix("::"))
|
|
354
|
+
return false unless method_definition.respond_to?(:defined_in)
|
|
355
|
+
|
|
356
|
+
SPACESHIP_IDENTITY_OWNERS.include?(method_definition.defined_in.to_s.delete_prefix("::"))
|
|
357
|
+
end
|
|
358
|
+
|
|
205
359
|
# Maps a Rigor::Type receiver to a `[class_name, kind, type_args]` triple where `kind` is either
|
|
206
360
|
# `:instance` or `:singleton` and `type_args` carries the receiver's generic instantiation (empty
|
|
207
361
|
# for raw or singleton receivers, since `Singleton[Foo]` carries no generic args today). Returns
|
|
@@ -237,6 +391,13 @@ module Rigor
|
|
|
237
391
|
# whole dispatch to Dynamic). The refinement-aware promotions (`String#upcase` →
|
|
238
392
|
# `uppercase-string`, …) run in their own catalog tier ABOVE this one, so they still win.
|
|
239
393
|
receiver_descriptor(receiver.base)
|
|
394
|
+
when Type::FloatRange
|
|
395
|
+
# ADR-109 WD4 — a bounded Float is a Float for every method the fold tiers do not own.
|
|
396
|
+
["Float", :instance, []]
|
|
397
|
+
when Type::IntegerRange
|
|
398
|
+
# #842 — a bounded Integer is an Integer for every method the fold tiers
|
|
399
|
+
# (ConstantFolding, ShapeDispatch#dispatch_integer_range) do not own.
|
|
400
|
+
["Integer", :instance, []]
|
|
240
401
|
when Type::Dynamic
|
|
241
402
|
receiver_descriptor(receiver.static_facet)
|
|
242
403
|
end
|
|
@@ -261,11 +422,19 @@ module Rigor
|
|
|
261
422
|
[Type::Combinator.union(*tuple.elements)]
|
|
262
423
|
end
|
|
263
424
|
|
|
425
|
+
# An open shape's unseen keys may hold any value (rbs-erasure.md § *Open shapes with extra-value
|
|
426
|
+
# bounds*), so its projection carries a `Dynamic[top]` arm on both sides. Without it every read the
|
|
427
|
+
# shape tier does not answer — a `Union` receiver, a non-literal key — took the known values for a
|
|
428
|
+
# key outside them: `h = { a: 1 }; h.default = 0 if flag; h[:b] == 1` folded always-truthy.
|
|
264
429
|
def hash_shape_type_args(shape)
|
|
265
430
|
return [] if shape.pairs.empty?
|
|
266
431
|
|
|
267
432
|
key_types = shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) }
|
|
268
433
|
value_types = shape.pairs.values
|
|
434
|
+
if shape.open?
|
|
435
|
+
key_types += [Type::Combinator.untyped]
|
|
436
|
+
value_types += [Type::Combinator.untyped]
|
|
437
|
+
end
|
|
269
438
|
[
|
|
270
439
|
Type::Combinator.union(*key_types),
|
|
271
440
|
Type::Combinator.union(*value_types)
|
|
@@ -280,20 +449,43 @@ module Rigor
|
|
|
280
449
|
method_definition.accessibility == :private
|
|
281
450
|
end
|
|
282
451
|
|
|
283
|
-
def lookup_method(environment, class_name, kind, method_name, scope = nil)
|
|
452
|
+
def lookup_method(environment, class_name, kind, method_name, scope = nil, call_node: nil)
|
|
284
453
|
direct = lookup_method_on(environment, class_name, kind, method_name)
|
|
285
454
|
return direct if direct
|
|
286
455
|
|
|
456
|
+
# Issue #1173 — the include-edge sibling of the superclass bridges below: a discovered
|
|
457
|
+
# class's `include M` where M is RBS-known resolves M's declaration here. It runs BEFORE
|
|
458
|
+
# them because an include edge precedes the superclass edge in the MRO (`Sub < Hash` with
|
|
459
|
+
# `include M` chains `Sub → M → Hash`): when both a nearer mixin and a bridged ancestor
|
|
460
|
+
# declare the name, the mixin is the method that runs. See {#included_module_method}.
|
|
461
|
+
included = included_module_method(environment, class_name, kind, method_name, scope)
|
|
462
|
+
return included if included
|
|
463
|
+
|
|
287
464
|
# ADR-43 — scoped inherited-method resolution. The direct lookup misses when `class_name` is a
|
|
288
465
|
# Ruby-source subclass absent from RBS (so no ancestor walk runs). If its discovered
|
|
289
466
|
# superclass chain reaches an allow-listed RBS-complete ancestor, resolve the method there so
|
|
290
467
|
# inherited contract calls (`self.manifest` on a plugin) resolve and the normal call rules
|
|
291
468
|
# apply. Bounded to the allow-list, so open hierarchies stay on the Dynamic fallback (no false
|
|
292
469
|
# positive on `< ActionController::Base`).
|
|
293
|
-
ancestor = allowed_rbs_complete_ancestor(environment, class_name, scope)
|
|
294
|
-
return
|
|
470
|
+
ancestor = allowed_rbs_complete_ancestor(environment, class_name, kind, method_name, scope)
|
|
471
|
+
return lookup_method_on(environment, ancestor, kind, method_name) if ancestor
|
|
472
|
+
|
|
473
|
+
# `extend M` in a class/module body lifts M's INSTANCE surface onto the extending object's
|
|
474
|
+
# singleton — `class F; extend T::Sig; sig { ... }; end` resolves `sig` through
|
|
475
|
+
# `T::Sig#sig`. Same contract as the superclass bridge: only allow-listed (manifest
|
|
476
|
+
# `rbs_complete_extends:`) modules qualify, so open hierarchies stay on Dynamic.
|
|
477
|
+
if kind == :singleton
|
|
478
|
+
mod = allowed_rbs_complete_extended_module(environment, class_name, method_name, scope,
|
|
479
|
+
call_node)
|
|
480
|
+
return lookup_method_on(environment, mod, :instance, method_name) if mod
|
|
481
|
+
end
|
|
295
482
|
|
|
296
|
-
|
|
483
|
+
# Issue #527 slice 1 — the same shape, one RBS ancestry wider: a Ruby-source subclass of a
|
|
484
|
+
# CORE or STDLIB class (`class SubHash < Hash`, `< StandardError`, `< ::StringScanner`)
|
|
485
|
+
# resolves its inherited calls there. Injected HERE rather than as a new tier because
|
|
486
|
+
# `dispatch_one` keys `self`, `instance`, the type-variable map and `SelfSubstitute` on the
|
|
487
|
+
# RECEIVER's class name, so changing only the lookup gets the correct binding for free.
|
|
488
|
+
core_stdlib_ancestor_method(environment, class_name, kind, method_name, scope)
|
|
297
489
|
end
|
|
298
490
|
|
|
299
491
|
def lookup_method_on(environment, class_name, kind, method_name)
|
|
@@ -310,36 +502,478 @@ module Rigor
|
|
|
310
502
|
# `class_name` is itself RBS-known (the direct lookup already had authority), or when the
|
|
311
503
|
# discovered chain reaches no allow-listed class. The walk carries a visited set so a malformed
|
|
312
504
|
# cyclic `A < B < A` source cannot loop.
|
|
313
|
-
|
|
505
|
+
#
|
|
506
|
+
# ADR-43 WD4 — the allow-list's manifest-declared half: a loaded plugin may name its own
|
|
507
|
+
# contract classes in `rbs_complete_ancestors:` (e.g. rigor-graphql's `GraphQL::Schema::Object`),
|
|
508
|
+
# extending the engine's hard-coded seed without editing this constant.
|
|
509
|
+
def allowed_rbs_complete_ancestor(environment, class_name, kind, method_name, scope)
|
|
314
510
|
return nil if scope.nil?
|
|
315
511
|
return nil if Rigor::Reflection.rbs_class_known?(class_name, environment: environment)
|
|
316
512
|
|
|
513
|
+
# A project `def` on the receiver class or on a nearer source ancestor shadows the
|
|
514
|
+
# bridged declaration. RBS dispatch runs before the discovered-method tier, so without
|
|
515
|
+
# this guard the bridge would resolve e.g. `field` on `GraphQL::Schema::Object` while
|
|
516
|
+
# the runtime actually calls a user `def self.field` on an intermediate `BaseObject` —
|
|
517
|
+
# a wrong return type and a false `undefined-method`/arity reading downstream.
|
|
518
|
+
return nil if scope.discovered_method?(class_name, method_name, kind)
|
|
519
|
+
|
|
520
|
+
registry = environment&.plugin_registry
|
|
521
|
+
each_source_ancestor_candidate(scope, class_name) do |candidate|
|
|
522
|
+
return nil if scope.discovered_method?(candidate, method_name, kind)
|
|
523
|
+
return candidate if ALLOWED_RBS_COMPLETE_ANCESTORS.include?(candidate) ||
|
|
524
|
+
registry&.rbs_complete_ancestor?(candidate)
|
|
525
|
+
end
|
|
526
|
+
nil
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# Issue #527 slice 1 — the RBS instance definition a Ruby-source class inherits from a CORE or
|
|
530
|
+
# STDLIB ancestor, or nil. `Oj::EasyHash < Hash` answering `Dynamic[top]` to `has_key?` while
|
|
531
|
+
# `{}.has_key?` folds was the largest single opacity family in the 2026-09-01 corpus sweep.
|
|
532
|
+
#
|
|
533
|
+
# Why this is not ADR-43's rejected alternative A. That ADR declined blanket inherited
|
|
534
|
+
# resolution because firing `call.undefined-method` against a PARTIAL gem RBS "would frighten
|
|
535
|
+
# working code". Two things narrow it here. The ancestry is core / stdlib, whose RBS is the
|
|
536
|
+
# method set every negative rule already trusts for a direct receiver of it. And the negative
|
|
537
|
+
# rules do not reach these receivers anyway: `undefined_method_diagnostic` and
|
|
538
|
+
# `arity_envelope_for` gate on `Reflection.rbs_class_known?` of the RECEIVER, which a
|
|
539
|
+
# Ruby-source subclass never is. So the risk this arm carries is not a new firing but a
|
|
540
|
+
# WRONG PRECISE TYPE propagating one hop — which is what the declines below are about.
|
|
541
|
+
#
|
|
542
|
+
# The declines, in order of what they protect:
|
|
543
|
+
#
|
|
544
|
+
# * `class_name` itself RBS-known — the direct lookup already had authority.
|
|
545
|
+
# * an ADR-26 plugin-declared open receiver, whose surface is larger than its declarations.
|
|
546
|
+
# * the subclass or a nearer SOURCE ancestor declares the name (ADR-110): the runtime calls
|
|
547
|
+
# the project's `def`, and resolving the inherited declaration would answer about a method
|
|
548
|
+
# that never runs. Asked of both discovery tables, because neither sees the whole of what a
|
|
549
|
+
# `def` / `attr_*` / `define_method` / `alias` contributes, and both suppress on budget
|
|
550
|
+
# exhaustion rather than answering "not declared" from an unfinished walk.
|
|
551
|
+
# * the walked ancestor, or the class the declaration is actually written on, is not core /
|
|
552
|
+
# stdlib. That is what keeps `class MyController < ActionController::Base` on `Dynamic[top]`
|
|
553
|
+
# (no RBS at all), and a subclass of an RBS-shipping GEM there too — slice 3's question.
|
|
554
|
+
# * an ADR-17 `pre_eval:` patch declares the name on the receiver or on any ancestor of the
|
|
555
|
+
# owner: the project has replaced the very method whose declaration this would adopt.
|
|
556
|
+
#
|
|
557
|
+
# Type variables are NOT inferred: a `Nominal[SubHash]` receiver carries no type arguments, so
|
|
558
|
+
# `build_type_vars` yields the empty map and `Hash[K, V]`'s free variables degrade to
|
|
559
|
+
# `Dynamic[top]` per the translator's contract. `SubHash#keys` is `Array[Dynamic[top]]`, which
|
|
560
|
+
# is exactly what a raw `Hash` receiver already answers — honest rather than a loss.
|
|
561
|
+
def core_stdlib_ancestor_method(environment, class_name, kind, method_name, scope)
|
|
562
|
+
return nil if scope.nil? || kind != :instance
|
|
563
|
+
return nil if environment.nil?
|
|
564
|
+
|
|
565
|
+
memo = core_stdlib_memo(environment, scope)
|
|
566
|
+
key = [class_name.to_s, method_name.to_sym]
|
|
567
|
+
return memo[key] if memo&.key?(key)
|
|
568
|
+
|
|
569
|
+
answer = compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
|
|
570
|
+
memo[key] = answer if memo
|
|
571
|
+
answer
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# Issue #1173 — the include-edge sibling of {#core_stdlib_ancestor_method}: a discovered
|
|
575
|
+
# class's `include M` where M is RBS-known (a project `sig/`, bundled core / stdlib, or a
|
|
576
|
+
# shipped gem signature) resolves M's declaration here. Ruby inserts an included module into
|
|
577
|
+
# the ancestor chain outright, so adopting its declaration is the dispatch the runtime
|
|
578
|
+
# performs — unlike the superclass arm there is no "which classes may be read as complete"
|
|
579
|
+
# question, only the usual shadow guards: a project `def` on the receiver or a nearer
|
|
580
|
+
# ancestor, an outside-the-body `include` / `class_eval` mark (#992), or an ADR-17 `pre_eval:`
|
|
581
|
+
# patch each mean the declaration found is not the method that runs.
|
|
582
|
+
#
|
|
583
|
+
# The walk reuses {ExternalAncestorResolution} with `mixins: true` — an include edge precedes
|
|
584
|
+
# the superclass edge at each BFS node, matching the MRO — and the answer is adopted ONLY
|
|
585
|
+
# when the resolved owner is an RBS module (`environment.rbs_module?`). A superclass-owned
|
|
586
|
+
# answer keeps declining: a non-core ancestor class is the gap #527's superclass slice
|
|
587
|
+
# deferred, and the walk's ordering already gave every include edge its chance first.
|
|
588
|
+
#
|
|
589
|
+
# `kind` is `:instance` only: an `include`d module's `def self.x` is not callable on the
|
|
590
|
+
# includer (the singleton side is a separate #527 item). `self` needs no help here —
|
|
591
|
+
# `dispatch_one` keys `self` / `instance` on the RECEIVER's class name, so a `-> self`
|
|
592
|
+
# module method answers the includer, matching CRuby.
|
|
593
|
+
def included_module_method(environment, class_name, kind, method_name, scope)
|
|
594
|
+
return nil if scope.nil? || kind != :instance
|
|
595
|
+
return nil if environment.nil?
|
|
596
|
+
|
|
597
|
+
memo = mixin_ancestor_memo(environment, scope)
|
|
598
|
+
key = [class_name.to_s, method_name.to_sym]
|
|
599
|
+
return memo[key] if memo&.key?(key)
|
|
600
|
+
|
|
601
|
+
answer = compute_included_module_method(environment, class_name, method_name, scope)
|
|
602
|
+
memo[key] = answer if memo
|
|
603
|
+
answer
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# The declines are conjunctive and ordered as {compute_core_stdlib_ancestor_method}'s are:
|
|
607
|
+
# the two walks that read the project's tables run LAST, only once an RBS declaration is
|
|
608
|
+
# actually in hand, because they read `Scope#superclass_of` / `#includes_of` and would file a
|
|
609
|
+
# file-granular ancestry edge for every call site otherwise.
|
|
610
|
+
def compute_included_module_method(environment, class_name, method_name, scope)
|
|
611
|
+
return nil if Rigor::Reflection.rbs_class_known?(class_name, environment: environment)
|
|
612
|
+
return nil if environment.plugin_registry&.open_receiver?(class_name)
|
|
613
|
+
|
|
614
|
+
definition, owner = Inference::ExternalAncestorResolution.resolve(
|
|
615
|
+
class_name, method_name, :instance,
|
|
616
|
+
scope: scope, environment: environment, record_dependencies: false, mixins: true
|
|
617
|
+
)
|
|
618
|
+
return nil if definition.nil?
|
|
619
|
+
return nil unless environment.rbs_module?(owner)
|
|
620
|
+
return nil if returns_the_walked_ancestry?(definition, owner, environment)
|
|
621
|
+
return nil if dynamic_surface_through_ancestors?(scope, class_name)
|
|
622
|
+
return nil if project_patched_through_ancestors?(environment, scope, class_name, owner, method_name)
|
|
623
|
+
return nil if source_declares_through_ancestors?(scope, class_name, method_name)
|
|
624
|
+
|
|
625
|
+
definition
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
# The same one-slot memo shape as {#core_stdlib_memo}; see there for why it is one slot keyed
|
|
629
|
+
# on the discovery index's identity, and why a recording run bypasses it.
|
|
630
|
+
MIXIN_ANCESTOR_MEMO_KEY = :__rigor_mixin_ancestor_dispatch__
|
|
631
|
+
private_constant :MIXIN_ANCESTOR_MEMO_KEY
|
|
632
|
+
|
|
633
|
+
def mixin_ancestor_memo(environment, scope)
|
|
634
|
+
return nil if Rigor::Analysis::DependencyRecorder.active?
|
|
635
|
+
|
|
636
|
+
discovery = scope.discovery
|
|
637
|
+
slot = Thread.current[MIXIN_ANCESTOR_MEMO_KEY]
|
|
638
|
+
unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
|
|
639
|
+
slot = [discovery, environment, {}]
|
|
640
|
+
Thread.current[MIXIN_ANCESTOR_MEMO_KEY] = slot
|
|
641
|
+
end
|
|
642
|
+
slot[2]
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# The declines are conjunctive, so their ORDER is free — and it is chosen so the two that walk
|
|
646
|
+
# the project's tables run LAST, only once a core / stdlib declaration is actually in hand.
|
|
647
|
+
# Every unresolved call on a Ruby-source receiver reaches here (`Widget.new.price` on a plain
|
|
648
|
+
# project class), and those walks read `Scope#superclass_of` / `#includes_of`, which file an
|
|
649
|
+
# ADR-46 ancestry edge. Running them unconditionally turned every cross-class METHOD call into
|
|
650
|
+
# a file-granular ancestry dependency — coarser than the symbol edge ADR-46 slice 4 files, and
|
|
651
|
+
# pinned against by `dependency_recorder_spec`. Reached at all, the edge is genuine: this
|
|
652
|
+
# answer does depend on the project not declaring the name on that ancestry.
|
|
653
|
+
def compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
|
|
654
|
+
return nil if Rigor::Reflection.rbs_class_known?(class_name, environment: environment)
|
|
655
|
+
return nil if environment.plugin_registry&.open_receiver?(class_name)
|
|
656
|
+
|
|
657
|
+
definition, owner = Inference::ExternalAncestorResolution.resolve(
|
|
658
|
+
class_name, method_name, :instance,
|
|
659
|
+
scope: scope, environment: environment, record_dependencies: false, mixins: false
|
|
660
|
+
)
|
|
661
|
+
return nil if definition.nil?
|
|
662
|
+
return nil unless core_or_stdlib_owned?(environment, owner, definition)
|
|
663
|
+
return nil if returns_the_walked_ancestry?(definition, owner, environment)
|
|
664
|
+
return nil if dynamic_surface_through_ancestors?(scope, class_name)
|
|
665
|
+
return nil if project_patched_through_ancestors?(environment, scope, class_name, owner, method_name)
|
|
666
|
+
return nil if source_declares_through_ancestors?(scope, class_name, method_name)
|
|
667
|
+
|
|
668
|
+
definition
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# The blocker this slice was first written without. CRuby PRESERVES THE SUBCLASS where core /
|
|
672
|
+
# stdlib RBS names the base class: `SubHash#merge` returns a `SubHash`, `SubSet#flatten` a
|
|
673
|
+
# `SubSet`, `SubPathname#basename` a `SubPathname`, `SubDate#+` a `SubDate` — verified against
|
|
674
|
+
# the interpreter. Adopting the declaration answers `Nominal[Hash]`, and because `Hash` IS
|
|
675
|
+
# RBS-known the negative rules then read it as a CLOSED surface: `sub.merge({}).own_method`
|
|
676
|
+
# drew an `error`-severity `call.undefined-method` on working code. That is ADR-5's failure
|
|
677
|
+
# exactly, one hop downstream — the propagation this slice's own boundary section names.
|
|
678
|
+
#
|
|
679
|
+
# RBS cannot distinguish the two families. `String#upcase: () -> String` really does return a
|
|
680
|
+
# plain `String` for a `String` subclass (Ruby 3.0 changed that), while `Hash#merge: () ->
|
|
681
|
+
# Hash[K, V]` really does return the subclass, and the two declarations are the same shape.
|
|
682
|
+
# So the answer is DECLINE, which is master's `Dynamic[top]` and therefore provably cannot
|
|
683
|
+
# regress a corpus target — rather than substituting the receiver as if the declaration read
|
|
684
|
+
# `-> self`. That substitution would be right for `merge` and wrong for `upcase`, and its
|
|
685
|
+
# wrongness is not purely a false negative: a `Nominal[SubStr]` that is really a `String`
|
|
686
|
+
# narrows `is_a?` guards and can reach `clause.unreachable` on a branch the runtime takes.
|
|
687
|
+
#
|
|
688
|
+
# `-> self` and `-> instance` returns are NOT affected and keep their precision: those already
|
|
689
|
+
# resolve against the receiver (`SubHash#clear` → `SubHash`, `SubStr#force_encoding` →
|
|
690
|
+
# `SubStr`, `MyError#exception` → `MyError`), which is what CRuby does.
|
|
691
|
+
#
|
|
692
|
+
# The test looks for the owner ANYWHERE in the return type, type arguments included. A first
|
|
693
|
+
# draft unwrapped only the top level, unions and optionals, on the reasoning that a class named
|
|
694
|
+
# inside `Array[...]` describes the elements rather than the returned object — true, and beside
|
|
695
|
+
# the point, because the ELEMENTS are subclass instances too. `Pathname#children: () ->
|
|
696
|
+
# Array[Pathname]` hands back an array of `SubPath`s (likewise `entries`, `each_child`,
|
|
697
|
+
# `ascend`, `descend`, `find`; only `glob` yields a plain `Pathname`), so
|
|
698
|
+
# `sub.children.first.own_method` fired the same `call.undefined-method` one level down.
|
|
699
|
+
# `Date#step`, `Date#upto` and `Set#classify` are the same family and escaped only by the shape
|
|
700
|
+
# of their declarations.
|
|
701
|
+
#
|
|
702
|
+
# The cost of the deeper walk is in the direction WD7 already accepts: `SubStr#chars` →
|
|
703
|
+
# `Array[String]` now declines although CRuby really does yield plain `String`s. `keys`,
|
|
704
|
+
# `to_a` and `classify` are unaffected, their arguments being type variables or `self`.
|
|
705
|
+
#
|
|
706
|
+
# An `Alias` that expands to the owner is not followed; that is a known gap in the FN direction.
|
|
707
|
+
def returns_the_walked_ancestry?(definition, owner, environment)
|
|
708
|
+
names = [owner.to_s.delete_prefix("::"), *rbs_instance_ancestor_names(owner, environment)].to_set
|
|
709
|
+
method_types = definition.respond_to?(:method_types) ? definition.method_types : nil
|
|
710
|
+
return false if method_types.nil?
|
|
711
|
+
|
|
712
|
+
method_types.any? do |method_type|
|
|
713
|
+
return_type = method_type.type.respond_to?(:return_type) ? method_type.type.return_type : nil
|
|
714
|
+
mentions_class?(return_type, names)
|
|
715
|
+
end
|
|
716
|
+
rescue StandardError
|
|
717
|
+
# A signature whose return type cannot be read is a gap, and a gap declines.
|
|
718
|
+
true
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
# Whether any of `names` appears as a class instance anywhere in `type`, descending through
|
|
722
|
+
# every child an RBS type exposes — union members, the inside of an optional, and type
|
|
723
|
+
# ARGUMENTS. See {returns_the_walked_ancestry?}.
|
|
724
|
+
def mentions_class?(type, names, depth = 0)
|
|
725
|
+
return false if type.nil? || depth > RETURN_TYPE_UNWRAP_DEPTH
|
|
726
|
+
return true if own_class_name_matches?(type, names)
|
|
727
|
+
return false unless type.respond_to?(:each_type)
|
|
728
|
+
|
|
729
|
+
type.each_type.any? { |child| mentions_class?(child, names, depth + 1) }
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def own_class_name_matches?(type, names)
|
|
733
|
+
type.is_a?(::RBS::Types::ClassInstance) && names.include?(type.name.to_s.delete_prefix("::"))
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
# A guard against a pathological or cyclic signature, not a semantic limit: real return types
|
|
737
|
+
# nest a level or two (`Array[Pathname]`, `Hash[Symbol, Array[String]]`).
|
|
738
|
+
RETURN_TYPE_UNWRAP_DEPTH = 8
|
|
739
|
+
private_constant :RETURN_TYPE_UNWRAP_DEPTH
|
|
740
|
+
|
|
741
|
+
# Issue #992's surface mark: a `Klass.include(M)` / `.prepend(M)` / `class_eval` written
|
|
742
|
+
# OUTSIDE the class body, which `ScopeIndexer` records as `ENVELOPE_DYNAMIC_MARK` because it
|
|
743
|
+
# can add members the in-body walks never see. `class Extended < Hash; end` followed by
|
|
744
|
+
# `Extended.include(Ext)` where `Ext#empty?` returns `42` must not adopt `Hash#empty?`. Asked
|
|
745
|
+
# of the receiver and of every SOURCE ancestor between it and the owner, because a mark on an
|
|
746
|
+
# intermediate reaches the receiver just as well.
|
|
747
|
+
def dynamic_surface_through_ancestors?(scope, class_name)
|
|
748
|
+
return true if dynamic_surface?(scope, class_name)
|
|
749
|
+
|
|
750
|
+
each_source_ancestor_candidate(scope, class_name) do |candidate|
|
|
751
|
+
return true if dynamic_surface?(scope, candidate)
|
|
752
|
+
end
|
|
753
|
+
false
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
def dynamic_surface?(scope, class_name)
|
|
757
|
+
Scope::DiscoveryIndex.rewritten_surface?(scope.parameter_envelopes_of(class_name))
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
# ADR-110's precedence, asked of both tables the project's own members land in. Either one
|
|
761
|
+
# answering true is a decline; both suppress (answer true) when their shared
|
|
762
|
+
# `Scope::ANCESTOR_WALK_LIMIT` budget runs out, and record a `BudgetTrace` hit there.
|
|
763
|
+
def source_declares_through_ancestors?(scope, class_name, method_name)
|
|
764
|
+
return true if scope.discovered_method_through_ancestors?(class_name, method_name, :instance)
|
|
765
|
+
|
|
766
|
+
!scope.user_def_through_ancestors(class_name, method_name).first.nil?
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
# Both ends of the declaration have to be core / stdlib: the ancestor the walk asked (`Hash`,
|
|
770
|
+
# `StringScanner`) and the class the declaration is written on (`Exception` for
|
|
771
|
+
# `StandardError#message`, `Comparable` for a `clamp`). Either being a gem's or the project's
|
|
772
|
+
# own RBS is slice 3's question, not this one's.
|
|
773
|
+
def core_or_stdlib_owned?(environment, owner, definition)
|
|
774
|
+
loader = environment.rbs_loader
|
|
775
|
+
return false if loader.nil? || !loader.respond_to?(:core_or_stdlib_class?)
|
|
776
|
+
return false unless loader.core_or_stdlib_class?(owner)
|
|
777
|
+
|
|
778
|
+
declared_on = definition.respond_to?(:defined_in) ? definition.defined_in : nil
|
|
779
|
+
return false if declared_on.nil?
|
|
780
|
+
|
|
781
|
+
loader.core_or_stdlib_class?(declared_on.to_s)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# ADR-17 — a `pre_eval:` file that reopens the receiver, any SOURCE ancestor between it and the
|
|
785
|
+
# owner, or any RBS ancestor of the owner, and redefines the name. The declaration this arm
|
|
786
|
+
# would adopt is then not the method that runs. The source chain is the half the first draft
|
|
787
|
+
# missed: `class Middle < Hash; end; class Leaf < Middle; end` with a `pre_eval:`
|
|
788
|
+
# `class Middle; def key?(k) = 42; end` answered `bool` for a call that returns `42`.
|
|
789
|
+
def project_patched_through_ancestors?(environment, scope, class_name, owner, method_name)
|
|
790
|
+
patched = environment.project_patched_methods
|
|
791
|
+
return false if patched.nil? || patched.empty?
|
|
792
|
+
|
|
793
|
+
owners = [class_name.to_s.delete_prefix("::"), *rbs_instance_ancestor_names(owner, environment)]
|
|
794
|
+
each_source_ancestor_candidate(scope, class_name) { |candidate| owners << candidate }
|
|
795
|
+
owners.any? do |name|
|
|
796
|
+
!patched.lookup(class_name: name, method_name: method_name, kind: :instance).nil?
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# Memo for the whole decision. Every call site of a class asks the same `(class, method)`
|
|
801
|
+
# question, and the answer is a pure function of the frozen discovery index and the
|
|
802
|
+
# environment, so it is cacheable on their identity. A run that is RECORDING ADR-46
|
|
803
|
+
# dependency edges bypasses it: the shadow probes above read the project's method tables, and
|
|
804
|
+
# a memo would swallow that edge for every file after the first.
|
|
805
|
+
#
|
|
806
|
+
# ONE slot, replaced rather than accumulated — see {ExternalAncestorResolution}'s twin for
|
|
807
|
+
# the measurement. A `Scope` hands each analysed file its own discovery index, so an
|
|
808
|
+
# identity-keyed store would pin every file's index, and every RBS definition resolved
|
|
809
|
+
# against it, for the length of the run.
|
|
810
|
+
CORE_STDLIB_ANCESTOR_MEMO_KEY = :__rigor_core_stdlib_ancestor_dispatch__
|
|
811
|
+
private_constant :CORE_STDLIB_ANCESTOR_MEMO_KEY
|
|
812
|
+
|
|
813
|
+
def core_stdlib_memo(environment, scope)
|
|
814
|
+
return nil if Rigor::Analysis::DependencyRecorder.active?
|
|
815
|
+
|
|
816
|
+
discovery = scope.discovery
|
|
817
|
+
slot = Thread.current[CORE_STDLIB_ANCESTOR_MEMO_KEY]
|
|
818
|
+
unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
|
|
819
|
+
slot = [discovery, environment, {}]
|
|
820
|
+
Thread.current[CORE_STDLIB_ANCESTOR_MEMO_KEY] = slot
|
|
821
|
+
end
|
|
822
|
+
slot[2]
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# BFS over the scope's as-written ancestry tables — include edges first, then the
|
|
826
|
+
# superclass, matching the MRO — yielding every ancestor-name candidate. The tables store
|
|
827
|
+
# names AS WRITTEN — `"::API::Base"`, bare `"Base"` — so each hop resolves through the
|
|
828
|
+
# nesting-aware `ancestor_name_candidates` rather than a raw lookup. Deliberately
|
|
829
|
+
# NOT `external_ancestor_name_candidates`: that walk records `ancestry_sources` edges via
|
|
830
|
+
# `record_class_dependency`, which would mislabel a dispatch lookup as an ancestry edge.
|
|
831
|
+
#
|
|
832
|
+
# Issue #1173 — the walk follows include edges too, because a shadow guard that reads only
|
|
833
|
+
# the superclass chain misses the nearer half of the ancestry: `class C < Base; include M;
|
|
834
|
+
# end` chains `C → M → Base`, and a `def`, an outside-the-body mark, or a `pre_eval:` patch
|
|
835
|
+
# on a source `M` all outrank `Base`'s declaration. A candidate that names a project class /
|
|
836
|
+
# module continues the walk ({Scope#known_user_class?} — a module that defines nothing and
|
|
837
|
+
# mixes nothing in still gates what the arm may adopt); an RBS-known or unresolved one is
|
|
838
|
+
# yielded for the caller's guards but its ancestry is the RBS side's business. The include
|
|
839
|
+
# table is read RAW (`scope.discovered_includes`), the same reason the superclass table is:
|
|
840
|
+
# `Scope#includes_of` files `record_class_dependency` on every read — a miss included —
|
|
841
|
+
# and this walk runs for every unresolved call on a project class (`Widget.new`), so the
|
|
842
|
+
# reader would turn each one into an ancestry edge ADR-46 slice 4 keeps at symbol
|
|
843
|
+
# granularity (`dependency_recorder_spec`).
|
|
844
|
+
def each_source_ancestor_candidate(scope, class_name)
|
|
317
845
|
supers = scope.discovered_superclasses
|
|
846
|
+
includes = scope.discovered_includes
|
|
847
|
+
queue = [class_name.to_s]
|
|
318
848
|
seen = {}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
849
|
+
until queue.empty?
|
|
850
|
+
current = queue.shift
|
|
851
|
+
next if current.nil? || seen[current]
|
|
322
852
|
|
|
323
853
|
seen[current] = true
|
|
324
|
-
current
|
|
854
|
+
((includes[current] || []) + [supers[current]].compact).each do |raw|
|
|
855
|
+
scope.ancestor_name_candidates(current, raw).each do |candidate|
|
|
856
|
+
yield candidate
|
|
857
|
+
queue << candidate if scope.known_user_class?(candidate)
|
|
858
|
+
end
|
|
859
|
+
end
|
|
860
|
+
end
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
# The extend-edge twin of `allowed_rbs_complete_ancestor` (manifest `rbs_complete_extends:`).
|
|
864
|
+
# `extend M` lifts M's INSTANCE surface onto the extending class object's singleton, so a
|
|
865
|
+
# singleton call on a Ruby-source class can resolve through a module the class — or one of
|
|
866
|
+
# its discovered superclasses — extends. Returns the first resolved candidate name that a
|
|
867
|
+
# loaded plugin allow-lists, or nil. Same guards as the superclass bridge, minus the
|
|
868
|
+
# RBS-known receiver exit: the direct lookup has already missed by the time this runs, and
|
|
869
|
+
# a class that is BOTH source-defined and RBS-known (`class F` in `sig/` plus `extend T::Sig`
|
|
870
|
+
# in the body) still carries the source edge — the runtime ancestry contains the module
|
|
871
|
+
# either way, so withholding the bridge would leave a real `sig` opaque. A nearer source
|
|
872
|
+
# `def self.x` still shadows any bridged module method.
|
|
873
|
+
def allowed_rbs_complete_extended_module(environment, class_name, method_name, scope,
|
|
874
|
+
call_node = nil)
|
|
875
|
+
return nil if scope.nil?
|
|
876
|
+
|
|
877
|
+
registry = environment&.plugin_registry
|
|
878
|
+
return nil if registry.nil?
|
|
879
|
+
|
|
880
|
+
supers = scope.discovered_superclasses
|
|
881
|
+
extends = scope.discovered_extends
|
|
882
|
+
queue = [class_name.to_s]
|
|
883
|
+
seen = {}
|
|
884
|
+
until queue.empty?
|
|
885
|
+
current = queue.shift
|
|
886
|
+
next if current.nil? || seen[current]
|
|
887
|
+
|
|
888
|
+
seen[current] = true
|
|
889
|
+
# The class's own `def self.x` sits ahead of every `extend` — once it has run.
|
|
890
|
+
# `singleton_def_shadows_call?` orders the def against the call site, so a `def self.sig`
|
|
891
|
+
# written AFTER this `sig {}` does not suppress the bridge.
|
|
892
|
+
return nil if scope.singleton_def_shadows_call?(current, method_name, call_node)
|
|
893
|
+
|
|
894
|
+
resolved = rbs_complete_extended_module_for(current, extends, environment, scope,
|
|
895
|
+
registry, method_name, call_node)
|
|
896
|
+
return nil if resolved.equal?(EXTEND_OWNER_STOP)
|
|
897
|
+
return resolved if resolved
|
|
898
|
+
|
|
899
|
+
raw = supers[current]
|
|
900
|
+
scope.ancestor_name_candidates(current, raw).each { |c| queue << c } if raw
|
|
901
|
+
end
|
|
902
|
+
nil
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
# One walk hop of `allowed_rbs_complete_extended_module`. Each `extend` edge binds to the
|
|
906
|
+
# first resolution candidate that exists at runtime. If that owner DEFINES `method_name`:
|
|
907
|
+
# an allow-listed RBS module is the answer; a project class or a non-allow-listed RBS name
|
|
908
|
+
# owns the call, so the hop returns {EXTEND_OWNER_STOP} and the outer walk must not search
|
|
909
|
+
# later extends or superclasses (a nested `Outer::CustomSig` would otherwise fall through
|
|
910
|
+
# to `T::Sig` and type the call as `nil`). An owner that does not define the method yields
|
|
911
|
+
# to the next extended module — Ruby's singleton ancestry searches every extend in turn.
|
|
912
|
+
def rbs_complete_extended_module_for(current, extends, environment, scope, registry,
|
|
913
|
+
method_name, call_node)
|
|
914
|
+
each_extended_module_name(current, extends, environment) do |mod_name|
|
|
915
|
+
owner = scope.ancestor_name_candidates(current, mod_name).find do |candidate|
|
|
916
|
+
scope.known_user_class?(candidate) ||
|
|
917
|
+
Rigor::Reflection.rbs_class_known?(candidate, environment: environment)
|
|
918
|
+
end
|
|
919
|
+
next if owner.nil?
|
|
920
|
+
next unless extend_owner_defines?(owner, method_name, call_node, scope, environment)
|
|
921
|
+
|
|
922
|
+
project_owned = scope.known_user_class?(owner)
|
|
923
|
+
return owner if !project_owned && registry.rbs_complete_extends?(owner)
|
|
924
|
+
|
|
925
|
+
return EXTEND_OWNER_STOP
|
|
325
926
|
end
|
|
326
927
|
nil
|
|
327
928
|
end
|
|
328
929
|
|
|
930
|
+
def extend_owner_defines?(owner, method_name, call_node, scope, environment)
|
|
931
|
+
return true if scope.instance_def_shadows_call?(owner, method_name, call_node)
|
|
932
|
+
|
|
933
|
+
!lookup_method_on(environment, owner, :instance, method_name).nil?
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
# Sentinel: a nearer `extend` answers `method_name`, so the allow-list must not continue.
|
|
937
|
+
EXTEND_OWNER_STOP = :__rbs_complete_extends_stop__
|
|
938
|
+
private_constant :EXTEND_OWNER_STOP
|
|
939
|
+
|
|
940
|
+
# The module names `current` extends, source table first (`discovered_extends`, stored in
|
|
941
|
+
# singleton-ancestor search order — nearest edge first) then the RBS side
|
|
942
|
+
# (`singleton_extended_modules`, already qualified) — an RBS superclass like `T::Struct`
|
|
943
|
+
# declares `extend T::Props::ClassMethods` in signature, and a source subclass inherits it.
|
|
944
|
+
def each_extended_module_name(current, extends, environment, &)
|
|
945
|
+
(extends[current] || []).each(&)
|
|
946
|
+
(environment&.singleton_extended_modules(current) || []).each(&)
|
|
947
|
+
end
|
|
948
|
+
|
|
329
949
|
# Slice 4 phase 2d substitution map. Zips the class's declared type-parameter names against the
|
|
330
|
-
# receiver's `type_args`. Returns an empty hash when either side is empty or when
|
|
331
|
-
#
|
|
332
|
-
# per the translator's contract.
|
|
950
|
+
# receiver's `type_args`. Returns an empty hash when either side is empty or when the receiver
|
|
951
|
+
# carries MORE arguments than the class declares -- in every such case free variables in the
|
|
952
|
+
# method's return type degrade to `Dynamic[Top]` per the translator's contract.
|
|
953
|
+
#
|
|
954
|
+
# Issue #1121 -- FEWER arguments than parameters is not a disagreement, it is the partial
|
|
955
|
+
# application RBS itself licenses for a class whose trailing parameters declare a default:
|
|
956
|
+
# `Enumerator::Lazy[out E, out R = void]` is spelled `Enumerator::Lazy[Elem]` by the very
|
|
957
|
+
# signature that hands one back (`Enumerable#lazy: () -> Enumerator::Lazy[Elem]`). Withholding
|
|
958
|
+
# the whole map there dropped the element binding on every lazy-enumerator receiver, so the
|
|
959
|
+
# block parameter of `lazy.map { |x| … }` was `Dynamic[top]` and the chain could not recover the
|
|
960
|
+
# element type. The supplied prefix MUST bind in declaration order and the omitted trailing names
|
|
961
|
+
# stay unbound (`Dynamic[top]`), exactly as any other free variable does.
|
|
333
962
|
def build_type_vars(environment, class_name, receiver_args)
|
|
334
|
-
return
|
|
963
|
+
return NO_TYPE_VARS if receiver_args.empty?
|
|
335
964
|
|
|
336
965
|
param_names = Rigor::Reflection.class_type_param_names(class_name, environment: environment)
|
|
337
|
-
return
|
|
338
|
-
return
|
|
966
|
+
return NO_TYPE_VARS if param_names.empty?
|
|
967
|
+
return NO_TYPE_VARS if receiver_args.size > param_names.size
|
|
339
968
|
|
|
340
|
-
param_names.zip(receiver_args).to_h
|
|
969
|
+
param_names.first(receiver_args.size).zip(receiver_args).to_h
|
|
341
970
|
end
|
|
342
971
|
|
|
972
|
+
# The shared empty substitution map: most receivers carry no type arguments, and the translator
|
|
973
|
+
# only ever reads the map (#775).
|
|
974
|
+
NO_TYPE_VARS = {}.freeze
|
|
975
|
+
private_constant :NO_TYPE_VARS
|
|
976
|
+
|
|
343
977
|
# rubocop:disable Metrics/ParameterLists
|
|
344
978
|
def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:,
|
|
345
979
|
method_name: nil, environment: nil, self_type_override: nil,
|
|
@@ -359,13 +993,16 @@ module Rigor
|
|
|
359
993
|
end
|
|
360
994
|
# `self_type_override` lets the user-class fallback path preserve the ORIGINAL receiver as the
|
|
361
995
|
# substitute for `Bases::Self` — so `Kernel#dup: () -> self` resolved through the Object
|
|
362
|
-
# fallback returns the caller's type, not Object.
|
|
996
|
+
# fallback returns the caller's type, not Object. `dispatch_one` also routes the receiver's
|
|
997
|
+
# type-argument-bearing projection through it ({SelfSubstitute}, #1092).
|
|
363
998
|
self_type = self_type_override || resolved_self_type
|
|
364
999
|
|
|
365
1000
|
candidates = OverloadSelector.select_candidates(
|
|
366
1001
|
method_definition,
|
|
367
1002
|
arg_types: args,
|
|
368
|
-
|
|
1003
|
+
# A `Dynamic` self (#1092) is a return-side answer; overload selection and ReceiverAffinity
|
|
1004
|
+
# read the static facet, as they did before the substitute carried the wrapping.
|
|
1005
|
+
self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
|
|
369
1006
|
instance_type: instance_type,
|
|
370
1007
|
type_vars: type_vars,
|
|
371
1008
|
block_required: !block_type.nil?,
|
|
@@ -377,6 +1014,7 @@ module Rigor
|
|
|
377
1014
|
record_dispatch_provenance(method_definition, candidates.first, scope, call_node, call_site)
|
|
378
1015
|
join_candidate_returns(
|
|
379
1016
|
candidates,
|
|
1017
|
+
method_definition: method_definition,
|
|
380
1018
|
self_type: self_type, instance_type: instance_type, type_vars: type_vars,
|
|
381
1019
|
args: args, block_type: block_type, scope: scope, call_node: call_node, call_site: call_site,
|
|
382
1020
|
alias_expander: environment.rbs_loader
|
|
@@ -416,17 +1054,20 @@ module Rigor
|
|
|
416
1054
|
# A candidate whose return does not translate leaves the join incomplete — decline (fail-soft
|
|
417
1055
|
# to Dynamic downstream) rather than answer a join missing an arm the runtime can take.
|
|
418
1056
|
# rubocop:disable-next Metrics/ParameterLists
|
|
419
|
-
def join_candidate_returns(candidates, self_type:, instance_type:, type_vars:, args:,
|
|
420
|
-
scope:, call_node:, call_site:, alias_expander: nil)
|
|
1057
|
+
def join_candidate_returns(candidates, method_definition:, self_type:, instance_type:, type_vars:, args:,
|
|
1058
|
+
block_type:, scope:, call_node:, call_site:, alias_expander: nil)
|
|
421
1059
|
returns = candidates.map do |method_type|
|
|
422
1060
|
full_type_vars = compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
|
|
423
|
-
RbsTypeTranslator.translate(
|
|
1061
|
+
returned = RbsTypeTranslator.translate(
|
|
424
1062
|
method_type.type.return_type,
|
|
425
1063
|
self_type: self_type,
|
|
426
1064
|
instance_type: instance_type,
|
|
427
1065
|
type_vars: full_type_vars,
|
|
428
1066
|
alias_expander: alias_expander
|
|
429
1067
|
)
|
|
1068
|
+
next returned unless combining_overload?(method_definition, method_type, call_site)
|
|
1069
|
+
|
|
1070
|
+
class_level_sum(returned, method_type, args)
|
|
430
1071
|
end
|
|
431
1072
|
return returns.first if returns.size == 1
|
|
432
1073
|
return nil if returns.any?(&:nil?)
|
|
@@ -438,11 +1079,100 @@ module Rigor
|
|
|
438
1079
|
end
|
|
439
1080
|
|
|
440
1081
|
def compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
|
|
441
|
-
vars = compose_block_type_vars(method_type, type_vars, block_type
|
|
1082
|
+
vars = compose_block_type_vars(method_type, type_vars, block_type, args,
|
|
1083
|
+
scope: scope, call_node: call_node, call_site: call_site)
|
|
442
1084
|
compose_arg_type_vars(method_type, vars, args, scope: scope, call_node: call_node,
|
|
443
1085
|
call_site: call_site)
|
|
444
1086
|
end
|
|
445
1087
|
|
|
1088
|
+
# Whether the overload is one `Enumerable` declares for `sum`, whose declared return the tier reads
|
|
1089
|
+
# at class level ({#class_level_sum}). Every overload spells its return as the sides the method adds
|
|
1090
|
+
# together, as a union or as one variable that covers both: `() -> (E | Integer)`,
|
|
1091
|
+
# `[T] () { (E) -> T } -> (Integer | T)`, `[T] (?T) -> (E | T)` and `[U] (?U) { (E) -> U } -> U`.
|
|
1092
|
+
# A value is not closed under that addition, so the value-pinned bindings of `E` (from the
|
|
1093
|
+
# receiver), `T` (from the argument, issue #303) and the block's type do not describe the result:
|
|
1094
|
+
# `[1, 2].each.sum(0.0)` is `3.0`, which `0.0 | 1 | 2` misses. `Array#fetch: [T] (int, T default)
|
|
1095
|
+
# -> (E | T)` is spelled the same way but
|
|
1096
|
+
# returns the default object itself, so the signature alone cannot tell the two apart and the
|
|
1097
|
+
# declaring module does. A class that declares its own `sum` states its own contract and keeps it.
|
|
1098
|
+
def combining_overload?(method_definition, method_type, call_site)
|
|
1099
|
+
return false unless call_site[1] == :sum
|
|
1100
|
+
|
|
1101
|
+
type_def = OptimisticOrigin.matching_type_def(method_definition, method_type)
|
|
1102
|
+
!type_def.nil? && type_def.defined_in.to_s.delete_prefix("::") == "Enumerable"
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
# Apart from the range shortcut ({#range_coerced_seed?}), CRuby's `enum_sum` adds each value to an
|
|
1106
|
+
# accumulator that starts at the seed. Between two of these classes `+` answers the later one
|
|
1107
|
+
# (`1 + 0.5` and `0.5 + 1` are Floats, `1 + 1r` is a Rational), so the accumulator's class only ever
|
|
1108
|
+
# moves up this order.
|
|
1109
|
+
SUM_PROMOTION_RANKS = { "Integer" => 0, "Rational" => 1, "Float" => 2, "Complex" => 3 }.freeze
|
|
1110
|
+
private_constant :SUM_PROMOTION_RANKS
|
|
1111
|
+
|
|
1112
|
+
# CRuby's seed when the call passes none.
|
|
1113
|
+
SUM_DEFAULT_SEED = ["Integer"].freeze
|
|
1114
|
+
private_constant :SUM_DEFAULT_SEED
|
|
1115
|
+
|
|
1116
|
+
# The seeds CRuby's integer-range shortcut adds to with plain `+` ({#range_coerced_seed?}).
|
|
1117
|
+
RANGE_ADDED_SEEDS = Set["Integer", "Float"].freeze
|
|
1118
|
+
private_constant :RANGE_ADDED_SEEDS
|
|
1119
|
+
|
|
1120
|
+
# The class-level reading of a `sum` overload's translated return, or `Dynamic[top]` when a member
|
|
1121
|
+
# widens to none of the value classes {#value_classes} admits. A class, not a value, is what the
|
|
1122
|
+
# addition keeps closed: `sum` over `0.0` and `1 | 2` is `3.0`, a Float, and over `1.5 | 2.5` is
|
|
1123
|
+
# `4.0`.
|
|
1124
|
+
#
|
|
1125
|
+
# The union of the members' classes would still read wider than the runtime, because the seed
|
|
1126
|
+
# promotes every value it absorbs: `ints.each.sum(0.0)` is always a Float, and `Float | Integer`
|
|
1127
|
+
# fires `def.return-type-mismatch` against a declared `-> Float` on correct code. So each class is
|
|
1128
|
+
# taken as the class the accumulator reaches from each seed class ({#promoted_class}), and the seed
|
|
1129
|
+
# itself stays for a receiver that yields nothing: `ints.each.sum(0.0)` reads `Float`, and
|
|
1130
|
+
# `floats.each.sum(0)` reads `Float | Integer`, whose Integer is the empty receiver's `0`.
|
|
1131
|
+
def class_level_sum(type, method_type, args)
|
|
1132
|
+
return nil if type.nil?
|
|
1133
|
+
|
|
1134
|
+
members = value_classes(type)
|
|
1135
|
+
return Type::Combinator.untyped if members.nil?
|
|
1136
|
+
|
|
1137
|
+
seeds = sum_seed_classes(method_type, args)
|
|
1138
|
+
return Type::Combinator.untyped if seeds.nil? || range_coerced_seed?(method_type, seeds)
|
|
1139
|
+
|
|
1140
|
+
reached = seeds.flat_map { |seed| members.map { |member| promoted_class(seed, member) } }
|
|
1141
|
+
Type::Combinator.union(*(seeds | reached).map { |name| Type::Combinator.nominal_of(name) })
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
# Whether CRuby may skip the accumulator and read the seed as a Float. With no block and a seed that
|
|
1145
|
+
# is not a Float, `enum_sum` sums a range with Integer endpoints by Gauss's formula and adds the
|
|
1146
|
+
# result to the seed. An Integer seed takes plain `+`; any other goes through `Integer#coerce`, which
|
|
1147
|
+
# converts it with `Float()`: `(1..3).sum(0r)` is `6.0` and `(1..3).sum("1.5")` is `7.5`. Any object
|
|
1148
|
+
# that answers `begin`, `end` and `exclude_end?` takes the same path, so the receiver's class cannot
|
|
1149
|
+
# rule it out.
|
|
1150
|
+
def range_coerced_seed?(method_type, seeds)
|
|
1151
|
+
method_type.block.nil? && seeds.any? { |seed| !RANGE_ADDED_SEEDS.include?(seed) }
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1154
|
+
# The classes of the value `sum` starts from: the argument when the overload takes one and the call
|
|
1155
|
+
# passes it, and CRuby's `0` otherwise.
|
|
1156
|
+
def sum_seed_classes(method_type, args)
|
|
1157
|
+
fun = method_type.type
|
|
1158
|
+
return SUM_DEFAULT_SEED if args.empty? || !fun.respond_to?(:required_positionals)
|
|
1159
|
+
return SUM_DEFAULT_SEED if fun.required_positionals.empty? && fun.optional_positionals.empty?
|
|
1160
|
+
|
|
1161
|
+
value_classes(args.first)
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1164
|
+
# The class the accumulator reaches when a `member` value is added to a `seed`-class one. `+` does not
|
|
1165
|
+
# add a String and a number (`"" + 1` and `1 + ""` raise), so such a pair keeps the member's class,
|
|
1166
|
+
# which reads wider than the runtime rather than narrower. The range shortcut that does convert a
|
|
1167
|
+
# String seed never reaches here ({#range_coerced_seed?}).
|
|
1168
|
+
def promoted_class(seed, member)
|
|
1169
|
+
seed_rank = SUM_PROMOTION_RANKS[seed]
|
|
1170
|
+
member_rank = SUM_PROMOTION_RANKS[member]
|
|
1171
|
+
return member if seed_rank.nil? || member_rank.nil?
|
|
1172
|
+
|
|
1173
|
+
seed_rank > member_rank ? seed : member
|
|
1174
|
+
end
|
|
1175
|
+
|
|
446
1176
|
# Record the `void → top` recovery when the selected overload declares `-> void` and both `scope` and
|
|
447
1177
|
# `call_node` are present (the direct-dispatch path). `void_site` is the `[class_name, method_name,
|
|
448
1178
|
# kind]` triple the {VoidOrigin} carries.
|
|
@@ -489,13 +1219,160 @@ module Rigor
|
|
|
489
1219
|
# block return type at the same call site. Anything outside this exact shape (no block clause,
|
|
490
1220
|
# an `untyped` block, a non-variable block return type, a variable not declared in
|
|
491
1221
|
# `type_params`) returns the original `type_vars` so fallbacks stay consistent.
|
|
492
|
-
|
|
1222
|
+
#
|
|
1223
|
+
# The block alone does not decide a variable that a parameter's type also names once the call
|
|
1224
|
+
# passes an argument. `Enumerable#sum: [U] (?U) { (E) -> U } -> U` adds the block's values to the
|
|
1225
|
+
# argument, `Enumerable#inject: [A] (A initial) { (A, E) -> A } -> A` returns the argument for an
|
|
1226
|
+
# empty receiver, and `Hash#transform_keys: [K2] (hash[_Key, K2]) { (K) -> K2 } -> Hash[K2, V]`
|
|
1227
|
+
# takes a mapping hit's value without yielding. `Dynamic[block_type]` would not do: a `Dynamic`
|
|
1228
|
+
# receiver dispatches through its static facet and answers exactly, so a facet that misses the
|
|
1229
|
+
# runtime value is wrong one call later (`(h.sum(0.0) { |_k, v| v } / h.size).nan?` read
|
|
1230
|
+
# `Integer#nan?`). Nor would joining in the argument as it stands, since
|
|
1231
|
+
# `[1, 2].each.sum(0.0) { |x| x }` is `3.0`, which neither side contains. The variable is bound
|
|
1232
|
+
# to {#shared_value_class} where both sides have one, and to `Dynamic[top]` otherwise. The key
|
|
1233
|
+
# stays in the map either way so {#compose_arg_type_vars} does not bind the variable from the
|
|
1234
|
+
# argument alone.
|
|
1235
|
+
def compose_block_type_vars(method_type, type_vars, block_type, args, scope:, call_node:, call_site:)
|
|
493
1236
|
return type_vars if block_type.nil?
|
|
494
1237
|
|
|
495
1238
|
block_var_name = method_type_block_return_variable(method_type)
|
|
496
1239
|
return type_vars if block_var_name.nil?
|
|
1240
|
+
return type_vars.merge(block_var_name => block_type) unless
|
|
1241
|
+
argument_reaches_variable?(method_type, block_var_name, args)
|
|
497
1242
|
|
|
498
|
-
|
|
1243
|
+
shared = shared_value_class(method_type, block_var_name, args, block_type, call_node)
|
|
1244
|
+
bound = shared && arg_binding_permitted?(scope, call_node, call_site) ? shared : Type::Combinator.untyped
|
|
1245
|
+
type_vars.merge(block_var_name => bound)
|
|
1246
|
+
end
|
|
1247
|
+
|
|
1248
|
+
# Whether an argument the call passes may land in a parameter whose type names `name`, anywhere
|
|
1249
|
+
# inside it (`hash[_Key, K2]` names `K2`). The argument count is not matched against the
|
|
1250
|
+
# parameter list: a `*splat` argument stands for any number of arguments, and keyword arguments
|
|
1251
|
+
# arrive as one more entry in `args`, so any argument counts as reaching every parameter.
|
|
1252
|
+
def argument_reaches_variable?(method_type, name, args)
|
|
1253
|
+
return false if args.empty?
|
|
1254
|
+
|
|
1255
|
+
method_type.type.each_param.any? { |param| mentions_variable?(param.type, name) }
|
|
1256
|
+
end
|
|
1257
|
+
|
|
1258
|
+
# The one value class that the arguments landing in `name`'s parameters and the block's type all
|
|
1259
|
+
# share, or nil to leave the variable `Dynamic[top]`. A class, not a value, because a combining
|
|
1260
|
+
# method keeps a class closed and not a value: `[1, 2].each.sum(1) { |x| -x }` is `-2`, which
|
|
1261
|
+
# neither `1` nor `-1 | -2` contains. RBS reads `[U] (?U) { (E) -> U } -> U` the same way, as a
|
|
1262
|
+
# `U` that covers the argument and the block alike. One class and not a union of several, because
|
|
1263
|
+
# `sum` absorbs: over Integers, `sum(0.0)` answers a Float every time, and `Float | Integer` would
|
|
1264
|
+
# fire `def.return-type-mismatch` against a declared `-> Float`.
|
|
1265
|
+
#
|
|
1266
|
+
# The block's type is one typing of its body, so it covers every call only when nothing the block
|
|
1267
|
+
# receives depends on the variable ({#block_receives_variable?}). The argument binding's gate
|
|
1268
|
+
# applies ({#arg_binding_permitted?}), since this reads the argument. The positions must be static
|
|
1269
|
+
# ({#arguments_at_variable}), and every side must widen to a class ({#value_classes}). The block's
|
|
1270
|
+
# type is trusted as far as the engine trusts it, so a hash filled through an alias, which reads
|
|
1271
|
+
# narrower than it is, binds its narrower class here too.
|
|
1272
|
+
def shared_value_class(method_type, name, args, block_type, call_node)
|
|
1273
|
+
return nil if block_receives_variable?(method_type.block, name)
|
|
1274
|
+
|
|
1275
|
+
reaching = arguments_at_variable(method_type, name, args, call_node)
|
|
1276
|
+
return nil if reaching.nil?
|
|
1277
|
+
|
|
1278
|
+
sides = [*reaching, block_type].map { |type| value_classes(type) }
|
|
1279
|
+
return nil unless sides.all?
|
|
1280
|
+
|
|
1281
|
+
classes = sides.flatten(1).uniq
|
|
1282
|
+
classes.size == 1 ? Type::Combinator.nominal_of(classes.first) : nil
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
# Whether the block's parameters or its `self` name the variable, as `inject`'s accumulator
|
|
1286
|
+
# (`{ (A, E) -> A }`) and `produce`'s previous element (`{ (T prev) -> T }`) do. Such a parameter
|
|
1287
|
+
# holds the argument on the first call and the block's own result after it, and the pass that
|
|
1288
|
+
# typed the block typed it once, as whatever reached it: the RBS probe leaves it untyped, but
|
|
1289
|
+
# `IteratorDispatch` hands an Array receiver's `inject` the seed, and a `&:+` block then reads
|
|
1290
|
+
# `0.+`, so `[1.5, 2].inject(0, &:+)`, which is `3.5`, would bind `Integer`.
|
|
1291
|
+
def block_receives_variable?(block, name)
|
|
1292
|
+
return true if block.self_type && mentions_variable?(block.self_type, name)
|
|
1293
|
+
|
|
1294
|
+
block.type.each_param.any? { |param| mentions_variable?(param.type, name) }
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
# The arguments that land in a positional parameter whose whole type is `name`, or nil when the
|
|
1298
|
+
# call's positions or the signature's shape leave that open. The call must pass plain positional
|
|
1299
|
+
# arguments: after a `*splat` that turns out empty, the next argument lands one parameter earlier,
|
|
1300
|
+
# and a forwarded `...` or keyword arguments may land anywhere. The signature must name `name` only
|
|
1301
|
+
# as a whole leading positional parameter's type, and have no trailing positional parameter at
|
|
1302
|
+
# all. One that names the variable takes its argument from the end of the list, which pairing the
|
|
1303
|
+
# leading parameters with the arguments in order would miss; one that does not is declined too,
|
|
1304
|
+
# conservatively.
|
|
1305
|
+
def arguments_at_variable(method_type, name, args, call_node)
|
|
1306
|
+
return nil unless plain_positional_arguments?(call_node, args.size)
|
|
1307
|
+
|
|
1308
|
+
fun = method_type.type
|
|
1309
|
+
return nil unless fun.respond_to?(:trailing_positionals) && fun.trailing_positionals.empty?
|
|
1310
|
+
return nil if named_outside_positionals?(fun, name)
|
|
1311
|
+
|
|
1312
|
+
positionals = fun.required_positionals + fun.optional_positionals
|
|
1313
|
+
positionals.zip(args).each_with_object([]) do |(param, arg), reaching|
|
|
1314
|
+
type = param.type
|
|
1315
|
+
next unless mentions_variable?(type, name)
|
|
1316
|
+
return nil unless type.is_a?(RBS::Types::Variable)
|
|
1317
|
+
|
|
1318
|
+
reaching << arg unless arg.nil?
|
|
1319
|
+
end
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
def plain_positional_arguments?(call_node, count)
|
|
1323
|
+
return false unless call_node.is_a?(Prism::CallNode)
|
|
1324
|
+
|
|
1325
|
+
arguments = call_node.arguments&.arguments || EMPTY_ARGUMENT_NODES
|
|
1326
|
+
arguments.size == count && arguments.none? do |argument|
|
|
1327
|
+
case argument
|
|
1328
|
+
when Prism::SplatNode, Prism::ForwardingArgumentsNode, Prism::KeywordHashNode then true
|
|
1329
|
+
else false
|
|
1330
|
+
end
|
|
1331
|
+
end
|
|
1332
|
+
end
|
|
1333
|
+
|
|
1334
|
+
def named_outside_positionals?(fun, name)
|
|
1335
|
+
others = [fun.rest_positionals, fun.rest_keywords].compact +
|
|
1336
|
+
fun.required_keywords.values + fun.optional_keywords.values
|
|
1337
|
+
others.any? { |param| mentions_variable?(param.type, name) }
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1340
|
+
# The class names `type`'s union members widen to, or nil when a member widens to none of
|
|
1341
|
+
# {CLOSED_VALUE_CLASSES}.
|
|
1342
|
+
def value_classes(type)
|
|
1343
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
1344
|
+
classes = members.map { |member| value_class(member) }
|
|
1345
|
+
classes.all? ? classes : nil
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
# A literal widens to its value's class, a bounded number to `Integer` or `Float`, and a refinement
|
|
1349
|
+
# or a difference to its base's class. Everything else declines: a generic class (`[] + [:a]`
|
|
1350
|
+
# concatenates elements rather than keeping either side's), a carrier with no class (a tuple, a
|
|
1351
|
+
# hash shape, `Dynamic`), and `nil` or `NilClass`, whose arm would fire
|
|
1352
|
+
# `call.possible-nil-receiver` where `Array#max` and `#first` bet on a non-empty receiver.
|
|
1353
|
+
def value_class(type)
|
|
1354
|
+
case type
|
|
1355
|
+
when Type::Nominal then closed_value_class(type.class_name)
|
|
1356
|
+
when Type::Constant then closed_value_class(type.value.class.name)
|
|
1357
|
+
when Type::IntegerRange then "Integer"
|
|
1358
|
+
when Type::FloatRange then "Float"
|
|
1359
|
+
when Type::Refined, Type::Difference then value_class(type.base)
|
|
1360
|
+
end
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
def closed_value_class(class_name)
|
|
1364
|
+
name = class_name&.delete_prefix("::")
|
|
1365
|
+
CLOSED_VALUE_CLASSES.include?(name) ? name : nil
|
|
1366
|
+
end
|
|
1367
|
+
|
|
1368
|
+
# A signature nested past {RETURN_TYPE_UNWRAP_DEPTH} counts as naming the variable, which is the
|
|
1369
|
+
# untyped answer.
|
|
1370
|
+
def mentions_variable?(type, name, depth = 0)
|
|
1371
|
+
return true if depth > RETURN_TYPE_UNWRAP_DEPTH
|
|
1372
|
+
return type.name == name if type.is_a?(::RBS::Types::Variable)
|
|
1373
|
+
return false unless type.respond_to?(:each_type)
|
|
1374
|
+
|
|
1375
|
+
type.each_type.any? { |child| mentions_variable?(child, name, depth + 1) }
|
|
499
1376
|
end
|
|
500
1377
|
|
|
501
1378
|
# Issue #303 — bind method-level type parameters from ARGUMENT positions, layering on top of the
|
|
@@ -505,7 +1382,8 @@ module Rigor
|
|
|
505
1382
|
# Envelope, deliberately the narrowest sound shape:
|
|
506
1383
|
#
|
|
507
1384
|
# * only a positional parameter (required or optional) whose declared type is EXACTLY
|
|
508
|
-
# `RBS::Types::Variable
|
|
1385
|
+
# `RBS::Types::Variable`, plus the one container shape issue #834 admits (see
|
|
1386
|
+
# {#range_element_binding}) — no general container walk, so `(Array[T]) -> T` still degrades;
|
|
509
1387
|
# * only names the SELECTED overload declares in its own `type_params` (a class-level variable
|
|
510
1388
|
# keeps its receiver-derived binding);
|
|
511
1389
|
# * an existing key wins, so the receiver and the block return type both outrank an argument;
|
|
@@ -534,14 +1412,42 @@ module Rigor
|
|
|
534
1412
|
positionals.zip(args).each_with_object({}) do |(param, arg), bindings|
|
|
535
1413
|
next if arg.nil?
|
|
536
1414
|
|
|
537
|
-
name =
|
|
1415
|
+
name, bound = param_binding(param, arg, declared, type_vars)
|
|
538
1416
|
next if name.nil?
|
|
539
|
-
next if no_static_evidence?(arg)
|
|
540
1417
|
|
|
541
|
-
|
|
1418
|
+
bound = Type::Combinator.widen_value_pinned(bound) if upper_bounded?(method_type, name)
|
|
1419
|
+
bindings[name] = bindings.key?(name) ? Type::Combinator.union(bindings[name], bound) : bound
|
|
1420
|
+
end
|
|
1421
|
+
end
|
|
1422
|
+
|
|
1423
|
+
# Issue #1347 — a variable that declares an upper bound (`[T < X]`) binds the argument widened off its
|
|
1424
|
+
# value-pinned members. The bound constrains a class, and `Rational#*: [T < Numeric](T) -> T` returns a
|
|
1425
|
+
# value of the argument's class, not the argument, so `r * 0.5` read the literal `0.5`. An unbounded
|
|
1426
|
+
# variable keeps the literal (`Ractor.make_shareable("x")` is `"x"`); a bounded identity return
|
|
1427
|
+
# (`String#setbyte`) gives its literal up. Any bound counts: `upper_bound` answers only a class, singleton
|
|
1428
|
+
# or interface bound, so an alias, union, intersection or optional bound is read through
|
|
1429
|
+
# `upper_bound_type` where the rbs version has it.
|
|
1430
|
+
def upper_bounded?(method_type, name)
|
|
1431
|
+
method_type.type_params.any? do |type_param|
|
|
1432
|
+
type_param.name == name &&
|
|
1433
|
+
(type_param.respond_to?(:upper_bound_type) ? type_param.upper_bound_type : type_param.upper_bound)
|
|
542
1434
|
end
|
|
543
1435
|
end
|
|
544
1436
|
|
|
1437
|
+
# The `(variable name, bound type)` a positional parameter contributes, or {NO_BINDING} when it
|
|
1438
|
+
# contributes none. The bare-variable shape is tried first because it is the overwhelmingly
|
|
1439
|
+
# common one; the `Range[A]` shape is reached only when the parameter is not a bare variable.
|
|
1440
|
+
def param_binding(param, arg, declared, type_vars)
|
|
1441
|
+
name = variable_param_name(param, declared, type_vars)
|
|
1442
|
+
unless name.nil?
|
|
1443
|
+
return NO_BINDING if no_static_evidence?(arg)
|
|
1444
|
+
|
|
1445
|
+
return [name, arg]
|
|
1446
|
+
end
|
|
1447
|
+
|
|
1448
|
+
range_element_binding(param, arg, declared, type_vars)
|
|
1449
|
+
end
|
|
1450
|
+
|
|
545
1451
|
def declared_type_param_names(method_type)
|
|
546
1452
|
params = method_type.respond_to?(:type_params) ? method_type.type_params : nil
|
|
547
1453
|
return EMPTY_TYPE_PARAM_NAMES if params.nil? || params.empty?
|
|
@@ -562,6 +1468,57 @@ module Rigor
|
|
|
562
1468
|
name
|
|
563
1469
|
end
|
|
564
1470
|
|
|
1471
|
+
# Issues #834 / #862 — the one container position the envelope admits: a `Range[A]` parameter
|
|
1472
|
+
# against a Range argument that names its own element. `Comparable#clamp: [A] (Range[A]) ->
|
|
1473
|
+
# (self | A)` otherwise leaves `A` unbound and `i.clamp(1..9)` answers `Dynamic[top] | Integer`,
|
|
1474
|
+
# even though the argument names the element outright. It stays Range-only because the
|
|
1475
|
+
# justification does not generalise: a Range is immutable and its element type is fixed at
|
|
1476
|
+
# construction, whereas an `Array[T]` argument's carrier may have been widened long before the
|
|
1477
|
+
# call reached here.
|
|
1478
|
+
def range_element_binding(param, arg, declared, type_vars)
|
|
1479
|
+
declared_type = param.type
|
|
1480
|
+
return NO_BINDING unless declared_type.is_a?(RBS::Types::ClassInstance)
|
|
1481
|
+
return NO_BINDING unless RANGE_TYPE_NAMES.include?(declared_type.name.to_s)
|
|
1482
|
+
return NO_BINDING unless declared_type.args.size == 1
|
|
1483
|
+
|
|
1484
|
+
element = declared_type.args.first
|
|
1485
|
+
return NO_BINDING unless element.is_a?(RBS::Types::Variable)
|
|
1486
|
+
return NO_BINDING unless declared.include?(element.name)
|
|
1487
|
+
return NO_BINDING if type_vars.key?(element.name)
|
|
1488
|
+
|
|
1489
|
+
bound = range_argument_element(arg)
|
|
1490
|
+
bound.nil? ? NO_BINDING : [element.name, bound]
|
|
1491
|
+
end
|
|
1492
|
+
|
|
1493
|
+
# The element a Range argument names, or nil when it names none.
|
|
1494
|
+
#
|
|
1495
|
+
# Two carriers qualify. A `Constant<Range>` contributes its endpoints lifted to their classes
|
|
1496
|
+
# ({RangeConstant.element_type}), not the two values, so `clamp(1..9)` answers `Integer` rather
|
|
1497
|
+
# than a `1 | 9` the runtime contradicts for every receiver already inside the bracket. A
|
|
1498
|
+
# `Nominal[Range, [T]]` (issue #862 — `1..ARGV.size` has no literal endpoint to read, so
|
|
1499
|
+
# `ExpressionTyper` hands back the nominal carrier) contributes `T` itself.
|
|
1500
|
+
#
|
|
1501
|
+
# A nominal carrier qualifies only when `T` is a Nominal or a union of Nominals. `untyped`, a
|
|
1502
|
+
# `Dynamic`, or a still-unbound type variable would launder an unknown into the result — the
|
|
1503
|
+
# caller's `self | A` would read as `self | unknown` while claiming to be inferred — so those
|
|
1504
|
+
# keep degrading as an unbound variable does.
|
|
1505
|
+
def range_argument_element(arg)
|
|
1506
|
+
return RangeConstant.element_type(arg) unless arg.is_a?(Type::Nominal)
|
|
1507
|
+
return nil unless RANGE_TYPE_NAMES.include?(arg.class_name)
|
|
1508
|
+
return nil unless arg.type_args.size == 1
|
|
1509
|
+
|
|
1510
|
+
element = arg.type_args.first
|
|
1511
|
+
nominal_only?(element) ? element : nil
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
def nominal_only?(type)
|
|
1515
|
+
case type
|
|
1516
|
+
when Type::Nominal then true
|
|
1517
|
+
when Type::Union then type.members.all?(Type::Nominal)
|
|
1518
|
+
else false
|
|
1519
|
+
end
|
|
1520
|
+
end
|
|
1521
|
+
|
|
565
1522
|
# `Dynamic[top]` is the engine's "we could not tell" answer, so binding a variable to it would
|
|
566
1523
|
# dress up an absence of evidence as an inference. The variable stays unbound and degrades as it
|
|
567
1524
|
# did before, which is the same value anyway.
|
|
@@ -625,11 +1582,11 @@ module Rigor
|
|
|
625
1582
|
|
|
626
1583
|
# ----- block parameter probe (Phase C sub-phase 1) -----
|
|
627
1584
|
|
|
628
|
-
def probe_block_param_types(receiver:, method_name:, args:, environment:)
|
|
1585
|
+
def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil)
|
|
629
1586
|
args ||= []
|
|
630
1587
|
case receiver
|
|
631
|
-
when Type::Union then probe_block_param_types_union(receiver, method_name, args, environment)
|
|
632
|
-
else probe_block_param_types_one(receiver, method_name, args, environment)
|
|
1588
|
+
when Type::Union then probe_block_param_types_union(receiver, method_name, args, environment, scope)
|
|
1589
|
+
else probe_block_param_types_one(receiver, method_name, args, environment, scope)
|
|
633
1590
|
end
|
|
634
1591
|
end
|
|
635
1592
|
|
|
@@ -637,9 +1594,9 @@ module Rigor
|
|
|
637
1594
|
# member resolves the same arity and types (otherwise the call sites would have to thread
|
|
638
1595
|
# per-member binders, which the slice does not support yet). Mismatches degrade to the empty
|
|
639
1596
|
# array so the binder defaults all params to Dynamic[Top].
|
|
640
|
-
def probe_block_param_types_union(receiver, method_name, args, environment)
|
|
1597
|
+
def probe_block_param_types_union(receiver, method_name, args, environment, scope)
|
|
641
1598
|
results = receiver.members.map do |member|
|
|
642
|
-
probe_block_param_types_one(member, method_name, args, environment)
|
|
1599
|
+
probe_block_param_types_one(member, method_name, args, environment, scope)
|
|
643
1600
|
end
|
|
644
1601
|
return [] if results.empty?
|
|
645
1602
|
return [] unless results.all? { |r| r == results.first }
|
|
@@ -647,12 +1604,12 @@ module Rigor
|
|
|
647
1604
|
results.first
|
|
648
1605
|
end
|
|
649
1606
|
|
|
650
|
-
def probe_block_param_types_one(receiver, method_name, args, environment)
|
|
1607
|
+
def probe_block_param_types_one(receiver, method_name, args, environment, scope)
|
|
651
1608
|
descriptor = receiver_descriptor(receiver)
|
|
652
1609
|
return [] unless descriptor
|
|
653
1610
|
|
|
654
1611
|
class_name, kind, receiver_args = descriptor
|
|
655
|
-
method_definition = lookup_method(environment, class_name, kind, method_name)
|
|
1612
|
+
method_definition = lookup_method(environment, class_name, kind, method_name, scope)
|
|
656
1613
|
return [] unless method_definition
|
|
657
1614
|
|
|
658
1615
|
type_vars = build_type_vars(environment, class_name, receiver_args)
|
|
@@ -662,14 +1619,20 @@ module Rigor
|
|
|
662
1619
|
kind: kind,
|
|
663
1620
|
args: args,
|
|
664
1621
|
type_vars: type_vars,
|
|
665
|
-
environment: environment
|
|
1622
|
+
environment: environment,
|
|
1623
|
+
receiver: receiver,
|
|
1624
|
+
receiver_args: receiver_args,
|
|
1625
|
+
method_name: method_name
|
|
666
1626
|
)
|
|
667
1627
|
rescue StandardError
|
|
668
1628
|
[]
|
|
669
1629
|
end
|
|
670
1630
|
|
|
1631
|
+
# rubocop:disable Metrics/ParameterLists
|
|
671
1632
|
def extract_block_param_types(method_definition, class_name:, kind:, args:, type_vars:,
|
|
672
|
-
environment: nil
|
|
1633
|
+
environment: nil, receiver: nil, receiver_args: [],
|
|
1634
|
+
method_name: nil)
|
|
1635
|
+
# rubocop:enable Metrics/ParameterLists
|
|
673
1636
|
instance_type = Type::Combinator.nominal_of(class_name)
|
|
674
1637
|
self_type =
|
|
675
1638
|
case kind
|
|
@@ -677,10 +1640,24 @@ module Rigor
|
|
|
677
1640
|
else instance_type
|
|
678
1641
|
end
|
|
679
1642
|
|
|
1643
|
+
# Issue #1130 — a block parameter that receives `self` (`Object#tap` yields it) must see the
|
|
1644
|
+
# receiver's type arguments through the SAME {SelfSubstitute} verdict the return path applies
|
|
1645
|
+
# (#1092), so `ints.tap { |a| }` binds `a` to `Array[Integer]` rather than the raw `Array`.
|
|
1646
|
+
# The block path reuses only the keep-vs-degrade verdict, NOT the return path's value-pin
|
|
1647
|
+
# widening: the block parameter is a destructure source, so the receiver's OWN type arguments
|
|
1648
|
+
# (pinned constants included — `[1, 2].tap { |a, b| }` auto-splats the element union
|
|
1649
|
+
# `1 | 2` per slot, the parity an explicit `a, b = [1, 2]` gets) are the substitution the
|
|
1650
|
+
# binder should see. A verdict decline (an element-changing mutator like `map!`) keeps the
|
|
1651
|
+
# raw nominal, so that receiver's block parameter still arrives without its type arguments.
|
|
1652
|
+
substitute = SelfSubstitute.for(receiver, receiver_args, method_name, args, nil)
|
|
1653
|
+
self_type = Type::Combinator.nominal_of(class_name, type_args: receiver_args) if substitute
|
|
1654
|
+
|
|
680
1655
|
method_type = OverloadSelector.select(
|
|
681
1656
|
method_definition,
|
|
682
1657
|
arg_types: args,
|
|
683
|
-
|
|
1658
|
+
# Overload selection reads a `Dynamic` self's static facet, mirroring the return path;
|
|
1659
|
+
# the substitution verdict here is built from the receiver's type arguments alone.
|
|
1660
|
+
self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
|
|
684
1661
|
instance_type: instance_type,
|
|
685
1662
|
type_vars: type_vars,
|
|
686
1663
|
block_required: true,
|