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
|
@@ -4,7 +4,10 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../scope"
|
|
6
6
|
require_relative "../type"
|
|
7
|
+
require_relative "../source/alias_names"
|
|
8
|
+
require_relative "../source/literals"
|
|
7
9
|
require_relative "../source/constant_path"
|
|
10
|
+
require_relative "object_mixins"
|
|
8
11
|
require_relative "../source/node_children"
|
|
9
12
|
require_relative "../source/node_walker"
|
|
10
13
|
require_relative "../source/parameter_envelope"
|
|
@@ -19,7 +22,11 @@ require_relative "last_line"
|
|
|
19
22
|
require_relative "last_status"
|
|
20
23
|
require_relative "error_info"
|
|
21
24
|
require_relative "hash_lookup_mutation"
|
|
25
|
+
require_relative "in_effect_refinements"
|
|
26
|
+
require_relative "refine_census"
|
|
27
|
+
require_relative "refine_self"
|
|
22
28
|
require_relative "index_write_widening"
|
|
29
|
+
require_relative "module_function_state"
|
|
23
30
|
require_relative "multi_target_binder"
|
|
24
31
|
require_relative "mutation_widening"
|
|
25
32
|
require_relative "narrowing"
|
|
@@ -27,6 +34,14 @@ require_relative "operand_effects"
|
|
|
27
34
|
require_relative "statement_evaluator"
|
|
28
35
|
require_relative "struct_fold_safety"
|
|
29
36
|
require_relative "unknown_store_widening"
|
|
37
|
+
require_relative "declaration_walk/traversal"
|
|
38
|
+
require_relative "scope_indexer/class_cvars_collector"
|
|
39
|
+
require_relative "scope_indexer/superclasses_collector"
|
|
40
|
+
require_relative "scope_indexer/def_nestings_collector"
|
|
41
|
+
require_relative "scope_indexer/layered_def_nestings"
|
|
42
|
+
require_relative "scope_indexer/mixin_accumulator"
|
|
43
|
+
require_relative "scope_indexer/certainty"
|
|
44
|
+
require_relative "scope_indexer/member_layouts_collector"
|
|
30
45
|
|
|
31
46
|
module Rigor
|
|
32
47
|
module Inference
|
|
@@ -98,8 +113,8 @@ module Rigor
|
|
|
98
113
|
# propagates it across every derived scope.
|
|
99
114
|
# Issue #722 residue 2 — the project's settled declaration names are the oracle the file's own
|
|
100
115
|
# compact headers are re-anchored against. `discovered_class_sources` would be the more direct
|
|
101
|
-
# reading, but it
|
|
102
|
-
#
|
|
116
|
+
# reading, but it names files rather than settled declarations, and the fold has already applied its
|
|
117
|
+
# own re-anchoring to this table.
|
|
103
118
|
declared_types, discovered_classes =
|
|
104
119
|
build_declaration_artifacts(root, default_scope.discovered_classes.keys.to_set)
|
|
105
120
|
# Merge the indexer's findings on top of whatever the base scope already carries so callers that seed cross-file
|
|
@@ -152,7 +167,9 @@ module Rigor
|
|
|
152
167
|
# discovered-methods existence table and the instance def-node table — see {#build_methods_and_def_nodes}.
|
|
153
168
|
# `seed_discovered_methods` seeds the former onto the scope and returns the def-node table for
|
|
154
169
|
# `merge_project_method_indexes` below.
|
|
155
|
-
|
|
170
|
+
# ADR-119 WD3 — the file's possible contributions, classified once for both def-contribution walks.
|
|
171
|
+
certainty = Certainty.possible_nodes(root)
|
|
172
|
+
seeded_scope, file_tables = seed_discovered_methods(seeded_scope, default_scope, root, certainty)
|
|
156
173
|
|
|
157
174
|
# v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method def nodes, the class -> superclass map, and the
|
|
158
175
|
# class/module -> included-modules map, each merged under the cross-file pre-pass seed (see below). v0.1.2 —
|
|
@@ -160,7 +177,7 @@ module Rigor
|
|
|
160
177
|
# `def.method-visibility-mismatch` and ADR-35 `def.override-visibility-reduced` CheckRules consult the table.
|
|
161
178
|
# Seeded inside `merge_project_method_indexes` so the per-file visibilities merge OVER the cross-file project
|
|
162
179
|
# seed rather than overwriting it.
|
|
163
|
-
seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root,
|
|
180
|
+
seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root, file_tables, certainty)
|
|
164
181
|
|
|
165
182
|
table = {}.compare_by_identity
|
|
166
183
|
table.default = seeded_scope
|
|
@@ -187,11 +204,22 @@ module Rigor
|
|
|
187
204
|
# onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns `[scope,
|
|
188
205
|
# file_def_nodes, file_envelopes]` so the caller can thread the def-node and issue #992 envelope tables into
|
|
189
206
|
# {#merge_project_method_indexes} without walking the file a second time.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
207
|
+
#
|
|
208
|
+
# ADR-119 WD3 — the file's `possible_discovered_methods` folds over the seed's by {#fold_possible_methods}, so a
|
|
209
|
+
# name the file defines certainly is no longer possible and a name only the file's possible contribution
|
|
210
|
+
# supplies becomes possible. The file's other siblings are returned for {#merge_project_method_indexes}.
|
|
211
|
+
def seed_discovered_methods(seeded_scope, default_scope, root, certainty = Certainty.possible_nodes(root))
|
|
212
|
+
file_methods, file_def_nodes, file_envelopes, file_refinements, file_siblings =
|
|
213
|
+
build_methods_and_def_nodes(root, default_scope.source_path, certainty: certainty)
|
|
214
|
+
seed_methods = default_scope.discovered_methods
|
|
215
|
+
discovered_methods = deep_merge_class_methods(seed_methods, file_methods)
|
|
216
|
+
discovery = seeded_scope.discovery.with(
|
|
217
|
+
discovered_methods: discovered_methods,
|
|
218
|
+
possible_discovered_methods: fold_possible_methods(
|
|
219
|
+
seeded_scope.discovery.possible_discovered_methods, seed_methods,
|
|
220
|
+
file_siblings[:possible_discovered_methods], file_methods
|
|
221
|
+
)
|
|
222
|
+
)
|
|
195
223
|
# Issue #1120 — only a file that refines something pays the overlay; the cross-file seed already carries
|
|
196
224
|
# every other file's refinements.
|
|
197
225
|
unless file_refinements.empty?
|
|
@@ -199,7 +227,7 @@ module Rigor
|
|
|
199
227
|
discovered_refinements: merge_refinement_tables(default_scope.discovered_refinements, file_refinements)
|
|
200
228
|
)
|
|
201
229
|
end
|
|
202
|
-
[seeded_scope.with_discovery(discovery), file_def_nodes, file_envelopes]
|
|
230
|
+
[seeded_scope.with_discovery(discovery), [file_def_nodes, file_envelopes, file_siblings]]
|
|
203
231
|
end
|
|
204
232
|
|
|
205
233
|
# ADR-48 Struct slice 3 — installs the top-level fold-safe-local set ({Inference::StructFoldSafety}). Struct
|
|
@@ -220,19 +248,22 @@ module Rigor
|
|
|
220
248
|
# Issue #992 — the envelope table is JOINED with the seed rather than overlaid: the seed already carries this
|
|
221
249
|
# file's own contribution (identical, so the join keeps it), and a reopening in a sibling file must still
|
|
222
250
|
# make a disagreeing name opaque here, which "same-file declarations win" would silently undo.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
251
|
+
#
|
|
252
|
+
# `file_tables` is `[file_def_nodes, file_envelopes, file_siblings]` from {#seed_discovered_methods}.
|
|
253
|
+
def merge_project_method_indexes(seeded_scope, default_scope, root, file_tables,
|
|
254
|
+
certainty = Certainty.possible_nodes(root))
|
|
255
|
+
# ADR-116 WD5 — the superclass, def-nesting and member-layout tables come from ONE shared walk.
|
|
256
|
+
walked = declaration_walk_tables(root, default_scope.source_path)
|
|
257
|
+
def_nodes, def_nestings = merge_def_node_tables(default_scope, walked, file_tables[0])
|
|
258
|
+
singleton_def_nodes, file_singleton = merge_singleton_def_nodes(default_scope, root, certainty)
|
|
259
|
+
superclasses, header_nestings = merge_ancestry_tables(default_scope, walked)
|
|
260
|
+
includes, prepends, *mixin_unpositioned = merge_mixin_tables(default_scope, root)
|
|
228
261
|
# ADR-35 — per-file visibilities merged OVER the cross-file seed (the current file is authoritative for its own
|
|
229
262
|
# classes; sibling-file ancestors are preserved from the project seed).
|
|
230
|
-
method_visibilities = default_scope
|
|
231
|
-
build_discovered_method_visibilities(root)
|
|
232
|
-
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
263
|
+
method_visibilities, file_visibilities = merge_method_visibilities(default_scope, root, certainty)
|
|
233
264
|
# ADR-48 — per-file Data + Struct member layouts merged OVER the cross-file seed (same-file declaration is
|
|
234
265
|
# authoritative).
|
|
235
|
-
data_member_layouts, struct_member_layouts = merge_member_layouts(default_scope,
|
|
266
|
+
data_member_layouts, struct_member_layouts = merge_member_layouts(default_scope, walked)
|
|
236
267
|
|
|
237
268
|
# #526 — this file's extends folded against the MERGED instance def-nodes (so `extend M` sees a
|
|
238
269
|
# sibling-file M through the cross-file seed). The project-wide fold in {#finalize_def_index}
|
|
@@ -240,8 +271,11 @@ module Rigor
|
|
|
240
271
|
#
|
|
241
272
|
# Issue #898 — and the same walk's table is now kept, merged over the cross-file seed the way
|
|
242
273
|
# `includes` is: `Narrowing` asks it what a class object's singleton ancestry holds.
|
|
243
|
-
|
|
244
|
-
|
|
274
|
+
siblings = per_file_siblings(seeded_scope.discovery.sibling_tables, file_tables[0], file_tables[2],
|
|
275
|
+
file_singleton, file_visibilities)
|
|
276
|
+
extends, methods_table, unpositioned =
|
|
277
|
+
merge_and_fold_extends(default_scope, root, def_nodes, singleton_def_nodes, seeded_scope, siblings,
|
|
278
|
+
mixin_unpositioned)
|
|
245
279
|
|
|
246
280
|
seeded_scope.with_discovery(
|
|
247
281
|
seeded_scope.discovery.with(
|
|
@@ -252,23 +286,60 @@ module Rigor
|
|
|
252
286
|
discovered_superclasses: superclasses,
|
|
253
287
|
discovered_header_nestings: header_nestings,
|
|
254
288
|
discovered_includes: includes,
|
|
255
|
-
discovered_prepends: prepends,
|
|
256
|
-
|
|
289
|
+
discovered_prepends: prepends, discovered_extends: extends,
|
|
290
|
+
unpositioned_mixins: unpositioned,
|
|
257
291
|
discovered_method_visibilities: method_visibilities,
|
|
258
|
-
discovered_parameter_envelopes: merge_envelope_seed(default_scope,
|
|
259
|
-
data_member_layouts: data_member_layouts,
|
|
260
|
-
|
|
261
|
-
|
|
292
|
+
discovered_parameter_envelopes: merge_envelope_seed(default_scope, file_tables[1]),
|
|
293
|
+
data_member_layouts: data_member_layouts, struct_member_layouts: struct_member_layouts,
|
|
294
|
+
discovered_deferred_ranges: merge_deferred_ranges_seed(default_scope, root),
|
|
295
|
+
# ADR-119 WD1/WD3 — the seed's siblings with this file's folded in ({#per_file_siblings}: the file wins
|
|
296
|
+
# every slot it writes), which the extends fold above then followed (copy on write, so the seed's
|
|
297
|
+
# tables are never written).
|
|
298
|
+
**siblings
|
|
262
299
|
)
|
|
263
300
|
)
|
|
264
301
|
end
|
|
265
302
|
|
|
303
|
+
# Returns `[merged table, [file table, file contested slots]]` ({#build_method_visibility_tables}).
|
|
304
|
+
def merge_method_visibilities(default_scope, root, certainty)
|
|
305
|
+
file_visibilities = build_method_visibility_tables(root, certainty: certainty)
|
|
306
|
+
merged = default_scope.discovered_method_visibilities.merge(
|
|
307
|
+
file_visibilities.first
|
|
308
|
+
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
309
|
+
[merged, file_visibilities]
|
|
310
|
+
end
|
|
311
|
+
|
|
266
312
|
# Per-file singleton def nodes merged OVER the cross-file seed (same-file declaration is
|
|
267
313
|
# authoritative for its own classes, sibling-file defs are preserved).
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
314
|
+
#
|
|
315
|
+
# Returns `[merged table, [file table, file contested slots]]` ({#build_singleton_def_tables}).
|
|
316
|
+
def merge_singleton_def_nodes(default_scope, root, certainty)
|
|
317
|
+
file_singleton = build_singleton_def_tables(root, certainty: certainty)
|
|
318
|
+
merged = default_scope.discovered_singleton_def_nodes.merge(
|
|
319
|
+
file_singleton.first
|
|
271
320
|
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
321
|
+
[merged, file_singleton]
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# ADR-119 WD3 — the per-file merge of the slot siblings, under the members' own merges: this file's
|
|
325
|
+
# `def_nodes`, singleton defs and visibilities win every slot they write, so the seed's contest on those keys
|
|
326
|
+
# is replaced by the file's ({#fold_slot_contests}); the envelopes are joined, so their contests union. The
|
|
327
|
+
# possible methods were folded already ({#seed_discovered_methods}).
|
|
328
|
+
def per_file_siblings(siblings, file_def_nodes, file_siblings, file_singleton, file_visibilities)
|
|
329
|
+
file_singleton_def_nodes, file_singleton_contested = file_singleton
|
|
330
|
+
siblings[:contested_discovered_method_visibilities] = fold_slot_contests(
|
|
331
|
+
siblings[:contested_discovered_method_visibilities], *file_visibilities
|
|
332
|
+
)
|
|
333
|
+
siblings[:contested_discovered_def_nodes] = fold_slot_contests(
|
|
334
|
+
siblings[:contested_discovered_def_nodes], file_def_nodes, file_siblings[:contested_discovered_def_nodes]
|
|
335
|
+
)
|
|
336
|
+
siblings[:contested_discovered_singleton_def_nodes] = fold_slot_contests(
|
|
337
|
+
siblings[:contested_discovered_singleton_def_nodes], file_singleton_def_nodes, file_singleton_contested
|
|
338
|
+
)
|
|
339
|
+
siblings[:contested_discovered_parameter_envelopes] = union_paths(
|
|
340
|
+
siblings[:contested_discovered_parameter_envelopes], file_siblings[:contested_discovered_parameter_envelopes]
|
|
341
|
+
)
|
|
342
|
+
siblings
|
|
272
343
|
end
|
|
273
344
|
|
|
274
345
|
# Issue #1123 — the two instance-side mixin tables, from ONE descent of this file. Each merges over
|
|
@@ -284,28 +355,60 @@ module Rigor
|
|
|
284
355
|
end,
|
|
285
356
|
default_scope.discovered_prepends.merge(file[:prepends]) do |_class, cross_file, per_file|
|
|
286
357
|
(per_file + cross_file).uniq
|
|
287
|
-
end
|
|
358
|
+
end,
|
|
359
|
+
file[:unpositioned]
|
|
288
360
|
]
|
|
289
361
|
end
|
|
290
362
|
|
|
363
|
+
# The union of `{class => {side => [names]}}` tables — how {Scope::DiscoveryIndex#unpositioned_mixins}
|
|
364
|
+
# folds: an edge any contribution cannot position stays unpositioned. The per-file index folds its own
|
|
365
|
+
# walk over a seed that already carries this file's edges, so the first table itself comes back unless a
|
|
366
|
+
# contribution holds a name the seed lacks: the whole project table is copied only then.
|
|
367
|
+
def union_unpositioned(first, *others)
|
|
368
|
+
result = nil
|
|
369
|
+
others.each do |table|
|
|
370
|
+
table.each do |owner, sides|
|
|
371
|
+
sides.each do |side, names|
|
|
372
|
+
have = (result || first).dig(owner, side)
|
|
373
|
+
next if have && names.all? { |name| have.include?(name) }
|
|
374
|
+
|
|
375
|
+
result ||= first.dup
|
|
376
|
+
result[owner] = (result[owner] || {}).merge(side => ((have || []) | names).freeze).freeze
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
result ? result.freeze : first
|
|
381
|
+
end
|
|
382
|
+
|
|
291
383
|
# The `extend`-edge half of {#merge_project_method_indexes}: merges this file's `extend`s over the
|
|
292
384
|
# cross-file seed AND folds them against the merged def tables — the #526 fold that turns an
|
|
293
385
|
# extended module's instance defs into singleton-side method entries on the extending class.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
386
|
+
#
|
|
387
|
+
# ADR-119 WD1 — the unpositioned union (the seed's, the mixin walks' and this file's extend side) is settled
|
|
388
|
+
# first, because the fold reads it to tell a certain extend edge from a listed one. `siblings` is written by
|
|
389
|
+
# the fold.
|
|
390
|
+
def merge_and_fold_extends(default_scope, root, def_nodes, singleton_def_nodes, seeded_scope, siblings,
|
|
391
|
+
mixin_unpositioned)
|
|
392
|
+
file_extends, extends, extend_unpositioned = merge_extend_tables(default_scope, root)
|
|
393
|
+
unpositioned = union_unpositioned(default_scope.discovery.unpositioned_mixins, *mixin_unpositioned,
|
|
394
|
+
extend_unpositioned)
|
|
395
|
+
methods_table = fold_per_file_extends(file_extends, def_nodes, singleton_def_nodes, seeded_scope, siblings,
|
|
396
|
+
unpositioned)
|
|
397
|
+
[extends, methods_table, unpositioned]
|
|
298
398
|
end
|
|
299
399
|
|
|
300
400
|
# Issue #1097 — this file's def / block / lambda ranges merged over the cross-file seed; the
|
|
301
401
|
# ordering predicates key the table by `source_path`, so the entry must exist even on a run
|
|
302
|
-
# that never built the project pre-pass.
|
|
303
|
-
#
|
|
402
|
+
# that never built the project pre-pass. A seeded entry is only the identical walk product when
|
|
403
|
+
# the pre-pass read the same bytes (a mutant analysed under a seed of the unmutated files does
|
|
404
|
+
# not), so the file is always re-walked and the seed is kept only when the rows are equal — which
|
|
405
|
+
# spares the project-wide Hash copy per file. Issue #1548.
|
|
304
406
|
def merge_deferred_ranges_seed(default_scope, root)
|
|
305
407
|
seeded = default_scope.discovered_deferred_ranges
|
|
306
|
-
|
|
408
|
+
fresh = build_deferred_ranges(root)
|
|
409
|
+
return seeded if seeded[default_scope.source_path] == fresh
|
|
307
410
|
|
|
308
|
-
seeded.merge(default_scope.source_path =>
|
|
411
|
+
seeded.merge(default_scope.source_path => fresh)
|
|
309
412
|
end
|
|
310
413
|
|
|
311
414
|
def merge_envelope_seed(default_scope, file_envelopes)
|
|
@@ -315,70 +418,69 @@ module Rigor
|
|
|
315
418
|
# The as-written superclass table and its issue #682 header-nesting twin, each merged over the cross-file
|
|
316
419
|
# seed. Returned as a pair for the same reason {#merge_def_node_tables} is: both come from ONE walk of the
|
|
317
420
|
# file, so a caller cannot pair a superclass name with a nesting recorded by a different parse.
|
|
318
|
-
def merge_ancestry_tables(default_scope,
|
|
319
|
-
|
|
320
|
-
[default_scope.discovered_superclasses.merge(file_superclasses),
|
|
421
|
+
def merge_ancestry_tables(default_scope, walked)
|
|
422
|
+
[default_scope.discovered_superclasses.merge(walked.fetch(:superclasses)),
|
|
321
423
|
merge_header_nestings(default_scope.discovery.discovered_header_nestings.dup,
|
|
322
|
-
|
|
424
|
+
walked.fetch(:header_nestings)).freeze]
|
|
323
425
|
end
|
|
324
426
|
|
|
325
427
|
# The instance-side def-node table and its issue #681 nesting twin, each merged over the cross-file seed.
|
|
326
428
|
# Returned as a pair so the two stay written together: a node the merge keeps must be the same object the
|
|
327
429
|
# nesting table keys, and they are only that if both halves take the same file's walk.
|
|
328
|
-
def merge_def_node_tables(default_scope,
|
|
430
|
+
def merge_def_node_tables(default_scope, walked, file_def_nodes)
|
|
329
431
|
def_nodes = default_scope.discovered_def_nodes.merge(
|
|
330
432
|
file_def_nodes
|
|
331
433
|
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
332
434
|
[def_nodes,
|
|
333
|
-
merge_def_nestings(default_scope.discovery.discovered_def_nestings,
|
|
435
|
+
merge_def_nestings(default_scope.discovery.discovered_def_nestings, walked.fetch(:def_nestings))]
|
|
334
436
|
end
|
|
335
437
|
|
|
336
438
|
# Issue #681 — the per-file nesting table over the cross-file seed. Both are keyed by node identity, so
|
|
337
|
-
# a same-file declaration and its cross-file twin are distinct keys and the
|
|
338
|
-
#
|
|
339
|
-
#
|
|
340
|
-
#
|
|
439
|
+
# a same-file declaration and its cross-file twin are distinct keys and the layering order is immaterial.
|
|
440
|
+
# Issue #1507 — layered by lookup ({LayeredDefNestings}) instead of copied: the copy was one insert per
|
|
441
|
+
# `def` in the project for every analysed file. Either table alone is returned as it is when the other is
|
|
442
|
+
# empty — since issue #716 a top-level `def` records its empty chain, so only a file declaring no `def` at
|
|
443
|
+
# all hands back the seed itself.
|
|
341
444
|
def merge_def_nestings(seed, file_nestings)
|
|
342
445
|
return seed if file_nestings.empty?
|
|
343
446
|
return file_nestings if seed.empty?
|
|
344
447
|
|
|
345
|
-
|
|
346
|
-
merged.merge!(seed)
|
|
347
|
-
merged.merge!(file_nestings)
|
|
348
|
-
merged.freeze
|
|
448
|
+
LayeredDefNestings.new(file_nestings, seed)
|
|
349
449
|
end
|
|
350
450
|
|
|
351
451
|
# Issue #898 — one walk, two consumers: the raw per-file table the #526 method fold reads, and the
|
|
352
452
|
# same table merged over the cross-file seed for the scope. Returned as a pair so a caller cannot
|
|
353
453
|
# pair the fold with a table a different parse produced, as {#merge_ancestry_tables} is.
|
|
354
454
|
def merge_extend_tables(default_scope, root)
|
|
355
|
-
|
|
455
|
+
file = extend_tables(root)
|
|
456
|
+
file_extends = file.fetch(:extends)
|
|
356
457
|
# The table stores singleton-ancestor search order, so the file under analysis — the
|
|
357
458
|
# later-loading contribution for a reopened class — prepends over the cross-file seed.
|
|
358
459
|
merged = default_scope.discovered_extends.merge(
|
|
359
460
|
file_extends
|
|
360
461
|
) { |_class, cross_file, per_file| (per_file + cross_file).uniq }
|
|
361
|
-
[file_extends, merged]
|
|
462
|
+
[file_extends, merged, file.fetch(:unpositioned)]
|
|
362
463
|
end
|
|
363
464
|
|
|
364
465
|
# The per-file half of the #526 fold: mutable copies of the merged tables take the extends, and the
|
|
365
466
|
# existence table (already seeded onto the scope) is rebuilt only when the fold touched it.
|
|
366
|
-
def fold_per_file_extends(extends, def_nodes, singleton_def_nodes, seeded_scope)
|
|
467
|
+
def fold_per_file_extends(extends, def_nodes, singleton_def_nodes, seeded_scope, siblings, unpositioned)
|
|
367
468
|
methods_table = seeded_scope.discovered_methods
|
|
368
469
|
return methods_table if extends.empty?
|
|
369
470
|
|
|
370
471
|
mutable_methods = methods_table.transform_values(&:dup)
|
|
371
|
-
fold_extends_into_singleton_tables(extends, def_nodes, singleton_def_nodes, mutable_methods
|
|
472
|
+
fold_extends_into_singleton_tables(extends, def_nodes, singleton_def_nodes, mutable_methods,
|
|
473
|
+
siblings: siblings, unpositioned: unpositioned)
|
|
372
474
|
mutable_methods
|
|
373
475
|
end
|
|
374
476
|
|
|
375
477
|
# ADR-48 — the per-file Data + Struct member-layout tables, each merged OVER the cross-file seed so a same-file
|
|
376
478
|
# declaration wins for its own classes. Returned as a pair to keep {#merge_project_method_indexes} under the
|
|
377
479
|
# method-size budget.
|
|
378
|
-
def merge_member_layouts(default_scope,
|
|
480
|
+
def merge_member_layouts(default_scope, walked)
|
|
379
481
|
[
|
|
380
|
-
default_scope.data_member_layouts.merge(
|
|
381
|
-
default_scope.struct_member_layouts.merge(
|
|
482
|
+
default_scope.data_member_layouts.merge(walked.fetch(:data_member_layouts)),
|
|
483
|
+
default_scope.struct_member_layouts.merge(walked.fetch(:struct_member_layouts))
|
|
382
484
|
]
|
|
383
485
|
end
|
|
384
486
|
|
|
@@ -1946,7 +2048,20 @@ module Rigor
|
|
|
1946
2048
|
# `Prism::ClassVariableWriteNode` writes inside ANY def body (instance or singleton) of the enclosing class,
|
|
1947
2049
|
# because Ruby cvars are shared across both facets. The resulting table is seeded into both instance and singleton
|
|
1948
2050
|
# method bodies through `Scope#class_cvars_for`.
|
|
2051
|
+
#
|
|
2052
|
+
# ADR-116 WD5 — built by {ClassCvarsCollector} on the shared {DeclarationWalk}; `RIGOR_SHADOW_RULE_WALK`
|
|
2053
|
+
# checks it against {#legacy_class_cvar_index}.
|
|
1949
2054
|
def build_class_cvar_index(root, default_scope)
|
|
2055
|
+
collector = ClassCvarsCollector.new
|
|
2056
|
+
DeclarationWalk.run(root, [collector], DeclarationWalk::Context.root(scope: default_scope))
|
|
2057
|
+
DeclarationWalk::Shadow.verified(:class_cvars, default_scope.source_path, collector.table) do
|
|
2058
|
+
legacy_class_cvar_index(root, default_scope)
|
|
2059
|
+
end
|
|
2060
|
+
end
|
|
2061
|
+
|
|
2062
|
+
# The walker {ClassCvarsCollector} replaced, kept as the shadow harness's oracle until ADR-116 WD5 has
|
|
2063
|
+
# ported every table walker onto {DeclarationWalk}.
|
|
2064
|
+
def legacy_class_cvar_index(root, default_scope)
|
|
1950
2065
|
accumulator = {}
|
|
1951
2066
|
walk_class_cvars(root, [], default_scope, accumulator)
|
|
1952
2067
|
accumulator.transform_values(&:freeze).freeze
|
|
@@ -2140,11 +2255,13 @@ module Rigor
|
|
|
2140
2255
|
# @return the `program_globals` table and the census, keyed by the discovery index members it fills
|
|
2141
2256
|
def build_program_global_index(root, default_scope)
|
|
2142
2257
|
accumulator = {}
|
|
2258
|
+
in_effect = InEffectRefinements.new(root)
|
|
2143
2259
|
census = { patched_line_readers: Set.new, clears_last_status: false, defines_case_equality: false,
|
|
2144
|
-
write_census: GlobalWriteCensus::Collector.new }
|
|
2260
|
+
write_census: GlobalWriteCensus::Collector.new(in_effect) }
|
|
2145
2261
|
gather_global_writes(root, default_scope, accumulator, census)
|
|
2146
2262
|
census[:patched_line_readers] = census[:patched_line_readers].freeze
|
|
2147
2263
|
census[:implicit_self_evidence] = LastLine::SelfEvidence.new(root)
|
|
2264
|
+
census[:in_effect_refinements] = in_effect
|
|
2148
2265
|
[accumulator.freeze, census]
|
|
2149
2266
|
end
|
|
2150
2267
|
|
|
@@ -2867,8 +2984,7 @@ module Rigor
|
|
|
2867
2984
|
# `ArgumentsNode`, and a `&blk` pass-through is a `BlockArgumentNode`.
|
|
2868
2985
|
def walk_constant_write_children(node, qualified_prefix, default_scope, accumulator, self_owner,
|
|
2869
2986
|
singleton_cref)
|
|
2870
|
-
rebound = rebound_block_self(node, qualified_prefix, default_scope, nil,
|
|
2871
|
-
rebound_self_base(self_owner))
|
|
2987
|
+
rebound = rebound_block_self(node, qualified_prefix, default_scope, nil, self_owner)
|
|
2872
2988
|
node.rigor_each_child do |child|
|
|
2873
2989
|
rebinds_self = rebound && child.is_a?(Prism::BlockNode)
|
|
2874
2990
|
owner = rebinds_self ? rebound : self_owner
|
|
@@ -2894,14 +3010,18 @@ module Rigor
|
|
|
2894
3010
|
# caller ({#meta_new_block_owner}) ([#710](https://github.com/rigortype/rigor/issues/710)). It is
|
|
2895
3011
|
# supplied only by the publication census; the typed walk returns at a `ConstantWriteNode` without
|
|
2896
3012
|
# descending into its rvalue, so no `self::` write inside one reaches that walk at all.
|
|
3013
|
+
#
|
|
3014
|
+
# `self_owner` is the enclosing body's self as the walk carries it, unsplit. Every walk asks this at
|
|
3015
|
+
# every node it visits, and only an eval call reads the self, so the split waits until one does
|
|
3016
|
+
# ([#1502](https://github.com/rigortype/rigor/issues/1502)).
|
|
2897
3017
|
def rebound_block_self(node, qualified_prefix, default_scope = nil, meta_owner = nil,
|
|
2898
|
-
|
|
3018
|
+
self_owner = nil)
|
|
2899
3019
|
return nil unless node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode)
|
|
2900
3020
|
return OPAQUE_SELF if OPAQUE_SELF_BLOCK_CALLS.include?(node.name)
|
|
2901
3021
|
return meta_owner || OPAQUE_SELF if meta_new_constant_rvalue?(node)
|
|
2902
3022
|
return nil unless RECEIVER_EVAL_CALLS.include?(node.name)
|
|
2903
3023
|
|
|
2904
|
-
eval_receiver_self(node.receiver, qualified_prefix, default_scope,
|
|
3024
|
+
eval_receiver_self(node.receiver, qualified_prefix, default_scope, rebound_self_base(self_owner))
|
|
2905
3025
|
end
|
|
2906
3026
|
|
|
2907
3027
|
# Issue #710 — the qualified name a `Klass = Class.new { … }` gives the block's class, or nil when
|
|
@@ -3128,23 +3248,249 @@ module Rigor
|
|
|
3128
3248
|
# Issue #1120 — and a fourth, `refinements`: `{refined class => {method => [refining modules]}}`, the
|
|
3129
3249
|
# `def`s of every `refine X do … end` body ({#record_refinement_defs}). Those are kept OUT of the first
|
|
3130
3250
|
# three tables, which answer everywhere: a refined method exists only where a `using` is in effect.
|
|
3131
|
-
|
|
3132
|
-
|
|
3251
|
+
#
|
|
3252
|
+
# ADR-119 WD3 — and a fifth, the file's def-contribution siblings ({MethodTables#siblings}):
|
|
3253
|
+
# `possible_discovered_methods`, `contested_discovered_def_nodes` and
|
|
3254
|
+
# `contested_discovered_parameter_envelopes`, from the {Certainty} classification `certainty` (computed here
|
|
3255
|
+
# when the caller has none).
|
|
3256
|
+
def build_methods_and_def_nodes(root, source_path = nil, certainty: Certainty.possible_nodes(root), census: nil)
|
|
3257
|
+
tables = MethodTables.new({}, {}, nil, certainty, true)
|
|
3258
|
+
tables.root = root
|
|
3259
|
+
tables.census = census
|
|
3133
3260
|
def_nodes = {}
|
|
3134
3261
|
walk_methods_and_def_nodes(root, [], false, tables, def_nodes, source_path)
|
|
3135
|
-
apply_alias_def_nodes(root, def_nodes)
|
|
3262
|
+
apply_alias_def_nodes(root, def_nodes, tables)
|
|
3136
3263
|
[tables.existence.transform_values(&:freeze).freeze, def_nodes.transform_values(&:freeze).freeze,
|
|
3137
|
-
tables.envelopes.transform_values(&:freeze).freeze, freeze_refinements(tables.refinements)
|
|
3264
|
+
tables.envelopes.transform_values(&:freeze).freeze, freeze_refinements(tables.refinements),
|
|
3265
|
+
tables.siblings]
|
|
3138
3266
|
end
|
|
3139
3267
|
|
|
3140
3268
|
# The accumulator {#walk_methods_and_def_nodes} threads: the existence table and its issue #992
|
|
3141
3269
|
# envelope twin, which only {#record_method} and {#record_surface_mark} write, and the issue #1120
|
|
3142
3270
|
# refinement table, which only {#record_refinement_defs} writes. That last one stays nil until a
|
|
3143
3271
|
# `refine` block is seen, so a file without one allocates nothing for it.
|
|
3144
|
-
|
|
3272
|
+
#
|
|
3273
|
+
# ADR-119 WD3 — `certainty` is the file's {Certainty} answer and `certain` whether the contribution being
|
|
3274
|
+
# recorded is certain ({#at} sets it before each recorder). `possible`, `contested_envelopes` and
|
|
3275
|
+
# `contested_def_nodes` are the siblings the recorders fill, each nil until a possible contribution writes it.
|
|
3276
|
+
#
|
|
3277
|
+
# Issue #1689 — `root` is the walked tree, and `in_effect` its {InEffectRefinements}, built the first time a
|
|
3278
|
+
# `refine`-shaped call asks {#refine_target_of}, so a file without one never builds it.
|
|
3279
|
+
#
|
|
3280
|
+
# ADR-121 WD7 — `census`, when an Array, collects `[start offset, outcome]` for every `refine`-shaped node the
|
|
3281
|
+
# walk accounts for ({ScopeIndexer.refine_census}); `refine_text` memoises the byte offsets of the `refine`
|
|
3282
|
+
# words in the file's text, so a file with none skips every census scan and a `def` with none is not scanned;
|
|
3283
|
+
# `literal_seen` holds the offsets of the refine literals a call's arguments already accounted for;
|
|
3284
|
+
# `source_bytes` memoises the byte offsets of the file's heredoc openers; `loose_constants` memoises
|
|
3285
|
+
# {ScopeIndexer.loose_constant_segments}. The module a `refine`-shaped node is charged to is what the in-effect
|
|
3286
|
+
# walk worked out for it ({#refine_context}).
|
|
3287
|
+
MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
|
|
3288
|
+
:contested_envelopes, :contested_def_nodes, :root, :in_effect, :census,
|
|
3289
|
+
:refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans) do
|
|
3290
|
+
# {ScopeIndexer.module_refine_target} against this tree's {InEffectRefinements}.
|
|
3291
|
+
def refine_target_of(node)
|
|
3292
|
+
return nil if ScopeIndexer.refine_target(node).nil?
|
|
3293
|
+
|
|
3294
|
+
ScopeIndexer.module_refine_target(node, in_effect_query)
|
|
3295
|
+
end
|
|
3296
|
+
|
|
3297
|
+
# {InEffectRefinements#class_body_refine?} for a `refine` call of this tree.
|
|
3298
|
+
def class_body_refine?(node) = ScopeIndexer.refine_call?(node) && in_effect_query.class_body_refine?(node)
|
|
3299
|
+
|
|
3300
|
+
def in_effect_query = (self.in_effect ||= InEffectRefinements.new(root))
|
|
3301
|
+
|
|
3302
|
+
# Does the walked file's text name `refine` as a word? Every census scan asks first, so a file that only says
|
|
3303
|
+
# `refined` or `refinement` pays nothing for it.
|
|
3304
|
+
def mentions_refine? = !refine_offsets.empty?
|
|
3305
|
+
|
|
3306
|
+
# Does the text in `[start, stop)` name `refine` as a word, or open a heredoc whose body (which follows the
|
|
3307
|
+
# line, and so may end after `stop`) could? A `def` that does neither is not scanned.
|
|
3308
|
+
def refine_in?(start, stop)
|
|
3309
|
+
ScopeIndexer.offset_in?(refine_offsets, start, stop) ||
|
|
3310
|
+
ScopeIndexer.offset_in?(self.source_bytes ||= ScopeIndexer.heredoc_offsets(root), start, stop)
|
|
3311
|
+
end
|
|
3312
|
+
|
|
3313
|
+
# ADR-121 WD7 — the `self` the `refine`-shaped `node` runs on, as the in-effect walk worked it out
|
|
3314
|
+
# ({InEffectRefinements#refine_context}); a module the walk cannot name for a node it did not record.
|
|
3315
|
+
def refine_context(node) = in_effect_query.refine_context(node) || RefineSelf.unknown
|
|
3316
|
+
|
|
3317
|
+
# The byte offsets of each `refine` word in the file's text, ascending.
|
|
3318
|
+
def refine_offsets
|
|
3319
|
+
self.refine_text ||= ScopeIndexer.refine_word_offsets(root)
|
|
3320
|
+
end
|
|
3321
|
+
|
|
3322
|
+
# Records that the walk accounted for the `refine`-shaped `node` with `outcome` (and the module it charged),
|
|
3323
|
+
# when a census is taken.
|
|
3324
|
+
def account(node, outcome, charged = nil)
|
|
3325
|
+
census&.push([node.location.start_offset, outcome, charged])
|
|
3326
|
+
end
|
|
3327
|
+
|
|
3328
|
+
# Marks a refine literal accounted for by its call, so the walk's descent does not account for it again.
|
|
3329
|
+
def see_literal(node) = (self.literal_seen ||= Set.new) << node.location.start_offset
|
|
3330
|
+
|
|
3331
|
+
def literal_seen?(node) = !literal_seen.nil? && literal_seen.include?(node.location.start_offset)
|
|
3332
|
+
|
|
3333
|
+
# A6 — the last segments of the constants this file binds by `||=` or by a literal `const_set`, which the
|
|
3334
|
+
# project-wide `constant_writers` a reader consults leaves out.
|
|
3335
|
+
def loose_constant?(segment)
|
|
3336
|
+
self.loose_constants ||= ScopeIndexer.loose_constant_segments(root)
|
|
3337
|
+
loose_constants.include?(segment)
|
|
3338
|
+
end
|
|
3339
|
+
|
|
3340
|
+
# Issue #1820 — does `node` sit inside a block or lambda of this tree? A block's `self` may be an object even in
|
|
3341
|
+
# a class body or at the top level (`s.instance_eval { … }`, a `define_method` block). The spans are found once
|
|
3342
|
+
# per file, on the first ask.
|
|
3343
|
+
def in_block?(node)
|
|
3344
|
+
self.block_spans ||= ScopeIndexer.block_spans(root)
|
|
3345
|
+
offset = node.location.start_offset
|
|
3346
|
+
block_spans.any? { |start, stop| offset >= start && offset < stop }
|
|
3347
|
+
end
|
|
3348
|
+
|
|
3349
|
+
# Sets {#certain} for the contribution `node` and returns the tables.
|
|
3350
|
+
def at(node)
|
|
3351
|
+
self.certain = !Certainty.possible?(certainty, node)
|
|
3352
|
+
self
|
|
3353
|
+
end
|
|
3354
|
+
|
|
3355
|
+
# The `{sibling name => table}` part this walk fills, frozen, with the shared empty value for each
|
|
3356
|
+
# sibling nothing wrote.
|
|
3357
|
+
def siblings
|
|
3358
|
+
empty = Scope::DiscoveryIndex::EMPTY_SIBLINGS
|
|
3359
|
+
{
|
|
3360
|
+
possible_discovered_methods:
|
|
3361
|
+
possible ? possible.transform_values(&:freeze).freeze : empty[:possible_discovered_methods],
|
|
3362
|
+
contested_discovered_def_nodes:
|
|
3363
|
+
frozen_paths(contested_def_nodes) || empty[:contested_discovered_def_nodes],
|
|
3364
|
+
contested_discovered_parameter_envelopes:
|
|
3365
|
+
frozen_paths(contested_envelopes) || empty[:contested_discovered_parameter_envelopes]
|
|
3366
|
+
}.freeze
|
|
3367
|
+
end
|
|
3368
|
+
|
|
3369
|
+
private
|
|
3370
|
+
|
|
3371
|
+
def frozen_paths(set)
|
|
3372
|
+
return nil if set.nil? || set.empty?
|
|
3373
|
+
|
|
3374
|
+
set.each(&:freeze).freeze
|
|
3375
|
+
end
|
|
3376
|
+
end
|
|
3145
3377
|
|
|
3146
3378
|
EMPTY_REFINEMENTS = {}.freeze
|
|
3147
|
-
|
|
3379
|
+
REFINEMENT_WILDCARD = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
3380
|
+
REFINE_WORD = /\brefine\b/
|
|
3381
|
+
HEREDOC_OPENER = /<<[~-]?["'`A-Za-z_]/
|
|
3382
|
+
private_constant :EMPTY_REFINEMENTS, :REFINEMENT_WILDCARD, :REFINE_WORD, :HEREDOC_OPENER
|
|
3383
|
+
|
|
3384
|
+
NO_OFFSETS = [].freeze
|
|
3385
|
+
private_constant :NO_OFFSETS
|
|
3386
|
+
|
|
3387
|
+
# The byte offsets of the `refine` words in `root`'s text (none for no tree), which `MethodTables#refine_in?`
|
|
3388
|
+
# matches node offsets against.
|
|
3389
|
+
def refine_word_offsets(root) = pattern_offsets(root, REFINE_WORD)
|
|
3390
|
+
|
|
3391
|
+
# The byte offsets of the heredoc openers (`<<~ID`, `<<-ID`, `<<ID`, `<<"ID"`) in `root`'s text: a heredoc's body
|
|
3392
|
+
# follows its line, so it may end after the `def` that opens it.
|
|
3393
|
+
def heredoc_offsets(root) = pattern_offsets(root, HEREDOC_OPENER)
|
|
3394
|
+
|
|
3395
|
+
def pattern_offsets(root, pattern)
|
|
3396
|
+
return NO_OFFSETS if root.nil?
|
|
3397
|
+
|
|
3398
|
+
# Byte-wise: a file may hold bytes its encoding does not (binary `__END__` data, a stray comment byte), and a
|
|
3399
|
+
# regexp over such a String raises.
|
|
3400
|
+
binary = root.send(:source).source.b
|
|
3401
|
+
return NO_OFFSETS unless pattern.match?(binary)
|
|
3402
|
+
|
|
3403
|
+
offsets = []
|
|
3404
|
+
position = 0
|
|
3405
|
+
while (match = pattern.match(binary, position))
|
|
3406
|
+
offsets << match.begin(0)
|
|
3407
|
+
position = match.end(0)
|
|
3408
|
+
end
|
|
3409
|
+
offsets.freeze
|
|
3410
|
+
end
|
|
3411
|
+
|
|
3412
|
+
# Is one of the ascending `offsets` in `[start, stop)`?
|
|
3413
|
+
# The `[start, end)` byte span of every block and lambda in `root`.
|
|
3414
|
+
def block_spans(root)
|
|
3415
|
+
spans = []
|
|
3416
|
+
return spans if root.nil?
|
|
3417
|
+
|
|
3418
|
+
Source::NodeWalker.each(root) do |node|
|
|
3419
|
+
next unless node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode)
|
|
3420
|
+
|
|
3421
|
+
location = node.location
|
|
3422
|
+
spans << [location.start_offset, location.end_offset]
|
|
3423
|
+
end
|
|
3424
|
+
spans
|
|
3425
|
+
end
|
|
3426
|
+
|
|
3427
|
+
def offset_in?(offsets, start, stop)
|
|
3428
|
+
index = offsets.bsearch_index { |offset| offset >= start }
|
|
3429
|
+
!index.nil? && offsets[index] < stop
|
|
3430
|
+
end
|
|
3431
|
+
|
|
3432
|
+
# A6 — the last segments of the constants `root` binds by `||=` (`K ||= String`, `A::K ||= …`) or by a
|
|
3433
|
+
# `const_set` whose name is a literal (`Object.const_set(:K, String)`), and (issue #1749) by any write in a
|
|
3434
|
+
# `class << …` body: the project-wide constant census files a write there under no name a `refine` argument
|
|
3435
|
+
# spells, but a `def` in that body reads it lexically (`class << self; K = String; def setup = refine(K) {…}`).
|
|
3436
|
+
# Scanned only for a file with a literal-target `refine`.
|
|
3437
|
+
def loose_constant_segments(root)
|
|
3438
|
+
segments = Set.new
|
|
3439
|
+
Source::NodeWalker.each(root) do |node|
|
|
3440
|
+
case node
|
|
3441
|
+
when Prism::ConstantOrWriteNode then segments << node.name.to_s
|
|
3442
|
+
when Prism::ConstantPathOrWriteNode then segments << node.target.name.to_s
|
|
3443
|
+
when Prism::SingletonClassNode then singleton_constant_segments(node.body, segments)
|
|
3444
|
+
when Prism::CallNode
|
|
3445
|
+
name = node.name == :const_set && node.arguments&.arguments&.first
|
|
3446
|
+
segments << name.unescaped if name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)
|
|
3447
|
+
end
|
|
3448
|
+
end
|
|
3449
|
+
segments
|
|
3450
|
+
end
|
|
3451
|
+
|
|
3452
|
+
# The last segment of every constant a write, an operator write or a multiple assignment target in `body` binds.
|
|
3453
|
+
def singleton_constant_segments(body, segments)
|
|
3454
|
+
return if body.nil?
|
|
3455
|
+
|
|
3456
|
+
Source::NodeWalker.each(body) do |node|
|
|
3457
|
+
case node
|
|
3458
|
+
when Prism::ConstantWriteNode, Prism::ConstantTargetNode, Prism::ConstantOperatorWriteNode,
|
|
3459
|
+
Prism::ConstantAndWriteNode, Prism::ConstantPathTargetNode
|
|
3460
|
+
segments << node.name.to_s
|
|
3461
|
+
when Prism::ConstantPathWriteNode, Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathAndWriteNode
|
|
3462
|
+
segments << node.target.name.to_s
|
|
3463
|
+
end
|
|
3464
|
+
end
|
|
3465
|
+
end
|
|
3466
|
+
|
|
3467
|
+
# ADR-121 WD7 — the census gate's view of one parsed file (`root`): `[refinement table, [[start offset,
|
|
3468
|
+
# outcome], …]]`, one pair for every `refine`-shaped node the method walk accounted for. The outcomes are
|
|
3469
|
+
# `:recorded` (a literal target and block, its names recorded; also one in a module's own singleton method,
|
|
3470
|
+
# #1749), `:class_unknown` (a block whose target the walk cannot name), `:targets_wildcard` (a `refine` that
|
|
3471
|
+
# runs once a method is called, other than a module's own singleton method's, or in a block that may rebind
|
|
3472
|
+
# `self`, charged to its module or the wildcard), `:literal` (A3's `:refine` / eval-string literal), `:nested` (a
|
|
3473
|
+
# `refine` in a refine body, or in a singleton method of one), `:dsl` (where `self` is a class, #1689) and
|
|
3474
|
+
# `:raises` (no block, a Proc as the block, another receiver, or a computed target where `self` is `main`), and
|
|
3475
|
+
# `:data` (a `:refine` literal where `self` is a plain class or one of its instances).
|
|
3476
|
+
def refine_census(root)
|
|
3477
|
+
census = []
|
|
3478
|
+
refinements = build_methods_and_def_nodes(root, census: census)[3]
|
|
3479
|
+
[refinements, census]
|
|
3480
|
+
end
|
|
3481
|
+
|
|
3482
|
+
# ADR-121 WD7 (issue #1814) — the refinement table one parsed file contributes: the census of
|
|
3483
|
+
# {#build_methods_and_def_nodes}, whose fourth table is the project walk's own (`seed_discovered_methods` and
|
|
3484
|
+
# `walk_def_contributions` take it from the same call), run for a caller that wants nothing else. The gem source
|
|
3485
|
+
# walker reads a gem file this way, so a gem module's rows carry the wildcards a project module's would
|
|
3486
|
+
# (`walker_walk_census_parity_spec.rb`, beside the walker's specs, holds the two to one table). A file whose text
|
|
3487
|
+
# never names `refine` as a word records no row, so it is not walked. `source_path` names an anonymous module's
|
|
3488
|
+
# row (`#<Module:<path>:L:C>`) as the project walk does, so two files' bare `Module.new` blocks never share one.
|
|
3489
|
+
def refinement_table(root, source_path = nil)
|
|
3490
|
+
return EMPTY_REFINEMENTS if refine_word_offsets(root).empty?
|
|
3491
|
+
|
|
3492
|
+
build_methods_and_def_nodes(root, source_path)[3]
|
|
3493
|
+
end
|
|
3148
3494
|
|
|
3149
3495
|
# Issue #1120 — a refinement table, deep-frozen; the shared empty table when nothing was recorded.
|
|
3150
3496
|
def freeze_refinements(table)
|
|
@@ -3168,8 +3514,7 @@ module Rigor
|
|
|
3168
3514
|
# implicit- or `self`-receiver `refine` with one constant argument and a literal block. A computed target
|
|
3169
3515
|
# (`refine(klass) { … }`) names no class this walk can key, so its body walks as it did before.
|
|
3170
3516
|
def refine_target(node)
|
|
3171
|
-
return nil unless
|
|
3172
|
-
return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
3517
|
+
return nil unless refine_call?(node)
|
|
3173
3518
|
|
|
3174
3519
|
arguments = node.arguments&.arguments
|
|
3175
3520
|
return nil unless arguments&.size == 1
|
|
@@ -3178,27 +3523,239 @@ module Rigor
|
|
|
3178
3523
|
target if target.is_a?(Prism::ConstantReadNode) || target.is_a?(Prism::ConstantPathNode)
|
|
3179
3524
|
end
|
|
3180
3525
|
|
|
3526
|
+
# An implicit- or `self`-receiver `refine` call with a literal block, whatever its argument.
|
|
3527
|
+
def refine_call?(node)
|
|
3528
|
+
node.name == :refine && node.block.is_a?(Prism::BlockNode) &&
|
|
3529
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
3530
|
+
end
|
|
3531
|
+
|
|
3532
|
+
# Issue #1689 — {#refine_target}, for a call that `in_effect` (its file's {InEffectRefinements}) does not place
|
|
3533
|
+
# where `self` is a class. `Class` undefines `refine`, so `refine X do … end` directly in a `class` body is the
|
|
3534
|
+
# class's own method (a DSL), and its block walks as any other block. Every walk that treats a refine body
|
|
3535
|
+
# specially asks this, so the method tables, the in-effect refinements and the typer agree on which calls are
|
|
3536
|
+
# refine bodies.
|
|
3537
|
+
def module_refine_target(node, in_effect)
|
|
3538
|
+
target = refine_target(node)
|
|
3539
|
+
target unless target.nil? || in_effect.class_body_refine?(node)
|
|
3540
|
+
end
|
|
3541
|
+
|
|
3181
3542
|
# Issue #1120 — records the instance `def`s of a `refine X do … end` body as refinement methods of X, keyed by
|
|
3182
|
-
# the refining module (`
|
|
3543
|
+
# the refining module (`refining`, the `self` the `refine` call runs on). X is resolved LEXICALLY
|
|
3183
3544
|
# (`qualified_prefix`), which is where Ruby resolves the argument even inside a `Module.new { … }` block
|
|
3184
3545
|
# whose `self` is anonymous; every name it can denote is recorded, as {#constant_receiver_candidates}
|
|
3185
3546
|
# explains. Nothing in the body reaches the class tables: `refine String do def shout` does not give
|
|
3186
3547
|
# `String` or the refining module a `shout` outside a `using`.
|
|
3187
|
-
|
|
3548
|
+
#
|
|
3549
|
+
# ADR-121 WD7 (issue #1799) — the names are what {RefineCensus.read_body} reads (`def`, `alias`,
|
|
3550
|
+
# `alias_method`, `define_method`, `attr_*`, `undef`), and a body that may define a name it cannot spell adds a
|
|
3551
|
+
# names-wildcard row per target. `refining` is `""` at the top level (where Ruby raises). The body is not
|
|
3552
|
+
# walked any further, so the census scans it here.
|
|
3553
|
+
#
|
|
3554
|
+
# A6 — a target this file binds by `||=` or a literal `const_set` (`K ||= String; refine(K)`) names whatever value
|
|
3555
|
+
# the write holds, so its names go under the wildcard class, as a computed target's do.
|
|
3556
|
+
def record_refinement_defs(node, target, qualified_prefix, refining, tables)
|
|
3557
|
+
loose = tables.loose_constant?(target.name.to_s)
|
|
3558
|
+
tables.account(node, loose ? :class_unknown : :recorded, census_charge(refining))
|
|
3188
3559
|
body = node.block.body
|
|
3189
3560
|
return if body.nil?
|
|
3190
3561
|
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3562
|
+
targets = loose ? [REFINEMENT_WILDCARD] : constant_receiver_candidates(target, qualified_prefix)
|
|
3563
|
+
reading = RefineCensus.read_body(body)
|
|
3564
|
+
refining.each do |owner|
|
|
3565
|
+
tables.refinements = RefineCensus.record_rows(tables.refinements, targets, reading, owner)
|
|
3566
|
+
end
|
|
3567
|
+
census_scan(body, qualified_prefix, tables, refine_body: true) if tables.mentions_refine?
|
|
3568
|
+
end
|
|
3569
|
+
|
|
3570
|
+
# What the census records as charged for `owners`: the one module, or every name.
|
|
3571
|
+
def census_charge(owners) = owners.size == 1 ? owners.first : owners
|
|
3572
|
+
|
|
3573
|
+
# ADR-121 WD7 — a literal-target `refine X do … end` the module-body walk reaches, charged where the in-effect
|
|
3574
|
+
# walk says it runs ({MethodTables#refine_context}): rows for the module it names (the walk's own name for an
|
|
3575
|
+
# anonymous `Module.new` block, `""` at the top level), or a targets-wildcard for that module (the wildcard
|
|
3576
|
+
# where it cannot be named) when it runs only once a method is called or on a module the walk cannot name.
|
|
3577
|
+
def record_literal_target_refine(node, target, qualified_prefix, owner_prefix, tables)
|
|
3578
|
+
context = tables.refine_context(node)
|
|
3579
|
+
here = context.here
|
|
3580
|
+
return record_refinement_defs(node, target, qualified_prefix, [""], tables) if here == RefineSelf::MAIN
|
|
3581
|
+
if context.deferred || here == REFINEMENT_WILDCARD
|
|
3582
|
+
return record_deferred_refine(node, RefineSelf.charged_modules(context), qualified_prefix, tables)
|
|
3583
|
+
end
|
|
3584
|
+
|
|
3585
|
+
record_refinement_defs(node, target, qualified_prefix, census_owners(context, owner_prefix), tables)
|
|
3586
|
+
end
|
|
3587
|
+
|
|
3588
|
+
# The modules a `refine` charged to `context` refines for; an anonymous `Module.new` block's is named by its
|
|
3589
|
+
# position (`owner_prefix`).
|
|
3590
|
+
def census_owners(context, owner_prefix)
|
|
3591
|
+
context.here == RefineSelf::ANONYMOUS ? [owner_prefix.join("::")] : RefineSelf.charged_modules(context)
|
|
3592
|
+
end
|
|
3593
|
+
|
|
3594
|
+
# ADR-121 WD7 — a targets-wildcard for each of `owners`, as a `refine` that runs once a method is called records
|
|
3595
|
+
# (an instance `def`, a `define_method` or `define_singleton_method` block; a module's own singleton method
|
|
3596
|
+
# records rows instead, #1749). The body is not walked any further, so the census scans it here.
|
|
3597
|
+
def record_deferred_refine(node, owners, qualified_prefix, tables)
|
|
3598
|
+
tables.account(node, :targets_wildcard, census_charge(owners))
|
|
3599
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3600
|
+
body = node.block.body
|
|
3601
|
+
census_scan(body, qualified_prefix, tables, refine_body: true) if body && tables.mentions_refine?
|
|
3602
|
+
end
|
|
3603
|
+
|
|
3604
|
+
# ADR-121 WD7 — a `refine`-shaped call the canonical arm ({#refine_target}) does not take: on another receiver
|
|
3605
|
+
# (`Module#refine` is private, so it raises), where `self` is a class (#1689's DSL), with no literal block (no
|
|
3606
|
+
# block raises, and so does a Proc passed as one), or with a target the walk cannot name (`refine(k)`,
|
|
3607
|
+
# `refine(self)`, `refine(K.dup)`). The last records the body's names under the wildcard class, so they decline
|
|
3608
|
+
# on every receiver, and a names-wildcard row when they are incomplete, which makes the module opaque; one that
|
|
3609
|
+
# runs once a method is called, or on a module the walk cannot name, records a targets-wildcard. At the top
|
|
3610
|
+
# level `self` is `main`, which has no `refine`.
|
|
3611
|
+
def account_other_refine(node, owner_prefix, tables)
|
|
3612
|
+
context = tables.refine_context(node)
|
|
3613
|
+
outcome, charged =
|
|
3614
|
+
if tables.class_body_refine?(node) || context.here == RefineSelf::CLASS then :dsl
|
|
3615
|
+
elsif !RefineCensus.self_call?(node) || !node.block.is_a?(Prism::BlockNode) ||
|
|
3616
|
+
context.here == RefineSelf::MAIN
|
|
3617
|
+
:raises
|
|
3618
|
+
elsif context.deferred || context.here == REFINEMENT_WILDCARD
|
|
3619
|
+
owners = RefineSelf.charged_modules(context)
|
|
3620
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3621
|
+
[:targets_wildcard, census_charge(owners)]
|
|
3622
|
+
else
|
|
3623
|
+
owners = census_owners(context, owner_prefix)
|
|
3624
|
+
owners.each { |owner| record_unreadable_target(node, owner, tables) }
|
|
3625
|
+
[:class_unknown, census_charge(owners)]
|
|
3626
|
+
end
|
|
3627
|
+
tables.account(node, outcome, charged)
|
|
3628
|
+
end
|
|
3629
|
+
|
|
3630
|
+
def record_unreadable_target(node, owner, tables)
|
|
3631
|
+
tables.refinements = RefineCensus.record_rows(tables.refinements, [REFINEMENT_WILDCARD],
|
|
3632
|
+
RefineCensus.read_body(node.block.body), owner)
|
|
3633
|
+
end
|
|
3634
|
+
|
|
3635
|
+
# `{"*" => {"*" => [owner]}}`: some `refine` of `owner` (every module, for the wildcard) is unreadable.
|
|
3636
|
+
def record_targets_wildcard(tables, owner)
|
|
3637
|
+
tables.refinements = RefineCensus.add_row(tables.refinements || {}, REFINEMENT_WILDCARD, REFINEMENT_WILDCARD,
|
|
3638
|
+
owner)
|
|
3639
|
+
end
|
|
3640
|
+
|
|
3641
|
+
# ADR-121 WD7 — the census over a subtree the method walk does not descend (a `def` body, a refine body), each
|
|
3642
|
+
# node charged where the in-effect walk says it runs. `refine_body` says `self` is the refinement itself, where a
|
|
3643
|
+
# nested `refine` refines for the refinement and only makes the body's names incomplete (§2.2).
|
|
3644
|
+
def census_scan(node, qualified_prefix, tables, refine_body: false)
|
|
3645
|
+
case node
|
|
3646
|
+
when Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
3647
|
+
refine_body = false
|
|
3648
|
+
when Prism::CallNode
|
|
3649
|
+
if (target = own_method_refine_target(node, tables))
|
|
3650
|
+
return record_own_method_refine(node, target, qualified_prefix, tables)
|
|
3651
|
+
end
|
|
3652
|
+
|
|
3653
|
+
census_call(node, qualified_prefix, tables, refine_body)
|
|
3654
|
+
when Prism::SymbolNode
|
|
3655
|
+
census_symbol(node, tables)
|
|
3656
|
+
end
|
|
3657
|
+
node.rigor_each_child { |child| census_scan(child, qualified_prefix, tables, refine_body: refine_body) }
|
|
3658
|
+
end
|
|
3659
|
+
|
|
3660
|
+
# ADR-121 WD7 (issue #1749) — the target of a canonical `refine X do … end` ({#refine_target}) in a module's own
|
|
3661
|
+
# singleton method (`def self.x`, a `def` in the module's `class << self`, or a block there that keeps `self`),
|
|
3662
|
+
# else nil. `self` is the module itself there, so the call refines X for it once the method is called. Anything
|
|
3663
|
+
# else in a method body (an instance `def`, a `define_method` or `define_singleton_method` block, a computed
|
|
3664
|
+
# target, a block that may rebind `self`) charges a targets-wildcard in {#census_call}, and a `refine` in a refine
|
|
3665
|
+
# body is nested there, whichever method it sits in: its context ({RefineSelf.refinement_body}) is not an
|
|
3666
|
+
# own method's.
|
|
3667
|
+
def own_method_refine_target(node, tables)
|
|
3668
|
+
return nil if node.name != :refine
|
|
3669
|
+
|
|
3670
|
+
target = tables.refine_target_of(node)
|
|
3671
|
+
target if target && tables.refine_context(node).own_method
|
|
3672
|
+
end
|
|
3673
|
+
|
|
3674
|
+
# Records `node` as the refine of its module, as a `refine` in the module body is recorded, with the refined
|
|
3675
|
+
# class marked as that one is ({#record_refinement}). The call's block is scanned here, so {#census_scan} does
|
|
3676
|
+
# not descend into it.
|
|
3677
|
+
def record_own_method_refine(node, target, qualified_prefix, tables)
|
|
3678
|
+
record_refinement(node, qualified_prefix, tables)
|
|
3679
|
+
owners = RefineSelf.charged_modules(tables.refine_context(node))
|
|
3680
|
+
record_refinement_defs(node, target, qualified_prefix, owners, tables)
|
|
3681
|
+
end
|
|
3682
|
+
|
|
3683
|
+
def census_call(node, qualified_prefix, tables, refine_body)
|
|
3684
|
+
census_literal_arguments(node, qualified_prefix, tables)
|
|
3685
|
+
return unless node.name == :refine
|
|
3686
|
+
|
|
3687
|
+
context = tables.refine_context(node)
|
|
3688
|
+
nested = refine_body || context.here == RefineSelf::REFINEMENT
|
|
3689
|
+
outcome, charged =
|
|
3690
|
+
if tables.class_body_refine?(node) || context.here == RefineSelf::CLASS then :dsl
|
|
3691
|
+
elsif nested && RefineCensus.self_call?(node) then :nested
|
|
3692
|
+
elsif !RefineCensus.self_call?(node) || node.block.nil? || context.here == RefineSelf::MAIN then :raises
|
|
3693
|
+
else
|
|
3694
|
+
owners = RefineSelf.charged_modules(context)
|
|
3695
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3696
|
+
[:targets_wildcard, census_charge(owners)]
|
|
3198
3697
|
end
|
|
3698
|
+
tables.account(node, outcome, charged)
|
|
3699
|
+
end
|
|
3700
|
+
|
|
3701
|
+
# A3 — a `:refine` Symbol argument of any call, or a String argument holding `refine` of an eval, `send` or
|
|
3702
|
+
# method-naming call, records a targets-wildcard for the module the call runs `refine` on: the receiver of a
|
|
3703
|
+
# `send` or `*_eval` (each name a constant one can denote), else the module the in-effect walk says `self` is
|
|
3704
|
+
# there. Where `self` is a plain class or one of its instances, a literal charged to `self` is data.
|
|
3705
|
+
def census_literal_arguments(node, qualified_prefix, tables)
|
|
3706
|
+
arguments = node.arguments&.arguments
|
|
3707
|
+
return if arguments.nil?
|
|
3708
|
+
|
|
3709
|
+
strings = RefineCensus.string_literal_call?(node)
|
|
3710
|
+
arguments.each do |argument|
|
|
3711
|
+
next unless RefineCensus.refine_symbol?(argument) || (strings && RefineCensus.refine_string?(argument))
|
|
3712
|
+
next if tables.literal_seen?(argument)
|
|
3713
|
+
|
|
3714
|
+
tables.see_literal(argument)
|
|
3715
|
+
census_literal_argument(node, argument, qualified_prefix, tables)
|
|
3199
3716
|
end
|
|
3200
3717
|
end
|
|
3201
3718
|
|
|
3719
|
+
def census_literal_argument(call, literal, qualified_prefix, tables)
|
|
3720
|
+
target = RefineCensus.literal_target(call)
|
|
3721
|
+
context = tables.refine_context(literal)
|
|
3722
|
+
return tables.account(literal, :data) if target == :self && context.literal == RefineSelf::CLASS
|
|
3723
|
+
return tables.account(literal, :nested) if target == :self && context.literal == RefineSelf::REFINEMENT
|
|
3724
|
+
|
|
3725
|
+
owners = literal_owners(call, target, RefineSelf.charged_modules(context, literal: true), qualified_prefix)
|
|
3726
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3727
|
+
tables.account(literal, :literal, target == :self ? census_charge(owners) : [:receiver, owners])
|
|
3728
|
+
end
|
|
3729
|
+
|
|
3730
|
+
def literal_owners(call, target, self_modules, qualified_prefix)
|
|
3731
|
+
case target
|
|
3732
|
+
when :self then self_modules
|
|
3733
|
+
when :unknown then [REFINEMENT_WILDCARD]
|
|
3734
|
+
else
|
|
3735
|
+
receiver = call.receiver
|
|
3736
|
+
names = constant_shaped?(receiver) ? constant_receiver_candidates(receiver, qualified_prefix) : []
|
|
3737
|
+
names.empty? ? [REFINEMENT_WILDCARD] : names
|
|
3738
|
+
end
|
|
3739
|
+
end
|
|
3740
|
+
|
|
3741
|
+
def constant_shaped?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
3742
|
+
|
|
3743
|
+
# A3 — a `:refine` Symbol anywhere else (`alias my_refine refine`, `%i[refine]`, a hash) names `Module#refine`
|
|
3744
|
+
# for code the walk cannot follow, run on the module the in-effect walk says `self` is there; where `self` is a
|
|
3745
|
+
# plain class or one of its instances it is data.
|
|
3746
|
+
def census_symbol(node, tables)
|
|
3747
|
+
return unless RefineCensus.refine_symbol?(node) && !tables.literal_seen?(node)
|
|
3748
|
+
|
|
3749
|
+
tables.see_literal(node)
|
|
3750
|
+
context = tables.refine_context(node)
|
|
3751
|
+
return tables.account(node, :data) if context.literal == RefineSelf::CLASS
|
|
3752
|
+
return tables.account(node, :nested) if context.literal == RefineSelf::REFINEMENT
|
|
3753
|
+
|
|
3754
|
+
owners = RefineSelf.charged_modules(context, literal: true)
|
|
3755
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3756
|
+
tables.account(node, :literal, census_charge(owners))
|
|
3757
|
+
end
|
|
3758
|
+
|
|
3202
3759
|
# The instance `def`s a refine body defines on the refined class: nested declarations open their own
|
|
3203
3760
|
# scope, and a `def self.x` or a `def` inside another `def` defines nothing on it.
|
|
3204
3761
|
def each_refinement_def(node, &)
|
|
@@ -3215,9 +3772,58 @@ module Rigor
|
|
|
3215
3772
|
|
|
3216
3773
|
# The walk's single existence writer. Everything except a `def` passes no envelope and so records
|
|
3217
3774
|
# {Source::ParameterEnvelope::OPAQUE}.
|
|
3775
|
+
#
|
|
3776
|
+
# ADR-119 WD3 — the existence table's `possible` sibling follows the contribution's certainty
|
|
3777
|
+
# ({MethodTables#certain}): a certain contribution removes its kind from the name's possible kind, and a
|
|
3778
|
+
# possible one adds its kind unless the table already answered that kind certainly. A possible contribution
|
|
3779
|
+
# also contests the envelope entry it joins (envelopes are joined, so one possible joiner is enough).
|
|
3218
3780
|
def record_method(tables, class_name, method_name, kind, envelope = Source::ParameterEnvelope::OPAQUE)
|
|
3781
|
+
follow_method_certainty(tables, class_name, method_name, kind) unless tables.certain && tables.possible.nil?
|
|
3219
3782
|
record_method_kind(tables.existence, class_name, method_name, kind)
|
|
3220
3783
|
record_envelope(tables.envelopes, class_name, [kind, method_name], envelope)
|
|
3784
|
+
contest_envelope(tables, class_name, [kind, method_name]) unless tables.certain
|
|
3785
|
+
end
|
|
3786
|
+
|
|
3787
|
+
# The `possible_discovered_methods` half of {#record_method}, read BEFORE the existence table is written.
|
|
3788
|
+
def follow_method_certainty(tables, class_name, method_name, kind)
|
|
3789
|
+
if tables.certain
|
|
3790
|
+
settle_possible_kind(tables.possible, class_name, method_name, kind)
|
|
3791
|
+
else
|
|
3792
|
+
sitting = tables.possible&.dig(class_name, method_name)
|
|
3793
|
+
return if kind_covers?(tables.existence[class_name]&.[](method_name), kind) && !kind_covers?(sitting, kind)
|
|
3794
|
+
|
|
3795
|
+
row = ((tables.possible ||= {})[class_name] ||= {})
|
|
3796
|
+
row[method_name] = merge_sibling_kind(sitting, kind)
|
|
3797
|
+
end
|
|
3798
|
+
end
|
|
3799
|
+
|
|
3800
|
+
# A certain contribution: `kind` leaves the name's possible kind, and an emptied row goes.
|
|
3801
|
+
def settle_possible_kind(possible, class_name, method_name, kind)
|
|
3802
|
+
row = possible[class_name]
|
|
3803
|
+
return if row.nil?
|
|
3804
|
+
|
|
3805
|
+
remaining = without_kind(row[method_name], kind)
|
|
3806
|
+
return if remaining == row[method_name]
|
|
3807
|
+
|
|
3808
|
+
remaining.nil? ? row.delete(method_name) : (row[method_name] = remaining)
|
|
3809
|
+
possible.delete(class_name) if row.empty?
|
|
3810
|
+
end
|
|
3811
|
+
|
|
3812
|
+
# Whether a recorded kind (`:instance`, `:singleton`, `:both`, or nil) answers `kind`.
|
|
3813
|
+
def kind_covers?(recorded, kind)
|
|
3814
|
+
!recorded.nil? && (recorded == kind || recorded == Scope::DiscoveryIndex::METHOD_KIND_BOTH)
|
|
3815
|
+
end
|
|
3816
|
+
|
|
3817
|
+
# `recorded` less `kind`: nil when nothing remains.
|
|
3818
|
+
def without_kind(recorded, kind)
|
|
3819
|
+
return nil if recorded.nil? || recorded == kind
|
|
3820
|
+
return recorded unless recorded == Scope::DiscoveryIndex::METHOD_KIND_BOTH
|
|
3821
|
+
|
|
3822
|
+
kind == :instance ? :singleton : :instance
|
|
3823
|
+
end
|
|
3824
|
+
|
|
3825
|
+
def contest_envelope(tables, class_name, key)
|
|
3826
|
+
(tables.contested_envelopes ||= Set.new) << [class_name, key]
|
|
3221
3827
|
end
|
|
3222
3828
|
|
|
3223
3829
|
def record_envelope(envelopes, class_name, key, envelope)
|
|
@@ -3303,7 +3909,10 @@ module Rigor
|
|
|
3303
3909
|
# `self::` header under a REBOUND self (eval/meta-new body) names
|
|
3304
3910
|
# `owner::Name` instead.
|
|
3305
3911
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
3306
|
-
|
|
3912
|
+
if node.is_a?(Prism::ClassNode) && node.superclass && methods_acc.mentions_refine?
|
|
3913
|
+
census_scan(node.superclass, qualified_prefix, methods_acc)
|
|
3914
|
+
end
|
|
3915
|
+
record_declaration_facts(node, child_prefix, methods_acc.at(node)) unless child_cref
|
|
3307
3916
|
body_prefix = child_cref ? [] : child_prefix
|
|
3308
3917
|
if node.body
|
|
3309
3918
|
walk_methods_and_def_nodes(node.body, body_prefix, false, methods_acc, def_nodes_acc,
|
|
@@ -3319,6 +3928,10 @@ module Rigor
|
|
|
3319
3928
|
# still decline. The lexical cref below is unnameable in every case.
|
|
3320
3929
|
singleton_prefix = singleton_body_prefix(node, in_singleton_class, owner_prefix,
|
|
3321
3930
|
qualified_prefix)
|
|
3931
|
+
tables = methods_acc.at(node)
|
|
3932
|
+
if !node.expression.is_a?(Prism::SelfNode) || tables.in_block?(node)
|
|
3933
|
+
record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
3934
|
+
end
|
|
3322
3935
|
walk_methods_and_def_nodes(node.expression, qualified_prefix, in_singleton_class,
|
|
3323
3936
|
methods_acc, def_nodes_acc, source_path, def_owner_prefix,
|
|
3324
3937
|
singleton_cref: singleton_cref,
|
|
@@ -3336,6 +3949,7 @@ module Rigor
|
|
|
3336
3949
|
return
|
|
3337
3950
|
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
|
|
3338
3951
|
Prism::ConstantPathOrWriteNode
|
|
3952
|
+
record_meta_superclass(node, qualified_prefix, def_owner_prefix, methods_acc.at(node))
|
|
3339
3953
|
if (split = meta_new_block_split(node, qualified_prefix, def_owner_prefix, singleton_cref))
|
|
3340
3954
|
enclosing, body, body_self = split
|
|
3341
3955
|
# A meta-new block rebinds only `self` — `Module.nesting` stays lexical — so
|
|
@@ -3346,7 +3960,9 @@ module Rigor
|
|
|
3346
3960
|
child_prefix = meta_new_child_prefix(node, qualified_prefix, def_owner_prefix)
|
|
3347
3961
|
meta_ownerless = singleton_cref &&
|
|
3348
3962
|
!meta_new_path_target_nameable?(node, def_owner_prefix)
|
|
3349
|
-
|
|
3963
|
+
if child_prefix && !meta_ownerless
|
|
3964
|
+
record_meta_new_facts(meta_new_rvalue(node), child_prefix, methods_acc.at(node))
|
|
3965
|
+
end
|
|
3350
3966
|
enclosing.each do |part|
|
|
3351
3967
|
walk_methods_and_def_nodes(part, qualified_prefix, in_singleton_class, methods_acc,
|
|
3352
3968
|
def_nodes_acc, source_path, def_owner_prefix,
|
|
@@ -3370,34 +3986,48 @@ module Rigor
|
|
|
3370
3986
|
# receiver, a singleton nothing names) or any ownerless prefix under an unnameable
|
|
3371
3987
|
# cref — is NOT top level; `record_def_node` would file the def under `<toplevel>`
|
|
3372
3988
|
# where an implicit-self call could find a method Ruby never installed there.
|
|
3989
|
+
location = node.location
|
|
3990
|
+
if methods_acc.mentions_refine? && methods_acc.refine_in?(location.start_offset, location.end_offset)
|
|
3991
|
+
node.rigor_each_child { |child| census_scan(child, qualified_prefix, methods_acc) }
|
|
3992
|
+
end
|
|
3373
3993
|
unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
|
|
3374
3994
|
(singleton_cref && owner_prefix.empty?)
|
|
3375
3995
|
singleton_def = in_singleton_class || defs_singleton
|
|
3376
|
-
record_def_method(node, owner_prefix, singleton_def, methods_acc)
|
|
3996
|
+
record_def_method(node, owner_prefix, singleton_def, methods_acc.at(node))
|
|
3377
3997
|
record_def_body_evidence(node, owner_prefix, methods_acc)
|
|
3378
|
-
record_def_node(node, owner_prefix, singleton_def, def_nodes_acc)
|
|
3998
|
+
record_def_node(node, owner_prefix, singleton_def, def_nodes_acc, methods_acc)
|
|
3379
3999
|
end
|
|
3380
4000
|
return
|
|
3381
4001
|
when Prism::AliasMethodNode, Prism::UndefNode
|
|
4002
|
+
census_scan(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
|
|
3382
4003
|
unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
|
|
3383
4004
|
(singleton_cref && owner_prefix.empty?)
|
|
3384
4005
|
record_alias_or_undef(node, owner_prefix, in_singleton_class || defs_singleton,
|
|
3385
|
-
methods_acc)
|
|
4006
|
+
methods_acc.at(node))
|
|
3386
4007
|
end
|
|
3387
4008
|
return
|
|
4009
|
+
when Prism::SymbolNode
|
|
4010
|
+
census_symbol(node, methods_acc) if methods_acc.mentions_refine?
|
|
3388
4011
|
when Prism::CallNode
|
|
4012
|
+
census_literal_arguments(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
|
|
3389
4013
|
if receiver_eval_call?(node)
|
|
3390
4014
|
return walk_eval_methods_and_defs(node, qualified_prefix, in_singleton_class, methods_acc,
|
|
3391
4015
|
def_nodes_acc, source_path, def_owner_prefix,
|
|
3392
4016
|
singleton_cref: singleton_cref,
|
|
3393
4017
|
defs_singleton: defs_singleton)
|
|
3394
4018
|
end
|
|
3395
|
-
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc,
|
|
4019
|
+
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
|
|
4020
|
+
source_path)
|
|
4021
|
+
tables = methods_acc.at(node)
|
|
4022
|
+
record_singleton_prepend(node, qualified_prefix, tables,
|
|
4023
|
+
self_ok: node.name == :prepend && tables.in_block?(node))
|
|
3396
4024
|
# Issue #1120 — a refine body's defs go to the refinement table and nowhere else.
|
|
3397
|
-
if (target =
|
|
3398
|
-
return
|
|
4025
|
+
if (target = methods_acc.refine_target_of(node))
|
|
4026
|
+
return record_literal_target_refine(node, target, qualified_prefix, owner_prefix, methods_acc)
|
|
3399
4027
|
end
|
|
3400
4028
|
|
|
4029
|
+
account_other_refine(node, owner_prefix, methods_acc) if node.name == :refine
|
|
4030
|
+
|
|
3401
4031
|
if anonymous
|
|
3402
4032
|
walk_anonymous_meta_block(node, anonymous, qualified_prefix, in_singleton_class, methods_acc,
|
|
3403
4033
|
def_nodes_acc, source_path, def_owner_prefix,
|
|
@@ -3463,8 +4093,24 @@ module Rigor
|
|
|
3463
4093
|
end
|
|
3464
4094
|
|
|
3465
4095
|
# The block form of a meta-new constant write: its members, and the module mark `Module.new` earns.
|
|
4096
|
+
# Issue #1820 — `K = Class.new(Parent)`, with or without a block, names a class whose superclass the superclass
|
|
4097
|
+
# table leaves out; it takes {Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK}.
|
|
4098
|
+
def record_meta_superclass(node, qualified_prefix, self_base, tables)
|
|
4099
|
+
rvalue = meta_new_rvalue(node)
|
|
4100
|
+
return unless class_new_call?(rvalue) && !(rvalue.arguments&.arguments || []).empty?
|
|
4101
|
+
|
|
4102
|
+
child_prefix = meta_new_child_prefix(node, qualified_prefix, self_base)
|
|
4103
|
+
return if child_prefix.nil? || child_prefix.empty?
|
|
4104
|
+
|
|
4105
|
+
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
|
|
4106
|
+
end
|
|
4107
|
+
|
|
3466
4108
|
def record_meta_new_facts(rvalue, child_prefix, tables)
|
|
3467
4109
|
record_meta_members(rvalue, child_prefix, tables)
|
|
4110
|
+
# `Module.new(&BODY)`: the body runs on the new module but cannot be read, so its surface is unknown (#1816).
|
|
4111
|
+
if rvalue.block.is_a?(Prism::BlockArgumentNode)
|
|
4112
|
+
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
4113
|
+
end
|
|
3468
4114
|
return unless module_new_call?(rvalue)
|
|
3469
4115
|
|
|
3470
4116
|
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK)
|
|
@@ -3497,7 +4143,7 @@ module Rigor
|
|
|
3497
4143
|
def record_alias_method_call(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
3498
4144
|
return if qualified_prefix.empty?
|
|
3499
4145
|
|
|
3500
|
-
names = alias_method_call_names(call_node)
|
|
4146
|
+
names = Source::AliasNames.alias_method_call_names(call_node)
|
|
3501
4147
|
return if names.nil?
|
|
3502
4148
|
|
|
3503
4149
|
kind = in_singleton_class ? :singleton : :instance
|
|
@@ -3514,7 +4160,7 @@ module Rigor
|
|
|
3514
4160
|
def walk_anonymous_meta_block(call_node, name, qualified_prefix, in_singleton_class, methods_acc, # rubocop:disable Metrics/ParameterLists
|
|
3515
4161
|
def_nodes_acc, source_path, def_owner_prefix = nil,
|
|
3516
4162
|
singleton_cref: false, defs_singleton: false)
|
|
3517
|
-
record_meta_members(call_node, [name], methods_acc)
|
|
4163
|
+
record_meta_members(call_node, [name], methods_acc.at(call_node))
|
|
3518
4164
|
call_node.rigor_each_child do |child|
|
|
3519
4165
|
if child.equal?(call_node.block)
|
|
3520
4166
|
body = call_node.block.body
|
|
@@ -3604,7 +4250,9 @@ module Rigor
|
|
|
3604
4250
|
# other two. {#meta_new_block_call} owns both unwrappings; every walk asks it, so no spelling can be
|
|
3605
4251
|
# recognised by one pass and missed by another.
|
|
3606
4252
|
def meta_new_block_body(node)
|
|
3607
|
-
meta_new_block_call(node)&.block
|
|
4253
|
+
block = meta_new_block_call(node)&.block
|
|
4254
|
+
# A `&BODY` / `&:sym` block ARGUMENT is not a literal block: its body is unreadable here (#1816).
|
|
4255
|
+
block.body if block.is_a?(Prism::BlockNode)
|
|
3608
4256
|
end
|
|
3609
4257
|
|
|
3610
4258
|
# The class-creating call a constant write's rvalue ultimately is, or nil when the write is not the idiom.
|
|
@@ -3858,16 +4506,57 @@ module Rigor
|
|
|
3858
4506
|
return if def_node.body.nil?
|
|
3859
4507
|
|
|
3860
4508
|
Source::NodeWalker.each(def_node.body) do |node|
|
|
4509
|
+
if node.is_a?(Prism::SingletonClassNode)
|
|
4510
|
+
record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
4511
|
+
next
|
|
4512
|
+
end
|
|
3861
4513
|
next unless node.is_a?(Prism::CallNode)
|
|
3862
4514
|
|
|
3863
4515
|
if node.name == :extend
|
|
3864
4516
|
record_object_extension(node, qualified_prefix, tables)
|
|
3865
4517
|
elsif SURFACE_EVAL_CALLS.include?(node.name) || SURFACE_NAMING_CALLS.include?(node.name)
|
|
3866
4518
|
record_body_rewrite(node, qualified_prefix, tables)
|
|
4519
|
+
else
|
|
4520
|
+
record_singleton_prepend(node, qualified_prefix, tables, self_ok: true)
|
|
3867
4521
|
end
|
|
3868
4522
|
end
|
|
3869
4523
|
end
|
|
3870
4524
|
|
|
4525
|
+
# Issue #1820 — `obj.singleton_class.prepend(M)`, or `prepend M` in a `class << obj` body, puts `M` ahead of
|
|
4526
|
+
# `obj`'s class even when the class already includes it (`prepend` re-inserts it; `extend` would not), so
|
|
4527
|
+
# `M`'s refinement answers first for that object. `M` takes {Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK},
|
|
4528
|
+
# the mark an `extend` in a method body already leaves: an object of any class may carry its instance methods
|
|
4529
|
+
# ahead of its class's own. A `singleton_class` of `self` (and a `class << self` body, which the caller leaves
|
|
4530
|
+
# out) written directly in a class or module body or at the top level is the class object's or `main`'s, not an
|
|
4531
|
+
# instance's; in a method body or any block (`self_ok`), where `self` may be an object, it counts.
|
|
4532
|
+
# `singleton_body` says the call sits in a `class << obj` body.
|
|
4533
|
+
def record_singleton_prepend(node, qualified_prefix, tables, singleton_body: false, self_ok: false)
|
|
4534
|
+
return unless node.name == :prepend
|
|
4535
|
+
|
|
4536
|
+
receiver = node.receiver
|
|
4537
|
+
singleton = singleton_body && (receiver.nil? || receiver.is_a?(Prism::SelfNode))
|
|
4538
|
+
singleton ||= singleton_class_call?(receiver, self_ok)
|
|
4539
|
+
record_object_extension(node, qualified_prefix, tables) if singleton
|
|
4540
|
+
end
|
|
4541
|
+
|
|
4542
|
+
def singleton_class_call?(receiver, self_ok)
|
|
4543
|
+
return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class
|
|
4544
|
+
return false unless receiver.arguments.nil?
|
|
4545
|
+
|
|
4546
|
+
object = receiver.receiver
|
|
4547
|
+
self_ok || !(object.nil? || object.is_a?(Prism::SelfNode))
|
|
4548
|
+
end
|
|
4549
|
+
|
|
4550
|
+
def record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
4551
|
+
return if node.body.nil?
|
|
4552
|
+
|
|
4553
|
+
Source::NodeWalker.each(node.body) do |inner|
|
|
4554
|
+
next unless inner.is_a?(Prism::CallNode)
|
|
4555
|
+
|
|
4556
|
+
record_singleton_prepend(inner, qualified_prefix, tables, singleton_body: true, self_ok: true)
|
|
4557
|
+
end
|
|
4558
|
+
end
|
|
4559
|
+
|
|
3871
4560
|
def record_body_rewrite(node, qualified_prefix, tables)
|
|
3872
4561
|
receiver = node.receiver
|
|
3873
4562
|
if receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
@@ -3928,13 +4617,30 @@ module Rigor
|
|
|
3928
4617
|
# same-named local method.
|
|
3929
4618
|
TOP_LEVEL_DEF_KEY = "<toplevel>"
|
|
3930
4619
|
|
|
3931
|
-
|
|
4620
|
+
#
|
|
4621
|
+
# ADR-119 WD3 — the slot follows its writer (later-wins): a certain write clears the key from
|
|
4622
|
+
# `contested_discovered_def_nodes` and a possible one adds it ({MethodTables#certain}, set for `def_node`).
|
|
4623
|
+
def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator, tables = nil)
|
|
3932
4624
|
return if def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
3933
4625
|
|
|
3934
4626
|
class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
|
|
3935
4627
|
accumulator[class_name] ||= {}
|
|
3936
4628
|
accumulator[class_name][def_node.name] = def_node
|
|
3937
|
-
|
|
4629
|
+
follow_slot_writer(tables, :contested_def_nodes, [class_name, def_node.name], tables.nil? || tables.certain)
|
|
4630
|
+
record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator, tables)
|
|
4631
|
+
end
|
|
4632
|
+
|
|
4633
|
+
# ADR-119 WD3 — a single-valued slot's contest follows the write that holds it: `certain` clears `key` from
|
|
4634
|
+
# the `slot` Set of `tables`, a possible write adds it. Nil `tables` (a caller with no certainty) is a no-op.
|
|
4635
|
+
def follow_slot_writer(tables, slot, key, certain)
|
|
4636
|
+
return if tables.nil?
|
|
4637
|
+
|
|
4638
|
+
if certain
|
|
4639
|
+
set = tables[slot]
|
|
4640
|
+
set&.delete(key)
|
|
4641
|
+
else
|
|
4642
|
+
(tables[slot] ||= Set.new) << key
|
|
4643
|
+
end
|
|
3938
4644
|
end
|
|
3939
4645
|
|
|
3940
4646
|
# #319 — a `def` inside an anonymous `Class.new` / `Module.new` body ALSO stays in the `<toplevel>` table.
|
|
@@ -3946,12 +4652,16 @@ module Rigor
|
|
|
3946
4652
|
# `spawn(...) { start }` block. Giving the body a class of its own must not silently retract it: the call
|
|
3947
4653
|
# resolves at runtime, and `call.unresolved-toplevel` firing on it would be a new false positive traded
|
|
3948
4654
|
# for the ones this change retires. Never clobbers a real top-level `def` of the same name.
|
|
3949
|
-
def record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator)
|
|
4655
|
+
def record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator, tables = nil)
|
|
3950
4656
|
return unless qualified_prefix.length == 1
|
|
3951
4657
|
return unless Type::AnonymousClassName.match?(qualified_prefix.first)
|
|
3952
4658
|
|
|
3953
4659
|
table = (accumulator[TOP_LEVEL_DEF_KEY] ||= {})
|
|
3954
|
-
table
|
|
4660
|
+
return if table.key?(def_node.name)
|
|
4661
|
+
|
|
4662
|
+
table[def_node.name] = def_node
|
|
4663
|
+
follow_slot_writer(tables, :contested_def_nodes, [TOP_LEVEL_DEF_KEY, def_node.name],
|
|
4664
|
+
tables.nil? || tables.certain)
|
|
3955
4665
|
end
|
|
3956
4666
|
|
|
3957
4667
|
# Module-singleton call resolution (ADR-57 follow-up) — the SINGLETON-side mirror of `build_discovered_def_nodes`.
|
|
@@ -3963,9 +4673,43 @@ module Rigor
|
|
|
3963
4673
|
# singleton defs (`def self.x` outside any class — `self` is `main`) are not recorded; they have no constant
|
|
3964
4674
|
# receiver to dispatch through.
|
|
3965
4675
|
def build_discovered_singleton_def_nodes(root)
|
|
3966
|
-
|
|
4676
|
+
build_singleton_def_tables(root).first
|
|
4677
|
+
end
|
|
4678
|
+
|
|
4679
|
+
# ADR-119 WD3 — {#build_discovered_singleton_def_nodes} with its sibling: `[table,
|
|
4680
|
+
# contested_discovered_singleton_def_nodes]`, the second a frozen Set of `[class, name]` keys whose slot a
|
|
4681
|
+
# possible `def self.x`, `class << self` def or `module_function` copy wrote last ({Certainty} `certainty`).
|
|
4682
|
+
def build_singleton_def_tables(root, certainty: Certainty.possible_nodes(root))
|
|
4683
|
+
accumulator = SingletonDefTable.new.track(certainty)
|
|
3967
4684
|
walk_singleton_def_nodes(root, [], false, accumulator)
|
|
3968
|
-
accumulator.transform_values(&:freeze).freeze
|
|
4685
|
+
[accumulator.transform_values(&:freeze).freeze, accumulator.contested_paths]
|
|
4686
|
+
end
|
|
4687
|
+
|
|
4688
|
+
# The singleton walk's accumulator: the `{class => {name => DefNode}}` table, plus the contested slots, which
|
|
4689
|
+
# follow each slot's writer ({#follow_slot_writer}'s rule).
|
|
4690
|
+
class SingletonDefTable < Hash
|
|
4691
|
+
def track(certainty)
|
|
4692
|
+
@certainty = certainty
|
|
4693
|
+
@contested = nil
|
|
4694
|
+
self
|
|
4695
|
+
end
|
|
4696
|
+
|
|
4697
|
+
# Writes the slot; `node` and `other` are the contributions the write rests on, possible when either is.
|
|
4698
|
+
def write(class_name, name, def_node, node, other = nil)
|
|
4699
|
+
(self[class_name] ||= {})[name] = def_node
|
|
4700
|
+
if Certainty.possible?(@certainty, node) || (other && Certainty.possible?(@certainty, other))
|
|
4701
|
+
(@contested ||= Set.new) << [class_name, name]
|
|
4702
|
+
else
|
|
4703
|
+
@contested&.delete([class_name, name])
|
|
4704
|
+
end
|
|
4705
|
+
end
|
|
4706
|
+
|
|
4707
|
+
def contested_paths
|
|
4708
|
+
contested = @contested
|
|
4709
|
+
return contested.each(&:freeze).freeze if contested && !contested.empty?
|
|
4710
|
+
|
|
4711
|
+
Scope::DiscoveryIndex::EMPTY_SIBLINGS[:contested_discovered_singleton_def_nodes]
|
|
4712
|
+
end
|
|
3969
4713
|
end
|
|
3970
4714
|
|
|
3971
4715
|
# Issue #1097 — `[[start_offset, end_offset, name, kind, owner], ...]` for every `def` / block /
|
|
@@ -4004,12 +4748,14 @@ module Rigor
|
|
|
4004
4748
|
defs_singleton: false)
|
|
4005
4749
|
return unless node.is_a?(Prism::Node)
|
|
4006
4750
|
|
|
4007
|
-
|
|
4751
|
+
# Tested by exact class (Prism's node classes have no subclasses): the three `any?` scans this
|
|
4752
|
+
# replaced ran a block per list member at every node ([#1507](https://github.com/rigortype/rigor/issues/1507)).
|
|
4753
|
+
if CLASS_BODY_NODES.include?(node.class)
|
|
4008
4754
|
return walk_deferred_lexical_body(node, qualified_prefix, in_singleton_class,
|
|
4009
4755
|
inside_deferred, mf_offsets, ranges, def_owner_prefix,
|
|
4010
4756
|
singleton_cref: singleton_cref)
|
|
4011
4757
|
end
|
|
4012
|
-
if META_WRITE_NODES.
|
|
4758
|
+
if META_WRITE_NODES.include?(node.class)
|
|
4013
4759
|
return walk_deferred_meta_new(node, qualified_prefix, in_singleton_class, inside_deferred,
|
|
4014
4760
|
mf_offsets, ranges, def_owner_prefix,
|
|
4015
4761
|
singleton_cref: singleton_cref)
|
|
@@ -4019,7 +4765,7 @@ module Rigor
|
|
|
4019
4765
|
mf_offsets, ranges, def_owner_prefix, singleton_cref,
|
|
4020
4766
|
defs_singleton)
|
|
4021
4767
|
end
|
|
4022
|
-
if DEFERRED_RANGE_NODES.
|
|
4768
|
+
if DEFERRED_RANGE_NODES.include?(node.class)
|
|
4023
4769
|
# Deferred: a call inside runs at invocation / interpreter-exit time. `BEGIN`
|
|
4024
4770
|
# (PreExecutionNode) is the opposite — eager, before the class body — and stays unlisted.
|
|
4025
4771
|
ranges << [node.location.start_offset, node.location.end_offset, nil, nil, nil]
|
|
@@ -4052,10 +4798,10 @@ module Rigor
|
|
|
4052
4798
|
defs_singleton: defs_singleton)
|
|
4053
4799
|
end
|
|
4054
4800
|
|
|
4055
|
-
DEFERRED_RANGE_NODES = [Prism::BlockNode, Prism::LambdaNode, Prism::PostExecutionNode].freeze
|
|
4056
|
-
META_WRITE_NODES = [Prism::ConstantWriteNode, Prism::ConstantPathWriteNode,
|
|
4057
|
-
|
|
4058
|
-
CLASS_BODY_NODES = [Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
4801
|
+
DEFERRED_RANGE_NODES = Set[Prism::BlockNode, Prism::LambdaNode, Prism::PostExecutionNode].freeze
|
|
4802
|
+
META_WRITE_NODES = Set[Prism::ConstantWriteNode, Prism::ConstantPathWriteNode,
|
|
4803
|
+
Prism::ConstantOrWriteNode, Prism::ConstantPathOrWriteNode].freeze
|
|
4804
|
+
CLASS_BODY_NODES = Set[Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
4059
4805
|
private_constant :DEFERRED_RANGE_NODES, :META_WRITE_NODES, :CLASS_BODY_NODES
|
|
4060
4806
|
|
|
4061
4807
|
def walk_deferred_children(node, qualified_prefix, in_singleton_class, inside_deferred, # rubocop:disable Metrics/ParameterLists
|
|
@@ -4156,7 +4902,7 @@ module Rigor
|
|
|
4156
4902
|
end
|
|
4157
4903
|
|
|
4158
4904
|
# Body-level entry for a class / module / `class <<` / meta-`new` / eval-block body: prescans
|
|
4159
|
-
# the body's `module_function` state first ({
|
|
4905
|
+
# the body's `module_function` state first ({ModuleFunctionState.prescan_deferred} — bare-call
|
|
4160
4906
|
# offsets plus the retro-install and `module_function def x` rows), then walks each statement
|
|
4161
4907
|
# with the offsets in hand. The prescan looks through nested containers (`if` / `begin` /
|
|
4162
4908
|
# `rescue` / blocks) because a `module_function` that RAN there still flips later defs —
|
|
@@ -4173,8 +4919,8 @@ module Rigor
|
|
|
4173
4919
|
# `module_function` under an unnameable definee (`class <<` + `instance_eval`)
|
|
4174
4920
|
# toggles the metaclass — its rows must not name the enclosing class.
|
|
4175
4921
|
prescan_owner = defs_singleton == :unnameable ? EMPTY_PREFIX : owner_prefix
|
|
4176
|
-
|
|
4177
|
-
|
|
4922
|
+
ModuleFunctionState.prescan_deferred(body, prescan_owner, in_singleton_class, mf_offsets,
|
|
4923
|
+
ranges)
|
|
4178
4924
|
end
|
|
4179
4925
|
statements_of(body).each do |stmt|
|
|
4180
4926
|
walk_deferred_ranges(stmt, qualified_prefix, in_singleton_class, inside_deferred,
|
|
@@ -4183,49 +4929,6 @@ module Rigor
|
|
|
4183
4929
|
end
|
|
4184
4930
|
end
|
|
4185
4931
|
|
|
4186
|
-
# The `module_function` prescan over one body's subtree: bare-call offsets (defs starting after
|
|
4187
|
-
# one are module functions), a `:singleton` row at each `module_function :name` call — the
|
|
4188
|
-
# retro-install happens AT THE CALL, not the def — and a `:both` / `:singleton` row for a
|
|
4189
|
-
# `module_function def x` argument. Nested class / module / `class <<` / def bodies are skipped:
|
|
4190
|
-
# `module_function` there targets a different module. An `END` body is skipped too — it runs at
|
|
4191
|
-
# interpreter exit, after every def — and so is a `*_eval` / `*_exec` block, whose `self`
|
|
4192
|
-
# rebinds to the receiver's module. Blocks, lambdas and control-flow containers are entered —
|
|
4193
|
-
# their `self` is still this module, so the call really can toggle.
|
|
4194
|
-
def collect_module_function_state(node, qualified_prefix, in_singleton_class, mf_offsets, ranges)
|
|
4195
|
-
return unless node.is_a?(Prism::Node)
|
|
4196
|
-
return if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) ||
|
|
4197
|
-
node.is_a?(Prism::SingletonClassNode) || node.is_a?(Prism::DefNode) ||
|
|
4198
|
-
node.is_a?(Prism::PostExecutionNode)
|
|
4199
|
-
|
|
4200
|
-
if node.is_a?(Prism::CallNode)
|
|
4201
|
-
if module_function_toggle?(node)
|
|
4202
|
-
return collect_module_function_call(node, qualified_prefix, in_singleton_class,
|
|
4203
|
-
mf_offsets, ranges)
|
|
4204
|
-
end
|
|
4205
|
-
if receiver_eval_call?(node)
|
|
4206
|
-
return collect_eval_call_state(node, qualified_prefix, in_singleton_class, mf_offsets,
|
|
4207
|
-
ranges)
|
|
4208
|
-
end
|
|
4209
|
-
end
|
|
4210
|
-
|
|
4211
|
-
node.rigor_each_child do |child|
|
|
4212
|
-
collect_module_function_state(child, qualified_prefix, in_singleton_class, mf_offsets, ranges)
|
|
4213
|
-
end
|
|
4214
|
-
end
|
|
4215
|
-
|
|
4216
|
-
# The prescan's eval arm: a `*_eval` / `*_exec` block's self rebinds to the receiver's module,
|
|
4217
|
-
# so only the call's receiver and arguments keep this module's `module_function` context.
|
|
4218
|
-
def collect_eval_call_state(node, qualified_prefix, in_singleton_class, mf_offsets, ranges)
|
|
4219
|
-
if node.receiver
|
|
4220
|
-
collect_module_function_state(node.receiver, qualified_prefix, in_singleton_class,
|
|
4221
|
-
mf_offsets, ranges)
|
|
4222
|
-
end
|
|
4223
|
-
node.arguments&.arguments&.each do |arg|
|
|
4224
|
-
collect_module_function_state(arg, qualified_prefix, in_singleton_class, mf_offsets,
|
|
4225
|
-
ranges)
|
|
4226
|
-
end
|
|
4227
|
-
end
|
|
4228
|
-
|
|
4229
4932
|
# The wider form: any call whose block rebinds `self` to the receiver — `instance_eval` and
|
|
4230
4933
|
# `instance_exec` included. Walks that read `self`-anchored facts use this gate; walks that
|
|
4231
4934
|
# own `def` leaves additionally split on {INSTANCE_EVAL_CALLS}, whose `def`s bind on the
|
|
@@ -4234,29 +4937,6 @@ module Rigor
|
|
|
4234
4937
|
node.block.is_a?(Prism::BlockNode) && RECEIVER_EVAL_CALLS.include?(node.name)
|
|
4235
4938
|
end
|
|
4236
4939
|
|
|
4237
|
-
def collect_module_function_call(node, qualified_prefix, in_singleton_class, mf_offsets, ranges)
|
|
4238
|
-
if bare_module_function?(node)
|
|
4239
|
-
mf_offsets << node.location.start_offset
|
|
4240
|
-
else
|
|
4241
|
-
owner = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
|
|
4242
|
-
node.arguments&.arguments&.each do |arg|
|
|
4243
|
-
if arg.is_a?(Prism::DefNode)
|
|
4244
|
-
kind = def_singleton?(arg, qualified_prefix, in_singleton_class) ? :singleton : :both
|
|
4245
|
-
ranges << [arg.location.start_offset, arg.location.end_offset, arg.name, kind, owner]
|
|
4246
|
-
elsif (name = symbol_argument_name(arg))
|
|
4247
|
-
ranges << [node.location.start_offset, node.location.end_offset, name, :singleton, owner]
|
|
4248
|
-
else
|
|
4249
|
-
collect_module_function_state(arg, qualified_prefix, in_singleton_class, mf_offsets,
|
|
4250
|
-
ranges)
|
|
4251
|
-
end
|
|
4252
|
-
end
|
|
4253
|
-
end
|
|
4254
|
-
return unless node.block
|
|
4255
|
-
|
|
4256
|
-
collect_module_function_state(node.block, qualified_prefix, in_singleton_class, mf_offsets,
|
|
4257
|
-
ranges)
|
|
4258
|
-
end
|
|
4259
|
-
|
|
4260
4940
|
# A `class_eval` / `module_eval` / `class_exec` / `module_exec` block is NOT deferred: it runs
|
|
4261
4941
|
# eagerly during the receiver's call, as a class-ish body — its `module_function` state gets
|
|
4262
4942
|
# its own prescan, and its defs belong to the RECEIVER's surface. A nameable receiver gives
|
|
@@ -4620,7 +5300,7 @@ module Rigor
|
|
|
4620
5300
|
|
|
4621
5301
|
kind = if def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
4622
5302
|
:singleton
|
|
4623
|
-
elsif
|
|
5303
|
+
elsif ModuleFunctionState.deferred_module_function?(mf_offsets, start)
|
|
4624
5304
|
:both
|
|
4625
5305
|
else
|
|
4626
5306
|
:instance
|
|
@@ -4773,12 +5453,27 @@ module Rigor
|
|
|
4773
5453
|
# CompactBase` calls it, and the bare `Post` inside `make` names `::Post` — the constant the compact
|
|
4774
5454
|
# declaration that OWNS the body reaches, not the one the receiver's spelling would suggest.
|
|
4775
5455
|
#
|
|
4776
|
-
#
|
|
4777
|
-
#
|
|
4778
|
-
#
|
|
4779
|
-
#
|
|
4780
|
-
#
|
|
5456
|
+
# Not a leaf of the fused methods/def-nodes walk: it needs the CHAIN threaded, which the fused walk does
|
|
5457
|
+
# not carry (a singleton-class body and a `Class.new` block body both push a qualified prefix while
|
|
5458
|
+
# pushing no `Module.nesting` entry). {DefNestingsCollector} declines at every `Prism::DefNode`, so a
|
|
5459
|
+
# def-dense file pays only the declaration spine.
|
|
5460
|
+
#
|
|
5461
|
+
# ADR-116 WD5 — production builds this table in {#declaration_walk_tables}' shared run, and nothing in
|
|
5462
|
+
# `lib` calls this builder. It runs the collector alone and stays for the specs, which compare it with
|
|
5463
|
+
# the shared run; `RIGOR_SHADOW_RULE_WALK` checks it against {#legacy_def_nestings}.
|
|
4781
5464
|
def build_def_nestings(root)
|
|
5465
|
+
collector = DefNestingsCollector.new
|
|
5466
|
+
DeclarationWalk.run(root, [collector], superclass_walk_root(nil))
|
|
5467
|
+
verified_def_nestings(root, collector.table)
|
|
5468
|
+
end
|
|
5469
|
+
|
|
5470
|
+
def verified_def_nestings(root, table, source_path = nil)
|
|
5471
|
+
DeclarationWalk::Shadow.verified(:def_nestings, source_path, table) { legacy_def_nestings(root) }
|
|
5472
|
+
end
|
|
5473
|
+
|
|
5474
|
+
# The walker {DefNestingsCollector} replaced, kept as the shadow harness's oracle until ADR-116 WD5 has
|
|
5475
|
+
# ported every table walker onto {DeclarationWalk}.
|
|
5476
|
+
def legacy_def_nestings(root)
|
|
4782
5477
|
accumulator = {}.compare_by_identity
|
|
4783
5478
|
walk_def_nestings(root, EMPTY_NESTING, accumulator)
|
|
4784
5479
|
accumulator.freeze
|
|
@@ -4904,24 +5599,21 @@ module Rigor
|
|
|
4904
5599
|
end
|
|
4905
5600
|
|
|
4906
5601
|
# Walks a class/module/singleton-class body's direct statements in source order, threading the
|
|
4907
|
-
# bare-`module_function` toggle: once a bare
|
|
4908
|
-
#
|
|
4909
|
-
#
|
|
5602
|
+
# bare-`module_function` toggle ({ModuleFunctionState.each_singleton_sibling}): once a bare
|
|
5603
|
+
# `module_function` is seen, every subsequent `def` in the body registers as a singleton method. Nested
|
|
5604
|
+
# classes/modules/defs recurse through the general walker so the toggle stays scoped to its own body, and a
|
|
5605
|
+
# `module_function :a, :b` named form records through {#record_module_function_names}.
|
|
4910
5606
|
def walk_singleton_body(body, qualified_prefix, in_singleton_class, accumulator,
|
|
4911
5607
|
def_owner_prefix = nil, singleton_cref: false,
|
|
4912
5608
|
defs_singleton: false)
|
|
4913
5609
|
owner_prefix = def_owner_prefix || qualified_prefix
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
if
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
# which this table cannot name either.
|
|
4922
|
-
record_module_function_names(stmt, owner_prefix, body, accumulator) unless
|
|
4923
|
-
defs_singleton == :unnameable
|
|
4924
|
-
end
|
|
5610
|
+
statements = statements_of(body)
|
|
5611
|
+
ModuleFunctionState.each_singleton_sibling(statements) do |stmt, module_function_on, named_call|
|
|
5612
|
+
if named_call
|
|
5613
|
+
# `:unnameable` — the named defs the call copies live on the metaclass,
|
|
5614
|
+
# which this table cannot name either.
|
|
5615
|
+
record_module_function_names(stmt, owner_prefix, statements, accumulator) unless
|
|
5616
|
+
defs_singleton == :unnameable
|
|
4925
5617
|
next
|
|
4926
5618
|
end
|
|
4927
5619
|
if stmt.is_a?(Prism::DefNode)
|
|
@@ -4972,42 +5664,30 @@ module Rigor
|
|
|
4972
5664
|
return if qualified_prefix.empty?
|
|
4973
5665
|
|
|
4974
5666
|
class_name = qualified_prefix.join("::")
|
|
4975
|
-
(accumulator
|
|
4976
|
-
end
|
|
4977
|
-
|
|
4978
|
-
# A bare `module_function` (no arguments) flips every following `def` in the module body to module-function
|
|
4979
|
-
# (instance + singleton) mode.
|
|
4980
|
-
def module_function_toggle?(node)
|
|
4981
|
-
node.name == :module_function && node.receiver.nil?
|
|
5667
|
+
write_singleton_slot(accumulator, class_name, def_node.name, def_node, def_node)
|
|
4982
5668
|
end
|
|
4983
5669
|
|
|
4984
|
-
|
|
4985
|
-
|
|
5670
|
+
# One singleton slot write, through {SingletonDefTable#write} when the walk tracks certainty.
|
|
5671
|
+
def write_singleton_slot(accumulator, class_name, name, def_node, node, other = nil)
|
|
5672
|
+
if accumulator.is_a?(SingletonDefTable)
|
|
5673
|
+
accumulator.write(class_name, name, def_node, node, other)
|
|
5674
|
+
else
|
|
5675
|
+
(accumulator[class_name] ||= {})[name] = def_node
|
|
5676
|
+
end
|
|
4986
5677
|
end
|
|
4987
5678
|
|
|
4988
|
-
# `module_function :a, :b` retro-marks named siblings
|
|
4989
|
-
#
|
|
4990
|
-
#
|
|
4991
|
-
|
|
4992
|
-
def record_module_function_names(node, qualified_prefix, body, accumulator)
|
|
5679
|
+
# `module_function :a, :b` retro-marks named siblings as module-functions: registers on the module's singleton
|
|
5680
|
+
# side each `DefNode` {ModuleFunctionState.each_singleton_copy} resolves among the body's direct `statements`.
|
|
5681
|
+
# Non-symbol arguments and names with no matching `def` are skipped (a miss degrades to today's `Dynamic`).
|
|
5682
|
+
def record_module_function_names(node, qualified_prefix, statements, accumulator)
|
|
4993
5683
|
return if qualified_prefix.empty?
|
|
4994
5684
|
|
|
4995
|
-
defs_by_name = statements_of(body).each_with_object({}) do |stmt, acc|
|
|
4996
|
-
acc[stmt.name] = stmt if stmt.is_a?(Prism::DefNode) && stmt.receiver.nil?
|
|
4997
|
-
end
|
|
4998
5685
|
class_name = qualified_prefix.join("::")
|
|
4999
|
-
node
|
|
5000
|
-
name
|
|
5001
|
-
def_node = name && defs_by_name[name]
|
|
5002
|
-
(accumulator[class_name] ||= {})[name] = def_node if def_node
|
|
5686
|
+
ModuleFunctionState.each_singleton_copy(node, statements) do |name, def_node|
|
|
5687
|
+
write_singleton_slot(accumulator, class_name, name, def_node, node, def_node)
|
|
5003
5688
|
end
|
|
5004
5689
|
end
|
|
5005
5690
|
|
|
5006
|
-
# The Symbol value of a `:name` / `"name"` literal argument, or nil.
|
|
5007
|
-
def symbol_argument_name(arg)
|
|
5008
|
-
arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
|
|
5009
|
-
end
|
|
5010
|
-
|
|
5011
5691
|
# ADR-24 slice 2 — per-class table mapping a fully qualified user class to its superclass name AS WRITTEN at the
|
|
5012
5692
|
# `class Foo < Bar` declaration. Only constant superclasses are recorded (`class Foo < Struct.new(...)` and other
|
|
5013
5693
|
# non-constant superclasses produce no entry). The as-written name is resolved to a qualified class at the call
|
|
@@ -5021,13 +5701,57 @@ module Rigor
|
|
|
5021
5701
|
# the enclosing cref — `[]` for the first spelling, `["Admin"]` for the second. A class reopened under two
|
|
5022
5702
|
# spellings keeps the LAST one walked, matching how `superclasses` itself merges.
|
|
5023
5703
|
#
|
|
5704
|
+
# ADR-116 WD5 — built by {SuperclassesCollector} on the shared {DeclarationWalk}, following the two
|
|
5705
|
+
# legacy variants it declares; `RIGOR_SHADOW_RULE_WALK` checks both tables against
|
|
5706
|
+
# {#legacy_superclass_tables}. Production builds both in {#declaration_walk_tables}' shared run, and
|
|
5707
|
+
# nothing in `lib` calls this builder: it runs the collector alone and stays for the specs, which
|
|
5708
|
+
# compare it with the shared run.
|
|
5709
|
+
#
|
|
5024
5710
|
# @return the `[superclasses, header_nestings]` pair
|
|
5025
5711
|
def build_superclass_tables(root, source_path = nil)
|
|
5026
|
-
|
|
5712
|
+
collector = SuperclassesCollector.new
|
|
5713
|
+
DeclarationWalk.run(root, [collector], superclass_walk_root(source_path))
|
|
5714
|
+
verified_superclass_tables(root, source_path, collector.tables)
|
|
5715
|
+
end
|
|
5716
|
+
|
|
5717
|
+
# The root context the superclass tables are walked from: the ancestry nesting starts empty and the
|
|
5718
|
+
# anonymous-class key carries the file's path.
|
|
5719
|
+
def superclass_walk_root(source_path, scope: nil)
|
|
5720
|
+
DeclarationWalk::Context.root(scope: scope, nesting: EMPTY_NESTING, source_path: source_path)
|
|
5721
|
+
end
|
|
5722
|
+
|
|
5723
|
+
# `tables` once the shadow harness has compared them with the legacy walker's, when it is on.
|
|
5724
|
+
def verified_superclass_tables(root, source_path, tables)
|
|
5725
|
+
DeclarationWalk::Shadow.verified(:superclass_tables, source_path, tables) do
|
|
5726
|
+
legacy_superclass_tables(root, source_path)
|
|
5727
|
+
end
|
|
5728
|
+
end
|
|
5729
|
+
|
|
5730
|
+
# The walker {SuperclassesCollector} replaced, kept as the shadow harness's oracle until ADR-116 WD5 has
|
|
5731
|
+
# ported every table walker onto {DeclarationWalk}.
|
|
5732
|
+
def legacy_superclass_tables(root, source_path = nil)
|
|
5733
|
+
accumulator = { superclasses: {}, header_nestings: {} }
|
|
5027
5734
|
walk_class_superclasses(root, [], accumulator, source_path)
|
|
5028
5735
|
[accumulator[:superclasses].freeze, accumulator[:header_nestings].freeze]
|
|
5029
5736
|
end
|
|
5030
5737
|
|
|
5738
|
+
# ADR-116 WD5 — the file's superclass, header-nesting, def-nesting and member-layout tables from ONE
|
|
5739
|
+
# shared {DeclarationWalk}: the file is descended once for all of them, where the legacy walkers
|
|
5740
|
+
# descended it once per table. `RIGOR_SHADOW_RULE_WALK` checks each table against its legacy walker.
|
|
5741
|
+
#
|
|
5742
|
+
# @return `{ superclasses:, header_nestings:, def_nestings:, data_member_layouts:, struct_member_layouts: }`
|
|
5743
|
+
def declaration_walk_tables(root, source_path = nil)
|
|
5744
|
+
superclasses = SuperclassesCollector.new
|
|
5745
|
+
layouts = MemberLayoutsCollector.new
|
|
5746
|
+
def_nestings = DefNestingsCollector.new
|
|
5747
|
+
DeclarationWalk.run(root, [superclasses, layouts, def_nestings], superclass_walk_root(source_path))
|
|
5748
|
+
supers, header_nestings = verified_superclass_tables(root, source_path, superclasses.tables)
|
|
5749
|
+
data, struct = verified_member_layouts(root, layouts.tables, source_path)
|
|
5750
|
+
{ superclasses: supers, header_nestings: header_nestings,
|
|
5751
|
+
def_nestings: verified_def_nestings(root, def_nestings.table, source_path),
|
|
5752
|
+
data_member_layouts: data, struct_member_layouts: struct }
|
|
5753
|
+
end
|
|
5754
|
+
|
|
5031
5755
|
# `self_base` names a rebound `self` for `self::`-anchored headers — a meta-new
|
|
5032
5756
|
# block's class; `[]` marks a self no name covers.
|
|
5033
5757
|
def walk_class_superclasses(node, qualified_prefix, accumulator, source_path = nil,
|
|
@@ -5342,7 +6066,31 @@ module Rigor
|
|
|
5342
6066
|
# `Data.define` is recorded: `Struct.new` instances are mutable, so member-value folding would be unsound (the
|
|
5343
6067
|
# Struct follow-up is deferred — see ADR-48 § "Struct follow-up"). Consumed by
|
|
5344
6068
|
# {Inference::MethodDispatcher::DataFolding} via {Scope#data_member_layout}.
|
|
6069
|
+
#
|
|
6070
|
+
# ADR-116 WD5 — built with its Struct sibling by {MemberLayoutsCollector}. Production builds both in
|
|
6071
|
+
# {#declaration_walk_tables}' shared run, and nothing in `lib` calls this builder: it stays for the specs,
|
|
6072
|
+
# and walks both layout tables to return this one.
|
|
5345
6073
|
def build_data_member_layouts(root)
|
|
6074
|
+
member_layout_tables(root).first
|
|
6075
|
+
end
|
|
6076
|
+
|
|
6077
|
+
# `[data_member_layouts, struct_member_layouts]` from a {MemberLayoutsCollector} run of its own, checked by
|
|
6078
|
+
# `RIGOR_SHADOW_RULE_WALK` against the two legacy walkers. Spec-only, like the two builders on it.
|
|
6079
|
+
def member_layout_tables(root)
|
|
6080
|
+
collector = MemberLayoutsCollector.new
|
|
6081
|
+
DeclarationWalk.run(root, [collector], superclass_walk_root(nil))
|
|
6082
|
+
verified_member_layouts(root, collector.tables)
|
|
6083
|
+
end
|
|
6084
|
+
|
|
6085
|
+
def verified_member_layouts(root, tables, source_path = nil)
|
|
6086
|
+
DeclarationWalk::Shadow.verified(:member_layouts, source_path, tables) do
|
|
6087
|
+
[legacy_data_member_layouts(root), legacy_struct_member_layouts(root)]
|
|
6088
|
+
end
|
|
6089
|
+
end
|
|
6090
|
+
|
|
6091
|
+
# The walker {MemberLayoutsCollector} replaced for the `Data.define` table, kept as the shadow harness's
|
|
6092
|
+
# oracle until ADR-116 WD5 has ported every table walker onto {DeclarationWalk}.
|
|
6093
|
+
def legacy_data_member_layouts(root)
|
|
5346
6094
|
accumulator = {}
|
|
5347
6095
|
walk_data_member_layouts(root, [], accumulator)
|
|
5348
6096
|
accumulator.freeze
|
|
@@ -5445,7 +6193,16 @@ module Rigor
|
|
|
5445
6193
|
# table so the existing `Data.define` value-shape contract (a bare `[Symbol]`) is untouched: a Struct entry
|
|
5446
6194
|
# carries `{ members:, keyword_init: }` because the dispatcher needs the flag to fold the matching `.new` call
|
|
5447
6195
|
# form (positional vs keyword) without manufacturing a wrong map.
|
|
6196
|
+
#
|
|
6197
|
+
# ADR-116 WD5 — production builds it in {#declaration_walk_tables}' shared run; this builder is spec-only,
|
|
6198
|
+
# like {#build_data_member_layouts}.
|
|
5448
6199
|
def build_struct_member_layouts(root)
|
|
6200
|
+
member_layout_tables(root).last
|
|
6201
|
+
end
|
|
6202
|
+
|
|
6203
|
+
# The walker {MemberLayoutsCollector} replaced for the `Struct.new` table, kept as the shadow harness's
|
|
6204
|
+
# oracle until ADR-116 WD5 has ported every table walker onto {DeclarationWalk}.
|
|
6205
|
+
def legacy_struct_member_layouts(root)
|
|
5449
6206
|
accumulator = {}
|
|
5450
6207
|
walk_struct_member_layouts(root, [], accumulator)
|
|
5451
6208
|
accumulator.freeze
|
|
@@ -5589,7 +6346,7 @@ module Rigor
|
|
|
5589
6346
|
# stored in instance-ancestor SEARCH order: a later `prepend` statement puts its module NEARER than
|
|
5590
6347
|
# an earlier one (`prepend A; prepend B` searches B before A) while the arguments of ONE
|
|
5591
6348
|
# `prepend A, B` keep call order — exactly the convention {#record_extend_targets} documents for
|
|
5592
|
-
# `extend`, and the order `Scope
|
|
6349
|
+
# `extend`, and the order `Scope::ResolutionChain` reads a body's prepends in. Ruby inserts a
|
|
5593
6350
|
# prepended module, and its own ancestry, immediately BEFORE the class that prepends it, so these
|
|
5594
6351
|
# names outrank the class's own `def`s; without the kind recorded separately from {#build_discovered_includes}
|
|
5595
6352
|
# there was nothing to order on and `prepend` was silently an `include`.
|
|
@@ -5599,13 +6356,17 @@ module Rigor
|
|
|
5599
6356
|
|
|
5600
6357
|
# One descent, both instance-side mixin tables: the walk classifies each mixin call it sees, so the
|
|
5601
6358
|
# two tables cost one walk rather than two. Each value is frozen and de-duplicated per class, as the
|
|
5602
|
-
# single-table builder always did.
|
|
6359
|
+
# single-table builder always did. `:unpositioned` names the recorded edges whose order is not a fact
|
|
6360
|
+
# ({MixinAccumulator}).
|
|
5603
6361
|
def mixin_tables(root)
|
|
5604
|
-
accumulator =
|
|
6362
|
+
accumulator = MixinAccumulator.new
|
|
6363
|
+
accumulator.direct_body(program_statements(root))
|
|
6364
|
+
accumulator.program_body(program_statements(root))
|
|
5605
6365
|
walk_class_includes(root, [], nil, accumulator)
|
|
5606
6366
|
{
|
|
5607
6367
|
includes: freeze_mixin_lists(accumulator, :include),
|
|
5608
|
-
prepends: freeze_mixin_lists(accumulator, :prepend)
|
|
6368
|
+
prepends: freeze_mixin_lists(accumulator, :prepend),
|
|
6369
|
+
unpositioned: accumulator.unpositioned
|
|
5609
6370
|
}
|
|
5610
6371
|
end
|
|
5611
6372
|
|
|
@@ -5620,7 +6381,10 @@ module Rigor
|
|
|
5620
6381
|
# already store nearest-first (see {#write_mixin_targets}), so the concat preserves MRO.
|
|
5621
6382
|
def freeze_mixin_lists(accumulator, kind)
|
|
5622
6383
|
accumulator.each_with_object({}) do |(class_name, kinds), out|
|
|
5623
|
-
names = mixin_names_for(kinds, kind)
|
|
6384
|
+
names = mixin_names_for(kinds, kind)
|
|
6385
|
+
# The instance-side projection is a SET in search order; the prepend table keeps every statement.
|
|
6386
|
+
names = names.uniq if kind == :include
|
|
6387
|
+
names = names.freeze
|
|
5624
6388
|
out[class_name] = names unless names.empty?
|
|
5625
6389
|
end.freeze
|
|
5626
6390
|
end
|
|
@@ -5635,6 +6399,12 @@ module Rigor
|
|
|
5635
6399
|
end
|
|
5636
6400
|
end
|
|
5637
6401
|
|
|
6402
|
+
# The top-level statements of a parsed file: the one body whose declarations need no enclosing
|
|
6403
|
+
# declaration to be direct ({MixinAccumulator}).
|
|
6404
|
+
def program_statements(root)
|
|
6405
|
+
root.is_a?(Prism::ProgramNode) ? root.statements : root
|
|
6406
|
+
end
|
|
6407
|
+
|
|
5638
6408
|
def walk_class_includes(node, qualified_prefix, current_class, accumulator,
|
|
5639
6409
|
singleton_self: false, singleton_cref: false)
|
|
5640
6410
|
return unless node.is_a?(Prism::Node)
|
|
@@ -5665,6 +6435,25 @@ module Rigor
|
|
|
5665
6435
|
singleton_self, singleton_cref)
|
|
5666
6436
|
end
|
|
5667
6437
|
|
|
6438
|
+
walk_includes_children(node, qualified_prefix, current_class, accumulator, singleton_self, singleton_cref)
|
|
6439
|
+
end
|
|
6440
|
+
|
|
6441
|
+
# The generic descent of {#walk_class_includes}; a `self.included(base)`-style hook's body walks with
|
|
6442
|
+
# its parameters in scope ({MixinAccumulator#with_hook_params}).
|
|
6443
|
+
def walk_includes_children(node, qualified_prefix, current_class, accumulator, singleton_self, singleton_cref)
|
|
6444
|
+
hook_params = hook_def_params(node)
|
|
6445
|
+
if hook_params
|
|
6446
|
+
return accumulator.with_hook_params(hook_params) do
|
|
6447
|
+
walk_includes_children_plain(node, qualified_prefix, current_class, accumulator, singleton_self,
|
|
6448
|
+
singleton_cref)
|
|
6449
|
+
end
|
|
6450
|
+
end
|
|
6451
|
+
|
|
6452
|
+
walk_includes_children_plain(node, qualified_prefix, current_class, accumulator, singleton_self, singleton_cref)
|
|
6453
|
+
end
|
|
6454
|
+
|
|
6455
|
+
def walk_includes_children_plain(node, qualified_prefix, current_class, accumulator, singleton_self,
|
|
6456
|
+
singleton_cref)
|
|
5668
6457
|
node.rigor_each_child do |child|
|
|
5669
6458
|
walk_class_includes(child, qualified_prefix, current_class, accumulator,
|
|
5670
6459
|
singleton_self: singleton_self, singleton_cref: singleton_cref)
|
|
@@ -5682,6 +6471,7 @@ module Rigor
|
|
|
5682
6471
|
return true unless node.body
|
|
5683
6472
|
|
|
5684
6473
|
_self_decl, child_prefix, child_cref = ctx
|
|
6474
|
+
accumulator.direct_body(node.body) if accumulator.direct?(node)
|
|
5685
6475
|
walk_class_includes(node.body, child_cref ? [] : child_prefix,
|
|
5686
6476
|
child_cref ? nil : child_prefix.join("::"), accumulator,
|
|
5687
6477
|
singleton_cref: child_cref)
|
|
@@ -5726,7 +6516,7 @@ module Rigor
|
|
|
5726
6516
|
walk_class_includes(arg, qualified_prefix, current_class, accumulator,
|
|
5727
6517
|
singleton_self: singleton_self, singleton_cref: singleton_cref)
|
|
5728
6518
|
end
|
|
5729
|
-
if (body =
|
|
6519
|
+
if (body = meta_new_block_body(node))
|
|
5730
6520
|
walk_class_includes(body, qualified_prefix, meta_owner, accumulator,
|
|
5731
6521
|
singleton_cref: body_cref)
|
|
5732
6522
|
end
|
|
@@ -5752,7 +6542,7 @@ module Rigor
|
|
|
5752
6542
|
# names, so it declines rather than re-anchoring to the lexical class.
|
|
5753
6543
|
unnameable_self = singleton_self || (singleton_cref && current_class.nil?)
|
|
5754
6544
|
rebound = rebound_block_self(node, qualified_prefix, nil, nil,
|
|
5755
|
-
|
|
6545
|
+
unnameable_self ? OPAQUE_SELF : current_class)
|
|
5756
6546
|
node.rigor_each_child do |child|
|
|
5757
6547
|
rebinds_self = rebound && child.is_a?(Prism::BlockNode)
|
|
5758
6548
|
owner =
|
|
@@ -5776,15 +6566,151 @@ module Rigor
|
|
|
5776
6566
|
# it or the set-shaped consumers (arity, visibility, undefined-method suppression) would answer
|
|
5777
6567
|
# differently for the two spellings of one edge.
|
|
5778
6568
|
def record_mixin_call(node, qualified_prefix, current_class, accumulator)
|
|
5779
|
-
|
|
6569
|
+
taint_opaque_eval(node, current_class, accumulator)
|
|
6570
|
+
kind, _arguments, via_send = mixin_call_view(node)
|
|
6571
|
+
return if kind.nil?
|
|
6572
|
+
|
|
6573
|
+
current_class ||= toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
6574
|
+
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
6575
|
+
.select { |_owner, side| side == :include }
|
|
6576
|
+
return if effects.empty?
|
|
6577
|
+
return if !via_send && instance_mixin_recorded?(node, qualified_prefix, current_class, accumulator)
|
|
6578
|
+
|
|
6579
|
+
# A mixin call the tables cannot hold still reshapes the ancestry: taint the whole owner side.
|
|
6580
|
+
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
6581
|
+
end
|
|
5780
6582
|
|
|
5781
|
-
|
|
5782
|
-
|
|
6583
|
+
# Records the instance-side edges of a call {#mixin_call_recorded?} accepts. False when it recorded
|
|
6584
|
+
# nothing — the caller then taints the owner instead.
|
|
6585
|
+
def instance_mixin_recorded?(node, qualified_prefix, current_class, accumulator)
|
|
6586
|
+
return false unless mixin_call_recorded?(node, current_class)
|
|
6587
|
+
|
|
6588
|
+
arguments = node.arguments&.arguments || []
|
|
6589
|
+
targets = arguments.filter_map { |arg| Source::ConstantPath.qualified_name(arg) }
|
|
6590
|
+
return false if targets.empty?
|
|
5783
6591
|
|
|
5784
6592
|
owner = node.receiver.nil? ? current_class : prepend_call_receiver(node, qualified_prefix)
|
|
5785
|
-
return if owner.nil?
|
|
6593
|
+
return false if owner.nil?
|
|
5786
6594
|
|
|
5787
6595
|
write_mixin_targets(accumulator, owner, targets, prepend: node.name == :prepend)
|
|
6596
|
+
accumulator.note(node, owner, :include, targets, complete: targets.size == arguments.size)
|
|
6597
|
+
true
|
|
6598
|
+
end
|
|
6599
|
+
|
|
6600
|
+
MIXIN_KINDS = %i[include prepend extend].freeze
|
|
6601
|
+
SEND_CALL_NAMES = %i[send public_send __send__].freeze
|
|
6602
|
+
private_constant :MIXIN_KINDS, :SEND_CALL_NAMES
|
|
6603
|
+
|
|
6604
|
+
# `[kind, arguments, via_send]` for a call that mixes modules in — `include` / `prepend` / `extend`, or
|
|
6605
|
+
# `send(:include, M)` and its `public_send` / `__send__` twins with a literal first argument — else nil.
|
|
6606
|
+
# A computed `send` name is not read as a mixin: it names nothing this walk could taint by.
|
|
6607
|
+
def mixin_call_view(node)
|
|
6608
|
+
return [node.name, node.arguments&.arguments || [], false] if MIXIN_KINDS.include?(node.name)
|
|
6609
|
+
return unless SEND_CALL_NAMES.include?(node.name)
|
|
6610
|
+
|
|
6611
|
+
arguments = node.arguments&.arguments || []
|
|
6612
|
+
first = arguments.first
|
|
6613
|
+
return unless first.is_a?(Prism::SymbolNode) || first.is_a?(Prism::StringNode)
|
|
6614
|
+
|
|
6615
|
+
# By string: a name literal need not be a valid symbol in its encoding.
|
|
6616
|
+
kind = MIXIN_KINDS.find { |candidate| candidate.name == first.unescaped }
|
|
6617
|
+
[kind, arguments.drop(1), true] if kind
|
|
6618
|
+
end
|
|
6619
|
+
|
|
6620
|
+
# `[[owner, side], …]` — the ancestry sides a mixin call reshapes. `self` and the implicit receiver
|
|
6621
|
+
# are the enclosing class (`:include` for `include` / `prepend`, `:extend` for `extend`, and both
|
|
6622
|
+
# flip to `:extend` inside `class << self`); `singleton_class` is the singleton side; a constant is
|
|
6623
|
+
# that class; any other receiver is unknown, so it may be the class that includes this one from a
|
|
6624
|
+
# hook (`base.include M`) and taints both sides of the enclosing class.
|
|
6625
|
+
def mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton:)
|
|
6626
|
+
receiver = node.receiver
|
|
6627
|
+
side = kind == :extend ? :extend : :include
|
|
6628
|
+
pairs =
|
|
6629
|
+
if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
6630
|
+
own_mixin_effects(kind, side, current_class, in_singleton)
|
|
6631
|
+
else
|
|
6632
|
+
receiver_mixin_effects(node, side, qualified_prefix, current_class, accumulator)
|
|
6633
|
+
end
|
|
6634
|
+
pairs.reject { |owner, _side| owner.nil? }
|
|
6635
|
+
end
|
|
6636
|
+
|
|
6637
|
+
# The implicit-receiver / `self` arm of {#mixin_effects}: inside `class << self`, `include` / `prepend`
|
|
6638
|
+
# are singleton-side and `extend` reaches the singleton's own singleton, which nothing names.
|
|
6639
|
+
def own_mixin_effects(kind, side, current_class, in_singleton)
|
|
6640
|
+
return [] if in_singleton && kind == :extend
|
|
6641
|
+
|
|
6642
|
+
[[current_class, in_singleton ? :extend : side]]
|
|
6643
|
+
end
|
|
6644
|
+
|
|
6645
|
+
# A receiver that is not `self` reshapes the class it names, or the singleton of `singleton_class`. Any
|
|
6646
|
+
# other receiver is an object of its own (`ids.prepend(1)`, `obj.extend(Decorator)`) and reshapes none of
|
|
6647
|
+
# the enclosing class's ancestry — except in a `self.included(base)`-style hook, where `base` IS the
|
|
6648
|
+
# class that includes the enclosing module and a call on it taints both sides of the enclosing class.
|
|
6649
|
+
def receiver_mixin_effects(node, side, qualified_prefix, current_class, accumulator)
|
|
6650
|
+
receiver = node.receiver
|
|
6651
|
+
return [[current_class, :extend]] if singleton_class_receiver?(receiver)
|
|
6652
|
+
|
|
6653
|
+
owner = prepend_call_receiver(node, qualified_prefix)
|
|
6654
|
+
return [[owner, side]] if owner
|
|
6655
|
+
return [] unless hook_receiver?(receiver, accumulator)
|
|
6656
|
+
|
|
6657
|
+
[[current_class, :include], [current_class, :extend]]
|
|
6658
|
+
end
|
|
6659
|
+
|
|
6660
|
+
HOOK_DEFS = %i[included extended prepended inherited].freeze
|
|
6661
|
+
MIXIN_EVAL_CALLS = %i[class_eval module_eval class_exec module_exec instance_eval instance_exec].freeze
|
|
6662
|
+
private_constant :HOOK_DEFS, :MIXIN_EVAL_CALLS
|
|
6663
|
+
|
|
6664
|
+
# The parameter names of a `def included(base)`-style hook (a hook is written `def self.included`, or
|
|
6665
|
+
# `def included` inside `class << self`), else nil.
|
|
6666
|
+
def hook_def_params(node)
|
|
6667
|
+
return nil unless node.is_a?(Prism::DefNode) && HOOK_DEFS.include?(node.name)
|
|
6668
|
+
|
|
6669
|
+
parameters = node.parameters
|
|
6670
|
+
return [] unless parameters
|
|
6671
|
+
|
|
6672
|
+
(parameters.requireds + parameters.optionals).filter_map do |parameter|
|
|
6673
|
+
parameter.name if parameter.respond_to?(:name)
|
|
6674
|
+
end
|
|
6675
|
+
end
|
|
6676
|
+
|
|
6677
|
+
# Whether `receiver` is a hook parameter, or `singleton_class` / a no-argument call chain off one.
|
|
6678
|
+
def hook_receiver?(receiver, accumulator)
|
|
6679
|
+
case receiver
|
|
6680
|
+
when Prism::LocalVariableReadNode then accumulator.hook_local?(receiver.name)
|
|
6681
|
+
when Prism::CallNode
|
|
6682
|
+
receiver.arguments.nil? && receiver.receiver && hook_receiver?(receiver.receiver, accumulator)
|
|
6683
|
+
else false
|
|
6684
|
+
end
|
|
6685
|
+
end
|
|
6686
|
+
|
|
6687
|
+
# `base.class_eval { include X }` in a hook, and `opaque.module_eval { include B }` anywhere: the block
|
|
6688
|
+
# opens a class this walk cannot name, so its mixin calls record nowhere, yet the enclosing class's
|
|
6689
|
+
# ancestry is what the reader was asked about. Taint both sides of the enclosing class. Outside a hook
|
|
6690
|
+
# the block is scanned for a mixin call first, so an ordinary `obj.instance_eval { … }` DSL taints nothing.
|
|
6691
|
+
def taint_opaque_eval(node, current_class, accumulator)
|
|
6692
|
+
return unless current_class && node.block.is_a?(Prism::BlockNode) && MIXIN_EVAL_CALLS.include?(node.name)
|
|
6693
|
+
|
|
6694
|
+
receiver = node.receiver
|
|
6695
|
+
return if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
6696
|
+
return if Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
6697
|
+
return unless hook_receiver?(receiver, accumulator) || block_mixes_in?(node.block)
|
|
6698
|
+
|
|
6699
|
+
accumulator.taint(current_class, :include)
|
|
6700
|
+
accumulator.taint(current_class, :extend)
|
|
6701
|
+
end
|
|
6702
|
+
|
|
6703
|
+
def block_mixes_in?(node)
|
|
6704
|
+
return true if node.is_a?(Prism::CallNode) && (MIXIN_KINDS.include?(node.name) || mixin_call_view(node))
|
|
6705
|
+
|
|
6706
|
+
found = false
|
|
6707
|
+
node.rigor_each_child { |child| found ||= block_mixes_in?(child) }
|
|
6708
|
+
found
|
|
6709
|
+
end
|
|
6710
|
+
|
|
6711
|
+
def singleton_class_receiver?(receiver)
|
|
6712
|
+
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil? &&
|
|
6713
|
+
(receiver.receiver.nil? || receiver.receiver.is_a?(Prism::SelfNode))
|
|
5788
6714
|
end
|
|
5789
6715
|
|
|
5790
6716
|
# Issue #1123 — one class's contribution to the two tables. A prepended module lands in BOTH: the
|
|
@@ -5802,12 +6728,15 @@ module Rigor
|
|
|
5802
6728
|
# freeze time — ahead of every include, where Ruby puts them.
|
|
5803
6729
|
def write_mixin_targets(accumulator, owner, targets, prepend:)
|
|
5804
6730
|
bucket = accumulator[owner] ||= {}
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
6731
|
+
kind, other = prepend ? %i[prepend include] : %i[include prepend]
|
|
6732
|
+
list = (bucket[kind] ||= [])
|
|
6733
|
+
# A target the OTHER kind already carries (`include M; prepend M`) is one body the two tables cannot
|
|
6734
|
+
# represent (Ruby's `[M, C, M]`), so the instance side is named unpositioned.
|
|
6735
|
+
accumulator.taint(owner, :include) if targets.any? { |target| bucket[other]&.include?(target) }
|
|
6736
|
+
# A repeated `include` keeps its first position (Ruby skips it). A repeated `prepend` is KEPT: the
|
|
6737
|
+
# prepend table holds every statement, nearest first, so the readers that walk the raw list see what
|
|
6738
|
+
# they always saw, and the resolution chain dedupes per owner, first statement winning.
|
|
6739
|
+
list.unshift(*(prepend ? targets : targets.reject { |target| list.include?(target) }))
|
|
5811
6740
|
end
|
|
5812
6741
|
|
|
5813
6742
|
# Whether a mixin call contributes to the tables at all: a receiverless `include` / `prepend` needs an
|
|
@@ -5822,6 +6751,25 @@ module Rigor
|
|
|
5822
6751
|
node.name == :prepend
|
|
5823
6752
|
end
|
|
5824
6753
|
|
|
6754
|
+
# Issue #1697 — a receiverless `include M` written as a statement of the file's own top level is
|
|
6755
|
+
# `main.include`, which is `Object.include`: it mixes M into `Object`. It is recorded under its own
|
|
6756
|
+
# owner key, `ObjectMixins::TOPLEVEL_INCLUDE_KEY`, rather than on `Object`, so it rides every table an include
|
|
6757
|
+
# rides (the declaration signature, `discovered_class_sources`, the seed bundle) while no ancestry
|
|
6758
|
+
# walk that types a call reaches it: what Ruby reaches first for a name such an include supplies
|
|
6759
|
+
# depends on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def`
|
|
6760
|
+
# on `main`), so only the diagnostic silences read it (`Inference::ObjectMixins`), and typing is
|
|
6761
|
+
# issue #1715's. Only a direct program statement counts.
|
|
6762
|
+
# The same call inside a top-level block, a method body or a conditional stays unrecorded, as before:
|
|
6763
|
+
# its `self` may not be `main` (a `describe do include M end` or `class_exec` block rebinds it, while
|
|
6764
|
+
# `[1].each do include M end` does not, and the walk cannot tell them apart), and it may not run. `main`
|
|
6765
|
+
# has no `prepend`, and a top-level `extend M` reaches `main`'s singleton alone, so neither is recorded.
|
|
6766
|
+
def toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
6767
|
+
return nil unless current_class.nil? && qualified_prefix.empty?
|
|
6768
|
+
return nil unless node.receiver.nil? && node.name == :include
|
|
6769
|
+
|
|
6770
|
+
accumulator.program_statement?(node) ? ObjectMixins::TOPLEVEL_INCLUDE_KEY : nil
|
|
6771
|
+
end
|
|
6772
|
+
|
|
5825
6773
|
# Issue #1123 — the class a `Recv.prepend(M)` call form targets, as the qualified name the prepend
|
|
5826
6774
|
# table is keyed by, or nil when the receiver names no static class. The receiver is a constant READ
|
|
5827
6775
|
# at the call site, so it resolves in the `Module.nesting` the call is written in — the same rule (and
|
|
@@ -5851,9 +6799,19 @@ module Rigor
|
|
|
5851
6799
|
# Issue #915 — plus `class << self; include M; end`, the same singleton ancestor spelled through the
|
|
5852
6800
|
# singleton-class body. It folds like an `extend` because Ruby makes it one.
|
|
5853
6801
|
def build_discovered_extends(root)
|
|
5854
|
-
|
|
6802
|
+
extend_tables(root).fetch(:extends)
|
|
6803
|
+
end
|
|
6804
|
+
|
|
6805
|
+
# {#build_discovered_extends} and the `extend` edges whose order is not a fact ({MixinAccumulator}), from
|
|
6806
|
+
# one descent.
|
|
6807
|
+
def extend_tables(root)
|
|
6808
|
+
accumulator = MixinAccumulator.new
|
|
6809
|
+
accumulator.direct_body(program_statements(root))
|
|
5855
6810
|
walk_class_extends(root, [], nil, accumulator)
|
|
5856
|
-
|
|
6811
|
+
{
|
|
6812
|
+
extends: accumulator.transform_values { |mods| mods.uniq.freeze }.freeze,
|
|
6813
|
+
unpositioned: accumulator.unpositioned
|
|
6814
|
+
}
|
|
5857
6815
|
end
|
|
5858
6816
|
|
|
5859
6817
|
def walk_class_extends(node, qualified_prefix, current_class, accumulator, in_singleton: false,
|
|
@@ -5880,18 +6838,40 @@ module Rigor
|
|
|
5880
6838
|
return if walk_extends_meta_new?(node, qualified_prefix, current_class, accumulator,
|
|
5881
6839
|
in_singleton, singleton_self, singleton_cref)
|
|
5882
6840
|
when Prism::CallNode
|
|
5883
|
-
record_extend_call(node, current_class, accumulator, in_singleton: in_singleton)
|
|
6841
|
+
record_extend_call(node, qualified_prefix, current_class, accumulator, in_singleton: in_singleton)
|
|
5884
6842
|
if receiver_eval_call?(node)
|
|
5885
6843
|
return walk_eval_extends_call(node, qualified_prefix, current_class, accumulator,
|
|
5886
6844
|
in_singleton: in_singleton, singleton_self: singleton_self,
|
|
5887
6845
|
singleton_cref: singleton_cref)
|
|
5888
6846
|
end
|
|
5889
|
-
if node.block.is_a?(Prism::BlockNode)
|
|
5890
|
-
|
|
5891
|
-
|
|
6847
|
+
if node.block.is_a?(Prism::BlockNode) &&
|
|
6848
|
+
extends_block_handled?(node, qualified_prefix, current_class, accumulator,
|
|
6849
|
+
in_singleton, singleton_self, singleton_cref)
|
|
6850
|
+
return
|
|
6851
|
+
end
|
|
6852
|
+
end
|
|
6853
|
+
|
|
6854
|
+
walk_extends_children(node, qualified_prefix, current_class, accumulator, in_singleton, singleton_self,
|
|
6855
|
+
singleton_cref)
|
|
6856
|
+
end
|
|
6857
|
+
|
|
6858
|
+
# The generic descent of {#walk_class_extends}; a hook's body walks with its parameters in scope.
|
|
6859
|
+
def walk_extends_children(node, qualified_prefix, current_class, accumulator, in_singleton, singleton_self,
|
|
6860
|
+
singleton_cref)
|
|
6861
|
+
hook_params = hook_def_params(node)
|
|
6862
|
+
if hook_params
|
|
6863
|
+
return accumulator.with_hook_params(hook_params) do
|
|
6864
|
+
walk_extends_children_plain(node, qualified_prefix, current_class, accumulator, in_singleton,
|
|
6865
|
+
singleton_self, singleton_cref)
|
|
5892
6866
|
end
|
|
5893
6867
|
end
|
|
5894
6868
|
|
|
6869
|
+
walk_extends_children_plain(node, qualified_prefix, current_class, accumulator, in_singleton, singleton_self,
|
|
6870
|
+
singleton_cref)
|
|
6871
|
+
end
|
|
6872
|
+
|
|
6873
|
+
def walk_extends_children_plain(node, qualified_prefix, current_class, accumulator, in_singleton,
|
|
6874
|
+
singleton_self, singleton_cref)
|
|
5895
6875
|
node.rigor_each_child do |child|
|
|
5896
6876
|
walk_class_extends(child, qualified_prefix, current_class, accumulator,
|
|
5897
6877
|
in_singleton: in_singleton, singleton_self: singleton_self,
|
|
@@ -5915,6 +6895,7 @@ module Rigor
|
|
|
5915
6895
|
# singleton body, `self` IS the singleton and `class << self` opens the
|
|
5916
6896
|
# singleton's own singleton (`#<Class:#<Class:C>>`), which nothing names.
|
|
5917
6897
|
opens_self = node.expression.is_a?(Prism::SelfNode) && !in_singleton
|
|
6898
|
+
accumulator.direct_body(node.body) if opens_self && accumulator.direct?(node)
|
|
5918
6899
|
walk_class_extends(node.body, qualified_prefix, opens_self ? current_class : nil,
|
|
5919
6900
|
accumulator, in_singleton: opens_self,
|
|
5920
6901
|
singleton_self: true, singleton_cref: true)
|
|
@@ -5931,6 +6912,8 @@ module Rigor
|
|
|
5931
6912
|
return unless child_prefix && node.body
|
|
5932
6913
|
|
|
5933
6914
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
6915
|
+
accumulator.direct_body(node.body) if accumulator.direct?(node)
|
|
6916
|
+
accumulator.body_statements(node.body)
|
|
5934
6917
|
walk_class_extends(node.body, child_cref ? [] : child_prefix,
|
|
5935
6918
|
child_cref ? nil : child_prefix.join("::"), accumulator,
|
|
5936
6919
|
singleton_cref: child_cref)
|
|
@@ -5954,6 +6937,110 @@ module Rigor
|
|
|
5954
6937
|
in_singleton, singleton_self, singleton_cref)
|
|
5955
6938
|
end
|
|
5956
6939
|
|
|
6940
|
+
# What the extends walk does with a block-carrying call: `:rebound` (its own eval / `define_method` / meta-new
|
|
6941
|
+
# handling), `:skip` (the block runs on an object this walk cannot vouch for, as an instance's `synchronize`
|
|
6942
|
+
# or an `on_load` hook, or may never run: `Registry.each { … }`), or `:descend` (it provably runs once with the
|
|
6943
|
+
# body's `self`, #1592, and its direct statements are positioned when the call itself is).
|
|
6944
|
+
def extends_block_plan(node, accumulator)
|
|
6945
|
+
return :rebound if rebinding_extends_block?(node)
|
|
6946
|
+
return :skip unless runs_body_block_once?(node, accumulator)
|
|
6947
|
+
|
|
6948
|
+
accumulator.direct_body(node.block.body) if accumulator.direct?(node)
|
|
6949
|
+
:descend
|
|
6950
|
+
end
|
|
6951
|
+
|
|
6952
|
+
# True when {#extends_block_plan} settled the block (walked as rebound, or skipped); false leaves it to the
|
|
6953
|
+
# generic descent.
|
|
6954
|
+
def extends_block_handled?(node, qualified_prefix, current_class, accumulator, in_singleton, singleton_self,
|
|
6955
|
+
singleton_cref)
|
|
6956
|
+
case extends_block_plan(node, accumulator)
|
|
6957
|
+
when :skip then true
|
|
6958
|
+
when :rebound
|
|
6959
|
+
walk_extends_block_call(node, qualified_prefix, current_class, accumulator,
|
|
6960
|
+
in_singleton, singleton_self, singleton_cref)
|
|
6961
|
+
true
|
|
6962
|
+
else false
|
|
6963
|
+
end
|
|
6964
|
+
end
|
|
6965
|
+
|
|
6966
|
+
ARRAY_ITERATORS = %i[each each_with_index map].freeze
|
|
6967
|
+
HASH_ITERATORS = %i[each each_pair each_key each_value map].freeze
|
|
6968
|
+
private_constant :ARRAY_ITERATORS, :HASH_ITERATORS
|
|
6969
|
+
|
|
6970
|
+
# A block that provably runs at least once with the class body's `self`: the call is a statement of the
|
|
6971
|
+
# body itself (not nested in a `def` or another block) and either `tap` / `then` on a literal (they
|
|
6972
|
+
# always yield once, self preserved) or an iterator over a literal that is known non-empty (a
|
|
6973
|
+
# non-empty Array or Hash literal, a positive `Integer#times`, an Integer-literal Range that is not empty,
|
|
6974
|
+
# `upto` / `downto` in the right order). A constant or variable receiver (`Registry.each`, a lazy `map`) may
|
|
6975
|
+
# never yield, so it is not one. The walk then records the block's direct statements as positioned: the
|
|
6976
|
+
# block runs where it is written, and a repeated `extend` is idempotent (#1592).
|
|
6977
|
+
def runs_body_block_once?(node, accumulator)
|
|
6978
|
+
return false unless accumulator.body_block?(node)
|
|
6979
|
+
return literal_or_self?(node.receiver) if %i[tap then].include?(node.name)
|
|
6980
|
+
|
|
6981
|
+
non_empty_literal_iteration?(node.name, node.receiver, node.arguments&.arguments || [])
|
|
6982
|
+
end
|
|
6983
|
+
|
|
6984
|
+
TAP_RECEIVERS = [Prism::IntegerNode, Prism::FloatNode, Prism::StringNode, Prism::SymbolNode, Prism::ArrayNode,
|
|
6985
|
+
Prism::HashNode, Prism::RangeNode, Prism::NilNode, Prism::TrueNode, Prism::FalseNode].freeze
|
|
6986
|
+
private_constant :TAP_RECEIVERS
|
|
6987
|
+
|
|
6988
|
+
# `tap` / `then` yield once only on a receiver whose method nobody overrode: a literal. A constant or `self`
|
|
6989
|
+
# may define its own `tap` (`def self.tap`), a promise's `then` defers, and a receiverless call may reach a
|
|
6990
|
+
# class-level DSL method, so none of those is read as running.
|
|
6991
|
+
def literal_or_self?(receiver)
|
|
6992
|
+
TAP_RECEIVERS.any? { |kind| receiver.is_a?(kind) }
|
|
6993
|
+
end
|
|
6994
|
+
|
|
6995
|
+
def non_empty_literal_iteration?(name, receiver, arguments)
|
|
6996
|
+
case receiver
|
|
6997
|
+
when Prism::ArrayNode
|
|
6998
|
+
ARRAY_ITERATORS.include?(name) && !receiver.elements.empty? &&
|
|
6999
|
+
receiver.elements.none?(Prism::SplatNode)
|
|
7000
|
+
when Prism::HashNode
|
|
7001
|
+
HASH_ITERATORS.include?(name) && !receiver.elements.empty? &&
|
|
7002
|
+
receiver.elements.all?(Prism::AssocNode)
|
|
7003
|
+
when Prism::IntegerNode then integer_iteration_runs?(name, receiver.value, arguments)
|
|
7004
|
+
when Prism::ParenthesesNode then non_empty_range_iteration?(name, receiver, arguments)
|
|
7005
|
+
else false
|
|
7006
|
+
end
|
|
7007
|
+
end
|
|
7008
|
+
|
|
7009
|
+
def integer_iteration_runs?(name, value, arguments)
|
|
7010
|
+
limit = arguments.first
|
|
7011
|
+
case name
|
|
7012
|
+
when :times then arguments.empty? && value.positive?
|
|
7013
|
+
when :upto then arguments.size == 1 && limit.is_a?(Prism::IntegerNode) && value <= limit.value
|
|
7014
|
+
when :downto then arguments.size == 1 && limit.is_a?(Prism::IntegerNode) && value >= limit.value
|
|
7015
|
+
else false
|
|
7016
|
+
end
|
|
7017
|
+
end
|
|
7018
|
+
|
|
7019
|
+
def positive_integer_literal?(node) = node.is_a?(Prism::IntegerNode) && node.value.positive?
|
|
7020
|
+
|
|
7021
|
+
# `(1..3).each` and `(1..3).map`, and `(1..3).step(2)` with a positive literal step.
|
|
7022
|
+
def non_empty_range_iteration?(name, parens, arguments)
|
|
7023
|
+
range = parens.body.is_a?(Prism::StatementsNode) && parens.body.body.size == 1 ? parens.body.body.first : nil
|
|
7024
|
+
return false unless range.is_a?(Prism::RangeNode)
|
|
7025
|
+
return false unless range.left.is_a?(Prism::IntegerNode) && range.right.is_a?(Prism::IntegerNode)
|
|
7026
|
+
|
|
7027
|
+
low = range.left.value
|
|
7028
|
+
high = range.right.value
|
|
7029
|
+
return false unless range.exclude_end? ? low < high : low <= high
|
|
7030
|
+
|
|
7031
|
+
case name
|
|
7032
|
+
when :each, :map then arguments.empty?
|
|
7033
|
+
when :step then positive_integer_literal?(arguments.first) && arguments.size == 1
|
|
7034
|
+
else false
|
|
7035
|
+
end
|
|
7036
|
+
end
|
|
7037
|
+
|
|
7038
|
+
# Whether {#walk_extends_block_call} owns this block-carrying call. Any other block (`[1].each { extend M }`,
|
|
7039
|
+
# `included do extend M end`) keeps the enclosing `self`, so the generic descent walks it (#1592).
|
|
7040
|
+
def rebinding_extends_block?(node)
|
|
7041
|
+
INSTANCE_EVAL_CALLS.include?(node.name) || node.name == :define_method || meta_new_constant_rvalue?(node)
|
|
7042
|
+
end
|
|
7043
|
+
|
|
5957
7044
|
# A block whose `self` is an object this walk cannot name — a `define_method` body (its
|
|
5958
7045
|
# self is the receiver's instance at call time) or an unnamed `Class.new { … }`-family
|
|
5959
7046
|
# block — walks ownerless with an unnameable self, while the call's receiver and
|
|
@@ -6001,7 +7088,7 @@ module Rigor
|
|
|
6001
7088
|
in_singleton: in_singleton, singleton_self: singleton_self,
|
|
6002
7089
|
singleton_cref: singleton_cref)
|
|
6003
7090
|
end
|
|
6004
|
-
if (body =
|
|
7091
|
+
if (body = meta_new_block_body(node))
|
|
6005
7092
|
walk_class_extends(body, qualified_prefix, meta_owner, accumulator,
|
|
6006
7093
|
singleton_cref: body_cref)
|
|
6007
7094
|
end
|
|
@@ -6067,19 +7154,39 @@ module Rigor
|
|
|
6067
7154
|
# `class << self; extend M; end` puts M on the singleton's OWN singleton, one level further out than
|
|
6068
7155
|
# anything this table describes, and `module_function` in a singleton body is not the scope toggle
|
|
6069
7156
|
# {#build_discovered_extends} over-approximates. So the two arms are disjoint rather than additive.
|
|
6070
|
-
|
|
6071
|
-
|
|
7157
|
+
# Inside `class << self` the instance walk is ownerless, so this walk also keeps the instance-side taints
|
|
7158
|
+
# (`base.include X` in a hook written there).
|
|
7159
|
+
def record_extend_call(node, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
7160
|
+
return record_module_function(node, current_class, accumulator, in_singleton) if node.name == :module_function
|
|
6072
7161
|
|
|
6073
|
-
|
|
6074
|
-
record_extend_targets(node, current_class, accumulator) if MIXIN_CALL_NAMES.include?(node.name)
|
|
6075
|
-
return
|
|
6076
|
-
end
|
|
7162
|
+
taint_opaque_eval(node, current_class, accumulator)
|
|
6077
7163
|
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
7164
|
+
kind, _arguments, via_send = mixin_call_view(node)
|
|
7165
|
+
return if kind.nil?
|
|
7166
|
+
|
|
7167
|
+
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: in_singleton)
|
|
7168
|
+
.select { |_owner, side| side == :extend || in_singleton }
|
|
7169
|
+
return if effects.empty?
|
|
7170
|
+
return if !via_send && singleton_mixin_recorded?(node, kind, current_class, accumulator, in_singleton)
|
|
7171
|
+
|
|
7172
|
+
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
7173
|
+
end
|
|
7174
|
+
|
|
7175
|
+
# The bare `module_function` toggle: the module extends itself. A direct statement only.
|
|
7176
|
+
def record_module_function(node, current_class, accumulator, in_singleton)
|
|
7177
|
+
return unless current_class && node.receiver.nil? && !in_singleton && ModuleFunctionState.extends_self?(node)
|
|
7178
|
+
|
|
7179
|
+
(accumulator[current_class] ||= []) << current_class
|
|
7180
|
+
accumulator.note(node, current_class, :extend, [current_class], complete: true)
|
|
7181
|
+
end
|
|
7182
|
+
|
|
7183
|
+
# Records the singleton-side edges of a receiverless `extend` (or, inside `class << self`, `include` /
|
|
7184
|
+
# `prepend`). False when it recorded nothing.
|
|
7185
|
+
def singleton_mixin_recorded?(node, kind, current_class, accumulator, in_singleton)
|
|
7186
|
+
return false unless current_class && node.receiver.nil?
|
|
7187
|
+
return false unless in_singleton ? MIXIN_CALL_NAMES.include?(kind) : kind == :extend
|
|
7188
|
+
|
|
7189
|
+
!record_extend_targets(node, current_class, accumulator).empty?
|
|
6083
7190
|
end
|
|
6084
7191
|
|
|
6085
7192
|
# The table stores search order, not call order: a later `extend` statement prepends its module
|
|
@@ -6088,11 +7195,22 @@ module Rigor
|
|
|
6088
7195
|
# and consumers iterating the list forward read the same order the singleton ancestry searches.
|
|
6089
7196
|
def record_extend_targets(node, current_class, accumulator)
|
|
6090
7197
|
targets = []
|
|
6091
|
-
node.arguments&.arguments
|
|
7198
|
+
arguments = node.arguments&.arguments || []
|
|
7199
|
+
arguments.each do |arg|
|
|
6092
7200
|
target = arg.is_a?(Prism::SelfNode) ? current_class : Source::ConstantPath.qualified_name(arg)
|
|
6093
7201
|
targets << target if target
|
|
6094
7202
|
end
|
|
6095
|
-
|
|
7203
|
+
return targets if targets.empty?
|
|
7204
|
+
|
|
7205
|
+
list = (accumulator[current_class] ||= [])
|
|
7206
|
+
# A repeated `extend` keeps the table position it always had (the later statement's: the freeze `uniq`s
|
|
7207
|
+
# the nearest-first list), which is not Ruby's (Ruby skips the repeat and keeps the first, #1573), and
|
|
7208
|
+
# the folded singleton tables read that order. The order is not a fact, so the singleton side is named
|
|
7209
|
+
# unpositioned and the chain settles to the readers' own answer.
|
|
7210
|
+
accumulator.taint(current_class, :extend) if targets.any? { |target| list.include?(target) }
|
|
7211
|
+
list.unshift(*targets)
|
|
7212
|
+
accumulator.note(node, current_class, :extend, targets, complete: targets.size == arguments.size)
|
|
7213
|
+
targets
|
|
6096
7214
|
end
|
|
6097
7215
|
|
|
6098
7216
|
# The materialization half of #526: for every `C extends M`, M's INSTANCE defs become C's
|
|
@@ -6100,36 +7218,133 @@ module Rigor
|
|
|
6100
7218
|
# call-site return inference runs with `self = Singleton[C]`, which is what Ruby binds). A name C
|
|
6101
7219
|
# already defines on its own singleton wins (`||=`); an extend target with no discovered defs
|
|
6102
7220
|
# contributes nothing (RBS-module extends stay on the dispatch tiers).
|
|
6103
|
-
|
|
7221
|
+
#
|
|
7222
|
+
# ADR-119 WD1 — the copy follows its siblings, which this writes (copy on write; a table the caller passed
|
|
7223
|
+
# is never mutated). `unpositioned` is the project's `unpositioned_mixins`; an edge it lists, or taints
|
|
7224
|
+
# with "*", is not a fact, so what it copies is `possible`:
|
|
7225
|
+
#
|
|
7226
|
+
# - a slot the copy WROTE is contested when the edge is listed or the source slot is contested (a slot some
|
|
7227
|
+
# other copy wrote was decided by that copy, which the `||=` lets win);
|
|
7228
|
+
# - the singleton name is `possible` when the edge is listed or the source name is possible on the instance
|
|
7229
|
+
# side, unless C's singleton already answers it certainly (never `:both` from here: the instance side is
|
|
7230
|
+
# not touched);
|
|
7231
|
+
# - a certain copy makes the singleton name certain, so `:singleton` leaves the name's possible kind.
|
|
7232
|
+
#
|
|
7233
|
+
# A contested source with a certain edge contests the slot and leaves the name certain: a value-only
|
|
7234
|
+
# contest does not make the name possible.
|
|
7235
|
+
def fold_extends_into_singleton_tables(extends, def_nodes, singleton_def_nodes, methods, siblings:,
|
|
7236
|
+
unpositioned:)
|
|
7237
|
+
owned = {}.compare_by_identity
|
|
6104
7238
|
extends.each do |class_name, mods|
|
|
7239
|
+
listed_mods = unpositioned.dig(class_name, :extend)
|
|
6105
7240
|
mods.each do |mod_name|
|
|
6106
|
-
source_defs = extend_source_defs(def_nodes, class_name, mod_name)
|
|
7241
|
+
resolved, source_defs = extend_source_defs(def_nodes, class_name, mod_name)
|
|
6107
7242
|
next if source_defs.nil?
|
|
6108
7243
|
|
|
6109
7244
|
# An inner table inherited unchanged from a frozen seed must be thawed before the fold writes.
|
|
6110
7245
|
target = singleton_def_nodes[class_name]
|
|
6111
7246
|
target = singleton_def_nodes[class_name] = (target ? target.dup : {}) if target.nil? || target.frozen?
|
|
7247
|
+
listed = extend_edge_listed?(listed_mods, mod_name)
|
|
6112
7248
|
source_defs.each do |method_name, def_node|
|
|
7249
|
+
wrote = target[method_name].nil?
|
|
6113
7250
|
target[method_name] ||= def_node
|
|
7251
|
+
certain_before = singleton_name_certain?(methods, siblings, class_name, method_name)
|
|
6114
7252
|
record_method_kind(methods, class_name, method_name, :singleton)
|
|
7253
|
+
follow_extend_siblings(siblings, owned, [class_name, resolved, method_name],
|
|
7254
|
+
wrote: wrote, listed: listed, certain_before: certain_before)
|
|
6115
7255
|
end
|
|
6116
7256
|
end
|
|
6117
7257
|
end
|
|
6118
7258
|
end
|
|
6119
7259
|
|
|
7260
|
+
# Whether `unpositioned_mixins` lists the extend edge to `mod_name` (or taints the whole side).
|
|
7261
|
+
def extend_edge_listed?(listed_mods, mod_name)
|
|
7262
|
+
!listed_mods.nil? && (listed_mods.include?(mod_name) || listed_mods.include?(MixinAccumulator::WILDCARD))
|
|
7263
|
+
end
|
|
7264
|
+
|
|
7265
|
+
# Whether `class_name`'s singleton already answers `method_name` through a certain fact: the existence table
|
|
7266
|
+
# covers the singleton side and the `possible` copy does not.
|
|
7267
|
+
def singleton_name_certain?(methods, siblings, class_name, method_name)
|
|
7268
|
+
return false unless covers_singleton?(methods.dig(class_name, method_name))
|
|
7269
|
+
|
|
7270
|
+
!covers_singleton?(siblings[:possible_discovered_methods].dig(class_name, method_name))
|
|
7271
|
+
end
|
|
7272
|
+
|
|
7273
|
+
SINGLETON_KINDS = [:singleton, Scope::DiscoveryIndex::METHOD_KIND_BOTH].freeze
|
|
7274
|
+
INSTANCE_KINDS = [:instance, Scope::DiscoveryIndex::METHOD_KIND_BOTH].freeze
|
|
7275
|
+
private_constant :SINGLETON_KINDS, :INSTANCE_KINDS
|
|
7276
|
+
|
|
7277
|
+
def covers_singleton?(kind)
|
|
7278
|
+
SINGLETON_KINDS.include?(kind)
|
|
7279
|
+
end
|
|
7280
|
+
|
|
7281
|
+
# The sibling half of one copied name; see {#fold_extends_into_singleton_tables}. `names` is
|
|
7282
|
+
# `[extender, resolved source module, method name]`.
|
|
7283
|
+
def follow_extend_siblings(siblings, owned, names, wrote:, listed:, certain_before:)
|
|
7284
|
+
class_name, resolved, method_name = names
|
|
7285
|
+
contested_source = !siblings[:contested_discovered_def_nodes].empty? &&
|
|
7286
|
+
siblings[:contested_discovered_def_nodes].include?([resolved, method_name])
|
|
7287
|
+
if wrote && (listed || contested_source)
|
|
7288
|
+
own_sibling_table(siblings, owned, :contested_discovered_singleton_def_nodes) << [class_name, method_name]
|
|
7289
|
+
end
|
|
7290
|
+
if listed || source_name_possible?(siblings, resolved, method_name)
|
|
7291
|
+
return if certain_before
|
|
7292
|
+
|
|
7293
|
+
row = (own_sibling_table(siblings, owned, :possible_discovered_methods)[class_name] ||= {})
|
|
7294
|
+
row[method_name] = merge_sibling_kind(row[method_name], :singleton)
|
|
7295
|
+
else
|
|
7296
|
+
strip_possible_singleton(siblings, owned, class_name, method_name)
|
|
7297
|
+
end
|
|
7298
|
+
end
|
|
7299
|
+
|
|
7300
|
+
def source_name_possible?(siblings, resolved, method_name)
|
|
7301
|
+
INSTANCE_KINDS.include?(siblings[:possible_discovered_methods].dig(resolved, method_name))
|
|
7302
|
+
end
|
|
7303
|
+
|
|
7304
|
+
def merge_sibling_kind(sitting, arriving)
|
|
7305
|
+
sitting.nil? || sitting == arriving ? arriving : Scope::DiscoveryIndex::METHOD_KIND_BOTH
|
|
7306
|
+
end
|
|
7307
|
+
|
|
7308
|
+
# A certain copy: `:singleton` leaves the `possible` kind (`:both` becomes `:instance`), and an emptied row goes.
|
|
7309
|
+
def strip_possible_singleton(siblings, owned, class_name, method_name)
|
|
7310
|
+
return unless covers_singleton?(siblings[:possible_discovered_methods].dig(class_name, method_name))
|
|
7311
|
+
|
|
7312
|
+
possible = own_sibling_table(siblings, owned, :possible_discovered_methods)
|
|
7313
|
+
row = possible[class_name]
|
|
7314
|
+
if row[method_name] == :singleton
|
|
7315
|
+
row.delete(method_name)
|
|
7316
|
+
possible.delete(class_name) if row.empty?
|
|
7317
|
+
else
|
|
7318
|
+
row[method_name] = :instance
|
|
7319
|
+
end
|
|
7320
|
+
end
|
|
7321
|
+
|
|
7322
|
+
# The sibling `key` as a table this fold owns: the first write copies it (a Set, or a Hash of Hashes) and
|
|
7323
|
+
# keeps the copy in `siblings`, so a table shared with a seed, a bundle or an empty constant is never written.
|
|
7324
|
+
def own_sibling_table(siblings, owned, key)
|
|
7325
|
+
table = siblings.fetch(key)
|
|
7326
|
+
return table if owned.key?(table)
|
|
7327
|
+
|
|
7328
|
+
copy = table.is_a?(Set) ? table.dup : table.transform_values(&:dup)
|
|
7329
|
+
owned[copy] = true
|
|
7330
|
+
siblings[key] = copy
|
|
7331
|
+
end
|
|
7332
|
+
|
|
6120
7333
|
# `extend CustomSig` inside `module Outer; class F` stores the as-written name, while
|
|
6121
7334
|
# `def_nodes` keys the module's defs as `Outer::CustomSig`. Walk the enclosing namespaces
|
|
6122
|
-
# innermost-first so the fold finds the same module Ruby constant-lookup would.
|
|
7335
|
+
# innermost-first so the fold finds the same module Ruby constant-lookup would. Returns
|
|
7336
|
+
# `[the key the defs were found under, the defs]`, or nil.
|
|
6123
7337
|
def extend_source_defs(def_nodes, class_name, mod_name)
|
|
6124
7338
|
found = def_nodes[mod_name]
|
|
6125
|
-
return found if found
|
|
7339
|
+
return [mod_name, found] if found
|
|
6126
7340
|
return nil if mod_name.nil? || mod_name.start_with?("::")
|
|
6127
7341
|
|
|
6128
7342
|
parts = class_name.to_s.split("::")
|
|
6129
7343
|
while parts.size > 1
|
|
6130
7344
|
parts.pop
|
|
6131
|
-
|
|
6132
|
-
|
|
7345
|
+
key = "#{parts.join('::')}::#{mod_name}"
|
|
7346
|
+
found = def_nodes[key]
|
|
7347
|
+
return [key, found] if found
|
|
6133
7348
|
end
|
|
6134
7349
|
nil
|
|
6135
7350
|
end
|
|
@@ -6155,9 +7370,83 @@ module Rigor
|
|
|
6155
7370
|
# Top-level (no surrounding class) defs do not contribute — Ruby's top-level visibility nuances (private at
|
|
6156
7371
|
# top-level marks the method on `Object`) are out of scope for v0.1.2.
|
|
6157
7372
|
def build_discovered_method_visibilities(root)
|
|
6158
|
-
|
|
7373
|
+
build_method_visibility_tables(root).first
|
|
7374
|
+
end
|
|
7375
|
+
|
|
7376
|
+
# ADR-119 WD3 — the visibility table and its `contested_discovered_method_visibilities` sibling, from ONE walk
|
|
7377
|
+
# over the file's {Certainty} classification `certainty`: `[table, contested key paths]`. The recorded values
|
|
7378
|
+
# are {#build_discovered_method_visibilities}'s; a slot is contested when its last writer is ({VisibilityTable}).
|
|
7379
|
+
def build_method_visibility_tables(root, certainty: Certainty.possible_nodes(root))
|
|
7380
|
+
accumulator = VisibilityTable.new.track(certainty)
|
|
6159
7381
|
walk_method_visibilities(root, [], false, :public, accumulator)
|
|
6160
|
-
accumulator.transform_values(&:freeze).freeze
|
|
7382
|
+
[accumulator.transform_values(&:freeze).freeze, accumulator.contested_paths]
|
|
7383
|
+
end
|
|
7384
|
+
|
|
7385
|
+
# The bare calls that set the running default visibility of the body they run in: the modifiers and
|
|
7386
|
+
# `module_function`, which {#walk_method_visibilities} does not track as a value (#1569) but which moves the
|
|
7387
|
+
# default all the same.
|
|
7388
|
+
VISIBILITY_TOGGLES = (VISIBILITY_MODIFIERS + %i[module_function]).freeze
|
|
7389
|
+
|
|
7390
|
+
# The `{class => {name => visibility}}` accumulator of {#walk_method_visibilities}, which also carries the
|
|
7391
|
+
# ADR-119 WD3 state of the slots it writes. A slot is contested when the write that holds it is possible: a
|
|
7392
|
+
# `def` or a named target (`private :x`) the {Certainty} classification reads as possible, or a `def` written
|
|
7393
|
+
# after an UNCERTAIN bare toggle of its body. A toggle is uncertain when it is possible itself or when the walk
|
|
7394
|
+
# does not carry it into the body's running default: one under any child of the body that is not its
|
|
7395
|
+
# statement list (`if c then private end`, `begin private end`, a block, `(private)`), since Ruby applies a
|
|
7396
|
+
# bare toggle there to the body's later `def`s while the walk keeps the default it had. A certain toggle
|
|
7397
|
+
# written directly in the body settles the default again. A named target does not read the running default,
|
|
7398
|
+
# so a toggle does not contest it. A certain write clears the slot's contest.
|
|
7399
|
+
class VisibilityTable < Hash
|
|
7400
|
+
# The bare toggles the walk has met so far; a body's statement loop compares it around each child.
|
|
7401
|
+
attr_accessor :toggles
|
|
7402
|
+
# Whether the running default of the body being walked rests on an uncertain toggle.
|
|
7403
|
+
attr_accessor :uncertain
|
|
7404
|
+
# The file's {Certainty} answer.
|
|
7405
|
+
attr_reader :certainty
|
|
7406
|
+
|
|
7407
|
+
def track(certainty)
|
|
7408
|
+
@certainty = certainty
|
|
7409
|
+
@contested = nil
|
|
7410
|
+
@toggles = 0
|
|
7411
|
+
@uncertain = false
|
|
7412
|
+
self
|
|
7413
|
+
end
|
|
7414
|
+
|
|
7415
|
+
# Records that `node` wrote the `[class_name, name]` slot; `toggled` when the write reads the running
|
|
7416
|
+
# default (a `def`).
|
|
7417
|
+
def wrote(class_name, name, node, toggled:)
|
|
7418
|
+
if Certainty.possible?(@certainty, node) || (toggled && @uncertain)
|
|
7419
|
+
(@contested ||= Set.new) << [class_name, name].freeze
|
|
7420
|
+
else
|
|
7421
|
+
@contested&.delete([class_name, name])
|
|
7422
|
+
end
|
|
7423
|
+
end
|
|
7424
|
+
|
|
7425
|
+
# Runs the block as a fresh body (a class or module, `class << self`, a meta-new or eval block), whose
|
|
7426
|
+
# default starts certain and whose toggles do not reach the enclosing body.
|
|
7427
|
+
def body
|
|
7428
|
+
saved_toggles = @toggles
|
|
7429
|
+
saved_uncertain = @uncertain
|
|
7430
|
+
@uncertain = false
|
|
7431
|
+
yield
|
|
7432
|
+
ensure
|
|
7433
|
+
@toggles = saved_toggles
|
|
7434
|
+
@uncertain = saved_uncertain
|
|
7435
|
+
end
|
|
7436
|
+
|
|
7437
|
+
def contested_paths
|
|
7438
|
+
contested = @contested
|
|
7439
|
+
return Scope::DiscoveryIndex::EMPTY_SIBLINGS[:contested_discovered_method_visibilities] if contested.nil?
|
|
7440
|
+
return Scope::DiscoveryIndex::EMPTY_SIBLINGS[:contested_discovered_method_visibilities] if contested.empty?
|
|
7441
|
+
|
|
7442
|
+
contested.freeze
|
|
7443
|
+
end
|
|
7444
|
+
end
|
|
7445
|
+
|
|
7446
|
+
# A bare toggle ({VISIBILITY_TOGGLES}): receiverless, with no arguments.
|
|
7447
|
+
def bare_visibility_toggle?(node)
|
|
7448
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && VISIBILITY_TOGGLES.include?(node.name) &&
|
|
7449
|
+
(node.arguments.nil? || node.arguments.arguments.empty?) && node.block.nil?
|
|
6161
7450
|
end
|
|
6162
7451
|
|
|
6163
7452
|
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
@@ -6179,8 +7468,10 @@ module Rigor
|
|
|
6179
7468
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
6180
7469
|
body_prefix = child_cref ? [] : child_prefix
|
|
6181
7470
|
if node.body
|
|
6182
|
-
|
|
6183
|
-
|
|
7471
|
+
accumulator.body do
|
|
7472
|
+
walk_method_visibilities(node.body, body_prefix, false, :public,
|
|
7473
|
+
accumulator, nil, singleton_cref: child_cref)
|
|
7474
|
+
end
|
|
6184
7475
|
end
|
|
6185
7476
|
return current_visibility
|
|
6186
7477
|
end
|
|
@@ -6200,9 +7491,11 @@ module Rigor
|
|
|
6200
7491
|
defs_singleton: defs_singleton)
|
|
6201
7492
|
end
|
|
6202
7493
|
if body
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
7494
|
+
accumulator.body do
|
|
7495
|
+
walk_method_visibilities(body, qualified_prefix, false, :public,
|
|
7496
|
+
accumulator, body_self,
|
|
7497
|
+
singleton_cref: singleton_cref)
|
|
7498
|
+
end
|
|
6206
7499
|
end
|
|
6207
7500
|
return current_visibility
|
|
6208
7501
|
end
|
|
@@ -6217,6 +7510,7 @@ module Rigor
|
|
|
6217
7510
|
defs_singleton: defs_singleton)
|
|
6218
7511
|
return current_visibility
|
|
6219
7512
|
end
|
|
7513
|
+
accumulator.toggles += 1 if bare_visibility_toggle?(node)
|
|
6220
7514
|
# The visibility table is instance-side only — `private :x` inside `class <<`
|
|
6221
7515
|
# (or a receiver-eval body that re-evaluates a singleton self) marks the
|
|
6222
7516
|
# SINGLETON method, which this table cannot express.
|
|
@@ -6230,13 +7524,23 @@ module Rigor
|
|
|
6230
7524
|
|
|
6231
7525
|
# Statement-position StatementsNode preserves left-to-right visibility flow; everything else recurses with the
|
|
6232
7526
|
# entry visibility unchanged.
|
|
7527
|
+
#
|
|
7528
|
+
# ADR-119 WD3 — the statement loop also settles whether the running default is uncertain
|
|
7529
|
+
# ({VisibilityTable}): a bare toggle written here sets it from its own certainty, and any other statement
|
|
7530
|
+
# under which the walk met a toggle makes it uncertain.
|
|
6233
7531
|
if node.is_a?(Prism::StatementsNode)
|
|
6234
7532
|
local_visibility = current_visibility
|
|
6235
7533
|
node.rigor_each_child do |child|
|
|
7534
|
+
toggles = accumulator.toggles
|
|
6236
7535
|
local_visibility = walk_method_visibilities(child, qualified_prefix, in_singleton_class,
|
|
6237
7536
|
local_visibility, accumulator, def_owner_prefix,
|
|
6238
7537
|
singleton_cref: singleton_cref,
|
|
6239
7538
|
defs_singleton: defs_singleton)
|
|
7539
|
+
if bare_visibility_toggle?(child)
|
|
7540
|
+
accumulator.uncertain = Certainty.possible?(accumulator.certainty, child)
|
|
7541
|
+
elsif accumulator.toggles != toggles
|
|
7542
|
+
accumulator.uncertain = true
|
|
7543
|
+
end
|
|
6240
7544
|
end
|
|
6241
7545
|
else
|
|
6242
7546
|
node.rigor_each_child do |child|
|
|
@@ -6263,8 +7567,10 @@ module Rigor
|
|
|
6263
7567
|
defs_singleton: defs_singleton)
|
|
6264
7568
|
return unless node.body
|
|
6265
7569
|
|
|
6266
|
-
|
|
6267
|
-
|
|
7570
|
+
accumulator.body do
|
|
7571
|
+
walk_method_visibilities(node.body, qualified_prefix, true, :public, accumulator,
|
|
7572
|
+
singleton_prefix, singleton_cref: true)
|
|
7573
|
+
end
|
|
6268
7574
|
end
|
|
6269
7575
|
|
|
6270
7576
|
# The eval-block arm of {#walk_method_visibilities}: the block is a fresh class body under
|
|
@@ -6295,10 +7601,12 @@ module Rigor
|
|
|
6295
7601
|
# receiver-as-module and stay instance-side (`in_singleton_class` off) for a class
|
|
6296
7602
|
# receiver.
|
|
6297
7603
|
call_singleton, defs_flag = eval_body_def_context(node, in_singleton_class)
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
7604
|
+
accumulator.body do
|
|
7605
|
+
node.block.rigor_each_child do |child|
|
|
7606
|
+
walk_method_visibilities(child, qualified_prefix, call_singleton, :public, accumulator,
|
|
7607
|
+
eval_prefix, singleton_cref: singleton_cref,
|
|
7608
|
+
defs_singleton: defs_flag)
|
|
7609
|
+
end
|
|
6302
7610
|
end
|
|
6303
7611
|
end
|
|
6304
7612
|
|
|
@@ -6309,6 +7617,7 @@ module Rigor
|
|
|
6309
7617
|
class_name = qualified_prefix.join("::")
|
|
6310
7618
|
accumulator[class_name] ||= {}
|
|
6311
7619
|
accumulator[class_name][def_node.name] = current_visibility
|
|
7620
|
+
accumulator.wrote(class_name, def_node.name, def_node, toggled: true)
|
|
6312
7621
|
end
|
|
6313
7622
|
|
|
6314
7623
|
# Recognises modifier calls on the implicit-self receiver inside a class body. Returns the (possibly updated)
|
|
@@ -6329,26 +7638,25 @@ module Rigor
|
|
|
6329
7638
|
if args.empty?
|
|
6330
7639
|
call_node.name
|
|
6331
7640
|
else
|
|
6332
|
-
apply_named_visibility(args, qualified_prefix, call_node
|
|
7641
|
+
apply_named_visibility(args, qualified_prefix, call_node, accumulator)
|
|
6333
7642
|
current_visibility
|
|
6334
7643
|
end
|
|
6335
7644
|
end
|
|
6336
7645
|
|
|
6337
|
-
def apply_named_visibility(args, qualified_prefix,
|
|
7646
|
+
def apply_named_visibility(args, qualified_prefix, call_node, accumulator)
|
|
6338
7647
|
class_name = qualified_prefix.join("::")
|
|
6339
7648
|
args.each do |arg|
|
|
6340
7649
|
name = visibility_target_name(arg)
|
|
6341
7650
|
next if name.nil?
|
|
6342
7651
|
|
|
6343
7652
|
accumulator[class_name] ||= {}
|
|
6344
|
-
accumulator[class_name][name] =
|
|
7653
|
+
accumulator[class_name][name] = call_node.name
|
|
7654
|
+
accumulator.wrote(class_name, name, call_node, toggled: false)
|
|
6345
7655
|
end
|
|
6346
7656
|
end
|
|
6347
7657
|
|
|
6348
7658
|
def visibility_target_name(arg)
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
nil
|
|
7659
|
+
Source::Literals.symbol_or_string(arg)
|
|
6352
7660
|
end
|
|
6353
7661
|
|
|
6354
7662
|
# Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
|
|
@@ -6376,15 +7684,30 @@ module Rigor
|
|
|
6376
7684
|
class_name = qualified_prefix.join("::")
|
|
6377
7685
|
undef_node.names.each do |name|
|
|
6378
7686
|
method_name = literal_method_name(name)
|
|
6379
|
-
|
|
7687
|
+
next unless method_name
|
|
7688
|
+
|
|
7689
|
+
record_method_envelope_opaque(tables, class_name, method_name)
|
|
7690
|
+
record_removed_name(tables, class_name, method_name)
|
|
6380
7691
|
end
|
|
6381
7692
|
end
|
|
6382
7693
|
|
|
7694
|
+
REMOVING_CALLS = %i[remove_method undef_method].to_set.freeze
|
|
7695
|
+
private_constant :REMOVING_CALLS
|
|
7696
|
+
|
|
7697
|
+
# Issue #1820 — `undef f`, `remove_method :f` or `undef_method :f` on the class, as its own entry beside the
|
|
7698
|
+
# opaque envelopes: an `attr_*` or `define_method` records an opaque envelope too, so only this says the name
|
|
7699
|
+
# is gone.
|
|
7700
|
+
def record_removed_name(tables, class_name, method_name)
|
|
7701
|
+
record_envelope(tables.envelopes, class_name, [Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND, method_name],
|
|
7702
|
+
Source::ParameterEnvelope::OPAQUE)
|
|
7703
|
+
end
|
|
7704
|
+
|
|
6383
7705
|
# Both kinds, and the envelope table only: the evidence says something rewrites the name, not which
|
|
6384
7706
|
# side it lives on, and it is not evidence that the name EXISTS, so the existence table is untouched.
|
|
6385
7707
|
def record_method_envelope_opaque(tables, class_name, method_name)
|
|
6386
7708
|
%i[instance singleton].each do |kind|
|
|
6387
7709
|
record_envelope(tables.envelopes, class_name, [kind, method_name], Source::ParameterEnvelope::OPAQUE)
|
|
7710
|
+
contest_envelope(tables, class_name, [kind, method_name]) unless tables.certain
|
|
6388
7711
|
end
|
|
6389
7712
|
end
|
|
6390
7713
|
|
|
@@ -6394,7 +7717,7 @@ module Rigor
|
|
|
6394
7717
|
# `def_delegator :@x, :f`) would leave a `def`'s envelope standing for a method that no longer has it.
|
|
6395
7718
|
NAME_NEUTRAL_MACROS = %i[
|
|
6396
7719
|
private public protected module_function private_class_method public_class_method
|
|
6397
|
-
private_constant public_constant require require_relative autoload
|
|
7720
|
+
private_constant public_constant require require_relative autoload autoload_relative
|
|
6398
7721
|
].to_set.freeze
|
|
6399
7722
|
private_constant :NAME_NEUTRAL_MACROS
|
|
6400
7723
|
|
|
@@ -6445,13 +7768,21 @@ module Rigor
|
|
|
6445
7768
|
if dynamic_surface_call?(node)
|
|
6446
7769
|
record_surface_mark(tables, class_name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
6447
7770
|
elsif node.name == :refine
|
|
6448
|
-
|
|
7771
|
+
# Issue #1689 — a `refine` where `self` is a class is the class's own method, and refines nothing.
|
|
7772
|
+
record_refinement(node, qualified_prefix, tables) unless tables.class_body_refine?(node)
|
|
6449
7773
|
elsif !NAME_NEUTRAL_MACROS.include?(node.name)
|
|
6450
|
-
(node
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
7774
|
+
record_named_arguments_opaque(node, class_name, tables)
|
|
7775
|
+
end
|
|
7776
|
+
end
|
|
7777
|
+
|
|
7778
|
+
def record_named_arguments_opaque(node, class_name, tables)
|
|
7779
|
+
(node.arguments&.arguments || []).each do |argument|
|
|
7780
|
+
# `memoize def f(a)` wraps the def it is handed exactly as `memoize :f` does.
|
|
7781
|
+
method_name = argument.is_a?(Prism::DefNode) ? argument.name : literal_method_name(argument)
|
|
7782
|
+
next unless method_name
|
|
7783
|
+
|
|
7784
|
+
record_method_envelope_opaque(tables, class_name, method_name)
|
|
7785
|
+
record_removed_name(tables, class_name, method_name) if REMOVING_CALLS.include?(node.name)
|
|
6455
7786
|
end
|
|
6456
7787
|
end
|
|
6457
7788
|
|
|
@@ -6505,8 +7836,11 @@ module Rigor
|
|
|
6505
7836
|
# Post-pass over the `def_nodes` accumulator: for every `alias` declaration inside a class body, if the original
|
|
6506
7837
|
# method name maps to a `Prism::DefNode`, register the new name pointing to the same node so inter-procedural
|
|
6507
7838
|
# return-type inference works for the aliased name.
|
|
6508
|
-
|
|
6509
|
-
|
|
7839
|
+
#
|
|
7840
|
+
# ADR-119 WD3 — the alias is the slot's writer: the new name's slot is contested when the alias is possible
|
|
7841
|
+
# or the slot it copies is contested, and cleared otherwise.
|
|
7842
|
+
def apply_alias_def_nodes(root, accumulator, tables = nil)
|
|
7843
|
+
alias_map = collect_class_alias_map(root, [], AliasMap.new.track(tables&.certainty))
|
|
6510
7844
|
alias_map.each do |class_name, aliases|
|
|
6511
7845
|
class_defs = accumulator[class_name]
|
|
6512
7846
|
next unless class_defs
|
|
@@ -6516,10 +7850,40 @@ module Rigor
|
|
|
6516
7850
|
next unless def_node.is_a?(Prism::DefNode)
|
|
6517
7851
|
|
|
6518
7852
|
(accumulator[class_name] ||= {})[new_name] = def_node
|
|
7853
|
+
next if tables.nil?
|
|
7854
|
+
|
|
7855
|
+
certain = !alias_map.possible?(class_name, new_name) &&
|
|
7856
|
+
!tables.contested_def_nodes&.include?([class_name, old_name])
|
|
7857
|
+
follow_slot_writer(tables, :contested_def_nodes, [class_name, new_name], certain)
|
|
6519
7858
|
end
|
|
6520
7859
|
end
|
|
6521
7860
|
end
|
|
6522
7861
|
|
|
7862
|
+
# The `{class => {new name => old name}}` map {#collect_class_alias_map} fills, which also remembers, per
|
|
7863
|
+
# entry, whether the alias that wrote it last is possible ({Certainty}).
|
|
7864
|
+
class AliasMap < Hash
|
|
7865
|
+
def track(certainty)
|
|
7866
|
+
@certainty = certainty || Certainty::EMPTY
|
|
7867
|
+
@possible = nil
|
|
7868
|
+
self
|
|
7869
|
+
end
|
|
7870
|
+
|
|
7871
|
+
# Records that `node` wrote the `[class_name, new_name]` entry.
|
|
7872
|
+
def wrote(class_name, new_name, node)
|
|
7873
|
+
return if @certainty.nil?
|
|
7874
|
+
|
|
7875
|
+
if Certainty.possible?(@certainty, node)
|
|
7876
|
+
(@possible ||= Set.new) << [class_name, new_name]
|
|
7877
|
+
else
|
|
7878
|
+
@possible&.delete([class_name, new_name])
|
|
7879
|
+
end
|
|
7880
|
+
end
|
|
7881
|
+
|
|
7882
|
+
def possible?(class_name, new_name)
|
|
7883
|
+
!@possible.nil? && @possible.include?([class_name, new_name])
|
|
7884
|
+
end
|
|
7885
|
+
end
|
|
7886
|
+
|
|
6523
7887
|
# Builds a map `{class_name => {new_name_sym => old_name_sym}}` by walking the tree for `AliasMethodNode` nodes
|
|
6524
7888
|
# inside class bodies. `leaf_owner` is the meta-new override: inside `K = Class.new { … }` the
|
|
6525
7889
|
# block's aliases bind on the class the write names — it overrides `qualified_prefix`, which
|
|
@@ -6577,9 +7941,9 @@ module Rigor
|
|
|
6577
7941
|
# `alias_method :new, :old` — the CallNode twin of the `alias` keyword (#533; liquid's i18n
|
|
6578
7942
|
# `t` alias was the corpus case). Unlike AliasMethodNode a call's children can carry further
|
|
6579
7943
|
# class bodies (`Class.new do … end`), so the walk continues below it either way.
|
|
6580
|
-
names = alias_method_call_names(node)
|
|
7944
|
+
names = Source::AliasNames.alias_method_call_names(node)
|
|
6581
7945
|
if names && !rec_prefix.empty? && !(singleton_cref && leaf_owner.nil?)
|
|
6582
|
-
(accumulator
|
|
7946
|
+
record_alias_call_entry(accumulator, rec_prefix.join("::"), names, node)
|
|
6583
7947
|
end
|
|
6584
7948
|
false
|
|
6585
7949
|
end
|
|
@@ -6638,21 +8002,6 @@ module Rigor
|
|
|
6638
8002
|
true
|
|
6639
8003
|
end
|
|
6640
8004
|
|
|
6641
|
-
# `[new_name, old_name]` for an implicit-self `alias_method` call with two literal symbol /
|
|
6642
|
-
# string arguments, or nil. A variable-named alias is runtime data and stays unrecorded.
|
|
6643
|
-
def alias_method_call_names(call_node)
|
|
6644
|
-
return nil unless call_node.name == :alias_method && call_node.receiver.nil?
|
|
6645
|
-
|
|
6646
|
-
args = call_node.arguments&.arguments
|
|
6647
|
-
return nil unless args && args.size == 2
|
|
6648
|
-
|
|
6649
|
-
new_name = literal_method_name(args[0])
|
|
6650
|
-
old_name = literal_method_name(args[1])
|
|
6651
|
-
return nil if new_name.nil? || old_name.nil?
|
|
6652
|
-
|
|
6653
|
-
[new_name, old_name]
|
|
6654
|
-
end
|
|
6655
|
-
|
|
6656
8005
|
# The class/module arm of {#collect_class_alias_map}: under an unnameable cref a
|
|
6657
8006
|
# bare/`self::` header opens `#<singleton>::Name` — the map files nothing for it;
|
|
6658
8007
|
# a rooted or explicit-base header re-anchors at a nameable prefix.
|
|
@@ -6688,14 +8037,21 @@ module Rigor
|
|
|
6688
8037
|
accumulator
|
|
6689
8038
|
end
|
|
6690
8039
|
|
|
8040
|
+
def record_alias_call_entry(accumulator, class_name, names, node)
|
|
8041
|
+
(accumulator[class_name] ||= {})[names.first] = names.last
|
|
8042
|
+
accumulator.wrote(class_name, names.first, node) if accumulator.is_a?(AliasMap)
|
|
8043
|
+
end
|
|
8044
|
+
|
|
6691
8045
|
def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
|
|
6692
8046
|
return if qualified_prefix.empty?
|
|
6693
|
-
|
|
8047
|
+
|
|
8048
|
+
names = Source::AliasNames.keyword_names(alias_node)
|
|
8049
|
+
return if names.nil?
|
|
6694
8050
|
|
|
6695
8051
|
class_name = qualified_prefix.join("::")
|
|
6696
|
-
new_name =
|
|
6697
|
-
old_name = alias_node.old_name.unescaped.to_sym
|
|
8052
|
+
new_name, old_name = names
|
|
6698
8053
|
(accumulator[class_name] ||= {})[new_name] = old_name
|
|
8054
|
+
accumulator.wrote(class_name, new_name, alias_node) if accumulator.is_a?(AliasMap)
|
|
6699
8055
|
end
|
|
6700
8056
|
|
|
6701
8057
|
def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
@@ -6793,9 +8149,7 @@ module Rigor
|
|
|
6793
8149
|
end
|
|
6794
8150
|
|
|
6795
8151
|
def literal_method_name(node)
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
node.unescaped&.to_sym
|
|
8152
|
+
Source::Literals.symbol_or_string(node)
|
|
6799
8153
|
end
|
|
6800
8154
|
|
|
6801
8155
|
# Walks every file in `paths` (each path is parsed once with `Prism.parse_file`) and returns the unioned
|
|
@@ -6848,6 +8202,8 @@ module Rigor
|
|
|
6848
8202
|
physical = buffer ? buffer.resolve(path) : path
|
|
6849
8203
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
6850
8204
|
accumulate_project_index(acc, path, root)
|
|
8205
|
+
rescue DeclarationWalk::ContractError
|
|
8206
|
+
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
6851
8207
|
rescue StandardError
|
|
6852
8208
|
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
6853
8209
|
next
|
|
@@ -6880,6 +8236,8 @@ module Rigor
|
|
|
6880
8236
|
code_fingerprints[path] = code_fingerprint(source, parsed.comments)
|
|
6881
8237
|
declaration_signatures[path] = declaration_signature(file_index)
|
|
6882
8238
|
refinements[path] = file_index[:refinements] if file_index[:refinements]
|
|
8239
|
+
rescue DeclarationWalk::ContractError
|
|
8240
|
+
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
6883
8241
|
rescue StandardError
|
|
6884
8242
|
next
|
|
6885
8243
|
end
|
|
@@ -6910,9 +8268,29 @@ module Rigor
|
|
|
6910
8268
|
append_def_signatures(parts, file_index[:def_nodes], "#")
|
|
6911
8269
|
append_def_signatures(parts, file_index[:singleton_def_nodes], ".")
|
|
6912
8270
|
append_envelope_signature(parts, file_index[:parameter_envelopes] || {})
|
|
8271
|
+
append_certainty_signature(parts, file_index[:siblings] || {})
|
|
6913
8272
|
Digest::SHA256.hexdigest(parts.join("\x00"))
|
|
6914
8273
|
end
|
|
6915
8274
|
|
|
8275
|
+
# ADR-119 WD3 — which of the file's contributions are possible is a declaration surface too: a `… end if X`
|
|
8276
|
+
# modifier, or moving a `def` into a block, keeps every def line and envelope and moves only this, while a
|
|
8277
|
+
# dependent's candidate-set read now declines where it answered.
|
|
8278
|
+
def append_certainty_signature(parts, siblings)
|
|
8279
|
+
certainty = []
|
|
8280
|
+
siblings.each do |name, table|
|
|
8281
|
+
next if table.empty?
|
|
8282
|
+
|
|
8283
|
+
if name == :possible_discovered_methods
|
|
8284
|
+
table.each do |class_name, row|
|
|
8285
|
+
row.each { |method_name, kind| certainty << "P:#{class_name}##{method_name}=#{kind}" }
|
|
8286
|
+
end
|
|
8287
|
+
else
|
|
8288
|
+
table.each { |path| certainty << "C:#{name}:#{path.inspect}" }
|
|
8289
|
+
end
|
|
8290
|
+
end
|
|
8291
|
+
parts.concat(certainty.sort)
|
|
8292
|
+
end
|
|
8293
|
+
|
|
6916
8294
|
# Issue #992 — the joined envelope table is a declaration surface the per-def signatures above cannot
|
|
6917
8295
|
# reconstruct: they keep the LAST `def` of a name, and they see neither a second `def` with another
|
|
6918
8296
|
# shape nor a `memoize :f` beside it, either of which moves a dependent's `call.wrong-arity` verdict.
|
|
@@ -6950,13 +8328,26 @@ module Rigor
|
|
|
6950
8328
|
def append_ancestry_signature(parts, file_index)
|
|
6951
8329
|
parts.concat(file_index[:class_sources].keys.sort_by(&:to_s).map { |cn| "c:#{cn}" })
|
|
6952
8330
|
parts.concat(sorted_by_class(file_index[:superclasses]).map { |cn, sc| "s:#{cn}<#{sc}" })
|
|
6953
|
-
parts
|
|
6954
|
-
"i:#{cn}=#{Array(mods).map(&:to_s).sort.join(',')}"
|
|
6955
|
-
end)
|
|
8331
|
+
append_mixin_signature(parts, file_index)
|
|
6956
8332
|
parts.concat(sorted_by_class(file_index[:data_member_layouts]).map { |cn, l| "d:#{cn}=#{l.inspect}" })
|
|
6957
8333
|
parts.concat(sorted_by_class(file_index[:struct_member_layouts]).map { |cn, l| "t:#{cn}=#{l.inspect}" })
|
|
6958
8334
|
end
|
|
6959
8335
|
|
|
8336
|
+
# The mixin surface of {#append_ancestry_signature}. The lists keep SOURCE order: this is one file's own
|
|
8337
|
+
# index, so the order is deterministic, and it is a fact an ancestor-order reader consumes, so an edit
|
|
8338
|
+
# that only reorders an `include` must move the signature. The unpositioned table is there for the same
|
|
8339
|
+
# reason: guarding an `include` changes no name and no order, only whether the order is known.
|
|
8340
|
+
def append_mixin_signature(parts, file_index)
|
|
8341
|
+
{ "i" => :includes, "p" => :prepends, "x" => :extends }.each do |tag, key|
|
|
8342
|
+
sorted_by_class(file_index[key] || {}).each { |cn, mods| parts << "#{tag}:#{cn}=#{Array(mods).join(',')}" }
|
|
8343
|
+
end
|
|
8344
|
+
sorted_by_class(file_index[:unpositioned_mixins] || {}).each do |cn, sides|
|
|
8345
|
+
sides.sort_by { |side, _| side.to_s }.each do |side, names|
|
|
8346
|
+
parts << "u:#{cn}.#{side}=#{names.sort.join(',')}"
|
|
8347
|
+
end
|
|
8348
|
+
end
|
|
8349
|
+
end
|
|
8350
|
+
|
|
6960
8351
|
# A class-keyed table's pairs sorted by class name (stringified) for a deterministic signature.
|
|
6961
8352
|
def sorted_by_class(table)
|
|
6962
8353
|
table.sort_by { |cn, _| cn.to_s }
|
|
@@ -7079,6 +8470,8 @@ module Rigor
|
|
|
7079
8470
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
7080
8471
|
collect_class_decls(root, [], classes)
|
|
7081
8472
|
accumulate_project_index(acc, path, root)
|
|
8473
|
+
rescue DeclarationWalk::ContractError
|
|
8474
|
+
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
7082
8475
|
rescue StandardError
|
|
7083
8476
|
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
7084
8477
|
next
|
|
@@ -7148,6 +8541,8 @@ module Rigor
|
|
|
7148
8541
|
classes.merge!(file_classes)
|
|
7149
8542
|
fold_file_index(acc, file_index)
|
|
7150
8543
|
end
|
|
8544
|
+
rescue DeclarationWalk::ContractError
|
|
8545
|
+
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
7151
8546
|
rescue StandardError
|
|
7152
8547
|
# Skip files that fail to parse / read; the per-file analyzer surfaces the parse error separately.
|
|
7153
8548
|
next
|
|
@@ -7170,6 +8565,7 @@ module Rigor
|
|
|
7170
8565
|
# Polymorphic over the def-node value: a re-walked file's `file_index` carries live nodes; a cached
|
|
7171
8566
|
# bundle's carries {DefHandle}s. The merges never deref the value, so both fold identically.
|
|
7172
8567
|
def fold_file_index(acc, file_index)
|
|
8568
|
+
fold_siblings(acc, file_index)
|
|
7173
8569
|
fold_def_tables(acc, file_index)
|
|
7174
8570
|
# Issue #992 — a pre-23 seed bundle carries no envelopes; the SCHEMA bump makes such a blob a cold
|
|
7175
8571
|
# rebuild, and an absent table only ever withholds a check.
|
|
@@ -7190,6 +8586,168 @@ module Rigor
|
|
|
7190
8586
|
fold_constant_tables(acc, file_index)
|
|
7191
8587
|
end
|
|
7192
8588
|
|
|
8589
|
+
# ADR-119 WD1 — the `possible_*` / `contested_*` siblings of {Scope::DiscoveryIndex::SIBLINGS}, carried as ONE
|
|
8590
|
+
# `{sibling name => table}` Hash (`acc[:siblings]`) through the accumulator, the fold, the compact-header
|
|
8591
|
+
# rename, the finalize and the seed bundle, so no copy path names a sibling and none can drop one. A pre-36
|
|
8592
|
+
# bundle carries the empty siblings of the walk before WD3's producers, and the SCHEMA bump makes such a blob
|
|
8593
|
+
# a cold rebuild. Called BEFORE the file's members fold, because the `possible` rule reads the accumulator's
|
|
8594
|
+
# member as it stood ({#fold_file_siblings}).
|
|
8595
|
+
def fold_siblings(acc, file_index)
|
|
8596
|
+
fold_file_siblings(acc, file_index[:siblings] || {},
|
|
8597
|
+
methods: file_index[:methods], def_nodes: file_index[:def_nodes],
|
|
8598
|
+
singleton_def_nodes: file_index[:singleton_def_nodes],
|
|
8599
|
+
method_visibilities: file_index[:method_visibilities])
|
|
8600
|
+
end
|
|
8601
|
+
|
|
8602
|
+
# ADR-119 WD3 — folds one file's siblings into the accumulator under each member's own fold, so the result is
|
|
8603
|
+
# the same in any file order and a bundle-served file folds as its live walk would:
|
|
8604
|
+
#
|
|
8605
|
+
# - `possible_discovered_methods` by {#fold_possible_methods} against `acc[:methods]` before this file's
|
|
8606
|
+
# `methods` join it;
|
|
8607
|
+
# - the def-node and visibility slots follow their writer, as the members fold later-wins: the file's contests
|
|
8608
|
+
# replace the accumulator's on every key the file writes ({#fold_slot_contests});
|
|
8609
|
+
# - every other sibling (the joined envelopes' contests among them) by union.
|
|
8610
|
+
def fold_file_siblings(acc, siblings, methods:, def_nodes:, singleton_def_nodes:, method_visibilities: {})
|
|
8611
|
+
target = acc[:siblings]
|
|
8612
|
+
siblings.each do |name, table|
|
|
8613
|
+
target[name] =
|
|
8614
|
+
case name
|
|
8615
|
+
when :possible_discovered_methods
|
|
8616
|
+
fold_possible_methods(target.fetch(name), acc[:methods], table, methods)
|
|
8617
|
+
when :contested_discovered_def_nodes then fold_slot_contests(target.fetch(name), def_nodes, table)
|
|
8618
|
+
when :contested_discovered_singleton_def_nodes
|
|
8619
|
+
fold_slot_contests(target.fetch(name), singleton_def_nodes, table)
|
|
8620
|
+
when :contested_discovered_method_visibilities
|
|
8621
|
+
fold_slot_contests(target.fetch(name), method_visibilities, table)
|
|
8622
|
+
else union_sibling_value(target.fetch(name), table)
|
|
8623
|
+
end
|
|
8624
|
+
end
|
|
8625
|
+
end
|
|
8626
|
+
|
|
8627
|
+
# ADR-119 WD3 — the `possible_discovered_methods` fold, per name and side: `P' = (P − C_f) ∪ (P_f − C)`,
|
|
8628
|
+
# where `C = member − P` is what the accumulator (or seed) answers certainly and `C_f = member_f − P_f` what
|
|
8629
|
+
# the file does. A side some contribution supplies certainly is never possible, whatever the order the files
|
|
8630
|
+
# fold in. Returns `possible` itself when nothing changes; otherwise a copy (the rows are copied once).
|
|
8631
|
+
def fold_possible_methods(possible, members, file_possible, file_members)
|
|
8632
|
+
result = strip_file_certain(possible, file_possible, file_members)
|
|
8633
|
+
add_file_possible(result, possible, members, file_possible)
|
|
8634
|
+
end
|
|
8635
|
+
|
|
8636
|
+
# `P − C_f`: `possible` less the sides the file answers certainly; `possible` itself when nothing goes.
|
|
8637
|
+
def strip_file_certain(possible, file_possible, file_members)
|
|
8638
|
+
result = possible
|
|
8639
|
+
possible.each do |class_name, row|
|
|
8640
|
+
file_row = file_members[class_name]
|
|
8641
|
+
next if file_row.nil?
|
|
8642
|
+
|
|
8643
|
+
row.each do |method_name, kind|
|
|
8644
|
+
certain = kind_minus(file_row[method_name], file_possible.dig(class_name, method_name))
|
|
8645
|
+
remaining = kind_minus(kind, certain)
|
|
8646
|
+
next if remaining == kind
|
|
8647
|
+
|
|
8648
|
+
result = possible.transform_values(&:dup) if result.equal?(possible)
|
|
8649
|
+
remaining.nil? ? result[class_name].delete(method_name) : (result[class_name][method_name] = remaining)
|
|
8650
|
+
end
|
|
8651
|
+
result.delete(class_name) if result[class_name].empty?
|
|
8652
|
+
end
|
|
8653
|
+
result
|
|
8654
|
+
end
|
|
8655
|
+
|
|
8656
|
+
# `∪ (P_f − C)`: the file's possible sides the accumulator (`members` less `possible`) does not answer
|
|
8657
|
+
# certainly. Copies `result` first when it is still `possible`.
|
|
8658
|
+
def add_file_possible(result, possible, members, file_possible)
|
|
8659
|
+
file_possible.each do |class_name, row|
|
|
8660
|
+
row.each do |method_name, kind|
|
|
8661
|
+
added = kind_minus(kind, kind_minus(members.dig(class_name, method_name),
|
|
8662
|
+
possible.dig(class_name, method_name)))
|
|
8663
|
+
next if added.nil?
|
|
8664
|
+
|
|
8665
|
+
merged = merge_sibling_kind(result.dig(class_name, method_name), added)
|
|
8666
|
+
next if merged == result.dig(class_name, method_name)
|
|
8667
|
+
|
|
8668
|
+
result = possible.transform_values(&:dup) if result.equal?(possible)
|
|
8669
|
+
(result[class_name] ||= {})[method_name] = merged
|
|
8670
|
+
end
|
|
8671
|
+
end
|
|
8672
|
+
result
|
|
8673
|
+
end
|
|
8674
|
+
|
|
8675
|
+
# The sides of `kind` that `removed` does not cover (nil when none); either may be nil.
|
|
8676
|
+
def kind_minus(kind, removed)
|
|
8677
|
+
return kind if removed.nil? || kind.nil?
|
|
8678
|
+
return nil if removed == Scope::DiscoveryIndex::METHOD_KIND_BOTH || removed == kind
|
|
8679
|
+
|
|
8680
|
+
kind == Scope::DiscoveryIndex::METHOD_KIND_BOTH ? without_kind(kind, removed) : kind
|
|
8681
|
+
end
|
|
8682
|
+
|
|
8683
|
+
# ADR-119 WD3 — a single-valued slot's contests after a later-wins fold: the slots the file writes take the
|
|
8684
|
+
# file's contests (`file_contested`), every other key keeps the accumulator's (`sitting`). `file_slots` is the
|
|
8685
|
+
# file's `{class => {name => value}}` table. Returns `sitting` itself when nothing changes.
|
|
8686
|
+
def fold_slot_contests(sitting, file_slots, file_contested)
|
|
8687
|
+
stale = sitting.empty? ? nil : sitting.select { |class_name, name| file_slots[class_name]&.key?(name) }
|
|
8688
|
+
return sitting if (stale.nil? || stale.empty?) && file_contested.empty?
|
|
8689
|
+
|
|
8690
|
+
result = stale.nil? || stale.empty? ? sitting.dup : sitting - stale
|
|
8691
|
+
result.merge(file_contested)
|
|
8692
|
+
end
|
|
8693
|
+
|
|
8694
|
+
def union_paths(sitting, arriving)
|
|
8695
|
+
return sitting if arriving.empty?
|
|
8696
|
+
return arriving if sitting.empty?
|
|
8697
|
+
|
|
8698
|
+
sitting | arriving
|
|
8699
|
+
end
|
|
8700
|
+
|
|
8701
|
+
# The union of two sibling tables or entries. A Hash merges per key and a Set or Array unions; two scalars
|
|
8702
|
+
# that disagree are a `discovered_methods`-style kind and combine to `:both`, as {#merge_method_kinds} does.
|
|
8703
|
+
def union_sibling_value(sitting, arriving)
|
|
8704
|
+
return arriving if sitting.nil? || sitting.empty?
|
|
8705
|
+
return sitting if arriving.nil? || arriving.empty?
|
|
8706
|
+
|
|
8707
|
+
case sitting
|
|
8708
|
+
when Hash then sitting.merge(arriving) { |_key, left, right| union_sibling_value(left, right) }
|
|
8709
|
+
when Set, Array then sitting | arriving
|
|
8710
|
+
else sitting == arriving ? sitting : Scope::DiscoveryIndex::METHOD_KIND_BOTH
|
|
8711
|
+
end
|
|
8712
|
+
end
|
|
8713
|
+
|
|
8714
|
+
# The sibling tables keyed by file path rather than by class name, which a compact-header rename leaves alone
|
|
8715
|
+
# exactly as it leaves the `deferred_ranges` member alone.
|
|
8716
|
+
SIBLINGS_KEYED_BY_PATH = %i[possible_discovered_deferred_ranges].freeze
|
|
8717
|
+
private_constant :SIBLINGS_KEYED_BY_PATH
|
|
8718
|
+
|
|
8719
|
+
# {#apply_compact_header_renames!} for the siblings: a class-keyed Hash re-keys (colliding keys union), and a
|
|
8720
|
+
# Set of key paths renames each path's first element, the class.
|
|
8721
|
+
def rename_siblings(siblings, renames)
|
|
8722
|
+
siblings.to_h do |name, table|
|
|
8723
|
+
next [name, table] if SIBLINGS_KEYED_BY_PATH.include?(name) || table.empty?
|
|
8724
|
+
|
|
8725
|
+
renamed = case table
|
|
8726
|
+
when Set then table.to_set { |path| [rename_compact_name(renames, path.first), *path.drop(1)] }
|
|
8727
|
+
else rename_sibling_hash(table, renames)
|
|
8728
|
+
end
|
|
8729
|
+
[name, renamed]
|
|
8730
|
+
end
|
|
8731
|
+
end
|
|
8732
|
+
|
|
8733
|
+
def rename_sibling_hash(table, renames)
|
|
8734
|
+
table.each_with_object({}) do |(class_name, entry), out|
|
|
8735
|
+
renamed = rename_compact_name(renames, class_name)
|
|
8736
|
+
out[renamed] = out.key?(renamed) ? union_sibling_value(out[renamed], entry) : entry
|
|
8737
|
+
end
|
|
8738
|
+
end
|
|
8739
|
+
|
|
8740
|
+
def freeze_siblings(siblings)
|
|
8741
|
+
siblings.each_value { |table| freeze_sibling_table(table) }
|
|
8742
|
+
siblings.freeze
|
|
8743
|
+
end
|
|
8744
|
+
|
|
8745
|
+
def freeze_sibling_table(table)
|
|
8746
|
+
table.each_value { |entry| freeze_sibling_table(entry) } if table.is_a?(Hash)
|
|
8747
|
+
table.each { |entry| entry.freeze if entry.is_a?(Array) } if table.is_a?(Set)
|
|
8748
|
+
table.freeze
|
|
8749
|
+
end
|
|
8750
|
+
|
|
7193
8751
|
# Issue #1120 — a union, so the fold is order-independent and a bundle-served file folds exactly as its live
|
|
7194
8752
|
# walk would. The slot stays nil until some file refines something (see {#finalize_def_index}).
|
|
7195
8753
|
def fold_refinements(acc, file_refinements)
|
|
@@ -7264,6 +8822,7 @@ module Rigor
|
|
|
7264
8822
|
# resolves the class as it did before the ordering fix, which is this table's empty state.
|
|
7265
8823
|
accumulate_prepend_lists(acc[:prepends], file_index[:prepends] || {})
|
|
7266
8824
|
accumulate_extend_lists(acc[:extends], file_index[:extends] || {})
|
|
8825
|
+
accumulate_unpositioned(acc[:unpositioned_mixins], file_index[:unpositioned_mixins] || {})
|
|
7267
8826
|
end
|
|
7268
8827
|
|
|
7269
8828
|
# The `extends` half stores singleton-ancestor search order ({#record_extend_targets}), so a file
|
|
@@ -7288,14 +8847,14 @@ module Rigor
|
|
|
7288
8847
|
# class. Shared with {#accumulate_extend_lists}' shape deliberately — the two tables answer the
|
|
7289
8848
|
# `prepend` question on the two sides of the class object, and both fold nearest-first.
|
|
7290
8849
|
def accumulate_prepend_lists(target, additions)
|
|
7291
|
-
additions.each { |cn, mods| target[cn] = (mods + (target[cn] || []))
|
|
8850
|
+
additions.each { |cn, mods| target[cn] = (mods + (target[cn] || [])) }
|
|
7292
8851
|
end
|
|
7293
8852
|
|
|
7294
8853
|
# ADR-85 WD2 — converts a file's live single-file index + its class table into a Marshal-clean seed
|
|
7295
8854
|
# bundle: the plain-data tables verbatim, the def-node tables re-expressed as `[node_id, name,
|
|
7296
8855
|
# fingerprint]` triples (the path is the bundle key), the class-source names (path implicit), and the
|
|
7297
8856
|
# content digest that gates the bundle's reuse.
|
|
7298
|
-
def build_seed_bundle(file_index, file_classes, digest, code_fingerprint)
|
|
8857
|
+
def build_seed_bundle(file_index, file_classes, digest, code_fingerprint) # rubocop:disable Metrics/AbcSize -- one read per bundle slot
|
|
7299
8858
|
{
|
|
7300
8859
|
digest: digest,
|
|
7301
8860
|
# B1 — the comment-stripped code fingerprint, so a recheck can prove this file's edit was
|
|
@@ -7324,6 +8883,9 @@ module Rigor
|
|
|
7324
8883
|
# the bundle stays Marshal-clean and a warm incremental file orders its prepends the way a cold
|
|
7325
8884
|
# walk of it does.
|
|
7326
8885
|
prepends: file_index[:prepends],
|
|
8886
|
+
# The mixin edges whose order is not a fact — plain `{class name => Array[String]}` data, so the
|
|
8887
|
+
# bundle stays Marshal-clean and a warm file flags the edges its cold walk flags.
|
|
8888
|
+
unpositioned_mixins: file_index[:unpositioned_mixins],
|
|
7327
8889
|
method_visibilities: file_index[:method_visibilities],
|
|
7328
8890
|
methods: file_index[:methods],
|
|
7329
8891
|
# Issue #992 — plain `{class name => {[kind, name] => [min, max, required_keywords] | :opaque}}`
|
|
@@ -7345,14 +8907,16 @@ module Rigor
|
|
|
7345
8907
|
# nothing), so the bundle stays Marshal-clean.
|
|
7346
8908
|
refinements: file_index[:refinements],
|
|
7347
8909
|
# Issue #1367 — the file's {GlobalWriteCensus}, a Set of frozen Arrays, which Marshal round-trips.
|
|
7348
|
-
global_write_census: file_index[:global_write_census]
|
|
8910
|
+
global_write_census: file_index[:global_write_census],
|
|
8911
|
+
# ADR-119 WD1 — `{sibling name => table}`, plain Hashes, Sets and rows, so the bundle stays Marshal-clean.
|
|
8912
|
+
siblings: file_index[:siblings]
|
|
7349
8913
|
}
|
|
7350
8914
|
end
|
|
7351
8915
|
|
|
7352
8916
|
# ADR-85 WD2 — reconstitutes a cached bundle into a single-file index {#fold_file_index} folds: the
|
|
7353
8917
|
# def-node triples become {DefHandle}s bound to this file's `path`, and the class-source names become a
|
|
7354
8918
|
# `{name => Set[path]}` table (the shape `accumulate_project_index` produces).
|
|
7355
|
-
def bundle_to_file_index(bundle, path)
|
|
8919
|
+
def bundle_to_file_index(bundle, path) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity -- one read per bundle slot
|
|
7356
8920
|
{
|
|
7357
8921
|
def_nodes: bundle_defs_to_handles(bundle[:def_nodes], path),
|
|
7358
8922
|
singleton_def_nodes: bundle_defs_to_handles(bundle[:singleton_def_nodes], path),
|
|
@@ -7368,6 +8932,8 @@ module Rigor
|
|
|
7368
8932
|
prepends: bundle[:prepends] || {},
|
|
7369
8933
|
# #526 — pre-extends bundles lack the key; default `{}` keeps the fold total.
|
|
7370
8934
|
extends: bundle[:extends] || {},
|
|
8935
|
+
# A pre-32 bundle lacks the key; the SCHEMA bump rebuilds it cold, and the fold defaults it anyway.
|
|
8936
|
+
unpositioned_mixins: bundle[:unpositioned_mixins],
|
|
7371
8937
|
method_visibilities: bundle[:method_visibilities],
|
|
7372
8938
|
methods: bundle[:methods],
|
|
7373
8939
|
parameter_envelopes: bundle[:parameter_envelopes] || {},
|
|
@@ -7384,7 +8950,9 @@ module Rigor
|
|
|
7384
8950
|
# Issue #1120 — absent from a pre-28 bundle, which the SCHEMA bump rebuilds cold.
|
|
7385
8951
|
refinements: bundle[:refinements],
|
|
7386
8952
|
# Issue #1367 — absent from a pre-29 bundle, which the SCHEMA bump rebuilds cold.
|
|
7387
|
-
global_write_census: bundle[:global_write_census] || GlobalWriteCensus::EMPTY
|
|
8953
|
+
global_write_census: bundle[:global_write_census] || GlobalWriteCensus::EMPTY,
|
|
8954
|
+
# ADR-119 WD1 — absent from a pre-35 bundle, which the SCHEMA bump rebuilds cold.
|
|
8955
|
+
siblings: bundle[:siblings] || Scope::DiscoveryIndex::EMPTY_SIBLINGS
|
|
7388
8956
|
}
|
|
7389
8957
|
end
|
|
7390
8958
|
|
|
@@ -7415,7 +8983,8 @@ module Rigor
|
|
|
7415
8983
|
def new_def_index_accumulator
|
|
7416
8984
|
{ def_nodes: {}, def_nestings: {}.compare_by_identity,
|
|
7417
8985
|
singleton_def_nodes: {}, def_sources: {}, singleton_def_sources: {},
|
|
7418
|
-
superclasses: {}, header_nestings: {}, includes: {}, prepends: {}, extends: {},
|
|
8986
|
+
superclasses: {}, header_nestings: {}, includes: {}, prepends: {}, extends: {}, unpositioned_mixins: {},
|
|
8987
|
+
method_visibilities: {},
|
|
7419
8988
|
methods: {},
|
|
7420
8989
|
deferred_ranges: {},
|
|
7421
8990
|
parameter_envelopes: {}, class_sources: {},
|
|
@@ -7424,7 +8993,9 @@ module Rigor
|
|
|
7424
8993
|
constant_writes: {},
|
|
7425
8994
|
data_member_layouts: {}, struct_member_layouts: {},
|
|
7426
8995
|
# Issue #1367 — the project's {GlobalWriteCensus}.
|
|
7427
|
-
global_write_census: Set.new
|
|
8996
|
+
global_write_census: Set.new,
|
|
8997
|
+
# ADR-119 WD1 — the pair siblings, `{sibling name => table}` (see {#fold_siblings}).
|
|
8998
|
+
siblings: Scope::DiscoveryIndex::EMPTY_SIBLINGS.dup }
|
|
7428
8999
|
end
|
|
7429
9000
|
|
|
7430
9001
|
# Post-processes and freezes a fully-folded def-index accumulator.
|
|
@@ -7437,27 +9008,42 @@ module Rigor
|
|
|
7437
9008
|
# Issue #644 — resolve the cross-file constant-reassignment rule here, where the whole project's
|
|
7438
9009
|
# write census is known, and turn the surviving literals into their published `Type::Constant`.
|
|
7439
9010
|
acc[:constant_values], acc[:constant_sources] = finalize_constant_writes(acc[:constant_writes])
|
|
7440
|
-
|
|
9011
|
+
# ADR-119 WD1 — the two passes below rewrite `def_nodes`, `singleton_def_nodes` and `methods`, and each carries
|
|
9012
|
+
# its sibling along. `subtract_def_methods` drops `def`-declared names from `methods`, and
|
|
9013
|
+
# {#subtract_sibling_methods!} applies the same drop to `possible_discovered_methods`. The extends fold copies
|
|
9014
|
+
# an extended module's instance defs onto the extender's singleton, and follows the siblings the same way (a
|
|
9015
|
+
# copy is possible or contested when its edge is listed in `unpositioned_mixins` or its source was).
|
|
9016
|
+
# Until a producer fills a sibling and an edge is listed, there is nothing for either pass to follow.
|
|
9017
|
+
fold_project_extends(acc)
|
|
7441
9018
|
# Cross-file method suppression is for the project's OWN accessors (attr_* / define_method / alias) — NOT for
|
|
7442
9019
|
# plain `def`s. A cross-file `def` on a class is exactly the ADR-17 monkey-patch case the undefined-method rule
|
|
7443
9020
|
# deliberately surfaces (fire + def-site annotation, nudging `pre_eval:`), so dropping the `def`-declared names
|
|
7444
9021
|
# keeps that contract intact while still letting `attr_reader :x` in one file suppress a false undefined-method
|
|
7445
9022
|
# for `obj.x` in another.
|
|
7446
9023
|
acc[:methods] = subtract_def_methods(acc[:methods], acc[:def_nodes])
|
|
9024
|
+
subtract_sibling_methods!(acc)
|
|
7447
9025
|
finalize_call_surface_tables(acc)
|
|
7448
|
-
%i[def_nodes singleton_def_nodes def_sources singleton_def_sources includes prepends
|
|
7449
|
-
methods parameter_envelopes class_sources constant_sources deferred_ranges].each do |key|
|
|
9026
|
+
%i[def_nodes singleton_def_nodes def_sources singleton_def_sources includes prepends
|
|
9027
|
+
method_visibilities methods parameter_envelopes class_sources constant_sources deferred_ranges].each do |key|
|
|
7450
9028
|
acc[key].each_value(&:freeze)
|
|
7451
9029
|
end
|
|
7452
9030
|
acc.transform_values(&:freeze)
|
|
7453
9031
|
end
|
|
7454
9032
|
|
|
9033
|
+
# The project-wide extends fold over the accumulator's tables and siblings.
|
|
9034
|
+
def fold_project_extends(acc)
|
|
9035
|
+
fold_extends_into_singleton_tables(acc[:extends], acc[:def_nodes], acc[:singleton_def_nodes], acc[:methods],
|
|
9036
|
+
siblings: acc[:siblings], unpositioned: acc[:unpositioned_mixins])
|
|
9037
|
+
end
|
|
9038
|
+
|
|
7455
9039
|
# The two whole-project tables only call rules read: the issue #992 envelope table gains its project-wide
|
|
7456
9040
|
# key, and the issue #1120 refinement table, nil until some file refines something, settles to a frozen one.
|
|
7457
9041
|
def finalize_call_surface_tables(acc)
|
|
7458
9042
|
acc[:parameter_envelopes][Scope::DiscoveryIndex::ENVELOPE_PROJECT_WIDE] ||= {}
|
|
7459
9043
|
acc[:refinements] = freeze_refinements(acc[:refinements])
|
|
7460
9044
|
acc[:global_write_census] = acc[:global_write_census].freeze
|
|
9045
|
+
acc[:unpositioned_mixins].each_value { |sides| sides.each_value(&:freeze).freeze }
|
|
9046
|
+
freeze_siblings(acc[:siblings])
|
|
7461
9047
|
end
|
|
7462
9048
|
|
|
7463
9049
|
# Removes, per class, the method names that have a project `def` node, leaving only
|
|
@@ -7478,6 +9064,30 @@ module Rigor
|
|
|
7478
9064
|
end
|
|
7479
9065
|
end
|
|
7480
9066
|
|
|
9067
|
+
# ADR-119 WD1 — the `possible` copy of `methods` loses the INSTANCE half of a name that has a project `def`
|
|
9068
|
+
# (`:instance` goes, `:both` becomes `:singleton`), so `possible_discovered_methods` stays a subset of its
|
|
9069
|
+
# member after the rule above. A `:singleton` entry stays: the `def` is the instance side, and the member
|
|
9070
|
+
# keeps the name's singleton half. Only that sibling is rewritten.
|
|
9071
|
+
def subtract_sibling_methods!(acc)
|
|
9072
|
+
possible = acc[:siblings][:possible_discovered_methods]
|
|
9073
|
+
return if possible.nil?
|
|
9074
|
+
|
|
9075
|
+
acc[:siblings] =
|
|
9076
|
+
acc[:siblings].merge(possible_discovered_methods: subtract_sibling_instance_defs(possible, acc[:def_nodes]))
|
|
9077
|
+
end
|
|
9078
|
+
|
|
9079
|
+
def subtract_sibling_instance_defs(possible, def_nodes)
|
|
9080
|
+
possible.each_with_object({}) do |(class_name, table), out|
|
|
9081
|
+
defs = def_nodes[class_name] || {}
|
|
9082
|
+
kept = table.each_with_object({}) do |(method_name, kind), acc|
|
|
9083
|
+
next acc[method_name] = kind unless defs.key?(method_name)
|
|
9084
|
+
|
|
9085
|
+
acc[method_name] = :singleton unless kind == :instance
|
|
9086
|
+
end
|
|
9087
|
+
out[class_name] = kept unless kept.empty?
|
|
9088
|
+
end
|
|
9089
|
+
end
|
|
9090
|
+
|
|
7481
9091
|
# Folds one file's class-keyed indexes into the cross-file accumulator. `method_visibilities` (ADR-35) is
|
|
7482
9092
|
# collected here so the override-visibility-reduced rule can read an ancestor's visibility declared in a sibling
|
|
7483
9093
|
# file.
|
|
@@ -7485,26 +9095,43 @@ module Rigor
|
|
|
7485
9095
|
# One combined descent yields both the methods existence table and the def-node table; the latter is also
|
|
7486
9096
|
# consumed by `record_class_sources`, so a def-dense file is walked once here instead of three times (methods +
|
|
7487
9097
|
# def-nodes ×2). See {#build_methods_and_def_nodes}.
|
|
7488
|
-
file_methods, file_def_nodes, file_envelopes, file_refinements
|
|
9098
|
+
file_methods, file_def_nodes, file_envelopes, file_refinements, file_singleton_def_nodes, certainty =
|
|
9099
|
+
walk_def_contributions(acc, path, root)
|
|
7489
9100
|
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
|
|
7490
9101
|
fold_parameter_envelopes(acc, file_envelopes)
|
|
7491
9102
|
fold_refinements(acc, file_refinements)
|
|
7492
9103
|
# Issue #681 — node-identity keyed, so this is a flat union: no two files can contribute the same key.
|
|
7493
|
-
|
|
9104
|
+
walked = declaration_walk_tables(root, path)
|
|
9105
|
+
acc[:def_nestings].merge!(walked.fetch(:def_nestings))
|
|
7494
9106
|
# ADR-46 slice 4 (singleton) — record the singleton-side `"path:line"` sources alongside the nodes,
|
|
7495
9107
|
# the exact mirror of the instance-side `merge_discovered_defs`, so a class/singleton-method body edit
|
|
7496
9108
|
# produces a changed `"Class.method"` fingerprint pair (and its call sites a symbol edge) instead of
|
|
7497
9109
|
# silently degrading to the file's full ancestry closure.
|
|
7498
9110
|
merge_discovered_defs(acc[:singleton_def_nodes], acc[:singleton_def_sources], path,
|
|
7499
|
-
|
|
7500
|
-
superclasses
|
|
9111
|
+
file_singleton_def_nodes)
|
|
9112
|
+
superclasses = walked.fetch(:superclasses)
|
|
7501
9113
|
acc[:superclasses].merge!(superclasses)
|
|
7502
|
-
merge_header_nestings(acc[:header_nestings], header_nestings)
|
|
9114
|
+
merge_header_nestings(acc[:header_nestings], walked.fetch(:header_nestings))
|
|
7503
9115
|
ancestry_keys = fold_file_mixin_tables(acc, root)
|
|
7504
9116
|
record_file_positions(acc, path, root, superclasses, ancestry_keys, file_def_nodes)
|
|
7505
9117
|
merge_constant_literal_tables(acc, root, path)
|
|
7506
|
-
merge_class_keyed_index_tables(acc, root, file_methods)
|
|
7507
|
-
merge_member_layout_tables(acc,
|
|
9118
|
+
merge_class_keyed_index_tables(acc, root, file_methods, certainty)
|
|
9119
|
+
merge_member_layout_tables(acc, walked)
|
|
9120
|
+
end
|
|
9121
|
+
|
|
9122
|
+
# The two def-contribution walks of {#accumulate_project_index}, over ONE {Certainty} classification, with the
|
|
9123
|
+
# file's siblings folded into the accumulator BEFORE its members join it (ADR-119 WD3: the `possible` rule
|
|
9124
|
+
# reads the accumulator's member as it stood). Returns `[methods, def_nodes, envelopes, refinements,
|
|
9125
|
+
# singleton_def_nodes, certainty]`, the classification passed on for the visibility walk.
|
|
9126
|
+
def walk_def_contributions(acc, path, root)
|
|
9127
|
+
certainty = Certainty.possible_nodes(root)
|
|
9128
|
+
file_methods, file_def_nodes, file_envelopes, file_refinements, file_siblings =
|
|
9129
|
+
build_methods_and_def_nodes(root, path, certainty: certainty)
|
|
9130
|
+
file_singleton_def_nodes, file_singleton_contested = build_singleton_def_tables(root, certainty: certainty)
|
|
9131
|
+
fold_file_siblings(acc, file_siblings.merge(contested_discovered_singleton_def_nodes: file_singleton_contested),
|
|
9132
|
+
methods: file_methods, def_nodes: file_def_nodes,
|
|
9133
|
+
singleton_def_nodes: file_singleton_def_nodes)
|
|
9134
|
+
[file_methods, file_def_nodes, file_envelopes, file_refinements, file_singleton_def_nodes, certainty]
|
|
7508
9135
|
end
|
|
7509
9136
|
|
|
7510
9137
|
# Issue #1123 — this file's three instance- / singleton-side module lists, folded into the
|
|
@@ -7516,12 +9143,24 @@ module Rigor
|
|
|
7516
9143
|
# {#accumulate_project_index} to hold its ABC budget.
|
|
7517
9144
|
def fold_file_mixin_tables(acc, root)
|
|
7518
9145
|
mixin = mixin_tables(root)
|
|
9146
|
+
extend_side = extend_tables(root)
|
|
7519
9147
|
accumulate_include_lists(acc[:includes], mixin[:includes])
|
|
7520
9148
|
accumulate_prepend_lists(acc[:prepends], mixin[:prepends])
|
|
7521
|
-
accumulate_extend_lists(acc[:extends],
|
|
9149
|
+
accumulate_extend_lists(acc[:extends], extend_side[:extends])
|
|
9150
|
+
accumulate_unpositioned(acc[:unpositioned_mixins], mixin[:unpositioned])
|
|
9151
|
+
accumulate_unpositioned(acc[:unpositioned_mixins], extend_side[:unpositioned])
|
|
7522
9152
|
mixin[:includes].merge(mixin[:prepends]) { |_cn, included_mods, _prepends| included_mods }
|
|
7523
9153
|
end
|
|
7524
9154
|
|
|
9155
|
+
# {Scope::DiscoveryIndex#unpositioned_mixins} folds by union, so the fold is order-independent and a
|
|
9156
|
+
# bundle-served file folds exactly as its live walk would.
|
|
9157
|
+
def accumulate_unpositioned(target, additions)
|
|
9158
|
+
additions.each do |class_name, sides|
|
|
9159
|
+
sitting = (target[class_name] ||= {})
|
|
9160
|
+
sides.each { |side, names| sitting[side] = sitting.key?(side) ? (sitting[side] | names) : names.dup }
|
|
9161
|
+
end
|
|
9162
|
+
end
|
|
9163
|
+
|
|
7525
9164
|
# Issue #644 — folds one file's publication census into the cross-file accumulator, keyed by
|
|
7526
9165
|
# (name, path) (kept out of {#accumulate_project_index} to hold its ABC budget). The conflict rule that
|
|
7527
9166
|
# makes the fold order irrelevant runs at {#finalize_constant_writes}. Issue #1367 — the same walk carries the
|
|
@@ -7536,16 +9175,24 @@ module Rigor
|
|
|
7536
9175
|
|
|
7537
9176
|
# Folds one file's Data + Struct member-layout tables into the cross-file accumulator (kept out of
|
|
7538
9177
|
# {#accumulate_project_index} to hold its ABC budget).
|
|
7539
|
-
def merge_member_layout_tables(acc,
|
|
7540
|
-
acc[:data_member_layouts].merge!(
|
|
7541
|
-
acc[:struct_member_layouts].merge!(
|
|
9178
|
+
def merge_member_layout_tables(acc, walked)
|
|
9179
|
+
acc[:data_member_layouts].merge!(walked.fetch(:data_member_layouts))
|
|
9180
|
+
acc[:struct_member_layouts].merge!(walked.fetch(:struct_member_layouts))
|
|
7542
9181
|
end
|
|
7543
9182
|
|
|
7544
9183
|
# Folds the per-class method-visibility and method-existence tables of one file into the cross-file accumulator
|
|
7545
9184
|
# (kept out of {#accumulate_project_index} to hold its ABC budget). `file_methods` is the existence table from the
|
|
7546
9185
|
# combined methods/def-nodes descent.
|
|
7547
|
-
|
|
7548
|
-
|
|
9186
|
+
#
|
|
9187
|
+
# ADR-119 WD3 — the visibility slots fold later-wins, so their contests follow the file that wrote them
|
|
9188
|
+
# ({#fold_slot_contests}), as the def-node slots' do.
|
|
9189
|
+
def merge_class_keyed_index_tables(acc, root, file_methods, certainty)
|
|
9190
|
+
file_visibilities, file_contested = build_method_visibility_tables(root, certainty: certainty)
|
|
9191
|
+
siblings = acc[:siblings]
|
|
9192
|
+
siblings[:contested_discovered_method_visibilities] = fold_slot_contests(
|
|
9193
|
+
siblings[:contested_discovered_method_visibilities], file_visibilities, file_contested
|
|
9194
|
+
)
|
|
9195
|
+
file_visibilities.each do |class_name, table|
|
|
7549
9196
|
(acc[:method_visibilities][class_name] ||= {}).merge!(table)
|
|
7550
9197
|
end
|
|
7551
9198
|
file_methods.each do |class_name, table|
|
|
@@ -7630,7 +9277,7 @@ module Rigor
|
|
|
7630
9277
|
|
|
7631
9278
|
def constant_write_census(root)
|
|
7632
9279
|
tables = CensusTables.new(writes: {}, seen: Set.new, declared: Set.new, aliases: {},
|
|
7633
|
-
write_census: GlobalWriteCensus::Collector.new)
|
|
9280
|
+
write_census: GlobalWriteCensus::Collector.new(InEffectRefinements.new(root)))
|
|
7634
9281
|
walk_constant_write_census(root, [], tables)
|
|
7635
9282
|
tables
|
|
7636
9283
|
end
|
|
@@ -7657,8 +9304,7 @@ module Rigor
|
|
|
7657
9304
|
tables.write_census.visit(node, top_level: qualified_prefix.empty?)
|
|
7658
9305
|
end
|
|
7659
9306
|
|
|
7660
|
-
rebound = rebound_block_self(node, qualified_prefix, nil, meta_owner,
|
|
7661
|
-
rebound_self_base(self_owner))
|
|
9307
|
+
rebound = rebound_block_self(node, qualified_prefix, nil, meta_owner, self_owner)
|
|
7662
9308
|
# A `ConstantWriteNode`'s only child is its rvalue, so this reaches exactly the call whose block the
|
|
7663
9309
|
# constant names — and nil everywhere else, leaving every other descent as it was.
|
|
7664
9310
|
#
|
|
@@ -8181,6 +9827,16 @@ module Rigor
|
|
|
8181
9827
|
end
|
|
8182
9828
|
end
|
|
8183
9829
|
|
|
9830
|
+
# {#rekey_class_table} for `unpositioned_mixins`: two bodies landing on one key keep BOTH sides' names,
|
|
9831
|
+
# where the Hash arm of {#combine_rekeyed_entries} would replace one side's list with the other's.
|
|
9832
|
+
def rekey_unpositioned(table, renames)
|
|
9833
|
+
table.each_with_object({}) do |(name, sides), out|
|
|
9834
|
+
out.merge!(rename_compact_name(renames, name) => sides) do |_key, sitting, arriving|
|
|
9835
|
+
sitting.merge(arriving) { |_side, a, b| a | b }
|
|
9836
|
+
end
|
|
9837
|
+
end
|
|
9838
|
+
end
|
|
9839
|
+
|
|
8184
9840
|
def rekey_parameter_envelopes(table, renames)
|
|
8185
9841
|
table.each_with_object({}) do |(name, entries), out|
|
|
8186
9842
|
out.merge!(rename_compact_name(renames, name) => entries) do |_key, sitting, arriving|
|
|
@@ -8221,7 +9877,7 @@ module Rigor
|
|
|
8221
9877
|
# definition — the pre-#728 answer for a name no site recorded — so it keeps unioning. A KEYED entry is
|
|
8222
9878
|
# the cref of the site that wrote that exact name, and the two sides are two different sites, so
|
|
8223
9879
|
# unioning their chains would hand each site's ancestor the other's namespace: `Wrap::Mixin` sorts
|
|
8224
|
-
# ahead of `::Mixin`, and
|
|
9880
|
+
# ahead of `::Mixin`, and the ancestor-name resolution takes the first known class as the sole
|
|
8225
9881
|
# resolution, so the top-level site's `include Mixin` silently became the `Wrap` one. That is not just
|
|
8226
9882
|
# a wrong class, it is a FALSE POSITIVE source — the two modules' same-named methods can differ in
|
|
8227
9883
|
# arity, and `call.wrong-arity` then fires on a correct program.
|
|
@@ -8309,6 +9965,8 @@ module Rigor
|
|
|
8309
9965
|
struct_member_layouts constant_writes].each do |key|
|
|
8310
9966
|
acc[key] = rekey_class_table(acc[key], renames)
|
|
8311
9967
|
end
|
|
9968
|
+
acc[:unpositioned_mixins] = rekey_unpositioned(acc[:unpositioned_mixins], renames)
|
|
9969
|
+
acc[:siblings] = rename_siblings(acc[:siblings], renames)
|
|
8312
9970
|
# Issue #992 — the envelope table cannot take {#combine_rekeyed_entries}' later-wins Hash merge: two
|
|
8313
9971
|
# bodies of one class landing on the same key are exactly the reopening whose disagreement must
|
|
8314
9972
|
# make a name opaque.
|
|
@@ -8486,10 +10144,11 @@ module Rigor
|
|
|
8486
10144
|
Source::ConstantPath.declaration_prefix(qualified_prefix, node.constant_path)
|
|
8487
10145
|
return false unless child_prefix
|
|
8488
10146
|
|
|
8489
|
-
if singleton_cref && !decl_nameable_under_cref?(node) && !self_decl&.any?
|
|
10147
|
+
if self_decl&.empty? || (singleton_cref && !decl_nameable_under_cref?(node) && !self_decl&.any?)
|
|
8490
10148
|
# `class D` below `class <<` opens `#<singleton>::D` — a real class nothing can
|
|
8491
10149
|
# name — so nothing is registered; the body still walks so nameable headers
|
|
8492
|
-
# below it re-anchor at a real cref.
|
|
10150
|
+
# below it re-anchor at a real cref. The same holds for `class self::D` where
|
|
10151
|
+
# `self` is opaque (`REGISTRY.first.class_eval { … }`): `self_decl` is `[]`.
|
|
8493
10152
|
if node.body
|
|
8494
10153
|
record_declarations(node.body, [], identity_table, discovered, renames,
|
|
8495
10154
|
nil, singleton_cref: true)
|
|
@@ -8551,7 +10210,7 @@ module Rigor
|
|
|
8551
10210
|
record_declarations(part, qualified_prefix, identity_table, discovered,
|
|
8552
10211
|
EMPTY_RENAMES, self_prefix, singleton_cref: singleton_cref)
|
|
8553
10212
|
end
|
|
8554
|
-
if (body = rvalue.block
|
|
10213
|
+
if (body = rvalue.block.is_a?(Prism::BlockNode) && rvalue.block.body)
|
|
8555
10214
|
record_declarations(body, qualified_prefix, identity_table, discovered,
|
|
8556
10215
|
EMPTY_RENAMES, child_prefix, singleton_cref: singleton_cref)
|
|
8557
10216
|
end
|
|
@@ -8786,11 +10445,22 @@ module Rigor
|
|
|
8786
10445
|
propagate(node.expression, table, current_scope)
|
|
8787
10446
|
propagate(node.rescue_expression, table, current_scope.forget_error_info.forget_last_status)
|
|
8788
10447
|
when Prism::PostExecutionNode then propagate_end_body(node, table, current_scope)
|
|
8789
|
-
else
|
|
8790
|
-
node.rigor_each_child { |child| propagate(child, table, current_scope) }
|
|
10448
|
+
else propagate_children(node, table, current_scope)
|
|
8791
10449
|
end
|
|
8792
10450
|
end
|
|
8793
10451
|
|
|
10452
|
+
SELF_OPENING_BODIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
10453
|
+
private_constant :SELF_OPENING_BODIES
|
|
10454
|
+
|
|
10455
|
+
# Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
|
|
10456
|
+
# unknown-`self` mark ({Scope#block_self_unknown?}) stops at it, as the evaluator's fresh body scope does.
|
|
10457
|
+
# Every other node hands its children the scope as is.
|
|
10458
|
+
def propagate_children(node, table, current_scope)
|
|
10459
|
+
child_scope =
|
|
10460
|
+
SELF_OPENING_BODIES.include?(node.class) ? current_scope.with_block_self_unknown(false) : current_scope
|
|
10461
|
+
node.rigor_each_child { |child| propagate(child, table, child_scope) }
|
|
10462
|
+
end
|
|
10463
|
+
|
|
8794
10464
|
# Issue #1361 — the block of `Thread.new`, `Fiber.new`, `define_method` and the other calls
|
|
8795
10465
|
# {FreshFrameBlocks.fresh_entry?} names does not read the match-global narrowing of the body it is written in.
|
|
8796
10466
|
# The evaluator enters it as {FreshFrameBlocks.entry} gives ({MatchRebinding.block_entry}), but a block in a
|
|
@@ -8837,12 +10507,117 @@ module Rigor
|
|
|
8837
10507
|
def unentered_block_entry(node, block, table, current_scope)
|
|
8838
10508
|
return current_scope unless block.is_a?(Prism::BlockNode) && !table.key?(block)
|
|
8839
10509
|
|
|
10510
|
+
narrowing = unentered_block_narrowing(node, current_scope)
|
|
8840
10511
|
current_scope = Narrowing.safe_navigation_block_scope(node, current_scope)
|
|
8841
10512
|
# Issue #1429 — nor a guard's narrowing of a global or constant the body may run after code rebinds.
|
|
8842
10513
|
current_scope = GuardRebinding.block_entry(current_scope, block, node)
|
|
8843
|
-
|
|
10514
|
+
entry =
|
|
10515
|
+
if FreshFrameBlocks.fresh_entry?(node, current_scope)
|
|
10516
|
+
FreshFrameBlocks.entry(current_scope, node)
|
|
10517
|
+
else
|
|
10518
|
+
LastLine.block_entry(FreshFrameBlocks.closure_entry(current_scope, block, node), block, node)
|
|
10519
|
+
end
|
|
10520
|
+
anonymous = AnonymousMetaClass.name_for(node, current_scope.source_path)
|
|
10521
|
+
return unentered_meta_class_body_entry(block, entry, anonymous, table) if anonymous
|
|
10522
|
+
|
|
10523
|
+
refined = refined_body_name(node, current_scope)
|
|
10524
|
+
record_class_body_defs(block, entry, table, Type::Combinator.nominal_of(refined), nil) if refined
|
|
10525
|
+
|
|
10526
|
+
# Issue #1717 — the body's `self` is unknown unless narrowed, as on the evaluator's entry; only the
|
|
10527
|
+
# `call.undefined-method` mark is set here, never #316's opaque mark (its bind gate reads the evaluator's).
|
|
10528
|
+
entry = entry.with_block_self_unknown(true) unless narrowing&.self_type
|
|
10529
|
+
return entry unless narrowing
|
|
10530
|
+
|
|
10531
|
+
MacroBlockSelfType.apply(entry, narrowing, keeps_unknown: current_scope.block_self_narrowing_unknown?(node))
|
|
10532
|
+
end
|
|
10533
|
+
|
|
10534
|
+
# Issue #1120 — the class a `refine X do … end` call's block refines, or nil when the call is not that shape
|
|
10535
|
+
# or is a `refine` where `self` is a class (issue #1689: the class's own method, its block an ordinary one).
|
|
10536
|
+
# The evaluator asks the same two questions ({StatementEvaluator#refined_class_context}).
|
|
10537
|
+
def refined_body_name(node, scope)
|
|
10538
|
+
target = refine_target(node)
|
|
10539
|
+
return nil if target.nil? || InEffectRefinements.class_body_refine?(scope, node)
|
|
10540
|
+
|
|
10541
|
+
refined_class_name(target, scope)
|
|
10542
|
+
end
|
|
10543
|
+
|
|
10544
|
+
# Issue #1120 — the name of the class `refine target do … end` refines: the class `target` types as, or the
|
|
10545
|
+
# constant as written when it does not type as a class object (a gem class with no RBS). Shared by
|
|
10546
|
+
# {StatementEvaluator#refined_class_context} and {#refined_body_name}.
|
|
10547
|
+
def refined_class_name(target, scope)
|
|
10548
|
+
refined = scope.type_of(target)
|
|
10549
|
+
name = refined.is_a?(Type::Singleton) ? refined.class_name : Source::ConstantPath.qualified_name(target)
|
|
10550
|
+
name&.delete_prefix("::")
|
|
10551
|
+
end
|
|
10552
|
+
|
|
10553
|
+
# Issue #1696 — the entry of a `Class.new` / `Module.new` / `Struct.new` / `Data.define` block the evaluator
|
|
10554
|
+
# never entered because it sits in a value position: `Object.const_set(:K, Class.new { attr_reader :x })`,
|
|
10555
|
+
# `[Struct.new(:a) { def b = a }]`. Its `self` is the class `name` ({AnonymousMetaClass.name_for}) names, as
|
|
10556
|
+
# {StatementEvaluator#enter_meta_class_body} gives the statement form; left on the enclosing `self`, a
|
|
10557
|
+
# top-level one read every macro in it as `call.unresolved-toplevel`. Its `def`s are recorded as
|
|
10558
|
+
# {#record_class_body_defs} says.
|
|
10559
|
+
def unentered_meta_class_body_entry(block, entry, name, table)
|
|
10560
|
+
entry = entry.with_block_self_type(Type::Combinator.singleton_of(name)).with_singleton_class_body(false)
|
|
10561
|
+
record_class_body_defs(block, entry, table, Type::Combinator.nominal_of(name), entry.self_type)
|
|
10562
|
+
entry
|
|
10563
|
+
end
|
|
10564
|
+
|
|
10565
|
+
# Issue #1696 — records each `def` a value-position class-body block defines ({#each_class_body_def}) with the
|
|
10566
|
+
# `self` its body runs on, since no evaluator pass enters it: `instance_self` for a plain `def`, `class_self`
|
|
10567
|
+
# for a `def self.m` (skipped when nil). A `refine X` block passes only `Nominal[X]`: Ruby runs its defs on an
|
|
10568
|
+
# instance of X, but the block itself on the refinement module, which Rigor does not model, so the block keeps
|
|
10569
|
+
# the unknown `self` an unnarrowed block has. A `def obj.m` is left to the walk.
|
|
10570
|
+
def record_class_body_defs(block, entry, table, instance_self, class_self)
|
|
10571
|
+
each_class_body_def(block.body) do |def_node|
|
|
10572
|
+
next if table.key?(def_node)
|
|
10573
|
+
|
|
10574
|
+
receiver = def_node.receiver
|
|
10575
|
+
def_self =
|
|
10576
|
+
if receiver.nil? then instance_self
|
|
10577
|
+
elsif receiver.is_a?(Prism::SelfNode) then class_self
|
|
10578
|
+
end
|
|
10579
|
+
table[def_node] = entry.with_block_self_type(def_self) if def_self
|
|
10580
|
+
end
|
|
10581
|
+
end
|
|
10582
|
+
|
|
10583
|
+
# The `def`s a class-body block defines on its class: every one in the body, `private def m` included, short
|
|
10584
|
+
# of a nested block, lambda or `class` / `module` / `class << …` body, each of which opens its own.
|
|
10585
|
+
def each_class_body_def(node, &)
|
|
10586
|
+
return unless node.is_a?(Prism::Node)
|
|
10587
|
+
return yield(node) if node.is_a?(Prism::DefNode)
|
|
10588
|
+
return if CLASS_BODY_DEF_BARRIERS.include?(node.class)
|
|
8844
10589
|
|
|
8845
|
-
|
|
10590
|
+
node.rigor_each_child { |child| each_class_body_def(child, &) }
|
|
10591
|
+
end
|
|
10592
|
+
|
|
10593
|
+
CLASS_BODY_DEF_BARRIERS = [
|
|
10594
|
+
Prism::BlockNode, Prism::LambdaNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
10595
|
+
].freeze
|
|
10596
|
+
private_constant :CLASS_BODY_DEF_BARRIERS
|
|
10597
|
+
|
|
10598
|
+
# The narrowing ({MacroBlockSelfType::Match}) an unentered block's body runs with — its `self` and, for a
|
|
10599
|
+
# `block_as_methods:` entry, the declared refinements (issue #1667) — when the call is one whose block the
|
|
10600
|
+
# engine narrows: an ADR-16 Tier A `block_as_methods:` match ({MacroBlockSelfType}) or `define_method` on the
|
|
10601
|
+
# lexical `self` ({DefineMethodBlockSelf}, issue #963). The evaluator applies the same two narrowings to
|
|
10602
|
+
# the blocks it enters ({StatementEvaluator#build_block_entry_scope}, {StatementEvaluator#enter_call_block})
|
|
10603
|
+
# and the value pass to every block it types ({ExpressionTyper#block_body_self_narrowing}); without this a
|
|
10604
|
+
# block in a value position — `puts(MyApp.get("/x") { redirect "/y" })`, `render json: Alba.serialize(x) {
|
|
10605
|
+
# attributes :id }`, `private define_method(:m) { helper }` — kept the enclosing `self`, and every DSL call
|
|
10606
|
+
# in it read as a call on that. The receiver is typed only when some plugin declares an entry for the
|
|
10607
|
+
# call's name, so a call no entry names costs one table lookup. A miss, or a raise, keeps the entry as
|
|
10608
|
+
# built — the false-positive-safe direction.
|
|
10609
|
+
def unentered_block_narrowing(node, scope)
|
|
10610
|
+
registry = scope.environment&.plugin_registry
|
|
10611
|
+
if registry && !registry.empty? && !registry.contribution_index.block_entries_for(node.name).empty?
|
|
10612
|
+
receiver_type =
|
|
10613
|
+
node.receiver ? scope.type_of(node.receiver) : MacroBlockSelfType.implicit_receiver_type(scope)
|
|
10614
|
+
match = MacroBlockSelfType.match_for(scope: scope, call_node: node, receiver_type: receiver_type)
|
|
10615
|
+
return match if match
|
|
10616
|
+
end
|
|
10617
|
+
defined = DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: node)
|
|
10618
|
+
defined && MacroBlockSelfType::Match.new(self_type: defined, refinements: InEffectRefinements::EMPTY)
|
|
10619
|
+
rescue StandardError
|
|
10620
|
+
nil
|
|
8846
10621
|
end
|
|
8847
10622
|
|
|
8848
10623
|
# The scope the children of an unentered block or lambda inherit. The evaluator enters a statement-level
|