rigortype 0.3.9 → 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/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 +127 -4
- data/docs/handbook/03-narrowing.md +95 -10
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/07-rbs-and-extended.md +16 -11
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/manual/02-cli-reference.md +63 -2
- data/docs/manual/03-configuration.md +7 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/16-rbs-extended-annotations.md +40 -7
- 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 +243 -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 +110 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
- 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 +212 -25
- 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 +1 -1
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -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 +36 -2
- data/lib/rigor/analysis/check_rules.rb +520 -49
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_session.rb +36 -4
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
- data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +20 -11
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
- data/lib/rigor/analysis/runner.rb +278 -20
- 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 +53 -10
- data/lib/rigor/bleeding_edge.rb +0 -2
- data/lib/rigor/builtins/hkt_builtins.rb +1 -0
- data/lib/rigor/builtins/imported_refinements.rb +4 -0
- data/lib/rigor/builtins/regex_refinement.rb +17 -9
- data/lib/rigor/builtins/static_return_refinements.rb +2 -0
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/incremental_snapshot.rb +33 -1
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_descriptor.rb +37 -7
- data/lib/rigor/cache/store.rb +0 -1
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- data/lib/rigor/cli/plugin_command.rb +3 -3
- data/lib/rigor/cli/prism_colorizer.rb +0 -1
- data/lib/rigor/cli/sig_gen_command.rb +210 -29
- data/lib/rigor/cli/skill_command.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +0 -1
- data/lib/rigor/cli/type_of_command.rb +19 -5
- 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.rb +21 -3
- data/lib/rigor/configuration/severity_profile.rb +19 -3
- data/lib/rigor/configuration.rb +66 -3
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +11 -2
- 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 +9 -2
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/effect_table.rb +11 -3
- data/lib/rigor/effects/envelope_check.rb +1 -1
- data/lib/rigor/effects/envelope_index.rb +15 -0
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/identity.rb +16 -0
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +21 -0
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +43 -31
- data/lib/rigor/effects/propagator.rb +295 -16
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +121 -72
- data/lib/rigor/effects/signature_sources.rb +1 -1
- data/lib/rigor/effects/snapshot.rb +2 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +385 -30
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/lockfile_resolver.rb +17 -0
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_loader.rb +399 -133
- data/lib/rigor/environment.rb +103 -38
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +139 -9
- 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 +73 -27
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +2 -1
- data/lib/rigor/inference/budget_trace.rb +2 -1
- 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 +387 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- 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/element_read_widening.rb +22 -10
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +1532 -496
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- 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/index_write_widening.rb +16 -3
- 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 +167 -17
- 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 +85 -24
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
- 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/rbs_dispatch.rb +881 -38
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +214 -13
- data/lib/rigor/inference/method_parameter_binder.rb +8 -3
- data/lib/rigor/inference/multi_target_binder.rb +340 -52
- data/lib/rigor/inference/mutation_rejoin.rb +4 -1
- data/lib/rigor/inference/mutation_widening.rb +70 -48
- data/lib/rigor/inference/narrowing.rb +540 -120
- 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 +152 -9
- data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +7 -2
- data/lib/rigor/inference/project_patched_scanner.rb +7 -3
- data/lib/rigor/inference/receiver_alias.rb +90 -1
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +15 -11
- 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 +4615 -551
- data/lib/rigor/inference/statement_evaluator.rb +3176 -490
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +44 -7
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
- data/lib/rigor/language_server/hover_renderer.rb +3 -3
- data/lib/rigor/language_server/project_context.rb +5 -3
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +168 -5
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +1 -1
- data/lib/rigor/plugin/effect_attribution.rb +58 -4
- data/lib/rigor/plugin/loader.rb +2 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/manifest.rb +71 -10
- data/lib/rigor/plugin/registry.rb +35 -1
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +3 -1
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +1 -2
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +19 -6
- data/lib/rigor/reflection.rb +72 -105
- data/lib/rigor/scope/discovery_index.rb +135 -2
- data/lib/rigor/scope.rb +859 -49
- 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 +558 -31
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +124 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +166 -35
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +1 -1
- data/lib/rigor/source/node_walker.rb +0 -3
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/combinator.rb +88 -12
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/hash_shape.rb +1 -1
- data/lib/rigor/type/refined.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
- 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 +239 -4
- 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/effects.rb +1 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -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 +380 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- 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.rb +72 -2
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
- 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 +217 -36
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -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/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 +12 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +6 -4
- 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 +1 -1
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +50 -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 +7 -6
- data/sig/rigor/scope.rbs +135 -20
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +6 -0
- data/sig/rigor.rbs +38 -20
- data/skills/rigor-ask/SKILL.md +8 -5
- 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 +12 -25
- 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 +9 -7
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +33 -8
- 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-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +4 -6
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
- data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +108 -1
|
@@ -5,9 +5,11 @@ require_relative "../../type"
|
|
|
5
5
|
require_relative "../../rbs_extended"
|
|
6
6
|
require_relative "../range_constant"
|
|
7
7
|
require_relative "../rbs_type_translator"
|
|
8
|
+
require_relative "../external_ancestor_resolution"
|
|
8
9
|
require_relative "../void_origin"
|
|
9
10
|
require_relative "../optimistic_origin"
|
|
10
11
|
require_relative "overload_selector"
|
|
12
|
+
require_relative "self_substitute"
|
|
11
13
|
|
|
12
14
|
module Rigor
|
|
13
15
|
module Inference
|
|
@@ -42,14 +44,16 @@ module Rigor
|
|
|
42
44
|
#
|
|
43
45
|
# Remaining limitations:
|
|
44
46
|
#
|
|
45
|
-
# *
|
|
46
|
-
#
|
|
47
|
-
# 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.
|
|
48
49
|
# * Method-level type parameters bind only from two positions: the block return type (Slice 6 phase C)
|
|
49
50
|
# and a positional parameter whose declared type is EXACTLY a type variable (issue #303 —
|
|
50
51
|
# `def foo[T]: (T) -> T` binds `T` from the first argument, and carries it into a generic return
|
|
51
52
|
# such as `-> Array[T]`). A variable reachable only through a container position (`Array[T] arg`),
|
|
52
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}).
|
|
53
57
|
#
|
|
54
58
|
# See docs/adr/4-type-inference-engine.md for the broader plan.
|
|
55
59
|
# rubocop:disable-next Metrics/ModuleLength
|
|
@@ -65,6 +69,86 @@ module Rigor
|
|
|
65
69
|
# objects answer to methods their RBS omits (`ActionController::Base`, `Hash`, …).
|
|
66
70
|
ALLOWED_RBS_COMPLETE_ANCESTORS = ["Rigor::Plugin::Base"].freeze
|
|
67
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
|
+
|
|
68
152
|
# Shared empty returns for the argument-position type-variable binding (issue #303). The
|
|
69
153
|
# no-candidate answer is by far the common case — every non-generic overload takes it — so it must
|
|
70
154
|
# not allocate.
|
|
@@ -74,6 +158,16 @@ module Rigor
|
|
|
74
158
|
private_constant :EMPTY_TYPE_PARAM_NAMES
|
|
75
159
|
NO_BINDING = [nil, nil].freeze
|
|
76
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
|
|
77
171
|
|
|
78
172
|
# Both spellings a resolved `RBS::Types::ClassInstance#name` — or a `Nominal#class_name` built
|
|
79
173
|
# from one — may carry for `Range`; core signatures absolutise, but a plugin-contributed one
|
|
@@ -148,7 +242,8 @@ module Rigor
|
|
|
148
242
|
receiver: context.receiver,
|
|
149
243
|
method_name: context.method_name,
|
|
150
244
|
args: context.args,
|
|
151
|
-
environment: environment
|
|
245
|
+
environment: environment,
|
|
246
|
+
scope: context.scope
|
|
152
247
|
)
|
|
153
248
|
end
|
|
154
249
|
|
|
@@ -186,7 +281,8 @@ module Rigor
|
|
|
186
281
|
return nil unless descriptor
|
|
187
282
|
|
|
188
283
|
class_name, kind, receiver_args = descriptor
|
|
189
|
-
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)
|
|
190
286
|
return nil unless method_definition
|
|
191
287
|
return nil if public_only && method_private?(method_definition)
|
|
192
288
|
# Issue #823 — a declaration that states the member's presence and parameters but not its
|
|
@@ -209,7 +305,8 @@ module Rigor
|
|
|
209
305
|
type_vars: type_vars,
|
|
210
306
|
block_type: block_type,
|
|
211
307
|
environment: environment,
|
|
212
|
-
self_type_override: self_type_override
|
|
308
|
+
self_type_override: self_type_override ||
|
|
309
|
+
SelfSubstitute.for(receiver, receiver_args, method_name, args, block_type),
|
|
213
310
|
scope: scope,
|
|
214
311
|
call_node: call_node
|
|
215
312
|
)
|
|
@@ -325,11 +422,19 @@ module Rigor
|
|
|
325
422
|
[Type::Combinator.union(*tuple.elements)]
|
|
326
423
|
end
|
|
327
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.
|
|
328
429
|
def hash_shape_type_args(shape)
|
|
329
430
|
return [] if shape.pairs.empty?
|
|
330
431
|
|
|
331
432
|
key_types = shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) }
|
|
332
433
|
value_types = shape.pairs.values
|
|
434
|
+
if shape.open?
|
|
435
|
+
key_types += [Type::Combinator.untyped]
|
|
436
|
+
value_types += [Type::Combinator.untyped]
|
|
437
|
+
end
|
|
333
438
|
[
|
|
334
439
|
Type::Combinator.union(*key_types),
|
|
335
440
|
Type::Combinator.union(*value_types)
|
|
@@ -344,20 +449,43 @@ module Rigor
|
|
|
344
449
|
method_definition.accessibility == :private
|
|
345
450
|
end
|
|
346
451
|
|
|
347
|
-
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)
|
|
348
453
|
direct = lookup_method_on(environment, class_name, kind, method_name)
|
|
349
454
|
return direct if direct
|
|
350
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
|
+
|
|
351
464
|
# ADR-43 — scoped inherited-method resolution. The direct lookup misses when `class_name` is a
|
|
352
465
|
# Ruby-source subclass absent from RBS (so no ancestor walk runs). If its discovered
|
|
353
466
|
# superclass chain reaches an allow-listed RBS-complete ancestor, resolve the method there so
|
|
354
467
|
# inherited contract calls (`self.manifest` on a plugin) resolve and the normal call rules
|
|
355
468
|
# apply. Bounded to the allow-list, so open hierarchies stay on the Dynamic fallback (no false
|
|
356
469
|
# positive on `< ActionController::Base`).
|
|
357
|
-
ancestor = allowed_rbs_complete_ancestor(environment, class_name, scope)
|
|
358
|
-
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
|
|
359
482
|
|
|
360
|
-
|
|
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)
|
|
361
489
|
end
|
|
362
490
|
|
|
363
491
|
def lookup_method_on(environment, class_name, kind, method_name)
|
|
@@ -374,34 +502,471 @@ module Rigor
|
|
|
374
502
|
# `class_name` is itself RBS-known (the direct lookup already had authority), or when the
|
|
375
503
|
# discovered chain reaches no allow-listed class. The walk carries a visited set so a malformed
|
|
376
504
|
# cyclic `A < B < A` source cannot loop.
|
|
377
|
-
|
|
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)
|
|
378
510
|
return nil if scope.nil?
|
|
379
511
|
return nil if Rigor::Reflection.rbs_class_known?(class_name, environment: environment)
|
|
380
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)
|
|
845
|
+
supers = scope.discovered_superclasses
|
|
846
|
+
includes = scope.discovered_includes
|
|
847
|
+
queue = [class_name.to_s]
|
|
848
|
+
seen = {}
|
|
849
|
+
until queue.empty?
|
|
850
|
+
current = queue.shift
|
|
851
|
+
next if current.nil? || seen[current]
|
|
852
|
+
|
|
853
|
+
seen[current] = true
|
|
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
|
+
|
|
381
880
|
supers = scope.discovered_superclasses
|
|
881
|
+
extends = scope.discovered_extends
|
|
882
|
+
queue = [class_name.to_s]
|
|
382
883
|
seen = {}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
884
|
+
until queue.empty?
|
|
885
|
+
current = queue.shift
|
|
886
|
+
next if current.nil? || seen[current]
|
|
386
887
|
|
|
387
888
|
seen[current] = true
|
|
388
|
-
|
|
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
|
|
389
926
|
end
|
|
390
927
|
nil
|
|
391
928
|
end
|
|
392
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
|
+
|
|
393
949
|
# Slice 4 phase 2d substitution map. Zips the class's declared type-parameter names against the
|
|
394
|
-
# receiver's `type_args`. Returns an empty hash when either side is empty or when
|
|
395
|
-
#
|
|
396
|
-
# 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.
|
|
397
962
|
def build_type_vars(environment, class_name, receiver_args)
|
|
398
963
|
return NO_TYPE_VARS if receiver_args.empty?
|
|
399
964
|
|
|
400
965
|
param_names = Rigor::Reflection.class_type_param_names(class_name, environment: environment)
|
|
401
966
|
return NO_TYPE_VARS if param_names.empty?
|
|
402
|
-
return NO_TYPE_VARS if
|
|
967
|
+
return NO_TYPE_VARS if receiver_args.size > param_names.size
|
|
403
968
|
|
|
404
|
-
param_names.zip(receiver_args).to_h
|
|
969
|
+
param_names.first(receiver_args.size).zip(receiver_args).to_h
|
|
405
970
|
end
|
|
406
971
|
|
|
407
972
|
# The shared empty substitution map: most receivers carry no type arguments, and the translator
|
|
@@ -428,13 +993,16 @@ module Rigor
|
|
|
428
993
|
end
|
|
429
994
|
# `self_type_override` lets the user-class fallback path preserve the ORIGINAL receiver as the
|
|
430
995
|
# substitute for `Bases::Self` — so `Kernel#dup: () -> self` resolved through the Object
|
|
431
|
-
# 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).
|
|
432
998
|
self_type = self_type_override || resolved_self_type
|
|
433
999
|
|
|
434
1000
|
candidates = OverloadSelector.select_candidates(
|
|
435
1001
|
method_definition,
|
|
436
1002
|
arg_types: args,
|
|
437
|
-
|
|
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,
|
|
438
1006
|
instance_type: instance_type,
|
|
439
1007
|
type_vars: type_vars,
|
|
440
1008
|
block_required: !block_type.nil?,
|
|
@@ -446,6 +1014,7 @@ module Rigor
|
|
|
446
1014
|
record_dispatch_provenance(method_definition, candidates.first, scope, call_node, call_site)
|
|
447
1015
|
join_candidate_returns(
|
|
448
1016
|
candidates,
|
|
1017
|
+
method_definition: method_definition,
|
|
449
1018
|
self_type: self_type, instance_type: instance_type, type_vars: type_vars,
|
|
450
1019
|
args: args, block_type: block_type, scope: scope, call_node: call_node, call_site: call_site,
|
|
451
1020
|
alias_expander: environment.rbs_loader
|
|
@@ -485,17 +1054,20 @@ module Rigor
|
|
|
485
1054
|
# A candidate whose return does not translate leaves the join incomplete — decline (fail-soft
|
|
486
1055
|
# to Dynamic downstream) rather than answer a join missing an arm the runtime can take.
|
|
487
1056
|
# rubocop:disable-next Metrics/ParameterLists
|
|
488
|
-
def join_candidate_returns(candidates, self_type:, instance_type:, type_vars:, args:,
|
|
489
|
-
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)
|
|
490
1059
|
returns = candidates.map do |method_type|
|
|
491
1060
|
full_type_vars = compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
|
|
492
|
-
RbsTypeTranslator.translate(
|
|
1061
|
+
returned = RbsTypeTranslator.translate(
|
|
493
1062
|
method_type.type.return_type,
|
|
494
1063
|
self_type: self_type,
|
|
495
1064
|
instance_type: instance_type,
|
|
496
1065
|
type_vars: full_type_vars,
|
|
497
1066
|
alias_expander: alias_expander
|
|
498
1067
|
)
|
|
1068
|
+
next returned unless combining_overload?(method_definition, method_type, call_site)
|
|
1069
|
+
|
|
1070
|
+
class_level_sum(returned, method_type, args)
|
|
499
1071
|
end
|
|
500
1072
|
return returns.first if returns.size == 1
|
|
501
1073
|
return nil if returns.any?(&:nil?)
|
|
@@ -507,11 +1079,100 @@ module Rigor
|
|
|
507
1079
|
end
|
|
508
1080
|
|
|
509
1081
|
def compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
|
|
510
|
-
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)
|
|
511
1084
|
compose_arg_type_vars(method_type, vars, args, scope: scope, call_node: call_node,
|
|
512
1085
|
call_site: call_site)
|
|
513
1086
|
end
|
|
514
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
|
+
|
|
515
1176
|
# Record the `void → top` recovery when the selected overload declares `-> void` and both `scope` and
|
|
516
1177
|
# `call_node` are present (the direct-dispatch path). `void_site` is the `[class_name, method_name,
|
|
517
1178
|
# kind]` triple the {VoidOrigin} carries.
|
|
@@ -558,13 +1219,160 @@ module Rigor
|
|
|
558
1219
|
# block return type at the same call site. Anything outside this exact shape (no block clause,
|
|
559
1220
|
# an `untyped` block, a non-variable block return type, a variable not declared in
|
|
560
1221
|
# `type_params`) returns the original `type_vars` so fallbacks stay consistent.
|
|
561
|
-
|
|
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:)
|
|
562
1236
|
return type_vars if block_type.nil?
|
|
563
1237
|
|
|
564
1238
|
block_var_name = method_type_block_return_variable(method_type)
|
|
565
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)
|
|
1242
|
+
|
|
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
|
|
566
1247
|
|
|
567
|
-
|
|
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) }
|
|
568
1376
|
end
|
|
569
1377
|
|
|
570
1378
|
# Issue #303 — bind method-level type parameters from ARGUMENT positions, layering on top of the
|
|
@@ -607,10 +1415,25 @@ module Rigor
|
|
|
607
1415
|
name, bound = param_binding(param, arg, declared, type_vars)
|
|
608
1416
|
next if name.nil?
|
|
609
1417
|
|
|
1418
|
+
bound = Type::Combinator.widen_value_pinned(bound) if upper_bounded?(method_type, name)
|
|
610
1419
|
bindings[name] = bindings.key?(name) ? Type::Combinator.union(bindings[name], bound) : bound
|
|
611
1420
|
end
|
|
612
1421
|
end
|
|
613
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)
|
|
1434
|
+
end
|
|
1435
|
+
end
|
|
1436
|
+
|
|
614
1437
|
# The `(variable name, bound type)` a positional parameter contributes, or {NO_BINDING} when it
|
|
615
1438
|
# contributes none. The bare-variable shape is tried first because it is the overwhelmingly
|
|
616
1439
|
# common one; the `Range[A]` shape is reached only when the parameter is not a bare variable.
|
|
@@ -759,11 +1582,11 @@ module Rigor
|
|
|
759
1582
|
|
|
760
1583
|
# ----- block parameter probe (Phase C sub-phase 1) -----
|
|
761
1584
|
|
|
762
|
-
def probe_block_param_types(receiver:, method_name:, args:, environment:)
|
|
1585
|
+
def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil)
|
|
763
1586
|
args ||= []
|
|
764
1587
|
case receiver
|
|
765
|
-
when Type::Union then probe_block_param_types_union(receiver, method_name, args, environment)
|
|
766
|
-
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)
|
|
767
1590
|
end
|
|
768
1591
|
end
|
|
769
1592
|
|
|
@@ -771,9 +1594,9 @@ module Rigor
|
|
|
771
1594
|
# member resolves the same arity and types (otherwise the call sites would have to thread
|
|
772
1595
|
# per-member binders, which the slice does not support yet). Mismatches degrade to the empty
|
|
773
1596
|
# array so the binder defaults all params to Dynamic[Top].
|
|
774
|
-
def probe_block_param_types_union(receiver, method_name, args, environment)
|
|
1597
|
+
def probe_block_param_types_union(receiver, method_name, args, environment, scope)
|
|
775
1598
|
results = receiver.members.map do |member|
|
|
776
|
-
probe_block_param_types_one(member, method_name, args, environment)
|
|
1599
|
+
probe_block_param_types_one(member, method_name, args, environment, scope)
|
|
777
1600
|
end
|
|
778
1601
|
return [] if results.empty?
|
|
779
1602
|
return [] unless results.all? { |r| r == results.first }
|
|
@@ -781,12 +1604,12 @@ module Rigor
|
|
|
781
1604
|
results.first
|
|
782
1605
|
end
|
|
783
1606
|
|
|
784
|
-
def probe_block_param_types_one(receiver, method_name, args, environment)
|
|
1607
|
+
def probe_block_param_types_one(receiver, method_name, args, environment, scope)
|
|
785
1608
|
descriptor = receiver_descriptor(receiver)
|
|
786
1609
|
return [] unless descriptor
|
|
787
1610
|
|
|
788
1611
|
class_name, kind, receiver_args = descriptor
|
|
789
|
-
method_definition = lookup_method(environment, class_name, kind, method_name)
|
|
1612
|
+
method_definition = lookup_method(environment, class_name, kind, method_name, scope)
|
|
790
1613
|
return [] unless method_definition
|
|
791
1614
|
|
|
792
1615
|
type_vars = build_type_vars(environment, class_name, receiver_args)
|
|
@@ -796,14 +1619,20 @@ module Rigor
|
|
|
796
1619
|
kind: kind,
|
|
797
1620
|
args: args,
|
|
798
1621
|
type_vars: type_vars,
|
|
799
|
-
environment: environment
|
|
1622
|
+
environment: environment,
|
|
1623
|
+
receiver: receiver,
|
|
1624
|
+
receiver_args: receiver_args,
|
|
1625
|
+
method_name: method_name
|
|
800
1626
|
)
|
|
801
1627
|
rescue StandardError
|
|
802
1628
|
[]
|
|
803
1629
|
end
|
|
804
1630
|
|
|
1631
|
+
# rubocop:disable Metrics/ParameterLists
|
|
805
1632
|
def extract_block_param_types(method_definition, class_name:, kind:, args:, type_vars:,
|
|
806
|
-
environment: nil
|
|
1633
|
+
environment: nil, receiver: nil, receiver_args: [],
|
|
1634
|
+
method_name: nil)
|
|
1635
|
+
# rubocop:enable Metrics/ParameterLists
|
|
807
1636
|
instance_type = Type::Combinator.nominal_of(class_name)
|
|
808
1637
|
self_type =
|
|
809
1638
|
case kind
|
|
@@ -811,10 +1640,24 @@ module Rigor
|
|
|
811
1640
|
else instance_type
|
|
812
1641
|
end
|
|
813
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
|
+
|
|
814
1655
|
method_type = OverloadSelector.select(
|
|
815
1656
|
method_definition,
|
|
816
1657
|
arg_types: args,
|
|
817
|
-
|
|
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,
|
|
818
1661
|
instance_type: instance_type,
|
|
819
1662
|
type_vars: type_vars,
|
|
820
1663
|
block_required: true,
|