rigortype 0.3.9 → 0.4.1
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 +5 -4
- 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/08-understanding-errors.md +3 -2
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +65 -4
- data/docs/manual/03-configuration.md +12 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- 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 +427 -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 +638 -136
- data/lib/rigor/analysis/dependency_recorder.rb +222 -17
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +309 -53
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- 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 +3 -1
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +46 -18
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
- data/lib/rigor/analysis/runner.rb +603 -54
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +93 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +409 -0
- data/lib/rigor/analysis/worker_session.rb +57 -13
- 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 +45 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +102 -8
- 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 +63 -4
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- 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/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +21 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +24 -3
- data/lib/rigor/configuration.rb +71 -7
- 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 +49 -37
- 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/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +27 -1
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +400 -134
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +104 -96
- 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 +88 -26
- 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 +393 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- 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/definer_resolution.rb +266 -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 +1665 -538
- data/lib/rigor/inference/external_ancestor_resolution.rb +264 -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 +488 -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 +179 -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 +901 -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/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +345 -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 +674 -124
- 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 +13 -11
- 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/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +5913 -701
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +3249 -497
- 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 +196 -13
- 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/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- 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/trust_policy.rb +3 -3
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +20 -9
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +4 -5
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +146 -0
- data/lib/rigor/reflection/constant_path.rb +41 -13
- data/lib/rigor/reflection.rb +73 -107
- data/lib/rigor/scope/discovery_index.rb +351 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +981 -198
- 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 +580 -47
- 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 +112 -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-ffi/lib/rigor/plugin/ffi.rb +10 -0
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.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 +14 -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 +2 -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 +177 -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 +46 -18
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +40 -10
- 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 +132 -1
data/lib/rigor/scope.rb
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
require_relative "type"
|
|
4
4
|
require_relative "environment"
|
|
5
5
|
require_relative "scope/discovery_index"
|
|
6
|
+
require_relative "scope/resolution_chain"
|
|
7
|
+
require_relative "inference/definer_resolution"
|
|
6
8
|
require_relative "analysis/fact_store"
|
|
7
9
|
require_relative "analysis/dependency_recorder"
|
|
8
10
|
require_relative "inference/expression_typer"
|
|
9
11
|
require_relative "inference/flow_tracer"
|
|
12
|
+
require_relative "inference/optimistic_origin"
|
|
10
13
|
require_relative "inference/statement_evaluator"
|
|
11
14
|
require_relative "inference/def_node_resolver"
|
|
12
15
|
|
|
@@ -23,10 +26,12 @@ module Rigor
|
|
|
23
26
|
:indexed_narrowings, :method_chain_narrowings,
|
|
24
27
|
:declaration_sourced, :published_constant_sourced,
|
|
25
28
|
:source_path, :discovery, :struct_fold_safe_locals,
|
|
26
|
-
:opaque_block_self, :lexical_nesting,
|
|
29
|
+
:opaque_block_self, :singleton_class_body, :lexical_nesting,
|
|
27
30
|
:dynamic_origins, :local_origins, :ivar_origins,
|
|
28
31
|
:void_origins, :plugin_typed_calls,
|
|
29
|
-
:optimistic_origins, :optimistic_locals, :optimistic_ivars
|
|
32
|
+
:optimistic_origins, :optimistic_locals, :optimistic_ivars,
|
|
33
|
+
:repeated_or_writes, :match_frame,
|
|
34
|
+
:constant_narrowings, :guard_records, :bot_guard_classes
|
|
30
35
|
|
|
31
36
|
# ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
|
|
32
37
|
# reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
|
|
@@ -47,8 +52,26 @@ module Rigor
|
|
|
47
52
|
def discovered_def_sources = @discovery.discovered_def_sources
|
|
48
53
|
def discovered_singleton_def_sources = @discovery.discovered_singleton_def_sources
|
|
49
54
|
def discovered_method_visibilities = @discovery.discovered_method_visibilities
|
|
55
|
+
def discovered_parameter_envelopes = @discovery.discovered_parameter_envelopes
|
|
50
56
|
def discovered_superclasses = @discovery.discovered_superclasses
|
|
57
|
+
|
|
58
|
+
# Issue #1097 — `{file path => [[start_offset, end_offset, name, kind, owner], ...]}`, the def /
|
|
59
|
+
# block / lambda body ranges {singleton_def_shadows_call?} / {instance_def_shadows_call?} order a
|
|
60
|
+
# project-defined override against.
|
|
61
|
+
def discovered_deferred_ranges = @discovery.discovered_deferred_ranges
|
|
62
|
+
# Issue #1120 — `{refined class name => {method name => [refining module names]}}`: what each
|
|
63
|
+
# `refine X do … end` block defines, visible only where a `using` of its module is in effect.
|
|
64
|
+
def discovered_refinements = @discovery.discovered_refinements
|
|
65
|
+
# Issue #1367 — the project's `Inference::GlobalWriteCensus`, a `Set` of frozen entries.
|
|
66
|
+
def discovered_global_write_census = @discovery.discovered_global_write_census
|
|
51
67
|
def discovered_includes = @discovery.discovered_includes
|
|
68
|
+
# Issue #1123 — `{qualified class or module name => [module names it `prepend`s, as written]}`, in
|
|
69
|
+
# instance-ancestor search order (nearest prepend first). The one table that tells a `prepend` from an
|
|
70
|
+
# `include`: {ResolutionChain} reads it to put a prepended module ahead of the class.
|
|
71
|
+
# The plugin-facing view: each class's prepends once, nearest first. The discovery index itself keeps every
|
|
72
|
+
# statement (a repeated `prepend` is a no-op in Ruby, the first statement winning), and the resolution chain
|
|
73
|
+
# reads that raw list.
|
|
74
|
+
def discovered_prepends = @discovery.discovered_prepends.transform_values { |names| names.uniq.freeze }.freeze
|
|
52
75
|
def discovered_extends = @discovery.discovered_extends
|
|
53
76
|
def discovered_class_sources = @discovery.discovered_class_sources
|
|
54
77
|
# Issue #644 — `{qualified constant name => Set[declaring file]}`; seeded only on an ADR-46 recording run.
|
|
@@ -56,6 +79,30 @@ module Rigor
|
|
|
56
79
|
def published_constant_names = @discovery.published_constant_names
|
|
57
80
|
def local_constant_names = @discovery.local_constant_names
|
|
58
81
|
|
|
82
|
+
# Issue #617 — the census names sharing `name`'s last segment that bind, spelled as the census spells
|
|
83
|
+
# them (a `*::LIMIT` wildcard included), so the caller still decides which of them a reference resolves
|
|
84
|
+
# to. A `||=`-only name is left out while no other file memoizes its segment: the memoization idiom's
|
|
85
|
+
# own write, however often one file repeats it, is not what binds it.
|
|
86
|
+
def bound_constant_names(name)
|
|
87
|
+
@discovery.constant_writers[name.split("::").last] || EMPTY_BOUND_CONSTANT_NAMES
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Issue #1290 — the census names sharing `name`'s last segment that some write other than a memo `||=`
|
|
91
|
+
# assigns: the ones that shadow an outer constant of the name, so the lexical ladder stops at them
|
|
92
|
+
# (`Reflection.resolve_constant_type`). That asks once per constant reference, so a bare name is looked up
|
|
93
|
+
# as it stands rather than split, and a qualified one is sliced after its last `::` rather than split into
|
|
94
|
+
# an Array of every segment ([#1502](https://github.com/rigortype/rigor/issues/1502)). The two agree on every
|
|
95
|
+
# name a parsed constant path spells. Only the `Foo::` Prism recovers from a syntax error differs, and no
|
|
96
|
+
# file with a parse error is typed.
|
|
97
|
+
def shadowing_constant_names(name)
|
|
98
|
+
separator = name.rindex("::")
|
|
99
|
+
segment = separator ? name[(separator + 2)..] : name
|
|
100
|
+
@discovery.constant_shadowers[segment] || EMPTY_BOUND_CONSTANT_NAMES
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
EMPTY_BOUND_CONSTANT_NAMES = [].freeze
|
|
104
|
+
private_constant :EMPTY_BOUND_CONSTANT_NAMES
|
|
105
|
+
|
|
59
106
|
# Issue #667 — the instance-variable names of `class_name` whose class-ivar seed comes from a foreign
|
|
60
107
|
# published constant (`@mode = AppConfig::MODE` in `initialize`, read in a sibling method).
|
|
61
108
|
# `StatementEvaluator#seed_instance_ivars` stamps the flow mark for these at method-body entry, the way
|
|
@@ -186,10 +233,38 @@ module Rigor
|
|
|
186
233
|
# Issue #667 — the empty answer of {#published_constant_ivars_for}, so a class with no such ivar (every
|
|
187
234
|
# class in a project that publishes nothing) allocates none.
|
|
188
235
|
EMPTY_PUBLISHED_CONSTANT_IVARS = Set.new.freeze
|
|
236
|
+
# The index `||=` sites of a repeating block body whose slot an earlier run of the body may have filled,
|
|
237
|
+
# keyed by node identity and laid by a block-return pass at the body's entry
|
|
238
|
+
# ({#with_repeated_or_writes}; `Inference::RepeatedOrWrites` decides which). The memoizing `||=` reading
|
|
239
|
+
# (`StatementEvaluator#index_compound_write_value`) is withheld at such a site. The mark belongs to the
|
|
240
|
+
# site, so no rebind or narrowing of a variable drops it, and a join keeps a site either arm holds: the
|
|
241
|
+
# mark only ever withholds that reading, whose answer is the narrower one.
|
|
242
|
+
EMPTY_REPEATED_OR_WRITES = {}.compare_by_identity.freeze
|
|
243
|
+
# Issue #1429 — the narrowing a guard leaves on a constant reference (`STDOUT.is_a?(StringIO)`, `if CONFIG`),
|
|
244
|
+
# keyed by the reference's spelling ({Inference::Narrowing.constant_key}: `"STDOUT"`, `"::Foo::BAR"`). A constant
|
|
245
|
+
# has no binding of its own in the scope, so the table is what a read in the guarded edge answers
|
|
246
|
+
# (`ExpressionTyper#type_of_constant_read`); a spelling it does not hold resolves as before.
|
|
247
|
+
EMPTY_CONSTANT_NARROWINGS = {}.freeze
|
|
248
|
+
# Issue #1429 — what each global and constant a guard narrowed was bound to before the guard, keyed
|
|
249
|
+
# `[:global, :$name]` / `[:constant, key]`. Ruby may rebind a global (or `const_set` a constant) whenever code the
|
|
250
|
+
# analysis cannot see runs, so a call or block that may run project or unresolved code restores each recorded
|
|
251
|
+
# binding to the union of this pre-guard type and its narrowed one ({#forget_guard_narrowings}). A write drops
|
|
252
|
+
# the record ({#with_global}), and a name the frame-local special-variable machinery owns is never recorded.
|
|
253
|
+
# Issue #1446 — a class guard's narrowing of an instance variable is recorded too, keyed `[:ivar, :@name]`
|
|
254
|
+
# ({#with_guarded_ivar}), since that code may reach `self` and rebind it; a write drops it ({#without_ivar_guard}).
|
|
255
|
+
EMPTY_GUARD_RECORDS = {}.freeze
|
|
256
|
+
# Issue #1446 — the classes a class guard narrowed a receiver to where the narrowing left it `bot`, keyed like
|
|
257
|
+
# {EMPTY_GUARD_RECORDS} with `[:local, :name]` too: `return unless @cb.is_a?(Proc)` binds `@cb` to `bot` when the
|
|
258
|
+
# guard is disjoint from its binding, but the value that passes it is a `Proc` (or a subclass of one), and a call
|
|
259
|
+
# on it dispatches on that class. `Inference::GuardRebinding` reads the class a `bot` receiver runs its methods
|
|
260
|
+
# on from here ({#bot_guard_classes_for}); a `bot` no class guard produced has no entry. A write or another
|
|
261
|
+
# narrowing of the name drops its entry, and a join keeps one only both arms hold.
|
|
262
|
+
EMPTY_BOT_GUARD_CLASSES = {}.freeze
|
|
189
263
|
private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
|
|
190
264
|
:EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED,
|
|
191
265
|
:EMPTY_FOLD_SAFE, :EMPTY_ORIGINS, :EMPTY_PUBLISHED_CONSTANT_SOURCED,
|
|
192
|
-
:EMPTY_PUBLISHED_CONSTANT_IVARS
|
|
266
|
+
:EMPTY_PUBLISHED_CONSTANT_IVARS, :EMPTY_REPEATED_OR_WRITES,
|
|
267
|
+
:EMPTY_CONSTANT_NARROWINGS, :EMPTY_GUARD_RECORDS, :EMPTY_BOT_GUARD_CLASSES
|
|
193
268
|
|
|
194
269
|
class << self
|
|
195
270
|
def empty(environment: Environment.default, source_path: nil)
|
|
@@ -230,7 +305,7 @@ module Rigor
|
|
|
230
305
|
@plugin_typed_calls.key?(node)
|
|
231
306
|
end
|
|
232
307
|
|
|
233
|
-
def initialize(
|
|
308
|
+
def initialize( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- one keyword and assignment per field
|
|
234
309
|
environment:, locals:,
|
|
235
310
|
fact_store: Analysis::FactStore.empty,
|
|
236
311
|
self_type: nil,
|
|
@@ -245,6 +320,7 @@ module Rigor
|
|
|
245
320
|
source_path: nil,
|
|
246
321
|
struct_fold_safe_locals: EMPTY_FOLD_SAFE,
|
|
247
322
|
opaque_block_self: false,
|
|
323
|
+
singleton_class_body: false,
|
|
248
324
|
lexical_nesting: nil,
|
|
249
325
|
dynamic_origins: {}.compare_by_identity,
|
|
250
326
|
local_origins: EMPTY_ORIGINS,
|
|
@@ -253,7 +329,12 @@ module Rigor
|
|
|
253
329
|
plugin_typed_calls: {}.compare_by_identity,
|
|
254
330
|
optimistic_origins: {}.compare_by_identity,
|
|
255
331
|
optimistic_locals: EMPTY_ORIGINS,
|
|
256
|
-
optimistic_ivars: EMPTY_ORIGINS
|
|
332
|
+
optimistic_ivars: EMPTY_ORIGINS,
|
|
333
|
+
repeated_or_writes: EMPTY_REPEATED_OR_WRITES,
|
|
334
|
+
match_frame: nil,
|
|
335
|
+
constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
|
|
336
|
+
guard_records: EMPTY_GUARD_RECORDS,
|
|
337
|
+
bot_guard_classes: EMPTY_BOT_GUARD_CLASSES
|
|
257
338
|
)
|
|
258
339
|
@environment = environment
|
|
259
340
|
@locals = locals
|
|
@@ -270,6 +351,7 @@ module Rigor
|
|
|
270
351
|
@source_path = source_path
|
|
271
352
|
@struct_fold_safe_locals = struct_fold_safe_locals
|
|
272
353
|
@opaque_block_self = opaque_block_self
|
|
354
|
+
@singleton_class_body = singleton_class_body
|
|
273
355
|
@lexical_nesting = lexical_nesting
|
|
274
356
|
@dynamic_origins = dynamic_origins
|
|
275
357
|
@local_origins = local_origins
|
|
@@ -279,6 +361,11 @@ module Rigor
|
|
|
279
361
|
@optimistic_origins = optimistic_origins
|
|
280
362
|
@optimistic_locals = optimistic_locals
|
|
281
363
|
@optimistic_ivars = optimistic_ivars
|
|
364
|
+
@repeated_or_writes = repeated_or_writes
|
|
365
|
+
@match_frame = match_frame
|
|
366
|
+
@constant_narrowings = constant_narrowings
|
|
367
|
+
@guard_records = guard_records
|
|
368
|
+
@bot_guard_classes = bot_guard_classes
|
|
282
369
|
freeze
|
|
283
370
|
end
|
|
284
371
|
|
|
@@ -291,19 +378,30 @@ module Rigor
|
|
|
291
378
|
self
|
|
292
379
|
end
|
|
293
380
|
|
|
294
|
-
def optimistic_local(name) = @optimistic_locals[name.to_sym]
|
|
295
|
-
def optimistic_ivar(name) = @optimistic_ivars[name.to_sym]
|
|
381
|
+
def optimistic_local(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_locals[name.to_sym])
|
|
382
|
+
def optimistic_ivar(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_ivars[name.to_sym])
|
|
383
|
+
|
|
384
|
+
# Issue #1302 — what the value a marked local / ivar is bound to answers on a miss, as its binding recorded
|
|
385
|
+
# it beside the mark, or {Inference::OptimisticOrigin::UNKNOWN_MISS}. The answer lives in the mark's own
|
|
386
|
+
# table entry ({Inference::OptimisticOrigin::BoundMark}), so every transition that keeps or drops the mark
|
|
387
|
+
# keeps or drops the answer with it.
|
|
388
|
+
def optimistic_local_miss(name) = Inference::OptimisticOrigin.bound_miss(@optimistic_locals[name.to_sym])
|
|
389
|
+
def optimistic_ivar_miss(name) = Inference::OptimisticOrigin.bound_miss(@optimistic_ivars[name.to_sym])
|
|
296
390
|
|
|
297
|
-
|
|
391
|
+
# `miss` is what the bound value answers when the bet fails ({Inference::OptimisticOrigin.miss_answer}); the
|
|
392
|
+
# default records none, so a read through the binding widens as an untold miss does.
|
|
393
|
+
def with_optimistic_local(name, cause, miss: Inference::OptimisticOrigin::UNKNOWN_MISS)
|
|
298
394
|
return self if cause.nil?
|
|
299
395
|
|
|
300
|
-
|
|
396
|
+
mark = Inference::OptimisticOrigin.bound_mark(cause, miss)
|
|
397
|
+
rebuild(optimistic_locals: @optimistic_locals.merge(name.to_sym => mark).freeze)
|
|
301
398
|
end
|
|
302
399
|
|
|
303
|
-
def with_optimistic_ivar(name, cause)
|
|
400
|
+
def with_optimistic_ivar(name, cause, miss: Inference::OptimisticOrigin::UNKNOWN_MISS)
|
|
304
401
|
return self if cause.nil?
|
|
305
402
|
|
|
306
|
-
|
|
403
|
+
mark = Inference::OptimisticOrigin.bound_mark(cause, miss)
|
|
404
|
+
rebuild(optimistic_ivars: @optimistic_ivars.merge(name.to_sym => mark).freeze)
|
|
307
405
|
end
|
|
308
406
|
|
|
309
407
|
# ADR-82 WD1 — the propagated origin of the `Dynamic` value currently bound to a local / instance
|
|
@@ -331,6 +429,13 @@ module Rigor
|
|
|
331
429
|
end
|
|
332
430
|
|
|
333
431
|
def with_local(name, type)
|
|
432
|
+
bind_local(name, type, keep_marks: false)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# The one body behind {#with_local} and {#with_mutated_local}, which differ only in whether the two marks a
|
|
436
|
+
# write drops survive. Passing the kept sets through unchanged, rather than dropping and re-adding them, keeps
|
|
437
|
+
# a mutation's rebind to the one `rebuild` a write costs.
|
|
438
|
+
def bind_local(name, type, keep_marks:)
|
|
334
439
|
# `rigor trace` — the moment a local enters the scope.
|
|
335
440
|
Inference::FlowTracer.bind(name, type) if Inference::FlowTracer.active?
|
|
336
441
|
new_locals = @locals.merge(name.to_sym => type).freeze
|
|
@@ -349,14 +454,16 @@ module Rigor
|
|
|
349
454
|
rebuild(locals: new_locals, fact_store: new_fact_store,
|
|
350
455
|
indexed_narrowings: new_indexed_narrowings,
|
|
351
456
|
method_chain_narrowings: new_chain_narrowings,
|
|
352
|
-
declaration_sourced:
|
|
457
|
+
declaration_sourced: keep_marks ? @declaration_sourced : drop_local_declaration_marks(name),
|
|
353
458
|
# Issue #667 — rebinding is flow-live for the published-constant mark too: the new value need
|
|
354
459
|
# not be a copy of anything. `with_published_constant_mark` re-stamps afterward when the write's
|
|
355
460
|
# rvalue is one.
|
|
356
461
|
published_constant_sourced: drop_published_constant_sourced_for(:local, name),
|
|
357
462
|
local_origins: drop_origin(@local_origins, name),
|
|
358
|
-
optimistic_locals: drop_origin(@optimistic_locals, name)
|
|
463
|
+
optimistic_locals: keep_marks ? @optimistic_locals : drop_origin(@optimistic_locals, name),
|
|
464
|
+
bot_guard_classes: drop_bot_guard_class(:local, name))
|
|
359
465
|
end
|
|
466
|
+
private :bind_local
|
|
360
467
|
|
|
361
468
|
def with_fact(fact)
|
|
362
469
|
rebuild(fact_store: fact_store.with_fact(fact))
|
|
@@ -420,6 +527,27 @@ module Rigor
|
|
|
420
527
|
# True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
|
|
421
528
|
def opaque_block_self? = @opaque_block_self
|
|
422
529
|
|
|
530
|
+
# Issue #963 — marks the body of a `class << ...` as such. Inside it `self` is the SINGLETON class, which
|
|
531
|
+
# Rigor models with the same `Singleton[X]` carrier a `class X` body gets, so the carrier alone cannot say
|
|
532
|
+
# which of the two a scope came from. The distinction decides what `define_method` does: in a `class << self`
|
|
533
|
+
# body it defines a CLASS method (`self` there is the singleton class), while in every other body reached
|
|
534
|
+
# with a `Singleton[X]` self — a `class X` body, `def self.x`, a `def` inside `class << self` — `self` is the
|
|
535
|
+
# class object and the same call defines an INSTANCE method.
|
|
536
|
+
#
|
|
537
|
+
# The mark is stamped at singleton-class-body entry and inherited by every scope derived inside it, blocks
|
|
538
|
+
# included; a `def` body starts from a fresh scope and therefore clears it by construction, which is exactly
|
|
539
|
+
# the boundary the distinction needs. A meta-class body (`Class.new do ... end`) clears it explicitly: that
|
|
540
|
+
# block is a class body of its own, whatever encloses it.
|
|
541
|
+
def with_singleton_class_body(flag)
|
|
542
|
+
return self if @singleton_class_body == flag
|
|
543
|
+
|
|
544
|
+
rebuild(singleton_class_body: flag)
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# True when this scope IS a `class << ...` body (not merely inside one lexically — a `def` reached from it
|
|
548
|
+
# answers false).
|
|
549
|
+
def singleton_class_body? = @singleton_class_body
|
|
550
|
+
|
|
423
551
|
# True when `name`'s `Struct` member reads are fold-safe in this body (the local is provably never mutated /
|
|
424
552
|
# aliased / escaped).
|
|
425
553
|
def struct_fold_safe?(name)
|
|
@@ -450,6 +578,11 @@ module Rigor
|
|
|
450
578
|
end
|
|
451
579
|
|
|
452
580
|
def with_ivar(name, type)
|
|
581
|
+
bind_ivar(name, type, @guard_records)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# The one body behind {#with_ivar} and {#with_guarded_ivar}, which differ only in the guard records they leave.
|
|
585
|
+
def bind_ivar(name, type, guard_records)
|
|
453
586
|
new_indexed_narrowings = drop_indexed_narrowings_for(:ivar, name)
|
|
454
587
|
new_chain_narrowings = drop_chain_narrowings_for(:ivar, name)
|
|
455
588
|
# ADR-58 WD1 — a method-local ivar write or narrowing is flow-live: drop any declaration-sourced mark so
|
|
@@ -461,8 +594,11 @@ module Rigor
|
|
|
461
594
|
declaration_sourced: drop_declaration_sourced_for(:ivar, name),
|
|
462
595
|
published_constant_sourced: drop_published_constant_sourced_for(:ivar, name),
|
|
463
596
|
ivar_origins: drop_origin(@ivar_origins, name),
|
|
464
|
-
optimistic_ivars: drop_origin(@optimistic_ivars, name)
|
|
597
|
+
optimistic_ivars: drop_origin(@optimistic_ivars, name),
|
|
598
|
+
guard_records: guard_records,
|
|
599
|
+
bot_guard_classes: drop_bot_guard_class(:ivar, name))
|
|
465
600
|
end
|
|
601
|
+
private :bind_ivar
|
|
466
602
|
|
|
467
603
|
# ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only provenance is the class-ivar index.
|
|
468
604
|
# Unlike `with_ivar` this binds the type AND records the declaration-sourced mark in one transition.
|
|
@@ -486,6 +622,20 @@ module Rigor
|
|
|
486
622
|
rebuild(declaration_sourced: add_declaration_sourced(:local, name))
|
|
487
623
|
end
|
|
488
624
|
|
|
625
|
+
# Issue #1287 — rebinds `name` for an in-place mutation of the object it already holds: a mutator's widening
|
|
626
|
+
# (`r << x`), a content floor after a closure or callee mutated it, an element or member write through it. The
|
|
627
|
+
# binding still names the same object, so this is not a flow-live write, and the marks `with_local` drops stay:
|
|
628
|
+
# ADR-58's declaration-sourced mark and issue #286's optimistic nil-freeness mark, with the miss answer issue
|
|
629
|
+
# #1302 records in it. A source-level write keeps going through `with_local`, which drops both.
|
|
630
|
+
#
|
|
631
|
+
# Two other per-local tables are deliberately still dropped. An ADR-82 `local_origins` cause explains the
|
|
632
|
+
# `Dynamic` the ASSIGNMENT bound, and a floor's `Dynamic[top]` has a different cause. Issue #667's
|
|
633
|
+
# published-constant mark can never be live here: only a frozen scalar publishes, and a frozen value is not
|
|
634
|
+
# mutated in place.
|
|
635
|
+
def with_mutated_local(name, type)
|
|
636
|
+
bind_local(name, type, keep_marks: true)
|
|
637
|
+
end
|
|
638
|
+
|
|
489
639
|
# Issue #667 — record that `name` is currently bound to a value copied out of a foreign published
|
|
490
640
|
# constant. Always applied AFTER the `with_local` / `with_ivar` that binds the value (both drop the mark
|
|
491
641
|
# unconditionally), exactly as {#with_local_declaration_mark} is.
|
|
@@ -505,6 +655,8 @@ module Rigor
|
|
|
505
655
|
# ADR-58 WD1 — true when `(kind, name)`'s binding optionality is purely declaration-sourced (no flow-live
|
|
506
656
|
# write/narrowing has touched it).
|
|
507
657
|
def declaration_sourced?(kind, name)
|
|
658
|
+
return false if @declaration_sourced.empty?
|
|
659
|
+
|
|
508
660
|
@declaration_sourced.include?([kind.to_sym, name.to_sym])
|
|
509
661
|
end
|
|
510
662
|
|
|
@@ -548,24 +700,324 @@ module Rigor
|
|
|
548
700
|
rebuild(cvars: @cvars.merge(name.to_sym => type).freeze)
|
|
549
701
|
end
|
|
550
702
|
|
|
703
|
+
# Issue #1362 — a write or narrowing of a global is flow-live, so it drops the ADR-58 `:global` mark
|
|
704
|
+
# {#seed_declaration_sourced_global} stamped on the program-global seed.
|
|
705
|
+
#
|
|
706
|
+
# Issue #1429 — `$stdout` and `$>` are one variable, so a write to either also restores a guard's narrowing of the
|
|
707
|
+
# other ({#forget_guard_narrowing_of}).
|
|
551
708
|
def with_global(name, type)
|
|
552
|
-
|
|
709
|
+
name = name.to_sym
|
|
710
|
+
written = rebuild(globals: @globals.merge(name => type).freeze,
|
|
711
|
+
declaration_sourced: drop_declaration_sourced_for(:global, name),
|
|
712
|
+
guard_records: drop_guard_record(:global, name),
|
|
713
|
+
bot_guard_classes: drop_bot_guard_class(:global, name))
|
|
714
|
+
alias_name = STDOUT_ALIASES[name]
|
|
715
|
+
alias_name ? written.forget_guard_narrowing_of(:global, alias_name) : written
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
STDOUT_ALIASES = { :$stdout => :$>, :$> => :$stdout }.freeze
|
|
719
|
+
private_constant :STDOUT_ALIASES
|
|
720
|
+
|
|
721
|
+
# Issue #1429 — {#forget_guard_narrowings} for the one name `[kind, name]`.
|
|
722
|
+
def forget_guard_narrowing_of(kind, name)
|
|
723
|
+
key = [kind, name]
|
|
724
|
+
pre_guard = @guard_records[key]
|
|
725
|
+
return self if pre_guard.nil?
|
|
726
|
+
|
|
727
|
+
if kind == :global
|
|
728
|
+
current = @globals[name]
|
|
729
|
+
globals = current ? @globals.merge(name => Type::Combinator.union(pre_guard, current)).freeze : @globals
|
|
730
|
+
rebuild(globals: globals, guard_records: @guard_records.except(key).freeze)
|
|
731
|
+
else
|
|
732
|
+
current = @constant_narrowings[name]
|
|
733
|
+
constants = if current
|
|
734
|
+
@constant_narrowings.merge(name => Type::Combinator.union(pre_guard, current)).freeze
|
|
735
|
+
else
|
|
736
|
+
@constant_narrowings
|
|
737
|
+
end
|
|
738
|
+
rebuild(constant_narrowings: constants, guard_records: @guard_records.except(key).freeze)
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# Issue #1429 — binds the global `name` to `type` on a guard's edge, recording `pre_guard`, the binding the guard
|
|
743
|
+
# narrowed, unless an earlier guard already recorded one ({EMPTY_GUARD_RECORDS}). `record: false` is the
|
|
744
|
+
# frame-local specials' form, which binds without a record: their own machinery forgets them.
|
|
745
|
+
def with_guarded_global(name, type, pre_guard, record: true)
|
|
746
|
+
name = name.to_sym
|
|
747
|
+
records = record ? add_guard_record([:global, name].freeze, pre_guard) : drop_guard_record(:global, name)
|
|
748
|
+
rebuild(globals: @globals.merge(name => type).freeze,
|
|
749
|
+
declaration_sourced: drop_declaration_sourced_for(:global, name),
|
|
750
|
+
guard_records: records,
|
|
751
|
+
bot_guard_classes: drop_bot_guard_class(:global, name))
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# Issue #1446 — binds the instance variable `name` to `type` on a class guard's edge, recording `pre_guard` as
|
|
755
|
+
# {#with_guarded_global} does. The truthiness, `nil?`, `&.` and `respond_to?` guards narrow an instance variable
|
|
756
|
+
# through {#with_ivar} and record nothing.
|
|
757
|
+
def with_guarded_ivar(name, type, pre_guard)
|
|
758
|
+
name = name.to_sym
|
|
759
|
+
bind_ivar(name, type, add_guard_record([:ivar, name].freeze, pre_guard))
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# Issue #1446 — this scope with no guard record for the instance variable `name`: what a write to it leaves.
|
|
763
|
+
# {#with_ivar} keeps the record, since a narrowing binds through it too.
|
|
764
|
+
def without_ivar_guard(name)
|
|
765
|
+
records = drop_guard_record(:ivar, name.to_sym)
|
|
766
|
+
records.equal?(@guard_records) ? self : rebuild(guard_records: records)
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
# Issue #1446 — true when a class guard's narrowing of the instance variable `name` is live.
|
|
770
|
+
def guard_narrowed_ivar?(name)
|
|
771
|
+
!@guard_records.empty? && @guard_records.key?([:ivar, name.to_sym])
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# Issue #1429 — the type a guard narrowed the constant reference `key` to, or nil.
|
|
775
|
+
def constant_narrowing(key)
|
|
776
|
+
return nil if @constant_narrowings.empty?
|
|
777
|
+
|
|
778
|
+
@constant_narrowings[key]
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
# Issue #1429 — the constant reference `key` read as `type` on a guard's edge, recording `pre_guard` as
|
|
782
|
+
# {#with_guarded_global} does.
|
|
783
|
+
def with_constant_narrowing(key, type, pre_guard)
|
|
784
|
+
rebuild(constant_narrowings: @constant_narrowings.merge(key => type).freeze,
|
|
785
|
+
guard_records: add_guard_record([:constant, key].freeze, pre_guard),
|
|
786
|
+
bot_guard_classes: drop_bot_guard_class(:constant, key))
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# Issue #1446 — this scope with `class_names` recorded as the classes a class guard narrowed the receiver `[kind,
|
|
790
|
+
# name]` to, where the guard left it `bot` ({EMPTY_BOT_GUARD_CLASSES}). Call it after the narrowing, which drops
|
|
791
|
+
# any earlier entry.
|
|
792
|
+
def with_bot_guard_classes(kind, name, class_names)
|
|
793
|
+
key = [kind, kind == :constant ? name : name.to_sym].freeze
|
|
794
|
+
rebuild(bot_guard_classes: @bot_guard_classes.merge(key => class_names.dup.freeze).freeze)
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
# Issue #1446 — the classes a class guard narrowed the `bot` receiver `[kind, name]` to, or nil.
|
|
798
|
+
def bot_guard_classes_for(kind, name)
|
|
799
|
+
return nil if @bot_guard_classes.empty?
|
|
800
|
+
|
|
801
|
+
@bot_guard_classes[[kind, name]]
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
# Issue #1429 — every constant reference whose last segment is `name` with no narrowing: what a write to a constant
|
|
805
|
+
# named `name` leaves, since `Foo::BAR` and `BAR` may name the one it wrote.
|
|
806
|
+
def without_constant_narrowings_named(name)
|
|
807
|
+
keys = @constant_narrowings.keys.select { |key| key.split("::").last == name }
|
|
808
|
+
return self if keys.empty?
|
|
809
|
+
|
|
810
|
+
records = @guard_records.reject { |(kind, key), _| kind == :constant && keys.include?(key) }
|
|
811
|
+
classes = @bot_guard_classes.except(*keys.map { |key| [:constant, key] }).freeze
|
|
812
|
+
rebuild(constant_narrowings: @constant_narrowings.except(*keys).freeze, guard_records: records.freeze,
|
|
813
|
+
bot_guard_classes: classes)
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
# Issue #1429 — the constant reference `key` with no narrowing.
|
|
817
|
+
def without_constant_narrowing(key)
|
|
818
|
+
return self unless @constant_narrowings.key?(key)
|
|
819
|
+
|
|
820
|
+
rebuild(constant_narrowings: @constant_narrowings.except(key).freeze,
|
|
821
|
+
guard_records: @guard_records.except([:constant, key]).freeze,
|
|
822
|
+
bot_guard_classes: drop_bot_guard_class(:constant, key))
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# True when a guard's narrowing of a global, constant or instance variable is live, the state
|
|
826
|
+
# {#forget_guard_narrowings} drops and so the gate on every scan that decides whether to.
|
|
827
|
+
def guard_narrowed?
|
|
828
|
+
!@guard_records.empty?
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
# Issue #1429 — this scope past code that may rebind a global or a constant: each one a guard narrowed reads
|
|
832
|
+
# the union of its narrowed type and the binding the guard narrowed, and no record is left. The union, not the
|
|
833
|
+
# pre-guard binding alone, because the code may leave the value as it was: `$stdout.is_a?(StringIO)`, then a
|
|
834
|
+
# helper, reads `IO | StringIO`, which keeps `$stdout.string` quiet as the guard intended.
|
|
835
|
+
#
|
|
836
|
+
# Issue #1446 — an instance variable a class guard narrowed is restored the same way.
|
|
837
|
+
def forget_guard_narrowings
|
|
838
|
+
return self if @guard_records.empty?
|
|
839
|
+
|
|
840
|
+
tables = { global: @globals, constant: @constant_narrowings, ivar: @ivars }
|
|
841
|
+
@guard_records.each do |(kind, name), pre_guard|
|
|
842
|
+
table = tables.fetch(kind)
|
|
843
|
+
current = table[name]
|
|
844
|
+
tables[kind] = table.merge(name => Type::Combinator.union(pre_guard, current)) if current
|
|
845
|
+
end
|
|
846
|
+
globals, constants, ivars = tables.values_at(:global, :constant, :ivar).map { |t| t.frozen? ? t : t.freeze }
|
|
847
|
+
rebuild(globals: globals, constant_narrowings: constants, ivars: ivars, guard_records: EMPTY_GUARD_RECORDS)
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
# Issue #1362 (ADR-58 parity, ADR-117 Decision point 2) — used by the method-entry and top-level seeds to bind a
|
|
851
|
+
# global Ruby's own signatures declare to its declared type joined with the file's writes, and to record that the
|
|
852
|
+
# binding is still that seed. The declared members are real type information but not diagnostic fuel
|
|
853
|
+
# ({Analysis::CheckRules::DeclarationSourcedGuard}); a write or narrowing drops the mark ({#with_global}).
|
|
854
|
+
def seed_declaration_sourced_global(name, type)
|
|
855
|
+
rebuild(globals: @globals.merge(name.to_sym => type).freeze,
|
|
856
|
+
declaration_sourced: add_declaration_sourced(:global, name))
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# Issue #1362 — a local written from a read of a marked global (`sep = $/`) carries ADR-58's `:local` mark, and
|
|
860
|
+
# this records the globals it may copy, so a consumer can compare against the file's writes to them
|
|
861
|
+
# ({#declaration_sourced_global_copies}). Applied after {#with_declaration_sourced_local}; the local's next
|
|
862
|
+
# rebinding drops both ({#bind_local}), and a join of two copies keeps both branches' globals.
|
|
863
|
+
def with_global_copy_marks(name, globals)
|
|
864
|
+
name = name.to_sym
|
|
865
|
+
added = globals.map { |global| [:global_copy, name, global.to_sym].freeze }
|
|
866
|
+
.reject { |ref| @declaration_sourced.include?(ref) }
|
|
867
|
+
return self if added.empty?
|
|
868
|
+
|
|
869
|
+
rebuild(declaration_sourced: @declaration_sourced.dup.merge(added).freeze)
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
# Issue #1362 — this scope with the local `name`'s ADR-58 mark and copy record dropped and its binding kept: a
|
|
873
|
+
# retried pass that re-enters with a binding the accumulated one accepts, but from another source.
|
|
874
|
+
def without_local_declaration_marks(name)
|
|
875
|
+
dropped = drop_local_declaration_marks(name)
|
|
876
|
+
dropped.equal?(@declaration_sourced) ? self : rebuild(declaration_sourced: dropped)
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
# The globals the local `name` is a marked copy of ({#with_global_copy_marks}), empty for a local that copies
|
|
880
|
+
# no marked global.
|
|
881
|
+
def declaration_sourced_global_copies(name)
|
|
882
|
+
return EMPTY_GLOBAL_COPIES unless declaration_sourced?(:local, name)
|
|
883
|
+
|
|
884
|
+
name = name.to_sym
|
|
885
|
+
@declaration_sourced.filter_map { |ref| ref[2] if ref[0] == :global_copy && ref[1] == name }
|
|
886
|
+
end
|
|
887
|
+
EMPTY_GLOBAL_COPIES = [].freeze
|
|
888
|
+
private_constant :EMPTY_GLOBAL_COPIES
|
|
889
|
+
|
|
890
|
+
# Mark `nodes`, index `||=` sites, as ones whose slot an earlier run of a repeating block body may have
|
|
891
|
+
# filled ({EMPTY_REPEATED_OR_WRITES}).
|
|
892
|
+
def with_repeated_or_writes(nodes)
|
|
893
|
+
return self if nodes.all? { |node| @repeated_or_writes.key?(node) }
|
|
894
|
+
|
|
895
|
+
marked = @repeated_or_writes.dup
|
|
896
|
+
nodes.each { |node| marked[node] = true }
|
|
897
|
+
rebuild(repeated_or_writes: marked.freeze)
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
# True when {#with_repeated_or_writes} marked the index `||=` node `node` (by identity).
|
|
901
|
+
def repeated_or_write?(node)
|
|
902
|
+
return false if @repeated_or_writes.empty?
|
|
903
|
+
|
|
904
|
+
@repeated_or_writes.key?(node)
|
|
553
905
|
end
|
|
554
906
|
|
|
555
907
|
# Regex match-data globals (`$~`, `$&`, `$1..$9`, the pre/post-match and last-paren back-references). Narrowed
|
|
556
|
-
# on a successful-`=~` / `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`)
|
|
557
|
-
#
|
|
558
|
-
#
|
|
559
|
-
#
|
|
908
|
+
# on a successful-`=~` / `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`). They live in
|
|
909
|
+
# the method frame's special-variable slot, which the method's blocks and closures share (issue #1358), so a
|
|
910
|
+
# later call that may run a match in this frame — a match-capable call, a block that may match, a call once
|
|
911
|
+
# the frame has made a closure that may — rebinds every one of them, and `eval_call` forgets the narrowed
|
|
912
|
+
# facts here. Always safe — only drops facts, so a subsequent read falls back to the default `String | nil`.
|
|
913
|
+
# Program-level `$GLOBAL = ...` seeds use other names and are untouched.
|
|
560
914
|
MATCH_DATA_GLOBALS = %i[$~ $& $` $' $+ $1 $2 $3 $4 $5 $6 $7 $8 $9].freeze
|
|
561
915
|
private_constant :MATCH_DATA_GLOBALS
|
|
562
916
|
|
|
563
917
|
def forget_match_globals
|
|
564
|
-
return self unless
|
|
918
|
+
return self unless match_globals_bound?
|
|
565
919
|
|
|
566
920
|
rebuild(globals: @globals.except(*MATCH_DATA_GLOBALS).freeze)
|
|
567
921
|
end
|
|
568
922
|
|
|
923
|
+
# Issue #1361 — this scope with every bound match-data global rebound to `Dynamic[top]`, and an unbound one left
|
|
924
|
+
# unbound: the view a `define_method` / `define_singleton_method` body enters with
|
|
925
|
+
# ({Inference::FreshFrameBlocks.entry}). The body reads the defining frame's slot whenever the method is called,
|
|
926
|
+
# which the narrowing where it is written neither proves nor refutes, so it is neither narrowed nor flagged.
|
|
927
|
+
def untyped_match_globals
|
|
928
|
+
return self unless match_globals_bound?
|
|
929
|
+
|
|
930
|
+
untyped = Type::Combinator.untyped
|
|
931
|
+
rebound = MATCH_DATA_GLOBALS.each_with_object({}) { |name, acc| acc[name] = untyped if @globals.key?(name) }
|
|
932
|
+
rebuild(globals: @globals.merge(rebound).freeze)
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
# True when any match-data global holds a binding, which a match edge or {#untyped_match_globals} makes: the only
|
|
936
|
+
# state {#forget_match_globals} can drop, and so the gate on every scan that decides whether to.
|
|
937
|
+
def match_globals_bound?
|
|
938
|
+
!@globals.empty? && MATCH_DATA_GLOBALS.any? { |name| @globals.key?(name) }
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
# Issue #1359 — the last line read, `$_`, lives in the same frame slot as the match globals, and so on the same
|
|
942
|
+
# terms: a `gets`-family call, a write, or a block or closure of the frame that may run either rebinds it
|
|
943
|
+
# ({Inference::LastLine}). It is bound only where a condition on a reader narrows it, or where code writes it.
|
|
944
|
+
LAST_LINE = :$_
|
|
945
|
+
private_constant :LAST_LINE
|
|
946
|
+
|
|
947
|
+
def forget_last_line
|
|
948
|
+
return self unless last_line_bound?
|
|
949
|
+
|
|
950
|
+
rebuild(globals: @globals.except(LAST_LINE).freeze)
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
# The `$_` half of {#untyped_match_globals}: a bound `$_` rebound to `Dynamic[top]`, an unbound one left alone.
|
|
954
|
+
def untyped_last_line
|
|
955
|
+
return self unless last_line_bound?
|
|
956
|
+
|
|
957
|
+
rebuild(globals: @globals.merge(LAST_LINE => Type::Combinator.untyped).freeze)
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
# The gate on every scan that decides whether to forget `$_`, as {#match_globals_bound?} is for the match globals.
|
|
961
|
+
def last_line_bound?
|
|
962
|
+
!@globals.empty? && @globals.key?(LAST_LINE)
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
# Issue #1360 — the exception being rescued, `$!`, and its backtrace, `$@`. Ruby finds them through the nearest
|
|
966
|
+
# rescue frame of the running execution context, not through the method frame, so they are bound only inside a
|
|
967
|
+
# `rescue` clause or a rescue modifier's fallback and read their earlier binding again once it exits
|
|
968
|
+
# ({Inference::ErrorInfo}). `$?`, the status of the last child process, is thread-local, and is bound after a
|
|
969
|
+
# subprocess call ({Inference::LastStatus}).
|
|
970
|
+
ERROR_INFO_GLOBALS = %i[$! $@].freeze
|
|
971
|
+
LAST_STATUS_GLOBALS = %i[$?].freeze
|
|
972
|
+
private_constant :ERROR_INFO_GLOBALS, :LAST_STATUS_GLOBALS
|
|
973
|
+
|
|
974
|
+
# This scope with `$!` and `$@` unbound: the view of a body that runs outside the rescue clause it is written in.
|
|
975
|
+
def forget_error_info = forget_globals(ERROR_INFO_GLOBALS)
|
|
976
|
+
|
|
977
|
+
# This scope with a bound `$!` or `$@` rebound to `Dynamic[top]`, and an unbound one left unbound, as
|
|
978
|
+
# {#untyped_match_globals} gives a `define_method` body.
|
|
979
|
+
def untyped_error_info = untyped_globals(ERROR_INFO_GLOBALS)
|
|
980
|
+
|
|
981
|
+
# This scope with `$?` unbound: the view of a body that may run on another thread.
|
|
982
|
+
def forget_last_status = forget_globals(LAST_STATUS_GLOBALS)
|
|
983
|
+
|
|
984
|
+
# The `$?` half of {#untyped_error_info}.
|
|
985
|
+
def untyped_last_status = untyped_globals(LAST_STATUS_GLOBALS)
|
|
986
|
+
|
|
987
|
+
def forget_globals(names)
|
|
988
|
+
return self if @globals.empty? || names.none? { |name| @globals.key?(name) }
|
|
989
|
+
|
|
990
|
+
rebuild(globals: @globals.except(*names).freeze)
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
def untyped_globals(names)
|
|
994
|
+
return self if @globals.empty? || names.none? { |name| @globals.key?(name) }
|
|
995
|
+
|
|
996
|
+
untyped = Type::Combinator.untyped
|
|
997
|
+
rebound = names.each_with_object({}) { |name, acc| acc[name] = untyped if @globals.key?(name) }
|
|
998
|
+
rebuild(globals: @globals.merge(rebound).freeze)
|
|
999
|
+
end
|
|
1000
|
+
private :forget_globals, :untyped_globals
|
|
1001
|
+
|
|
1002
|
+
# Issue #1358 — stamps the frame `body` runs in ({Inference::MatchRebinding::Frame}) on a method, class or
|
|
1003
|
+
# file body's entry scope; a method passes its `parameters` too, whose defaults run in the same frame. Every
|
|
1004
|
+
# scope derived from it, a block's included, runs in that frame.
|
|
1005
|
+
def with_match_frame(body, parameters = nil)
|
|
1006
|
+
rebuild(match_frame: Inference::MatchRebinding::Frame.new(body, parameters))
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
# True when this scope's frame makes a closure that may rebind its match globals whenever it is invoked
|
|
1010
|
+
# ({Inference::MatchRebinding.matching_closure?}). False where no body stamped a frame.
|
|
1011
|
+
def match_rebinding_closure?
|
|
1012
|
+
!@match_frame.nil? && @match_frame.matching_closure?(self)
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
# True when this scope's frame makes a closure that may set its `$_` whenever it is invoked
|
|
1016
|
+
# ({Inference::LastLine.closure?}). False where no body stamped a frame.
|
|
1017
|
+
def last_line_closure?
|
|
1018
|
+
!@match_frame.nil? && @match_frame.last_line_closure?(self)
|
|
1019
|
+
end
|
|
1020
|
+
|
|
569
1021
|
# Slice 7 phase 2 — class-level ivar accumulator. Keyed by the qualified class name (e.g. `"Rigor::Scope"`);
|
|
570
1022
|
# the value is a `Hash[Symbol, Type::t]` of every ivar that appears as a write target inside any def body of
|
|
571
1023
|
# that class. `StatementEvaluator#build_method_entry_scope` seeds the method body's `ivars` map from this
|
|
@@ -612,6 +1064,18 @@ module Rigor
|
|
|
612
1064
|
@self_type.nil?
|
|
613
1065
|
end
|
|
614
1066
|
|
|
1067
|
+
# ADR-119 WD1 — the RAW slot of the def tables for `class_name` and `method_name`: the instance side by default,
|
|
1068
|
+
# the singleton side for `kind: :singleton`. The slot is a live `Prism::DefNode` or a {Inference::DefHandle}
|
|
1069
|
+
# (ADR-85 WD3), returned as stored: no dependency recording and no handle resolution. ADR-119 C1d/C2 decide how
|
|
1070
|
+
# a contested slot answers here. Callers that need a node go through {#user_def_for} or {#singleton_def_for}.
|
|
1071
|
+
# The `same_slot` census reads of this slot stay justified: certainty reaches them through the declaration
|
|
1072
|
+
# signature the producer fingerprints (C1d-a), not through this accessor.
|
|
1073
|
+
def def_node_slot(class_name, method_name, kind = :instance)
|
|
1074
|
+
table = kind == :singleton ? discovered_singleton_def_nodes : discovered_def_nodes
|
|
1075
|
+
per_class = table[class_name]
|
|
1076
|
+
per_class && per_class[method_name]
|
|
1077
|
+
end
|
|
1078
|
+
|
|
615
1079
|
# v0.0.2 #5 — per-class table mapping `method_name (Symbol) → Prism::DefNode`. Populated by `ScopeIndexer`
|
|
616
1080
|
# alongside `discovered_methods` for instance-side defs only (singleton-side and `define_method`-introduced
|
|
617
1081
|
# methods do not contribute a static body the engine can re-type). Consumed by `ExpressionTyper` to do
|
|
@@ -699,12 +1163,15 @@ module Rigor
|
|
|
699
1163
|
same_file_top_level_def?(method_name) ? node : nil
|
|
700
1164
|
end
|
|
701
1165
|
|
|
1166
|
+
# ADR-119 WD1 errata, deliberately unchanged: this compares the recorded site's FILE with the call's file, and
|
|
1167
|
+
# which file a def sits in does not depend on whether it executes. The `<toplevel>` slots are contested
|
|
1168
|
+
# truthfully (ADR-119 C1d-a) but unread until C2 makes the top-level reader decline on them.
|
|
702
1169
|
def same_file_top_level_def?(method_name)
|
|
703
1170
|
key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
704
1171
|
site = discovered_def_sources.dig(key, method_name.to_sym)
|
|
705
1172
|
return true if site.nil? || @source_path.nil?
|
|
706
1173
|
|
|
707
|
-
File.
|
|
1174
|
+
File.absolute_path(site.sub(/:\d+\z/, "")) == File.absolute_path(@source_path)
|
|
708
1175
|
end
|
|
709
1176
|
private :same_file_top_level_def?
|
|
710
1177
|
|
|
@@ -756,6 +1223,86 @@ module Rigor
|
|
|
756
1223
|
site
|
|
757
1224
|
end
|
|
758
1225
|
|
|
1226
|
+
# Issue #1097 — whether `class_name`'s own singleton `def method_name` has RUN by the time `call_node`
|
|
1227
|
+
# executes. Such a def precedes every `extend` in the singleton ancestry, so once it exists it owns the
|
|
1228
|
+
# call — but `sig { ... }` written BEFORE `def self.sig` in the same body still resolves through the
|
|
1229
|
+
# already-extended module, because `def` takes effect at execution. The site table stores
|
|
1230
|
+
# `"path:line"`: ordering applies only to calls executed eagerly in the same file's class body — a
|
|
1231
|
+
# call inside a def body runs at invocation time, and a def in another file can never be ordered
|
|
1232
|
+
# against the call site, so both conservatively count as shadowing. A nil `call_node`
|
|
1233
|
+
# (position-less dispatch probes) does the same.
|
|
1234
|
+
def singleton_def_shadows_call?(class_name, method_name, call_node)
|
|
1235
|
+
def_shadows_call?(user_singleton_def_site_for(class_name, method_name), class_name, method_name,
|
|
1236
|
+
:singleton, call_node)
|
|
1237
|
+
end
|
|
1238
|
+
|
|
1239
|
+
# The instance-side twin of {#singleton_def_shadows_call?}: whether `class_name`'s `def method_name`
|
|
1240
|
+
# has run by call time — for `extend M` edges, where M's instance surface is what answers.
|
|
1241
|
+
def instance_def_shadows_call?(class_name, method_name, call_node)
|
|
1242
|
+
def_shadows_call?(user_def_site_for(class_name, method_name), class_name, method_name, :instance,
|
|
1243
|
+
call_node)
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1246
|
+
# Shared ordering half of the two `*_def_shadows_call?` predicates, over this file's
|
|
1247
|
+
# `discovered_deferred_ranges` (issue #1097). `exists` — a recorded `"path:line"` site or the
|
|
1248
|
+
# method-existence table — says a def of `method_name`/`kind` on `class_name` is known at all; a
|
|
1249
|
+
# discovered def with no site (an `attr_*` sibling, a bundle seed that predates the table) counts
|
|
1250
|
+
# through `discovered_method?`. The ranges then answer the timing questions, and can supply defs
|
|
1251
|
+
# the site table missed (a `module_function`-installed `sig` never enters the singleton-def
|
|
1252
|
+
# table). A foreign-file site cannot be ordered against the call — shadowing. A nil `call_node`
|
|
1253
|
+
# (position-less dispatch probes) or a file the index never saw falls back to `exists`.
|
|
1254
|
+
def def_shadows_call?(site, class_name, method_name, kind, call_node)
|
|
1255
|
+
exists = !site.nil? || discovered_method?(class_name, method_name, kind)
|
|
1256
|
+
return exists if call_node.nil?
|
|
1257
|
+
|
|
1258
|
+
path = source_path
|
|
1259
|
+
return exists unless path
|
|
1260
|
+
|
|
1261
|
+
if site
|
|
1262
|
+
site_path, = site.rpartition(":")
|
|
1263
|
+
return true unless site_path == path
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
ranges = @discovery.discovered_deferred_ranges[path]
|
|
1267
|
+
return exists if ranges.nil?
|
|
1268
|
+
|
|
1269
|
+
deferred_ranges_shadow_call?(ranges, method_name.to_sym, kind, class_name.to_s,
|
|
1270
|
+
call_node.location, exists)
|
|
1271
|
+
end
|
|
1272
|
+
private :def_shadows_call?
|
|
1273
|
+
|
|
1274
|
+
# The range scan behind {#def_shadows_call?}. A call CONTAINED in any def / block / lambda /
|
|
1275
|
+
# `END` range is deferred — it runs at invocation time — so it is shadowed iff a matching def is
|
|
1276
|
+
# known (`exists`, or a row the site table missed). An EAGER call is shadowed iff the earliest
|
|
1277
|
+
# same-name, same-kind (`:both` matches either), same-OWNER row starts before it: owner scoping
|
|
1278
|
+
# keeps `class A`'s defs from ordering `class F`'s calls, and earliest-of keeps
|
|
1279
|
+
# `def self.sig; sig {}; def self.sig` honest where the def-node table is later-wins. With no
|
|
1280
|
+
# matching row the answer is `exists` — "a def exists but cannot be ordered" stays conservative,
|
|
1281
|
+
# "no def at all" stays permissive.
|
|
1282
|
+
def deferred_ranges_shadow_call?(ranges, method_name, kind, class_name, call_loc, exists)
|
|
1283
|
+
contained = false
|
|
1284
|
+
matching = false
|
|
1285
|
+
earliest = nil
|
|
1286
|
+
ranges.each do |(start, finish, name, def_kind, owner)|
|
|
1287
|
+
contained ||= start <= call_loc.start_offset && call_loc.end_offset <= finish
|
|
1288
|
+
next unless deferred_row_orders_call?(name, def_kind, owner, method_name, kind, class_name)
|
|
1289
|
+
|
|
1290
|
+
matching = true
|
|
1291
|
+
earliest = start if earliest.nil? || start < earliest
|
|
1292
|
+
end
|
|
1293
|
+
return exists || matching if contained
|
|
1294
|
+
|
|
1295
|
+
earliest.nil? ? exists : earliest <= call_loc.start_offset
|
|
1296
|
+
end
|
|
1297
|
+
private :deferred_ranges_shadow_call?
|
|
1298
|
+
|
|
1299
|
+
# Whether a range row can order the call: same method name, same qualified owner, and a def kind
|
|
1300
|
+
# that answers this predicate's side (`:both` — `module_function` — matches either).
|
|
1301
|
+
def deferred_row_orders_call?(name, def_kind, owner, method_name, kind, class_name)
|
|
1302
|
+
name == method_name && owner == class_name && (def_kind == kind || def_kind == :both)
|
|
1303
|
+
end
|
|
1304
|
+
private :deferred_row_orders_call?
|
|
1305
|
+
|
|
759
1306
|
# ADR-24 slice 2 — per-class table mapping a fully qualified user-class name to its superclass name AS WRITTEN
|
|
760
1307
|
# at the `class Foo < Bar` declaration (`"Bar"`, possibly a qualified `"A::B"`). Populated by `ScopeIndexer` —
|
|
761
1308
|
# per-file plus the cross-file project pre-pass — and consumed by
|
|
@@ -792,10 +1339,15 @@ module Rigor
|
|
|
792
1339
|
end
|
|
793
1340
|
|
|
794
1341
|
# ADR-24 slice 2 — per-class/module table mapping a fully qualified user class or module to the list of
|
|
795
|
-
# module names it `include`s / `prepend`s,
|
|
1342
|
+
# module names it `include`s / `prepend`s, in instance-ancestor SEARCH order: prepended modules first
|
|
1343
|
+
# (Ruby inserts them ahead of the class itself), then included modules nearest-first — `include A;
|
|
1344
|
+
# include B` searches B before A, while `include A, B` keeps `["A", "B"]` (each statement's argument
|
|
1345
|
+
# list lands as one unit, ahead of the earlier statements'). Populated by `ScopeIndexer` (per-file
|
|
796
1346
|
# plus the cross-file pre-pass) and consumed by `ExpressionTyper#resolve_user_def_through_ancestors` so an
|
|
797
|
-
# implicit-self call resolves against an included module's `def`s, not just the superclass chain.
|
|
798
|
-
# names are resolved to qualified classes at walk time.
|
|
1347
|
+
# implicit-self call resolves against an included module's `def`s, not just the superclass chain.
|
|
1348
|
+
# As-written names are resolved to qualified classes at walk time. Issue #1173 — the list was call
|
|
1349
|
+
# order until then, a divergence from Ruby's later-include-wins the order-sensitive consumers (the
|
|
1350
|
+
# BFS mixin step, the external-ancestor walk, override visibility) silently absorbed.
|
|
799
1351
|
def includes_of(class_name)
|
|
800
1352
|
record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
|
|
801
1353
|
@discovery.discovered_includes[class_name.to_s] || []
|
|
@@ -829,118 +1381,130 @@ module Rigor
|
|
|
829
1381
|
names.uniq
|
|
830
1382
|
end
|
|
831
1383
|
|
|
832
|
-
# ADR-24
|
|
833
|
-
#
|
|
834
|
-
#
|
|
835
|
-
#
|
|
1384
|
+
# ADR-24, amended for #1567 / #1568 / #1570 / #1571 — every reader below answers in Ruby's linearised
|
|
1385
|
+
# ancestor order, read from {ResolutionChain} (an internal class: `Scope`'s own surface is the plugin API
|
|
1386
|
+
# `spec/rigor/public_api_drift_spec.rb` pins, and the chain is not part of it). None walks `includes_of` /
|
|
1387
|
+
# `superclass_of` / the extends table itself; `spec/rigor/scope/ancestry_walker_detection_spec.rb` fails on
|
|
1388
|
+
# a method that does. Each keeps its signature and its return type exactly: only WHICH definer it answers
|
|
1389
|
+
# moved. `name_memo:` is still accepted and no longer needed — the chain memoises name resolution itself.
|
|
1390
|
+
UNUSED_NAME_MEMO = {}.freeze
|
|
1391
|
+
private_constant :UNUSED_NAME_MEMO
|
|
836
1392
|
#
|
|
837
|
-
#
|
|
838
|
-
#
|
|
839
|
-
#
|
|
840
|
-
|
|
841
|
-
#
|
|
842
|
-
#
|
|
843
|
-
#
|
|
1393
|
+
# Every reader below asks {ResolutionChain#settle} whether its chain answer stands, and where it does not
|
|
1394
|
+
# answers what the walk it replaced answered ({ResolutionChain::MasterOrder}) — the tables cannot say which
|
|
1395
|
+
# world ran, and a disagreement is no reason to answer anything new.
|
|
1396
|
+
|
|
1397
|
+
# ADR-24 slice 2 — the user-side method lookup: the first project `def` of `method_name` along
|
|
1398
|
+
# `class_name`'s instance chain, as `[def_node, owner]`, or `[nil, nil]` when nothing the project declares
|
|
1399
|
+
# defines it. An external ancestor is passed over, as before: a caller asking "does the project define
|
|
1400
|
+
# this" must not read an RBS module's declaration as absence (#1572 is the typing read that will stop
|
|
1401
|
+
# there).
|
|
844
1402
|
#
|
|
845
|
-
#
|
|
846
|
-
#
|
|
847
|
-
#
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1403
|
+
# It lives HERE, not in a consumer, because it reads nothing but this scope's frozen discovery index — the
|
|
1404
|
+
# property that lets `ExpressionTyper` memoise its results run-wide. `ExpressionTyper#compute_user_def_with_owner`
|
|
1405
|
+
# wraps it in that memo for the dispatch hot path, and {Inference::MethodDispatcher::StructMaterialization}'s
|
|
1406
|
+
# `.with` guard reads the same answer (#598 review).
|
|
1407
|
+
#
|
|
1408
|
+
# #1567 — the walk was breadth-first until the chain replaced it: `class C < Base; include A` with `A`
|
|
1409
|
+
# including `M` answered `Base#foo` where Ruby calls `M#foo`. The prepend wedge #1123 added (a prepended
|
|
1410
|
+
# module searched ahead of the class's own `def`s) and the include search order #1173 fixed are both
|
|
1411
|
+
# positions on the chain now, at every class on it.
|
|
1412
|
+
def user_def_through_ancestors(class_name, method_name, name_memo: UNUSED_NAME_MEMO) # rubocop:disable Lint/UnusedMethodArgument
|
|
1413
|
+
chain = ResolutionChain.for(self, class_name.to_s, :instance, :methods)
|
|
1414
|
+
found = first_user_def(chain, method_name)
|
|
1415
|
+
owner = found&.last
|
|
1416
|
+
if chain.settle(self, owner, owner: owner) { |retro| first_user_def(retro, method_name)&.last } == :master
|
|
1417
|
+
found = master_user_def(class_name.to_s, method_name)
|
|
1418
|
+
end
|
|
1419
|
+
return found if found
|
|
855
1420
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
return ancestor_walk_gave_up if visited > ANCESTOR_WALK_LIMIT
|
|
1421
|
+
chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
|
|
1422
|
+
end
|
|
859
1423
|
|
|
860
|
-
|
|
861
|
-
|
|
1424
|
+
def first_user_def(chain, method_name)
|
|
1425
|
+
chain.search(self) do |entry|
|
|
1426
|
+
next if entry.external?
|
|
862
1427
|
|
|
863
|
-
|
|
1428
|
+
node = user_def_for(entry.name, method_name)
|
|
1429
|
+
[node, entry.name] if node
|
|
864
1430
|
end
|
|
865
|
-
[nil, nil]
|
|
866
1431
|
end
|
|
867
1432
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1433
|
+
def master_user_def(class_name, method_name)
|
|
1434
|
+
names = ResolutionChain::MasterOrder.definer_sequence(self, class_name)
|
|
1435
|
+
names.each { |name| record_class_dependency(name) } if Analysis::DependencyRecorder.active?
|
|
1436
|
+
names.each do |name|
|
|
1437
|
+
node = user_def_for(name, method_name)
|
|
1438
|
+
return [node, name] if node
|
|
1439
|
+
end
|
|
1440
|
+
nil
|
|
1441
|
+
end
|
|
1442
|
+
private :first_user_def, :master_user_def
|
|
1443
|
+
|
|
1444
|
+
# Issue #731 — the singleton-side twin of {#user_def_through_ancestors}: the first `def self.` /
|
|
1445
|
+
# `class << self` body of `method_name` along `class_name`'s singleton chain, as `[def_node, owner]`, or
|
|
1446
|
+
# `[nil, nil]` — each class object's own table, then the modules it extends in Ruby's order, then the
|
|
1447
|
+
# superclass's. `ScopeIndexer` folds an extended module's own `def`s into the extender's table, so those
|
|
1448
|
+
# answer at the extender; a module the extended one includes is its own entry (#1567's singleton shape:
|
|
1449
|
+
# `class C < Base; extend A` with `A` including `M` is `M#foo`, not `Base.foo`). Where the chain does
|
|
1450
|
+
# not stand, the answer is the class objects' alone, as the superclass-only walk this replaced gave.
|
|
1451
|
+
def singleton_def_through_ancestors(class_name, method_name, name_memo: UNUSED_NAME_MEMO) # rubocop:disable Lint/UnusedMethodArgument
|
|
1452
|
+
chain = ResolutionChain.for(self, class_name.to_s, :singleton, :methods)
|
|
1453
|
+
found = first_singleton_def(chain, method_name)
|
|
1454
|
+
owner = found&.last
|
|
1455
|
+
if chain.settle(self, owner, owner: owner) { |retro| first_singleton_def(retro, method_name)&.last } == :master
|
|
1456
|
+
found = first_singleton_def(chain, method_name, :singleton)
|
|
1457
|
+
end
|
|
1458
|
+
return found if found
|
|
885
1459
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
return ancestor_walk_gave_up if visited > ANCESTOR_WALK_LIMIT
|
|
1460
|
+
chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
|
|
1461
|
+
end
|
|
889
1462
|
|
|
890
|
-
|
|
891
|
-
|
|
1463
|
+
def first_singleton_def(chain, method_name, side = nil)
|
|
1464
|
+
chain.search(self, side: side) do |entry|
|
|
1465
|
+
next if entry.external?
|
|
892
1466
|
|
|
893
|
-
|
|
1467
|
+
name = entry.name
|
|
1468
|
+
node = entry.side == :singleton ? singleton_def_for(name, method_name) : user_def_for(name, method_name)
|
|
1469
|
+
[node, entry.name] if node
|
|
894
1470
|
end
|
|
895
|
-
[nil, nil]
|
|
896
1471
|
end
|
|
1472
|
+
private :first_singleton_def
|
|
897
1473
|
|
|
898
1474
|
# Issue #633 — the ancestors a project class reaches that the project itself does NOT declare: the
|
|
899
|
-
# `< StandardError` / `< Array` superclasses and the `include Comparable` mixins the
|
|
900
|
-
#
|
|
1475
|
+
# `< StandardError` / `< Array` superclasses and the `include Comparable` mixins, as the CANDIDATE LIST for
|
|
1476
|
+
# each as-written name ({#ancestor_name_candidates}: the nesting spellings first, the bare name last), in
|
|
1477
|
+
# Ruby's order along the instance chain — the order the caller that adopts the FIRST answering one (#1173)
|
|
1478
|
+
# depends on. Resolving a candidate means asking the RBS environment, which this frozen-index read does
|
|
1479
|
+
# not do; the caller takes the first candidate its own oracle knows. Where the chain does not stand, the
|
|
1480
|
+
# groups are the depth-first ones the walk this replaced emitted.
|
|
901
1481
|
#
|
|
902
|
-
#
|
|
903
|
-
#
|
|
904
|
-
#
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
groups
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
current = queue.shift
|
|
914
|
-
next if current.nil? || seen[current]
|
|
915
|
-
|
|
916
|
-
seen[current] = true
|
|
917
|
-
visited += 1
|
|
918
|
-
break if visited > ANCESTOR_WALK_LIMIT
|
|
919
|
-
|
|
920
|
-
collect_external_ancestors(current, queue, groups, name_memo)
|
|
1482
|
+
# Issue #527 slice 1 — `mixins: false` answers the superclass edge only: a consumer resolving one KIND of
|
|
1483
|
+
# inheritance edge at a time (the dispatch arm lands `< Hash` before `include Enumerable`) narrows it, and
|
|
1484
|
+
# records only the superclass chain it read.
|
|
1485
|
+
def external_ancestor_name_candidates(class_name, name_memo: UNUSED_NAME_MEMO, mixins: true) # rubocop:disable Lint/UnusedMethodArgument
|
|
1486
|
+
chain = ResolutionChain.for(self, class_name.to_s, :instance, :methods)
|
|
1487
|
+
groups = external_groups(chain, mixins)
|
|
1488
|
+
if chain.settle(self, groups) { |retro| external_groups(retro, mixins) } == :master
|
|
1489
|
+
groups = ResolutionChain::MasterOrder.external_groups(self, class_name.to_s, mixins)
|
|
1490
|
+
end
|
|
1491
|
+
if Analysis::DependencyRecorder.active?
|
|
1492
|
+
mixins ? chain.record(self) : chain.level_classes.each { |name| record_class_dependency(name) if name }
|
|
921
1493
|
end
|
|
1494
|
+
# Issue #527 — the cut is a budget event, not an answer: consumers read the groups as "the ancestors
|
|
1495
|
+
# this class reaches", so a short list must be visible in `--stats` beside the other walks' exhaustions.
|
|
1496
|
+
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT) if chain.truncated?
|
|
922
1497
|
groups
|
|
923
1498
|
end
|
|
924
1499
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
def collect_external_ancestors(current, queue, groups, name_memo)
|
|
928
|
-
raw_names = includes_of(current).dup
|
|
929
|
-
raw_super = superclass_of(current)
|
|
930
|
-
raw_names << raw_super if raw_super
|
|
931
|
-
raw_names.each do |raw|
|
|
932
|
-
resolved = resolve_ancestor_class_name(current, raw, name_memo)
|
|
933
|
-
if resolved
|
|
934
|
-
queue.push(resolved)
|
|
935
|
-
else
|
|
936
|
-
groups << ancestor_name_candidates(current, raw)
|
|
937
|
-
end
|
|
938
|
-
end
|
|
1500
|
+
def external_groups(chain, mixins)
|
|
1501
|
+
chain.entries.filter_map { |entry| entry.candidates if entry.external? && (mixins || entry.superclass_edge) }
|
|
939
1502
|
end
|
|
940
|
-
private :
|
|
1503
|
+
private :external_groups
|
|
941
1504
|
|
|
942
|
-
# The
|
|
943
|
-
|
|
1505
|
+
# The budget {ResolutionChain::LIMIT} enforces; a hierarchy past it gives up rather than reading an answer
|
|
1506
|
+
# off a cut chain (ADR-41 WD4).
|
|
1507
|
+
ANCESTOR_WALK_LIMIT = ResolutionChain::LIMIT
|
|
944
1508
|
|
|
945
1509
|
EMPTY_HEADER_NESTING = [].freeze
|
|
946
1510
|
private_constant :EMPTY_HEADER_NESTING
|
|
@@ -949,13 +1513,13 @@ module Rigor
|
|
|
949
1513
|
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
950
1514
|
[nil, nil]
|
|
951
1515
|
end
|
|
1516
|
+
private :ancestor_walk_gave_up
|
|
952
1517
|
|
|
953
|
-
# Issue #723 — the
|
|
954
|
-
#
|
|
955
|
-
#
|
|
956
|
-
# `
|
|
957
|
-
#
|
|
958
|
-
# needs the broader one.
|
|
1518
|
+
# Issue #723 — the question {#user_def_through_ancestors} asks, asked of the DISCOVERY table rather than
|
|
1519
|
+
# the def-node table: does the project define `method_name` on `class_name` or on any ancestor the project
|
|
1520
|
+
# itself declares? The two tables are not interchangeable — `discovered_methods` also carries
|
|
1521
|
+
# `define_method`, `attr_*` and the whole singleton side, none of which contributes a `Prism::DefNode` —
|
|
1522
|
+
# and the suppression probe shared by the `call.*` check rules needs the broader one.
|
|
959
1523
|
#
|
|
960
1524
|
# Why it must walk at all: `Analysis::CheckRules#source_declared_method?` asked `discovered_method?`,
|
|
961
1525
|
# which is keyed on the receiver's OWN name, while the typer resolves the same call through this
|
|
@@ -963,72 +1527,47 @@ module Rigor
|
|
|
963
1527
|
# `call.undefined-method` on a method `dump_type` resolved on the same line of the same run — writing
|
|
964
1528
|
# MORE RBS made the run worse, the incentive #653 removed for plugin-typed calls.
|
|
965
1529
|
#
|
|
966
|
-
# `kind: :singleton`
|
|
967
|
-
#
|
|
968
|
-
#
|
|
969
|
-
|
|
970
|
-
def discovered_method_through_ancestors?(class_name, method_name, kind, name_memo: {})
|
|
1530
|
+
# `kind: :singleton` reads the singleton chain: the class objects' own methods (an extended module's are
|
|
1531
|
+
# folded into its extender's) and the instance methods of the modules they extend and those modules'
|
|
1532
|
+
# includes. Existence is a union, so the chain's two worlds, which hold the same modules, agree on it.
|
|
1533
|
+
def discovered_method_through_ancestors?(class_name, method_name, kind, name_memo: UNUSED_NAME_MEMO) # rubocop:disable Lint/UnusedMethodArgument
|
|
971
1534
|
return false if class_name.nil?
|
|
972
1535
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
until queue.empty?
|
|
977
|
-
current = queue.shift
|
|
978
|
-
next if current.nil? || seen[current]
|
|
979
|
-
|
|
980
|
-
seen[current] = true
|
|
981
|
-
visited += 1
|
|
982
|
-
# Budget exhaustion is uncertainty, not absence: answering "not declared" here would hand a
|
|
983
|
-
# `call.undefined-method` a fired verdict it has no evidence for. Suppress and record the hit.
|
|
984
|
-
if visited > ANCESTOR_WALK_LIMIT
|
|
985
|
-
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
986
|
-
return true
|
|
987
|
-
end
|
|
988
|
-
|
|
989
|
-
return true if discovered_method?(current, method_name, kind)
|
|
1536
|
+
chain = ResolutionChain.for(self, class_name.to_s, kind == :singleton ? :singleton : :instance, :methods)
|
|
1537
|
+
found = chain.search(self) do |entry|
|
|
1538
|
+
next false if entry.external?
|
|
990
1539
|
|
|
991
|
-
|
|
992
|
-
end
|
|
993
|
-
false
|
|
994
|
-
end
|
|
995
|
-
|
|
996
|
-
# Pushes `current`'s direct ancestors onto the BFS queue: included / prepended modules first (Ruby places
|
|
997
|
-
# mixins nearer than the superclass), then the superclass. Each as-written name is resolved against the
|
|
998
|
-
# nesting `current`'s declaration header is written in ({#ancestor_name_candidates}); names that resolve
|
|
999
|
-
# to no project class/module are dropped. `mixins: false` walks the superclass chain alone, for the
|
|
1000
|
-
# singleton-side question where an `include` contributes nothing.
|
|
1001
|
-
def enqueue_ancestors(current, queue, name_memo, mixins: true)
|
|
1002
|
-
if mixins
|
|
1003
|
-
includes_of(current).each do |raw|
|
|
1004
|
-
resolved = resolve_ancestor_class_name(current, raw, name_memo)
|
|
1005
|
-
queue.push(resolved) if resolved
|
|
1006
|
-
end
|
|
1540
|
+
discovered_method?(entry.name, method_name, entry.side == :singleton ? :singleton : kind_for(kind))
|
|
1007
1541
|
end
|
|
1008
|
-
|
|
1009
|
-
|
|
1542
|
+
return true if found
|
|
1543
|
+
# Budget exhaustion is uncertainty, not absence: answering "not declared" here would hand a
|
|
1544
|
+
# `call.undefined-method` a fired verdict it has no evidence for. Suppress and record the hit.
|
|
1545
|
+
return false unless chain.truncated?
|
|
1010
1546
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1547
|
+
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
1548
|
+
true
|
|
1013
1549
|
end
|
|
1014
1550
|
|
|
1015
|
-
#
|
|
1016
|
-
#
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
by_subclass = (name_memo[subclass_qualified] ||= {})
|
|
1020
|
-
return by_subclass[raw_ancestor] if by_subclass.key?(raw_ancestor)
|
|
1551
|
+
# A module entry on the singleton chain answers with its INSTANCE methods; on the instance chain, the
|
|
1552
|
+
# caller's kind stands.
|
|
1553
|
+
def kind_for(kind) = kind == :singleton ? :instance : kind
|
|
1554
|
+
private :kind_for
|
|
1021
1555
|
|
|
1022
|
-
|
|
1556
|
+
# Pushes `current`'s direct ancestors onto a breadth-first queue: included / prepended modules first, then
|
|
1557
|
+
# the superclass, each resolved against the nesting `current`'s declaration header is written in; names
|
|
1558
|
+
# that resolve to no project class/module are dropped. `mixins: false` gives the superclass alone. No
|
|
1559
|
+
# reader in the engine walks this way any more (they read {ResolutionChain}); it stays for the plugin
|
|
1560
|
+
# surface, with the ADR-46 class edge the walk it served always filed.
|
|
1561
|
+
def enqueue_ancestors(current, queue, name_memo, mixins: true) # rubocop:disable Lint/UnusedMethodArgument
|
|
1562
|
+
record_class_dependency(current) if Analysis::DependencyRecorder.active?
|
|
1563
|
+
queue.concat(ResolutionChain.direct_ancestors(self, current.to_s, mixins))
|
|
1023
1564
|
end
|
|
1024
|
-
private :ancestor_walk_gave_up, :resolve_ancestor_class_name
|
|
1025
1565
|
|
|
1026
1566
|
# Issue #682 — the candidate names an ancestor spelled `raw_ancestor` can denote in `subclass_qualified`,
|
|
1027
1567
|
# in Ruby's lookup order, most-qualified first and the bare name last. The single owner of the question:
|
|
1028
|
-
# three walks resolved a superclass / include name apiece (
|
|
1029
|
-
# `
|
|
1030
|
-
#
|
|
1031
|
-
# subclass's qualified name one `::` segment at a time.
|
|
1568
|
+
# three walks resolved a superclass / include name apiece (the method walk, the constant ladder's
|
|
1569
|
+
# ancestor rung, and `Analysis::CheckRules`' override-visibility rule — all three now {ResolutionChain}),
|
|
1570
|
+
# and each derived the order by PEELING the subclass's qualified name one `::` segment at a time.
|
|
1032
1571
|
#
|
|
1033
1572
|
# That peel is the NESTED spelling's answer, given to both spellings. Ruby evaluates a superclass
|
|
1034
1573
|
# expression before entering the body, so the cref that governs it is the one the declaration's HEADER
|
|
@@ -1040,9 +1579,9 @@ module Rigor
|
|
|
1040
1579
|
# The header nesting is a property of the DECLARATION, not of the reader, so it is read from the
|
|
1041
1580
|
# discovery table `Inference::ScopeIndexer` records it in rather than from this scope's own
|
|
1042
1581
|
# `#lexical_nesting`. Answering off the reader's chain would be wrong past the first hop of the ancestor
|
|
1043
|
-
#
|
|
1044
|
-
# corrupt the
|
|
1045
|
-
# `Reflection.ancestor_constant_scopes
|
|
1582
|
+
# chain (the chain describes the reader's class, not the ancestor being resolved) and would silently
|
|
1583
|
+
# corrupt the {ResolutionChain} memo and the ones layered on it (`Inference::ExpressionTyper#class_graph_buckets`,
|
|
1584
|
+
# `Reflection.ancestor_constant_scopes`), all of which key on the class name alone because this walk is
|
|
1046
1585
|
# a pure function of the frozen discovery tables.
|
|
1047
1586
|
#
|
|
1048
1587
|
# The peel survives as the FALLBACK, for a class no declaration walk recorded — a scope seeded without
|
|
@@ -1062,9 +1601,70 @@ module Rigor
|
|
|
1062
1601
|
|
|
1063
1602
|
recorded = @discovery.discovered_header_nestings[subclass_qualified.to_s]
|
|
1064
1603
|
entries = recorded ? recorded_header_nesting(recorded, raw) : peeled_header_nesting(subclass_qualified)
|
|
1604
|
+
if !entries.empty? && DiscoveryIndex.ambiguous_header_nesting?(entries)
|
|
1605
|
+
return ambiguous_ancestor_candidates(entries, raw_ancestor)
|
|
1606
|
+
end
|
|
1607
|
+
|
|
1608
|
+
entries.map { |entry| "#{entry}::#{raw_ancestor}" } << raw_ancestor.to_s
|
|
1609
|
+
end
|
|
1610
|
+
|
|
1611
|
+
# Issue #986 — the candidate list for a raw name two declaration sites of ONE class wrote in different
|
|
1612
|
+
# crefs (the compact-header rename pass landed both on this key). Each alternative chain gets its own
|
|
1613
|
+
# candidate list, in the order this walk would have used for that site alone.
|
|
1614
|
+
#
|
|
1615
|
+
# When two of them resolve to two DIFFERENT project classes there is no candidate list to return: at
|
|
1616
|
+
# runtime both `include`s run, and which of the two same-named modules ends up nearer in the MRO is the
|
|
1617
|
+
# load order of the two files, which this walk cannot know. Answering with either is a WRONG ANCESTOR,
|
|
1618
|
+
# and a wrong ancestor is a false-positive source rather than a missed one — two same-named modules can
|
|
1619
|
+
# declare the same method at different arities, and `call.wrong-arity` then fires on a correct program.
|
|
1620
|
+
# The empty list declines instead: the name resolves to no project class, the receiver's methods stay
|
|
1621
|
+
# `Dynamic`, and every rule reading this walk goes quiet.
|
|
1622
|
+
#
|
|
1623
|
+
# Where the alternatives AGREE, or only one of them resolves at all, there is nothing to adjudicate: the
|
|
1624
|
+
# answer is the union of their chains, most-qualified first — the same list a class whose sites needed
|
|
1625
|
+
# no rename gets — so an unambiguous collision is unchanged, and an EXTERNAL ancestor keeps the full
|
|
1626
|
+
# candidate list `#external_ancestor_name_candidates` hands the gem / RBS probe.
|
|
1627
|
+
def ambiguous_ancestor_candidates(alternatives, raw_ancestor)
|
|
1628
|
+
return [] if ambiguous_ancestor_resolutions_of(alternatives, raw_ancestor).size > 1
|
|
1629
|
+
|
|
1630
|
+
entries = alternatives.reduce([], :|).sort_by { |entry| [-entry.split("::").size, entry] }
|
|
1065
1631
|
entries.map { |entry| "#{entry}::#{raw_ancestor}" } << raw_ancestor.to_s
|
|
1066
1632
|
end
|
|
1067
1633
|
|
|
1634
|
+
def ambiguous_ancestor_resolutions_of(alternatives, raw_ancestor)
|
|
1635
|
+
alternatives.filter_map do |chain|
|
|
1636
|
+
(chain.map { |entry| "#{entry}::#{raw_ancestor}" } << raw_ancestor.to_s)
|
|
1637
|
+
.find { |candidate| known_user_class?(candidate) }
|
|
1638
|
+
end.uniq
|
|
1639
|
+
end
|
|
1640
|
+
private :ambiguous_ancestor_candidates, :ambiguous_ancestor_resolutions_of
|
|
1641
|
+
|
|
1642
|
+
# Issue #986 — the several project classes an ancestor name resolves to when the compact-header rename
|
|
1643
|
+
# collision left it ambiguous, and `EMPTY_HEADER_NESTING` for every other name. {#ancestor_name_candidates}
|
|
1644
|
+
# declines such a name because no ONE class is its answer; a caller that also knows which METHOD it is
|
|
1645
|
+
# looking up can do better than that decline, and a rule that reports on a method must, because the
|
|
1646
|
+
# decline otherwise silences it for the whole receiver — its own `def`s and its unambiguous ancestors
|
|
1647
|
+
# included.
|
|
1648
|
+
#
|
|
1649
|
+
# Both of these classes are ancestors at runtime: both `include`s run, and only their MRO ORDER is the
|
|
1650
|
+
# load order this walk cannot see. So a method only one of them declares is answered by that one
|
|
1651
|
+
# whatever the order, and only a method they BOTH declare is unanswerable.
|
|
1652
|
+
# `Analysis::CheckRules::SourceArity` reads this for exactly that: it takes both as mixin levels and
|
|
1653
|
+
# declines on the disagreement its own envelope join already knows how to spot.
|
|
1654
|
+
def ambiguous_ancestor_resolutions(subclass_qualified, raw_ancestor)
|
|
1655
|
+
raw = raw_ancestor.to_s
|
|
1656
|
+
return EMPTY_HEADER_NESTING if raw.start_with?("::")
|
|
1657
|
+
|
|
1658
|
+
recorded = @discovery.discovered_header_nestings[subclass_qualified.to_s]
|
|
1659
|
+
return EMPTY_HEADER_NESTING if recorded.nil?
|
|
1660
|
+
|
|
1661
|
+
entries = recorded_header_nesting(recorded, raw)
|
|
1662
|
+
return EMPTY_HEADER_NESTING if entries.empty? || !DiscoveryIndex.ambiguous_header_nesting?(entries)
|
|
1663
|
+
|
|
1664
|
+
resolved = ambiguous_ancestor_resolutions_of(entries, raw_ancestor)
|
|
1665
|
+
resolved.size > 1 ? resolved : EMPTY_HEADER_NESTING
|
|
1666
|
+
end
|
|
1667
|
+
|
|
1068
1668
|
# Issue #728 — the chain of the declaration site that WROTE `raw`, which is the cref Ruby resolves that
|
|
1069
1669
|
# one name in. A class's sites need not agree: `class Foo < Base` at the top level and a rooted
|
|
1070
1670
|
# `class ::Foo; include Helper; end` inside `module Outer` are two sites of `Foo`, and the union of
|
|
@@ -1088,10 +1688,6 @@ module Rigor
|
|
|
1088
1688
|
end
|
|
1089
1689
|
private :peeled_header_nesting
|
|
1090
1690
|
|
|
1091
|
-
def compute_ancestor_class_name(subclass_qualified, raw_ancestor)
|
|
1092
|
-
ancestor_name_candidates(subclass_qualified, raw_ancestor).find { |c| known_user_class?(c) }
|
|
1093
|
-
end
|
|
1094
|
-
|
|
1095
1691
|
# Issue #723 — `discovered_methods` is in the list because the other three miss a class whose only
|
|
1096
1692
|
# project-side content is CLASS methods: `class Base; def self.build = :built; end` records no instance
|
|
1097
1693
|
# def node, no superclass and no include, so the ancestor-name resolver did not recognise `Base` as a
|
|
@@ -1106,13 +1702,13 @@ module Rigor
|
|
|
1106
1702
|
# narrower predicate (`discovered_classes` alone) would read a project class as having left the project
|
|
1107
1703
|
# and attribute its root name to a same-named locked gem — a WRONG provenance label, which is the one
|
|
1108
1704
|
# direction ADR-82 forbids.
|
|
1109
|
-
private :ancestor_walk_gave_up, :compute_ancestor_class_name
|
|
1110
1705
|
|
|
1111
1706
|
# Records, for a resolved cross-class ancestry read, every file that declares `class_name` (its declaration /
|
|
1112
|
-
# reopening / superclass / include sites). The `discovered_class_sources` table it reads is populated
|
|
1113
|
-
#
|
|
1114
|
-
#
|
|
1115
|
-
# never appear in the source map). Gated by the caller on the recorder being
|
|
1707
|
+
# reopening / superclass / include sites). The `discovered_class_sources` table it reads is populated by the
|
|
1708
|
+
# cross-file project pre-pass ({Inference::ScopeIndexer.discovered_def_index_for_paths}); a scope built
|
|
1709
|
+
# without one holds none, and the read then files nothing. No-op when the class is not a project class
|
|
1710
|
+
# (core / stdlib / gem names never appear in the source map). Gated by the caller on the recorder being
|
|
1711
|
+
# active.
|
|
1116
1712
|
def record_class_dependency(class_name)
|
|
1117
1713
|
sites = @discovery.discovered_class_sources[class_name.to_s]
|
|
1118
1714
|
return if sites.nil?
|
|
@@ -1132,8 +1728,7 @@ module Rigor
|
|
|
1132
1728
|
# so a bare referent is re-checked when the class appears or disappears and at no other time. A file edge
|
|
1133
1729
|
# would have re-checked every bare referent of every class the file declares on any edit to it.
|
|
1134
1730
|
def record_class_existence(class_name)
|
|
1135
|
-
|
|
1136
|
-
Analysis::DependencyRecorder.read_name(:class, segment) if segment
|
|
1731
|
+
Analysis::DependencyRecorder.read_last_segment(:class, class_name.to_s)
|
|
1137
1732
|
end
|
|
1138
1733
|
|
|
1139
1734
|
# Issue #644 — the positive ADR-46 edge for a cross-file VALUE constant. `Reflection.constant_type_at`
|
|
@@ -1152,6 +1747,17 @@ module Rigor
|
|
|
1152
1747
|
sites.each { |site| Analysis::DependencyRecorder.read_site(site) }
|
|
1153
1748
|
end
|
|
1154
1749
|
|
|
1750
|
+
# Issue #992 — one class's `discovered_parameter_envelopes` bucket (`{[kind, method_name] => envelope}` plus
|
|
1751
|
+
# the class-wide marks), or an empty Hash. Records the ADR-46 class edge first: a verdict read off the
|
|
1752
|
+
# bucket depends on every file that declares the class, and a `memoize :f` added to one of them moves it.
|
|
1753
|
+
def parameter_envelopes_of(class_name)
|
|
1754
|
+
record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
|
|
1755
|
+
@discovery.discovered_parameter_envelopes[class_name.to_s] || EMPTY_PARAMETER_ENVELOPES
|
|
1756
|
+
end
|
|
1757
|
+
|
|
1758
|
+
EMPTY_PARAMETER_ENVELOPES = {}.freeze
|
|
1759
|
+
private_constant :EMPTY_PARAMETER_ENVELOPES
|
|
1760
|
+
|
|
1155
1761
|
# v0.1.2 — per-class table mapping `method_name (Symbol) → :public | :private | :protected`. Populated by
|
|
1156
1762
|
# `ScopeIndexer` for every `def` it sees inside a class body, with the visibility taken from the surrounding
|
|
1157
1763
|
# `private` / `protected` / `public` modifier state plus any post-hoc `private :name, ...` named-argument
|
|
@@ -1272,10 +1878,37 @@ module Rigor
|
|
|
1272
1878
|
joined_locals = join_bindings(locals, other.locals)
|
|
1273
1879
|
joined_ivars = join_bindings(ivars, other.ivars)
|
|
1274
1880
|
joined_cvars = join_bindings(cvars, other.cvars)
|
|
1275
|
-
joined_globals = join_bindings(globals, other.globals)
|
|
1881
|
+
joined_globals = unbind_split_last_line(join_bindings(globals, other.globals), other)
|
|
1276
1882
|
build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other)
|
|
1277
1883
|
end
|
|
1278
1884
|
|
|
1885
|
+
# Issue #1429 — a join keeps a guard record while the joined scope still narrows its name: a global both arms
|
|
1886
|
+
# bind, a constant both arms narrow (one an arm does not narrow reads its resolved type after the join, so its
|
|
1887
|
+
# narrowing is gone), and an instance variable both arms bind (#1446). An arm without a record contributes nothing
|
|
1888
|
+
# to the restore target, whose union with the joined binding {#forget_guard_narrowings} takes, so it still covers
|
|
1889
|
+
# that arm's binding.
|
|
1890
|
+
# `joined` maps each record kind to the joined scope's table of that kind.
|
|
1891
|
+
def join_guard_records(other, joined)
|
|
1892
|
+
mine = @guard_records
|
|
1893
|
+
theirs = other.guard_records
|
|
1894
|
+
return EMPTY_GUARD_RECORDS if mine.empty? && theirs.empty?
|
|
1895
|
+
|
|
1896
|
+
merged = mine.merge(theirs) { |_key, left, right| Type::Combinator.union(left, right) }
|
|
1897
|
+
kept = merged.select { |(kind, name), _| joined.fetch(kind).key?(name) }
|
|
1898
|
+
kept.empty? ? EMPTY_GUARD_RECORDS : kept.freeze
|
|
1899
|
+
end
|
|
1900
|
+
private :join_guard_records
|
|
1901
|
+
|
|
1902
|
+
# Issue #1359 — arms that bind `$_` apart join with it unbound rather than to their union. The arms of a reader
|
|
1903
|
+
# condition bind `String` and `nil`, and `String?` after `if gets … end` would report correct code that proves the
|
|
1904
|
+
# line some other way (`ok = gets ? true : false; return unless ok; line = $_; line.chomp`).
|
|
1905
|
+
def unbind_split_last_line(joined, other)
|
|
1906
|
+
return joined unless joined.key?(LAST_LINE) && @globals[LAST_LINE] != other.globals[LAST_LINE]
|
|
1907
|
+
|
|
1908
|
+
joined.except(LAST_LINE).freeze
|
|
1909
|
+
end
|
|
1910
|
+
private :unbind_split_last_line
|
|
1911
|
+
|
|
1279
1912
|
def ==(other)
|
|
1280
1913
|
other.is_a?(Scope) &&
|
|
1281
1914
|
environment.equal?(other.environment) &&
|
|
@@ -1287,8 +1920,7 @@ module Rigor
|
|
|
1287
1920
|
@globals == other.globals &&
|
|
1288
1921
|
@indexed_narrowings == other.indexed_narrowings &&
|
|
1289
1922
|
@method_chain_narrowings == other.method_chain_narrowings &&
|
|
1290
|
-
|
|
1291
|
-
@published_constant_sourced == other.published_constant_sourced
|
|
1923
|
+
same_marks?(other)
|
|
1292
1924
|
end
|
|
1293
1925
|
alias eql? ==
|
|
1294
1926
|
|
|
@@ -1298,6 +1930,19 @@ module Rigor
|
|
|
1298
1930
|
|
|
1299
1931
|
private
|
|
1300
1932
|
|
|
1933
|
+
# The marks {#==} compares: ADR-58's, issue #667's and the repeated `||=` sites, and #1429's guard state.
|
|
1934
|
+
def same_marks?(other)
|
|
1935
|
+
@declaration_sourced == other.declaration_sourced &&
|
|
1936
|
+
@published_constant_sourced == other.published_constant_sourced &&
|
|
1937
|
+
@repeated_or_writes == other.repeated_or_writes &&
|
|
1938
|
+
same_guard_state?(other)
|
|
1939
|
+
end
|
|
1940
|
+
|
|
1941
|
+
def same_guard_state?(other)
|
|
1942
|
+
@constant_narrowings == other.constant_narrowings && @guard_records == other.guard_records &&
|
|
1943
|
+
@bot_guard_classes == other.bot_guard_classes
|
|
1944
|
+
end
|
|
1945
|
+
|
|
1301
1946
|
def rebuild(
|
|
1302
1947
|
locals: @locals, fact_store: @fact_store, self_type: @self_type,
|
|
1303
1948
|
ivars: @ivars, cvars: @cvars, globals: @globals,
|
|
@@ -1309,6 +1954,7 @@ module Rigor
|
|
|
1309
1954
|
source_path: @source_path,
|
|
1310
1955
|
struct_fold_safe_locals: @struct_fold_safe_locals,
|
|
1311
1956
|
opaque_block_self: @opaque_block_self,
|
|
1957
|
+
singleton_class_body: @singleton_class_body,
|
|
1312
1958
|
lexical_nesting: @lexical_nesting,
|
|
1313
1959
|
dynamic_origins: @dynamic_origins,
|
|
1314
1960
|
local_origins: @local_origins,
|
|
@@ -1317,7 +1963,12 @@ module Rigor
|
|
|
1317
1963
|
plugin_typed_calls: @plugin_typed_calls,
|
|
1318
1964
|
optimistic_origins: @optimistic_origins,
|
|
1319
1965
|
optimistic_locals: @optimistic_locals,
|
|
1320
|
-
optimistic_ivars: @optimistic_ivars
|
|
1966
|
+
optimistic_ivars: @optimistic_ivars,
|
|
1967
|
+
repeated_or_writes: @repeated_or_writes,
|
|
1968
|
+
match_frame: @match_frame,
|
|
1969
|
+
constant_narrowings: @constant_narrowings,
|
|
1970
|
+
guard_records: @guard_records,
|
|
1971
|
+
bot_guard_classes: @bot_guard_classes
|
|
1321
1972
|
)
|
|
1322
1973
|
self.class.new(
|
|
1323
1974
|
environment: environment, locals: locals,
|
|
@@ -1331,6 +1982,7 @@ module Rigor
|
|
|
1331
1982
|
source_path: source_path,
|
|
1332
1983
|
struct_fold_safe_locals: struct_fold_safe_locals,
|
|
1333
1984
|
opaque_block_self: opaque_block_self,
|
|
1985
|
+
singleton_class_body: singleton_class_body,
|
|
1334
1986
|
lexical_nesting: lexical_nesting,
|
|
1335
1987
|
dynamic_origins: dynamic_origins,
|
|
1336
1988
|
local_origins: local_origins,
|
|
@@ -1339,7 +1991,12 @@ module Rigor
|
|
|
1339
1991
|
plugin_typed_calls: plugin_typed_calls,
|
|
1340
1992
|
optimistic_origins: optimistic_origins,
|
|
1341
1993
|
optimistic_locals: optimistic_locals,
|
|
1342
|
-
optimistic_ivars: optimistic_ivars
|
|
1994
|
+
optimistic_ivars: optimistic_ivars,
|
|
1995
|
+
repeated_or_writes: repeated_or_writes,
|
|
1996
|
+
match_frame: match_frame,
|
|
1997
|
+
constant_narrowings: constant_narrowings,
|
|
1998
|
+
guard_records: guard_records,
|
|
1999
|
+
bot_guard_classes: bot_guard_classes
|
|
1343
2000
|
)
|
|
1344
2001
|
end
|
|
1345
2002
|
|
|
@@ -1359,7 +2016,7 @@ module Rigor
|
|
|
1359
2016
|
|
|
1360
2017
|
def build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other)
|
|
1361
2018
|
self.class.new(
|
|
1362
|
-
environment: environment,
|
|
2019
|
+
environment: @environment,
|
|
1363
2020
|
locals: joined_locals.freeze,
|
|
1364
2021
|
fact_store: fact_store.join(other.fact_store),
|
|
1365
2022
|
self_type: self_type == other.self_type ? self_type : nil,
|
|
@@ -1379,7 +2036,7 @@ module Rigor
|
|
|
1379
2036
|
# end; m == :x` must not warn because one arm's copy is invisible to the reader's author. ADR-67
|
|
1380
2037
|
# WD6b's `:inferred_param` taint takes the same direction for the same reason.
|
|
1381
2038
|
published_constant_sourced: join_published_constant_sourced(other),
|
|
1382
|
-
source_path: source_path,
|
|
2039
|
+
source_path: @source_path,
|
|
1383
2040
|
# Issue #589 — the fold-safe set MUST survive a merge. It was simply absent from this constructor
|
|
1384
2041
|
# call, so it fell back to the empty default and every `if` / `while` in a method silently revoked
|
|
1385
2042
|
# struct member folding for the whole body after it: `s = S.new("r"); while c; i += 1; end; s.raw`
|
|
@@ -1405,6 +2062,10 @@ module Rigor
|
|
|
1405
2062
|
# block entry by `entering_opaque_block` and inherited through `rebuild`), so both arms usually carry
|
|
1406
2063
|
# the same value and the `||` is that value.
|
|
1407
2064
|
opaque_block_self: @opaque_block_self || other.opaque_block_self,
|
|
2065
|
+
# Issue #963 — a body property like the two above, so both arms of an in-body merge carry the identical
|
|
2066
|
+
# value and the `||` is that value. `||` is also the safe direction on its own terms: keeping the mark
|
|
2067
|
+
# declines the `define_method` narrowing, which is the pre-#963 answer.
|
|
2068
|
+
singleton_class_body: @singleton_class_body || other.singleton_class_body,
|
|
1408
2069
|
# Issue #652 — the recorded `Module.nesting`, stamped once at body entry and threaded by `rebuild`
|
|
1409
2070
|
# exactly as the fold-safe set is. A join is a control-flow merge INSIDE one body, so both arms
|
|
1410
2071
|
# always carry the identical chain; taking this scope's is that chain. Dropping it would silently
|
|
@@ -1416,11 +2077,55 @@ module Rigor
|
|
|
1416
2077
|
void_origins: @void_origins,
|
|
1417
2078
|
plugin_typed_calls: @plugin_typed_calls,
|
|
1418
2079
|
optimistic_origins: @optimistic_origins,
|
|
1419
|
-
optimistic_locals:
|
|
1420
|
-
optimistic_ivars:
|
|
2080
|
+
optimistic_locals: join_optimistic_marks(@optimistic_locals, other.optimistic_locals),
|
|
2081
|
+
optimistic_ivars: join_optimistic_marks(@optimistic_ivars, other.optimistic_ivars),
|
|
2082
|
+
# UNION, the published-constant mark's direction: the mark only withholds the memoizing `||=`
|
|
2083
|
+
# reading, so keeping a site either arm holds is the wider answer.
|
|
2084
|
+
repeated_or_writes: join_repeated_or_writes(other),
|
|
2085
|
+
# Issue #1358 — the frame the body runs in, stamped at its entry like the nesting above, so both arms
|
|
2086
|
+
# of a merge inside one body carry the same one; `||` keeps it should either arm lack it, since dropping
|
|
2087
|
+
# it only loses the frame's resets.
|
|
2088
|
+
match_frame: @match_frame || other.match_frame,
|
|
2089
|
+
# Issue #1429 — the guard narrowings of globals and constants and their pre-guard records, and of instance
|
|
2090
|
+
# variables (#1446).
|
|
2091
|
+
**join_guard_narrowings(other, joined_globals, joined_ivars)
|
|
1421
2092
|
)
|
|
1422
2093
|
end
|
|
1423
2094
|
|
|
2095
|
+
def join_guard_narrowings(other, joined_globals, joined_ivars)
|
|
2096
|
+
joined_constants = join_constant_narrowings(other)
|
|
2097
|
+
joined = { global: joined_globals, ivar: joined_ivars, constant: joined_constants }
|
|
2098
|
+
{ constant_narrowings: joined_constants, guard_records: join_guard_records(other, joined),
|
|
2099
|
+
bot_guard_classes: join_bot_guard_classes(other) }
|
|
2100
|
+
end
|
|
2101
|
+
|
|
2102
|
+
# Issue #1446 — an entry both arms hold, with the classes of either: the joined receiver reads `bot` only where
|
|
2103
|
+
# both arms do. One only an arm holds is dropped, so a call on the joined `bot` reads as on an unknown one.
|
|
2104
|
+
def join_bot_guard_classes(other)
|
|
2105
|
+
mine = @bot_guard_classes
|
|
2106
|
+
theirs = other.bot_guard_classes
|
|
2107
|
+
return mine if mine.equal?(theirs)
|
|
2108
|
+
return EMPTY_BOT_GUARD_CLASSES if mine.empty? || theirs.empty?
|
|
2109
|
+
|
|
2110
|
+
joined = mine.each_with_object({}) do |(key, names), acc|
|
|
2111
|
+
other_names = theirs[key]
|
|
2112
|
+
acc[key] = (names | other_names).freeze if other_names
|
|
2113
|
+
end
|
|
2114
|
+
joined.empty? ? EMPTY_BOT_GUARD_CLASSES : joined.freeze
|
|
2115
|
+
end
|
|
2116
|
+
|
|
2117
|
+
# Issue #1429 — a constant reference both arms narrow reads the union; one only an arm narrows reads its resolved
|
|
2118
|
+
# type again, which the other arm reads too.
|
|
2119
|
+
def join_constant_narrowings(other)
|
|
2120
|
+
mine = @constant_narrowings
|
|
2121
|
+
theirs = other.constant_narrowings
|
|
2122
|
+
return mine if mine.equal?(theirs)
|
|
2123
|
+
return EMPTY_CONSTANT_NARROWINGS if mine.empty? || theirs.empty?
|
|
2124
|
+
|
|
2125
|
+
joined = join_bindings(mine, theirs)
|
|
2126
|
+
joined.empty? ? EMPTY_CONSTANT_NARROWINGS : joined
|
|
2127
|
+
end
|
|
2128
|
+
|
|
1424
2129
|
# Issue #589 — intersect the struct fold-safe grants. Zero-alloc on the common path, where both arms
|
|
1425
2130
|
# carry the same frozen set the body scan stamped.
|
|
1426
2131
|
# A `nil` on EITHER side yields no grants. The field is keyword-defaulted to `EMPTY_FOLD_SAFE` and
|
|
@@ -1448,6 +2153,17 @@ module Rigor
|
|
|
1448
2153
|
theirs.merge(mine).freeze
|
|
1449
2154
|
end
|
|
1450
2155
|
|
|
2156
|
+
# Issue #286's mark tables join as {#join_origins} does, except for the miss answer a mark may carry (issue
|
|
2157
|
+
# #1302): a name both arms mark keeps its answer only when the arms agree on it. A name one arm marks keeps
|
|
2158
|
+
# that arm's answer, since a miss runs through that arm only.
|
|
2159
|
+
def join_optimistic_marks(mine, theirs)
|
|
2160
|
+
return mine if mine.equal?(theirs) || theirs.empty?
|
|
2161
|
+
return theirs if mine.empty?
|
|
2162
|
+
|
|
2163
|
+
theirs.merge(mine) { |_name, their_mark, my_mark| Inference::OptimisticOrigin.join_bound_marks(my_mark, their_mark) }
|
|
2164
|
+
.freeze
|
|
2165
|
+
end
|
|
2166
|
+
|
|
1451
2167
|
# ADR-82 WD1 — rebinding drops any propagated origin for the name (the new value's provenance is set
|
|
1452
2168
|
# afterward by `with_local_origin` when it is a `Dynamic` with a recorded cause). Zero-alloc when the name
|
|
1453
2169
|
# carries no origin — the overwhelming common case, so this stays off the with_local hot-path budget.
|
|
@@ -1464,6 +2180,11 @@ module Rigor
|
|
|
1464
2180
|
# value on the branch where it derives from an inferred parameter, so if EITHER branch taints it, a
|
|
1465
2181
|
# downstream use could observe the lower-bound value and firing on it is an FP (`x = param else x = 5;
|
|
1466
2182
|
# x.foo`). Intersecting would lose the taint at the merge and re-surface the false positive.
|
|
2183
|
+
#
|
|
2184
|
+
# Issue #1362 — a local both branches mark as copies of global seeds keeps its mark with the union of the globals
|
|
2185
|
+
# the two record, so a consumer compares against the writes to every one of them. A `(:local, name)` mark only
|
|
2186
|
+
# one branch backs with a record is dropped with it: that branch copies a global and the other an ivar, so no
|
|
2187
|
+
# comparison answers for the merge and it is flow-live.
|
|
1467
2188
|
def join_declaration_sourced(other)
|
|
1468
2189
|
mine = @declaration_sourced
|
|
1469
2190
|
theirs = other.declaration_sourced
|
|
@@ -1476,12 +2197,33 @@ module Rigor
|
|
|
1476
2197
|
if mine.empty? || theirs.empty?
|
|
1477
2198
|
EMPTY_DECLARATION_SOURCED
|
|
1478
2199
|
else
|
|
1479
|
-
mine.select { |ref| ref[0] != :inferred_param && theirs.include?(ref) }
|
|
2200
|
+
join_copy_records(mine.select { |ref| ref[0] != :inferred_param && theirs.include?(ref) }, mine, theirs)
|
|
1480
2201
|
end
|
|
1481
2202
|
merged = inferred.merge(intersected)
|
|
1482
2203
|
merged.empty? ? EMPTY_DECLARATION_SOURCED : merged.freeze
|
|
1483
2204
|
end
|
|
1484
2205
|
|
|
2206
|
+
# `kept`, the refs both branches carry, with the `[:global_copy, …]` records rejoined ({#join_declaration_sourced}).
|
|
2207
|
+
def join_copy_records(kept, mine, theirs)
|
|
2208
|
+
mine_copies = global_copy_records(mine)
|
|
2209
|
+
theirs_copies = global_copy_records(theirs)
|
|
2210
|
+
return kept if mine_copies.empty? && theirs_copies.empty?
|
|
2211
|
+
|
|
2212
|
+
joined = kept.reject { |ref| ref[0] == :global_copy }
|
|
2213
|
+
(mine_copies.keys | theirs_copies.keys).each do |name|
|
|
2214
|
+
if mine_copies.key?(name) && theirs_copies.key?(name) && joined.include?([:local, name])
|
|
2215
|
+
joined.concat(mine_copies[name] | theirs_copies[name])
|
|
2216
|
+
else
|
|
2217
|
+
joined.delete([:local, name])
|
|
2218
|
+
end
|
|
2219
|
+
end
|
|
2220
|
+
joined
|
|
2221
|
+
end
|
|
2222
|
+
|
|
2223
|
+
def global_copy_records(refs)
|
|
2224
|
+
refs.each_with_object({}) { |ref, acc| (acc[ref[1]] ||= []) << ref if ref[0] == :global_copy }
|
|
2225
|
+
end
|
|
2226
|
+
|
|
1485
2227
|
def indexed_key(receiver_kind, receiver_name, key)
|
|
1486
2228
|
IndexedKey.new(
|
|
1487
2229
|
receiver_kind: receiver_kind.to_sym,
|
|
@@ -1538,6 +2280,15 @@ module Rigor
|
|
|
1538
2280
|
(mine | theirs).freeze
|
|
1539
2281
|
end
|
|
1540
2282
|
|
|
2283
|
+
def join_repeated_or_writes(other)
|
|
2284
|
+
mine = @repeated_or_writes
|
|
2285
|
+
theirs = other.repeated_or_writes
|
|
2286
|
+
return mine if mine.equal?(theirs) || theirs.empty?
|
|
2287
|
+
return theirs if mine.empty?
|
|
2288
|
+
|
|
2289
|
+
mine.merge(theirs).freeze
|
|
2290
|
+
end
|
|
2291
|
+
|
|
1541
2292
|
def add_declaration_sourced(kind, name)
|
|
1542
2293
|
ref = [kind.to_sym, name.to_sym]
|
|
1543
2294
|
return @declaration_sourced if @declaration_sourced.include?(ref)
|
|
@@ -1556,6 +2307,38 @@ module Rigor
|
|
|
1556
2307
|
dropped.freeze
|
|
1557
2308
|
end
|
|
1558
2309
|
|
|
2310
|
+
# The `(:local, name)` mark and, when it was there, the `[:global_copy, name, global]` records that only ever sit
|
|
2311
|
+
# beside it ({#with_global_copy_marks}).
|
|
2312
|
+
def drop_local_declaration_marks(name)
|
|
2313
|
+
dropped = drop_declaration_sourced_for(:local, name)
|
|
2314
|
+
return dropped if dropped.equal?(@declaration_sourced)
|
|
2315
|
+
|
|
2316
|
+
name = name.to_sym
|
|
2317
|
+
return dropped unless dropped.any? { |ref| ref[0] == :global_copy && ref[1] == name }
|
|
2318
|
+
|
|
2319
|
+
dropped.dup.delete_if { |ref| ref[0] == :global_copy && ref[1] == name }.freeze
|
|
2320
|
+
end
|
|
2321
|
+
|
|
2322
|
+
def add_guard_record(key, pre_guard)
|
|
2323
|
+
return @guard_records if @guard_records.key?(key)
|
|
2324
|
+
|
|
2325
|
+
@guard_records.merge(key => pre_guard).freeze
|
|
2326
|
+
end
|
|
2327
|
+
|
|
2328
|
+
def drop_bot_guard_class(kind, name)
|
|
2329
|
+
return @bot_guard_classes if @bot_guard_classes.empty?
|
|
2330
|
+
|
|
2331
|
+
key = [kind, kind == :constant ? name : name.to_sym]
|
|
2332
|
+
@bot_guard_classes.key?(key) ? @bot_guard_classes.except(key).freeze : @bot_guard_classes
|
|
2333
|
+
end
|
|
2334
|
+
|
|
2335
|
+
def drop_guard_record(kind, name)
|
|
2336
|
+
return @guard_records if @guard_records.empty?
|
|
2337
|
+
|
|
2338
|
+
key = [kind, name]
|
|
2339
|
+
@guard_records.key?(key) ? @guard_records.except(key).freeze : @guard_records
|
|
2340
|
+
end
|
|
2341
|
+
|
|
1559
2342
|
def drop_chain_narrowings_for(receiver_kind, receiver_name)
|
|
1560
2343
|
return @method_chain_narrowings if @method_chain_narrowings.empty?
|
|
1561
2344
|
|