rigortype 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/capability_roles/capability_roles.rbs +36 -0
- data/data/core_overlay/enumerable.rbs +51 -0
- data/data/core_overlay/enumerator.rbs +84 -0
- data/data/core_overlay/hash_rbs3.rbs +41 -0
- data/data/core_overlay/process.rbs +40 -0
- data/data/core_overlay/string_io.rbs +33 -0
- data/data/effects/core.yml +3 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
- data/docs/handbook/02-everyday-types.md +17 -14
- data/docs/handbook/03-narrowing.md +132 -14
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/05-methods-and-blocks.md +1 -1
- data/docs/handbook/06-classes.md +2 -2
- data/docs/handbook/07-rbs-and-extended.md +19 -1
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +525 -13
- data/docs/handbook/README.md +1 -1
- data/docs/handbook/appendix-elixir.md +2 -2
- data/docs/handbook/appendix-go.md +2 -2
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +2 -2
- data/docs/handbook/appendix-phpstan.md +1 -1
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +4 -4
- data/docs/handbook/appendix-typescript.md +1 -1
- data/docs/llms.txt +2 -0
- data/docs/manual/02-cli-reference.md +95 -6
- data/docs/manual/03-configuration.md +18 -0
- data/docs/manual/04-diagnostics.md +15 -0
- data/docs/manual/07-plugins.md +15 -5
- data/docs/manual/08-skills.md +23 -2
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/11-ci.md +9 -0
- data/docs/manual/15-type-protection-coverage.md +8 -0
- data/docs/manual/16-rbs-extended-annotations.md +46 -8
- data/docs/manual/18-removing-dead-code.md +10 -8
- data/docs/manual/19-effect-labels.md +10 -0
- data/docs/manual/plugins/README.md +7 -0
- data/docs/manual/plugins/rigor-actioncable.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +7 -0
- data/docs/manual/plugins/rigor-actionpack.md +261 -0
- data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
- data/docs/manual/plugins/rigor-activejob.md +7 -0
- data/docs/manual/plugins/rigor-activerecord.md +138 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
- data/docs/manual/plugins/rigor-grape.md +106 -0
- data/docs/manual/plugins/rigor-graphql.md +23 -2
- data/docs/manual/plugins/rigor-pundit.md +8 -1
- data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
- data/docs/manual/plugins/rigor-rails-routes.md +7 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
- data/docs/manual/plugins/rigor-sidekiq.md +8 -1
- data/docs/manual/plugins/rigor-sorbet.md +21 -5
- data/exe/rigor +19 -4
- data/lib/rigor/analysis/baseline.rb +3 -3
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
- data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
- data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
- data/lib/rigor/analysis/check_rules.rb +718 -79
- data/lib/rigor/analysis/crash_signature.rb +8 -24
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
- data/lib/rigor/analysis/erb_template_detector.rb +1 -2
- data/lib/rigor/analysis/fact_store.rb +15 -1
- data/lib/rigor/analysis/incremental.rb +42 -0
- data/lib/rigor/analysis/incremental_session.rb +117 -26
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/graph.rb +3 -5
- data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
- data/lib/rigor/analysis/reachability/project_files.rb +2 -2
- data/lib/rigor/analysis/reachability/scan.rb +5 -5
- data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
- data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
- data/lib/rigor/analysis/result.rb +1 -3
- data/lib/rigor/analysis/rule_catalog.rb +131 -1
- data/lib/rigor/analysis/run_cache_key.rb +21 -12
- data/lib/rigor/analysis/run_cache_probe.rb +3 -5
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
- data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
- data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +407 -51
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +91 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +399 -0
- data/lib/rigor/analysis/worker_session.rb +62 -20
- data/lib/rigor/bleeding_edge.rb +10 -24
- data/lib/rigor/builtins/hkt_builtins.rb +3 -2
- data/lib/rigor/builtins/imported_refinements.rb +159 -21
- data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
- data/lib/rigor/builtins/regex_refinement.rb +21 -13
- data/lib/rigor/builtins/static_return_refinements.rb +7 -6
- data/lib/rigor/cache/annotation_location.rb +2 -4
- data/lib/rigor/cache/descriptor.rb +63 -20
- data/lib/rigor/cache/engine_source.rb +33 -5
- data/lib/rigor/cache/file_digest.rb +9 -1
- data/lib/rigor/cache/incremental_snapshot.rb +72 -6
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
- data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
- data/lib/rigor/cache/rbs_constant_table.rb +0 -3
- data/lib/rigor/cache/rbs_descriptor.rb +112 -14
- data/lib/rigor/cache/rbs_environment.rb +8 -4
- data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
- data/lib/rigor/cache/store.rb +76 -32
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/annotate_command.rb +5 -6
- data/lib/rigor/cli/check_command.rb +57 -10
- data/lib/rigor/cli/check_invocation.rb +6 -11
- data/lib/rigor/cli/check_runner_factory.rb +1 -5
- data/lib/rigor/cli/coverage_command.rb +1 -1
- data/lib/rigor/cli/coverage_mutation.rb +12 -3
- data/lib/rigor/cli/coverage_scan.rb +1 -5
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/doc_links.rb +4 -4
- data/lib/rigor/cli/docs_command.rb +6 -6
- data/lib/rigor/cli/doctor_command.rb +85 -38
- data/lib/rigor/cli/effects_command.rb +2 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +34 -1
- data/lib/rigor/cli/fused_protection_report.rb +7 -1
- data/lib/rigor/cli/lsp_command.rb +1 -1
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
- data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
- data/lib/rigor/cli/mutation_protection_report.rb +7 -1
- data/lib/rigor/cli/plugin_command.rb +4 -4
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/prism_colorizer.rb +1 -2
- data/lib/rigor/cli/protection_fork_scan.rb +6 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +212 -31
- data/lib/rigor/cli/skill_command.rb +2 -2
- data/lib/rigor/cli/skill_deep_probe.rb +4 -4
- data/lib/rigor/cli/skill_describe.rb +35 -35
- data/lib/rigor/cli/trace_command.rb +4 -4
- data/lib/rigor/cli/trace_renderer.rb +5 -6
- data/lib/rigor/cli/triage_command.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +29 -11
- data/lib/rigor/cli/type_of_renderer.rb +20 -9
- data/lib/rigor/cli/type_of_template_probe.rb +189 -0
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli/unused_command.rb +11 -3
- data/lib/rigor/cli/upgrade_command.rb +1 -1
- data/lib/rigor/cli.rb +89 -9
- data/lib/rigor/config_audit.rb +30 -5
- data/lib/rigor/configuration/severity_profile.rb +25 -9
- data/lib/rigor/configuration.rb +109 -7
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +12 -5
- data/lib/rigor/effects/callee_rule.rb +368 -0
- data/lib/rigor/effects/catalog.rb +7 -4
- data/lib/rigor/effects/collector.rb +11 -5
- data/lib/rigor/effects/config_envelopes.rb +15 -10
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/definition_lines.rb +25 -6
- data/lib/rigor/effects/effect_table.rb +11 -4
- data/lib/rigor/effects/entry_points.rb +0 -2
- data/lib/rigor/effects/envelope_check.rb +9 -9
- data/lib/rigor/effects/envelope_index.rb +20 -8
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +5 -7
- data/lib/rigor/effects/identity.rb +20 -6
- data/lib/rigor/effects/inline_anchor.rb +7 -7
- data/lib/rigor/effects/label_intent.rb +3 -4
- data/lib/rigor/effects/liskov_check.rb +8 -8
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +22 -1
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +47 -37
- data/lib/rigor/effects/propagator.rb +297 -19
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +122 -73
- data/lib/rigor/effects/signature_sources.rb +4 -6
- data/lib/rigor/effects/snapshot.rb +12 -11
- data/lib/rigor/effects/snapshot_diff.rb +1 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +393 -38
- data/lib/rigor/effects/unknown_label_check.rb +3 -8
- data/lib/rigor/effects/unknown_label_report.rb +3 -4
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
- data/lib/rigor/environment/class_registry.rb +3 -1
- data/lib/rigor/environment/failure_slot.rb +2 -2
- data/lib/rigor/environment/installed_gem_set.rb +85 -0
- data/lib/rigor/environment/lockfile_resolver.rb +68 -4
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
- data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
- data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
- data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
- data/lib/rigor/environment/rbs_loader.rb +674 -79
- data/lib/rigor/environment.rb +174 -24
- data/lib/rigor/flow_contribution/merger.rb +0 -2
- data/lib/rigor/flow_contribution.rb +11 -13
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +292 -17
- data/lib/rigor/inference/block_auto_splat.rb +216 -0
- data/lib/rigor/inference/block_call_timing.rb +338 -0
- data/lib/rigor/inference/block_parameter_binder.rb +75 -30
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +7 -6
- data/lib/rigor/inference/budget_trace.rb +3 -6
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +389 -18
- data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/coverage_scanner.rb +4 -6
- data/lib/rigor/inference/def_return_typer.rb +11 -7
- data/lib/rigor/inference/define_method_block_self.rb +64 -0
- data/lib/rigor/inference/dynamic_origin.rb +1 -1
- data/lib/rigor/inference/element_read_widening.rb +180 -0
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +2124 -468
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fork_map.rb +5 -7
- data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
- data/lib/rigor/inference/global_write_census.rb +239 -0
- data/lib/rigor/inference/guard_rebinding.rb +447 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/hkt_reducer.rb +2 -3
- data/lib/rigor/inference/hkt_registry.rb +4 -7
- data/lib/rigor/inference/index_write_widening.rb +16 -7
- data/lib/rigor/inference/indexed_narrowing.rb +61 -9
- data/lib/rigor/inference/jump_targets.rb +82 -0
- data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
- data/lib/rigor/inference/last_line.rb +340 -0
- data/lib/rigor/inference/last_status.rb +144 -0
- data/lib/rigor/inference/macro_block_self_type.rb +168 -21
- data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
- data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
- data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
- data/lib/rigor/inference/match_rebinding.rb +392 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
- data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +225 -25
- data/lib/rigor/inference/method_parameter_binder.rb +12 -9
- data/lib/rigor/inference/multi_target_binder.rb +341 -55
- data/lib/rigor/inference/mutation_rejoin.rb +171 -0
- data/lib/rigor/inference/mutation_widening.rb +156 -85
- data/lib/rigor/inference/narrowing.rb +835 -162
- data/lib/rigor/inference/operand_effects.rb +167 -0
- data/lib/rigor/inference/operand_walk.rb +88 -0
- data/lib/rigor/inference/optimistic_origin.rb +153 -16
- data/lib/rigor/inference/origin_lookup.rb +2 -3
- data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
- data/lib/rigor/inference/pre_eval_constants.rb +6 -5
- data/lib/rigor/inference/precision_scanner.rb +3 -4
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +9 -4
- data/lib/rigor/inference/project_patched_scanner.rb +10 -6
- data/lib/rigor/inference/protection_scanner.rb +1 -2
- data/lib/rigor/inference/range_constant.rb +57 -0
- data/lib/rigor/inference/rbs_type_translator.rb +49 -11
- data/lib/rigor/inference/receiver_alias.rb +93 -4
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +76 -0
- data/lib/rigor/inference/repeated_or_writes.rb +463 -0
- data/lib/rigor/inference/return_barrier.rb +54 -0
- data/lib/rigor/inference/rewrite_mutation.rb +120 -0
- data/lib/rigor/inference/scope_indexer.rb +5228 -627
- data/lib/rigor/inference/statement_evaluator.rb +3452 -595
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +97 -0
- data/lib/rigor/inference/struct_fold_safety.rb +6 -7
- data/lib/rigor/inference/synthetic_method_index.rb +1 -2
- data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/inference/version_guard.rb +28 -21
- data/lib/rigor/inference/void_origin.rb +3 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -4
- data/lib/rigor/language_server/buffer_table.rb +4 -4
- data/lib/rigor/language_server/completion_provider.rb +1 -1
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -1
- data/lib/rigor/language_server/hover_provider.rb +1 -1
- data/lib/rigor/language_server/hover_renderer.rb +4 -4
- data/lib/rigor/language_server/incremental_sync.rb +6 -6
- data/lib/rigor/language_server/project_context.rb +7 -8
- data/lib/rigor/language_server/publish_batcher.rb +2 -2
- data/lib/rigor/language_server/selection_range_provider.rb +2 -2
- data/lib/rigor/language_server/server.rb +9 -9
- data/lib/rigor/language_server/signature_help_provider.rb +1 -1
- data/lib/rigor/language_server/uri.rb +1 -1
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +258 -26
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +167 -0
- data/lib/rigor/plugin/effect_attribution.rb +65 -11
- data/lib/rigor/plugin/effect_entry_points.rb +3 -3
- data/lib/rigor/plugin/fact_store.rb +6 -6
- data/lib/rigor/plugin/io_boundary.rb +93 -8
- data/lib/rigor/plugin/isolation.rb +29 -6
- data/lib/rigor/plugin/loader.rb +33 -13
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +94 -11
- data/lib/rigor/plugin/registry.rb +76 -12
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -4
- data/lib/rigor/plugin/type_node_resolver.rb +3 -3
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/plugin_gap_advisory.rb +96 -0
- data/lib/rigor/project_environment.rb +138 -0
- data/lib/rigor/protection/analysis_guard.rb +93 -14
- data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
- data/lib/rigor/protection/dependency_closure.rb +5 -8
- data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
- data/lib/rigor/protection/discovery_seed.rb +14 -12
- data/lib/rigor/protection/kill_signature.rb +1 -4
- data/lib/rigor/protection/measurement_integrity.rb +1 -3
- data/lib/rigor/protection/mutation_cache.rb +7 -11
- data/lib/rigor/protection/mutation_scanner.rb +12 -15
- data/lib/rigor/protection/mutator.rb +2 -1
- data/lib/rigor/protection/test_suite_oracle.rb +5 -5
- data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/reporter.rb +36 -10
- data/lib/rigor/rbs_extended.rb +88 -21
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +139 -0
- data/lib/rigor/reflection.rb +93 -110
- data/lib/rigor/runtime/jit.rb +6 -8
- data/lib/rigor/scope/discovery_index.rb +165 -2
- data/lib/rigor/scope.rb +1041 -32
- data/lib/rigor/sig_gen/alias_index.rb +289 -0
- data/lib/rigor/sig_gen/classification.rb +22 -5
- data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
- data/lib/rigor/sig_gen/generator.rb +673 -53
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/layout_index.rb +3 -4
- data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +18 -16
- data/lib/rigor/sig_gen/path_mapper.rb +4 -6
- data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
- data/lib/rigor/sig_gen/renderer.rb +128 -14
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
- data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +189 -41
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +153 -6
- data/lib/rigor/source/literals.rb +0 -23
- data/lib/rigor/source/node_children.rb +0 -2
- data/lib/rigor/source/node_locator.rb +5 -11
- data/lib/rigor/source/node_walker.rb +0 -6
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +1 -3
- data/lib/rigor/triage.rb +3 -5
- data/lib/rigor/type/accepts_result.rb +24 -6
- data/lib/rigor/type/combinator.rb +144 -5
- data/lib/rigor/type/data_class.rb +2 -2
- data/lib/rigor/type/data_instance.rb +4 -4
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/float_range.rb +128 -0
- data/lib/rigor/type/hash_shape.rb +6 -6
- data/lib/rigor/type/integer_range.rb +13 -8
- data/lib/rigor/type/nominal.rb +8 -2
- data/lib/rigor/type/refined.rb +4 -3
- data/lib/rigor/type/struct_class.rb +3 -3
- data/lib/rigor/type/struct_instance.rb +4 -4
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +1 -1
- data/lib/rigor/type_node/range_literal.rb +25 -0
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
- data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
- data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
- data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
- data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
- data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
- data/sig/rigor/analysis/baseline.rbs +69 -7
- data/sig/rigor/analysis/fact_store.rbs +1 -1
- data/sig/rigor/analysis/project_scan.rbs +74 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
- data/sig/rigor/cache.rbs +2 -3
- data/sig/rigor/effects/config_envelopes.rbs +100 -0
- data/sig/rigor/effects/effect_table.rbs +60 -0
- data/sig/rigor/effects/envelope.rbs +97 -0
- data/sig/rigor/effects/envelope_index.rbs +33 -0
- data/sig/rigor/effects/file_collection.rbs +81 -0
- data/sig/rigor/effects/label.rbs +26 -0
- data/sig/rigor/effects/label_set.rbs +46 -0
- data/sig/rigor/effects/method_key.rbs +24 -0
- data/sig/rigor/effects/origin.rbs +50 -0
- data/sig/rigor/effects/plugin_facts.rbs +141 -0
- data/sig/rigor/effects/registry.rbs +68 -0
- data/sig/rigor/effects/summary.rbs +50 -0
- data/sig/rigor/effects/taint_cause.rbs +12 -0
- data/sig/rigor/environment.rbs +17 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +9 -14
- data/sig/rigor/plugin/additional_initializer.rbs +36 -0
- data/sig/rigor/plugin/base.rbs +30 -7
- data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
- data/sig/rigor/plugin/effect_attribution.rbs +66 -0
- data/sig/rigor/plugin/effect_edge.rbs +33 -0
- data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +51 -11
- data/sig/rigor/plugin/protocol_contract.rbs +68 -0
- data/sig/rigor/plugin/registry.rbs +62 -1
- data/sig/rigor/plugin.rbs +1 -1
- data/sig/rigor/rbs_extended.rbs +1 -1
- data/sig/rigor/reflection.rbs +9 -6
- data/sig/rigor/scope.rbs +148 -12
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +33 -1
- data/sig/rigor.rbs +39 -20
- data/skills/rigor-ask/SKILL.md +16 -11
- data/skills/rigor-baseline-reduce/SKILL.md +4 -6
- data/skills/rigor-ci-setup/SKILL.md +17 -21
- data/skills/rigor-doctor/SKILL.md +24 -22
- data/skills/rigor-doctor/references/01-checks.md +97 -33
- data/skills/rigor-editor-setup/SKILL.md +6 -4
- data/skills/rigor-mcp-setup/SKILL.md +5 -4
- data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
- data/skills/rigor-next-steps/SKILL.md +4 -2
- data/skills/rigor-plugin-author/SKILL.md +19 -23
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
- data/skills/rigor-plugin-review/SKILL.md +6 -4
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
- data/skills/rigor-plugin-tune/SKILL.md +4 -2
- data/skills/rigor-project-init/SKILL.md +15 -10
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +43 -14
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
- data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
- data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
- data/skills/rigor-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +226 -0
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
- data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +133 -3
- data/lib/rigor/cli/probe_environment.rb +0 -85
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
data/lib/rigor/scope.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "analysis/fact_store"
|
|
|
7
7
|
require_relative "analysis/dependency_recorder"
|
|
8
8
|
require_relative "inference/expression_typer"
|
|
9
9
|
require_relative "inference/flow_tracer"
|
|
10
|
+
require_relative "inference/optimistic_origin"
|
|
10
11
|
require_relative "inference/statement_evaluator"
|
|
11
12
|
require_relative "inference/def_node_resolver"
|
|
12
13
|
|
|
@@ -21,12 +22,14 @@ module Rigor
|
|
|
21
22
|
attr_reader :environment, :locals, :fact_store, :self_type,
|
|
22
23
|
:ivars, :cvars, :globals,
|
|
23
24
|
:indexed_narrowings, :method_chain_narrowings,
|
|
24
|
-
:declaration_sourced,
|
|
25
|
+
:declaration_sourced, :published_constant_sourced,
|
|
25
26
|
:source_path, :discovery, :struct_fold_safe_locals,
|
|
26
|
-
:opaque_block_self, :lexical_nesting,
|
|
27
|
+
:opaque_block_self, :singleton_class_body, :lexical_nesting,
|
|
27
28
|
:dynamic_origins, :local_origins, :ivar_origins,
|
|
28
29
|
:void_origins, :plugin_typed_calls,
|
|
29
|
-
:optimistic_origins, :optimistic_locals, :optimistic_ivars
|
|
30
|
+
:optimistic_origins, :optimistic_locals, :optimistic_ivars,
|
|
31
|
+
:repeated_or_writes, :match_frame,
|
|
32
|
+
:constant_narrowings, :guard_records
|
|
30
33
|
|
|
31
34
|
# ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
|
|
32
35
|
# reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
|
|
@@ -47,14 +50,58 @@ module Rigor
|
|
|
47
50
|
def discovered_def_sources = @discovery.discovered_def_sources
|
|
48
51
|
def discovered_singleton_def_sources = @discovery.discovered_singleton_def_sources
|
|
49
52
|
def discovered_method_visibilities = @discovery.discovered_method_visibilities
|
|
53
|
+
def discovered_parameter_envelopes = @discovery.discovered_parameter_envelopes
|
|
50
54
|
def discovered_superclasses = @discovery.discovered_superclasses
|
|
55
|
+
|
|
56
|
+
# Issue #1097 — `{file path => [[start_offset, end_offset, name, kind, owner], ...]}`, the def /
|
|
57
|
+
# block / lambda body ranges {singleton_def_shadows_call?} / {instance_def_shadows_call?} order a
|
|
58
|
+
# project-defined override against.
|
|
59
|
+
def discovered_deferred_ranges = @discovery.discovered_deferred_ranges
|
|
60
|
+
# Issue #1120 — `{refined class name => {method name => [refining module names]}}`: what each
|
|
61
|
+
# `refine X do … end` block defines, visible only where a `using` of its module is in effect.
|
|
62
|
+
def discovered_refinements = @discovery.discovered_refinements
|
|
63
|
+
# Issue #1367 — the project's `Inference::GlobalWriteCensus`, a `Set` of frozen entries.
|
|
64
|
+
def discovered_global_write_census = @discovery.discovered_global_write_census
|
|
51
65
|
def discovered_includes = @discovery.discovered_includes
|
|
66
|
+
# Issue #1123 — `{qualified class or module name => [module names it `prepend`s, as written]}`, in
|
|
67
|
+
# instance-ancestor search order (nearest prepend first). Feeds {#prepends_of}, the one table that tells
|
|
68
|
+
# a `prepend` from an `include` at `def`-priority level.
|
|
69
|
+
def discovered_prepends = @discovery.discovered_prepends
|
|
70
|
+
def discovered_extends = @discovery.discovered_extends
|
|
52
71
|
def discovered_class_sources = @discovery.discovered_class_sources
|
|
53
72
|
# Issue #644 — `{qualified constant name => Set[declaring file]}`; seeded only on an ADR-46 recording run.
|
|
54
73
|
def constant_sources = @discovery.constant_sources
|
|
55
74
|
def published_constant_names = @discovery.published_constant_names
|
|
56
75
|
def local_constant_names = @discovery.local_constant_names
|
|
57
76
|
|
|
77
|
+
# Issue #617 — the census names sharing `name`'s last segment that bind, spelled as the census spells
|
|
78
|
+
# them (a `*::LIMIT` wildcard included), so the caller still decides which of them a reference resolves
|
|
79
|
+
# to. A `||=`-only name is left out while no other file memoizes its segment: the memoization idiom's
|
|
80
|
+
# own write, however often one file repeats it, is not what binds it.
|
|
81
|
+
def bound_constant_names(name)
|
|
82
|
+
@discovery.constant_writers[name.split("::").last] || EMPTY_BOUND_CONSTANT_NAMES
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Issue #1290 — the census names sharing `name`'s last segment that some write other than a memo `||=`
|
|
86
|
+
# assigns: the ones that shadow an outer constant of the name, so the lexical ladder stops at them
|
|
87
|
+
# (`Reflection.resolve_constant_type`). That asks once per constant reference, so a bare name is looked up
|
|
88
|
+
# as it stands rather than split.
|
|
89
|
+
def shadowing_constant_names(name)
|
|
90
|
+
segment = name.include?("::") ? name.split("::").last : name
|
|
91
|
+
@discovery.constant_shadowers[segment] || EMPTY_BOUND_CONSTANT_NAMES
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
EMPTY_BOUND_CONSTANT_NAMES = [].freeze
|
|
95
|
+
private_constant :EMPTY_BOUND_CONSTANT_NAMES
|
|
96
|
+
|
|
97
|
+
# Issue #667 — the instance-variable names of `class_name` whose class-ivar seed comes from a foreign
|
|
98
|
+
# published constant (`@mode = AppConfig::MODE` in `initialize`, read in a sibling method).
|
|
99
|
+
# `StatementEvaluator#seed_instance_ivars` stamps the flow mark for these at method-body entry, the way
|
|
100
|
+
# ADR-58's seed stamps its own; a method-local write or narrowing then drops it through `with_ivar`.
|
|
101
|
+
def published_constant_ivars_for(class_name)
|
|
102
|
+
@discovery.published_constant_ivars[class_name] || EMPTY_PUBLISHED_CONSTANT_IVARS
|
|
103
|
+
end
|
|
104
|
+
|
|
58
105
|
# Issue #644 — true when the constant reference `name` (as written: `MODE` or `AppConfig::MODE`) is
|
|
59
106
|
# published by the cross-file value-constant table and is NOT assigned by the file being analysed: a
|
|
60
107
|
# value this file's author cannot see the declaration of. {Analysis::CheckRules::PublishedConstantGuard}
|
|
@@ -76,14 +123,27 @@ module Rigor
|
|
|
76
123
|
# over already-qualified names rather than a second lexical ladder: the engine's ladder
|
|
77
124
|
# (`Reflection.resolve_constant_type`) answers with a TYPE and not with the candidate that won, and
|
|
78
125
|
# reimplementing the walk here is the very thing that made the receiver resolution wrong once already.
|
|
126
|
+
#
|
|
127
|
+
# Issue #667 — a third answer sits beside those two. `MODE2 = AppConfig::MODE` is assigned by this file,
|
|
128
|
+
# so the exemption above releases it, yet its value is still one the author never saw: the alias only
|
|
129
|
+
# renames the foreign declaration. `published_constant_alias_names` holds the last segments of exactly
|
|
130
|
+
# those aliases, and matching on the last segment over-answers in the WITHHOLDING direction, which is the
|
|
131
|
+
# safe one.
|
|
79
132
|
def published_constant?(name)
|
|
80
133
|
names = @discovery.published_constant_names
|
|
134
|
+
return true if published_constant_alias?(name)
|
|
81
135
|
return false if names.empty?
|
|
82
136
|
return false unless names.include?(name.split("::").last)
|
|
83
137
|
|
|
84
138
|
!locally_declared_constant?(name)
|
|
85
139
|
end
|
|
86
140
|
|
|
141
|
+
def published_constant_alias?(name)
|
|
142
|
+
aliases = @discovery.published_constant_alias_names
|
|
143
|
+
!aliases.empty? && aliases.include?(name.split("::").last)
|
|
144
|
+
end
|
|
145
|
+
private :published_constant_alias?
|
|
146
|
+
|
|
87
147
|
def locally_declared_constant?(reference_name)
|
|
88
148
|
local = @discovery.local_constant_names
|
|
89
149
|
return false if local.empty?
|
|
@@ -153,9 +213,40 @@ module Rigor
|
|
|
153
213
|
# threaded by reference through transitions; reset per method body (a fresh entry scope drops it), so the
|
|
154
214
|
# name keys never collide across bodies.
|
|
155
215
|
EMPTY_ORIGINS = {}.freeze
|
|
216
|
+
# Issue #667 — the set of variable references currently bound to a value COPIED out of a constant the
|
|
217
|
+
# project published and this file does not declare. Members are frozen `[kind, name]` pairs, the same
|
|
218
|
+
# spelling {EMPTY_DECLARATION_SOURCED} uses, but a DELIBERATELY SEPARATE carrier: this mark joins by
|
|
219
|
+
# UNION where ADR-58's kinds intersect, and it answers a different question (what a value's *constancy*
|
|
220
|
+
# rests on, not what a binding's *optionality* rests on) for a different consumer. Folding it into the
|
|
221
|
+
# ADR-58 set would be the third establishing transition its *Non-transitivity* passage reserves as a
|
|
222
|
+
# decision, on a Set that already carries two opposite join policies.
|
|
223
|
+
EMPTY_PUBLISHED_CONSTANT_SOURCED = Set.new.freeze
|
|
224
|
+
# Issue #667 — the empty answer of {#published_constant_ivars_for}, so a class with no such ivar (every
|
|
225
|
+
# class in a project that publishes nothing) allocates none.
|
|
226
|
+
EMPTY_PUBLISHED_CONSTANT_IVARS = Set.new.freeze
|
|
227
|
+
# The index `||=` sites of a repeating block body whose slot an earlier run of the body may have filled,
|
|
228
|
+
# keyed by node identity and laid by a block-return pass at the body's entry
|
|
229
|
+
# ({#with_repeated_or_writes}; `Inference::RepeatedOrWrites` decides which). The memoizing `||=` reading
|
|
230
|
+
# (`StatementEvaluator#index_compound_write_value`) is withheld at such a site. The mark belongs to the
|
|
231
|
+
# site, so no rebind or narrowing of a variable drops it, and a join keeps a site either arm holds: the
|
|
232
|
+
# mark only ever withholds that reading, whose answer is the narrower one.
|
|
233
|
+
EMPTY_REPEATED_OR_WRITES = {}.compare_by_identity.freeze
|
|
234
|
+
# Issue #1429 — the narrowing a guard leaves on a constant reference (`STDOUT.is_a?(StringIO)`, `if CONFIG`),
|
|
235
|
+
# keyed by the reference's spelling ({Inference::Narrowing.constant_key}: `"STDOUT"`, `"::Foo::BAR"`). A constant
|
|
236
|
+
# has no binding of its own in the scope, so the table is what a read in the guarded edge answers
|
|
237
|
+
# (`ExpressionTyper#type_of_constant_read`); a spelling it does not hold resolves as before.
|
|
238
|
+
EMPTY_CONSTANT_NARROWINGS = {}.freeze
|
|
239
|
+
# Issue #1429 — what each global and constant a guard narrowed was bound to before the guard, keyed
|
|
240
|
+
# `[:global, :$name]` / `[:constant, key]`. Ruby may rebind a global (or `const_set` a constant) whenever code the
|
|
241
|
+
# analysis cannot see runs, so a call or block that may run project or unresolved code restores each recorded
|
|
242
|
+
# binding to the union of this pre-guard type and its narrowed one ({#forget_guard_narrowings}). A write drops
|
|
243
|
+
# the record ({#with_global}), and a name the frame-local special-variable machinery owns is never recorded.
|
|
244
|
+
EMPTY_GUARD_RECORDS = {}.freeze
|
|
156
245
|
private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
|
|
157
246
|
:EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED,
|
|
158
|
-
:EMPTY_FOLD_SAFE, :EMPTY_ORIGINS
|
|
247
|
+
:EMPTY_FOLD_SAFE, :EMPTY_ORIGINS, :EMPTY_PUBLISHED_CONSTANT_SOURCED,
|
|
248
|
+
:EMPTY_PUBLISHED_CONSTANT_IVARS, :EMPTY_REPEATED_OR_WRITES,
|
|
249
|
+
:EMPTY_CONSTANT_NARROWINGS, :EMPTY_GUARD_RECORDS
|
|
159
250
|
|
|
160
251
|
class << self
|
|
161
252
|
def empty(environment: Environment.default, source_path: nil)
|
|
@@ -207,9 +298,11 @@ module Rigor
|
|
|
207
298
|
indexed_narrowings: EMPTY_INDEXED_NARROWINGS,
|
|
208
299
|
method_chain_narrowings: EMPTY_CHAIN_NARROWINGS,
|
|
209
300
|
declaration_sourced: EMPTY_DECLARATION_SOURCED,
|
|
301
|
+
published_constant_sourced: EMPTY_PUBLISHED_CONSTANT_SOURCED,
|
|
210
302
|
source_path: nil,
|
|
211
303
|
struct_fold_safe_locals: EMPTY_FOLD_SAFE,
|
|
212
304
|
opaque_block_self: false,
|
|
305
|
+
singleton_class_body: false,
|
|
213
306
|
lexical_nesting: nil,
|
|
214
307
|
dynamic_origins: {}.compare_by_identity,
|
|
215
308
|
local_origins: EMPTY_ORIGINS,
|
|
@@ -218,7 +311,11 @@ module Rigor
|
|
|
218
311
|
plugin_typed_calls: {}.compare_by_identity,
|
|
219
312
|
optimistic_origins: {}.compare_by_identity,
|
|
220
313
|
optimistic_locals: EMPTY_ORIGINS,
|
|
221
|
-
optimistic_ivars: EMPTY_ORIGINS
|
|
314
|
+
optimistic_ivars: EMPTY_ORIGINS,
|
|
315
|
+
repeated_or_writes: EMPTY_REPEATED_OR_WRITES,
|
|
316
|
+
match_frame: nil,
|
|
317
|
+
constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
|
|
318
|
+
guard_records: EMPTY_GUARD_RECORDS
|
|
222
319
|
)
|
|
223
320
|
@environment = environment
|
|
224
321
|
@locals = locals
|
|
@@ -231,9 +328,11 @@ module Rigor
|
|
|
231
328
|
@indexed_narrowings = indexed_narrowings
|
|
232
329
|
@method_chain_narrowings = method_chain_narrowings
|
|
233
330
|
@declaration_sourced = declaration_sourced
|
|
331
|
+
@published_constant_sourced = published_constant_sourced
|
|
234
332
|
@source_path = source_path
|
|
235
333
|
@struct_fold_safe_locals = struct_fold_safe_locals
|
|
236
334
|
@opaque_block_self = opaque_block_self
|
|
335
|
+
@singleton_class_body = singleton_class_body
|
|
237
336
|
@lexical_nesting = lexical_nesting
|
|
238
337
|
@dynamic_origins = dynamic_origins
|
|
239
338
|
@local_origins = local_origins
|
|
@@ -243,6 +342,10 @@ module Rigor
|
|
|
243
342
|
@optimistic_origins = optimistic_origins
|
|
244
343
|
@optimistic_locals = optimistic_locals
|
|
245
344
|
@optimistic_ivars = optimistic_ivars
|
|
345
|
+
@repeated_or_writes = repeated_or_writes
|
|
346
|
+
@match_frame = match_frame
|
|
347
|
+
@constant_narrowings = constant_narrowings
|
|
348
|
+
@guard_records = guard_records
|
|
246
349
|
freeze
|
|
247
350
|
end
|
|
248
351
|
|
|
@@ -255,19 +358,30 @@ module Rigor
|
|
|
255
358
|
self
|
|
256
359
|
end
|
|
257
360
|
|
|
258
|
-
def optimistic_local(name) = @optimistic_locals[name.to_sym]
|
|
259
|
-
def optimistic_ivar(name) = @optimistic_ivars[name.to_sym]
|
|
361
|
+
def optimistic_local(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_locals[name.to_sym])
|
|
362
|
+
def optimistic_ivar(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_ivars[name.to_sym])
|
|
363
|
+
|
|
364
|
+
# Issue #1302 — what the value a marked local / ivar is bound to answers on a miss, as its binding recorded
|
|
365
|
+
# it beside the mark, or {Inference::OptimisticOrigin::UNKNOWN_MISS}. The answer lives in the mark's own
|
|
366
|
+
# table entry ({Inference::OptimisticOrigin::BoundMark}), so every transition that keeps or drops the mark
|
|
367
|
+
# keeps or drops the answer with it.
|
|
368
|
+
def optimistic_local_miss(name) = Inference::OptimisticOrigin.bound_miss(@optimistic_locals[name.to_sym])
|
|
369
|
+
def optimistic_ivar_miss(name) = Inference::OptimisticOrigin.bound_miss(@optimistic_ivars[name.to_sym])
|
|
260
370
|
|
|
261
|
-
|
|
371
|
+
# `miss` is what the bound value answers when the bet fails ({Inference::OptimisticOrigin.miss_answer}); the
|
|
372
|
+
# default records none, so a read through the binding widens as an untold miss does.
|
|
373
|
+
def with_optimistic_local(name, cause, miss: Inference::OptimisticOrigin::UNKNOWN_MISS)
|
|
262
374
|
return self if cause.nil?
|
|
263
375
|
|
|
264
|
-
|
|
376
|
+
mark = Inference::OptimisticOrigin.bound_mark(cause, miss)
|
|
377
|
+
rebuild(optimistic_locals: @optimistic_locals.merge(name.to_sym => mark).freeze)
|
|
265
378
|
end
|
|
266
379
|
|
|
267
|
-
def with_optimistic_ivar(name, cause)
|
|
380
|
+
def with_optimistic_ivar(name, cause, miss: Inference::OptimisticOrigin::UNKNOWN_MISS)
|
|
268
381
|
return self if cause.nil?
|
|
269
382
|
|
|
270
|
-
|
|
383
|
+
mark = Inference::OptimisticOrigin.bound_mark(cause, miss)
|
|
384
|
+
rebuild(optimistic_ivars: @optimistic_ivars.merge(name.to_sym => mark).freeze)
|
|
271
385
|
end
|
|
272
386
|
|
|
273
387
|
# ADR-82 WD1 — the propagated origin of the `Dynamic` value currently bound to a local / instance
|
|
@@ -295,6 +409,13 @@ module Rigor
|
|
|
295
409
|
end
|
|
296
410
|
|
|
297
411
|
def with_local(name, type)
|
|
412
|
+
bind_local(name, type, keep_marks: false)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# The one body behind {#with_local} and {#with_mutated_local}, which differ only in whether the two marks a
|
|
416
|
+
# write drops survive. Passing the kept sets through unchanged, rather than dropping and re-adding them, keeps
|
|
417
|
+
# a mutation's rebind to the one `rebuild` a write costs.
|
|
418
|
+
def bind_local(name, type, keep_marks:)
|
|
298
419
|
# `rigor trace` — the moment a local enters the scope.
|
|
299
420
|
Inference::FlowTracer.bind(name, type) if Inference::FlowTracer.active?
|
|
300
421
|
new_locals = @locals.merge(name.to_sym => type).freeze
|
|
@@ -313,10 +434,15 @@ module Rigor
|
|
|
313
434
|
rebuild(locals: new_locals, fact_store: new_fact_store,
|
|
314
435
|
indexed_narrowings: new_indexed_narrowings,
|
|
315
436
|
method_chain_narrowings: new_chain_narrowings,
|
|
316
|
-
declaration_sourced:
|
|
437
|
+
declaration_sourced: keep_marks ? @declaration_sourced : drop_local_declaration_marks(name),
|
|
438
|
+
# Issue #667 — rebinding is flow-live for the published-constant mark too: the new value need
|
|
439
|
+
# not be a copy of anything. `with_published_constant_mark` re-stamps afterward when the write's
|
|
440
|
+
# rvalue is one.
|
|
441
|
+
published_constant_sourced: drop_published_constant_sourced_for(:local, name),
|
|
317
442
|
local_origins: drop_origin(@local_origins, name),
|
|
318
|
-
optimistic_locals: drop_origin(@optimistic_locals, name))
|
|
443
|
+
optimistic_locals: keep_marks ? @optimistic_locals : drop_origin(@optimistic_locals, name))
|
|
319
444
|
end
|
|
445
|
+
private :bind_local
|
|
320
446
|
|
|
321
447
|
def with_fact(fact)
|
|
322
448
|
rebuild(fact_store: fact_store.with_fact(fact))
|
|
@@ -380,6 +506,27 @@ module Rigor
|
|
|
380
506
|
# True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
|
|
381
507
|
def opaque_block_self? = @opaque_block_self
|
|
382
508
|
|
|
509
|
+
# Issue #963 — marks the body of a `class << ...` as such. Inside it `self` is the SINGLETON class, which
|
|
510
|
+
# Rigor models with the same `Singleton[X]` carrier a `class X` body gets, so the carrier alone cannot say
|
|
511
|
+
# which of the two a scope came from. The distinction decides what `define_method` does: in a `class << self`
|
|
512
|
+
# body it defines a CLASS method (`self` there is the singleton class), while in every other body reached
|
|
513
|
+
# with a `Singleton[X]` self — a `class X` body, `def self.x`, a `def` inside `class << self` — `self` is the
|
|
514
|
+
# class object and the same call defines an INSTANCE method.
|
|
515
|
+
#
|
|
516
|
+
# The mark is stamped at singleton-class-body entry and inherited by every scope derived inside it, blocks
|
|
517
|
+
# included; a `def` body starts from a fresh scope and therefore clears it by construction, which is exactly
|
|
518
|
+
# the boundary the distinction needs. A meta-class body (`Class.new do ... end`) clears it explicitly: that
|
|
519
|
+
# block is a class body of its own, whatever encloses it.
|
|
520
|
+
def with_singleton_class_body(flag)
|
|
521
|
+
return self if @singleton_class_body == flag
|
|
522
|
+
|
|
523
|
+
rebuild(singleton_class_body: flag)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# True when this scope IS a `class << ...` body (not merely inside one lexically — a `def` reached from it
|
|
527
|
+
# answers false).
|
|
528
|
+
def singleton_class_body? = @singleton_class_body
|
|
529
|
+
|
|
383
530
|
# True when `name`'s `Struct` member reads are fold-safe in this body (the local is provably never mutated /
|
|
384
531
|
# aliased / escaped).
|
|
385
532
|
def struct_fold_safe?(name)
|
|
@@ -419,6 +566,7 @@ module Rigor
|
|
|
419
566
|
indexed_narrowings: new_indexed_narrowings,
|
|
420
567
|
method_chain_narrowings: new_chain_narrowings,
|
|
421
568
|
declaration_sourced: drop_declaration_sourced_for(:ivar, name),
|
|
569
|
+
published_constant_sourced: drop_published_constant_sourced_for(:ivar, name),
|
|
422
570
|
ivar_origins: drop_origin(@ivar_origins, name),
|
|
423
571
|
optimistic_ivars: drop_origin(@optimistic_ivars, name))
|
|
424
572
|
end
|
|
@@ -445,9 +593,41 @@ module Rigor
|
|
|
445
593
|
rebuild(declaration_sourced: add_declaration_sourced(:local, name))
|
|
446
594
|
end
|
|
447
595
|
|
|
596
|
+
# Issue #1287 — rebinds `name` for an in-place mutation of the object it already holds: a mutator's widening
|
|
597
|
+
# (`r << x`), a content floor after a closure or callee mutated it, an element or member write through it. The
|
|
598
|
+
# binding still names the same object, so this is not a flow-live write, and the marks `with_local` drops stay:
|
|
599
|
+
# ADR-58's declaration-sourced mark and issue #286's optimistic nil-freeness mark, with the miss answer issue
|
|
600
|
+
# #1302 records in it. A source-level write keeps going through `with_local`, which drops both.
|
|
601
|
+
#
|
|
602
|
+
# Two other per-local tables are deliberately still dropped. An ADR-82 `local_origins` cause explains the
|
|
603
|
+
# `Dynamic` the ASSIGNMENT bound, and a floor's `Dynamic[top]` has a different cause. Issue #667's
|
|
604
|
+
# published-constant mark can never be live here: only a frozen scalar publishes, and a frozen value is not
|
|
605
|
+
# mutated in place.
|
|
606
|
+
def with_mutated_local(name, type)
|
|
607
|
+
bind_local(name, type, keep_marks: true)
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# Issue #667 — record that `name` is currently bound to a value copied out of a foreign published
|
|
611
|
+
# constant. Always applied AFTER the `with_local` / `with_ivar` that binds the value (both drop the mark
|
|
612
|
+
# unconditionally), exactly as {#with_local_declaration_mark} is.
|
|
613
|
+
def with_published_constant_mark(kind, name)
|
|
614
|
+
rebuild(published_constant_sourced: add_published_constant_sourced(kind, name))
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# Issue #667 — true when `(kind, name)`'s current binding is a copy of a constant the project published
|
|
618
|
+
# and this file does not declare. Asked through
|
|
619
|
+
# {Analysis::CheckRules::PublishedConstantGuard}, never directly from a rule.
|
|
620
|
+
def published_constant_sourced?(kind, name)
|
|
621
|
+
return false if @published_constant_sourced.empty?
|
|
622
|
+
|
|
623
|
+
@published_constant_sourced.include?([kind.to_sym, name.to_sym])
|
|
624
|
+
end
|
|
625
|
+
|
|
448
626
|
# ADR-58 WD1 — true when `(kind, name)`'s binding optionality is purely declaration-sourced (no flow-live
|
|
449
627
|
# write/narrowing has touched it).
|
|
450
628
|
def declaration_sourced?(kind, name)
|
|
629
|
+
return false if @declaration_sourced.empty?
|
|
630
|
+
|
|
451
631
|
@declaration_sourced.include?([kind.to_sym, name.to_sym])
|
|
452
632
|
end
|
|
453
633
|
|
|
@@ -491,24 +671,287 @@ module Rigor
|
|
|
491
671
|
rebuild(cvars: @cvars.merge(name.to_sym => type).freeze)
|
|
492
672
|
end
|
|
493
673
|
|
|
674
|
+
# Issue #1362 — a write or narrowing of a global is flow-live, so it drops the ADR-58 `:global` mark
|
|
675
|
+
# {#seed_declaration_sourced_global} stamped on the program-global seed.
|
|
676
|
+
#
|
|
677
|
+
# Issue #1429 — `$stdout` and `$>` are one variable, so a write to either also restores a guard's narrowing of the
|
|
678
|
+
# other ({#forget_guard_narrowing_of}).
|
|
494
679
|
def with_global(name, type)
|
|
495
|
-
|
|
680
|
+
name = name.to_sym
|
|
681
|
+
written = rebuild(globals: @globals.merge(name => type).freeze,
|
|
682
|
+
declaration_sourced: drop_declaration_sourced_for(:global, name),
|
|
683
|
+
guard_records: drop_guard_record(:global, name))
|
|
684
|
+
alias_name = STDOUT_ALIASES[name]
|
|
685
|
+
alias_name ? written.forget_guard_narrowing_of(:global, alias_name) : written
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
STDOUT_ALIASES = { :$stdout => :$>, :$> => :$stdout }.freeze
|
|
689
|
+
private_constant :STDOUT_ALIASES
|
|
690
|
+
|
|
691
|
+
# Issue #1429 — {#forget_guard_narrowings} for the one name `[kind, name]`.
|
|
692
|
+
def forget_guard_narrowing_of(kind, name)
|
|
693
|
+
key = [kind, name]
|
|
694
|
+
pre_guard = @guard_records[key]
|
|
695
|
+
return self if pre_guard.nil?
|
|
696
|
+
|
|
697
|
+
if kind == :global
|
|
698
|
+
current = @globals[name]
|
|
699
|
+
globals = current ? @globals.merge(name => Type::Combinator.union(pre_guard, current)).freeze : @globals
|
|
700
|
+
rebuild(globals: globals, guard_records: @guard_records.except(key).freeze)
|
|
701
|
+
else
|
|
702
|
+
current = @constant_narrowings[name]
|
|
703
|
+
constants = if current
|
|
704
|
+
@constant_narrowings.merge(name => Type::Combinator.union(pre_guard, current)).freeze
|
|
705
|
+
else
|
|
706
|
+
@constant_narrowings
|
|
707
|
+
end
|
|
708
|
+
rebuild(constant_narrowings: constants, guard_records: @guard_records.except(key).freeze)
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Issue #1429 — binds the global `name` to `type` on a guard's edge, recording `pre_guard`, the binding the guard
|
|
713
|
+
# narrowed, unless an earlier guard already recorded one ({EMPTY_GUARD_RECORDS}). `record: false` is the
|
|
714
|
+
# frame-local specials' form, which binds without a record: their own machinery forgets them.
|
|
715
|
+
def with_guarded_global(name, type, pre_guard, record: true)
|
|
716
|
+
name = name.to_sym
|
|
717
|
+
records = record ? add_guard_record([:global, name].freeze, pre_guard) : drop_guard_record(:global, name)
|
|
718
|
+
rebuild(globals: @globals.merge(name => type).freeze,
|
|
719
|
+
declaration_sourced: drop_declaration_sourced_for(:global, name),
|
|
720
|
+
guard_records: records)
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
# Issue #1429 — the type a guard narrowed the constant reference `key` to, or nil.
|
|
724
|
+
def constant_narrowing(key)
|
|
725
|
+
return nil if @constant_narrowings.empty?
|
|
726
|
+
|
|
727
|
+
@constant_narrowings[key]
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
# Issue #1429 — the constant reference `key` read as `type` on a guard's edge, recording `pre_guard` as
|
|
731
|
+
# {#with_guarded_global} does.
|
|
732
|
+
def with_constant_narrowing(key, type, pre_guard)
|
|
733
|
+
rebuild(constant_narrowings: @constant_narrowings.merge(key => type).freeze,
|
|
734
|
+
guard_records: add_guard_record([:constant, key].freeze, pre_guard))
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# Issue #1429 — every constant reference whose last segment is `name` with no narrowing: what a write to a constant
|
|
738
|
+
# named `name` leaves, since `Foo::BAR` and `BAR` may name the one it wrote.
|
|
739
|
+
def without_constant_narrowings_named(name)
|
|
740
|
+
keys = @constant_narrowings.keys.select { |key| key.split("::").last == name }
|
|
741
|
+
return self if keys.empty?
|
|
742
|
+
|
|
743
|
+
records = @guard_records.reject { |(kind, key), _| kind == :constant && keys.include?(key) }
|
|
744
|
+
rebuild(constant_narrowings: @constant_narrowings.except(*keys).freeze, guard_records: records.freeze)
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
# Issue #1429 — the constant reference `key` with no narrowing.
|
|
748
|
+
def without_constant_narrowing(key)
|
|
749
|
+
return self unless @constant_narrowings.key?(key)
|
|
750
|
+
|
|
751
|
+
rebuild(constant_narrowings: @constant_narrowings.except(key).freeze,
|
|
752
|
+
guard_records: @guard_records.except([:constant, key]).freeze)
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# True when a guard's narrowing of a global or constant is live, the state {#forget_guard_narrowings} drops and
|
|
756
|
+
# so the gate on every scan that decides whether to.
|
|
757
|
+
def guard_narrowed?
|
|
758
|
+
!@guard_records.empty?
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# Issue #1429 — this scope past code that may rebind a global or a constant: each one a guard narrowed reads
|
|
762
|
+
# the union of its narrowed type and the binding the guard narrowed, and no record is left. The union, not the
|
|
763
|
+
# pre-guard binding alone, because the code may leave the value as it was: `$stdout.is_a?(StringIO)`, then a
|
|
764
|
+
# helper, reads `IO | StringIO`, which keeps `$stdout.string` quiet as the guard intended.
|
|
765
|
+
def forget_guard_narrowings
|
|
766
|
+
return self if @guard_records.empty?
|
|
767
|
+
|
|
768
|
+
globals = @globals
|
|
769
|
+
constants = @constant_narrowings
|
|
770
|
+
@guard_records.each do |(kind, name), pre_guard|
|
|
771
|
+
if kind == :global
|
|
772
|
+
current = globals[name]
|
|
773
|
+
globals = globals.merge(name => Type::Combinator.union(pre_guard, current)) if current
|
|
774
|
+
else
|
|
775
|
+
current = constants[name]
|
|
776
|
+
constants = constants.merge(name => Type::Combinator.union(pre_guard, current)) if current
|
|
777
|
+
end
|
|
778
|
+
end
|
|
779
|
+
rebuild(globals: globals.frozen? ? globals : globals.freeze,
|
|
780
|
+
constant_narrowings: constants.frozen? ? constants : constants.freeze,
|
|
781
|
+
guard_records: EMPTY_GUARD_RECORDS)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# Issue #1362 (ADR-58 parity, ADR-117 Decision point 2) — used by the method-entry and top-level seeds to bind a
|
|
785
|
+
# global Ruby's own signatures declare to its declared type joined with the file's writes, and to record that the
|
|
786
|
+
# binding is still that seed. The declared members are real type information but not diagnostic fuel
|
|
787
|
+
# ({Analysis::CheckRules::DeclarationSourcedGuard}); a write or narrowing drops the mark ({#with_global}).
|
|
788
|
+
def seed_declaration_sourced_global(name, type)
|
|
789
|
+
rebuild(globals: @globals.merge(name.to_sym => type).freeze,
|
|
790
|
+
declaration_sourced: add_declaration_sourced(:global, name))
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# Issue #1362 — a local written from a read of a marked global (`sep = $/`) carries ADR-58's `:local` mark, and
|
|
794
|
+
# this records the globals it may copy, so a consumer can compare against the file's writes to them
|
|
795
|
+
# ({#declaration_sourced_global_copies}). Applied after {#with_declaration_sourced_local}; the local's next
|
|
796
|
+
# rebinding drops both ({#bind_local}), and a join of two copies keeps both branches' globals.
|
|
797
|
+
def with_global_copy_marks(name, globals)
|
|
798
|
+
name = name.to_sym
|
|
799
|
+
added = globals.map { |global| [:global_copy, name, global.to_sym].freeze }
|
|
800
|
+
.reject { |ref| @declaration_sourced.include?(ref) }
|
|
801
|
+
return self if added.empty?
|
|
802
|
+
|
|
803
|
+
rebuild(declaration_sourced: @declaration_sourced.dup.merge(added).freeze)
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
# Issue #1362 — this scope with the local `name`'s ADR-58 mark and copy record dropped and its binding kept: a
|
|
807
|
+
# retried pass that re-enters with a binding the accumulated one accepts, but from another source.
|
|
808
|
+
def without_local_declaration_marks(name)
|
|
809
|
+
dropped = drop_local_declaration_marks(name)
|
|
810
|
+
dropped.equal?(@declaration_sourced) ? self : rebuild(declaration_sourced: dropped)
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
# The globals the local `name` is a marked copy of ({#with_global_copy_marks}), empty for a local that copies
|
|
814
|
+
# no marked global.
|
|
815
|
+
def declaration_sourced_global_copies(name)
|
|
816
|
+
return EMPTY_GLOBAL_COPIES unless declaration_sourced?(:local, name)
|
|
817
|
+
|
|
818
|
+
name = name.to_sym
|
|
819
|
+
@declaration_sourced.filter_map { |ref| ref[2] if ref[0] == :global_copy && ref[1] == name }
|
|
820
|
+
end
|
|
821
|
+
EMPTY_GLOBAL_COPIES = [].freeze
|
|
822
|
+
private_constant :EMPTY_GLOBAL_COPIES
|
|
823
|
+
|
|
824
|
+
# Mark `nodes`, index `||=` sites, as ones whose slot an earlier run of a repeating block body may have
|
|
825
|
+
# filled ({EMPTY_REPEATED_OR_WRITES}).
|
|
826
|
+
def with_repeated_or_writes(nodes)
|
|
827
|
+
return self if nodes.all? { |node| @repeated_or_writes.key?(node) }
|
|
828
|
+
|
|
829
|
+
marked = @repeated_or_writes.dup
|
|
830
|
+
nodes.each { |node| marked[node] = true }
|
|
831
|
+
rebuild(repeated_or_writes: marked.freeze)
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
# True when {#with_repeated_or_writes} marked the index `||=` node `node` (by identity).
|
|
835
|
+
def repeated_or_write?(node)
|
|
836
|
+
return false if @repeated_or_writes.empty?
|
|
837
|
+
|
|
838
|
+
@repeated_or_writes.key?(node)
|
|
496
839
|
end
|
|
497
840
|
|
|
498
841
|
# Regex match-data globals (`$~`, `$&`, `$1..$9`, the pre/post-match and last-paren back-references). Narrowed
|
|
499
|
-
# on a successful-`=~` / `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`)
|
|
500
|
-
#
|
|
501
|
-
#
|
|
502
|
-
#
|
|
842
|
+
# on a successful-`=~` / `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`). They live in
|
|
843
|
+
# the method frame's special-variable slot, which the method's blocks and closures share (issue #1358), so a
|
|
844
|
+
# later call that may run a match in this frame — a match-capable call, a block that may match, a call once
|
|
845
|
+
# the frame has made a closure that may — rebinds every one of them, and `eval_call` forgets the narrowed
|
|
846
|
+
# facts here. Always safe — only drops facts, so a subsequent read falls back to the default `String | nil`.
|
|
847
|
+
# Program-level `$GLOBAL = ...` seeds use other names and are untouched.
|
|
503
848
|
MATCH_DATA_GLOBALS = %i[$~ $& $` $' $+ $1 $2 $3 $4 $5 $6 $7 $8 $9].freeze
|
|
504
849
|
private_constant :MATCH_DATA_GLOBALS
|
|
505
850
|
|
|
506
851
|
def forget_match_globals
|
|
507
|
-
return self unless
|
|
852
|
+
return self unless match_globals_bound?
|
|
508
853
|
|
|
509
854
|
rebuild(globals: @globals.except(*MATCH_DATA_GLOBALS).freeze)
|
|
510
855
|
end
|
|
511
856
|
|
|
857
|
+
# Issue #1361 — this scope with every bound match-data global rebound to `Dynamic[top]`, and an unbound one left
|
|
858
|
+
# unbound: the view a `define_method` / `define_singleton_method` body enters with
|
|
859
|
+
# ({Inference::FreshFrameBlocks.entry}). The body reads the defining frame's slot whenever the method is called,
|
|
860
|
+
# which the narrowing where it is written neither proves nor refutes, so it is neither narrowed nor flagged.
|
|
861
|
+
def untyped_match_globals
|
|
862
|
+
return self unless match_globals_bound?
|
|
863
|
+
|
|
864
|
+
untyped = Type::Combinator.untyped
|
|
865
|
+
rebound = MATCH_DATA_GLOBALS.each_with_object({}) { |name, acc| acc[name] = untyped if @globals.key?(name) }
|
|
866
|
+
rebuild(globals: @globals.merge(rebound).freeze)
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
# True when any match-data global holds a binding, which a match edge or {#untyped_match_globals} makes: the only
|
|
870
|
+
# state {#forget_match_globals} can drop, and so the gate on every scan that decides whether to.
|
|
871
|
+
def match_globals_bound?
|
|
872
|
+
!@globals.empty? && MATCH_DATA_GLOBALS.any? { |name| @globals.key?(name) }
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
# Issue #1359 — the last line read, `$_`, lives in the same frame slot as the match globals, and so on the same
|
|
876
|
+
# terms: a `gets`-family call, a write, or a block or closure of the frame that may run either rebinds it
|
|
877
|
+
# ({Inference::LastLine}). It is bound only where a condition on a reader narrows it, or where code writes it.
|
|
878
|
+
LAST_LINE = :$_
|
|
879
|
+
private_constant :LAST_LINE
|
|
880
|
+
|
|
881
|
+
def forget_last_line
|
|
882
|
+
return self unless last_line_bound?
|
|
883
|
+
|
|
884
|
+
rebuild(globals: @globals.except(LAST_LINE).freeze)
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
# The `$_` half of {#untyped_match_globals}: a bound `$_` rebound to `Dynamic[top]`, an unbound one left alone.
|
|
888
|
+
def untyped_last_line
|
|
889
|
+
return self unless last_line_bound?
|
|
890
|
+
|
|
891
|
+
rebuild(globals: @globals.merge(LAST_LINE => Type::Combinator.untyped).freeze)
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
# The gate on every scan that decides whether to forget `$_`, as {#match_globals_bound?} is for the match globals.
|
|
895
|
+
def last_line_bound?
|
|
896
|
+
!@globals.empty? && @globals.key?(LAST_LINE)
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
# Issue #1360 — the exception being rescued, `$!`, and its backtrace, `$@`. Ruby finds them through the nearest
|
|
900
|
+
# rescue frame of the running execution context, not through the method frame, so they are bound only inside a
|
|
901
|
+
# `rescue` clause or a rescue modifier's fallback and read their earlier binding again once it exits
|
|
902
|
+
# ({Inference::ErrorInfo}). `$?`, the status of the last child process, is thread-local, and is bound after a
|
|
903
|
+
# subprocess call ({Inference::LastStatus}).
|
|
904
|
+
ERROR_INFO_GLOBALS = %i[$! $@].freeze
|
|
905
|
+
LAST_STATUS_GLOBALS = %i[$?].freeze
|
|
906
|
+
private_constant :ERROR_INFO_GLOBALS, :LAST_STATUS_GLOBALS
|
|
907
|
+
|
|
908
|
+
# This scope with `$!` and `$@` unbound: the view of a body that runs outside the rescue clause it is written in.
|
|
909
|
+
def forget_error_info = forget_globals(ERROR_INFO_GLOBALS)
|
|
910
|
+
|
|
911
|
+
# This scope with a bound `$!` or `$@` rebound to `Dynamic[top]`, and an unbound one left unbound, as
|
|
912
|
+
# {#untyped_match_globals} gives a `define_method` body.
|
|
913
|
+
def untyped_error_info = untyped_globals(ERROR_INFO_GLOBALS)
|
|
914
|
+
|
|
915
|
+
# This scope with `$?` unbound: the view of a body that may run on another thread.
|
|
916
|
+
def forget_last_status = forget_globals(LAST_STATUS_GLOBALS)
|
|
917
|
+
|
|
918
|
+
# The `$?` half of {#untyped_error_info}.
|
|
919
|
+
def untyped_last_status = untyped_globals(LAST_STATUS_GLOBALS)
|
|
920
|
+
|
|
921
|
+
def forget_globals(names)
|
|
922
|
+
return self if @globals.empty? || names.none? { |name| @globals.key?(name) }
|
|
923
|
+
|
|
924
|
+
rebuild(globals: @globals.except(*names).freeze)
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
def untyped_globals(names)
|
|
928
|
+
return self if @globals.empty? || names.none? { |name| @globals.key?(name) }
|
|
929
|
+
|
|
930
|
+
untyped = Type::Combinator.untyped
|
|
931
|
+
rebound = names.each_with_object({}) { |name, acc| acc[name] = untyped if @globals.key?(name) }
|
|
932
|
+
rebuild(globals: @globals.merge(rebound).freeze)
|
|
933
|
+
end
|
|
934
|
+
private :forget_globals, :untyped_globals
|
|
935
|
+
|
|
936
|
+
# Issue #1358 — stamps the frame `body` runs in ({Inference::MatchRebinding::Frame}) on a method, class or
|
|
937
|
+
# file body's entry scope; a method passes its `parameters` too, whose defaults run in the same frame. Every
|
|
938
|
+
# scope derived from it, a block's included, runs in that frame.
|
|
939
|
+
def with_match_frame(body, parameters = nil)
|
|
940
|
+
rebuild(match_frame: Inference::MatchRebinding::Frame.new(body, parameters))
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
# True when this scope's frame makes a closure that may rebind its match globals whenever it is invoked
|
|
944
|
+
# ({Inference::MatchRebinding.matching_closure?}). False where no body stamped a frame.
|
|
945
|
+
def match_rebinding_closure?
|
|
946
|
+
!@match_frame.nil? && @match_frame.matching_closure?(self)
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
# True when this scope's frame makes a closure that may set its `$_` whenever it is invoked
|
|
950
|
+
# ({Inference::LastLine.closure?}). False where no body stamped a frame.
|
|
951
|
+
def last_line_closure?
|
|
952
|
+
!@match_frame.nil? && @match_frame.last_line_closure?(self)
|
|
953
|
+
end
|
|
954
|
+
|
|
512
955
|
# Slice 7 phase 2 — class-level ivar accumulator. Keyed by the qualified class name (e.g. `"Rigor::Scope"`);
|
|
513
956
|
# the value is a `Hash[Symbol, Type::t]` of every ivar that appears as a write target inside any def body of
|
|
514
957
|
# that class. `StatementEvaluator#build_method_entry_scope` seeds the method body's `ivars` map from this
|
|
@@ -699,6 +1142,86 @@ module Rigor
|
|
|
699
1142
|
site
|
|
700
1143
|
end
|
|
701
1144
|
|
|
1145
|
+
# Issue #1097 — whether `class_name`'s own singleton `def method_name` has RUN by the time `call_node`
|
|
1146
|
+
# executes. Such a def precedes every `extend` in the singleton ancestry, so once it exists it owns the
|
|
1147
|
+
# call — but `sig { ... }` written BEFORE `def self.sig` in the same body still resolves through the
|
|
1148
|
+
# already-extended module, because `def` takes effect at execution. The site table stores
|
|
1149
|
+
# `"path:line"`: ordering applies only to calls executed eagerly in the same file's class body — a
|
|
1150
|
+
# call inside a def body runs at invocation time, and a def in another file can never be ordered
|
|
1151
|
+
# against the call site, so both conservatively count as shadowing. A nil `call_node`
|
|
1152
|
+
# (position-less dispatch probes) does the same.
|
|
1153
|
+
def singleton_def_shadows_call?(class_name, method_name, call_node)
|
|
1154
|
+
def_shadows_call?(user_singleton_def_site_for(class_name, method_name), class_name, method_name,
|
|
1155
|
+
:singleton, call_node)
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
# The instance-side twin of {#singleton_def_shadows_call?}: whether `class_name`'s `def method_name`
|
|
1159
|
+
# has run by call time — for `extend M` edges, where M's instance surface is what answers.
|
|
1160
|
+
def instance_def_shadows_call?(class_name, method_name, call_node)
|
|
1161
|
+
def_shadows_call?(user_def_site_for(class_name, method_name), class_name, method_name, :instance,
|
|
1162
|
+
call_node)
|
|
1163
|
+
end
|
|
1164
|
+
|
|
1165
|
+
# Shared ordering half of the two `*_def_shadows_call?` predicates, over this file's
|
|
1166
|
+
# `discovered_deferred_ranges` (issue #1097). `exists` — a recorded `"path:line"` site or the
|
|
1167
|
+
# method-existence table — says a def of `method_name`/`kind` on `class_name` is known at all; a
|
|
1168
|
+
# discovered def with no site (an `attr_*` sibling, a bundle seed that predates the table) counts
|
|
1169
|
+
# through `discovered_method?`. The ranges then answer the timing questions, and can supply defs
|
|
1170
|
+
# the site table missed (a `module_function`-installed `sig` never enters the singleton-def
|
|
1171
|
+
# table). A foreign-file site cannot be ordered against the call — shadowing. A nil `call_node`
|
|
1172
|
+
# (position-less dispatch probes) or a file the index never saw falls back to `exists`.
|
|
1173
|
+
def def_shadows_call?(site, class_name, method_name, kind, call_node)
|
|
1174
|
+
exists = !site.nil? || discovered_method?(class_name, method_name, kind)
|
|
1175
|
+
return exists if call_node.nil?
|
|
1176
|
+
|
|
1177
|
+
path = source_path
|
|
1178
|
+
return exists unless path
|
|
1179
|
+
|
|
1180
|
+
if site
|
|
1181
|
+
site_path, = site.rpartition(":")
|
|
1182
|
+
return true unless site_path == path
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
ranges = @discovery.discovered_deferred_ranges[path]
|
|
1186
|
+
return exists if ranges.nil?
|
|
1187
|
+
|
|
1188
|
+
deferred_ranges_shadow_call?(ranges, method_name.to_sym, kind, class_name.to_s,
|
|
1189
|
+
call_node.location, exists)
|
|
1190
|
+
end
|
|
1191
|
+
private :def_shadows_call?
|
|
1192
|
+
|
|
1193
|
+
# The range scan behind {#def_shadows_call?}. A call CONTAINED in any def / block / lambda /
|
|
1194
|
+
# `END` range is deferred — it runs at invocation time — so it is shadowed iff a matching def is
|
|
1195
|
+
# known (`exists`, or a row the site table missed). An EAGER call is shadowed iff the earliest
|
|
1196
|
+
# same-name, same-kind (`:both` matches either), same-OWNER row starts before it: owner scoping
|
|
1197
|
+
# keeps `class A`'s defs from ordering `class F`'s calls, and earliest-of keeps
|
|
1198
|
+
# `def self.sig; sig {}; def self.sig` honest where the def-node table is later-wins. With no
|
|
1199
|
+
# matching row the answer is `exists` — "a def exists but cannot be ordered" stays conservative,
|
|
1200
|
+
# "no def at all" stays permissive.
|
|
1201
|
+
def deferred_ranges_shadow_call?(ranges, method_name, kind, class_name, call_loc, exists)
|
|
1202
|
+
contained = false
|
|
1203
|
+
matching = false
|
|
1204
|
+
earliest = nil
|
|
1205
|
+
ranges.each do |(start, finish, name, def_kind, owner)|
|
|
1206
|
+
contained ||= start <= call_loc.start_offset && call_loc.end_offset <= finish
|
|
1207
|
+
next unless deferred_row_orders_call?(name, def_kind, owner, method_name, kind, class_name)
|
|
1208
|
+
|
|
1209
|
+
matching = true
|
|
1210
|
+
earliest = start if earliest.nil? || start < earliest
|
|
1211
|
+
end
|
|
1212
|
+
return exists || matching if contained
|
|
1213
|
+
|
|
1214
|
+
earliest.nil? ? exists : earliest <= call_loc.start_offset
|
|
1215
|
+
end
|
|
1216
|
+
private :deferred_ranges_shadow_call?
|
|
1217
|
+
|
|
1218
|
+
# Whether a range row can order the call: same method name, same qualified owner, and a def kind
|
|
1219
|
+
# that answers this predicate's side (`:both` — `module_function` — matches either).
|
|
1220
|
+
def deferred_row_orders_call?(name, def_kind, owner, method_name, kind, class_name)
|
|
1221
|
+
name == method_name && owner == class_name && (def_kind == kind || def_kind == :both)
|
|
1222
|
+
end
|
|
1223
|
+
private :deferred_row_orders_call?
|
|
1224
|
+
|
|
702
1225
|
# ADR-24 slice 2 — per-class table mapping a fully qualified user-class name to its superclass name AS WRITTEN
|
|
703
1226
|
# at the `class Foo < Bar` declaration (`"Bar"`, possibly a qualified `"A::B"`). Populated by `ScopeIndexer` —
|
|
704
1227
|
# per-file plus the cross-file project pre-pass — and consumed by
|
|
@@ -735,15 +1258,74 @@ module Rigor
|
|
|
735
1258
|
end
|
|
736
1259
|
|
|
737
1260
|
# ADR-24 slice 2 — per-class/module table mapping a fully qualified user class or module to the list of
|
|
738
|
-
# module names it `include`s / `prepend`s,
|
|
1261
|
+
# module names it `include`s / `prepend`s, in instance-ancestor SEARCH order: prepended modules first
|
|
1262
|
+
# (Ruby inserts them ahead of the class itself), then included modules nearest-first — `include A;
|
|
1263
|
+
# include B` searches B before A, while `include A, B` keeps `["A", "B"]` (each statement's argument
|
|
1264
|
+
# list lands as one unit, ahead of the earlier statements'). Populated by `ScopeIndexer` (per-file
|
|
739
1265
|
# plus the cross-file pre-pass) and consumed by `ExpressionTyper#resolve_user_def_through_ancestors` so an
|
|
740
|
-
# implicit-self call resolves against an included module's `def`s, not just the superclass chain.
|
|
741
|
-
# names are resolved to qualified classes at walk time.
|
|
1266
|
+
# implicit-self call resolves against an included module's `def`s, not just the superclass chain.
|
|
1267
|
+
# As-written names are resolved to qualified classes at walk time. Issue #1173 — the list was call
|
|
1268
|
+
# order until then, a divergence from Ruby's later-include-wins the order-sensitive consumers (the
|
|
1269
|
+
# BFS mixin step, the external-ancestor walk, override visibility) silently absorbed.
|
|
742
1270
|
def includes_of(class_name)
|
|
743
1271
|
record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
|
|
744
1272
|
@discovery.discovered_includes[class_name.to_s] || []
|
|
745
1273
|
end
|
|
746
1274
|
|
|
1275
|
+
# Issue #1123 — the modules `class_name` `prepend`s, in instance-ancestor SEARCH order: the nearest
|
|
1276
|
+
# prepend first, so `prepend A; prepend B` answers `["B", "A"]` and `prepend A, B` keeps `["A", "B"]`
|
|
1277
|
+
# (each Ruby statement's argument list lands as one unit, ahead of the earlier statements'). Read by
|
|
1278
|
+
# {#user_def_through_ancestors}'s prepend wedge — Ruby inserts a prepended module, and its own ancestry,
|
|
1279
|
+
# IMMEDIATELY BEFORE the class that prepends it, so these names are searched ahead of the class's own
|
|
1280
|
+
# `def`s. Empty for a class the project never prepends, which is the behaviour every class had before
|
|
1281
|
+
# `prepend` was ordered at all.
|
|
1282
|
+
#
|
|
1283
|
+
# The same names ALSO appear in {#includes_of} — that table answers "which modules does this class
|
|
1284
|
+
# carry", which is what the arity, visibility, reflection and constant-scope consumers ask, and a
|
|
1285
|
+
# prepended module does contribute instance methods. This table adds the ORDER and the KIND; it never
|
|
1286
|
+
# removes a name from the other one.
|
|
1287
|
+
#
|
|
1288
|
+
# The class dependency is recorded only on a HIT, as {#data_member_layout} does: the wedge asks this
|
|
1289
|
+
# for every class the walk visits, so recording on a miss would give a caller file an ancestry edge on
|
|
1290
|
+
# a class that prepends nothing — an edge where ADR-46 slice 4 keeps method-body reads at
|
|
1291
|
+
# symbol granularity.
|
|
1292
|
+
def prepends_of(class_name)
|
|
1293
|
+
names = @discovery.discovered_prepends[class_name.to_s]
|
|
1294
|
+
return EMPTY_MIXIN_NAMES if names.nil?
|
|
1295
|
+
|
|
1296
|
+
record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
|
|
1297
|
+
names
|
|
1298
|
+
end
|
|
1299
|
+
private :prepends_of
|
|
1300
|
+
|
|
1301
|
+
# Issue #898 — the module names `extend`ed onto `class_name`'s SINGLETON, as written, gathered up the
|
|
1302
|
+
# as-written superclass chain because a singleton class inherits its superclass's singleton class
|
|
1303
|
+
# (`class Base; extend Comparable; end; class Widget < Base; end` leaves `Widget.is_a?(Comparable)`
|
|
1304
|
+
# true). Empty for a class the project never extends, and for every scope that saw no seeding pass.
|
|
1305
|
+
#
|
|
1306
|
+
# This is EVIDENCE FOR a singleton ancestor, never against one: the walk sees only what a constant
|
|
1307
|
+
# argument to a receiverless `extend` (or, since #915, a receiverless `include` / `prepend` inside a
|
|
1308
|
+
# `class << self` body) written inside a declaration body spells, so a runtime `Widget.extend(m)` and an
|
|
1309
|
+
# `extend` in a file outside the analysed set are both absent from it, as is an `extend` declared only
|
|
1310
|
+
# in RBS — that one reaches the same consumer through `Environment#singleton_extended_modules` instead.
|
|
1311
|
+
# Its one consumer ({Inference::Narrowing.narrow_class}) therefore reads it
|
|
1312
|
+
# only to WITHHOLD a `Bot`, and never to assert that a guard matches.
|
|
1313
|
+
def singleton_extends_of(class_name)
|
|
1314
|
+
table = @discovery.discovered_extends
|
|
1315
|
+
return [] if table.empty?
|
|
1316
|
+
|
|
1317
|
+
names = []
|
|
1318
|
+
current = class_name.to_s
|
|
1319
|
+
seen = {}
|
|
1320
|
+
while current && !seen[current] && seen.size <= ANCESTOR_WALK_LIMIT
|
|
1321
|
+
seen[current] = true
|
|
1322
|
+
record_class_dependency(current) if Analysis::DependencyRecorder.active?
|
|
1323
|
+
names.concat(table[current] || [])
|
|
1324
|
+
current = @discovery.discovered_superclasses[current]
|
|
1325
|
+
end
|
|
1326
|
+
names.uniq
|
|
1327
|
+
end
|
|
1328
|
+
|
|
747
1329
|
# ADR-24 slice 2 — the user-side ancestor walk: resolves `method_name` against `class_name`'s own `def`s,
|
|
748
1330
|
# then breadth-first through its included / prepended modules (mixins first, as Ruby orders them) and its
|
|
749
1331
|
# superclass chain. Returns `[def_node, owner]`, or `[nil, nil]` when nothing in the project defines it.
|
|
@@ -760,6 +1342,15 @@ module Rigor
|
|
|
760
1342
|
# `name_memo` is a CACHE, not semantics: the per-edge as-written-name resolutions, so a caller resolving
|
|
761
1343
|
# many methods against one class pays each ancestor edge once. Omit it and the walk is identical, just
|
|
762
1344
|
# uncached.
|
|
1345
|
+
#
|
|
1346
|
+
# Issue #1123 — a prepended module is searched through {#prepends_of} AHEAD of the class's own `def`s
|
|
1347
|
+
# ({#prepend_wedge_names}), because Ruby inserts it, and its own ancestry, immediately before the class
|
|
1348
|
+
# that prepends it: `class Base; def speak = "base"; end; Base.prepend(Loud)` answers `Loud#speak`, and
|
|
1349
|
+
# the `super` in that body then reaches `Base#speak`. An included module still
|
|
1350
|
+
# sits AFTER the class in this walk, but — since #1173 — `includes_of` feeds the mixin step in
|
|
1351
|
+
# instance-ancestor search order, so `include A; include B` searches B first the way CRuby does. The
|
|
1352
|
+
# wedge is searched at EVERY node, not just the entry class: a prepend on an ancestor (`class Sub <
|
|
1353
|
+
# Base` where `Base` prepends a module) wins for the subclass too, exactly as Ruby dispatches it.
|
|
763
1354
|
def user_def_through_ancestors(class_name, method_name, name_memo: {})
|
|
764
1355
|
queue = [class_name.to_s]
|
|
765
1356
|
seen = {}
|
|
@@ -772,6 +1363,9 @@ module Rigor
|
|
|
772
1363
|
visited += 1
|
|
773
1364
|
return ancestor_walk_gave_up if visited > ANCESTOR_WALK_LIMIT
|
|
774
1365
|
|
|
1366
|
+
wedge_hit = search_prepend_wedge(current, method_name, name_memo, seen)
|
|
1367
|
+
return wedge_hit if wedge_hit
|
|
1368
|
+
|
|
775
1369
|
found = user_def_for(current, method_name)
|
|
776
1370
|
return [found, current] if found
|
|
777
1371
|
|
|
@@ -780,6 +1374,63 @@ module Rigor
|
|
|
780
1374
|
[nil, nil]
|
|
781
1375
|
end
|
|
782
1376
|
|
|
1377
|
+
# Issue #1123 — the `[def_node, owner]` pair a class's prepend wedge answers `method_name` with, or nil
|
|
1378
|
+
# when nothing in the wedge defines it and the walk should ask the class's own `def`s next.
|
|
1379
|
+
#
|
|
1380
|
+
# Every name the wedge takes is marked on `seen`, the SAME set the walk's breadth-first step uses: a
|
|
1381
|
+
# prepended module also sits in `includes_of`, so the mixin step would otherwise re-ask a name the wedge
|
|
1382
|
+
# already answered — and, more importantly, marking it there keeps a `prepend` graph that loops back on
|
|
1383
|
+
# the walk (a prepended module reached again from somewhere else) from being expanded twice.
|
|
1384
|
+
def search_prepend_wedge(class_name, method_name, name_memo, seen)
|
|
1385
|
+
prepend_wedge_names(class_name, name_memo).each do |name|
|
|
1386
|
+
next if seen[name]
|
|
1387
|
+
|
|
1388
|
+
seen[name] = true
|
|
1389
|
+
found = user_def_for(name, method_name)
|
|
1390
|
+
return [found, name] if found
|
|
1391
|
+
end
|
|
1392
|
+
nil
|
|
1393
|
+
end
|
|
1394
|
+
|
|
1395
|
+
# Issue #1123 — the qualified names the instance MRO searches ahead of `class_name`'s own definitions,
|
|
1396
|
+
# in the order it searches them: for each of `class_name`'s prepends, nearest first, that module's own
|
|
1397
|
+
# prepend wedge, then the module itself, then its includes and superclass.
|
|
1398
|
+
#
|
|
1399
|
+
# A prepended module's own ancestry is part of the wedge because Ruby inserts the whole sub-chain with
|
|
1400
|
+
# it — `module Loud; include Loud::Prefix; end` prepended into `Base` puts `Loud::Prefix` ahead of
|
|
1401
|
+
# `Base`, so the wedge has to reach it. `seen` is shared across one wedge computation and guards a cyclic
|
|
1402
|
+
# `prepend`/`include` graph (and the overlap with `includes_of`, which carries the prepend names too);
|
|
1403
|
+
# the walk's own set is applied by {#search_prepend_wedge}, and each name is expanded at most once.
|
|
1404
|
+
def prepend_wedge_names(class_name, name_memo, seen = {})
|
|
1405
|
+
out = []
|
|
1406
|
+
prepends_of(class_name).each do |raw|
|
|
1407
|
+
resolved = resolve_ancestor_class_name(class_name, raw, name_memo)
|
|
1408
|
+
next if resolved.nil? || seen[resolved]
|
|
1409
|
+
|
|
1410
|
+
seen[resolved] = true
|
|
1411
|
+
out.concat(subchain_names(resolved, name_memo, seen))
|
|
1412
|
+
end
|
|
1413
|
+
out
|
|
1414
|
+
end
|
|
1415
|
+
|
|
1416
|
+
# Issue #1123 — appends `node`'s own instance search order to `out`: its prepend wedge, the node
|
|
1417
|
+
# itself, then its includes and superclass, each expanded the same way. Since #1173 the include
|
|
1418
|
+
# half is `includes_of`'s search order — the ORDER inside a prepended module's own include list is
|
|
1419
|
+
# not re-derived here, only read.
|
|
1420
|
+
def subchain_names(node, name_memo, seen)
|
|
1421
|
+
out = prepend_wedge_names(node, name_memo, seen)
|
|
1422
|
+
out << node
|
|
1423
|
+
(includes_of(node) + [superclass_of(node)].compact).each do |raw|
|
|
1424
|
+
resolved = resolve_ancestor_class_name(node, raw, name_memo)
|
|
1425
|
+
next if resolved.nil? || seen[resolved]
|
|
1426
|
+
|
|
1427
|
+
seen[resolved] = true
|
|
1428
|
+
out.concat(subchain_names(resolved, name_memo, seen))
|
|
1429
|
+
end
|
|
1430
|
+
out
|
|
1431
|
+
end
|
|
1432
|
+
private :search_prepend_wedge, :prepend_wedge_names, :subchain_names
|
|
1433
|
+
|
|
783
1434
|
# Issue #731 — the singleton-side twin of {#user_def_through_ancestors}: resolves `method_name` against
|
|
784
1435
|
# `class_name`'s own `def self.` / `class << self` bodies, then up the SUPERCLASS chain. Returns
|
|
785
1436
|
# `[def_node, owner]`, or `[nil, nil]`.
|
|
@@ -810,9 +1461,78 @@ module Rigor
|
|
|
810
1461
|
[nil, nil]
|
|
811
1462
|
end
|
|
812
1463
|
|
|
1464
|
+
# Issue #633 — the ancestors a project class reaches that the project itself does NOT declare: the
|
|
1465
|
+
# `< StandardError` / `< Array` superclasses and the `include Comparable` mixins the two walks above
|
|
1466
|
+
# deliberately drop, gathered depth-first over the project ancestry (an included module's own
|
|
1467
|
+
# sub-chain precedes the includer's superclass edge) so an inherited edge counts too — and, since
|
|
1468
|
+
# #1173, so the groups arrive in instance-ancestor search order for the caller that adopts the
|
|
1469
|
+
# FIRST answering one.
|
|
1470
|
+
#
|
|
1471
|
+
# Each entry is the CANDIDATE LIST for one as-written name ({#ancestor_name_candidates}: the nesting
|
|
1472
|
+
# spellings first, the bare name last), not a resolved class — resolving it means asking the RBS
|
|
1473
|
+
# environment, which this frozen-index walk does not read. The caller takes the first candidate its own
|
|
1474
|
+
# oracle knows and ignores the rest, the same "most-qualified first" order the project-side resolution
|
|
1475
|
+
# uses.
|
|
1476
|
+
# Issue #527 slice 1 — `mixins: false` follows the SUPERCLASS chain only. Ruby reaches an included
|
|
1477
|
+
# module's methods too, so the default answers the whole ancestry; a consumer resolving one KIND of
|
|
1478
|
+
# inheritance edge at a time (the dispatch arm lands `< Hash` before `include Enumerable`) narrows
|
|
1479
|
+
# it, the same way {#singleton_def_through_ancestors} narrows {#enqueue_ancestors}.
|
|
1480
|
+
def external_ancestor_name_candidates(class_name, name_memo: {}, mixins: true)
|
|
1481
|
+
groups = []
|
|
1482
|
+
collect_external_ancestors(class_name.to_s, groups, name_memo, mixins, {}, [0])
|
|
1483
|
+
groups
|
|
1484
|
+
end
|
|
1485
|
+
|
|
1486
|
+
# One node of {#external_ancestor_name_candidates}: the project-declared ancestors continue the walk,
|
|
1487
|
+
# everything else is reported as a candidate list.
|
|
1488
|
+
#
|
|
1489
|
+
# Issue #1173 — the walk is depth-first pre-order, not breadth-first: a resolved ancestor's own
|
|
1490
|
+
# edges are followed BEFORE the next sibling edge, because Ruby inserts an included module's whole
|
|
1491
|
+
# sub-chain ahead of the includer's superclass (`class C < Hash; include M` where `M` itself
|
|
1492
|
+
# includes `Widen` chains `C → M → Widen → Hash`; BFS would report `Hash` ahead of `Widen`). Each
|
|
1493
|
+
# node's edges still go includes-then-superclass, and `includes_of` is already search order, so the
|
|
1494
|
+
# emitted groups arrive in MRO order — which the #1173 include arm depends on when two ancestors
|
|
1495
|
+
# both declare the name.
|
|
1496
|
+
def collect_external_ancestors(current, groups, name_memo, mixins, seen, visited)
|
|
1497
|
+
return if current.nil? || seen[current]
|
|
1498
|
+
|
|
1499
|
+
seen[current] = true
|
|
1500
|
+
visited[0] += 1
|
|
1501
|
+
if visited[0] > ANCESTOR_WALK_LIMIT
|
|
1502
|
+
# Issue #527 — the give-up is a budget event, not an answer. Consumers read the groups as
|
|
1503
|
+
# "the ancestors this class reaches", so a truncated list must be visible in `--stats`
|
|
1504
|
+
# alongside the other walks' exhaustions rather than silently short. Hit once: every node
|
|
1505
|
+
# visited past the limit returns here too, and only the first should count.
|
|
1506
|
+
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT) if visited[0] == ANCESTOR_WALK_LIMIT + 1
|
|
1507
|
+
return
|
|
1508
|
+
end
|
|
1509
|
+
|
|
1510
|
+
raw_names = mixins ? includes_of(current).dup : []
|
|
1511
|
+
raw_super = superclass_of(current)
|
|
1512
|
+
raw_names << raw_super if raw_super
|
|
1513
|
+
raw_names.each do |raw|
|
|
1514
|
+
resolved = resolve_ancestor_class_name(current, raw, name_memo)
|
|
1515
|
+
if resolved
|
|
1516
|
+
collect_external_ancestors(resolved, groups, name_memo, mixins, seen, visited)
|
|
1517
|
+
else
|
|
1518
|
+
groups << ancestor_name_candidates(current, raw)
|
|
1519
|
+
end
|
|
1520
|
+
end
|
|
1521
|
+
end
|
|
1522
|
+
private :collect_external_ancestors
|
|
1523
|
+
|
|
813
1524
|
# The BFS node cap; a hierarchy past it gives up rather than walking unboundedly (ADR-41 WD4).
|
|
814
1525
|
ANCESTOR_WALK_LIMIT = 100
|
|
815
1526
|
|
|
1527
|
+
EMPTY_HEADER_NESTING = [].freeze
|
|
1528
|
+
private_constant :EMPTY_HEADER_NESTING
|
|
1529
|
+
|
|
1530
|
+
# The answer for a class that prepends nothing — the common case, so the wedge does not allocate a
|
|
1531
|
+
# fresh empty list per visited node. Shared and frozen, and never handed to a caller that could
|
|
1532
|
+
# mutate it ({#prepends_of} is private to this walk).
|
|
1533
|
+
EMPTY_MIXIN_NAMES = [].freeze
|
|
1534
|
+
private_constant :EMPTY_MIXIN_NAMES
|
|
1535
|
+
|
|
816
1536
|
def ancestor_walk_gave_up
|
|
817
1537
|
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
818
1538
|
[nil, nil]
|
|
@@ -929,10 +1649,86 @@ module Rigor
|
|
|
929
1649
|
return [raw.delete_prefix("::")] if raw.start_with?("::")
|
|
930
1650
|
|
|
931
1651
|
recorded = @discovery.discovered_header_nestings[subclass_qualified.to_s]
|
|
932
|
-
entries = recorded
|
|
1652
|
+
entries = recorded ? recorded_header_nesting(recorded, raw) : peeled_header_nesting(subclass_qualified)
|
|
1653
|
+
if !entries.empty? && DiscoveryIndex.ambiguous_header_nesting?(entries)
|
|
1654
|
+
return ambiguous_ancestor_candidates(entries, raw_ancestor)
|
|
1655
|
+
end
|
|
1656
|
+
|
|
1657
|
+
entries.map { |entry| "#{entry}::#{raw_ancestor}" } << raw_ancestor.to_s
|
|
1658
|
+
end
|
|
1659
|
+
|
|
1660
|
+
# Issue #986 — the candidate list for a raw name two declaration sites of ONE class wrote in different
|
|
1661
|
+
# crefs (the compact-header rename pass landed both on this key). Each alternative chain gets its own
|
|
1662
|
+
# candidate list, in the order this walk would have used for that site alone.
|
|
1663
|
+
#
|
|
1664
|
+
# When two of them resolve to two DIFFERENT project classes there is no candidate list to return: at
|
|
1665
|
+
# runtime both `include`s run, and which of the two same-named modules ends up nearer in the MRO is the
|
|
1666
|
+
# load order of the two files, which this walk cannot know. Answering with either is a WRONG ANCESTOR,
|
|
1667
|
+
# and a wrong ancestor is a false-positive source rather than a missed one — two same-named modules can
|
|
1668
|
+
# declare the same method at different arities, and `call.wrong-arity` then fires on a correct program.
|
|
1669
|
+
# The empty list declines instead: the name resolves to no project class, the receiver's methods stay
|
|
1670
|
+
# `Dynamic`, and every rule reading this walk goes quiet.
|
|
1671
|
+
#
|
|
1672
|
+
# Where the alternatives AGREE, or only one of them resolves at all, there is nothing to adjudicate: the
|
|
1673
|
+
# answer is the union of their chains, most-qualified first — the same list a class whose sites needed
|
|
1674
|
+
# no rename gets — so an unambiguous collision is unchanged, and an EXTERNAL ancestor keeps the full
|
|
1675
|
+
# candidate list `#external_ancestor_name_candidates` hands the gem / RBS probe.
|
|
1676
|
+
def ambiguous_ancestor_candidates(alternatives, raw_ancestor)
|
|
1677
|
+
return [] if ambiguous_ancestor_resolutions_of(alternatives, raw_ancestor).size > 1
|
|
1678
|
+
|
|
1679
|
+
entries = alternatives.reduce([], :|).sort_by { |entry| [-entry.split("::").size, entry] }
|
|
933
1680
|
entries.map { |entry| "#{entry}::#{raw_ancestor}" } << raw_ancestor.to_s
|
|
934
1681
|
end
|
|
935
1682
|
|
|
1683
|
+
def ambiguous_ancestor_resolutions_of(alternatives, raw_ancestor)
|
|
1684
|
+
alternatives.filter_map do |chain|
|
|
1685
|
+
(chain.map { |entry| "#{entry}::#{raw_ancestor}" } << raw_ancestor.to_s)
|
|
1686
|
+
.find { |candidate| known_user_class?(candidate) }
|
|
1687
|
+
end.uniq
|
|
1688
|
+
end
|
|
1689
|
+
private :ambiguous_ancestor_candidates, :ambiguous_ancestor_resolutions_of
|
|
1690
|
+
|
|
1691
|
+
# Issue #986 — the several project classes an ancestor name resolves to when the compact-header rename
|
|
1692
|
+
# collision left it ambiguous, and `EMPTY_HEADER_NESTING` for every other name. {#ancestor_name_candidates}
|
|
1693
|
+
# declines such a name because no ONE class is its answer; a caller that also knows which METHOD it is
|
|
1694
|
+
# looking up can do better than that decline, and a rule that reports on a method must, because the
|
|
1695
|
+
# decline otherwise silences it for the whole receiver — its own `def`s and its unambiguous ancestors
|
|
1696
|
+
# included.
|
|
1697
|
+
#
|
|
1698
|
+
# Both of these classes are ancestors at runtime: both `include`s run, and only their MRO ORDER is the
|
|
1699
|
+
# load order this walk cannot see. So a method only one of them declares is answered by that one
|
|
1700
|
+
# whatever the order, and only a method they BOTH declare is unanswerable.
|
|
1701
|
+
# `Analysis::CheckRules::SourceArity` reads this for exactly that: it takes both as mixin levels and
|
|
1702
|
+
# declines on the disagreement its own envelope join already knows how to spot.
|
|
1703
|
+
def ambiguous_ancestor_resolutions(subclass_qualified, raw_ancestor)
|
|
1704
|
+
raw = raw_ancestor.to_s
|
|
1705
|
+
return EMPTY_HEADER_NESTING if raw.start_with?("::")
|
|
1706
|
+
|
|
1707
|
+
recorded = @discovery.discovered_header_nestings[subclass_qualified.to_s]
|
|
1708
|
+
return EMPTY_HEADER_NESTING if recorded.nil?
|
|
1709
|
+
|
|
1710
|
+
entries = recorded_header_nesting(recorded, raw)
|
|
1711
|
+
return EMPTY_HEADER_NESTING if entries.empty? || !DiscoveryIndex.ambiguous_header_nesting?(entries)
|
|
1712
|
+
|
|
1713
|
+
resolved = ambiguous_ancestor_resolutions_of(entries, raw_ancestor)
|
|
1714
|
+
resolved.size > 1 ? resolved : EMPTY_HEADER_NESTING
|
|
1715
|
+
end
|
|
1716
|
+
|
|
1717
|
+
# Issue #728 — the chain of the declaration site that WROTE `raw`, which is the cref Ruby resolves that
|
|
1718
|
+
# one name in. A class's sites need not agree: `class Foo < Base` at the top level and a rooted
|
|
1719
|
+
# `class ::Foo; include Helper; end` inside `module Outer` are two sites of `Foo`, and the union of
|
|
1720
|
+
# their chains put `Outer::Base` — a class Ruby never looks at — ahead of `::Base` for the superclass
|
|
1721
|
+
# the top-level site wrote. That is a WRONG CLASS, not a wider candidate list.
|
|
1722
|
+
#
|
|
1723
|
+
# The unkeyed entry is the union, and answers a name no site recorded under its own key: an `include`
|
|
1724
|
+
# attributed to this class from OUTSIDE its declaration (`Recv.class_eval { include M }`, which
|
|
1725
|
+
# `Inference::ScopeIndexer#walk_class_includes` names but the header walk cannot see), or a dynamic
|
|
1726
|
+
# mixin argument. It is the pre-#728 answer, so those are unchanged rather than degraded.
|
|
1727
|
+
def recorded_header_nesting(bucket, raw)
|
|
1728
|
+
bucket[raw] || bucket[DiscoveryIndex::UNKEYED_HEADER_NESTING] || EMPTY_HEADER_NESTING
|
|
1729
|
+
end
|
|
1730
|
+
private :recorded_header_nesting
|
|
1731
|
+
|
|
936
1732
|
# The pre-#682 reading of a qualified class name: every proper prefix of it, innermost first, as if the
|
|
937
1733
|
# class had been declared one `module` keyword per segment.
|
|
938
1734
|
def peeled_header_nesting(subclass_qualified)
|
|
@@ -974,6 +1770,21 @@ module Rigor
|
|
|
974
1770
|
end
|
|
975
1771
|
private :record_class_dependency
|
|
976
1772
|
|
|
1773
|
+
# Issue #639 — the EXISTENCE edge for a bare class reference, `class:<last segment>`, the hit-side twin of
|
|
1774
|
+
# the miss-side key `Inference::ExpressionTyper` already records. ADR-46 slice 1c left the existence hit
|
|
1775
|
+
# edgeless on the reasoning that a file merely referencing `Post` depends on `Post`'s methods, not on its
|
|
1776
|
+
# bare existence; that stops holding the moment the DECLARING FILE IS DELETED, because existence is then
|
|
1777
|
+
# exactly what changed and a reference-only consumer had recorded nothing to be re-checked by.
|
|
1778
|
+
#
|
|
1779
|
+
# NAME-keyed rather than a positive edge to the declaring file, which is the distinction the ADR's cost
|
|
1780
|
+
# argument turns on: the declared-class set of a file does not move when a method body in it is edited,
|
|
1781
|
+
# so a bare referent is re-checked when the class appears or disappears and at no other time. A file edge
|
|
1782
|
+
# would have re-checked every bare referent of every class the file declares on any edit to it.
|
|
1783
|
+
def record_class_existence(class_name)
|
|
1784
|
+
segment = class_name.to_s.delete_prefix("::").split("::").last
|
|
1785
|
+
Analysis::DependencyRecorder.read_name(:class, segment) if segment
|
|
1786
|
+
end
|
|
1787
|
+
|
|
977
1788
|
# Issue #644 — the positive ADR-46 edge for a cross-file VALUE constant. `Reflection.constant_type_at`
|
|
978
1789
|
# calls this the moment a candidate resolves through `in_source_constants`, so the reader depends on the
|
|
979
1790
|
# file that wrote the constant: editing the literal, or deleting the file, re-checks the reader. Recorded
|
|
@@ -990,6 +1801,17 @@ module Rigor
|
|
|
990
1801
|
sites.each { |site| Analysis::DependencyRecorder.read_site(site) }
|
|
991
1802
|
end
|
|
992
1803
|
|
|
1804
|
+
# Issue #992 — one class's `discovered_parameter_envelopes` bucket (`{[kind, method_name] => envelope}` plus
|
|
1805
|
+
# the class-wide marks), or an empty Hash. Records the ADR-46 class edge first: a verdict read off the
|
|
1806
|
+
# bucket depends on every file that declares the class, and a `memoize :f` added to one of them moves it.
|
|
1807
|
+
def parameter_envelopes_of(class_name)
|
|
1808
|
+
record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
|
|
1809
|
+
@discovery.discovered_parameter_envelopes[class_name.to_s] || EMPTY_PARAMETER_ENVELOPES
|
|
1810
|
+
end
|
|
1811
|
+
|
|
1812
|
+
EMPTY_PARAMETER_ENVELOPES = {}.freeze
|
|
1813
|
+
private_constant :EMPTY_PARAMETER_ENVELOPES
|
|
1814
|
+
|
|
993
1815
|
# v0.1.2 — per-class table mapping `method_name (Symbol) → :public | :private | :protected`. Populated by
|
|
994
1816
|
# `ScopeIndexer` for every `def` it sees inside a class body, with the visibility taken from the surrounding
|
|
995
1817
|
# `private` / `protected` / `public` modifier state plus any post-hoc `private :name, ...` named-argument
|
|
@@ -1110,10 +1932,37 @@ module Rigor
|
|
|
1110
1932
|
joined_locals = join_bindings(locals, other.locals)
|
|
1111
1933
|
joined_ivars = join_bindings(ivars, other.ivars)
|
|
1112
1934
|
joined_cvars = join_bindings(cvars, other.cvars)
|
|
1113
|
-
joined_globals = join_bindings(globals, other.globals)
|
|
1935
|
+
joined_globals = unbind_split_last_line(join_bindings(globals, other.globals), other)
|
|
1114
1936
|
build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other)
|
|
1115
1937
|
end
|
|
1116
1938
|
|
|
1939
|
+
# Issue #1429 — a join keeps a guard record while the joined scope still narrows its name: a global both arms
|
|
1940
|
+
# bind, a constant both arms narrow (one an arm does not narrow reads its resolved type after the join, so its
|
|
1941
|
+
# narrowing is gone). An arm without a record contributes nothing to the restore target, whose union with the
|
|
1942
|
+
# joined binding {#forget_guard_narrowings} takes, so it still covers that arm's binding.
|
|
1943
|
+
def join_guard_records(other, joined_globals, joined_constants)
|
|
1944
|
+
mine = @guard_records
|
|
1945
|
+
theirs = other.guard_records
|
|
1946
|
+
return EMPTY_GUARD_RECORDS if mine.empty? && theirs.empty?
|
|
1947
|
+
|
|
1948
|
+
merged = mine.merge(theirs) { |_key, left, right| Type::Combinator.union(left, right) }
|
|
1949
|
+
kept = merged.select do |(kind, name), _|
|
|
1950
|
+
kind == :global ? joined_globals.key?(name) : joined_constants.key?(name)
|
|
1951
|
+
end
|
|
1952
|
+
kept.empty? ? EMPTY_GUARD_RECORDS : kept.freeze
|
|
1953
|
+
end
|
|
1954
|
+
private :join_guard_records
|
|
1955
|
+
|
|
1956
|
+
# Issue #1359 — arms that bind `$_` apart join with it unbound rather than to their union. The arms of a reader
|
|
1957
|
+
# condition bind `String` and `nil`, and `String?` after `if gets … end` would report correct code that proves the
|
|
1958
|
+
# line some other way (`ok = gets ? true : false; return unless ok; line = $_; line.chomp`).
|
|
1959
|
+
def unbind_split_last_line(joined, other)
|
|
1960
|
+
return joined unless joined.key?(LAST_LINE) && @globals[LAST_LINE] != other.globals[LAST_LINE]
|
|
1961
|
+
|
|
1962
|
+
joined.except(LAST_LINE).freeze
|
|
1963
|
+
end
|
|
1964
|
+
private :unbind_split_last_line
|
|
1965
|
+
|
|
1117
1966
|
def ==(other)
|
|
1118
1967
|
other.is_a?(Scope) &&
|
|
1119
1968
|
environment.equal?(other.environment) &&
|
|
@@ -1125,7 +1974,7 @@ module Rigor
|
|
|
1125
1974
|
@globals == other.globals &&
|
|
1126
1975
|
@indexed_narrowings == other.indexed_narrowings &&
|
|
1127
1976
|
@method_chain_narrowings == other.method_chain_narrowings &&
|
|
1128
|
-
|
|
1977
|
+
same_marks?(other)
|
|
1129
1978
|
end
|
|
1130
1979
|
alias eql? ==
|
|
1131
1980
|
|
|
@@ -1135,6 +1984,18 @@ module Rigor
|
|
|
1135
1984
|
|
|
1136
1985
|
private
|
|
1137
1986
|
|
|
1987
|
+
# The marks {#==} compares: ADR-58's, issue #667's and the repeated `||=` sites, and #1429's guard state.
|
|
1988
|
+
def same_marks?(other)
|
|
1989
|
+
@declaration_sourced == other.declaration_sourced &&
|
|
1990
|
+
@published_constant_sourced == other.published_constant_sourced &&
|
|
1991
|
+
@repeated_or_writes == other.repeated_or_writes &&
|
|
1992
|
+
same_guard_state?(other)
|
|
1993
|
+
end
|
|
1994
|
+
|
|
1995
|
+
def same_guard_state?(other)
|
|
1996
|
+
@constant_narrowings == other.constant_narrowings && @guard_records == other.guard_records
|
|
1997
|
+
end
|
|
1998
|
+
|
|
1138
1999
|
def rebuild(
|
|
1139
2000
|
locals: @locals, fact_store: @fact_store, self_type: @self_type,
|
|
1140
2001
|
ivars: @ivars, cvars: @cvars, globals: @globals,
|
|
@@ -1142,9 +2003,11 @@ module Rigor
|
|
|
1142
2003
|
indexed_narrowings: @indexed_narrowings,
|
|
1143
2004
|
method_chain_narrowings: @method_chain_narrowings,
|
|
1144
2005
|
declaration_sourced: @declaration_sourced,
|
|
2006
|
+
published_constant_sourced: @published_constant_sourced,
|
|
1145
2007
|
source_path: @source_path,
|
|
1146
2008
|
struct_fold_safe_locals: @struct_fold_safe_locals,
|
|
1147
2009
|
opaque_block_self: @opaque_block_self,
|
|
2010
|
+
singleton_class_body: @singleton_class_body,
|
|
1148
2011
|
lexical_nesting: @lexical_nesting,
|
|
1149
2012
|
dynamic_origins: @dynamic_origins,
|
|
1150
2013
|
local_origins: @local_origins,
|
|
@@ -1153,7 +2016,11 @@ module Rigor
|
|
|
1153
2016
|
plugin_typed_calls: @plugin_typed_calls,
|
|
1154
2017
|
optimistic_origins: @optimistic_origins,
|
|
1155
2018
|
optimistic_locals: @optimistic_locals,
|
|
1156
|
-
optimistic_ivars: @optimistic_ivars
|
|
2019
|
+
optimistic_ivars: @optimistic_ivars,
|
|
2020
|
+
repeated_or_writes: @repeated_or_writes,
|
|
2021
|
+
match_frame: @match_frame,
|
|
2022
|
+
constant_narrowings: @constant_narrowings,
|
|
2023
|
+
guard_records: @guard_records
|
|
1157
2024
|
)
|
|
1158
2025
|
self.class.new(
|
|
1159
2026
|
environment: environment, locals: locals,
|
|
@@ -1163,9 +2030,11 @@ module Rigor
|
|
|
1163
2030
|
indexed_narrowings: indexed_narrowings,
|
|
1164
2031
|
method_chain_narrowings: method_chain_narrowings,
|
|
1165
2032
|
declaration_sourced: declaration_sourced,
|
|
2033
|
+
published_constant_sourced: published_constant_sourced,
|
|
1166
2034
|
source_path: source_path,
|
|
1167
2035
|
struct_fold_safe_locals: struct_fold_safe_locals,
|
|
1168
2036
|
opaque_block_self: opaque_block_self,
|
|
2037
|
+
singleton_class_body: singleton_class_body,
|
|
1169
2038
|
lexical_nesting: lexical_nesting,
|
|
1170
2039
|
dynamic_origins: dynamic_origins,
|
|
1171
2040
|
local_origins: local_origins,
|
|
@@ -1174,7 +2043,11 @@ module Rigor
|
|
|
1174
2043
|
plugin_typed_calls: plugin_typed_calls,
|
|
1175
2044
|
optimistic_origins: optimistic_origins,
|
|
1176
2045
|
optimistic_locals: optimistic_locals,
|
|
1177
|
-
optimistic_ivars: optimistic_ivars
|
|
2046
|
+
optimistic_ivars: optimistic_ivars,
|
|
2047
|
+
repeated_or_writes: repeated_or_writes,
|
|
2048
|
+
match_frame: match_frame,
|
|
2049
|
+
constant_narrowings: constant_narrowings,
|
|
2050
|
+
guard_records: guard_records
|
|
1178
2051
|
)
|
|
1179
2052
|
end
|
|
1180
2053
|
|
|
@@ -1194,7 +2067,7 @@ module Rigor
|
|
|
1194
2067
|
|
|
1195
2068
|
def build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other)
|
|
1196
2069
|
self.class.new(
|
|
1197
|
-
environment: environment,
|
|
2070
|
+
environment: @environment,
|
|
1198
2071
|
locals: joined_locals.freeze,
|
|
1199
2072
|
fact_store: fact_store.join(other.fact_store),
|
|
1200
2073
|
self_type: self_type == other.self_type ? self_type : nil,
|
|
@@ -1208,7 +2081,13 @@ module Rigor
|
|
|
1208
2081
|
# path made the binding flow-live (a method-local nil write / failed-guard narrowing), the merge is
|
|
1209
2082
|
# flow-live and `possible-nil-receiver` fires as before.
|
|
1210
2083
|
declaration_sourced: join_declaration_sourced(other),
|
|
1211
|
-
|
|
2084
|
+
# Issue #667 — UNION, the opposite of the line above, which is the first reason this mark does not
|
|
2085
|
+
# ride the ADR-58 Set. The mark only ever WITHHOLDS a firing, so keeping it when either arm bound the
|
|
2086
|
+
# name from a published constant is the false-positive-safe merge: `if c then m = MODE else m = MODE2
|
|
2087
|
+
# end; m == :x` must not warn because one arm's copy is invisible to the reader's author. ADR-67
|
|
2088
|
+
# WD6b's `:inferred_param` taint takes the same direction for the same reason.
|
|
2089
|
+
published_constant_sourced: join_published_constant_sourced(other),
|
|
2090
|
+
source_path: @source_path,
|
|
1212
2091
|
# Issue #589 — the fold-safe set MUST survive a merge. It was simply absent from this constructor
|
|
1213
2092
|
# call, so it fell back to the empty default and every `if` / `while` in a method silently revoked
|
|
1214
2093
|
# struct member folding for the whole body after it: `s = S.new("r"); while c; i += 1; end; s.raw`
|
|
@@ -1234,6 +2113,10 @@ module Rigor
|
|
|
1234
2113
|
# block entry by `entering_opaque_block` and inherited through `rebuild`), so both arms usually carry
|
|
1235
2114
|
# the same value and the `||` is that value.
|
|
1236
2115
|
opaque_block_self: @opaque_block_self || other.opaque_block_self,
|
|
2116
|
+
# Issue #963 — a body property like the two above, so both arms of an in-body merge carry the identical
|
|
2117
|
+
# value and the `||` is that value. `||` is also the safe direction on its own terms: keeping the mark
|
|
2118
|
+
# declines the `define_method` narrowing, which is the pre-#963 answer.
|
|
2119
|
+
singleton_class_body: @singleton_class_body || other.singleton_class_body,
|
|
1237
2120
|
# Issue #652 — the recorded `Module.nesting`, stamped once at body entry and threaded by `rebuild`
|
|
1238
2121
|
# exactly as the fold-safe set is. A join is a control-flow merge INSIDE one body, so both arms
|
|
1239
2122
|
# always carry the identical chain; taking this scope's is that chain. Dropping it would silently
|
|
@@ -1245,11 +2128,38 @@ module Rigor
|
|
|
1245
2128
|
void_origins: @void_origins,
|
|
1246
2129
|
plugin_typed_calls: @plugin_typed_calls,
|
|
1247
2130
|
optimistic_origins: @optimistic_origins,
|
|
1248
|
-
optimistic_locals:
|
|
1249
|
-
optimistic_ivars:
|
|
2131
|
+
optimistic_locals: join_optimistic_marks(@optimistic_locals, other.optimistic_locals),
|
|
2132
|
+
optimistic_ivars: join_optimistic_marks(@optimistic_ivars, other.optimistic_ivars),
|
|
2133
|
+
# UNION, the published-constant mark's direction: the mark only withholds the memoizing `||=`
|
|
2134
|
+
# reading, so keeping a site either arm holds is the wider answer.
|
|
2135
|
+
repeated_or_writes: join_repeated_or_writes(other),
|
|
2136
|
+
# Issue #1358 — the frame the body runs in, stamped at its entry like the nesting above, so both arms
|
|
2137
|
+
# of a merge inside one body carry the same one; `||` keeps it should either arm lack it, since dropping
|
|
2138
|
+
# it only loses the frame's resets.
|
|
2139
|
+
match_frame: @match_frame || other.match_frame,
|
|
2140
|
+
# Issue #1429 — the guard narrowings of globals and constants and their pre-guard records.
|
|
2141
|
+
**join_guard_narrowings(other, joined_globals)
|
|
1250
2142
|
)
|
|
1251
2143
|
end
|
|
1252
2144
|
|
|
2145
|
+
def join_guard_narrowings(other, joined_globals)
|
|
2146
|
+
joined_constants = join_constant_narrowings(other)
|
|
2147
|
+
{ constant_narrowings: joined_constants,
|
|
2148
|
+
guard_records: join_guard_records(other, joined_globals, joined_constants) }
|
|
2149
|
+
end
|
|
2150
|
+
|
|
2151
|
+
# Issue #1429 — a constant reference both arms narrow reads the union; one only an arm narrows reads its resolved
|
|
2152
|
+
# type again, which the other arm reads too.
|
|
2153
|
+
def join_constant_narrowings(other)
|
|
2154
|
+
mine = @constant_narrowings
|
|
2155
|
+
theirs = other.constant_narrowings
|
|
2156
|
+
return mine if mine.equal?(theirs)
|
|
2157
|
+
return EMPTY_CONSTANT_NARROWINGS if mine.empty? || theirs.empty?
|
|
2158
|
+
|
|
2159
|
+
joined = join_bindings(mine, theirs)
|
|
2160
|
+
joined.empty? ? EMPTY_CONSTANT_NARROWINGS : joined
|
|
2161
|
+
end
|
|
2162
|
+
|
|
1253
2163
|
# Issue #589 — intersect the struct fold-safe grants. Zero-alloc on the common path, where both arms
|
|
1254
2164
|
# carry the same frozen set the body scan stamped.
|
|
1255
2165
|
# A `nil` on EITHER side yields no grants. The field is keyword-defaulted to `EMPTY_FOLD_SAFE` and
|
|
@@ -1277,6 +2187,17 @@ module Rigor
|
|
|
1277
2187
|
theirs.merge(mine).freeze
|
|
1278
2188
|
end
|
|
1279
2189
|
|
|
2190
|
+
# Issue #286's mark tables join as {#join_origins} does, except for the miss answer a mark may carry (issue
|
|
2191
|
+
# #1302): a name both arms mark keeps its answer only when the arms agree on it. A name one arm marks keeps
|
|
2192
|
+
# that arm's answer, since a miss runs through that arm only.
|
|
2193
|
+
def join_optimistic_marks(mine, theirs)
|
|
2194
|
+
return mine if mine.equal?(theirs) || theirs.empty?
|
|
2195
|
+
return theirs if mine.empty?
|
|
2196
|
+
|
|
2197
|
+
theirs.merge(mine) { |_name, their_mark, my_mark| Inference::OptimisticOrigin.join_bound_marks(my_mark, their_mark) }
|
|
2198
|
+
.freeze
|
|
2199
|
+
end
|
|
2200
|
+
|
|
1280
2201
|
# ADR-82 WD1 — rebinding drops any propagated origin for the name (the new value's provenance is set
|
|
1281
2202
|
# afterward by `with_local_origin` when it is a `Dynamic` with a recorded cause). Zero-alloc when the name
|
|
1282
2203
|
# carries no origin — the overwhelming common case, so this stays off the with_local hot-path budget.
|
|
@@ -1293,6 +2214,11 @@ module Rigor
|
|
|
1293
2214
|
# value on the branch where it derives from an inferred parameter, so if EITHER branch taints it, a
|
|
1294
2215
|
# downstream use could observe the lower-bound value and firing on it is an FP (`x = param else x = 5;
|
|
1295
2216
|
# x.foo`). Intersecting would lose the taint at the merge and re-surface the false positive.
|
|
2217
|
+
#
|
|
2218
|
+
# Issue #1362 — a local both branches mark as copies of global seeds keeps its mark with the union of the globals
|
|
2219
|
+
# the two record, so a consumer compares against the writes to every one of them. A `(:local, name)` mark only
|
|
2220
|
+
# one branch backs with a record is dropped with it: that branch copies a global and the other an ivar, so no
|
|
2221
|
+
# comparison answers for the merge and it is flow-live.
|
|
1296
2222
|
def join_declaration_sourced(other)
|
|
1297
2223
|
mine = @declaration_sourced
|
|
1298
2224
|
theirs = other.declaration_sourced
|
|
@@ -1305,12 +2231,33 @@ module Rigor
|
|
|
1305
2231
|
if mine.empty? || theirs.empty?
|
|
1306
2232
|
EMPTY_DECLARATION_SOURCED
|
|
1307
2233
|
else
|
|
1308
|
-
mine.select { |ref| ref[0] != :inferred_param && theirs.include?(ref) }
|
|
2234
|
+
join_copy_records(mine.select { |ref| ref[0] != :inferred_param && theirs.include?(ref) }, mine, theirs)
|
|
1309
2235
|
end
|
|
1310
2236
|
merged = inferred.merge(intersected)
|
|
1311
2237
|
merged.empty? ? EMPTY_DECLARATION_SOURCED : merged.freeze
|
|
1312
2238
|
end
|
|
1313
2239
|
|
|
2240
|
+
# `kept`, the refs both branches carry, with the `[:global_copy, …]` records rejoined ({#join_declaration_sourced}).
|
|
2241
|
+
def join_copy_records(kept, mine, theirs)
|
|
2242
|
+
mine_copies = global_copy_records(mine)
|
|
2243
|
+
theirs_copies = global_copy_records(theirs)
|
|
2244
|
+
return kept if mine_copies.empty? && theirs_copies.empty?
|
|
2245
|
+
|
|
2246
|
+
joined = kept.reject { |ref| ref[0] == :global_copy }
|
|
2247
|
+
(mine_copies.keys | theirs_copies.keys).each do |name|
|
|
2248
|
+
if mine_copies.key?(name) && theirs_copies.key?(name) && joined.include?([:local, name])
|
|
2249
|
+
joined.concat(mine_copies[name] | theirs_copies[name])
|
|
2250
|
+
else
|
|
2251
|
+
joined.delete([:local, name])
|
|
2252
|
+
end
|
|
2253
|
+
end
|
|
2254
|
+
joined
|
|
2255
|
+
end
|
|
2256
|
+
|
|
2257
|
+
def global_copy_records(refs)
|
|
2258
|
+
refs.each_with_object({}) { |ref, acc| (acc[ref[1]] ||= []) << ref if ref[0] == :global_copy }
|
|
2259
|
+
end
|
|
2260
|
+
|
|
1314
2261
|
def indexed_key(receiver_kind, receiver_name, key)
|
|
1315
2262
|
IndexedKey.new(
|
|
1316
2263
|
receiver_kind: receiver_kind.to_sym,
|
|
@@ -1339,6 +2286,43 @@ module Rigor
|
|
|
1339
2286
|
end
|
|
1340
2287
|
|
|
1341
2288
|
# ADR-58 WD1 — set/clear the declaration-sourced provenance mark.
|
|
2289
|
+
# Issue #667 — the #667 carrier's add/drop pair. Deliberately not reusing
|
|
2290
|
+
# {#add_declaration_sourced} / {#drop_declaration_sourced_for}: the two sets are joined by opposite
|
|
2291
|
+
# policies, so sharing the storage would make every future edit to either one a decision about both.
|
|
2292
|
+
def add_published_constant_sourced(kind, name)
|
|
2293
|
+
ref = [kind.to_sym, name.to_sym].freeze
|
|
2294
|
+
return @published_constant_sourced if @published_constant_sourced.include?(ref)
|
|
2295
|
+
|
|
2296
|
+
(@published_constant_sourced.dup << ref).freeze
|
|
2297
|
+
end
|
|
2298
|
+
|
|
2299
|
+
def drop_published_constant_sourced_for(kind, name)
|
|
2300
|
+
return @published_constant_sourced if @published_constant_sourced.empty?
|
|
2301
|
+
|
|
2302
|
+
ref = [kind.to_sym, name.to_sym]
|
|
2303
|
+
return @published_constant_sourced unless @published_constant_sourced.include?(ref)
|
|
2304
|
+
|
|
2305
|
+
(@published_constant_sourced - [ref]).freeze
|
|
2306
|
+
end
|
|
2307
|
+
|
|
2308
|
+
def join_published_constant_sourced(other)
|
|
2309
|
+
mine = @published_constant_sourced
|
|
2310
|
+
theirs = other.published_constant_sourced
|
|
2311
|
+
return mine if mine.equal?(theirs) || theirs.empty?
|
|
2312
|
+
return theirs if mine.empty?
|
|
2313
|
+
|
|
2314
|
+
(mine | theirs).freeze
|
|
2315
|
+
end
|
|
2316
|
+
|
|
2317
|
+
def join_repeated_or_writes(other)
|
|
2318
|
+
mine = @repeated_or_writes
|
|
2319
|
+
theirs = other.repeated_or_writes
|
|
2320
|
+
return mine if mine.equal?(theirs) || theirs.empty?
|
|
2321
|
+
return theirs if mine.empty?
|
|
2322
|
+
|
|
2323
|
+
mine.merge(theirs).freeze
|
|
2324
|
+
end
|
|
2325
|
+
|
|
1342
2326
|
def add_declaration_sourced(kind, name)
|
|
1343
2327
|
ref = [kind.to_sym, name.to_sym]
|
|
1344
2328
|
return @declaration_sourced if @declaration_sourced.include?(ref)
|
|
@@ -1357,6 +2341,31 @@ module Rigor
|
|
|
1357
2341
|
dropped.freeze
|
|
1358
2342
|
end
|
|
1359
2343
|
|
|
2344
|
+
# The `(:local, name)` mark and, when it was there, the `[:global_copy, name, global]` records that only ever sit
|
|
2345
|
+
# beside it ({#with_global_copy_marks}).
|
|
2346
|
+
def drop_local_declaration_marks(name)
|
|
2347
|
+
dropped = drop_declaration_sourced_for(:local, name)
|
|
2348
|
+
return dropped if dropped.equal?(@declaration_sourced)
|
|
2349
|
+
|
|
2350
|
+
name = name.to_sym
|
|
2351
|
+
return dropped unless dropped.any? { |ref| ref[0] == :global_copy && ref[1] == name }
|
|
2352
|
+
|
|
2353
|
+
dropped.dup.delete_if { |ref| ref[0] == :global_copy && ref[1] == name }.freeze
|
|
2354
|
+
end
|
|
2355
|
+
|
|
2356
|
+
def add_guard_record(key, pre_guard)
|
|
2357
|
+
return @guard_records if @guard_records.key?(key)
|
|
2358
|
+
|
|
2359
|
+
@guard_records.merge(key => pre_guard).freeze
|
|
2360
|
+
end
|
|
2361
|
+
|
|
2362
|
+
def drop_guard_record(kind, name)
|
|
2363
|
+
return @guard_records if @guard_records.empty?
|
|
2364
|
+
|
|
2365
|
+
key = [kind, name]
|
|
2366
|
+
@guard_records.key?(key) ? @guard_records.except(key).freeze : @guard_records
|
|
2367
|
+
end
|
|
2368
|
+
|
|
1360
2369
|
def drop_chain_narrowings_for(receiver_kind, receiver_name)
|
|
1361
2370
|
return @method_chain_narrowings if @method_chain_narrowings.empty?
|
|
1362
2371
|
|