rigortype 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +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 +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +289 -57
- 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/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 +3 -1
- data/lib/rigor/analysis/incremental_session.rb +14 -1
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
- data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +34 -3
- data/lib/rigor/analysis/worker_session.rb +7 -4
- data/lib/rigor/cache/incremental_snapshot.rb +22 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +13 -7
- data/lib/rigor/cli.rb +10 -2
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +46 -2
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +283 -14
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment.rb +21 -3
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +215 -16
- 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 +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +59 -12
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +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_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +687 -60
- data/lib/rigor/inference/statement_evaluator.rb +71 -20
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/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 +29 -6
- data/lib/rigor/protection/mutation_cache.rb +1 -1
- 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 +189 -10
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +55 -5
- 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-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 +27 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- metadata +66 -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"
|
|
@@ -2249,11 +2255,13 @@ module Rigor
|
|
|
2249
2255
|
# @return the `program_globals` table and the census, keyed by the discovery index members it fills
|
|
2250
2256
|
def build_program_global_index(root, default_scope)
|
|
2251
2257
|
accumulator = {}
|
|
2258
|
+
in_effect = InEffectRefinements.new(root)
|
|
2252
2259
|
census = { patched_line_readers: Set.new, clears_last_status: false, defines_case_equality: false,
|
|
2253
|
-
write_census: GlobalWriteCensus::Collector.new }
|
|
2260
|
+
write_census: GlobalWriteCensus::Collector.new(in_effect) }
|
|
2254
2261
|
gather_global_writes(root, default_scope, accumulator, census)
|
|
2255
2262
|
census[:patched_line_readers] = census[:patched_line_readers].freeze
|
|
2256
2263
|
census[:implicit_self_evidence] = LastLine::SelfEvidence.new(root)
|
|
2264
|
+
census[:in_effect_refinements] = in_effect
|
|
2257
2265
|
[accumulator.freeze, census]
|
|
2258
2266
|
end
|
|
2259
2267
|
|
|
@@ -3245,8 +3253,10 @@ module Rigor
|
|
|
3245
3253
|
# `possible_discovered_methods`, `contested_discovered_def_nodes` and
|
|
3246
3254
|
# `contested_discovered_parameter_envelopes`, from the {Certainty} classification `certainty` (computed here
|
|
3247
3255
|
# when the caller has none).
|
|
3248
|
-
def build_methods_and_def_nodes(root, source_path = nil, certainty: Certainty.possible_nodes(root))
|
|
3256
|
+
def build_methods_and_def_nodes(root, source_path = nil, certainty: Certainty.possible_nodes(root), census: nil)
|
|
3249
3257
|
tables = MethodTables.new({}, {}, nil, certainty, true)
|
|
3258
|
+
tables.root = root
|
|
3259
|
+
tables.census = census
|
|
3250
3260
|
def_nodes = {}
|
|
3251
3261
|
walk_methods_and_def_nodes(root, [], false, tables, def_nodes, source_path)
|
|
3252
3262
|
apply_alias_def_nodes(root, def_nodes, tables)
|
|
@@ -3263,8 +3273,79 @@ module Rigor
|
|
|
3263
3273
|
# ADR-119 WD3 — `certainty` is the file's {Certainty} answer and `certain` whether the contribution being
|
|
3264
3274
|
# recorded is certain ({#at} sets it before each recorder). `possible`, `contested_envelopes` and
|
|
3265
3275
|
# `contested_def_nodes` are the siblings the recorders fill, each nil until a possible contribution writes it.
|
|
3276
|
+
#
|
|
3277
|
+
# Issue #1689 — `root` is the walked tree, and `in_effect` its {InEffectRefinements}, built the first time a
|
|
3278
|
+
# `refine`-shaped call asks {#refine_target_of}, so a file without one never builds it.
|
|
3279
|
+
#
|
|
3280
|
+
# ADR-121 WD7 — `census`, when an Array, collects `[start offset, outcome]` for every `refine`-shaped node the
|
|
3281
|
+
# walk accounts for ({ScopeIndexer.refine_census}); `refine_text` memoises the byte offsets of the `refine`
|
|
3282
|
+
# words in the file's text, so a file with none skips every census scan and a `def` with none is not scanned;
|
|
3283
|
+
# `literal_seen` holds the offsets of the refine literals a call's arguments already accounted for;
|
|
3284
|
+
# `source_bytes` memoises the byte offsets of the file's heredoc openers; `loose_constants` memoises
|
|
3285
|
+
# {ScopeIndexer.loose_constant_segments}. The module a `refine`-shaped node is charged to is what the in-effect
|
|
3286
|
+
# walk worked out for it ({#refine_context}).
|
|
3266
3287
|
MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
|
|
3267
|
-
:contested_envelopes, :contested_def_nodes
|
|
3288
|
+
:contested_envelopes, :contested_def_nodes, :root, :in_effect, :census,
|
|
3289
|
+
:refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans) do
|
|
3290
|
+
# {ScopeIndexer.module_refine_target} against this tree's {InEffectRefinements}.
|
|
3291
|
+
def refine_target_of(node)
|
|
3292
|
+
return nil if ScopeIndexer.refine_target(node).nil?
|
|
3293
|
+
|
|
3294
|
+
ScopeIndexer.module_refine_target(node, in_effect_query)
|
|
3295
|
+
end
|
|
3296
|
+
|
|
3297
|
+
# {InEffectRefinements#class_body_refine?} for a `refine` call of this tree.
|
|
3298
|
+
def class_body_refine?(node) = ScopeIndexer.refine_call?(node) && in_effect_query.class_body_refine?(node)
|
|
3299
|
+
|
|
3300
|
+
def in_effect_query = (self.in_effect ||= InEffectRefinements.new(root))
|
|
3301
|
+
|
|
3302
|
+
# Does the walked file's text name `refine` as a word? Every census scan asks first, so a file that only says
|
|
3303
|
+
# `refined` or `refinement` pays nothing for it.
|
|
3304
|
+
def mentions_refine? = !refine_offsets.empty?
|
|
3305
|
+
|
|
3306
|
+
# Does the text in `[start, stop)` name `refine` as a word, or open a heredoc whose body (which follows the
|
|
3307
|
+
# line, and so may end after `stop`) could? A `def` that does neither is not scanned.
|
|
3308
|
+
def refine_in?(start, stop)
|
|
3309
|
+
ScopeIndexer.offset_in?(refine_offsets, start, stop) ||
|
|
3310
|
+
ScopeIndexer.offset_in?(self.source_bytes ||= ScopeIndexer.heredoc_offsets(root), start, stop)
|
|
3311
|
+
end
|
|
3312
|
+
|
|
3313
|
+
# ADR-121 WD7 — the `self` the `refine`-shaped `node` runs on, as the in-effect walk worked it out
|
|
3314
|
+
# ({InEffectRefinements#refine_context}); a module the walk cannot name for a node it did not record.
|
|
3315
|
+
def refine_context(node) = in_effect_query.refine_context(node) || RefineSelf.unknown
|
|
3316
|
+
|
|
3317
|
+
# The byte offsets of each `refine` word in the file's text, ascending.
|
|
3318
|
+
def refine_offsets
|
|
3319
|
+
self.refine_text ||= ScopeIndexer.refine_word_offsets(root)
|
|
3320
|
+
end
|
|
3321
|
+
|
|
3322
|
+
# Records that the walk accounted for the `refine`-shaped `node` with `outcome` (and the module it charged),
|
|
3323
|
+
# when a census is taken.
|
|
3324
|
+
def account(node, outcome, charged = nil)
|
|
3325
|
+
census&.push([node.location.start_offset, outcome, charged])
|
|
3326
|
+
end
|
|
3327
|
+
|
|
3328
|
+
# Marks a refine literal accounted for by its call, so the walk's descent does not account for it again.
|
|
3329
|
+
def see_literal(node) = (self.literal_seen ||= Set.new) << node.location.start_offset
|
|
3330
|
+
|
|
3331
|
+
def literal_seen?(node) = !literal_seen.nil? && literal_seen.include?(node.location.start_offset)
|
|
3332
|
+
|
|
3333
|
+
# A6 — the last segments of the constants this file binds by `||=` or by a literal `const_set`, which the
|
|
3334
|
+
# project-wide `constant_writers` a reader consults leaves out.
|
|
3335
|
+
def loose_constant?(segment)
|
|
3336
|
+
self.loose_constants ||= ScopeIndexer.loose_constant_segments(root)
|
|
3337
|
+
loose_constants.include?(segment)
|
|
3338
|
+
end
|
|
3339
|
+
|
|
3340
|
+
# Issue #1820 — does `node` sit inside a block or lambda of this tree? A block's `self` may be an object even in
|
|
3341
|
+
# a class body or at the top level (`s.instance_eval { … }`, a `define_method` block). The spans are found once
|
|
3342
|
+
# per file, on the first ask.
|
|
3343
|
+
def in_block?(node)
|
|
3344
|
+
self.block_spans ||= ScopeIndexer.block_spans(root)
|
|
3345
|
+
offset = node.location.start_offset
|
|
3346
|
+
block_spans.any? { |start, stop| offset >= start && offset < stop }
|
|
3347
|
+
end
|
|
3348
|
+
|
|
3268
3349
|
# Sets {#certain} for the contribution `node` and returns the tables.
|
|
3269
3350
|
def at(node)
|
|
3270
3351
|
self.certain = !Certainty.possible?(certainty, node)
|
|
@@ -3295,7 +3376,121 @@ module Rigor
|
|
|
3295
3376
|
end
|
|
3296
3377
|
|
|
3297
3378
|
EMPTY_REFINEMENTS = {}.freeze
|
|
3298
|
-
|
|
3379
|
+
REFINEMENT_WILDCARD = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
3380
|
+
REFINE_WORD = /\brefine\b/
|
|
3381
|
+
HEREDOC_OPENER = /<<[~-]?["'`A-Za-z_]/
|
|
3382
|
+
private_constant :EMPTY_REFINEMENTS, :REFINEMENT_WILDCARD, :REFINE_WORD, :HEREDOC_OPENER
|
|
3383
|
+
|
|
3384
|
+
NO_OFFSETS = [].freeze
|
|
3385
|
+
private_constant :NO_OFFSETS
|
|
3386
|
+
|
|
3387
|
+
# The byte offsets of the `refine` words in `root`'s text (none for no tree), which `MethodTables#refine_in?`
|
|
3388
|
+
# matches node offsets against.
|
|
3389
|
+
def refine_word_offsets(root) = pattern_offsets(root, REFINE_WORD)
|
|
3390
|
+
|
|
3391
|
+
# The byte offsets of the heredoc openers (`<<~ID`, `<<-ID`, `<<ID`, `<<"ID"`) in `root`'s text: a heredoc's body
|
|
3392
|
+
# follows its line, so it may end after the `def` that opens it.
|
|
3393
|
+
def heredoc_offsets(root) = pattern_offsets(root, HEREDOC_OPENER)
|
|
3394
|
+
|
|
3395
|
+
def pattern_offsets(root, pattern)
|
|
3396
|
+
return NO_OFFSETS if root.nil?
|
|
3397
|
+
|
|
3398
|
+
# Byte-wise: a file may hold bytes its encoding does not (binary `__END__` data, a stray comment byte), and a
|
|
3399
|
+
# regexp over such a String raises.
|
|
3400
|
+
binary = root.send(:source).source.b
|
|
3401
|
+
return NO_OFFSETS unless pattern.match?(binary)
|
|
3402
|
+
|
|
3403
|
+
offsets = []
|
|
3404
|
+
position = 0
|
|
3405
|
+
while (match = pattern.match(binary, position))
|
|
3406
|
+
offsets << match.begin(0)
|
|
3407
|
+
position = match.end(0)
|
|
3408
|
+
end
|
|
3409
|
+
offsets.freeze
|
|
3410
|
+
end
|
|
3411
|
+
|
|
3412
|
+
# Is one of the ascending `offsets` in `[start, stop)`?
|
|
3413
|
+
# The `[start, end)` byte span of every block and lambda in `root`.
|
|
3414
|
+
def block_spans(root)
|
|
3415
|
+
spans = []
|
|
3416
|
+
return spans if root.nil?
|
|
3417
|
+
|
|
3418
|
+
Source::NodeWalker.each(root) do |node|
|
|
3419
|
+
next unless node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode)
|
|
3420
|
+
|
|
3421
|
+
location = node.location
|
|
3422
|
+
spans << [location.start_offset, location.end_offset]
|
|
3423
|
+
end
|
|
3424
|
+
spans
|
|
3425
|
+
end
|
|
3426
|
+
|
|
3427
|
+
def offset_in?(offsets, start, stop)
|
|
3428
|
+
index = offsets.bsearch_index { |offset| offset >= start }
|
|
3429
|
+
!index.nil? && offsets[index] < stop
|
|
3430
|
+
end
|
|
3431
|
+
|
|
3432
|
+
# A6 — the last segments of the constants `root` binds by `||=` (`K ||= String`, `A::K ||= …`) or by a
|
|
3433
|
+
# `const_set` whose name is a literal (`Object.const_set(:K, String)`), and (issue #1749) by any write in a
|
|
3434
|
+
# `class << …` body: the project-wide constant census files a write there under no name a `refine` argument
|
|
3435
|
+
# spells, but a `def` in that body reads it lexically (`class << self; K = String; def setup = refine(K) {…}`).
|
|
3436
|
+
# Scanned only for a file with a literal-target `refine`.
|
|
3437
|
+
def loose_constant_segments(root)
|
|
3438
|
+
segments = Set.new
|
|
3439
|
+
Source::NodeWalker.each(root) do |node|
|
|
3440
|
+
case node
|
|
3441
|
+
when Prism::ConstantOrWriteNode then segments << node.name.to_s
|
|
3442
|
+
when Prism::ConstantPathOrWriteNode then segments << node.target.name.to_s
|
|
3443
|
+
when Prism::SingletonClassNode then singleton_constant_segments(node.body, segments)
|
|
3444
|
+
when Prism::CallNode
|
|
3445
|
+
name = node.name == :const_set && node.arguments&.arguments&.first
|
|
3446
|
+
segments << name.unescaped if name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)
|
|
3447
|
+
end
|
|
3448
|
+
end
|
|
3449
|
+
segments
|
|
3450
|
+
end
|
|
3451
|
+
|
|
3452
|
+
# The last segment of every constant a write, an operator write or a multiple assignment target in `body` binds.
|
|
3453
|
+
def singleton_constant_segments(body, segments)
|
|
3454
|
+
return if body.nil?
|
|
3455
|
+
|
|
3456
|
+
Source::NodeWalker.each(body) do |node|
|
|
3457
|
+
case node
|
|
3458
|
+
when Prism::ConstantWriteNode, Prism::ConstantTargetNode, Prism::ConstantOperatorWriteNode,
|
|
3459
|
+
Prism::ConstantAndWriteNode, Prism::ConstantPathTargetNode
|
|
3460
|
+
segments << node.name.to_s
|
|
3461
|
+
when Prism::ConstantPathWriteNode, Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathAndWriteNode
|
|
3462
|
+
segments << node.target.name.to_s
|
|
3463
|
+
end
|
|
3464
|
+
end
|
|
3465
|
+
end
|
|
3466
|
+
|
|
3467
|
+
# ADR-121 WD7 — the census gate's view of one parsed file (`root`): `[refinement table, [[start offset,
|
|
3468
|
+
# outcome], …]]`, one pair for every `refine`-shaped node the method walk accounted for. The outcomes are
|
|
3469
|
+
# `:recorded` (a literal target and block, its names recorded; also one in a module's own singleton method,
|
|
3470
|
+
# #1749), `:class_unknown` (a block whose target the walk cannot name), `:targets_wildcard` (a `refine` that
|
|
3471
|
+
# runs once a method is called, other than a module's own singleton method's, or in a block that may rebind
|
|
3472
|
+
# `self`, charged to its module or the wildcard), `:literal` (A3's `:refine` / eval-string literal), `:nested` (a
|
|
3473
|
+
# `refine` in a refine body, or in a singleton method of one), `:dsl` (where `self` is a class, #1689) and
|
|
3474
|
+
# `:raises` (no block, a Proc as the block, another receiver, or a computed target where `self` is `main`), and
|
|
3475
|
+
# `:data` (a `:refine` literal where `self` is a plain class or one of its instances).
|
|
3476
|
+
def refine_census(root)
|
|
3477
|
+
census = []
|
|
3478
|
+
refinements = build_methods_and_def_nodes(root, census: census)[3]
|
|
3479
|
+
[refinements, census]
|
|
3480
|
+
end
|
|
3481
|
+
|
|
3482
|
+
# ADR-121 WD7 (issue #1814) — the refinement table one parsed file contributes: the census of
|
|
3483
|
+
# {#build_methods_and_def_nodes}, whose fourth table is the project walk's own (`seed_discovered_methods` and
|
|
3484
|
+
# `walk_def_contributions` take it from the same call), run for a caller that wants nothing else. The gem source
|
|
3485
|
+
# walker reads a gem file this way, so a gem module's rows carry the wildcards a project module's would
|
|
3486
|
+
# (`walker_walk_census_parity_spec.rb`, beside the walker's specs, holds the two to one table). A file whose text
|
|
3487
|
+
# never names `refine` as a word records no row, so it is not walked. `source_path` names an anonymous module's
|
|
3488
|
+
# row (`#<Module:<path>:L:C>`) as the project walk does, so two files' bare `Module.new` blocks never share one.
|
|
3489
|
+
def refinement_table(root, source_path = nil)
|
|
3490
|
+
return EMPTY_REFINEMENTS if refine_word_offsets(root).empty?
|
|
3491
|
+
|
|
3492
|
+
build_methods_and_def_nodes(root, source_path)[3]
|
|
3493
|
+
end
|
|
3299
3494
|
|
|
3300
3495
|
# Issue #1120 — a refinement table, deep-frozen; the shared empty table when nothing was recorded.
|
|
3301
3496
|
def freeze_refinements(table)
|
|
@@ -3319,8 +3514,7 @@ module Rigor
|
|
|
3319
3514
|
# implicit- or `self`-receiver `refine` with one constant argument and a literal block. A computed target
|
|
3320
3515
|
# (`refine(klass) { … }`) names no class this walk can key, so its body walks as it did before.
|
|
3321
3516
|
def refine_target(node)
|
|
3322
|
-
return nil unless
|
|
3323
|
-
return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
3517
|
+
return nil unless refine_call?(node)
|
|
3324
3518
|
|
|
3325
3519
|
arguments = node.arguments&.arguments
|
|
3326
3520
|
return nil unless arguments&.size == 1
|
|
@@ -3329,25 +3523,237 @@ module Rigor
|
|
|
3329
3523
|
target if target.is_a?(Prism::ConstantReadNode) || target.is_a?(Prism::ConstantPathNode)
|
|
3330
3524
|
end
|
|
3331
3525
|
|
|
3526
|
+
# An implicit- or `self`-receiver `refine` call with a literal block, whatever its argument.
|
|
3527
|
+
def refine_call?(node)
|
|
3528
|
+
node.name == :refine && node.block.is_a?(Prism::BlockNode) &&
|
|
3529
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
3530
|
+
end
|
|
3531
|
+
|
|
3532
|
+
# Issue #1689 — {#refine_target}, for a call that `in_effect` (its file's {InEffectRefinements}) does not place
|
|
3533
|
+
# where `self` is a class. `Class` undefines `refine`, so `refine X do … end` directly in a `class` body is the
|
|
3534
|
+
# class's own method (a DSL), and its block walks as any other block. Every walk that treats a refine body
|
|
3535
|
+
# specially asks this, so the method tables, the in-effect refinements and the typer agree on which calls are
|
|
3536
|
+
# refine bodies.
|
|
3537
|
+
def module_refine_target(node, in_effect)
|
|
3538
|
+
target = refine_target(node)
|
|
3539
|
+
target unless target.nil? || in_effect.class_body_refine?(node)
|
|
3540
|
+
end
|
|
3541
|
+
|
|
3332
3542
|
# 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 (`
|
|
3543
|
+
# the refining module (`refining`, the `self` the `refine` call runs on). X is resolved LEXICALLY
|
|
3334
3544
|
# (`qualified_prefix`), which is where Ruby resolves the argument even inside a `Module.new { … }` block
|
|
3335
3545
|
# whose `self` is anonymous; every name it can denote is recorded, as {#constant_receiver_candidates}
|
|
3336
3546
|
# explains. Nothing in the body reaches the class tables: `refine String do def shout` does not give
|
|
3337
3547
|
# `String` or the refining module a `shout` outside a `using`.
|
|
3338
|
-
|
|
3548
|
+
#
|
|
3549
|
+
# ADR-121 WD7 (issue #1799) — the names are what {RefineCensus.read_body} reads (`def`, `alias`,
|
|
3550
|
+
# `alias_method`, `define_method`, `attr_*`, `undef`), and a body that may define a name it cannot spell adds a
|
|
3551
|
+
# names-wildcard row per target. `refining` is `""` at the top level (where Ruby raises). The body is not
|
|
3552
|
+
# walked any further, so the census scans it here.
|
|
3553
|
+
#
|
|
3554
|
+
# A6 — a target this file binds by `||=` or a literal `const_set` (`K ||= String; refine(K)`) names whatever value
|
|
3555
|
+
# the write holds, so its names go under the wildcard class, as a computed target's do.
|
|
3556
|
+
def record_refinement_defs(node, target, qualified_prefix, refining, tables)
|
|
3557
|
+
loose = tables.loose_constant?(target.name.to_s)
|
|
3558
|
+
tables.account(node, loose ? :class_unknown : :recorded, census_charge(refining))
|
|
3339
3559
|
body = node.block.body
|
|
3340
3560
|
return if body.nil?
|
|
3341
3561
|
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3562
|
+
targets = loose ? [REFINEMENT_WILDCARD] : constant_receiver_candidates(target, qualified_prefix)
|
|
3563
|
+
reading = RefineCensus.read_body(body)
|
|
3564
|
+
refining.each do |owner|
|
|
3565
|
+
tables.refinements = RefineCensus.record_rows(tables.refinements, targets, reading, owner)
|
|
3566
|
+
end
|
|
3567
|
+
census_scan(body, qualified_prefix, tables, refine_body: true) if tables.mentions_refine?
|
|
3568
|
+
end
|
|
3569
|
+
|
|
3570
|
+
# What the census records as charged for `owners`: the one module, or every name.
|
|
3571
|
+
def census_charge(owners) = owners.size == 1 ? owners.first : owners
|
|
3572
|
+
|
|
3573
|
+
# ADR-121 WD7 — a literal-target `refine X do … end` the module-body walk reaches, charged where the in-effect
|
|
3574
|
+
# walk says it runs ({MethodTables#refine_context}): rows for the module it names (the walk's own name for an
|
|
3575
|
+
# anonymous `Module.new` block, `""` at the top level), or a targets-wildcard for that module (the wildcard
|
|
3576
|
+
# where it cannot be named) when it runs only once a method is called or on a module the walk cannot name.
|
|
3577
|
+
def record_literal_target_refine(node, target, qualified_prefix, owner_prefix, tables)
|
|
3578
|
+
context = tables.refine_context(node)
|
|
3579
|
+
here = context.here
|
|
3580
|
+
return record_refinement_defs(node, target, qualified_prefix, [""], tables) if here == RefineSelf::MAIN
|
|
3581
|
+
if context.deferred || here == REFINEMENT_WILDCARD
|
|
3582
|
+
return record_deferred_refine(node, RefineSelf.charged_modules(context), qualified_prefix, tables)
|
|
3583
|
+
end
|
|
3584
|
+
|
|
3585
|
+
record_refinement_defs(node, target, qualified_prefix, census_owners(context, owner_prefix), tables)
|
|
3586
|
+
end
|
|
3587
|
+
|
|
3588
|
+
# The modules a `refine` charged to `context` refines for; an anonymous `Module.new` block's is named by its
|
|
3589
|
+
# position (`owner_prefix`).
|
|
3590
|
+
def census_owners(context, owner_prefix)
|
|
3591
|
+
context.here == RefineSelf::ANONYMOUS ? [owner_prefix.join("::")] : RefineSelf.charged_modules(context)
|
|
3592
|
+
end
|
|
3593
|
+
|
|
3594
|
+
# ADR-121 WD7 — a targets-wildcard for each of `owners`, as a `refine` that runs once a method is called records
|
|
3595
|
+
# (an instance `def`, a `define_method` or `define_singleton_method` block; a module's own singleton method
|
|
3596
|
+
# records rows instead, #1749). The body is not walked any further, so the census scans it here.
|
|
3597
|
+
def record_deferred_refine(node, owners, qualified_prefix, tables)
|
|
3598
|
+
tables.account(node, :targets_wildcard, census_charge(owners))
|
|
3599
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3600
|
+
body = node.block.body
|
|
3601
|
+
census_scan(body, qualified_prefix, tables, refine_body: true) if body && tables.mentions_refine?
|
|
3602
|
+
end
|
|
3603
|
+
|
|
3604
|
+
# ADR-121 WD7 — a `refine`-shaped call the canonical arm ({#refine_target}) does not take: on another receiver
|
|
3605
|
+
# (`Module#refine` is private, so it raises), where `self` is a class (#1689's DSL), with no literal block (no
|
|
3606
|
+
# block raises, and so does a Proc passed as one), or with a target the walk cannot name (`refine(k)`,
|
|
3607
|
+
# `refine(self)`, `refine(K.dup)`). The last records the body's names under the wildcard class, so they decline
|
|
3608
|
+
# on every receiver, and a names-wildcard row when they are incomplete, which makes the module opaque; one that
|
|
3609
|
+
# runs once a method is called, or on a module the walk cannot name, records a targets-wildcard. At the top
|
|
3610
|
+
# level `self` is `main`, which has no `refine`.
|
|
3611
|
+
def account_other_refine(node, owner_prefix, tables)
|
|
3612
|
+
context = tables.refine_context(node)
|
|
3613
|
+
outcome, charged =
|
|
3614
|
+
if tables.class_body_refine?(node) || context.here == RefineSelf::CLASS then :dsl
|
|
3615
|
+
elsif !RefineCensus.self_call?(node) || !node.block.is_a?(Prism::BlockNode) ||
|
|
3616
|
+
context.here == RefineSelf::MAIN
|
|
3617
|
+
:raises
|
|
3618
|
+
elsif context.deferred || context.here == REFINEMENT_WILDCARD
|
|
3619
|
+
owners = RefineSelf.charged_modules(context)
|
|
3620
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3621
|
+
[:targets_wildcard, census_charge(owners)]
|
|
3622
|
+
else
|
|
3623
|
+
owners = census_owners(context, owner_prefix)
|
|
3624
|
+
owners.each { |owner| record_unreadable_target(node, owner, tables) }
|
|
3625
|
+
[:class_unknown, census_charge(owners)]
|
|
3626
|
+
end
|
|
3627
|
+
tables.account(node, outcome, charged)
|
|
3628
|
+
end
|
|
3629
|
+
|
|
3630
|
+
def record_unreadable_target(node, owner, tables)
|
|
3631
|
+
tables.refinements = RefineCensus.record_rows(tables.refinements, [REFINEMENT_WILDCARD],
|
|
3632
|
+
RefineCensus.read_body(node.block.body), owner)
|
|
3633
|
+
end
|
|
3634
|
+
|
|
3635
|
+
# `{"*" => {"*" => [owner]}}`: some `refine` of `owner` (every module, for the wildcard) is unreadable.
|
|
3636
|
+
def record_targets_wildcard(tables, owner)
|
|
3637
|
+
tables.refinements = RefineCensus.add_row(tables.refinements || {}, REFINEMENT_WILDCARD, REFINEMENT_WILDCARD,
|
|
3638
|
+
owner)
|
|
3639
|
+
end
|
|
3640
|
+
|
|
3641
|
+
# ADR-121 WD7 — the census over a subtree the method walk does not descend (a `def` body, a refine body), each
|
|
3642
|
+
# node charged where the in-effect walk says it runs. `refine_body` says `self` is the refinement itself, where a
|
|
3643
|
+
# nested `refine` refines for the refinement and only makes the body's names incomplete (§2.2).
|
|
3644
|
+
def census_scan(node, qualified_prefix, tables, refine_body: false)
|
|
3645
|
+
case node
|
|
3646
|
+
when Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
3647
|
+
refine_body = false
|
|
3648
|
+
when Prism::CallNode
|
|
3649
|
+
if (target = own_method_refine_target(node, tables))
|
|
3650
|
+
return record_own_method_refine(node, target, qualified_prefix, tables)
|
|
3349
3651
|
end
|
|
3652
|
+
|
|
3653
|
+
census_call(node, qualified_prefix, tables, refine_body)
|
|
3654
|
+
when Prism::SymbolNode
|
|
3655
|
+
census_symbol(node, tables)
|
|
3350
3656
|
end
|
|
3657
|
+
node.rigor_each_child { |child| census_scan(child, qualified_prefix, tables, refine_body: refine_body) }
|
|
3658
|
+
end
|
|
3659
|
+
|
|
3660
|
+
# ADR-121 WD7 (issue #1749) — the target of a canonical `refine X do … end` ({#refine_target}) in a module's own
|
|
3661
|
+
# singleton method (`def self.x`, a `def` in the module's `class << self`, or a block there that keeps `self`),
|
|
3662
|
+
# else nil. `self` is the module itself there, so the call refines X for it once the method is called. Anything
|
|
3663
|
+
# else in a method body (an instance `def`, a `define_method` or `define_singleton_method` block, a computed
|
|
3664
|
+
# target, a block that may rebind `self`) charges a targets-wildcard in {#census_call}, and a `refine` in a refine
|
|
3665
|
+
# body is nested there, whichever method it sits in: its context ({RefineSelf.refinement_body}) is not an
|
|
3666
|
+
# own method's.
|
|
3667
|
+
def own_method_refine_target(node, tables)
|
|
3668
|
+
return nil if node.name != :refine
|
|
3669
|
+
|
|
3670
|
+
target = tables.refine_target_of(node)
|
|
3671
|
+
target if target && tables.refine_context(node).own_method
|
|
3672
|
+
end
|
|
3673
|
+
|
|
3674
|
+
# Records `node` as the refine of its module, as a `refine` in the module body is recorded, with the refined
|
|
3675
|
+
# class marked as that one is ({#record_refinement}). The call's block is scanned here, so {#census_scan} does
|
|
3676
|
+
# not descend into it.
|
|
3677
|
+
def record_own_method_refine(node, target, qualified_prefix, tables)
|
|
3678
|
+
record_refinement(node, qualified_prefix, tables)
|
|
3679
|
+
owners = RefineSelf.charged_modules(tables.refine_context(node))
|
|
3680
|
+
record_refinement_defs(node, target, qualified_prefix, owners, tables)
|
|
3681
|
+
end
|
|
3682
|
+
|
|
3683
|
+
def census_call(node, qualified_prefix, tables, refine_body)
|
|
3684
|
+
census_literal_arguments(node, qualified_prefix, tables)
|
|
3685
|
+
return unless node.name == :refine
|
|
3686
|
+
|
|
3687
|
+
context = tables.refine_context(node)
|
|
3688
|
+
nested = refine_body || context.here == RefineSelf::REFINEMENT
|
|
3689
|
+
outcome, charged =
|
|
3690
|
+
if tables.class_body_refine?(node) || context.here == RefineSelf::CLASS then :dsl
|
|
3691
|
+
elsif nested && RefineCensus.self_call?(node) then :nested
|
|
3692
|
+
elsif !RefineCensus.self_call?(node) || node.block.nil? || context.here == RefineSelf::MAIN then :raises
|
|
3693
|
+
else
|
|
3694
|
+
owners = RefineSelf.charged_modules(context)
|
|
3695
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3696
|
+
[:targets_wildcard, census_charge(owners)]
|
|
3697
|
+
end
|
|
3698
|
+
tables.account(node, outcome, charged)
|
|
3699
|
+
end
|
|
3700
|
+
|
|
3701
|
+
# A3 — a `:refine` Symbol argument of any call, or a String argument holding `refine` of an eval, `send` or
|
|
3702
|
+
# method-naming call, records a targets-wildcard for the module the call runs `refine` on: the receiver of a
|
|
3703
|
+
# `send` or `*_eval` (each name a constant one can denote), else the module the in-effect walk says `self` is
|
|
3704
|
+
# there. Where `self` is a plain class or one of its instances, a literal charged to `self` is data.
|
|
3705
|
+
def census_literal_arguments(node, qualified_prefix, tables)
|
|
3706
|
+
arguments = node.arguments&.arguments
|
|
3707
|
+
return if arguments.nil?
|
|
3708
|
+
|
|
3709
|
+
strings = RefineCensus.string_literal_call?(node)
|
|
3710
|
+
arguments.each do |argument|
|
|
3711
|
+
next unless RefineCensus.refine_symbol?(argument) || (strings && RefineCensus.refine_string?(argument))
|
|
3712
|
+
next if tables.literal_seen?(argument)
|
|
3713
|
+
|
|
3714
|
+
tables.see_literal(argument)
|
|
3715
|
+
census_literal_argument(node, argument, qualified_prefix, tables)
|
|
3716
|
+
end
|
|
3717
|
+
end
|
|
3718
|
+
|
|
3719
|
+
def census_literal_argument(call, literal, qualified_prefix, tables)
|
|
3720
|
+
target = RefineCensus.literal_target(call)
|
|
3721
|
+
context = tables.refine_context(literal)
|
|
3722
|
+
return tables.account(literal, :data) if target == :self && context.literal == RefineSelf::CLASS
|
|
3723
|
+
return tables.account(literal, :nested) if target == :self && context.literal == RefineSelf::REFINEMENT
|
|
3724
|
+
|
|
3725
|
+
owners = literal_owners(call, target, RefineSelf.charged_modules(context, literal: true), qualified_prefix)
|
|
3726
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3727
|
+
tables.account(literal, :literal, target == :self ? census_charge(owners) : [:receiver, owners])
|
|
3728
|
+
end
|
|
3729
|
+
|
|
3730
|
+
def literal_owners(call, target, self_modules, qualified_prefix)
|
|
3731
|
+
case target
|
|
3732
|
+
when :self then self_modules
|
|
3733
|
+
when :unknown then [REFINEMENT_WILDCARD]
|
|
3734
|
+
else
|
|
3735
|
+
receiver = call.receiver
|
|
3736
|
+
names = constant_shaped?(receiver) ? constant_receiver_candidates(receiver, qualified_prefix) : []
|
|
3737
|
+
names.empty? ? [REFINEMENT_WILDCARD] : names
|
|
3738
|
+
end
|
|
3739
|
+
end
|
|
3740
|
+
|
|
3741
|
+
def constant_shaped?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
3742
|
+
|
|
3743
|
+
# A3 — a `:refine` Symbol anywhere else (`alias my_refine refine`, `%i[refine]`, a hash) names `Module#refine`
|
|
3744
|
+
# for code the walk cannot follow, run on the module the in-effect walk says `self` is there; where `self` is a
|
|
3745
|
+
# plain class or one of its instances it is data.
|
|
3746
|
+
def census_symbol(node, tables)
|
|
3747
|
+
return unless RefineCensus.refine_symbol?(node) && !tables.literal_seen?(node)
|
|
3748
|
+
|
|
3749
|
+
tables.see_literal(node)
|
|
3750
|
+
context = tables.refine_context(node)
|
|
3751
|
+
return tables.account(node, :data) if context.literal == RefineSelf::CLASS
|
|
3752
|
+
return tables.account(node, :nested) if context.literal == RefineSelf::REFINEMENT
|
|
3753
|
+
|
|
3754
|
+
owners = RefineSelf.charged_modules(context, literal: true)
|
|
3755
|
+
owners.each { |owner| record_targets_wildcard(tables, owner) }
|
|
3756
|
+
tables.account(node, :literal, census_charge(owners))
|
|
3351
3757
|
end
|
|
3352
3758
|
|
|
3353
3759
|
# The instance `def`s a refine body defines on the refined class: nested declarations open their own
|
|
@@ -3503,6 +3909,9 @@ module Rigor
|
|
|
3503
3909
|
# `self::` header under a REBOUND self (eval/meta-new body) names
|
|
3504
3910
|
# `owner::Name` instead.
|
|
3505
3911
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
3912
|
+
if node.is_a?(Prism::ClassNode) && node.superclass && methods_acc.mentions_refine?
|
|
3913
|
+
census_scan(node.superclass, qualified_prefix, methods_acc)
|
|
3914
|
+
end
|
|
3506
3915
|
record_declaration_facts(node, child_prefix, methods_acc.at(node)) unless child_cref
|
|
3507
3916
|
body_prefix = child_cref ? [] : child_prefix
|
|
3508
3917
|
if node.body
|
|
@@ -3519,6 +3928,10 @@ module Rigor
|
|
|
3519
3928
|
# still decline. The lexical cref below is unnameable in every case.
|
|
3520
3929
|
singleton_prefix = singleton_body_prefix(node, in_singleton_class, owner_prefix,
|
|
3521
3930
|
qualified_prefix)
|
|
3931
|
+
tables = methods_acc.at(node)
|
|
3932
|
+
if !node.expression.is_a?(Prism::SelfNode) || tables.in_block?(node)
|
|
3933
|
+
record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
3934
|
+
end
|
|
3522
3935
|
walk_methods_and_def_nodes(node.expression, qualified_prefix, in_singleton_class,
|
|
3523
3936
|
methods_acc, def_nodes_acc, source_path, def_owner_prefix,
|
|
3524
3937
|
singleton_cref: singleton_cref,
|
|
@@ -3536,6 +3949,7 @@ module Rigor
|
|
|
3536
3949
|
return
|
|
3537
3950
|
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
|
|
3538
3951
|
Prism::ConstantPathOrWriteNode
|
|
3952
|
+
record_meta_superclass(node, qualified_prefix, def_owner_prefix, methods_acc.at(node))
|
|
3539
3953
|
if (split = meta_new_block_split(node, qualified_prefix, def_owner_prefix, singleton_cref))
|
|
3540
3954
|
enclosing, body, body_self = split
|
|
3541
3955
|
# A meta-new block rebinds only `self` — `Module.nesting` stays lexical — so
|
|
@@ -3572,6 +3986,10 @@ module Rigor
|
|
|
3572
3986
|
# receiver, a singleton nothing names) or any ownerless prefix under an unnameable
|
|
3573
3987
|
# cref — is NOT top level; `record_def_node` would file the def under `<toplevel>`
|
|
3574
3988
|
# where an implicit-self call could find a method Ruby never installed there.
|
|
3989
|
+
location = node.location
|
|
3990
|
+
if methods_acc.mentions_refine? && methods_acc.refine_in?(location.start_offset, location.end_offset)
|
|
3991
|
+
node.rigor_each_child { |child| census_scan(child, qualified_prefix, methods_acc) }
|
|
3992
|
+
end
|
|
3575
3993
|
unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
|
|
3576
3994
|
(singleton_cref && owner_prefix.empty?)
|
|
3577
3995
|
singleton_def = in_singleton_class || defs_singleton
|
|
@@ -3581,13 +3999,17 @@ module Rigor
|
|
|
3581
3999
|
end
|
|
3582
4000
|
return
|
|
3583
4001
|
when Prism::AliasMethodNode, Prism::UndefNode
|
|
4002
|
+
census_scan(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
|
|
3584
4003
|
unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
|
|
3585
4004
|
(singleton_cref && owner_prefix.empty?)
|
|
3586
4005
|
record_alias_or_undef(node, owner_prefix, in_singleton_class || defs_singleton,
|
|
3587
4006
|
methods_acc.at(node))
|
|
3588
4007
|
end
|
|
3589
4008
|
return
|
|
4009
|
+
when Prism::SymbolNode
|
|
4010
|
+
census_symbol(node, methods_acc) if methods_acc.mentions_refine?
|
|
3590
4011
|
when Prism::CallNode
|
|
4012
|
+
census_literal_arguments(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
|
|
3591
4013
|
if receiver_eval_call?(node)
|
|
3592
4014
|
return walk_eval_methods_and_defs(node, qualified_prefix, in_singleton_class, methods_acc,
|
|
3593
4015
|
def_nodes_acc, source_path, def_owner_prefix,
|
|
@@ -3596,11 +4018,16 @@ module Rigor
|
|
|
3596
4018
|
end
|
|
3597
4019
|
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
|
|
3598
4020
|
source_path)
|
|
4021
|
+
tables = methods_acc.at(node)
|
|
4022
|
+
record_singleton_prepend(node, qualified_prefix, tables,
|
|
4023
|
+
self_ok: node.name == :prepend && tables.in_block?(node))
|
|
3599
4024
|
# Issue #1120 — a refine body's defs go to the refinement table and nowhere else.
|
|
3600
|
-
if (target =
|
|
3601
|
-
return
|
|
4025
|
+
if (target = methods_acc.refine_target_of(node))
|
|
4026
|
+
return record_literal_target_refine(node, target, qualified_prefix, owner_prefix, methods_acc)
|
|
3602
4027
|
end
|
|
3603
4028
|
|
|
4029
|
+
account_other_refine(node, owner_prefix, methods_acc) if node.name == :refine
|
|
4030
|
+
|
|
3604
4031
|
if anonymous
|
|
3605
4032
|
walk_anonymous_meta_block(node, anonymous, qualified_prefix, in_singleton_class, methods_acc,
|
|
3606
4033
|
def_nodes_acc, source_path, def_owner_prefix,
|
|
@@ -3666,8 +4093,24 @@ module Rigor
|
|
|
3666
4093
|
end
|
|
3667
4094
|
|
|
3668
4095
|
# The block form of a meta-new constant write: its members, and the module mark `Module.new` earns.
|
|
4096
|
+
# Issue #1820 — `K = Class.new(Parent)`, with or without a block, names a class whose superclass the superclass
|
|
4097
|
+
# table leaves out; it takes {Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK}.
|
|
4098
|
+
def record_meta_superclass(node, qualified_prefix, self_base, tables)
|
|
4099
|
+
rvalue = meta_new_rvalue(node)
|
|
4100
|
+
return unless class_new_call?(rvalue) && !(rvalue.arguments&.arguments || []).empty?
|
|
4101
|
+
|
|
4102
|
+
child_prefix = meta_new_child_prefix(node, qualified_prefix, self_base)
|
|
4103
|
+
return if child_prefix.nil? || child_prefix.empty?
|
|
4104
|
+
|
|
4105
|
+
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
|
|
4106
|
+
end
|
|
4107
|
+
|
|
3669
4108
|
def record_meta_new_facts(rvalue, child_prefix, tables)
|
|
3670
4109
|
record_meta_members(rvalue, child_prefix, tables)
|
|
4110
|
+
# `Module.new(&BODY)`: the body runs on the new module but cannot be read, so its surface is unknown (#1816).
|
|
4111
|
+
if rvalue.block.is_a?(Prism::BlockArgumentNode)
|
|
4112
|
+
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
4113
|
+
end
|
|
3671
4114
|
return unless module_new_call?(rvalue)
|
|
3672
4115
|
|
|
3673
4116
|
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK)
|
|
@@ -3700,7 +4143,7 @@ module Rigor
|
|
|
3700
4143
|
def record_alias_method_call(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
3701
4144
|
return if qualified_prefix.empty?
|
|
3702
4145
|
|
|
3703
|
-
names = alias_method_call_names(call_node)
|
|
4146
|
+
names = Source::AliasNames.alias_method_call_names(call_node)
|
|
3704
4147
|
return if names.nil?
|
|
3705
4148
|
|
|
3706
4149
|
kind = in_singleton_class ? :singleton : :instance
|
|
@@ -3807,7 +4250,9 @@ module Rigor
|
|
|
3807
4250
|
# other two. {#meta_new_block_call} owns both unwrappings; every walk asks it, so no spelling can be
|
|
3808
4251
|
# recognised by one pass and missed by another.
|
|
3809
4252
|
def meta_new_block_body(node)
|
|
3810
|
-
meta_new_block_call(node)&.block
|
|
4253
|
+
block = meta_new_block_call(node)&.block
|
|
4254
|
+
# A `&BODY` / `&:sym` block ARGUMENT is not a literal block: its body is unreadable here (#1816).
|
|
4255
|
+
block.body if block.is_a?(Prism::BlockNode)
|
|
3811
4256
|
end
|
|
3812
4257
|
|
|
3813
4258
|
# The class-creating call a constant write's rvalue ultimately is, or nil when the write is not the idiom.
|
|
@@ -4061,16 +4506,57 @@ module Rigor
|
|
|
4061
4506
|
return if def_node.body.nil?
|
|
4062
4507
|
|
|
4063
4508
|
Source::NodeWalker.each(def_node.body) do |node|
|
|
4509
|
+
if node.is_a?(Prism::SingletonClassNode)
|
|
4510
|
+
record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
4511
|
+
next
|
|
4512
|
+
end
|
|
4064
4513
|
next unless node.is_a?(Prism::CallNode)
|
|
4065
4514
|
|
|
4066
4515
|
if node.name == :extend
|
|
4067
4516
|
record_object_extension(node, qualified_prefix, tables)
|
|
4068
4517
|
elsif SURFACE_EVAL_CALLS.include?(node.name) || SURFACE_NAMING_CALLS.include?(node.name)
|
|
4069
4518
|
record_body_rewrite(node, qualified_prefix, tables)
|
|
4519
|
+
else
|
|
4520
|
+
record_singleton_prepend(node, qualified_prefix, tables, self_ok: true)
|
|
4070
4521
|
end
|
|
4071
4522
|
end
|
|
4072
4523
|
end
|
|
4073
4524
|
|
|
4525
|
+
# Issue #1820 — `obj.singleton_class.prepend(M)`, or `prepend M` in a `class << obj` body, puts `M` ahead of
|
|
4526
|
+
# `obj`'s class even when the class already includes it (`prepend` re-inserts it; `extend` would not), so
|
|
4527
|
+
# `M`'s refinement answers first for that object. `M` takes {Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK},
|
|
4528
|
+
# the mark an `extend` in a method body already leaves: an object of any class may carry its instance methods
|
|
4529
|
+
# ahead of its class's own. A `singleton_class` of `self` (and a `class << self` body, which the caller leaves
|
|
4530
|
+
# out) written directly in a class or module body or at the top level is the class object's or `main`'s, not an
|
|
4531
|
+
# instance's; in a method body or any block (`self_ok`), where `self` may be an object, it counts.
|
|
4532
|
+
# `singleton_body` says the call sits in a `class << obj` body.
|
|
4533
|
+
def record_singleton_prepend(node, qualified_prefix, tables, singleton_body: false, self_ok: false)
|
|
4534
|
+
return unless node.name == :prepend
|
|
4535
|
+
|
|
4536
|
+
receiver = node.receiver
|
|
4537
|
+
singleton = singleton_body && (receiver.nil? || receiver.is_a?(Prism::SelfNode))
|
|
4538
|
+
singleton ||= singleton_class_call?(receiver, self_ok)
|
|
4539
|
+
record_object_extension(node, qualified_prefix, tables) if singleton
|
|
4540
|
+
end
|
|
4541
|
+
|
|
4542
|
+
def singleton_class_call?(receiver, self_ok)
|
|
4543
|
+
return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class
|
|
4544
|
+
return false unless receiver.arguments.nil?
|
|
4545
|
+
|
|
4546
|
+
object = receiver.receiver
|
|
4547
|
+
self_ok || !(object.nil? || object.is_a?(Prism::SelfNode))
|
|
4548
|
+
end
|
|
4549
|
+
|
|
4550
|
+
def record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
4551
|
+
return if node.body.nil?
|
|
4552
|
+
|
|
4553
|
+
Source::NodeWalker.each(node.body) do |inner|
|
|
4554
|
+
next unless inner.is_a?(Prism::CallNode)
|
|
4555
|
+
|
|
4556
|
+
record_singleton_prepend(inner, qualified_prefix, tables, singleton_body: true, self_ok: true)
|
|
4557
|
+
end
|
|
4558
|
+
end
|
|
4559
|
+
|
|
4074
4560
|
def record_body_rewrite(node, qualified_prefix, tables)
|
|
4075
4561
|
receiver = node.receiver
|
|
4076
4562
|
if receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
@@ -5875,6 +6361,7 @@ module Rigor
|
|
|
5875
6361
|
def mixin_tables(root)
|
|
5876
6362
|
accumulator = MixinAccumulator.new
|
|
5877
6363
|
accumulator.direct_body(program_statements(root))
|
|
6364
|
+
accumulator.program_body(program_statements(root))
|
|
5878
6365
|
walk_class_includes(root, [], nil, accumulator)
|
|
5879
6366
|
{
|
|
5880
6367
|
includes: freeze_mixin_lists(accumulator, :include),
|
|
@@ -6029,7 +6516,7 @@ module Rigor
|
|
|
6029
6516
|
walk_class_includes(arg, qualified_prefix, current_class, accumulator,
|
|
6030
6517
|
singleton_self: singleton_self, singleton_cref: singleton_cref)
|
|
6031
6518
|
end
|
|
6032
|
-
if (body =
|
|
6519
|
+
if (body = meta_new_block_body(node))
|
|
6033
6520
|
walk_class_includes(body, qualified_prefix, meta_owner, accumulator,
|
|
6034
6521
|
singleton_cref: body_cref)
|
|
6035
6522
|
end
|
|
@@ -6083,6 +6570,7 @@ module Rigor
|
|
|
6083
6570
|
kind, _arguments, via_send = mixin_call_view(node)
|
|
6084
6571
|
return if kind.nil?
|
|
6085
6572
|
|
|
6573
|
+
current_class ||= toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
6086
6574
|
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
6087
6575
|
.select { |_owner, side| side == :include }
|
|
6088
6576
|
return if effects.empty?
|
|
@@ -6263,6 +6751,25 @@ module Rigor
|
|
|
6263
6751
|
node.name == :prepend
|
|
6264
6752
|
end
|
|
6265
6753
|
|
|
6754
|
+
# Issue #1697 — a receiverless `include M` written as a statement of the file's own top level is
|
|
6755
|
+
# `main.include`, which is `Object.include`: it mixes M into `Object`. It is recorded under its own
|
|
6756
|
+
# owner key, `ObjectMixins::TOPLEVEL_INCLUDE_KEY`, rather than on `Object`, so it rides every table an include
|
|
6757
|
+
# rides (the declaration signature, `discovered_class_sources`, the seed bundle) while no ancestry
|
|
6758
|
+
# walk that types a call reaches it: what Ruby reaches first for a name such an include supplies
|
|
6759
|
+
# depends on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def`
|
|
6760
|
+
# on `main`), so only the diagnostic silences read it (`Inference::ObjectMixins`), and typing is
|
|
6761
|
+
# issue #1715's. Only a direct program statement counts.
|
|
6762
|
+
# The same call inside a top-level block, a method body or a conditional stays unrecorded, as before:
|
|
6763
|
+
# its `self` may not be `main` (a `describe do include M end` or `class_exec` block rebinds it, while
|
|
6764
|
+
# `[1].each do include M end` does not, and the walk cannot tell them apart), and it may not run. `main`
|
|
6765
|
+
# has no `prepend`, and a top-level `extend M` reaches `main`'s singleton alone, so neither is recorded.
|
|
6766
|
+
def toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
6767
|
+
return nil unless current_class.nil? && qualified_prefix.empty?
|
|
6768
|
+
return nil unless node.receiver.nil? && node.name == :include
|
|
6769
|
+
|
|
6770
|
+
accumulator.program_statement?(node) ? ObjectMixins::TOPLEVEL_INCLUDE_KEY : nil
|
|
6771
|
+
end
|
|
6772
|
+
|
|
6266
6773
|
# Issue #1123 — the class a `Recv.prepend(M)` call form targets, as the qualified name the prepend
|
|
6267
6774
|
# table is keyed by, or nil when the receiver names no static class. The receiver is a constant READ
|
|
6268
6775
|
# at the call site, so it resolves in the `Module.nesting` the call is written in — the same rule (and
|
|
@@ -6581,7 +7088,7 @@ module Rigor
|
|
|
6581
7088
|
in_singleton: in_singleton, singleton_self: singleton_self,
|
|
6582
7089
|
singleton_cref: singleton_cref)
|
|
6583
7090
|
end
|
|
6584
|
-
if (body =
|
|
7091
|
+
if (body = meta_new_block_body(node))
|
|
6585
7092
|
walk_class_extends(body, qualified_prefix, meta_owner, accumulator,
|
|
6586
7093
|
singleton_cref: body_cref)
|
|
6587
7094
|
end
|
|
@@ -7149,9 +7656,7 @@ module Rigor
|
|
|
7149
7656
|
end
|
|
7150
7657
|
|
|
7151
7658
|
def visibility_target_name(arg)
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
nil
|
|
7659
|
+
Source::Literals.symbol_or_string(arg)
|
|
7155
7660
|
end
|
|
7156
7661
|
|
|
7157
7662
|
# Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
|
|
@@ -7179,10 +7684,24 @@ module Rigor
|
|
|
7179
7684
|
class_name = qualified_prefix.join("::")
|
|
7180
7685
|
undef_node.names.each do |name|
|
|
7181
7686
|
method_name = literal_method_name(name)
|
|
7182
|
-
|
|
7687
|
+
next unless method_name
|
|
7688
|
+
|
|
7689
|
+
record_method_envelope_opaque(tables, class_name, method_name)
|
|
7690
|
+
record_removed_name(tables, class_name, method_name)
|
|
7183
7691
|
end
|
|
7184
7692
|
end
|
|
7185
7693
|
|
|
7694
|
+
REMOVING_CALLS = %i[remove_method undef_method].to_set.freeze
|
|
7695
|
+
private_constant :REMOVING_CALLS
|
|
7696
|
+
|
|
7697
|
+
# Issue #1820 — `undef f`, `remove_method :f` or `undef_method :f` on the class, as its own entry beside the
|
|
7698
|
+
# opaque envelopes: an `attr_*` or `define_method` records an opaque envelope too, so only this says the name
|
|
7699
|
+
# is gone.
|
|
7700
|
+
def record_removed_name(tables, class_name, method_name)
|
|
7701
|
+
record_envelope(tables.envelopes, class_name, [Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND, method_name],
|
|
7702
|
+
Source::ParameterEnvelope::OPAQUE)
|
|
7703
|
+
end
|
|
7704
|
+
|
|
7186
7705
|
# Both kinds, and the envelope table only: the evidence says something rewrites the name, not which
|
|
7187
7706
|
# side it lives on, and it is not evidence that the name EXISTS, so the existence table is untouched.
|
|
7188
7707
|
def record_method_envelope_opaque(tables, class_name, method_name)
|
|
@@ -7198,7 +7717,7 @@ module Rigor
|
|
|
7198
7717
|
# `def_delegator :@x, :f`) would leave a `def`'s envelope standing for a method that no longer has it.
|
|
7199
7718
|
NAME_NEUTRAL_MACROS = %i[
|
|
7200
7719
|
private public protected module_function private_class_method public_class_method
|
|
7201
|
-
private_constant public_constant require require_relative autoload
|
|
7720
|
+
private_constant public_constant require require_relative autoload autoload_relative
|
|
7202
7721
|
].to_set.freeze
|
|
7203
7722
|
private_constant :NAME_NEUTRAL_MACROS
|
|
7204
7723
|
|
|
@@ -7249,13 +7768,21 @@ module Rigor
|
|
|
7249
7768
|
if dynamic_surface_call?(node)
|
|
7250
7769
|
record_surface_mark(tables, class_name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
7251
7770
|
elsif node.name == :refine
|
|
7252
|
-
|
|
7771
|
+
# Issue #1689 — a `refine` where `self` is a class is the class's own method, and refines nothing.
|
|
7772
|
+
record_refinement(node, qualified_prefix, tables) unless tables.class_body_refine?(node)
|
|
7253
7773
|
elsif !NAME_NEUTRAL_MACROS.include?(node.name)
|
|
7254
|
-
(node
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7774
|
+
record_named_arguments_opaque(node, class_name, tables)
|
|
7775
|
+
end
|
|
7776
|
+
end
|
|
7777
|
+
|
|
7778
|
+
def record_named_arguments_opaque(node, class_name, tables)
|
|
7779
|
+
(node.arguments&.arguments || []).each do |argument|
|
|
7780
|
+
# `memoize def f(a)` wraps the def it is handed exactly as `memoize :f` does.
|
|
7781
|
+
method_name = argument.is_a?(Prism::DefNode) ? argument.name : literal_method_name(argument)
|
|
7782
|
+
next unless method_name
|
|
7783
|
+
|
|
7784
|
+
record_method_envelope_opaque(tables, class_name, method_name)
|
|
7785
|
+
record_removed_name(tables, class_name, method_name) if REMOVING_CALLS.include?(node.name)
|
|
7259
7786
|
end
|
|
7260
7787
|
end
|
|
7261
7788
|
|
|
@@ -7414,7 +7941,7 @@ module Rigor
|
|
|
7414
7941
|
# `alias_method :new, :old` — the CallNode twin of the `alias` keyword (#533; liquid's i18n
|
|
7415
7942
|
# `t` alias was the corpus case). Unlike AliasMethodNode a call's children can carry further
|
|
7416
7943
|
# class bodies (`Class.new do … end`), so the walk continues below it either way.
|
|
7417
|
-
names = alias_method_call_names(node)
|
|
7944
|
+
names = Source::AliasNames.alias_method_call_names(node)
|
|
7418
7945
|
if names && !rec_prefix.empty? && !(singleton_cref && leaf_owner.nil?)
|
|
7419
7946
|
record_alias_call_entry(accumulator, rec_prefix.join("::"), names, node)
|
|
7420
7947
|
end
|
|
@@ -7475,21 +8002,6 @@ module Rigor
|
|
|
7475
8002
|
true
|
|
7476
8003
|
end
|
|
7477
8004
|
|
|
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
8005
|
# The class/module arm of {#collect_class_alias_map}: under an unnameable cref a
|
|
7494
8006
|
# bare/`self::` header opens `#<singleton>::Name` — the map files nothing for it;
|
|
7495
8007
|
# a rooted or explicit-base header re-anchors at a nameable prefix.
|
|
@@ -7532,11 +8044,12 @@ module Rigor
|
|
|
7532
8044
|
|
|
7533
8045
|
def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
|
|
7534
8046
|
return if qualified_prefix.empty?
|
|
7535
|
-
|
|
8047
|
+
|
|
8048
|
+
names = Source::AliasNames.keyword_names(alias_node)
|
|
8049
|
+
return if names.nil?
|
|
7536
8050
|
|
|
7537
8051
|
class_name = qualified_prefix.join("::")
|
|
7538
|
-
new_name =
|
|
7539
|
-
old_name = alias_node.old_name.unescaped.to_sym
|
|
8052
|
+
new_name, old_name = names
|
|
7540
8053
|
(accumulator[class_name] ||= {})[new_name] = old_name
|
|
7541
8054
|
accumulator.wrote(class_name, new_name, alias_node) if accumulator.is_a?(AliasMap)
|
|
7542
8055
|
end
|
|
@@ -7636,9 +8149,7 @@ module Rigor
|
|
|
7636
8149
|
end
|
|
7637
8150
|
|
|
7638
8151
|
def literal_method_name(node)
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
node.unescaped&.to_sym
|
|
8152
|
+
Source::Literals.symbol_or_string(node)
|
|
7642
8153
|
end
|
|
7643
8154
|
|
|
7644
8155
|
# Walks every file in `paths` (each path is parsed once with `Prism.parse_file`) and returns the unioned
|
|
@@ -8766,7 +9277,7 @@ module Rigor
|
|
|
8766
9277
|
|
|
8767
9278
|
def constant_write_census(root)
|
|
8768
9279
|
tables = CensusTables.new(writes: {}, seen: Set.new, declared: Set.new, aliases: {},
|
|
8769
|
-
write_census: GlobalWriteCensus::Collector.new)
|
|
9280
|
+
write_census: GlobalWriteCensus::Collector.new(InEffectRefinements.new(root)))
|
|
8770
9281
|
walk_constant_write_census(root, [], tables)
|
|
8771
9282
|
tables
|
|
8772
9283
|
end
|
|
@@ -9699,7 +10210,7 @@ module Rigor
|
|
|
9699
10210
|
record_declarations(part, qualified_prefix, identity_table, discovered,
|
|
9700
10211
|
EMPTY_RENAMES, self_prefix, singleton_cref: singleton_cref)
|
|
9701
10212
|
end
|
|
9702
|
-
if (body = rvalue.block
|
|
10213
|
+
if (body = rvalue.block.is_a?(Prism::BlockNode) && rvalue.block.body)
|
|
9703
10214
|
record_declarations(body, qualified_prefix, identity_table, discovered,
|
|
9704
10215
|
EMPTY_RENAMES, child_prefix, singleton_cref: singleton_cref)
|
|
9705
10216
|
end
|
|
@@ -9934,11 +10445,22 @@ module Rigor
|
|
|
9934
10445
|
propagate(node.expression, table, current_scope)
|
|
9935
10446
|
propagate(node.rescue_expression, table, current_scope.forget_error_info.forget_last_status)
|
|
9936
10447
|
when Prism::PostExecutionNode then propagate_end_body(node, table, current_scope)
|
|
9937
|
-
else
|
|
9938
|
-
node.rigor_each_child { |child| propagate(child, table, current_scope) }
|
|
10448
|
+
else propagate_children(node, table, current_scope)
|
|
9939
10449
|
end
|
|
9940
10450
|
end
|
|
9941
10451
|
|
|
10452
|
+
SELF_OPENING_BODIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
10453
|
+
private_constant :SELF_OPENING_BODIES
|
|
10454
|
+
|
|
10455
|
+
# Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
|
|
10456
|
+
# unknown-`self` mark ({Scope#block_self_unknown?}) stops at it, as the evaluator's fresh body scope does.
|
|
10457
|
+
# Every other node hands its children the scope as is.
|
|
10458
|
+
def propagate_children(node, table, current_scope)
|
|
10459
|
+
child_scope =
|
|
10460
|
+
SELF_OPENING_BODIES.include?(node.class) ? current_scope.with_block_self_unknown(false) : current_scope
|
|
10461
|
+
node.rigor_each_child { |child| propagate(child, table, child_scope) }
|
|
10462
|
+
end
|
|
10463
|
+
|
|
9942
10464
|
# Issue #1361 — the block of `Thread.new`, `Fiber.new`, `define_method` and the other calls
|
|
9943
10465
|
# {FreshFrameBlocks.fresh_entry?} names does not read the match-global narrowing of the body it is written in.
|
|
9944
10466
|
# The evaluator enters it as {FreshFrameBlocks.entry} gives ({MatchRebinding.block_entry}), but a block in a
|
|
@@ -9985,12 +10507,117 @@ module Rigor
|
|
|
9985
10507
|
def unentered_block_entry(node, block, table, current_scope)
|
|
9986
10508
|
return current_scope unless block.is_a?(Prism::BlockNode) && !table.key?(block)
|
|
9987
10509
|
|
|
10510
|
+
narrowing = unentered_block_narrowing(node, current_scope)
|
|
9988
10511
|
current_scope = Narrowing.safe_navigation_block_scope(node, current_scope)
|
|
9989
10512
|
# Issue #1429 — nor a guard's narrowing of a global or constant the body may run after code rebinds.
|
|
9990
10513
|
current_scope = GuardRebinding.block_entry(current_scope, block, node)
|
|
9991
|
-
|
|
10514
|
+
entry =
|
|
10515
|
+
if FreshFrameBlocks.fresh_entry?(node, current_scope)
|
|
10516
|
+
FreshFrameBlocks.entry(current_scope, node)
|
|
10517
|
+
else
|
|
10518
|
+
LastLine.block_entry(FreshFrameBlocks.closure_entry(current_scope, block, node), block, node)
|
|
10519
|
+
end
|
|
10520
|
+
anonymous = AnonymousMetaClass.name_for(node, current_scope.source_path)
|
|
10521
|
+
return unentered_meta_class_body_entry(block, entry, anonymous, table) if anonymous
|
|
10522
|
+
|
|
10523
|
+
refined = refined_body_name(node, current_scope)
|
|
10524
|
+
record_class_body_defs(block, entry, table, Type::Combinator.nominal_of(refined), nil) if refined
|
|
10525
|
+
|
|
10526
|
+
# Issue #1717 — the body's `self` is unknown unless narrowed, as on the evaluator's entry; only the
|
|
10527
|
+
# `call.undefined-method` mark is set here, never #316's opaque mark (its bind gate reads the evaluator's).
|
|
10528
|
+
entry = entry.with_block_self_unknown(true) unless narrowing&.self_type
|
|
10529
|
+
return entry unless narrowing
|
|
10530
|
+
|
|
10531
|
+
MacroBlockSelfType.apply(entry, narrowing, keeps_unknown: current_scope.block_self_narrowing_unknown?(node))
|
|
10532
|
+
end
|
|
10533
|
+
|
|
10534
|
+
# Issue #1120 — the class a `refine X do … end` call's block refines, or nil when the call is not that shape
|
|
10535
|
+
# or is a `refine` where `self` is a class (issue #1689: the class's own method, its block an ordinary one).
|
|
10536
|
+
# The evaluator asks the same two questions ({StatementEvaluator#refined_class_context}).
|
|
10537
|
+
def refined_body_name(node, scope)
|
|
10538
|
+
target = refine_target(node)
|
|
10539
|
+
return nil if target.nil? || InEffectRefinements.class_body_refine?(scope, node)
|
|
10540
|
+
|
|
10541
|
+
refined_class_name(target, scope)
|
|
10542
|
+
end
|
|
10543
|
+
|
|
10544
|
+
# Issue #1120 — the name of the class `refine target do … end` refines: the class `target` types as, or the
|
|
10545
|
+
# constant as written when it does not type as a class object (a gem class with no RBS). Shared by
|
|
10546
|
+
# {StatementEvaluator#refined_class_context} and {#refined_body_name}.
|
|
10547
|
+
def refined_class_name(target, scope)
|
|
10548
|
+
refined = scope.type_of(target)
|
|
10549
|
+
name = refined.is_a?(Type::Singleton) ? refined.class_name : Source::ConstantPath.qualified_name(target)
|
|
10550
|
+
name&.delete_prefix("::")
|
|
10551
|
+
end
|
|
10552
|
+
|
|
10553
|
+
# Issue #1696 — the entry of a `Class.new` / `Module.new` / `Struct.new` / `Data.define` block the evaluator
|
|
10554
|
+
# never entered because it sits in a value position: `Object.const_set(:K, Class.new { attr_reader :x })`,
|
|
10555
|
+
# `[Struct.new(:a) { def b = a }]`. Its `self` is the class `name` ({AnonymousMetaClass.name_for}) names, as
|
|
10556
|
+
# {StatementEvaluator#enter_meta_class_body} gives the statement form; left on the enclosing `self`, a
|
|
10557
|
+
# top-level one read every macro in it as `call.unresolved-toplevel`. Its `def`s are recorded as
|
|
10558
|
+
# {#record_class_body_defs} says.
|
|
10559
|
+
def unentered_meta_class_body_entry(block, entry, name, table)
|
|
10560
|
+
entry = entry.with_block_self_type(Type::Combinator.singleton_of(name)).with_singleton_class_body(false)
|
|
10561
|
+
record_class_body_defs(block, entry, table, Type::Combinator.nominal_of(name), entry.self_type)
|
|
10562
|
+
entry
|
|
10563
|
+
end
|
|
10564
|
+
|
|
10565
|
+
# Issue #1696 — records each `def` a value-position class-body block defines ({#each_class_body_def}) with the
|
|
10566
|
+
# `self` its body runs on, since no evaluator pass enters it: `instance_self` for a plain `def`, `class_self`
|
|
10567
|
+
# for a `def self.m` (skipped when nil). A `refine X` block passes only `Nominal[X]`: Ruby runs its defs on an
|
|
10568
|
+
# instance of X, but the block itself on the refinement module, which Rigor does not model, so the block keeps
|
|
10569
|
+
# the unknown `self` an unnarrowed block has. A `def obj.m` is left to the walk.
|
|
10570
|
+
def record_class_body_defs(block, entry, table, instance_self, class_self)
|
|
10571
|
+
each_class_body_def(block.body) do |def_node|
|
|
10572
|
+
next if table.key?(def_node)
|
|
10573
|
+
|
|
10574
|
+
receiver = def_node.receiver
|
|
10575
|
+
def_self =
|
|
10576
|
+
if receiver.nil? then instance_self
|
|
10577
|
+
elsif receiver.is_a?(Prism::SelfNode) then class_self
|
|
10578
|
+
end
|
|
10579
|
+
table[def_node] = entry.with_block_self_type(def_self) if def_self
|
|
10580
|
+
end
|
|
10581
|
+
end
|
|
10582
|
+
|
|
10583
|
+
# The `def`s a class-body block defines on its class: every one in the body, `private def m` included, short
|
|
10584
|
+
# of a nested block, lambda or `class` / `module` / `class << …` body, each of which opens its own.
|
|
10585
|
+
def each_class_body_def(node, &)
|
|
10586
|
+
return unless node.is_a?(Prism::Node)
|
|
10587
|
+
return yield(node) if node.is_a?(Prism::DefNode)
|
|
10588
|
+
return if CLASS_BODY_DEF_BARRIERS.include?(node.class)
|
|
9992
10589
|
|
|
9993
|
-
|
|
10590
|
+
node.rigor_each_child { |child| each_class_body_def(child, &) }
|
|
10591
|
+
end
|
|
10592
|
+
|
|
10593
|
+
CLASS_BODY_DEF_BARRIERS = [
|
|
10594
|
+
Prism::BlockNode, Prism::LambdaNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
10595
|
+
].freeze
|
|
10596
|
+
private_constant :CLASS_BODY_DEF_BARRIERS
|
|
10597
|
+
|
|
10598
|
+
# The narrowing ({MacroBlockSelfType::Match}) an unentered block's body runs with — its `self` and, for a
|
|
10599
|
+
# `block_as_methods:` entry, the declared refinements (issue #1667) — when the call is one whose block the
|
|
10600
|
+
# engine narrows: an ADR-16 Tier A `block_as_methods:` match ({MacroBlockSelfType}) or `define_method` on the
|
|
10601
|
+
# lexical `self` ({DefineMethodBlockSelf}, issue #963). The evaluator applies the same two narrowings to
|
|
10602
|
+
# the blocks it enters ({StatementEvaluator#build_block_entry_scope}, {StatementEvaluator#enter_call_block})
|
|
10603
|
+
# and the value pass to every block it types ({ExpressionTyper#block_body_self_narrowing}); without this a
|
|
10604
|
+
# block in a value position — `puts(MyApp.get("/x") { redirect "/y" })`, `render json: Alba.serialize(x) {
|
|
10605
|
+
# attributes :id }`, `private define_method(:m) { helper }` — kept the enclosing `self`, and every DSL call
|
|
10606
|
+
# in it read as a call on that. The receiver is typed only when some plugin declares an entry for the
|
|
10607
|
+
# call's name, so a call no entry names costs one table lookup. A miss, or a raise, keeps the entry as
|
|
10608
|
+
# built — the false-positive-safe direction.
|
|
10609
|
+
def unentered_block_narrowing(node, scope)
|
|
10610
|
+
registry = scope.environment&.plugin_registry
|
|
10611
|
+
if registry && !registry.empty? && !registry.contribution_index.block_entries_for(node.name).empty?
|
|
10612
|
+
receiver_type =
|
|
10613
|
+
node.receiver ? scope.type_of(node.receiver) : MacroBlockSelfType.implicit_receiver_type(scope)
|
|
10614
|
+
match = MacroBlockSelfType.match_for(scope: scope, call_node: node, receiver_type: receiver_type)
|
|
10615
|
+
return match if match
|
|
10616
|
+
end
|
|
10617
|
+
defined = DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: node)
|
|
10618
|
+
defined && MacroBlockSelfType::Match.new(self_type: defined, refinements: InEffectRefinements::EMPTY)
|
|
10619
|
+
rescue StandardError
|
|
10620
|
+
nil
|
|
9994
10621
|
end
|
|
9995
10622
|
|
|
9996
10623
|
# The scope the children of an unentered block or lambda inherit. The evaluator enters a statement-level
|