rigortype 0.4.1 → 0.4.3
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 +1 -1
- 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/11-sig-gen.md +18 -5
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +7 -5
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +41 -4
- 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 +11 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +292 -59
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- 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/effects_cache_probe.rb +1 -1
- 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 +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
- data/lib/rigor/analysis/incremental_session.rb +103 -23
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +241 -37
- data/lib/rigor/analysis/reachability/scan.rb +490 -77
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +62 -13
- data/lib/rigor/analysis/worker_session.rb +8 -5
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +55 -5
- 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 +140 -40
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +6 -3
- data/lib/rigor/cli/unused_command.rb +166 -27
- data/lib/rigor/cli.rb +39 -5
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +61 -2
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +316 -14
- data/lib/rigor/environment/required_features.rb +208 -0
- data/lib/rigor/environment.rb +33 -8
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +261 -29
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- 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 +286 -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 +59 -12
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -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/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +96 -14
- data/lib/rigor/inference/module_function_state.rb +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +12 -7
- data/lib/rigor/inference/project_patched_scanner.rb +108 -26
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
- data/lib/rigor/inference/refined_dispatch.rb +320 -0
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +827 -65
- data/lib/rigor/inference/statement_evaluator.rb +294 -51
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +87 -5
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +48 -6
- data/lib/rigor/protection/mutation_cache.rb +3 -2
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +211 -21
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +100 -10
- 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 +34 -2
- 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-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -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 +7 -6
- 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/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +30 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +9 -4
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +83 -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,6 +22,9 @@ 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"
|
|
23
29
|
require_relative "module_function_state"
|
|
24
30
|
require_relative "multi_target_binder"
|
|
@@ -95,13 +101,17 @@ module Rigor
|
|
|
95
101
|
# bodies from their CONVERGED bindings so per-line probes
|
|
96
102
|
# (`rigor annotate`) reflect the post-writeback state, not the
|
|
97
103
|
# cap-N intermediate constants. Off for the check path.
|
|
104
|
+
# @param widen_clauses — check-path flag (issue #1231): a rescue or
|
|
105
|
+
# `ensure` clause records the variables the `begin` body writes as
|
|
106
|
+
# `Dynamic[top]`. Off for every other reader, so parameter inference
|
|
107
|
+
# and sig-gen observe the clause's plain entry.
|
|
98
108
|
# @return identity-comparing
|
|
99
109
|
# table whose default value is `default_scope`.
|
|
100
110
|
# The no-op re-anchoring table (#722 residue 2): shared so the overwhelmingly common
|
|
101
111
|
# nothing-to-re-anchor walk allocates none.
|
|
102
112
|
EMPTY_RENAMES = {}.freeze
|
|
103
113
|
|
|
104
|
-
def index(root, default_scope:, converged_loop_recording: false) # rubocop:disable Metrics/AbcSize
|
|
114
|
+
def index(root, default_scope:, converged_loop_recording: false, widen_clauses: false) # rubocop:disable Metrics/AbcSize
|
|
105
115
|
# Slice A-declarations. Build the declaration overrides first so every scope handed to the StatementEvaluator
|
|
106
116
|
# already carries the table; structural sharing through `Scope#with_local` / `#with_fact` / `#with_self_type`
|
|
107
117
|
# propagates it across every derived scope.
|
|
@@ -188,7 +198,8 @@ module Rigor
|
|
|
188
198
|
|
|
189
199
|
on_enter = ->(node, scope) { table[node] = scope }
|
|
190
200
|
StatementEvaluator.new(scope: seeded_scope, on_enter: on_enter,
|
|
191
|
-
converged_loop_recording: converged_loop_recording
|
|
201
|
+
converged_loop_recording: converged_loop_recording,
|
|
202
|
+
widen_clauses: widen_clauses).evaluate(root)
|
|
192
203
|
|
|
193
204
|
propagate(root, table, seeded_scope)
|
|
194
205
|
table
|
|
@@ -2249,11 +2260,13 @@ module Rigor
|
|
|
2249
2260
|
# @return the `program_globals` table and the census, keyed by the discovery index members it fills
|
|
2250
2261
|
def build_program_global_index(root, default_scope)
|
|
2251
2262
|
accumulator = {}
|
|
2263
|
+
in_effect = InEffectRefinements.new(root)
|
|
2252
2264
|
census = { patched_line_readers: Set.new, clears_last_status: false, defines_case_equality: false,
|
|
2253
|
-
write_census: GlobalWriteCensus::Collector.new }
|
|
2265
|
+
write_census: GlobalWriteCensus::Collector.new(in_effect) }
|
|
2254
2266
|
gather_global_writes(root, default_scope, accumulator, census)
|
|
2255
2267
|
census[:patched_line_readers] = census[:patched_line_readers].freeze
|
|
2256
2268
|
census[:implicit_self_evidence] = LastLine::SelfEvidence.new(root)
|
|
2269
|
+
census[:in_effect_refinements] = in_effect
|
|
2257
2270
|
[accumulator.freeze, census]
|
|
2258
2271
|
end
|
|
2259
2272
|
|
|
@@ -3245,8 +3258,10 @@ module Rigor
|
|
|
3245
3258
|
# `possible_discovered_methods`, `contested_discovered_def_nodes` and
|
|
3246
3259
|
# `contested_discovered_parameter_envelopes`, from the {Certainty} classification `certainty` (computed here
|
|
3247
3260
|
# when the caller has none).
|
|
3248
|
-
def build_methods_and_def_nodes(root, source_path = nil, certainty: Certainty.possible_nodes(root))
|
|
3261
|
+
def build_methods_and_def_nodes(root, source_path = nil, certainty: Certainty.possible_nodes(root), census: nil)
|
|
3249
3262
|
tables = MethodTables.new({}, {}, nil, certainty, true)
|
|
3263
|
+
tables.root = root
|
|
3264
|
+
tables.census = census
|
|
3250
3265
|
def_nodes = {}
|
|
3251
3266
|
walk_methods_and_def_nodes(root, [], false, tables, def_nodes, source_path)
|
|
3252
3267
|
apply_alias_def_nodes(root, def_nodes, tables)
|
|
@@ -3263,8 +3278,79 @@ module Rigor
|
|
|
3263
3278
|
# ADR-119 WD3 — `certainty` is the file's {Certainty} answer and `certain` whether the contribution being
|
|
3264
3279
|
# recorded is certain ({#at} sets it before each recorder). `possible`, `contested_envelopes` and
|
|
3265
3280
|
# `contested_def_nodes` are the siblings the recorders fill, each nil until a possible contribution writes it.
|
|
3281
|
+
#
|
|
3282
|
+
# Issue #1689 — `root` is the walked tree, and `in_effect` its {InEffectRefinements}, built the first time a
|
|
3283
|
+
# `refine`-shaped call asks {#refine_target_of}, so a file without one never builds it.
|
|
3284
|
+
#
|
|
3285
|
+
# ADR-121 WD7 — `census`, when an Array, collects `[start offset, outcome]` for every `refine`-shaped node the
|
|
3286
|
+
# walk accounts for ({ScopeIndexer.refine_census}); `refine_text` memoises the byte offsets of the `refine`
|
|
3287
|
+
# words in the file's text, so a file with none skips every census scan and a `def` with none is not scanned;
|
|
3288
|
+
# `literal_seen` holds the offsets of the refine literals a call's arguments already accounted for;
|
|
3289
|
+
# `source_bytes` memoises the byte offsets of the file's heredoc openers; `loose_constants` memoises
|
|
3290
|
+
# {ScopeIndexer.loose_constant_segments}. The module a `refine`-shaped node is charged to is what the in-effect
|
|
3291
|
+
# walk worked out for it ({#refine_context}).
|
|
3266
3292
|
MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
|
|
3267
|
-
:contested_envelopes, :contested_def_nodes
|
|
3293
|
+
:contested_envelopes, :contested_def_nodes, :root, :in_effect, :census,
|
|
3294
|
+
:refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans) do
|
|
3295
|
+
# {ScopeIndexer.module_refine_target} against this tree's {InEffectRefinements}.
|
|
3296
|
+
def refine_target_of(node)
|
|
3297
|
+
return nil if ScopeIndexer.refine_target(node).nil?
|
|
3298
|
+
|
|
3299
|
+
ScopeIndexer.module_refine_target(node, in_effect_query)
|
|
3300
|
+
end
|
|
3301
|
+
|
|
3302
|
+
# {InEffectRefinements#class_body_refine?} for a `refine` call of this tree.
|
|
3303
|
+
def class_body_refine?(node) = ScopeIndexer.refine_call?(node) && in_effect_query.class_body_refine?(node)
|
|
3304
|
+
|
|
3305
|
+
def in_effect_query = (self.in_effect ||= InEffectRefinements.new(root))
|
|
3306
|
+
|
|
3307
|
+
# Does the walked file's text name `refine` as a word? Every census scan asks first, so a file that only says
|
|
3308
|
+
# `refined` or `refinement` pays nothing for it.
|
|
3309
|
+
def mentions_refine? = !refine_offsets.empty?
|
|
3310
|
+
|
|
3311
|
+
# Does the text in `[start, stop)` name `refine` as a word, or open a heredoc whose body (which follows the
|
|
3312
|
+
# line, and so may end after `stop`) could? A `def` that does neither is not scanned.
|
|
3313
|
+
def refine_in?(start, stop)
|
|
3314
|
+
ScopeIndexer.offset_in?(refine_offsets, start, stop) ||
|
|
3315
|
+
ScopeIndexer.offset_in?(self.source_bytes ||= ScopeIndexer.heredoc_offsets(root), start, stop)
|
|
3316
|
+
end
|
|
3317
|
+
|
|
3318
|
+
# ADR-121 WD7 — the `self` the `refine`-shaped `node` runs on, as the in-effect walk worked it out
|
|
3319
|
+
# ({InEffectRefinements#refine_context}); a module the walk cannot name for a node it did not record.
|
|
3320
|
+
def refine_context(node) = in_effect_query.refine_context(node) || RefineSelf.unknown
|
|
3321
|
+
|
|
3322
|
+
# The byte offsets of each `refine` word in the file's text, ascending.
|
|
3323
|
+
def refine_offsets
|
|
3324
|
+
self.refine_text ||= ScopeIndexer.refine_word_offsets(root)
|
|
3325
|
+
end
|
|
3326
|
+
|
|
3327
|
+
# Records that the walk accounted for the `refine`-shaped `node` with `outcome` (and the module it charged),
|
|
3328
|
+
# when a census is taken.
|
|
3329
|
+
def account(node, outcome, charged = nil)
|
|
3330
|
+
census&.push([node.location.start_offset, outcome, charged])
|
|
3331
|
+
end
|
|
3332
|
+
|
|
3333
|
+
# Marks a refine literal accounted for by its call, so the walk's descent does not account for it again.
|
|
3334
|
+
def see_literal(node) = (self.literal_seen ||= Set.new) << node.location.start_offset
|
|
3335
|
+
|
|
3336
|
+
def literal_seen?(node) = !literal_seen.nil? && literal_seen.include?(node.location.start_offset)
|
|
3337
|
+
|
|
3338
|
+
# A6 — the last segments of the constants this file binds by `||=` or by a literal `const_set`, which the
|
|
3339
|
+
# project-wide `constant_writers` a reader consults leaves out.
|
|
3340
|
+
def loose_constant?(segment)
|
|
3341
|
+
self.loose_constants ||= ScopeIndexer.loose_constant_segments(root)
|
|
3342
|
+
loose_constants.include?(segment)
|
|
3343
|
+
end
|
|
3344
|
+
|
|
3345
|
+
# Issue #1820 — does `node` sit inside a block or lambda of this tree? A block's `self` may be an object even in
|
|
3346
|
+
# a class body or at the top level (`s.instance_eval { … }`, a `define_method` block). The spans are found once
|
|
3347
|
+
# per file, on the first ask.
|
|
3348
|
+
def in_block?(node)
|
|
3349
|
+
self.block_spans ||= ScopeIndexer.block_spans(root)
|
|
3350
|
+
offset = node.location.start_offset
|
|
3351
|
+
block_spans.any? { |start, stop| offset >= start && offset < stop }
|
|
3352
|
+
end
|
|
3353
|
+
|
|
3268
3354
|
# Sets {#certain} for the contribution `node` and returns the tables.
|
|
3269
3355
|
def at(node)
|
|
3270
3356
|
self.certain = !Certainty.possible?(certainty, node)
|
|
@@ -3295,7 +3381,121 @@ module Rigor
|
|
|
3295
3381
|
end
|
|
3296
3382
|
|
|
3297
3383
|
EMPTY_REFINEMENTS = {}.freeze
|
|
3298
|
-
|
|
3384
|
+
REFINEMENT_WILDCARD = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
3385
|
+
REFINE_WORD = /\brefine\b/
|
|
3386
|
+
HEREDOC_OPENER = /<<[~-]?["'`A-Za-z_]/
|
|
3387
|
+
private_constant :EMPTY_REFINEMENTS, :REFINEMENT_WILDCARD, :REFINE_WORD, :HEREDOC_OPENER
|
|
3388
|
+
|
|
3389
|
+
NO_OFFSETS = [].freeze
|
|
3390
|
+
private_constant :NO_OFFSETS
|
|
3391
|
+
|
|
3392
|
+
# The byte offsets of the `refine` words in `root`'s text (none for no tree), which `MethodTables#refine_in?`
|
|
3393
|
+
# matches node offsets against.
|
|
3394
|
+
def refine_word_offsets(root) = pattern_offsets(root, REFINE_WORD)
|
|
3395
|
+
|
|
3396
|
+
# The byte offsets of the heredoc openers (`<<~ID`, `<<-ID`, `<<ID`, `<<"ID"`) in `root`'s text: a heredoc's body
|
|
3397
|
+
# follows its line, so it may end after the `def` that opens it.
|
|
3398
|
+
def heredoc_offsets(root) = pattern_offsets(root, HEREDOC_OPENER)
|
|
3399
|
+
|
|
3400
|
+
def pattern_offsets(root, pattern)
|
|
3401
|
+
return NO_OFFSETS if root.nil?
|
|
3402
|
+
|
|
3403
|
+
# Byte-wise: a file may hold bytes its encoding does not (binary `__END__` data, a stray comment byte), and a
|
|
3404
|
+
# regexp over such a String raises.
|
|
3405
|
+
binary = root.send(:source).source.b
|
|
3406
|
+
return NO_OFFSETS unless pattern.match?(binary)
|
|
3407
|
+
|
|
3408
|
+
offsets = []
|
|
3409
|
+
position = 0
|
|
3410
|
+
while (match = pattern.match(binary, position))
|
|
3411
|
+
offsets << match.begin(0)
|
|
3412
|
+
position = match.end(0)
|
|
3413
|
+
end
|
|
3414
|
+
offsets.freeze
|
|
3415
|
+
end
|
|
3416
|
+
|
|
3417
|
+
# Is one of the ascending `offsets` in `[start, stop)`?
|
|
3418
|
+
# The `[start, end)` byte span of every block and lambda in `root`.
|
|
3419
|
+
def block_spans(root)
|
|
3420
|
+
spans = []
|
|
3421
|
+
return spans if root.nil?
|
|
3422
|
+
|
|
3423
|
+
Source::NodeWalker.each(root) do |node|
|
|
3424
|
+
next unless node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode)
|
|
3425
|
+
|
|
3426
|
+
location = node.location
|
|
3427
|
+
spans << [location.start_offset, location.end_offset]
|
|
3428
|
+
end
|
|
3429
|
+
spans
|
|
3430
|
+
end
|
|
3431
|
+
|
|
3432
|
+
def offset_in?(offsets, start, stop)
|
|
3433
|
+
index = offsets.bsearch_index { |offset| offset >= start }
|
|
3434
|
+
!index.nil? && offsets[index] < stop
|
|
3435
|
+
end
|
|
3436
|
+
|
|
3437
|
+
# A6 — the last segments of the constants `root` binds by `||=` (`K ||= String`, `A::K ||= …`) or by a
|
|
3438
|
+
# `const_set` whose name is a literal (`Object.const_set(:K, String)`), and (issue #1749) by any write in a
|
|
3439
|
+
# `class << …` body: the project-wide constant census files a write there under no name a `refine` argument
|
|
3440
|
+
# spells, but a `def` in that body reads it lexically (`class << self; K = String; def setup = refine(K) {…}`).
|
|
3441
|
+
# Scanned only for a file with a literal-target `refine`.
|
|
3442
|
+
def loose_constant_segments(root)
|
|
3443
|
+
segments = Set.new
|
|
3444
|
+
Source::NodeWalker.each(root) do |node|
|
|
3445
|
+
case node
|
|
3446
|
+
when Prism::ConstantOrWriteNode then segments << node.name.to_s
|
|
3447
|
+
when Prism::ConstantPathOrWriteNode then segments << node.target.name.to_s
|
|
3448
|
+
when Prism::SingletonClassNode then singleton_constant_segments(node.body, segments)
|
|
3449
|
+
when Prism::CallNode
|
|
3450
|
+
name = node.name == :const_set && node.arguments&.arguments&.first
|
|
3451
|
+
segments << name.unescaped if name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)
|
|
3452
|
+
end
|
|
3453
|
+
end
|
|
3454
|
+
segments
|
|
3455
|
+
end
|
|
3456
|
+
|
|
3457
|
+
# The last segment of every constant a write, an operator write or a multiple assignment target in `body` binds.
|
|
3458
|
+
def singleton_constant_segments(body, segments)
|
|
3459
|
+
return if body.nil?
|
|
3460
|
+
|
|
3461
|
+
Source::NodeWalker.each(body) do |node|
|
|
3462
|
+
case node
|
|
3463
|
+
when Prism::ConstantWriteNode, Prism::ConstantTargetNode, Prism::ConstantOperatorWriteNode,
|
|
3464
|
+
Prism::ConstantAndWriteNode, Prism::ConstantPathTargetNode
|
|
3465
|
+
segments << node.name.to_s
|
|
3466
|
+
when Prism::ConstantPathWriteNode, Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathAndWriteNode
|
|
3467
|
+
segments << node.target.name.to_s
|
|
3468
|
+
end
|
|
3469
|
+
end
|
|
3470
|
+
end
|
|
3471
|
+
|
|
3472
|
+
# ADR-121 WD7 — the census gate's view of one parsed file (`root`): `[refinement table, [[start offset,
|
|
3473
|
+
# outcome], …]]`, one pair for every `refine`-shaped node the method walk accounted for. The outcomes are
|
|
3474
|
+
# `:recorded` (a literal target and block, its names recorded; also one in a module's own singleton method,
|
|
3475
|
+
# #1749), `:class_unknown` (a block whose target the walk cannot name), `:targets_wildcard` (a `refine` that
|
|
3476
|
+
# runs once a method is called, other than a module's own singleton method's, or in a block that may rebind
|
|
3477
|
+
# `self`, charged to its module or the wildcard), `:literal` (A3's `:refine` / eval-string literal), `:nested` (a
|
|
3478
|
+
# `refine` in a refine body, or in a singleton method of one), `:dsl` (where `self` is a class, #1689) and
|
|
3479
|
+
# `:raises` (no block, a Proc as the block, another receiver, or a computed target where `self` is `main`), and
|
|
3480
|
+
# `:data` (a `:refine` literal where `self` is a plain class or one of its instances).
|
|
3481
|
+
def refine_census(root)
|
|
3482
|
+
census = []
|
|
3483
|
+
refinements = build_methods_and_def_nodes(root, census: census)[3]
|
|
3484
|
+
[refinements, census]
|
|
3485
|
+
end
|
|
3486
|
+
|
|
3487
|
+
# ADR-121 WD7 (issue #1814) — the refinement table one parsed file contributes: the census of
|
|
3488
|
+
# {#build_methods_and_def_nodes}, whose fourth table is the project walk's own (`seed_discovered_methods` and
|
|
3489
|
+
# `walk_def_contributions` take it from the same call), run for a caller that wants nothing else. The gem source
|
|
3490
|
+
# walker reads a gem file this way, so a gem module's rows carry the wildcards a project module's would
|
|
3491
|
+
# (`walker_walk_census_parity_spec.rb`, beside the walker's specs, holds the two to one table). A file whose text
|
|
3492
|
+
# never names `refine` as a word records no row, so it is not walked. `source_path` names an anonymous module's
|
|
3493
|
+
# row (`#<Module:<path>:L:C>`) as the project walk does, so two files' bare `Module.new` blocks never share one.
|
|
3494
|
+
def refinement_table(root, source_path = nil)
|
|
3495
|
+
return EMPTY_REFINEMENTS if refine_word_offsets(root).empty?
|
|
3496
|
+
|
|
3497
|
+
build_methods_and_def_nodes(root, source_path)[3]
|
|
3498
|
+
end
|
|
3299
3499
|
|
|
3300
3500
|
# Issue #1120 — a refinement table, deep-frozen; the shared empty table when nothing was recorded.
|
|
3301
3501
|
def freeze_refinements(table)
|
|
@@ -3319,8 +3519,7 @@ module Rigor
|
|
|
3319
3519
|
# implicit- or `self`-receiver `refine` with one constant argument and a literal block. A computed target
|
|
3320
3520
|
# (`refine(klass) { … }`) names no class this walk can key, so its body walks as it did before.
|
|
3321
3521
|
def refine_target(node)
|
|
3322
|
-
return nil unless
|
|
3323
|
-
return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
3522
|
+
return nil unless refine_call?(node)
|
|
3324
3523
|
|
|
3325
3524
|
arguments = node.arguments&.arguments
|
|
3326
3525
|
return nil unless arguments&.size == 1
|
|
@@ -3329,27 +3528,239 @@ module Rigor
|
|
|
3329
3528
|
target if target.is_a?(Prism::ConstantReadNode) || target.is_a?(Prism::ConstantPathNode)
|
|
3330
3529
|
end
|
|
3331
3530
|
|
|
3531
|
+
# An implicit- or `self`-receiver `refine` call with a literal block, whatever its argument.
|
|
3532
|
+
def refine_call?(node)
|
|
3533
|
+
node.name == :refine && node.block.is_a?(Prism::BlockNode) &&
|
|
3534
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
3535
|
+
end
|
|
3536
|
+
|
|
3537
|
+
# Issue #1689 — {#refine_target}, for a call that `in_effect` (its file's {InEffectRefinements}) does not place
|
|
3538
|
+
# where `self` is a class. `Class` undefines `refine`, so `refine X do … end` directly in a `class` body is the
|
|
3539
|
+
# class's own method (a DSL), and its block walks as any other block. Every walk that treats a refine body
|
|
3540
|
+
# specially asks this, so the method tables, the in-effect refinements and the typer agree on which calls are
|
|
3541
|
+
# refine bodies.
|
|
3542
|
+
def module_refine_target(node, in_effect)
|
|
3543
|
+
target = refine_target(node)
|
|
3544
|
+
target unless target.nil? || in_effect.class_body_refine?(node)
|
|
3545
|
+
end
|
|
3546
|
+
|
|
3332
3547
|
# Issue #1120 — records the instance `def`s of a `refine X do … end` body as refinement methods of X, keyed by
|
|
3333
|
-
# the refining module (`
|
|
3548
|
+
# the refining module (`refining`, the `self` the `refine` call runs on). X is resolved LEXICALLY
|
|
3334
3549
|
# (`qualified_prefix`), which is where Ruby resolves the argument even inside a `Module.new { … }` block
|
|
3335
3550
|
# whose `self` is anonymous; every name it can denote is recorded, as {#constant_receiver_candidates}
|
|
3336
3551
|
# explains. Nothing in the body reaches the class tables: `refine String do def shout` does not give
|
|
3337
3552
|
# `String` or the refining module a `shout` outside a `using`.
|
|
3338
|
-
|
|
3553
|
+
#
|
|
3554
|
+
# ADR-121 WD7 (issue #1799) — the names are what {RefineCensus.read_body} reads (`def`, `alias`,
|
|
3555
|
+
# `alias_method`, `define_method`, `attr_*`, `undef`), and a body that may define a name it cannot spell adds a
|
|
3556
|
+
# names-wildcard row per target. `refining` is `""` at the top level (where Ruby raises). The body is not
|
|
3557
|
+
# walked any further, so the census scans it here.
|
|
3558
|
+
#
|
|
3559
|
+
# A6 — a target this file binds by `||=` or a literal `const_set` (`K ||= String; refine(K)`) names whatever value
|
|
3560
|
+
# the write holds, so its names go under the wildcard class, as a computed target's do.
|
|
3561
|
+
def record_refinement_defs(node, target, qualified_prefix, refining, tables)
|
|
3562
|
+
loose = tables.loose_constant?(target.name.to_s)
|
|
3563
|
+
tables.account(node, loose ? :class_unknown : :recorded, census_charge(refining))
|
|
3339
3564
|
body = node.block.body
|
|
3340
3565
|
return if body.nil?
|
|
3341
3566
|
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3567
|
+
targets = loose ? [REFINEMENT_WILDCARD] : constant_receiver_candidates(target, qualified_prefix)
|
|
3568
|
+
reading = RefineCensus.read_body(body)
|
|
3569
|
+
refining.each do |owner|
|
|
3570
|
+
tables.refinements = RefineCensus.record_rows(tables.refinements, targets, reading, owner)
|
|
3571
|
+
end
|
|
3572
|
+
census_scan(body, qualified_prefix, tables, refine_body: true) if tables.mentions_refine?
|
|
3573
|
+
end
|
|
3574
|
+
|
|
3575
|
+
# What the census records as charged for `owners`: the one module, or every name.
|
|
3576
|
+
def census_charge(owners) = owners.size == 1 ? owners.first : owners
|
|
3577
|
+
|
|
3578
|
+
# ADR-121 WD7 — a literal-target `refine X do … end` the module-body walk reaches, charged where the in-effect
|
|
3579
|
+
# walk says it runs ({MethodTables#refine_context}): rows for the module it names (the walk's own name for an
|
|
3580
|
+
# anonymous `Module.new` block, `""` at the top level), or a targets-wildcard for that module (the wildcard
|
|
3581
|
+
# where it cannot be named) when it runs only once a method is called or on a module the walk cannot name.
|
|
3582
|
+
def record_literal_target_refine(node, target, qualified_prefix, owner_prefix, tables)
|
|
3583
|
+
context = tables.refine_context(node)
|
|
3584
|
+
here = context.here
|
|
3585
|
+
return record_refinement_defs(node, target, qualified_prefix, [""], tables) if here == RefineSelf::MAIN
|
|
3586
|
+
if context.deferred || here == REFINEMENT_WILDCARD
|
|
3587
|
+
return record_deferred_refine(node, RefineSelf.charged_modules(context), qualified_prefix, tables)
|
|
3588
|
+
end
|
|
3589
|
+
|
|
3590
|
+
record_refinement_defs(node, target, qualified_prefix, census_owners(context, owner_prefix), tables)
|
|
3591
|
+
end
|
|
3592
|
+
|
|
3593
|
+
# The modules a `refine` charged to `context` refines for; an anonymous `Module.new` block's is named by its
|
|
3594
|
+
# position (`owner_prefix`).
|
|
3595
|
+
def census_owners(context, owner_prefix)
|
|
3596
|
+
context.here == RefineSelf::ANONYMOUS ? [owner_prefix.join("::")] : RefineSelf.charged_modules(context)
|
|
3597
|
+
end
|
|
3598
|
+
|
|
3599
|
+
# ADR-121 WD7 — a targets-wildcard for each of `owners`, as a `refine` that runs once a method is called records
|
|
3600
|
+
# (an instance `def`, a `define_method` or `define_singleton_method` block; a module's own singleton method
|
|
3601
|
+
# records rows instead, #1749). The body is not walked any further, so the census scans it here.
|
|
3602
|
+
def record_deferred_refine(node, owners, qualified_prefix, tables)
|
|
3603
|
+
tables.account(node, :targets_wildcard, census_charge(owners))
|
|
3604
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3605
|
+
body = node.block.body
|
|
3606
|
+
census_scan(body, qualified_prefix, tables, refine_body: true) if body && tables.mentions_refine?
|
|
3607
|
+
end
|
|
3608
|
+
|
|
3609
|
+
# ADR-121 WD7 — a `refine`-shaped call the canonical arm ({#refine_target}) does not take: on another receiver
|
|
3610
|
+
# (`Module#refine` is private, so it raises), where `self` is a class (#1689's DSL), with no literal block (no
|
|
3611
|
+
# block raises, and so does a Proc passed as one), or with a target the walk cannot name (`refine(k)`,
|
|
3612
|
+
# `refine(self)`, `refine(K.dup)`). The last records the body's names under the wildcard class, so they decline
|
|
3613
|
+
# on every receiver, and a names-wildcard row when they are incomplete, which makes the module opaque; one that
|
|
3614
|
+
# runs once a method is called, or on a module the walk cannot name, records a targets-wildcard. At the top
|
|
3615
|
+
# level `self` is `main`, which has no `refine`.
|
|
3616
|
+
def account_other_refine(node, owner_prefix, tables)
|
|
3617
|
+
context = tables.refine_context(node)
|
|
3618
|
+
outcome, charged =
|
|
3619
|
+
if tables.class_body_refine?(node) || context.here == RefineSelf::CLASS then :dsl
|
|
3620
|
+
elsif !RefineCensus.self_call?(node) || !node.block.is_a?(Prism::BlockNode) ||
|
|
3621
|
+
context.here == RefineSelf::MAIN
|
|
3622
|
+
:raises
|
|
3623
|
+
elsif context.deferred || context.here == REFINEMENT_WILDCARD
|
|
3624
|
+
owners = RefineSelf.charged_modules(context)
|
|
3625
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3626
|
+
[:targets_wildcard, census_charge(owners)]
|
|
3627
|
+
else
|
|
3628
|
+
owners = census_owners(context, owner_prefix)
|
|
3629
|
+
owners.each { |owner| record_unreadable_target(node, owner, tables) }
|
|
3630
|
+
[:class_unknown, census_charge(owners)]
|
|
3631
|
+
end
|
|
3632
|
+
tables.account(node, outcome, charged)
|
|
3633
|
+
end
|
|
3634
|
+
|
|
3635
|
+
def record_unreadable_target(node, owner, tables)
|
|
3636
|
+
tables.refinements = RefineCensus.record_rows(tables.refinements, [REFINEMENT_WILDCARD],
|
|
3637
|
+
RefineCensus.read_body(node.block.body), owner)
|
|
3638
|
+
end
|
|
3639
|
+
|
|
3640
|
+
# `{"*" => {"*" => [owner]}}`: some `refine` of `owner` (every module, for the wildcard) is unreadable.
|
|
3641
|
+
def record_targets_wildcard(tables, owner)
|
|
3642
|
+
tables.refinements = RefineCensus.add_row(tables.refinements || {}, REFINEMENT_WILDCARD, REFINEMENT_WILDCARD,
|
|
3643
|
+
owner)
|
|
3644
|
+
end
|
|
3645
|
+
|
|
3646
|
+
# ADR-121 WD7 — the census over a subtree the method walk does not descend (a `def` body, a refine body), each
|
|
3647
|
+
# node charged where the in-effect walk says it runs. `refine_body` says `self` is the refinement itself, where a
|
|
3648
|
+
# nested `refine` refines for the refinement and only makes the body's names incomplete (§2.2).
|
|
3649
|
+
def census_scan(node, qualified_prefix, tables, refine_body: false)
|
|
3650
|
+
case node
|
|
3651
|
+
when Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
3652
|
+
refine_body = false
|
|
3653
|
+
when Prism::CallNode
|
|
3654
|
+
if (target = own_method_refine_target(node, tables))
|
|
3655
|
+
return record_own_method_refine(node, target, qualified_prefix, tables)
|
|
3656
|
+
end
|
|
3657
|
+
|
|
3658
|
+
census_call(node, qualified_prefix, tables, refine_body)
|
|
3659
|
+
when Prism::SymbolNode
|
|
3660
|
+
census_symbol(node, tables)
|
|
3661
|
+
end
|
|
3662
|
+
node.rigor_each_child { |child| census_scan(child, qualified_prefix, tables, refine_body: refine_body) }
|
|
3663
|
+
end
|
|
3664
|
+
|
|
3665
|
+
# ADR-121 WD7 (issue #1749) — the target of a canonical `refine X do … end` ({#refine_target}) in a module's own
|
|
3666
|
+
# singleton method (`def self.x`, a `def` in the module's `class << self`, or a block there that keeps `self`),
|
|
3667
|
+
# else nil. `self` is the module itself there, so the call refines X for it once the method is called. Anything
|
|
3668
|
+
# else in a method body (an instance `def`, a `define_method` or `define_singleton_method` block, a computed
|
|
3669
|
+
# target, a block that may rebind `self`) charges a targets-wildcard in {#census_call}, and a `refine` in a refine
|
|
3670
|
+
# body is nested there, whichever method it sits in: its context ({RefineSelf.refinement_body}) is not an
|
|
3671
|
+
# own method's.
|
|
3672
|
+
def own_method_refine_target(node, tables)
|
|
3673
|
+
return nil if node.name != :refine
|
|
3674
|
+
|
|
3675
|
+
target = tables.refine_target_of(node)
|
|
3676
|
+
target if target && tables.refine_context(node).own_method
|
|
3677
|
+
end
|
|
3678
|
+
|
|
3679
|
+
# Records `node` as the refine of its module, as a `refine` in the module body is recorded, with the refined
|
|
3680
|
+
# class marked as that one is ({#record_refinement}). The call's block is scanned here, so {#census_scan} does
|
|
3681
|
+
# not descend into it.
|
|
3682
|
+
def record_own_method_refine(node, target, qualified_prefix, tables)
|
|
3683
|
+
record_refinement(node, qualified_prefix, tables)
|
|
3684
|
+
owners = RefineSelf.charged_modules(tables.refine_context(node))
|
|
3685
|
+
record_refinement_defs(node, target, qualified_prefix, owners, tables)
|
|
3686
|
+
end
|
|
3687
|
+
|
|
3688
|
+
def census_call(node, qualified_prefix, tables, refine_body)
|
|
3689
|
+
census_literal_arguments(node, qualified_prefix, tables)
|
|
3690
|
+
return unless node.name == :refine
|
|
3691
|
+
|
|
3692
|
+
context = tables.refine_context(node)
|
|
3693
|
+
nested = refine_body || context.here == RefineSelf::REFINEMENT
|
|
3694
|
+
outcome, charged =
|
|
3695
|
+
if tables.class_body_refine?(node) || context.here == RefineSelf::CLASS then :dsl
|
|
3696
|
+
elsif nested && RefineCensus.self_call?(node) then :nested
|
|
3697
|
+
elsif !RefineCensus.self_call?(node) || node.block.nil? || context.here == RefineSelf::MAIN then :raises
|
|
3698
|
+
else
|
|
3699
|
+
owners = RefineSelf.charged_modules(context)
|
|
3700
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3701
|
+
[:targets_wildcard, census_charge(owners)]
|
|
3349
3702
|
end
|
|
3703
|
+
tables.account(node, outcome, charged)
|
|
3704
|
+
end
|
|
3705
|
+
|
|
3706
|
+
# A3 — a `:refine` Symbol argument of any call, or a String argument holding `refine` of an eval, `send` or
|
|
3707
|
+
# method-naming call, records a targets-wildcard for the module the call runs `refine` on: the receiver of a
|
|
3708
|
+
# `send` or `*_eval` (each name a constant one can denote), else the module the in-effect walk says `self` is
|
|
3709
|
+
# there. Where `self` is a plain class or one of its instances, a literal charged to `self` is data.
|
|
3710
|
+
def census_literal_arguments(node, qualified_prefix, tables)
|
|
3711
|
+
arguments = node.arguments&.arguments
|
|
3712
|
+
return if arguments.nil?
|
|
3713
|
+
|
|
3714
|
+
strings = RefineCensus.string_literal_call?(node)
|
|
3715
|
+
arguments.each do |argument|
|
|
3716
|
+
next unless RefineCensus.refine_symbol?(argument) || (strings && RefineCensus.refine_string?(argument))
|
|
3717
|
+
next if tables.literal_seen?(argument)
|
|
3718
|
+
|
|
3719
|
+
tables.see_literal(argument)
|
|
3720
|
+
census_literal_argument(node, argument, qualified_prefix, tables)
|
|
3721
|
+
end
|
|
3722
|
+
end
|
|
3723
|
+
|
|
3724
|
+
def census_literal_argument(call, literal, qualified_prefix, tables)
|
|
3725
|
+
target = RefineCensus.literal_target(call)
|
|
3726
|
+
context = tables.refine_context(literal)
|
|
3727
|
+
return tables.account(literal, :data) if target == :self && context.literal == RefineSelf::CLASS
|
|
3728
|
+
return tables.account(literal, :nested) if target == :self && context.literal == RefineSelf::REFINEMENT
|
|
3729
|
+
|
|
3730
|
+
owners = literal_owners(call, target, RefineSelf.charged_modules(context, literal: true), qualified_prefix)
|
|
3731
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3732
|
+
tables.account(literal, :literal, target == :self ? census_charge(owners) : [:receiver, owners])
|
|
3733
|
+
end
|
|
3734
|
+
|
|
3735
|
+
def literal_owners(call, target, self_modules, qualified_prefix)
|
|
3736
|
+
case target
|
|
3737
|
+
when :self then self_modules
|
|
3738
|
+
when :unknown then [REFINEMENT_WILDCARD]
|
|
3739
|
+
else
|
|
3740
|
+
receiver = call.receiver
|
|
3741
|
+
names = constant_shaped?(receiver) ? constant_receiver_candidates(receiver, qualified_prefix) : []
|
|
3742
|
+
names.empty? ? [REFINEMENT_WILDCARD] : names
|
|
3350
3743
|
end
|
|
3351
3744
|
end
|
|
3352
3745
|
|
|
3746
|
+
def constant_shaped?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
3747
|
+
|
|
3748
|
+
# A3 — a `:refine` Symbol anywhere else (`alias my_refine refine`, `%i[refine]`, a hash) names `Module#refine`
|
|
3749
|
+
# for code the walk cannot follow, run on the module the in-effect walk says `self` is there; where `self` is a
|
|
3750
|
+
# plain class or one of its instances it is data.
|
|
3751
|
+
def census_symbol(node, tables)
|
|
3752
|
+
return unless RefineCensus.refine_symbol?(node) && !tables.literal_seen?(node)
|
|
3753
|
+
|
|
3754
|
+
tables.see_literal(node)
|
|
3755
|
+
context = tables.refine_context(node)
|
|
3756
|
+
return tables.account(node, :data) if context.literal == RefineSelf::CLASS
|
|
3757
|
+
return tables.account(node, :nested) if context.literal == RefineSelf::REFINEMENT
|
|
3758
|
+
|
|
3759
|
+
owners = RefineSelf.charged_modules(context, literal: true)
|
|
3760
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3761
|
+
tables.account(node, :literal, census_charge(owners))
|
|
3762
|
+
end
|
|
3763
|
+
|
|
3353
3764
|
# The instance `def`s a refine body defines on the refined class: nested declarations open their own
|
|
3354
3765
|
# scope, and a `def self.x` or a `def` inside another `def` defines nothing on it.
|
|
3355
3766
|
def each_refinement_def(node, &)
|
|
@@ -3503,6 +3914,9 @@ module Rigor
|
|
|
3503
3914
|
# `self::` header under a REBOUND self (eval/meta-new body) names
|
|
3504
3915
|
# `owner::Name` instead.
|
|
3505
3916
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
3917
|
+
if node.is_a?(Prism::ClassNode) && node.superclass && methods_acc.mentions_refine?
|
|
3918
|
+
census_scan(node.superclass, qualified_prefix, methods_acc)
|
|
3919
|
+
end
|
|
3506
3920
|
record_declaration_facts(node, child_prefix, methods_acc.at(node)) unless child_cref
|
|
3507
3921
|
body_prefix = child_cref ? [] : child_prefix
|
|
3508
3922
|
if node.body
|
|
@@ -3519,6 +3933,10 @@ module Rigor
|
|
|
3519
3933
|
# still decline. The lexical cref below is unnameable in every case.
|
|
3520
3934
|
singleton_prefix = singleton_body_prefix(node, in_singleton_class, owner_prefix,
|
|
3521
3935
|
qualified_prefix)
|
|
3936
|
+
tables = methods_acc.at(node)
|
|
3937
|
+
if !node.expression.is_a?(Prism::SelfNode) || tables.in_block?(node)
|
|
3938
|
+
record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
3939
|
+
end
|
|
3522
3940
|
walk_methods_and_def_nodes(node.expression, qualified_prefix, in_singleton_class,
|
|
3523
3941
|
methods_acc, def_nodes_acc, source_path, def_owner_prefix,
|
|
3524
3942
|
singleton_cref: singleton_cref,
|
|
@@ -3536,6 +3954,7 @@ module Rigor
|
|
|
3536
3954
|
return
|
|
3537
3955
|
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
|
|
3538
3956
|
Prism::ConstantPathOrWriteNode
|
|
3957
|
+
record_meta_superclass(node, qualified_prefix, def_owner_prefix, methods_acc.at(node))
|
|
3539
3958
|
if (split = meta_new_block_split(node, qualified_prefix, def_owner_prefix, singleton_cref))
|
|
3540
3959
|
enclosing, body, body_self = split
|
|
3541
3960
|
# A meta-new block rebinds only `self` — `Module.nesting` stays lexical — so
|
|
@@ -3572,6 +3991,11 @@ module Rigor
|
|
|
3572
3991
|
# receiver, a singleton nothing names) or any ownerless prefix under an unnameable
|
|
3573
3992
|
# cref — is NOT top level; `record_def_node` would file the def under `<toplevel>`
|
|
3574
3993
|
# where an implicit-self call could find a method Ruby never installed there.
|
|
3994
|
+
location = node.location
|
|
3995
|
+
if methods_acc.mentions_refine? && methods_acc.refine_in?(location.start_offset, location.end_offset)
|
|
3996
|
+
node.rigor_each_child { |child| census_scan(child, qualified_prefix, methods_acc) }
|
|
3997
|
+
end
|
|
3998
|
+
record_foreign_singleton_def(node, qualified_prefix, owner_prefix, methods_acc.at(node))
|
|
3575
3999
|
unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
|
|
3576
4000
|
(singleton_cref && owner_prefix.empty?)
|
|
3577
4001
|
singleton_def = in_singleton_class || defs_singleton
|
|
@@ -3581,13 +4005,17 @@ module Rigor
|
|
|
3581
4005
|
end
|
|
3582
4006
|
return
|
|
3583
4007
|
when Prism::AliasMethodNode, Prism::UndefNode
|
|
4008
|
+
census_scan(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
|
|
3584
4009
|
unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
|
|
3585
4010
|
(singleton_cref && owner_prefix.empty?)
|
|
3586
4011
|
record_alias_or_undef(node, owner_prefix, in_singleton_class || defs_singleton,
|
|
3587
4012
|
methods_acc.at(node))
|
|
3588
4013
|
end
|
|
3589
4014
|
return
|
|
4015
|
+
when Prism::SymbolNode
|
|
4016
|
+
census_symbol(node, methods_acc) if methods_acc.mentions_refine?
|
|
3590
4017
|
when Prism::CallNode
|
|
4018
|
+
census_literal_arguments(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
|
|
3591
4019
|
if receiver_eval_call?(node)
|
|
3592
4020
|
return walk_eval_methods_and_defs(node, qualified_prefix, in_singleton_class, methods_acc,
|
|
3593
4021
|
def_nodes_acc, source_path, def_owner_prefix,
|
|
@@ -3596,11 +4024,16 @@ module Rigor
|
|
|
3596
4024
|
end
|
|
3597
4025
|
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
|
|
3598
4026
|
source_path)
|
|
4027
|
+
tables = methods_acc.at(node)
|
|
4028
|
+
record_singleton_prepend(node, qualified_prefix, tables,
|
|
4029
|
+
self_ok: node.name == :prepend && tables.in_block?(node))
|
|
3599
4030
|
# Issue #1120 — a refine body's defs go to the refinement table and nowhere else.
|
|
3600
|
-
if (target =
|
|
3601
|
-
return
|
|
4031
|
+
if (target = methods_acc.refine_target_of(node))
|
|
4032
|
+
return record_literal_target_refine(node, target, qualified_prefix, owner_prefix, methods_acc)
|
|
3602
4033
|
end
|
|
3603
4034
|
|
|
4035
|
+
account_other_refine(node, owner_prefix, methods_acc) if node.name == :refine
|
|
4036
|
+
|
|
3604
4037
|
if anonymous
|
|
3605
4038
|
walk_anonymous_meta_block(node, anonymous, qualified_prefix, in_singleton_class, methods_acc,
|
|
3606
4039
|
def_nodes_acc, source_path, def_owner_prefix,
|
|
@@ -3666,8 +4099,24 @@ module Rigor
|
|
|
3666
4099
|
end
|
|
3667
4100
|
|
|
3668
4101
|
# The block form of a meta-new constant write: its members, and the module mark `Module.new` earns.
|
|
4102
|
+
# Issue #1820 — `K = Class.new(Parent)`, with or without a block, names a class whose superclass the superclass
|
|
4103
|
+
# table leaves out; it takes {Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK}.
|
|
4104
|
+
def record_meta_superclass(node, qualified_prefix, self_base, tables)
|
|
4105
|
+
rvalue = meta_new_rvalue(node)
|
|
4106
|
+
return unless class_new_call?(rvalue) && !(rvalue.arguments&.arguments || []).empty?
|
|
4107
|
+
|
|
4108
|
+
child_prefix = meta_new_child_prefix(node, qualified_prefix, self_base)
|
|
4109
|
+
return if child_prefix.nil? || child_prefix.empty?
|
|
4110
|
+
|
|
4111
|
+
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
|
|
4112
|
+
end
|
|
4113
|
+
|
|
3669
4114
|
def record_meta_new_facts(rvalue, child_prefix, tables)
|
|
3670
4115
|
record_meta_members(rvalue, child_prefix, tables)
|
|
4116
|
+
# `Module.new(&BODY)`: the body runs on the new module but cannot be read, so its surface is unknown (#1816).
|
|
4117
|
+
if rvalue.block.is_a?(Prism::BlockArgumentNode)
|
|
4118
|
+
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
4119
|
+
end
|
|
3671
4120
|
return unless module_new_call?(rvalue)
|
|
3672
4121
|
|
|
3673
4122
|
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK)
|
|
@@ -3700,7 +4149,7 @@ module Rigor
|
|
|
3700
4149
|
def record_alias_method_call(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
3701
4150
|
return if qualified_prefix.empty?
|
|
3702
4151
|
|
|
3703
|
-
names = alias_method_call_names(call_node)
|
|
4152
|
+
names = Source::AliasNames.alias_method_call_names(call_node)
|
|
3704
4153
|
return if names.nil?
|
|
3705
4154
|
|
|
3706
4155
|
kind = in_singleton_class ? :singleton : :instance
|
|
@@ -3807,7 +4256,9 @@ module Rigor
|
|
|
3807
4256
|
# other two. {#meta_new_block_call} owns both unwrappings; every walk asks it, so no spelling can be
|
|
3808
4257
|
# recognised by one pass and missed by another.
|
|
3809
4258
|
def meta_new_block_body(node)
|
|
3810
|
-
meta_new_block_call(node)&.block
|
|
4259
|
+
block = meta_new_block_call(node)&.block
|
|
4260
|
+
# A `&BODY` / `&:sym` block ARGUMENT is not a literal block: its body is unreadable here (#1816).
|
|
4261
|
+
block.body if block.is_a?(Prism::BlockNode)
|
|
3811
4262
|
end
|
|
3812
4263
|
|
|
3813
4264
|
# The class-creating call a constant write's rvalue ultimately is, or nil when the write is not the idiom.
|
|
@@ -4061,16 +4512,57 @@ module Rigor
|
|
|
4061
4512
|
return if def_node.body.nil?
|
|
4062
4513
|
|
|
4063
4514
|
Source::NodeWalker.each(def_node.body) do |node|
|
|
4515
|
+
if node.is_a?(Prism::SingletonClassNode)
|
|
4516
|
+
record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
4517
|
+
next
|
|
4518
|
+
end
|
|
4064
4519
|
next unless node.is_a?(Prism::CallNode)
|
|
4065
4520
|
|
|
4066
4521
|
if node.name == :extend
|
|
4067
4522
|
record_object_extension(node, qualified_prefix, tables)
|
|
4068
4523
|
elsif SURFACE_EVAL_CALLS.include?(node.name) || SURFACE_NAMING_CALLS.include?(node.name)
|
|
4069
4524
|
record_body_rewrite(node, qualified_prefix, tables)
|
|
4525
|
+
else
|
|
4526
|
+
record_singleton_prepend(node, qualified_prefix, tables, self_ok: true)
|
|
4070
4527
|
end
|
|
4071
4528
|
end
|
|
4072
4529
|
end
|
|
4073
4530
|
|
|
4531
|
+
# Issue #1820 — `obj.singleton_class.prepend(M)`, or `prepend M` in a `class << obj` body, puts `M` ahead of
|
|
4532
|
+
# `obj`'s class even when the class already includes it (`prepend` re-inserts it; `extend` would not), so
|
|
4533
|
+
# `M`'s refinement answers first for that object. `M` takes {Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK},
|
|
4534
|
+
# the mark an `extend` in a method body already leaves: an object of any class may carry its instance methods
|
|
4535
|
+
# ahead of its class's own. A `singleton_class` of `self` (and a `class << self` body, which the caller leaves
|
|
4536
|
+
# out) written directly in a class or module body or at the top level is the class object's or `main`'s, not an
|
|
4537
|
+
# instance's; in a method body or any block (`self_ok`), where `self` may be an object, it counts.
|
|
4538
|
+
# `singleton_body` says the call sits in a `class << obj` body.
|
|
4539
|
+
def record_singleton_prepend(node, qualified_prefix, tables, singleton_body: false, self_ok: false)
|
|
4540
|
+
return unless node.name == :prepend
|
|
4541
|
+
|
|
4542
|
+
receiver = node.receiver
|
|
4543
|
+
singleton = singleton_body && (receiver.nil? || receiver.is_a?(Prism::SelfNode))
|
|
4544
|
+
singleton ||= singleton_class_call?(receiver, self_ok)
|
|
4545
|
+
record_object_extension(node, qualified_prefix, tables) if singleton
|
|
4546
|
+
end
|
|
4547
|
+
|
|
4548
|
+
def singleton_class_call?(receiver, self_ok)
|
|
4549
|
+
return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class
|
|
4550
|
+
return false unless receiver.arguments.nil?
|
|
4551
|
+
|
|
4552
|
+
object = receiver.receiver
|
|
4553
|
+
self_ok || !(object.nil? || object.is_a?(Prism::SelfNode))
|
|
4554
|
+
end
|
|
4555
|
+
|
|
4556
|
+
def record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
4557
|
+
return if node.body.nil?
|
|
4558
|
+
|
|
4559
|
+
Source::NodeWalker.each(node.body) do |inner|
|
|
4560
|
+
next unless inner.is_a?(Prism::CallNode)
|
|
4561
|
+
|
|
4562
|
+
record_singleton_prepend(inner, qualified_prefix, tables, singleton_body: true, self_ok: true)
|
|
4563
|
+
end
|
|
4564
|
+
end
|
|
4565
|
+
|
|
4074
4566
|
def record_body_rewrite(node, qualified_prefix, tables)
|
|
4075
4567
|
receiver = node.receiver
|
|
4076
4568
|
if receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
@@ -4243,6 +4735,50 @@ module Rigor
|
|
|
4243
4735
|
acc[:deferred_ranges][path] = build_deferred_ranges(root)
|
|
4244
4736
|
record_class_sources(acc[:class_sources], path, root, superclasses, includes, file_def_nodes,
|
|
4245
4737
|
acc[:compact_headers])
|
|
4738
|
+
record_singleton_class_sources(acc[:singleton_class_sources], path, root)
|
|
4739
|
+
end
|
|
4740
|
+
|
|
4741
|
+
# `{name => Set[path]}` for a bundle's name list (a bundle written before the list existed has none).
|
|
4742
|
+
def bundle_path_sets(names, path) = (names || []).to_h { |name| [name, Set[path]] }
|
|
4743
|
+
|
|
4744
|
+
# Issue #1752 — the constants a `class << Const` body opens in this file, and those whose singleton side a call
|
|
4745
|
+
# there may rewrite ({#singleton_side_call?}). Such a body records facts about
|
|
4746
|
+
# `Const`'s singleton class (a `remove_method`, a `def`) without declaring `Const`, so it stays out of
|
|
4747
|
+
# `class_sources` (whose readers take a name there as one the project declares) and joins only the per-file
|
|
4748
|
+
# class-declaration sets the incremental `class:` edges diff (`Runner#class_declarations`).
|
|
4749
|
+
def record_singleton_class_sources(sources, path, root)
|
|
4750
|
+
collect_singleton_class_targets(root, []) { |name| (sources[name] ||= Set.new) << path }
|
|
4751
|
+
end
|
|
4752
|
+
|
|
4753
|
+
def collect_singleton_class_targets(node, lexical_prefix, &)
|
|
4754
|
+
case node
|
|
4755
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
4756
|
+
prefix = Source::ConstantPath.declaration_prefix(lexical_prefix, node.constant_path)
|
|
4757
|
+
node.body && prefix && collect_singleton_class_targets(node.body, prefix, &)
|
|
4758
|
+
return
|
|
4759
|
+
when Prism::SingletonClassNode
|
|
4760
|
+
rendered = singleton_receiver_constant_name(node.expression)
|
|
4761
|
+
yield eval_constant_receiver_prefix(node, node.expression, rendered, lexical_prefix).join("::") if rendered
|
|
4762
|
+
when Prism::CallNode
|
|
4763
|
+
yield_singleton_side_constant(node, node.receiver, lexical_prefix, &) if singleton_side_call?(node)
|
|
4764
|
+
end
|
|
4765
|
+
node.rigor_each_child { |child| collect_singleton_class_targets(child, lexical_prefix, &) }
|
|
4766
|
+
end
|
|
4767
|
+
|
|
4768
|
+
# A call on a constant that may rewrite its singleton side (`Const.singleton_class`,
|
|
4769
|
+
# `Const.define_singleton_method`, `Const.extend(M)`, `Const.instance_eval { … }`) records facts about `Const`'s
|
|
4770
|
+
# singleton methods the refinement walk reads from a file that need not declare `Const`, so the file joins the
|
|
4771
|
+
# constant's set too. Written inside `Const`'s own body, the name is in the file's set already; a `def Const.x`
|
|
4772
|
+
# files its own `method:` key.
|
|
4773
|
+
def singleton_side_call?(node)
|
|
4774
|
+
node.name == :singleton_class || surface_rewriting_call?(node)
|
|
4775
|
+
end
|
|
4776
|
+
|
|
4777
|
+
def yield_singleton_side_constant(node, receiver, lexical_prefix)
|
|
4778
|
+
return unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
4779
|
+
|
|
4780
|
+
rendered = singleton_receiver_constant_name(receiver)
|
|
4781
|
+
yield eval_constant_receiver_prefix(node, receiver, rendered, lexical_prefix).join("::") if rendered
|
|
4246
4782
|
end
|
|
4247
4783
|
|
|
4248
4784
|
def build_deferred_ranges(root)
|
|
@@ -5875,6 +6411,7 @@ module Rigor
|
|
|
5875
6411
|
def mixin_tables(root)
|
|
5876
6412
|
accumulator = MixinAccumulator.new
|
|
5877
6413
|
accumulator.direct_body(program_statements(root))
|
|
6414
|
+
accumulator.program_body(program_statements(root))
|
|
5878
6415
|
walk_class_includes(root, [], nil, accumulator)
|
|
5879
6416
|
{
|
|
5880
6417
|
includes: freeze_mixin_lists(accumulator, :include),
|
|
@@ -6029,7 +6566,7 @@ module Rigor
|
|
|
6029
6566
|
walk_class_includes(arg, qualified_prefix, current_class, accumulator,
|
|
6030
6567
|
singleton_self: singleton_self, singleton_cref: singleton_cref)
|
|
6031
6568
|
end
|
|
6032
|
-
if (body =
|
|
6569
|
+
if (body = meta_new_block_body(node))
|
|
6033
6570
|
walk_class_includes(body, qualified_prefix, meta_owner, accumulator,
|
|
6034
6571
|
singleton_cref: body_cref)
|
|
6035
6572
|
end
|
|
@@ -6083,6 +6620,7 @@ module Rigor
|
|
|
6083
6620
|
kind, _arguments, via_send = mixin_call_view(node)
|
|
6084
6621
|
return if kind.nil?
|
|
6085
6622
|
|
|
6623
|
+
current_class ||= toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
6086
6624
|
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
6087
6625
|
.select { |_owner, side| side == :include }
|
|
6088
6626
|
return if effects.empty?
|
|
@@ -6263,6 +6801,25 @@ module Rigor
|
|
|
6263
6801
|
node.name == :prepend
|
|
6264
6802
|
end
|
|
6265
6803
|
|
|
6804
|
+
# Issue #1697 — a receiverless `include M` written as a statement of the file's own top level is
|
|
6805
|
+
# `main.include`, which is `Object.include`: it mixes M into `Object`. It is recorded under its own
|
|
6806
|
+
# owner key, `ObjectMixins::TOPLEVEL_INCLUDE_KEY`, rather than on `Object`, so it rides every table an include
|
|
6807
|
+
# rides (the declaration signature, `discovered_class_sources`, the seed bundle) while no ancestry
|
|
6808
|
+
# walk that types a call reaches it: what Ruby reaches first for a name such an include supplies
|
|
6809
|
+
# depends on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def`
|
|
6810
|
+
# on `main`), so only the diagnostic silences read it (`Inference::ObjectMixins`), and typing is
|
|
6811
|
+
# issue #1715's. Only a direct program statement counts.
|
|
6812
|
+
# The same call inside a top-level block, a method body or a conditional stays unrecorded, as before:
|
|
6813
|
+
# its `self` may not be `main` (a `describe do include M end` or `class_exec` block rebinds it, while
|
|
6814
|
+
# `[1].each do include M end` does not, and the walk cannot tell them apart), and it may not run. `main`
|
|
6815
|
+
# has no `prepend`, and a top-level `extend M` reaches `main`'s singleton alone, so neither is recorded.
|
|
6816
|
+
def toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
6817
|
+
return nil unless current_class.nil? && qualified_prefix.empty?
|
|
6818
|
+
return nil unless node.receiver.nil? && node.name == :include
|
|
6819
|
+
|
|
6820
|
+
accumulator.program_statement?(node) ? ObjectMixins::TOPLEVEL_INCLUDE_KEY : nil
|
|
6821
|
+
end
|
|
6822
|
+
|
|
6266
6823
|
# Issue #1123 — the class a `Recv.prepend(M)` call form targets, as the qualified name the prepend
|
|
6267
6824
|
# table is keyed by, or nil when the receiver names no static class. The receiver is a constant READ
|
|
6268
6825
|
# at the call site, so it resolves in the `Module.nesting` the call is written in — the same rule (and
|
|
@@ -6581,7 +7138,7 @@ module Rigor
|
|
|
6581
7138
|
in_singleton: in_singleton, singleton_self: singleton_self,
|
|
6582
7139
|
singleton_cref: singleton_cref)
|
|
6583
7140
|
end
|
|
6584
|
-
if (body =
|
|
7141
|
+
if (body = meta_new_block_body(node))
|
|
6585
7142
|
walk_class_extends(body, qualified_prefix, meta_owner, accumulator,
|
|
6586
7143
|
singleton_cref: body_cref)
|
|
6587
7144
|
end
|
|
@@ -6653,6 +7210,7 @@ module Rigor
|
|
|
6653
7210
|
return record_module_function(node, current_class, accumulator, in_singleton) if node.name == :module_function
|
|
6654
7211
|
|
|
6655
7212
|
taint_opaque_eval(node, current_class, accumulator)
|
|
7213
|
+
return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
|
|
6656
7214
|
|
|
6657
7215
|
kind, _arguments, via_send = mixin_call_view(node)
|
|
6658
7216
|
return if kind.nil?
|
|
@@ -6665,6 +7223,49 @@ module Rigor
|
|
|
6665
7223
|
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
6666
7224
|
end
|
|
6667
7225
|
|
|
7226
|
+
# Issue #1752 — a call that rewrites a singleton class reached through `singleton_class` (`singleton_class.
|
|
7227
|
+
# class_eval { def x }`, `singleton_class.send(:define_method, :x)`, `Const.singleton_class.prepend(M)`,
|
|
7228
|
+
# `Const.singleton_class.send(:include, M)`) puts methods or modules on that singleton class that no edge
|
|
7229
|
+
# records, so its singleton side is unpositioned: the owner is the enclosing class for a bare or `self.`
|
|
7230
|
+
# receiver, else the constant. True when it tainted.
|
|
7231
|
+
def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
|
|
7232
|
+
receiver = node.receiver
|
|
7233
|
+
return false unless singleton_class_of_receiver?(receiver) && surface_rewriting_call?(node)
|
|
7234
|
+
return false if singleton_class_named_call?(node)
|
|
7235
|
+
|
|
7236
|
+
owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current_class)
|
|
7237
|
+
return false if owner.nil?
|
|
7238
|
+
|
|
7239
|
+
accumulator.taint(owner, :extend)
|
|
7240
|
+
true
|
|
7241
|
+
end
|
|
7242
|
+
|
|
7243
|
+
# A bare, `self.` or constant receiver's `singleton_class` call.
|
|
7244
|
+
def singleton_class_of_receiver?(receiver)
|
|
7245
|
+
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil?
|
|
7246
|
+
end
|
|
7247
|
+
|
|
7248
|
+
# An `attr_*` on a singleton class (`singleton_class.attr_accessor :conf`) rewrites only the names it spells, so
|
|
7249
|
+
# it makes those names' singleton envelopes opaque ({#record_singleton_class_names}) and leaves the rest of the
|
|
7250
|
+
# singleton side readable. An `attr_*` with a name it does not spell as a literal (`attr_accessor(*KEYS)`, a
|
|
7251
|
+
# block's variable), and an `alias_method` or `define_method` there, still taint the side: the own-definer read
|
|
7252
|
+
# would otherwise answer the body Ruby replaced (`definer_resolution_witness_spec.rb`).
|
|
7253
|
+
def singleton_class_named_call?(node)
|
|
7254
|
+
arguments = node.arguments&.arguments || []
|
|
7255
|
+
ATTR_MACROS.include?(node.name) && !arguments.empty? &&
|
|
7256
|
+
arguments.all? { |argument| literal_method_name(argument) }
|
|
7257
|
+
end
|
|
7258
|
+
|
|
7259
|
+
def singleton_class_owner(node, object, qualified_prefix, current_class)
|
|
7260
|
+
return current_class if object.nil? || object.is_a?(Prism::SelfNode)
|
|
7261
|
+
|
|
7262
|
+
rendered = Source::ConstantPath.qualified_name_or_nil(object)
|
|
7263
|
+
return nil if rendered.nil?
|
|
7264
|
+
return rendered if Source::ConstantPath.rooted?(object) || qualified_prefix.empty?
|
|
7265
|
+
|
|
7266
|
+
eval_constant_receiver_prefix(node, object, rendered, qualified_prefix).join("::")
|
|
7267
|
+
end
|
|
7268
|
+
|
|
6668
7269
|
# The bare `module_function` toggle: the module extends itself. A direct statement only.
|
|
6669
7270
|
def record_module_function(node, current_class, accumulator, in_singleton)
|
|
6670
7271
|
return unless current_class && node.receiver.nil? && !in_singleton && ModuleFunctionState.extends_self?(node)
|
|
@@ -7149,9 +7750,7 @@ module Rigor
|
|
|
7149
7750
|
end
|
|
7150
7751
|
|
|
7151
7752
|
def visibility_target_name(arg)
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
nil
|
|
7753
|
+
Source::Literals.symbol_or_string(arg)
|
|
7155
7754
|
end
|
|
7156
7755
|
|
|
7157
7756
|
# Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
|
|
@@ -7179,10 +7778,24 @@ module Rigor
|
|
|
7179
7778
|
class_name = qualified_prefix.join("::")
|
|
7180
7779
|
undef_node.names.each do |name|
|
|
7181
7780
|
method_name = literal_method_name(name)
|
|
7182
|
-
|
|
7781
|
+
next unless method_name
|
|
7782
|
+
|
|
7783
|
+
record_method_envelope_opaque(tables, class_name, method_name)
|
|
7784
|
+
record_removed_name(tables, class_name, method_name)
|
|
7183
7785
|
end
|
|
7184
7786
|
end
|
|
7185
7787
|
|
|
7788
|
+
REMOVING_CALLS = %i[remove_method undef_method].to_set.freeze
|
|
7789
|
+
private_constant :REMOVING_CALLS
|
|
7790
|
+
|
|
7791
|
+
# Issue #1820 — `undef f`, `remove_method :f` or `undef_method :f` on the class, as its own entry beside the
|
|
7792
|
+
# opaque envelopes: an `attr_*` or `define_method` records an opaque envelope too, so only this says the name
|
|
7793
|
+
# is gone.
|
|
7794
|
+
def record_removed_name(tables, class_name, method_name)
|
|
7795
|
+
record_envelope(tables.envelopes, class_name, [Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND, method_name],
|
|
7796
|
+
Source::ParameterEnvelope::OPAQUE)
|
|
7797
|
+
end
|
|
7798
|
+
|
|
7186
7799
|
# Both kinds, and the envelope table only: the evidence says something rewrites the name, not which
|
|
7187
7800
|
# side it lives on, and it is not evidence that the name EXISTS, so the existence table is untouched.
|
|
7188
7801
|
def record_method_envelope_opaque(tables, class_name, method_name)
|
|
@@ -7198,7 +7811,7 @@ module Rigor
|
|
|
7198
7811
|
# `def_delegator :@x, :f`) would leave a `def`'s envelope standing for a method that no longer has it.
|
|
7199
7812
|
NAME_NEUTRAL_MACROS = %i[
|
|
7200
7813
|
private public protected module_function private_class_method public_class_method
|
|
7201
|
-
private_constant public_constant require require_relative autoload
|
|
7814
|
+
private_constant public_constant require require_relative autoload autoload_relative
|
|
7202
7815
|
].to_set.freeze
|
|
7203
7816
|
private_constant :NAME_NEUTRAL_MACROS
|
|
7204
7817
|
|
|
@@ -7239,6 +7852,42 @@ module Rigor
|
|
|
7239
7852
|
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
7240
7853
|
record_surface_mark(tables, name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
7241
7854
|
end
|
|
7855
|
+
elsif singleton_class_of_receiver?(receiver) && singleton_class_named_call?(node)
|
|
7856
|
+
record_singleton_class_names(node, receiver.receiver, qualified_prefix, tables)
|
|
7857
|
+
end
|
|
7858
|
+
end
|
|
7859
|
+
|
|
7860
|
+
# Issue #1752 — the names a {#singleton_class_named_call?} rewrites on its singleton class's owner, each
|
|
7861
|
+
# singleton envelope opaque: the evidence the refinement typer reads as a rewritten singleton name.
|
|
7862
|
+
def record_singleton_class_names(node, object, qualified_prefix, tables)
|
|
7863
|
+
current_class = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
|
|
7864
|
+
owner = singleton_class_owner(node, object, qualified_prefix, current_class)
|
|
7865
|
+
return if owner.nil?
|
|
7866
|
+
|
|
7867
|
+
(node.arguments&.arguments || []).each do |argument|
|
|
7868
|
+
method_name = literal_method_name(argument)
|
|
7869
|
+
next unless method_name
|
|
7870
|
+
|
|
7871
|
+
names = [method_name]
|
|
7872
|
+
names << :"#{method_name}=" if %i[attr_writer attr_accessor].include?(node.name)
|
|
7873
|
+
names.each do |name|
|
|
7874
|
+
record_envelope(tables.envelopes, owner, [:singleton, name], Source::ParameterEnvelope::OPAQUE)
|
|
7875
|
+
contest_envelope(tables, owner, [:singleton, name]) unless tables.certain
|
|
7876
|
+
end
|
|
7877
|
+
end
|
|
7878
|
+
end
|
|
7879
|
+
|
|
7880
|
+
# Issue #1752 — `def Const.x` whose receiver is not the lexical `self` (written at the top level or in another
|
|
7881
|
+
# class's body) defines `Const`'s singleton method, which no table files under `Const`: its singleton name's
|
|
7882
|
+
# envelope turns opaque, the evidence the refinement typer reads as a rewritten singleton name.
|
|
7883
|
+
def record_foreign_singleton_def(node, qualified_prefix, owner_prefix, tables)
|
|
7884
|
+
receiver = node.receiver
|
|
7885
|
+
return unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
7886
|
+
return if def_receiver_targets_lexical_self?(receiver, owner_prefix)
|
|
7887
|
+
|
|
7888
|
+
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
7889
|
+
record_envelope(tables.envelopes, name, [:singleton, node.name], Source::ParameterEnvelope::OPAQUE)
|
|
7890
|
+
contest_envelope(tables, name, [:singleton, node.name]) unless tables.certain
|
|
7242
7891
|
end
|
|
7243
7892
|
end
|
|
7244
7893
|
|
|
@@ -7249,13 +7898,21 @@ module Rigor
|
|
|
7249
7898
|
if dynamic_surface_call?(node)
|
|
7250
7899
|
record_surface_mark(tables, class_name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
7251
7900
|
elsif node.name == :refine
|
|
7252
|
-
|
|
7901
|
+
# Issue #1689 — a `refine` where `self` is a class is the class's own method, and refines nothing.
|
|
7902
|
+
record_refinement(node, qualified_prefix, tables) unless tables.class_body_refine?(node)
|
|
7253
7903
|
elsif !NAME_NEUTRAL_MACROS.include?(node.name)
|
|
7254
|
-
(node
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7904
|
+
record_named_arguments_opaque(node, class_name, tables)
|
|
7905
|
+
end
|
|
7906
|
+
end
|
|
7907
|
+
|
|
7908
|
+
def record_named_arguments_opaque(node, class_name, tables)
|
|
7909
|
+
(node.arguments&.arguments || []).each do |argument|
|
|
7910
|
+
# `memoize def f(a)` wraps the def it is handed exactly as `memoize :f` does.
|
|
7911
|
+
method_name = argument.is_a?(Prism::DefNode) ? argument.name : literal_method_name(argument)
|
|
7912
|
+
next unless method_name
|
|
7913
|
+
|
|
7914
|
+
record_method_envelope_opaque(tables, class_name, method_name)
|
|
7915
|
+
record_removed_name(tables, class_name, method_name) if REMOVING_CALLS.include?(node.name)
|
|
7259
7916
|
end
|
|
7260
7917
|
end
|
|
7261
7918
|
|
|
@@ -7414,7 +8071,7 @@ module Rigor
|
|
|
7414
8071
|
# `alias_method :new, :old` — the CallNode twin of the `alias` keyword (#533; liquid's i18n
|
|
7415
8072
|
# `t` alias was the corpus case). Unlike AliasMethodNode a call's children can carry further
|
|
7416
8073
|
# class bodies (`Class.new do … end`), so the walk continues below it either way.
|
|
7417
|
-
names = alias_method_call_names(node)
|
|
8074
|
+
names = Source::AliasNames.alias_method_call_names(node)
|
|
7418
8075
|
if names && !rec_prefix.empty? && !(singleton_cref && leaf_owner.nil?)
|
|
7419
8076
|
record_alias_call_entry(accumulator, rec_prefix.join("::"), names, node)
|
|
7420
8077
|
end
|
|
@@ -7475,21 +8132,6 @@ module Rigor
|
|
|
7475
8132
|
true
|
|
7476
8133
|
end
|
|
7477
8134
|
|
|
7478
|
-
# `[new_name, old_name]` for an implicit-self `alias_method` call with two literal symbol /
|
|
7479
|
-
# string arguments, or nil. A variable-named alias is runtime data and stays unrecorded.
|
|
7480
|
-
def alias_method_call_names(call_node)
|
|
7481
|
-
return nil unless call_node.name == :alias_method && call_node.receiver.nil?
|
|
7482
|
-
|
|
7483
|
-
args = call_node.arguments&.arguments
|
|
7484
|
-
return nil unless args && args.size == 2
|
|
7485
|
-
|
|
7486
|
-
new_name = literal_method_name(args[0])
|
|
7487
|
-
old_name = literal_method_name(args[1])
|
|
7488
|
-
return nil if new_name.nil? || old_name.nil?
|
|
7489
|
-
|
|
7490
|
-
[new_name, old_name]
|
|
7491
|
-
end
|
|
7492
|
-
|
|
7493
8135
|
# The class/module arm of {#collect_class_alias_map}: under an unnameable cref a
|
|
7494
8136
|
# bare/`self::` header opens `#<singleton>::Name` — the map files nothing for it;
|
|
7495
8137
|
# a rooted or explicit-base header re-anchors at a nameable prefix.
|
|
@@ -7532,11 +8174,12 @@ module Rigor
|
|
|
7532
8174
|
|
|
7533
8175
|
def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
|
|
7534
8176
|
return if qualified_prefix.empty?
|
|
7535
|
-
|
|
8177
|
+
|
|
8178
|
+
names = Source::AliasNames.keyword_names(alias_node)
|
|
8179
|
+
return if names.nil?
|
|
7536
8180
|
|
|
7537
8181
|
class_name = qualified_prefix.join("::")
|
|
7538
|
-
new_name =
|
|
7539
|
-
old_name = alias_node.old_name.unescaped.to_sym
|
|
8182
|
+
new_name, old_name = names
|
|
7540
8183
|
(accumulator[class_name] ||= {})[new_name] = old_name
|
|
7541
8184
|
accumulator.wrote(class_name, new_name, alias_node) if accumulator.is_a?(AliasMap)
|
|
7542
8185
|
end
|
|
@@ -7636,9 +8279,7 @@ module Rigor
|
|
|
7636
8279
|
end
|
|
7637
8280
|
|
|
7638
8281
|
def literal_method_name(node)
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
node.unescaped&.to_sym
|
|
8282
|
+
Source::Literals.symbol_or_string(node)
|
|
7642
8283
|
end
|
|
7643
8284
|
|
|
7644
8285
|
# Walks every file in `paths` (each path is parsed once with `Prism.parse_file`) and returns the unioned
|
|
@@ -8293,6 +8934,9 @@ module Rigor
|
|
|
8293
8934
|
merge_header_nestings(acc[:header_nestings], file_index[:header_nestings] || {})
|
|
8294
8935
|
fold_mixin_lists(acc, file_index)
|
|
8295
8936
|
file_index[:class_sources].each { |cn, files| (acc[:class_sources][cn] ||= Set.new).merge(files) }
|
|
8937
|
+
(file_index[:singleton_class_sources] || {}).each do |cn, files|
|
|
8938
|
+
(acc[:singleton_class_sources][cn] ||= Set.new).merge(files)
|
|
8939
|
+
end
|
|
8296
8940
|
# Issue #722 residue 2 — a pre-#722 seed bundle carries no candidates; the SCHEMA bump makes such a
|
|
8297
8941
|
# blob a cold rebuild, but default so any in-flight fold stays total.
|
|
8298
8942
|
acc[:compact_headers].merge!(file_index[:compact_headers] || {})
|
|
@@ -8381,6 +9025,7 @@ module Rigor
|
|
|
8381
9025
|
# data, so the bundle stays Marshal-clean and a warm file joins the envelopes its cold walk records.
|
|
8382
9026
|
parameter_envelopes: file_index[:parameter_envelopes],
|
|
8383
9027
|
class_source_names: file_index[:class_sources].keys,
|
|
9028
|
+
singleton_class_source_names: (file_index[:singleton_class_sources] || {}).keys,
|
|
8384
9029
|
# Issue #722 residue 2 — plain data, so the bundle stays Marshal-clean and a warm incremental file
|
|
8385
9030
|
# re-anchors its compact headers the way a cold walk of it does.
|
|
8386
9031
|
compact_headers: file_index[:compact_headers],
|
|
@@ -8405,7 +9050,7 @@ module Rigor
|
|
|
8405
9050
|
# ADR-85 WD2 — reconstitutes a cached bundle into a single-file index {#fold_file_index} folds: the
|
|
8406
9051
|
# def-node triples become {DefHandle}s bound to this file's `path`, and the class-source names become a
|
|
8407
9052
|
# `{name => Set[path]}` table (the shape `accumulate_project_index` produces).
|
|
8408
|
-
def bundle_to_file_index(bundle, path) # rubocop:disable Metrics/
|
|
9053
|
+
def bundle_to_file_index(bundle, path) # rubocop:disable Metrics/AbcSize -- one read per bundle slot
|
|
8409
9054
|
{
|
|
8410
9055
|
def_nodes: bundle_defs_to_handles(bundle[:def_nodes], path),
|
|
8411
9056
|
singleton_def_nodes: bundle_defs_to_handles(bundle[:singleton_def_nodes], path),
|
|
@@ -8426,7 +9071,8 @@ module Rigor
|
|
|
8426
9071
|
method_visibilities: bundle[:method_visibilities],
|
|
8427
9072
|
methods: bundle[:methods],
|
|
8428
9073
|
parameter_envelopes: bundle[:parameter_envelopes] || {},
|
|
8429
|
-
class_sources: bundle[:class_source_names]
|
|
9074
|
+
class_sources: bundle_path_sets(bundle[:class_source_names], path),
|
|
9075
|
+
singleton_class_sources: bundle_path_sets(bundle[:singleton_class_source_names], path),
|
|
8430
9076
|
compact_headers: bundle[:compact_headers] || {},
|
|
8431
9077
|
# Issue #644 — a pre-#644 bundle carries no census; the SCHEMA bump makes such a blob a cold
|
|
8432
9078
|
# rebuild, but default so any in-flight fold stays total.
|
|
@@ -8476,7 +9122,7 @@ module Rigor
|
|
|
8476
9122
|
method_visibilities: {},
|
|
8477
9123
|
methods: {},
|
|
8478
9124
|
deferred_ranges: {},
|
|
8479
|
-
parameter_envelopes: {}, class_sources: {},
|
|
9125
|
+
parameter_envelopes: {}, class_sources: {}, singleton_class_sources: {},
|
|
8480
9126
|
# Issue #722 residue 2 — compact-header re-anchor candidates, adjudicated in {#finalize_def_index}.
|
|
8481
9127
|
compact_headers: {},
|
|
8482
9128
|
constant_writes: {},
|
|
@@ -8766,7 +9412,7 @@ module Rigor
|
|
|
8766
9412
|
|
|
8767
9413
|
def constant_write_census(root)
|
|
8768
9414
|
tables = CensusTables.new(writes: {}, seen: Set.new, declared: Set.new, aliases: {},
|
|
8769
|
-
write_census: GlobalWriteCensus::Collector.new)
|
|
9415
|
+
write_census: GlobalWriteCensus::Collector.new(InEffectRefinements.new(root)))
|
|
8770
9416
|
walk_constant_write_census(root, [], tables)
|
|
8771
9417
|
tables
|
|
8772
9418
|
end
|
|
@@ -9699,7 +10345,7 @@ module Rigor
|
|
|
9699
10345
|
record_declarations(part, qualified_prefix, identity_table, discovered,
|
|
9700
10346
|
EMPTY_RENAMES, self_prefix, singleton_cref: singleton_cref)
|
|
9701
10347
|
end
|
|
9702
|
-
if (body = rvalue.block
|
|
10348
|
+
if (body = rvalue.block.is_a?(Prism::BlockNode) && rvalue.block.body)
|
|
9703
10349
|
record_declarations(body, qualified_prefix, identity_table, discovered,
|
|
9704
10350
|
EMPTY_RENAMES, child_prefix, singleton_cref: singleton_cref)
|
|
9705
10351
|
end
|
|
@@ -9934,11 +10580,22 @@ module Rigor
|
|
|
9934
10580
|
propagate(node.expression, table, current_scope)
|
|
9935
10581
|
propagate(node.rescue_expression, table, current_scope.forget_error_info.forget_last_status)
|
|
9936
10582
|
when Prism::PostExecutionNode then propagate_end_body(node, table, current_scope)
|
|
9937
|
-
else
|
|
9938
|
-
node.rigor_each_child { |child| propagate(child, table, current_scope) }
|
|
10583
|
+
else propagate_children(node, table, current_scope)
|
|
9939
10584
|
end
|
|
9940
10585
|
end
|
|
9941
10586
|
|
|
10587
|
+
SELF_OPENING_BODIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
10588
|
+
private_constant :SELF_OPENING_BODIES
|
|
10589
|
+
|
|
10590
|
+
# Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
|
|
10591
|
+
# unknown-`self` mark ({Scope#block_self_unknown?}) stops at it, as the evaluator's fresh body scope does.
|
|
10592
|
+
# Every other node hands its children the scope as is.
|
|
10593
|
+
def propagate_children(node, table, current_scope)
|
|
10594
|
+
child_scope =
|
|
10595
|
+
SELF_OPENING_BODIES.include?(node.class) ? current_scope.with_block_self_unknown(false) : current_scope
|
|
10596
|
+
node.rigor_each_child { |child| propagate(child, table, child_scope) }
|
|
10597
|
+
end
|
|
10598
|
+
|
|
9942
10599
|
# Issue #1361 — the block of `Thread.new`, `Fiber.new`, `define_method` and the other calls
|
|
9943
10600
|
# {FreshFrameBlocks.fresh_entry?} names does not read the match-global narrowing of the body it is written in.
|
|
9944
10601
|
# The evaluator enters it as {FreshFrameBlocks.entry} gives ({MatchRebinding.block_entry}), but a block in a
|
|
@@ -9985,12 +10642,117 @@ module Rigor
|
|
|
9985
10642
|
def unentered_block_entry(node, block, table, current_scope)
|
|
9986
10643
|
return current_scope unless block.is_a?(Prism::BlockNode) && !table.key?(block)
|
|
9987
10644
|
|
|
10645
|
+
narrowing = unentered_block_narrowing(node, current_scope)
|
|
9988
10646
|
current_scope = Narrowing.safe_navigation_block_scope(node, current_scope)
|
|
9989
10647
|
# Issue #1429 — nor a guard's narrowing of a global or constant the body may run after code rebinds.
|
|
9990
10648
|
current_scope = GuardRebinding.block_entry(current_scope, block, node)
|
|
9991
|
-
|
|
10649
|
+
entry =
|
|
10650
|
+
if FreshFrameBlocks.fresh_entry?(node, current_scope)
|
|
10651
|
+
FreshFrameBlocks.entry(current_scope, node)
|
|
10652
|
+
else
|
|
10653
|
+
LastLine.block_entry(FreshFrameBlocks.closure_entry(current_scope, block, node), block, node)
|
|
10654
|
+
end
|
|
10655
|
+
anonymous = AnonymousMetaClass.name_for(node, current_scope.source_path)
|
|
10656
|
+
return unentered_meta_class_body_entry(block, entry, anonymous, table) if anonymous
|
|
10657
|
+
|
|
10658
|
+
refined = refined_body_name(node, current_scope)
|
|
10659
|
+
record_class_body_defs(block, entry, table, Type::Combinator.nominal_of(refined), nil) if refined
|
|
10660
|
+
|
|
10661
|
+
# Issue #1717 — the body's `self` is unknown unless narrowed, as on the evaluator's entry; only the
|
|
10662
|
+
# `call.undefined-method` mark is set here, never #316's opaque mark (its bind gate reads the evaluator's).
|
|
10663
|
+
entry = entry.with_block_self_unknown(true) unless narrowing&.self_type
|
|
10664
|
+
return entry unless narrowing
|
|
10665
|
+
|
|
10666
|
+
MacroBlockSelfType.apply(entry, narrowing, keeps_unknown: current_scope.block_self_narrowing_unknown?(node))
|
|
10667
|
+
end
|
|
10668
|
+
|
|
10669
|
+
# Issue #1120 — the class a `refine X do … end` call's block refines, or nil when the call is not that shape
|
|
10670
|
+
# or is a `refine` where `self` is a class (issue #1689: the class's own method, its block an ordinary one).
|
|
10671
|
+
# The evaluator asks the same two questions ({StatementEvaluator#refined_class_context}).
|
|
10672
|
+
def refined_body_name(node, scope)
|
|
10673
|
+
target = refine_target(node)
|
|
10674
|
+
return nil if target.nil? || InEffectRefinements.class_body_refine?(scope, node)
|
|
10675
|
+
|
|
10676
|
+
refined_class_name(target, scope)
|
|
10677
|
+
end
|
|
10678
|
+
|
|
10679
|
+
# Issue #1120 — the name of the class `refine target do … end` refines: the class `target` types as, or the
|
|
10680
|
+
# constant as written when it does not type as a class object (a gem class with no RBS). Shared by
|
|
10681
|
+
# {StatementEvaluator#refined_class_context} and {#refined_body_name}.
|
|
10682
|
+
def refined_class_name(target, scope)
|
|
10683
|
+
refined = scope.type_of(target)
|
|
10684
|
+
name = refined.is_a?(Type::Singleton) ? refined.class_name : Source::ConstantPath.qualified_name(target)
|
|
10685
|
+
name&.delete_prefix("::")
|
|
10686
|
+
end
|
|
10687
|
+
|
|
10688
|
+
# Issue #1696 — the entry of a `Class.new` / `Module.new` / `Struct.new` / `Data.define` block the evaluator
|
|
10689
|
+
# never entered because it sits in a value position: `Object.const_set(:K, Class.new { attr_reader :x })`,
|
|
10690
|
+
# `[Struct.new(:a) { def b = a }]`. Its `self` is the class `name` ({AnonymousMetaClass.name_for}) names, as
|
|
10691
|
+
# {StatementEvaluator#enter_meta_class_body} gives the statement form; left on the enclosing `self`, a
|
|
10692
|
+
# top-level one read every macro in it as `call.unresolved-toplevel`. Its `def`s are recorded as
|
|
10693
|
+
# {#record_class_body_defs} says.
|
|
10694
|
+
def unentered_meta_class_body_entry(block, entry, name, table)
|
|
10695
|
+
entry = entry.with_block_self_type(Type::Combinator.singleton_of(name)).with_singleton_class_body(false)
|
|
10696
|
+
record_class_body_defs(block, entry, table, Type::Combinator.nominal_of(name), entry.self_type)
|
|
10697
|
+
entry
|
|
10698
|
+
end
|
|
10699
|
+
|
|
10700
|
+
# Issue #1696 — records each `def` a value-position class-body block defines ({#each_class_body_def}) with the
|
|
10701
|
+
# `self` its body runs on, since no evaluator pass enters it: `instance_self` for a plain `def`, `class_self`
|
|
10702
|
+
# for a `def self.m` (skipped when nil). A `refine X` block passes only `Nominal[X]`: Ruby runs its defs on an
|
|
10703
|
+
# instance of X, but the block itself on the refinement module, which Rigor does not model, so the block keeps
|
|
10704
|
+
# the unknown `self` an unnarrowed block has. A `def obj.m` is left to the walk.
|
|
10705
|
+
def record_class_body_defs(block, entry, table, instance_self, class_self)
|
|
10706
|
+
each_class_body_def(block.body) do |def_node|
|
|
10707
|
+
next if table.key?(def_node)
|
|
10708
|
+
|
|
10709
|
+
receiver = def_node.receiver
|
|
10710
|
+
def_self =
|
|
10711
|
+
if receiver.nil? then instance_self
|
|
10712
|
+
elsif receiver.is_a?(Prism::SelfNode) then class_self
|
|
10713
|
+
end
|
|
10714
|
+
table[def_node] = entry.with_block_self_type(def_self) if def_self
|
|
10715
|
+
end
|
|
10716
|
+
end
|
|
9992
10717
|
|
|
9993
|
-
|
|
10718
|
+
# The `def`s a class-body block defines on its class: every one in the body, `private def m` included, short
|
|
10719
|
+
# of a nested block, lambda or `class` / `module` / `class << …` body, each of which opens its own.
|
|
10720
|
+
def each_class_body_def(node, &)
|
|
10721
|
+
return unless node.is_a?(Prism::Node)
|
|
10722
|
+
return yield(node) if node.is_a?(Prism::DefNode)
|
|
10723
|
+
return if CLASS_BODY_DEF_BARRIERS.include?(node.class)
|
|
10724
|
+
|
|
10725
|
+
node.rigor_each_child { |child| each_class_body_def(child, &) }
|
|
10726
|
+
end
|
|
10727
|
+
|
|
10728
|
+
CLASS_BODY_DEF_BARRIERS = [
|
|
10729
|
+
Prism::BlockNode, Prism::LambdaNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
10730
|
+
].freeze
|
|
10731
|
+
private_constant :CLASS_BODY_DEF_BARRIERS
|
|
10732
|
+
|
|
10733
|
+
# The narrowing ({MacroBlockSelfType::Match}) an unentered block's body runs with — its `self` and, for a
|
|
10734
|
+
# `block_as_methods:` entry, the declared refinements (issue #1667) — when the call is one whose block the
|
|
10735
|
+
# engine narrows: an ADR-16 Tier A `block_as_methods:` match ({MacroBlockSelfType}) or `define_method` on the
|
|
10736
|
+
# lexical `self` ({DefineMethodBlockSelf}, issue #963). The evaluator applies the same two narrowings to
|
|
10737
|
+
# the blocks it enters ({StatementEvaluator#build_block_entry_scope}, {StatementEvaluator#enter_call_block})
|
|
10738
|
+
# and the value pass to every block it types ({ExpressionTyper#block_body_self_narrowing}); without this a
|
|
10739
|
+
# block in a value position — `puts(MyApp.get("/x") { redirect "/y" })`, `render json: Alba.serialize(x) {
|
|
10740
|
+
# attributes :id }`, `private define_method(:m) { helper }` — kept the enclosing `self`, and every DSL call
|
|
10741
|
+
# in it read as a call on that. The receiver is typed only when some plugin declares an entry for the
|
|
10742
|
+
# call's name, so a call no entry names costs one table lookup. A miss, or a raise, keeps the entry as
|
|
10743
|
+
# built — the false-positive-safe direction.
|
|
10744
|
+
def unentered_block_narrowing(node, scope)
|
|
10745
|
+
registry = scope.environment&.plugin_registry
|
|
10746
|
+
if registry && !registry.empty? && !registry.contribution_index.block_entries_for(node.name).empty?
|
|
10747
|
+
receiver_type =
|
|
10748
|
+
node.receiver ? scope.type_of(node.receiver) : MacroBlockSelfType.implicit_receiver_type(scope)
|
|
10749
|
+
match = MacroBlockSelfType.match_for(scope: scope, call_node: node, receiver_type: receiver_type)
|
|
10750
|
+
return match if match
|
|
10751
|
+
end
|
|
10752
|
+
defined = DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: node)
|
|
10753
|
+
defined && MacroBlockSelfType::Match.new(self_type: defined, refinements: InEffectRefinements::EMPTY)
|
|
10754
|
+
rescue StandardError
|
|
10755
|
+
nil
|
|
9994
10756
|
end
|
|
9995
10757
|
|
|
9996
10758
|
# The scope the children of an unentered block or lambda inherit. The evaluator enters a statement-level
|