rigortype 0.4.0 → 0.4.2
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 +4 -3
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +6 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
- data/lib/rigor/analysis/check_rules.rb +407 -144
- data/lib/rigor/analysis/dependency_recorder.rb +215 -18
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
- data/lib/rigor/analysis/incremental_session.rb +287 -50
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +43 -9
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +371 -49
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +11 -7
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +92 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +85 -14
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +16 -10
- data/lib/rigor/cli.rb +11 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +11 -0
- data/lib/rigor/configuration.rb +52 -7
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +284 -15
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +34 -73
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +26 -3
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +351 -61
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +54 -13
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +73 -14
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +214 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +166 -22
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +2129 -354
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +149 -32
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +124 -7
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +4 -4
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +211 -46
- data/lib/rigor/reflection/constant_path.rb +33 -7
- data/lib/rigor/reflection.rb +41 -10
- data/lib/rigor/scope/discovery_index.rb +258 -3
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +1032 -0
- data/lib/rigor/scope.rb +480 -328
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +77 -21
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +59 -3
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/scope.rbs +69 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +90 -1
data/lib/rigor/scope.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require_relative "type"
|
|
4
4
|
require_relative "environment"
|
|
5
5
|
require_relative "scope/discovery_index"
|
|
6
|
+
require_relative "scope/resolution_chain"
|
|
7
|
+
require_relative "inference/definer_resolution"
|
|
8
|
+
require_relative "inference/in_effect_refinements"
|
|
6
9
|
require_relative "analysis/fact_store"
|
|
7
10
|
require_relative "analysis/dependency_recorder"
|
|
8
11
|
require_relative "inference/expression_typer"
|
|
@@ -24,12 +27,12 @@ module Rigor
|
|
|
24
27
|
:indexed_narrowings, :method_chain_narrowings,
|
|
25
28
|
:declaration_sourced, :published_constant_sourced,
|
|
26
29
|
:source_path, :discovery, :struct_fold_safe_locals,
|
|
27
|
-
:opaque_block_self, :singleton_class_body, :lexical_nesting,
|
|
30
|
+
:opaque_block_self, :block_self_unknown, :singleton_class_body, :lexical_nesting,
|
|
28
31
|
:dynamic_origins, :local_origins, :ivar_origins,
|
|
29
32
|
:void_origins, :plugin_typed_calls,
|
|
30
33
|
:optimistic_origins, :optimistic_locals, :optimistic_ivars,
|
|
31
34
|
:repeated_or_writes, :match_frame,
|
|
32
|
-
:constant_narrowings, :guard_records
|
|
35
|
+
:constant_narrowings, :guard_records, :bot_guard_classes, :declared_refinements
|
|
33
36
|
|
|
34
37
|
# ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
|
|
35
38
|
# reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
|
|
@@ -60,13 +63,30 @@ module Rigor
|
|
|
60
63
|
# Issue #1120 — `{refined class name => {method name => [refining module names]}}`: what each
|
|
61
64
|
# `refine X do … end` block defines, visible only where a `using` of its module is in effect.
|
|
62
65
|
def discovered_refinements = @discovery.discovered_refinements
|
|
66
|
+
|
|
67
|
+
# Issue #1673 (ADR-121 WD1) — the in-effect refinements at `node`: the refining-module names whose refinements
|
|
68
|
+
# Ruby applies there, ordered so a later activation comes later (the later one wins), each once at its first
|
|
69
|
+
# position, each `using`'d module expanded through its includes and prepends in CRuby's activation order.
|
|
70
|
+
# `declared` is a block source's modules, appended and expanded on the same terms; it defaults to this scope's
|
|
71
|
+
# {#declared_refinements}, a plugin-declared refined block's (#1667). An entry may be
|
|
72
|
+
# `Inference::InEffectRefinements::UNKNOWN`, which means any refinement may be in effect. A node another file
|
|
73
|
+
# wrote (a callee body typed under this file's scope) answers `declared` alone. Reads include edges, so it records
|
|
74
|
+
# the dependencies `Inference::InEffectRefinements.activated_modules` names; a consumer that asks for a method
|
|
75
|
+
# name also records `refinement:<name>` (`Inference::InEffectRefinements.refining_modules`).
|
|
76
|
+
def in_effect_refinements(node, declared = @declared_refinements)
|
|
77
|
+
Inference::InEffectRefinements.for_node(self, node, declared)
|
|
78
|
+
end
|
|
79
|
+
|
|
63
80
|
# Issue #1367 — the project's `Inference::GlobalWriteCensus`, a `Set` of frozen entries.
|
|
64
81
|
def discovered_global_write_census = @discovery.discovered_global_write_census
|
|
65
82
|
def discovered_includes = @discovery.discovered_includes
|
|
66
83
|
# Issue #1123 — `{qualified class or module name => [module names it `prepend`s, as written]}`, in
|
|
67
|
-
# instance-ancestor search order (nearest prepend first).
|
|
68
|
-
# a
|
|
69
|
-
|
|
84
|
+
# instance-ancestor search order (nearest prepend first). The one table that tells a `prepend` from an
|
|
85
|
+
# `include`: {ResolutionChain} reads it to put a prepended module ahead of the class.
|
|
86
|
+
# The plugin-facing view: each class's prepends once, nearest first. The discovery index itself keeps every
|
|
87
|
+
# statement (a repeated `prepend` is a no-op in Ruby, the first statement winning), and the resolution chain
|
|
88
|
+
# reads that raw list.
|
|
89
|
+
def discovered_prepends = @discovery.discovered_prepends.transform_values { |names| names.uniq.freeze }.freeze
|
|
70
90
|
def discovered_extends = @discovery.discovered_extends
|
|
71
91
|
def discovered_class_sources = @discovery.discovered_class_sources
|
|
72
92
|
# Issue #644 — `{qualified constant name => Set[declaring file]}`; seeded only on an ADR-46 recording run.
|
|
@@ -85,9 +105,13 @@ module Rigor
|
|
|
85
105
|
# Issue #1290 — the census names sharing `name`'s last segment that some write other than a memo `||=`
|
|
86
106
|
# assigns: the ones that shadow an outer constant of the name, so the lexical ladder stops at them
|
|
87
107
|
# (`Reflection.resolve_constant_type`). That asks once per constant reference, so a bare name is looked up
|
|
88
|
-
# as it stands rather than split
|
|
108
|
+
# as it stands rather than split, and a qualified one is sliced after its last `::` rather than split into
|
|
109
|
+
# an Array of every segment ([#1502](https://github.com/rigortype/rigor/issues/1502)). The two agree on every
|
|
110
|
+
# name a parsed constant path spells. Only the `Foo::` Prism recovers from a syntax error differs, and no
|
|
111
|
+
# file with a parse error is typed.
|
|
89
112
|
def shadowing_constant_names(name)
|
|
90
|
-
|
|
113
|
+
separator = name.rindex("::")
|
|
114
|
+
segment = separator ? name[(separator + 2)..] : name
|
|
91
115
|
@discovery.constant_shadowers[segment] || EMPTY_BOUND_CONSTANT_NAMES
|
|
92
116
|
end
|
|
93
117
|
|
|
@@ -173,7 +197,9 @@ module Rigor
|
|
|
173
197
|
# today.
|
|
174
198
|
# - `receiver_name` is the variable's Symbol.
|
|
175
199
|
# - `key` is the Ruby value of the literal index (Symbol / String / Integer). Non-literal keys
|
|
176
|
-
# (`params[field]`)
|
|
200
|
+
# (`params[field]`) have no stable address for a recorded value. A `key?` guard with a non-literal key
|
|
201
|
+
# (issue #1703) records an {Inference::KeyPresenceGuard::KeyExpr} here instead, with `receiver_kind`
|
|
202
|
+
# `:const` allowed; its entry marks presence only.
|
|
177
203
|
IndexedKey = Data.define(:receiver_kind, :receiver_name, :key)
|
|
178
204
|
|
|
179
205
|
# Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain per A1
|
|
@@ -241,12 +267,21 @@ module Rigor
|
|
|
241
267
|
# analysis cannot see runs, so a call or block that may run project or unresolved code restores each recorded
|
|
242
268
|
# binding to the union of this pre-guard type and its narrowed one ({#forget_guard_narrowings}). A write drops
|
|
243
269
|
# the record ({#with_global}), and a name the frame-local special-variable machinery owns is never recorded.
|
|
270
|
+
# Issue #1446 — a class guard's narrowing of an instance variable is recorded too, keyed `[:ivar, :@name]`
|
|
271
|
+
# ({#with_guarded_ivar}), since that code may reach `self` and rebind it; a write drops it ({#without_ivar_guard}).
|
|
244
272
|
EMPTY_GUARD_RECORDS = {}.freeze
|
|
273
|
+
# Issue #1446 — the classes a class guard narrowed a receiver to where the narrowing left it `bot`, keyed like
|
|
274
|
+
# {EMPTY_GUARD_RECORDS} with `[:local, :name]` too: `return unless @cb.is_a?(Proc)` binds `@cb` to `bot` when the
|
|
275
|
+
# guard is disjoint from its binding, but the value that passes it is a `Proc` (or a subclass of one), and a call
|
|
276
|
+
# on it dispatches on that class. `Inference::GuardRebinding` reads the class a `bot` receiver runs its methods
|
|
277
|
+
# on from here ({#bot_guard_classes_for}); a `bot` no class guard produced has no entry. A write or another
|
|
278
|
+
# narrowing of the name drops its entry, and a join keeps one only both arms hold.
|
|
279
|
+
EMPTY_BOT_GUARD_CLASSES = {}.freeze
|
|
245
280
|
private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
|
|
246
281
|
:EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED,
|
|
247
282
|
:EMPTY_FOLD_SAFE, :EMPTY_ORIGINS, :EMPTY_PUBLISHED_CONSTANT_SOURCED,
|
|
248
283
|
:EMPTY_PUBLISHED_CONSTANT_IVARS, :EMPTY_REPEATED_OR_WRITES,
|
|
249
|
-
:EMPTY_CONSTANT_NARROWINGS, :EMPTY_GUARD_RECORDS
|
|
284
|
+
:EMPTY_CONSTANT_NARROWINGS, :EMPTY_GUARD_RECORDS, :EMPTY_BOT_GUARD_CLASSES
|
|
250
285
|
|
|
251
286
|
class << self
|
|
252
287
|
def empty(environment: Environment.default, source_path: nil)
|
|
@@ -287,7 +322,7 @@ module Rigor
|
|
|
287
322
|
@plugin_typed_calls.key?(node)
|
|
288
323
|
end
|
|
289
324
|
|
|
290
|
-
def initialize(
|
|
325
|
+
def initialize( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- one keyword and assignment per field
|
|
291
326
|
environment:, locals:,
|
|
292
327
|
fact_store: Analysis::FactStore.empty,
|
|
293
328
|
self_type: nil,
|
|
@@ -302,6 +337,7 @@ module Rigor
|
|
|
302
337
|
source_path: nil,
|
|
303
338
|
struct_fold_safe_locals: EMPTY_FOLD_SAFE,
|
|
304
339
|
opaque_block_self: false,
|
|
340
|
+
block_self_unknown: false,
|
|
305
341
|
singleton_class_body: false,
|
|
306
342
|
lexical_nesting: nil,
|
|
307
343
|
dynamic_origins: {}.compare_by_identity,
|
|
@@ -315,7 +351,9 @@ module Rigor
|
|
|
315
351
|
repeated_or_writes: EMPTY_REPEATED_OR_WRITES,
|
|
316
352
|
match_frame: nil,
|
|
317
353
|
constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
|
|
318
|
-
guard_records: EMPTY_GUARD_RECORDS
|
|
354
|
+
guard_records: EMPTY_GUARD_RECORDS,
|
|
355
|
+
bot_guard_classes: EMPTY_BOT_GUARD_CLASSES,
|
|
356
|
+
declared_refinements: Inference::InEffectRefinements::EMPTY
|
|
319
357
|
)
|
|
320
358
|
@environment = environment
|
|
321
359
|
@locals = locals
|
|
@@ -332,6 +370,7 @@ module Rigor
|
|
|
332
370
|
@source_path = source_path
|
|
333
371
|
@struct_fold_safe_locals = struct_fold_safe_locals
|
|
334
372
|
@opaque_block_self = opaque_block_self
|
|
373
|
+
@block_self_unknown = block_self_unknown
|
|
335
374
|
@singleton_class_body = singleton_class_body
|
|
336
375
|
@lexical_nesting = lexical_nesting
|
|
337
376
|
@dynamic_origins = dynamic_origins
|
|
@@ -346,6 +385,8 @@ module Rigor
|
|
|
346
385
|
@match_frame = match_frame
|
|
347
386
|
@constant_narrowings = constant_narrowings
|
|
348
387
|
@guard_records = guard_records
|
|
388
|
+
@bot_guard_classes = bot_guard_classes
|
|
389
|
+
@declared_refinements = declared_refinements
|
|
349
390
|
freeze
|
|
350
391
|
end
|
|
351
392
|
|
|
@@ -358,6 +399,14 @@ module Rigor
|
|
|
358
399
|
self
|
|
359
400
|
end
|
|
360
401
|
|
|
402
|
+
# Issue #1703 — this scope with private copies of the identity-keyed side tables (dynamic, void and optimistic
|
|
403
|
+
# origins, plugin-typed calls), so a second walk from it — the `key?`-guarded re-walk — records into its own
|
|
404
|
+
# tables and never into the ones the file's analysis reads.
|
|
405
|
+
def with_isolated_side_tables
|
|
406
|
+
rebuild(dynamic_origins: @dynamic_origins.dup, void_origins: @void_origins.dup,
|
|
407
|
+
plugin_typed_calls: @plugin_typed_calls.dup, optimistic_origins: @optimistic_origins.dup)
|
|
408
|
+
end
|
|
409
|
+
|
|
361
410
|
def optimistic_local(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_locals[name.to_sym])
|
|
362
411
|
def optimistic_ivar(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_ivars[name.to_sym])
|
|
363
412
|
|
|
@@ -440,7 +489,8 @@ module Rigor
|
|
|
440
489
|
# rvalue is one.
|
|
441
490
|
published_constant_sourced: drop_published_constant_sourced_for(:local, name),
|
|
442
491
|
local_origins: drop_origin(@local_origins, name),
|
|
443
|
-
optimistic_locals: keep_marks ? @optimistic_locals : drop_origin(@optimistic_locals, name)
|
|
492
|
+
optimistic_locals: keep_marks ? @optimistic_locals : drop_origin(@optimistic_locals, name),
|
|
493
|
+
bot_guard_classes: drop_bot_guard_class(:local, name))
|
|
444
494
|
end
|
|
445
495
|
private :bind_local
|
|
446
496
|
|
|
@@ -455,6 +505,39 @@ module Rigor
|
|
|
455
505
|
rebuild(self_type: type)
|
|
456
506
|
end
|
|
457
507
|
|
|
508
|
+
# Issue #1717 — installs the `self` a block body runs with when the engine KNOWS it: an ADR-16
|
|
509
|
+
# `block_as_methods:` match, `define_method`, or a `Class.new` / `refine` body (#319). Like
|
|
510
|
+
# {#with_self_type} it leaves the #316 mark alone; it also sets {#block_self_unknown?} to `keeps_unknown`,
|
|
511
|
+
# which a caller passes from {#block_self_narrowing_unknown?}. A nil `type` narrows nothing and keeps it.
|
|
512
|
+
#
|
|
513
|
+
# Issue #1695 — instance variables belong to `self`, so a narrowing onto a DIFFERENT `self` than the enclosing
|
|
514
|
+
# scope's also drops every instance-variable binding the body would otherwise inherit ({#without_ivar_bindings}):
|
|
515
|
+
# a `define_method` block in a class body runs on an instance, and the class body's `@label = nil` is the class
|
|
516
|
+
# object's ivar, not the one the block reads. The body's reads answer `Dynamic` until it writes them. A narrowing
|
|
517
|
+
# that keeps the caller's `self` (an ADR-16 `:lexical` entry) keeps them.
|
|
518
|
+
def with_block_self_type(type, keeps_unknown: false)
|
|
519
|
+
unknown = type.nil? ? @block_self_unknown : keeps_unknown
|
|
520
|
+
return rebuild(self_type: type, block_self_unknown: unknown) if type.nil? || type == @self_type
|
|
521
|
+
|
|
522
|
+
without_ivar_bindings(type, unknown)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Issue #1717 — whether a block-self narrowing for `call_node`, asked from this (the CALLER's) scope, is
|
|
526
|
+
# built on an unknown `self`. `define_method` and an implicit- or `self`-receiver `block_as_methods:` match
|
|
527
|
+
# read the lexical `self`, which inside a block of unknown `self` is the enclosing method's, not the
|
|
528
|
+
# block's; a narrowing keyed on an explicit receiver (`Grape::API.namespace do`) does not depend on it.
|
|
529
|
+
#
|
|
530
|
+
# Issue #1759 — one narrowing is not: a `define_method` written directly in a `refine X` block
|
|
531
|
+
# ({REFINEMENT_BLOCK_SELF}) defines a method of the refinement, which runs on an instance of X, and the
|
|
532
|
+
# `Nominal[X]` it reads off the block's lexical `Singleton[X]` is that `self`.
|
|
533
|
+
def block_self_narrowing_unknown?(call_node)
|
|
534
|
+
return false unless @block_self_unknown
|
|
535
|
+
return false if @block_self_unknown == REFINEMENT_BLOCK_SELF && call_node.name == :define_method
|
|
536
|
+
|
|
537
|
+
receiver = call_node.receiver
|
|
538
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
539
|
+
end
|
|
540
|
+
|
|
458
541
|
# Issue #652 — installs the body's REAL `Module.nesting`, innermost first, as recorded at declaration
|
|
459
542
|
# time by `Inference::StatementEvaluator` (`["Admin::UsersController"]` for a compact
|
|
460
543
|
# `class Admin::UsersController`, `["Admin::UsersController", "Admin"]` for the nested spelling). A
|
|
@@ -497,15 +580,52 @@ module Rigor
|
|
|
497
580
|
# `Array#each` without knowing the callee. The flag is set at every block entry that leaves `self_type`
|
|
498
581
|
# unnarrowed and is inherited by every scope derived inside the block; it never leaks past the block,
|
|
499
582
|
# because `eval_call` returns the caller's scope unchanged.
|
|
583
|
+
#
|
|
584
|
+
# Entering also sets {#block_self_unknown?} (issue #1717), the separate mark `call.undefined-method` reads.
|
|
500
585
|
def entering_opaque_block
|
|
501
|
-
return self if @opaque_block_self
|
|
586
|
+
return self if @opaque_block_self && @block_self_unknown == true
|
|
502
587
|
|
|
503
|
-
rebuild(opaque_block_self: true)
|
|
588
|
+
rebuild(opaque_block_self: true, block_self_unknown: true)
|
|
504
589
|
end
|
|
505
590
|
|
|
506
591
|
# True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
|
|
507
592
|
def opaque_block_self? = @opaque_block_self
|
|
508
593
|
|
|
594
|
+
# Issue #1717 — true when this scope sits in a block whose `self` the engine does not know, so an explicit
|
|
595
|
+
# `self.m` there is no evidence `m` is missing: the yielding method may `instance_exec` the block on another
|
|
596
|
+
# object. Read ONLY by `call.undefined-method`'s explicit-`self` exemption, and kept apart from
|
|
597
|
+
# {#opaque_block_self?} so that mark's reach (the top-level-def bind gate) is untouched. Set with
|
|
598
|
+
# {#entering_opaque_block} on the evaluator's block entries and by the indexer's walk of an unentered block
|
|
599
|
+
# ({#with_block_self_unknown}); cleared where a block's `self` is narrowed ({#with_block_self_type}) and, on
|
|
600
|
+
# the indexer's walk, at a `def` / `class` / `module` body, which the evaluator starts from a fresh scope.
|
|
601
|
+
def block_self_unknown? = @block_self_unknown ? true : false
|
|
602
|
+
|
|
603
|
+
# Issue #1759 — the {#block_self_unknown?} value a `refine X do … end` block body is entered with
|
|
604
|
+
# (`with_block_self_type(Singleton[X], keeps_unknown: REFINEMENT_BLOCK_SELF)`). It reads as unknown everywhere
|
|
605
|
+
# `true` does; what it adds is that the body's lexical `self` is X's singleton, which a `define_method` in it
|
|
606
|
+
# reads correctly ({#block_self_narrowing_unknown?}). A block nested in the body takes plain `true`.
|
|
607
|
+
REFINEMENT_BLOCK_SELF = :refinement
|
|
608
|
+
|
|
609
|
+
# Issue #1763 — true directly in a `refine X do … end` block body ({REFINEMENT_BLOCK_SELF}). The body's
|
|
610
|
+
# `self_type` is the `Singleton[X]` its implicit-self calls and `define_method` read, but the block runs on the
|
|
611
|
+
# refinement module, so a `self` read as a value (`x = self`) types as `Dynamic[top]` there.
|
|
612
|
+
def refinement_block_self? = @block_self_unknown == REFINEMENT_BLOCK_SELF
|
|
613
|
+
|
|
614
|
+
# The join of two arms' {#block_self_unknown?} values: `true` if either has it, else either's mark.
|
|
615
|
+
def merged_block_self_unknown(other)
|
|
616
|
+
return true if @block_self_unknown == true || other == true
|
|
617
|
+
|
|
618
|
+
@block_self_unknown || other
|
|
619
|
+
end
|
|
620
|
+
private :merged_block_self_unknown
|
|
621
|
+
|
|
622
|
+
# Issue #1717 — sets {#block_self_unknown?} to `flag`, leaving every other field (the #316 mark included).
|
|
623
|
+
def with_block_self_unknown(flag)
|
|
624
|
+
return self if @block_self_unknown == flag
|
|
625
|
+
|
|
626
|
+
rebuild(block_self_unknown: flag)
|
|
627
|
+
end
|
|
628
|
+
|
|
509
629
|
# Issue #963 — marks the body of a `class << ...` as such. Inside it `self` is the SINGLETON class, which
|
|
510
630
|
# Rigor models with the same `Singleton[X]` carrier a `class X` body gets, so the carrier alone cannot say
|
|
511
631
|
# which of the two a scope came from. The distinction decides what `define_method` does: in a `class << self`
|
|
@@ -523,6 +643,19 @@ module Rigor
|
|
|
523
643
|
rebuild(singleton_class_body: flag)
|
|
524
644
|
end
|
|
525
645
|
|
|
646
|
+
# Issue #1667 (ADR-121 WD5) — this scope with `names` appended to {#declared_refinements}, each once at its first
|
|
647
|
+
# position: the modules a plugin-declared refined block (`block_as_methods:` `refinements:`) runs under. Stamped
|
|
648
|
+
# at block entry and inherited by every scope derived inside the body, nested blocks included, and by a `def` or
|
|
649
|
+
# class body written in it (`StatementEvaluator#build_fresh_body_scope`), as CRuby's cref is.
|
|
650
|
+
def with_declared_refinements(names)
|
|
651
|
+
return self if names.empty?
|
|
652
|
+
|
|
653
|
+
merged = (@declared_refinements | names).freeze
|
|
654
|
+
return self if merged == @declared_refinements
|
|
655
|
+
|
|
656
|
+
rebuild(declared_refinements: merged)
|
|
657
|
+
end
|
|
658
|
+
|
|
526
659
|
# True when this scope IS a `class << ...` body (not merely inside one lexically — a `def` reached from it
|
|
527
660
|
# answers false).
|
|
528
661
|
def singleton_class_body? = @singleton_class_body
|
|
@@ -557,6 +690,11 @@ module Rigor
|
|
|
557
690
|
end
|
|
558
691
|
|
|
559
692
|
def with_ivar(name, type)
|
|
693
|
+
bind_ivar(name, type, @guard_records)
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# The one body behind {#with_ivar} and {#with_guarded_ivar}, which differ only in the guard records they leave.
|
|
697
|
+
def bind_ivar(name, type, guard_records)
|
|
560
698
|
new_indexed_narrowings = drop_indexed_narrowings_for(:ivar, name)
|
|
561
699
|
new_chain_narrowings = drop_chain_narrowings_for(:ivar, name)
|
|
562
700
|
# ADR-58 WD1 — a method-local ivar write or narrowing is flow-live: drop any declaration-sourced mark so
|
|
@@ -568,8 +706,50 @@ module Rigor
|
|
|
568
706
|
declaration_sourced: drop_declaration_sourced_for(:ivar, name),
|
|
569
707
|
published_constant_sourced: drop_published_constant_sourced_for(:ivar, name),
|
|
570
708
|
ivar_origins: drop_origin(@ivar_origins, name),
|
|
571
|
-
optimistic_ivars: drop_origin(@optimistic_ivars, name)
|
|
709
|
+
optimistic_ivars: drop_origin(@optimistic_ivars, name),
|
|
710
|
+
guard_records: guard_records,
|
|
711
|
+
bot_guard_classes: drop_bot_guard_class(:ivar, name))
|
|
712
|
+
end
|
|
713
|
+
private :bind_ivar
|
|
714
|
+
|
|
715
|
+
# Issue #1695 — this scope with no instance-variable binding and none of the per-ivar state that rides on one:
|
|
716
|
+
# the narrowings keyed on or rooted at an ivar, its ADR-58 and #667 marks, ADR-82 origins, #286 optimistic marks
|
|
717
|
+
# and #1446 guard records, the state {#bind_ivar} drops for one name. It installs {#with_block_self_type}'s
|
|
718
|
+
# `self_type` and unknown-`self` mark in the same rebuild.
|
|
719
|
+
def without_ivar_bindings(self_type, block_self_unknown)
|
|
720
|
+
rebuild(
|
|
721
|
+
self_type: self_type,
|
|
722
|
+
block_self_unknown: block_self_unknown,
|
|
723
|
+
ivars: EMPTY_VAR_BINDINGS,
|
|
724
|
+
indexed_narrowings: reject_kept(@indexed_narrowings) { |k, _| ivar_indexed_key?(k) },
|
|
725
|
+
method_chain_narrowings: reject_kept(@method_chain_narrowings) { |k, _| k.receiver_kind == :ivar },
|
|
726
|
+
declaration_sourced: reject_kept(@declaration_sourced) { |ref| ref[0] == :ivar },
|
|
727
|
+
published_constant_sourced: reject_kept(@published_constant_sourced) { |ref| ref[0] == :ivar },
|
|
728
|
+
ivar_origins: EMPTY_ORIGINS,
|
|
729
|
+
optimistic_ivars: EMPTY_ORIGINS,
|
|
730
|
+
guard_records: reject_kept(@guard_records) { |key, _| key[0] == :ivar },
|
|
731
|
+
bot_guard_classes: reject_kept(@bot_guard_classes) { |key, _| key[0] == :ivar }
|
|
732
|
+
)
|
|
572
733
|
end
|
|
734
|
+
private :without_ivar_bindings
|
|
735
|
+
|
|
736
|
+
# `collection` without the entries the block selects, or `collection` itself when it selects none. It keeps the
|
|
737
|
+
# collection's class: `Set#reject` answers an Array, which a Set carrier's readers (`merge`, `|`, `==`) reject.
|
|
738
|
+
def reject_kept(collection, &)
|
|
739
|
+
return collection if collection.empty? || collection.none?(&)
|
|
740
|
+
|
|
741
|
+
collection.dup.delete_if(&).freeze
|
|
742
|
+
end
|
|
743
|
+
private :reject_kept
|
|
744
|
+
|
|
745
|
+
# An indexed narrowing of an ivar, or of a `key?` guard's key read from one ({#key_guard_rooted_at?}).
|
|
746
|
+
def ivar_indexed_key?(indexed_key)
|
|
747
|
+
return true if indexed_key.receiver_kind == :ivar
|
|
748
|
+
|
|
749
|
+
key = indexed_key.key
|
|
750
|
+
key.is_a?(Inference::KeyPresenceGuard::KeyExpr) && key.root[0] == :ivar
|
|
751
|
+
end
|
|
752
|
+
private :ivar_indexed_key?
|
|
573
753
|
|
|
574
754
|
# ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only provenance is the class-ivar index.
|
|
575
755
|
# Unlike `with_ivar` this binds the type AND records the declaration-sourced mark in one transition.
|
|
@@ -680,7 +860,8 @@ module Rigor
|
|
|
680
860
|
name = name.to_sym
|
|
681
861
|
written = rebuild(globals: @globals.merge(name => type).freeze,
|
|
682
862
|
declaration_sourced: drop_declaration_sourced_for(:global, name),
|
|
683
|
-
guard_records: drop_guard_record(:global, name)
|
|
863
|
+
guard_records: drop_guard_record(:global, name),
|
|
864
|
+
bot_guard_classes: drop_bot_guard_class(:global, name))
|
|
684
865
|
alias_name = STDOUT_ALIASES[name]
|
|
685
866
|
alias_name ? written.forget_guard_narrowing_of(:global, alias_name) : written
|
|
686
867
|
end
|
|
@@ -717,7 +898,28 @@ module Rigor
|
|
|
717
898
|
records = record ? add_guard_record([:global, name].freeze, pre_guard) : drop_guard_record(:global, name)
|
|
718
899
|
rebuild(globals: @globals.merge(name => type).freeze,
|
|
719
900
|
declaration_sourced: drop_declaration_sourced_for(:global, name),
|
|
720
|
-
guard_records: records
|
|
901
|
+
guard_records: records,
|
|
902
|
+
bot_guard_classes: drop_bot_guard_class(:global, name))
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
# Issue #1446 — binds the instance variable `name` to `type` on a class guard's edge, recording `pre_guard` as
|
|
906
|
+
# {#with_guarded_global} does. The truthiness, `nil?`, `&.` and `respond_to?` guards narrow an instance variable
|
|
907
|
+
# through {#with_ivar} and record nothing.
|
|
908
|
+
def with_guarded_ivar(name, type, pre_guard)
|
|
909
|
+
name = name.to_sym
|
|
910
|
+
bind_ivar(name, type, add_guard_record([:ivar, name].freeze, pre_guard))
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
# Issue #1446 — this scope with no guard record for the instance variable `name`: what a write to it leaves.
|
|
914
|
+
# {#with_ivar} keeps the record, since a narrowing binds through it too.
|
|
915
|
+
def without_ivar_guard(name)
|
|
916
|
+
records = drop_guard_record(:ivar, name.to_sym)
|
|
917
|
+
records.equal?(@guard_records) ? self : rebuild(guard_records: records)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
# Issue #1446 — true when a class guard's narrowing of the instance variable `name` is live.
|
|
921
|
+
def guard_narrowed_ivar?(name)
|
|
922
|
+
!@guard_records.empty? && @guard_records.key?([:ivar, name.to_sym])
|
|
721
923
|
end
|
|
722
924
|
|
|
723
925
|
# Issue #1429 — the type a guard narrowed the constant reference `key` to, or nil.
|
|
@@ -731,7 +933,23 @@ module Rigor
|
|
|
731
933
|
# {#with_guarded_global} does.
|
|
732
934
|
def with_constant_narrowing(key, type, pre_guard)
|
|
733
935
|
rebuild(constant_narrowings: @constant_narrowings.merge(key => type).freeze,
|
|
734
|
-
guard_records: add_guard_record([:constant, key].freeze, pre_guard)
|
|
936
|
+
guard_records: add_guard_record([:constant, key].freeze, pre_guard),
|
|
937
|
+
bot_guard_classes: drop_bot_guard_class(:constant, key))
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
# Issue #1446 — this scope with `class_names` recorded as the classes a class guard narrowed the receiver `[kind,
|
|
941
|
+
# name]` to, where the guard left it `bot` ({EMPTY_BOT_GUARD_CLASSES}). Call it after the narrowing, which drops
|
|
942
|
+
# any earlier entry.
|
|
943
|
+
def with_bot_guard_classes(kind, name, class_names)
|
|
944
|
+
key = [kind, kind == :constant ? name : name.to_sym].freeze
|
|
945
|
+
rebuild(bot_guard_classes: @bot_guard_classes.merge(key => class_names.dup.freeze).freeze)
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
# Issue #1446 — the classes a class guard narrowed the `bot` receiver `[kind, name]` to, or nil.
|
|
949
|
+
def bot_guard_classes_for(kind, name)
|
|
950
|
+
return nil if @bot_guard_classes.empty?
|
|
951
|
+
|
|
952
|
+
@bot_guard_classes[[kind, name]]
|
|
735
953
|
end
|
|
736
954
|
|
|
737
955
|
# Issue #1429 — every constant reference whose last segment is `name` with no narrowing: what a write to a constant
|
|
@@ -741,7 +959,9 @@ module Rigor
|
|
|
741
959
|
return self if keys.empty?
|
|
742
960
|
|
|
743
961
|
records = @guard_records.reject { |(kind, key), _| kind == :constant && keys.include?(key) }
|
|
744
|
-
|
|
962
|
+
classes = @bot_guard_classes.except(*keys.map { |key| [:constant, key] }).freeze
|
|
963
|
+
rebuild(constant_narrowings: @constant_narrowings.except(*keys).freeze, guard_records: records.freeze,
|
|
964
|
+
bot_guard_classes: classes)
|
|
745
965
|
end
|
|
746
966
|
|
|
747
967
|
# Issue #1429 — the constant reference `key` with no narrowing.
|
|
@@ -749,11 +969,12 @@ module Rigor
|
|
|
749
969
|
return self unless @constant_narrowings.key?(key)
|
|
750
970
|
|
|
751
971
|
rebuild(constant_narrowings: @constant_narrowings.except(key).freeze,
|
|
752
|
-
guard_records: @guard_records.except([:constant, key]).freeze
|
|
972
|
+
guard_records: @guard_records.except([:constant, key]).freeze,
|
|
973
|
+
bot_guard_classes: drop_bot_guard_class(:constant, key))
|
|
753
974
|
end
|
|
754
975
|
|
|
755
|
-
# True when a guard's narrowing of a global or
|
|
756
|
-
# so the gate on every scan that decides whether to.
|
|
976
|
+
# True when a guard's narrowing of a global, constant or instance variable is live, the state
|
|
977
|
+
# {#forget_guard_narrowings} drops and so the gate on every scan that decides whether to.
|
|
757
978
|
def guard_narrowed?
|
|
758
979
|
!@guard_records.empty?
|
|
759
980
|
end
|
|
@@ -762,23 +983,19 @@ module Rigor
|
|
|
762
983
|
# the union of its narrowed type and the binding the guard narrowed, and no record is left. The union, not the
|
|
763
984
|
# pre-guard binding alone, because the code may leave the value as it was: `$stdout.is_a?(StringIO)`, then a
|
|
764
985
|
# helper, reads `IO | StringIO`, which keeps `$stdout.string` quiet as the guard intended.
|
|
986
|
+
#
|
|
987
|
+
# Issue #1446 — an instance variable a class guard narrowed is restored the same way.
|
|
765
988
|
def forget_guard_narrowings
|
|
766
989
|
return self if @guard_records.empty?
|
|
767
990
|
|
|
768
|
-
|
|
769
|
-
constants = @constant_narrowings
|
|
991
|
+
tables = { global: @globals, constant: @constant_narrowings, ivar: @ivars }
|
|
770
992
|
@guard_records.each do |(kind, name), pre_guard|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
else
|
|
775
|
-
current = constants[name]
|
|
776
|
-
constants = constants.merge(name => Type::Combinator.union(pre_guard, current)) if current
|
|
777
|
-
end
|
|
993
|
+
table = tables.fetch(kind)
|
|
994
|
+
current = table[name]
|
|
995
|
+
tables[kind] = table.merge(name => Type::Combinator.union(pre_guard, current)) if current
|
|
778
996
|
end
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
guard_records: EMPTY_GUARD_RECORDS)
|
|
997
|
+
globals, constants, ivars = tables.values_at(:global, :constant, :ivar).map { |t| t.frozen? ? t : t.freeze }
|
|
998
|
+
rebuild(globals: globals, constant_narrowings: constants, ivars: ivars, guard_records: EMPTY_GUARD_RECORDS)
|
|
782
999
|
end
|
|
783
1000
|
|
|
784
1001
|
# Issue #1362 (ADR-58 parity, ADR-117 Decision point 2) — used by the method-entry and top-level seeds to bind a
|
|
@@ -998,6 +1215,18 @@ module Rigor
|
|
|
998
1215
|
@self_type.nil?
|
|
999
1216
|
end
|
|
1000
1217
|
|
|
1218
|
+
# ADR-119 WD1 — the RAW slot of the def tables for `class_name` and `method_name`: the instance side by default,
|
|
1219
|
+
# the singleton side for `kind: :singleton`. The slot is a live `Prism::DefNode` or a {Inference::DefHandle}
|
|
1220
|
+
# (ADR-85 WD3), returned as stored: no dependency recording and no handle resolution. ADR-119 C1d/C2 decide how
|
|
1221
|
+
# a contested slot answers here. Callers that need a node go through {#user_def_for} or {#singleton_def_for}.
|
|
1222
|
+
# The `same_slot` census reads of this slot stay justified: certainty reaches them through the declaration
|
|
1223
|
+
# signature the producer fingerprints (C1d-a), not through this accessor.
|
|
1224
|
+
def def_node_slot(class_name, method_name, kind = :instance)
|
|
1225
|
+
table = kind == :singleton ? discovered_singleton_def_nodes : discovered_def_nodes
|
|
1226
|
+
per_class = table[class_name]
|
|
1227
|
+
per_class && per_class[method_name]
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1001
1230
|
# v0.0.2 #5 — per-class table mapping `method_name (Symbol) → Prism::DefNode`. Populated by `ScopeIndexer`
|
|
1002
1231
|
# alongside `discovered_methods` for instance-side defs only (singleton-side and `define_method`-introduced
|
|
1003
1232
|
# methods do not contribute a static body the engine can re-type). Consumed by `ExpressionTyper` to do
|
|
@@ -1085,12 +1314,15 @@ module Rigor
|
|
|
1085
1314
|
same_file_top_level_def?(method_name) ? node : nil
|
|
1086
1315
|
end
|
|
1087
1316
|
|
|
1317
|
+
# ADR-119 WD1 errata, deliberately unchanged: this compares the recorded site's FILE with the call's file, and
|
|
1318
|
+
# which file a def sits in does not depend on whether it executes. The `<toplevel>` slots are contested
|
|
1319
|
+
# truthfully (ADR-119 C1d-a) but unread until C2 makes the top-level reader decline on them.
|
|
1088
1320
|
def same_file_top_level_def?(method_name)
|
|
1089
1321
|
key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
1090
1322
|
site = discovered_def_sources.dig(key, method_name.to_sym)
|
|
1091
1323
|
return true if site.nil? || @source_path.nil?
|
|
1092
1324
|
|
|
1093
|
-
File.
|
|
1325
|
+
File.absolute_path(site.sub(/:\d+\z/, "")) == File.absolute_path(@source_path)
|
|
1094
1326
|
end
|
|
1095
1327
|
private :same_file_top_level_def?
|
|
1096
1328
|
|
|
@@ -1272,32 +1504,6 @@ module Rigor
|
|
|
1272
1504
|
@discovery.discovered_includes[class_name.to_s] || []
|
|
1273
1505
|
end
|
|
1274
1506
|
|
|
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
1507
|
# Issue #898 — the module names `extend`ed onto `class_name`'s SINGLETON, as written, gathered up the
|
|
1302
1508
|
# as-written superclass chain because a singleton class inherits its superclass's singleton class
|
|
1303
1509
|
# (`class Base; extend Comparable; end; class Widget < Base; end` leaves `Widget.is_a?(Comparable)`
|
|
@@ -1326,224 +1532,145 @@ module Rigor
|
|
|
1326
1532
|
names.uniq
|
|
1327
1533
|
end
|
|
1328
1534
|
|
|
1329
|
-
# ADR-24
|
|
1330
|
-
#
|
|
1331
|
-
#
|
|
1332
|
-
#
|
|
1535
|
+
# ADR-24, amended for #1567 / #1568 / #1570 / #1571 — every reader below answers in Ruby's linearised
|
|
1536
|
+
# ancestor order, read from {ResolutionChain} (an internal class: `Scope`'s own surface is the plugin API
|
|
1537
|
+
# `spec/rigor/public_api_drift_spec.rb` pins, and the chain is not part of it). None walks `includes_of` /
|
|
1538
|
+
# `superclass_of` / the extends table itself; `spec/rigor/scope/ancestry_walker_detection_spec.rb` fails on
|
|
1539
|
+
# a method that does. Each keeps its signature and its return type exactly: only WHICH definer it answers
|
|
1540
|
+
# moved. `name_memo:` is still accepted and no longer needed — the chain memoises name resolution itself.
|
|
1541
|
+
UNUSED_NAME_MEMO = {}.freeze
|
|
1542
|
+
private_constant :UNUSED_NAME_MEMO
|
|
1333
1543
|
#
|
|
1334
|
-
#
|
|
1335
|
-
#
|
|
1336
|
-
#
|
|
1337
|
-
|
|
1338
|
-
#
|
|
1339
|
-
#
|
|
1340
|
-
#
|
|
1544
|
+
# Every reader below asks {ResolutionChain#settle} whether its chain answer stands, and where it does not
|
|
1545
|
+
# answers what the walk it replaced answered ({ResolutionChain::MasterOrder}) — the tables cannot say which
|
|
1546
|
+
# world ran, and a disagreement is no reason to answer anything new.
|
|
1547
|
+
|
|
1548
|
+
# ADR-24 slice 2 — the user-side method lookup: the first project `def` of `method_name` along
|
|
1549
|
+
# `class_name`'s instance chain, as `[def_node, owner]`, or `[nil, nil]` when nothing the project declares
|
|
1550
|
+
# defines it. An external ancestor is passed over, as before: a caller asking "does the project define
|
|
1551
|
+
# this" must not read an RBS module's declaration as absence (#1572 is the typing read that will stop
|
|
1552
|
+
# there).
|
|
1341
1553
|
#
|
|
1342
|
-
#
|
|
1343
|
-
#
|
|
1344
|
-
#
|
|
1554
|
+
# It lives HERE, not in a consumer, because it reads nothing but this scope's frozen discovery index — the
|
|
1555
|
+
# property that lets `ExpressionTyper` memoise its results run-wide. `ExpressionTyper#compute_user_def_with_owner`
|
|
1556
|
+
# wraps it in that memo for the dispatch hot path, and {Inference::MethodDispatcher::StructMaterialization}'s
|
|
1557
|
+
# `.with` guard reads the same answer (#598 review).
|
|
1345
1558
|
#
|
|
1346
|
-
#
|
|
1347
|
-
#
|
|
1348
|
-
#
|
|
1349
|
-
#
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
until queue.empty?
|
|
1359
|
-
current = queue.shift
|
|
1360
|
-
next if current.nil? || seen[current]
|
|
1361
|
-
|
|
1362
|
-
seen[current] = true
|
|
1363
|
-
visited += 1
|
|
1364
|
-
return ancestor_walk_gave_up if visited > ANCESTOR_WALK_LIMIT
|
|
1559
|
+
# #1567 — the walk was breadth-first until the chain replaced it: `class C < Base; include A` with `A`
|
|
1560
|
+
# including `M` answered `Base#foo` where Ruby calls `M#foo`. The prepend wedge #1123 added (a prepended
|
|
1561
|
+
# module searched ahead of the class's own `def`s) and the include search order #1173 fixed are both
|
|
1562
|
+
# positions on the chain now, at every class on it.
|
|
1563
|
+
def user_def_through_ancestors(class_name, method_name, name_memo: UNUSED_NAME_MEMO) # rubocop:disable Lint/UnusedMethodArgument
|
|
1564
|
+
chain = ResolutionChain.for(self, class_name.to_s, :instance, :methods)
|
|
1565
|
+
found = first_user_def(chain, method_name)
|
|
1566
|
+
owner = found&.last
|
|
1567
|
+
if chain.settle(self, owner, owner: owner) { |retro| first_user_def(retro, method_name)&.last } == :master
|
|
1568
|
+
found = master_user_def(class_name.to_s, method_name)
|
|
1569
|
+
end
|
|
1570
|
+
return found if found
|
|
1365
1571
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1572
|
+
chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
|
|
1573
|
+
end
|
|
1368
1574
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1575
|
+
def first_user_def(chain, method_name)
|
|
1576
|
+
chain.search(self) do |entry|
|
|
1577
|
+
next if entry.external?
|
|
1371
1578
|
|
|
1372
|
-
|
|
1579
|
+
node = user_def_for(entry.name, method_name)
|
|
1580
|
+
[node, entry.name] if node
|
|
1373
1581
|
end
|
|
1374
|
-
[nil, nil]
|
|
1375
1582
|
end
|
|
1376
1583
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
|
1584
|
+
def master_user_def(class_name, method_name)
|
|
1585
|
+
names = ResolutionChain::MasterOrder.definer_sequence(self, class_name)
|
|
1586
|
+
names.each { |name| record_class_dependency(name) } if Analysis::DependencyRecorder.active?
|
|
1587
|
+
names.each do |name|
|
|
1588
|
+
node = user_def_for(name, method_name)
|
|
1589
|
+
return [node, name] if node
|
|
1391
1590
|
end
|
|
1392
1591
|
nil
|
|
1393
1592
|
end
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
#
|
|
1397
|
-
#
|
|
1398
|
-
#
|
|
1399
|
-
#
|
|
1400
|
-
#
|
|
1401
|
-
# `
|
|
1402
|
-
#
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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))
|
|
1593
|
+
private :first_user_def, :master_user_def
|
|
1594
|
+
|
|
1595
|
+
# Issue #731 — the singleton-side twin of {#user_def_through_ancestors}: the first `def self.` /
|
|
1596
|
+
# `class << self` body of `method_name` along `class_name`'s singleton chain, as `[def_node, owner]`, or
|
|
1597
|
+
# `[nil, nil]` — each class object's own table, then the modules it extends in Ruby's order, then the
|
|
1598
|
+
# superclass's. `ScopeIndexer` folds an extended module's own `def`s into the extender's table, so those
|
|
1599
|
+
# answer at the extender; a module the extended one includes is its own entry (#1567's singleton shape:
|
|
1600
|
+
# `class C < Base; extend A` with `A` including `M` is `M#foo`, not `Base.foo`). Where the chain does
|
|
1601
|
+
# not stand, the answer is the class objects' alone, as the superclass-only walk this replaced gave.
|
|
1602
|
+
def singleton_def_through_ancestors(class_name, method_name, name_memo: UNUSED_NAME_MEMO) # rubocop:disable Lint/UnusedMethodArgument
|
|
1603
|
+
chain = ResolutionChain.for(self, class_name.to_s, :singleton, :methods)
|
|
1604
|
+
found = first_singleton_def(chain, method_name)
|
|
1605
|
+
owner = found&.last
|
|
1606
|
+
if chain.settle(self, owner, owner: owner) { |retro| first_singleton_def(retro, method_name)&.last } == :master
|
|
1607
|
+
found = first_singleton_def(chain, method_name, :singleton)
|
|
1429
1608
|
end
|
|
1430
|
-
|
|
1431
|
-
end
|
|
1432
|
-
private :search_prepend_wedge, :prepend_wedge_names, :subchain_names
|
|
1433
|
-
|
|
1434
|
-
# Issue #731 — the singleton-side twin of {#user_def_through_ancestors}: resolves `method_name` against
|
|
1435
|
-
# `class_name`'s own `def self.` / `class << self` bodies, then up the SUPERCLASS chain. Returns
|
|
1436
|
-
# `[def_node, owner]`, or `[nil, nil]`.
|
|
1437
|
-
#
|
|
1438
|
-
# Superclasses only, and that is the whole shape of class-method inheritance in Ruby: a subclass
|
|
1439
|
-
# inherits its parent's class methods, while an `include`d module contributes INSTANCE methods and its
|
|
1440
|
-
# own `def self.x` is not callable on the includer. `extend M` needs no walk either — `ScopeIndexer`
|
|
1441
|
-
# folds an extend into the extender's own singleton entries before the table is frozen — so the one
|
|
1442
|
-
# edge this misses is a module's singleton surface reached through some other route, which nothing
|
|
1443
|
-
# resolves today.
|
|
1444
|
-
def singleton_def_through_ancestors(class_name, method_name, name_memo: {})
|
|
1445
|
-
queue = [class_name.to_s]
|
|
1446
|
-
seen = {}
|
|
1447
|
-
visited = 0
|
|
1448
|
-
until queue.empty?
|
|
1449
|
-
current = queue.shift
|
|
1450
|
-
next if current.nil? || seen[current]
|
|
1609
|
+
return found if found
|
|
1451
1610
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
return ancestor_walk_gave_up if visited > ANCESTOR_WALK_LIMIT
|
|
1611
|
+
chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
|
|
1612
|
+
end
|
|
1455
1613
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1614
|
+
def first_singleton_def(chain, method_name, side = nil)
|
|
1615
|
+
chain.search(self, side: side) do |entry|
|
|
1616
|
+
next if entry.external?
|
|
1458
1617
|
|
|
1459
|
-
|
|
1618
|
+
name = entry.name
|
|
1619
|
+
node = entry.side == :singleton ? singleton_def_for(name, method_name) : user_def_for(name, method_name)
|
|
1620
|
+
[node, entry.name] if node
|
|
1460
1621
|
end
|
|
1461
|
-
[nil, nil]
|
|
1462
1622
|
end
|
|
1623
|
+
private :first_singleton_def
|
|
1463
1624
|
|
|
1464
1625
|
# 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
|
|
1466
|
-
#
|
|
1467
|
-
#
|
|
1468
|
-
#
|
|
1469
|
-
#
|
|
1626
|
+
# `< StandardError` / `< Array` superclasses and the `include Comparable` mixins, as the CANDIDATE LIST for
|
|
1627
|
+
# each as-written name ({#ancestor_name_candidates}: the nesting spellings first, the bare name last), in
|
|
1628
|
+
# Ruby's order along the instance chain — the order the caller that adopts the FIRST answering one (#1173)
|
|
1629
|
+
# depends on. Resolving a candidate means asking the RBS environment, which this frozen-index read does
|
|
1630
|
+
# not do; the caller takes the first candidate its own oracle knows. Where the chain does not stand, the
|
|
1631
|
+
# groups are the depth-first ones the walk this replaced emitted.
|
|
1470
1632
|
#
|
|
1471
|
-
#
|
|
1472
|
-
#
|
|
1473
|
-
#
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1633
|
+
# Issue #527 slice 1 — `mixins: false` answers the superclass edge only: a consumer resolving one KIND of
|
|
1634
|
+
# inheritance edge at a time (the dispatch arm lands `< Hash` before `include Enumerable`) narrows it, and
|
|
1635
|
+
# records only the superclass chain it read.
|
|
1636
|
+
def external_ancestor_name_candidates(class_name, name_memo: UNUSED_NAME_MEMO, mixins: true) # rubocop:disable Lint/UnusedMethodArgument
|
|
1637
|
+
chain = ResolutionChain.for(self, class_name.to_s, :instance, :methods)
|
|
1638
|
+
groups = external_groups(chain, mixins)
|
|
1639
|
+
if chain.settle(self, groups) { |retro| external_groups(retro, mixins) } == :master
|
|
1640
|
+
groups = ResolutionChain::MasterOrder.external_groups(self, class_name.to_s, mixins)
|
|
1641
|
+
end
|
|
1642
|
+
if Analysis::DependencyRecorder.active?
|
|
1643
|
+
mixins ? chain.record(self) : chain.level_classes.each { |name| record_class_dependency(name) if name }
|
|
1644
|
+
end
|
|
1645
|
+
# Issue #527 — the cut is a budget event, not an answer: consumers read the groups as "the ancestors
|
|
1646
|
+
# this class reaches", so a short list must be visible in `--stats` beside the other walks' exhaustions.
|
|
1647
|
+
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT) if chain.truncated?
|
|
1483
1648
|
groups
|
|
1484
1649
|
end
|
|
1485
1650
|
|
|
1486
|
-
|
|
1487
|
-
|
|
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
|
|
1651
|
+
def external_groups(chain, mixins)
|
|
1652
|
+
chain.entries.filter_map { |entry| entry.candidates if entry.external? && (mixins || entry.superclass_edge) }
|
|
1521
1653
|
end
|
|
1522
|
-
private :
|
|
1654
|
+
private :external_groups
|
|
1523
1655
|
|
|
1524
|
-
# The
|
|
1525
|
-
|
|
1656
|
+
# The budget {ResolutionChain::LIMIT} enforces; a hierarchy past it gives up rather than reading an answer
|
|
1657
|
+
# off a cut chain (ADR-41 WD4).
|
|
1658
|
+
ANCESTOR_WALK_LIMIT = ResolutionChain::LIMIT
|
|
1526
1659
|
|
|
1527
1660
|
EMPTY_HEADER_NESTING = [].freeze
|
|
1528
1661
|
private_constant :EMPTY_HEADER_NESTING
|
|
1529
1662
|
|
|
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
|
-
|
|
1536
1663
|
def ancestor_walk_gave_up
|
|
1537
1664
|
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
1538
1665
|
[nil, nil]
|
|
1539
1666
|
end
|
|
1667
|
+
private :ancestor_walk_gave_up
|
|
1540
1668
|
|
|
1541
|
-
# Issue #723 — the
|
|
1542
|
-
#
|
|
1543
|
-
#
|
|
1544
|
-
# `
|
|
1545
|
-
#
|
|
1546
|
-
# needs the broader one.
|
|
1669
|
+
# Issue #723 — the question {#user_def_through_ancestors} asks, asked of the DISCOVERY table rather than
|
|
1670
|
+
# the def-node table: does the project define `method_name` on `class_name` or on any ancestor the project
|
|
1671
|
+
# itself declares? The two tables are not interchangeable — `discovered_methods` also carries
|
|
1672
|
+
# `define_method`, `attr_*` and the whole singleton side, none of which contributes a `Prism::DefNode` —
|
|
1673
|
+
# and the suppression probe shared by the `call.*` check rules needs the broader one.
|
|
1547
1674
|
#
|
|
1548
1675
|
# Why it must walk at all: `Analysis::CheckRules#source_declared_method?` asked `discovered_method?`,
|
|
1549
1676
|
# which is keyed on the receiver's OWN name, while the typer resolves the same call through this
|
|
@@ -1551,72 +1678,47 @@ module Rigor
|
|
|
1551
1678
|
# `call.undefined-method` on a method `dump_type` resolved on the same line of the same run — writing
|
|
1552
1679
|
# MORE RBS made the run worse, the incentive #653 removed for plugin-typed calls.
|
|
1553
1680
|
#
|
|
1554
|
-
# `kind: :singleton`
|
|
1555
|
-
#
|
|
1556
|
-
#
|
|
1557
|
-
|
|
1558
|
-
def discovered_method_through_ancestors?(class_name, method_name, kind, name_memo: {})
|
|
1681
|
+
# `kind: :singleton` reads the singleton chain: the class objects' own methods (an extended module's are
|
|
1682
|
+
# folded into its extender's) and the instance methods of the modules they extend and those modules'
|
|
1683
|
+
# includes. Existence is a union, so the chain's two worlds, which hold the same modules, agree on it.
|
|
1684
|
+
def discovered_method_through_ancestors?(class_name, method_name, kind, name_memo: UNUSED_NAME_MEMO) # rubocop:disable Lint/UnusedMethodArgument
|
|
1559
1685
|
return false if class_name.nil?
|
|
1560
1686
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
until queue.empty?
|
|
1565
|
-
current = queue.shift
|
|
1566
|
-
next if current.nil? || seen[current]
|
|
1687
|
+
chain = ResolutionChain.for(self, class_name.to_s, kind == :singleton ? :singleton : :instance, :methods)
|
|
1688
|
+
found = chain.search(self) do |entry|
|
|
1689
|
+
next false if entry.external?
|
|
1567
1690
|
|
|
1568
|
-
|
|
1569
|
-
visited += 1
|
|
1570
|
-
# Budget exhaustion is uncertainty, not absence: answering "not declared" here would hand a
|
|
1571
|
-
# `call.undefined-method` a fired verdict it has no evidence for. Suppress and record the hit.
|
|
1572
|
-
if visited > ANCESTOR_WALK_LIMIT
|
|
1573
|
-
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
1574
|
-
return true
|
|
1575
|
-
end
|
|
1576
|
-
|
|
1577
|
-
return true if discovered_method?(current, method_name, kind)
|
|
1578
|
-
|
|
1579
|
-
enqueue_ancestors(current, queue, name_memo, mixins: kind != :singleton)
|
|
1691
|
+
discovered_method?(entry.name, method_name, entry.side == :singleton ? :singleton : kind_for(kind))
|
|
1580
1692
|
end
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
# mixins nearer than the superclass), then the superclass. Each as-written name is resolved against the
|
|
1586
|
-
# nesting `current`'s declaration header is written in ({#ancestor_name_candidates}); names that resolve
|
|
1587
|
-
# to no project class/module are dropped. `mixins: false` walks the superclass chain alone, for the
|
|
1588
|
-
# singleton-side question where an `include` contributes nothing.
|
|
1589
|
-
def enqueue_ancestors(current, queue, name_memo, mixins: true)
|
|
1590
|
-
if mixins
|
|
1591
|
-
includes_of(current).each do |raw|
|
|
1592
|
-
resolved = resolve_ancestor_class_name(current, raw, name_memo)
|
|
1593
|
-
queue.push(resolved) if resolved
|
|
1594
|
-
end
|
|
1595
|
-
end
|
|
1596
|
-
raw_super = superclass_of(current)
|
|
1597
|
-
return if raw_super.nil?
|
|
1693
|
+
return true if found
|
|
1694
|
+
# Budget exhaustion is uncertainty, not absence: answering "not declared" here would hand a
|
|
1695
|
+
# `call.undefined-method` a fired verdict it has no evidence for. Suppress and record the hit.
|
|
1696
|
+
return false unless chain.truncated?
|
|
1598
1697
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1698
|
+
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
1699
|
+
true
|
|
1601
1700
|
end
|
|
1602
1701
|
|
|
1603
|
-
#
|
|
1604
|
-
#
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
by_subclass = (name_memo[subclass_qualified] ||= {})
|
|
1608
|
-
return by_subclass[raw_ancestor] if by_subclass.key?(raw_ancestor)
|
|
1702
|
+
# A module entry on the singleton chain answers with its INSTANCE methods; on the instance chain, the
|
|
1703
|
+
# caller's kind stands.
|
|
1704
|
+
def kind_for(kind) = kind == :singleton ? :instance : kind
|
|
1705
|
+
private :kind_for
|
|
1609
1706
|
|
|
1610
|
-
|
|
1707
|
+
# Pushes `current`'s direct ancestors onto a breadth-first queue: included / prepended modules first, then
|
|
1708
|
+
# the superclass, each resolved against the nesting `current`'s declaration header is written in; names
|
|
1709
|
+
# that resolve to no project class/module are dropped. `mixins: false` gives the superclass alone. No
|
|
1710
|
+
# reader in the engine walks this way any more (they read {ResolutionChain}); it stays for the plugin
|
|
1711
|
+
# surface, with the ADR-46 class edge the walk it served always filed.
|
|
1712
|
+
def enqueue_ancestors(current, queue, name_memo, mixins: true) # rubocop:disable Lint/UnusedMethodArgument
|
|
1713
|
+
record_class_dependency(current) if Analysis::DependencyRecorder.active?
|
|
1714
|
+
queue.concat(ResolutionChain.direct_ancestors(self, current.to_s, mixins))
|
|
1611
1715
|
end
|
|
1612
|
-
private :ancestor_walk_gave_up, :resolve_ancestor_class_name
|
|
1613
1716
|
|
|
1614
1717
|
# Issue #682 — the candidate names an ancestor spelled `raw_ancestor` can denote in `subclass_qualified`,
|
|
1615
1718
|
# in Ruby's lookup order, most-qualified first and the bare name last. The single owner of the question:
|
|
1616
|
-
# three walks resolved a superclass / include name apiece (
|
|
1617
|
-
# `
|
|
1618
|
-
#
|
|
1619
|
-
# subclass's qualified name one `::` segment at a time.
|
|
1719
|
+
# three walks resolved a superclass / include name apiece (the method walk, the constant ladder's
|
|
1720
|
+
# ancestor rung, and `Analysis::CheckRules`' override-visibility rule — all three now {ResolutionChain}),
|
|
1721
|
+
# and each derived the order by PEELING the subclass's qualified name one `::` segment at a time.
|
|
1620
1722
|
#
|
|
1621
1723
|
# That peel is the NESTED spelling's answer, given to both spellings. Ruby evaluates a superclass
|
|
1622
1724
|
# expression before entering the body, so the cref that governs it is the one the declaration's HEADER
|
|
@@ -1628,9 +1730,9 @@ module Rigor
|
|
|
1628
1730
|
# The header nesting is a property of the DECLARATION, not of the reader, so it is read from the
|
|
1629
1731
|
# discovery table `Inference::ScopeIndexer` records it in rather than from this scope's own
|
|
1630
1732
|
# `#lexical_nesting`. Answering off the reader's chain would be wrong past the first hop of the ancestor
|
|
1631
|
-
#
|
|
1632
|
-
# corrupt the
|
|
1633
|
-
# `Reflection.ancestor_constant_scopes
|
|
1733
|
+
# chain (the chain describes the reader's class, not the ancestor being resolved) and would silently
|
|
1734
|
+
# corrupt the {ResolutionChain} memo and the ones layered on it (`Inference::ExpressionTyper#class_graph_buckets`,
|
|
1735
|
+
# `Reflection.ancestor_constant_scopes`), all of which key on the class name alone because this walk is
|
|
1634
1736
|
# a pure function of the frozen discovery tables.
|
|
1635
1737
|
#
|
|
1636
1738
|
# The peel survives as the FALLBACK, for a class no declaration walk recorded — a scope seeded without
|
|
@@ -1737,10 +1839,6 @@ module Rigor
|
|
|
1737
1839
|
end
|
|
1738
1840
|
private :peeled_header_nesting
|
|
1739
1841
|
|
|
1740
|
-
def compute_ancestor_class_name(subclass_qualified, raw_ancestor)
|
|
1741
|
-
ancestor_name_candidates(subclass_qualified, raw_ancestor).find { |c| known_user_class?(c) }
|
|
1742
|
-
end
|
|
1743
|
-
|
|
1744
1842
|
# Issue #723 — `discovered_methods` is in the list because the other three miss a class whose only
|
|
1745
1843
|
# project-side content is CLASS methods: `class Base; def self.build = :built; end` records no instance
|
|
1746
1844
|
# def node, no superclass and no include, so the ancestor-name resolver did not recognise `Base` as a
|
|
@@ -1755,13 +1853,13 @@ module Rigor
|
|
|
1755
1853
|
# narrower predicate (`discovered_classes` alone) would read a project class as having left the project
|
|
1756
1854
|
# and attribute its root name to a same-named locked gem — a WRONG provenance label, which is the one
|
|
1757
1855
|
# direction ADR-82 forbids.
|
|
1758
|
-
private :ancestor_walk_gave_up, :compute_ancestor_class_name
|
|
1759
1856
|
|
|
1760
1857
|
# Records, for a resolved cross-class ancestry read, every file that declares `class_name` (its declaration /
|
|
1761
|
-
# reopening / superclass / include sites). The `discovered_class_sources` table it reads is populated
|
|
1762
|
-
#
|
|
1763
|
-
#
|
|
1764
|
-
# never appear in the source map). Gated by the caller on the recorder being
|
|
1858
|
+
# reopening / superclass / include sites). The `discovered_class_sources` table it reads is populated by the
|
|
1859
|
+
# cross-file project pre-pass ({Inference::ScopeIndexer.discovered_def_index_for_paths}); a scope built
|
|
1860
|
+
# without one holds none, and the read then files nothing. No-op when the class is not a project class
|
|
1861
|
+
# (core / stdlib / gem names never appear in the source map). Gated by the caller on the recorder being
|
|
1862
|
+
# active.
|
|
1765
1863
|
def record_class_dependency(class_name)
|
|
1766
1864
|
sites = @discovery.discovered_class_sources[class_name.to_s]
|
|
1767
1865
|
return if sites.nil?
|
|
@@ -1781,8 +1879,7 @@ module Rigor
|
|
|
1781
1879
|
# so a bare referent is re-checked when the class appears or disappears and at no other time. A file edge
|
|
1782
1880
|
# would have re-checked every bare referent of every class the file declares on any edit to it.
|
|
1783
1881
|
def record_class_existence(class_name)
|
|
1784
|
-
|
|
1785
|
-
Analysis::DependencyRecorder.read_name(:class, segment) if segment
|
|
1882
|
+
Analysis::DependencyRecorder.read_last_segment(:class, class_name.to_s)
|
|
1786
1883
|
end
|
|
1787
1884
|
|
|
1788
1885
|
# Issue #644 — the positive ADR-46 edge for a cross-file VALUE constant. `Reflection.constant_type_at`
|
|
@@ -1938,17 +2035,17 @@ module Rigor
|
|
|
1938
2035
|
|
|
1939
2036
|
# Issue #1429 — a join keeps a guard record while the joined scope still narrows its name: a global both arms
|
|
1940
2037
|
# 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
|
|
1942
|
-
# joined binding {#forget_guard_narrowings} takes, so it still covers
|
|
1943
|
-
|
|
2038
|
+
# narrowing is gone), and an instance variable both arms bind (#1446). An arm without a record contributes nothing
|
|
2039
|
+
# to the restore target, whose union with the joined binding {#forget_guard_narrowings} takes, so it still covers
|
|
2040
|
+
# that arm's binding.
|
|
2041
|
+
# `joined` maps each record kind to the joined scope's table of that kind.
|
|
2042
|
+
def join_guard_records(other, joined)
|
|
1944
2043
|
mine = @guard_records
|
|
1945
2044
|
theirs = other.guard_records
|
|
1946
2045
|
return EMPTY_GUARD_RECORDS if mine.empty? && theirs.empty?
|
|
1947
2046
|
|
|
1948
2047
|
merged = mine.merge(theirs) { |_key, left, right| Type::Combinator.union(left, right) }
|
|
1949
|
-
kept = merged.select
|
|
1950
|
-
kind == :global ? joined_globals.key?(name) : joined_constants.key?(name)
|
|
1951
|
-
end
|
|
2048
|
+
kept = merged.select { |(kind, name), _| joined.fetch(kind).key?(name) }
|
|
1952
2049
|
kept.empty? ? EMPTY_GUARD_RECORDS : kept.freeze
|
|
1953
2050
|
end
|
|
1954
2051
|
private :join_guard_records
|
|
@@ -1993,7 +2090,8 @@ module Rigor
|
|
|
1993
2090
|
end
|
|
1994
2091
|
|
|
1995
2092
|
def same_guard_state?(other)
|
|
1996
|
-
@constant_narrowings == other.constant_narrowings && @guard_records == other.guard_records
|
|
2093
|
+
@constant_narrowings == other.constant_narrowings && @guard_records == other.guard_records &&
|
|
2094
|
+
@bot_guard_classes == other.bot_guard_classes
|
|
1997
2095
|
end
|
|
1998
2096
|
|
|
1999
2097
|
def rebuild(
|
|
@@ -2007,6 +2105,7 @@ module Rigor
|
|
|
2007
2105
|
source_path: @source_path,
|
|
2008
2106
|
struct_fold_safe_locals: @struct_fold_safe_locals,
|
|
2009
2107
|
opaque_block_self: @opaque_block_self,
|
|
2108
|
+
block_self_unknown: @block_self_unknown,
|
|
2010
2109
|
singleton_class_body: @singleton_class_body,
|
|
2011
2110
|
lexical_nesting: @lexical_nesting,
|
|
2012
2111
|
dynamic_origins: @dynamic_origins,
|
|
@@ -2020,7 +2119,9 @@ module Rigor
|
|
|
2020
2119
|
repeated_or_writes: @repeated_or_writes,
|
|
2021
2120
|
match_frame: @match_frame,
|
|
2022
2121
|
constant_narrowings: @constant_narrowings,
|
|
2023
|
-
guard_records: @guard_records
|
|
2122
|
+
guard_records: @guard_records,
|
|
2123
|
+
bot_guard_classes: @bot_guard_classes,
|
|
2124
|
+
declared_refinements: @declared_refinements
|
|
2024
2125
|
)
|
|
2025
2126
|
self.class.new(
|
|
2026
2127
|
environment: environment, locals: locals,
|
|
@@ -2034,6 +2135,7 @@ module Rigor
|
|
|
2034
2135
|
source_path: source_path,
|
|
2035
2136
|
struct_fold_safe_locals: struct_fold_safe_locals,
|
|
2036
2137
|
opaque_block_self: opaque_block_self,
|
|
2138
|
+
block_self_unknown: block_self_unknown,
|
|
2037
2139
|
singleton_class_body: singleton_class_body,
|
|
2038
2140
|
lexical_nesting: lexical_nesting,
|
|
2039
2141
|
dynamic_origins: dynamic_origins,
|
|
@@ -2047,7 +2149,9 @@ module Rigor
|
|
|
2047
2149
|
repeated_or_writes: repeated_or_writes,
|
|
2048
2150
|
match_frame: match_frame,
|
|
2049
2151
|
constant_narrowings: constant_narrowings,
|
|
2050
|
-
guard_records: guard_records
|
|
2152
|
+
guard_records: guard_records,
|
|
2153
|
+
bot_guard_classes: bot_guard_classes,
|
|
2154
|
+
declared_refinements: declared_refinements
|
|
2051
2155
|
)
|
|
2052
2156
|
end
|
|
2053
2157
|
|
|
@@ -2065,7 +2169,8 @@ module Rigor
|
|
|
2065
2169
|
result.freeze
|
|
2066
2170
|
end
|
|
2067
2171
|
|
|
2068
|
-
|
|
2172
|
+
# One keyword per Scope field, each with its join rule beside it; the size is the field count.
|
|
2173
|
+
def build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
2069
2174
|
self.class.new(
|
|
2070
2175
|
environment: @environment,
|
|
2071
2176
|
locals: joined_locals.freeze,
|
|
@@ -2113,6 +2218,9 @@ module Rigor
|
|
|
2113
2218
|
# block entry by `entering_opaque_block` and inherited through `rebuild`), so both arms usually carry
|
|
2114
2219
|
# the same value and the `||` is that value.
|
|
2115
2220
|
opaque_block_self: @opaque_block_self || other.opaque_block_self,
|
|
2221
|
+
# Issue #1717 — the same body property, and `||` keeps the exemption it grants (the FP-safe side).
|
|
2222
|
+
# A plain `true` outranks the refine body's {REFINEMENT_BLOCK_SELF} for the same reason.
|
|
2223
|
+
block_self_unknown: merged_block_self_unknown(other.block_self_unknown),
|
|
2116
2224
|
# Issue #963 — a body property like the two above, so both arms of an in-body merge carry the identical
|
|
2117
2225
|
# value and the `||` is that value. `||` is also the safe direction on its own terms: keeping the mark
|
|
2118
2226
|
# declines the `define_method` narrowing, which is the pre-#963 answer.
|
|
@@ -2137,15 +2245,44 @@ module Rigor
|
|
|
2137
2245
|
# of a merge inside one body carry the same one; `||` keeps it should either arm lack it, since dropping
|
|
2138
2246
|
# it only loses the frame's resets.
|
|
2139
2247
|
match_frame: @match_frame || other.match_frame,
|
|
2140
|
-
# Issue #
|
|
2141
|
-
|
|
2248
|
+
# Issue #1667 — a body property stamped at block entry like the frame above, so both arms of a merge carry
|
|
2249
|
+
# the same list; the union keeps either arm's modules, the declining direction.
|
|
2250
|
+
declared_refinements: join_declared_refinements(other),
|
|
2251
|
+
# Issue #1429 — the guard narrowings of globals and constants and their pre-guard records, and of instance
|
|
2252
|
+
# variables (#1446).
|
|
2253
|
+
**join_guard_narrowings(other, joined_globals, joined_ivars)
|
|
2142
2254
|
)
|
|
2143
2255
|
end
|
|
2144
2256
|
|
|
2145
|
-
def
|
|
2257
|
+
def join_declared_refinements(other)
|
|
2258
|
+
mine = @declared_refinements
|
|
2259
|
+
theirs = other.declared_refinements
|
|
2260
|
+
return mine if theirs.equal?(mine) || theirs.empty?
|
|
2261
|
+
return theirs if mine.empty?
|
|
2262
|
+
|
|
2263
|
+
(mine | theirs).freeze
|
|
2264
|
+
end
|
|
2265
|
+
|
|
2266
|
+
def join_guard_narrowings(other, joined_globals, joined_ivars)
|
|
2146
2267
|
joined_constants = join_constant_narrowings(other)
|
|
2147
|
-
{
|
|
2148
|
-
|
|
2268
|
+
joined = { global: joined_globals, ivar: joined_ivars, constant: joined_constants }
|
|
2269
|
+
{ constant_narrowings: joined_constants, guard_records: join_guard_records(other, joined),
|
|
2270
|
+
bot_guard_classes: join_bot_guard_classes(other) }
|
|
2271
|
+
end
|
|
2272
|
+
|
|
2273
|
+
# Issue #1446 — an entry both arms hold, with the classes of either: the joined receiver reads `bot` only where
|
|
2274
|
+
# both arms do. One only an arm holds is dropped, so a call on the joined `bot` reads as on an unknown one.
|
|
2275
|
+
def join_bot_guard_classes(other)
|
|
2276
|
+
mine = @bot_guard_classes
|
|
2277
|
+
theirs = other.bot_guard_classes
|
|
2278
|
+
return mine if mine.equal?(theirs)
|
|
2279
|
+
return EMPTY_BOT_GUARD_CLASSES if mine.empty? || theirs.empty?
|
|
2280
|
+
|
|
2281
|
+
joined = mine.each_with_object({}) do |(key, names), acc|
|
|
2282
|
+
other_names = theirs[key]
|
|
2283
|
+
acc[key] = (names | other_names).freeze if other_names
|
|
2284
|
+
end
|
|
2285
|
+
joined.empty? ? EMPTY_BOT_GUARD_CLASSES : joined.freeze
|
|
2149
2286
|
end
|
|
2150
2287
|
|
|
2151
2288
|
# Issue #1429 — a constant reference both arms narrow reads the union; one only an arm narrows reads its resolved
|
|
@@ -2280,11 +2417,19 @@ module Rigor
|
|
|
2280
2417
|
sym_kind = receiver_kind.to_sym
|
|
2281
2418
|
sym_name = receiver_name.to_sym
|
|
2282
2419
|
filtered = @indexed_narrowings.reject do |k, _|
|
|
2283
|
-
k.receiver_kind == sym_kind && k.receiver_name == sym_name
|
|
2420
|
+
(k.receiver_kind == sym_kind && k.receiver_name == sym_name) || key_guard_rooted_at?(k.key, sym_kind, sym_name)
|
|
2284
2421
|
end
|
|
2285
2422
|
filtered.size == @indexed_narrowings.size ? @indexed_narrowings : filtered.freeze
|
|
2286
2423
|
end
|
|
2287
2424
|
|
|
2425
|
+
# Issue #1703 — whether `key` is a `key?` guard's key that is the variable `kind`/`name`, or a chain read from it.
|
|
2426
|
+
def key_guard_rooted_at?(key, kind, name)
|
|
2427
|
+
return false unless key.is_a?(Inference::KeyPresenceGuard::KeyExpr)
|
|
2428
|
+
|
|
2429
|
+
root = key.root
|
|
2430
|
+
root[0] == kind && root[1] == name
|
|
2431
|
+
end
|
|
2432
|
+
|
|
2288
2433
|
# ADR-58 WD1 — set/clear the declaration-sourced provenance mark.
|
|
2289
2434
|
# Issue #667 — the #667 carrier's add/drop pair. Deliberately not reusing
|
|
2290
2435
|
# {#add_declaration_sourced} / {#drop_declaration_sourced_for}: the two sets are joined by opposite
|
|
@@ -2359,6 +2504,13 @@ module Rigor
|
|
|
2359
2504
|
@guard_records.merge(key => pre_guard).freeze
|
|
2360
2505
|
end
|
|
2361
2506
|
|
|
2507
|
+
def drop_bot_guard_class(kind, name)
|
|
2508
|
+
return @bot_guard_classes if @bot_guard_classes.empty?
|
|
2509
|
+
|
|
2510
|
+
key = [kind, kind == :constant ? name : name.to_sym]
|
|
2511
|
+
@bot_guard_classes.key?(key) ? @bot_guard_classes.except(key).freeze : @bot_guard_classes
|
|
2512
|
+
end
|
|
2513
|
+
|
|
2362
2514
|
def drop_guard_record(kind, name)
|
|
2363
2515
|
return @guard_records if @guard_records.empty?
|
|
2364
2516
|
|