rigortype 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -3
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +5 -0
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/lib/rigor/analysis/check_rules/source_arity.rb +163 -59
- data/lib/rigor/analysis/check_rules.rb +118 -87
- data/lib/rigor/analysis/dependency_recorder.rb +200 -18
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +273 -49
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -8
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +2 -2
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner.rb +337 -46
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +4 -3
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +70 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +5 -0
- data/lib/rigor/configuration.rb +6 -5
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +1 -1
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +13 -70
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +136 -45
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/guard_rebinding.rb +53 -12
- data/lib/rigor/inference/macro_block_self_type.rb +14 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +46 -26
- data/lib/rigor/inference/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +147 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +1443 -295
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +80 -14
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +3 -3
- data/lib/rigor/reflection/constant_ancestors.rb +96 -47
- data/lib/rigor/reflection/constant_path.rb +22 -7
- data/lib/rigor/reflection.rb +2 -3
- data/lib/rigor/scope/discovery_index.rb +218 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +295 -322
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +22 -16
- data/lib/rigor/type/combinator.rb +25 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -0
- data/sig/rigor/scope.rbs +43 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +25 -1
data/lib/rigor/scope.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "type"
|
|
4
4
|
require_relative "environment"
|
|
5
5
|
require_relative "scope/discovery_index"
|
|
6
|
+
require_relative "scope/resolution_chain"
|
|
7
|
+
require_relative "inference/definer_resolution"
|
|
6
8
|
require_relative "analysis/fact_store"
|
|
7
9
|
require_relative "analysis/dependency_recorder"
|
|
8
10
|
require_relative "inference/expression_typer"
|
|
@@ -29,7 +31,7 @@ module Rigor
|
|
|
29
31
|
:void_origins, :plugin_typed_calls,
|
|
30
32
|
:optimistic_origins, :optimistic_locals, :optimistic_ivars,
|
|
31
33
|
:repeated_or_writes, :match_frame,
|
|
32
|
-
:constant_narrowings, :guard_records
|
|
34
|
+
:constant_narrowings, :guard_records, :bot_guard_classes
|
|
33
35
|
|
|
34
36
|
# ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
|
|
35
37
|
# reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
|
|
@@ -64,9 +66,12 @@ module Rigor
|
|
|
64
66
|
def discovered_global_write_census = @discovery.discovered_global_write_census
|
|
65
67
|
def discovered_includes = @discovery.discovered_includes
|
|
66
68
|
# Issue #1123 — `{qualified class or module name => [module names it `prepend`s, as written]}`, in
|
|
67
|
-
# instance-ancestor search order (nearest prepend first).
|
|
68
|
-
# a
|
|
69
|
-
|
|
69
|
+
# instance-ancestor search order (nearest prepend first). The one table that tells a `prepend` from an
|
|
70
|
+
# `include`: {ResolutionChain} reads it to put a prepended module ahead of the class.
|
|
71
|
+
# The plugin-facing view: each class's prepends once, nearest first. The discovery index itself keeps every
|
|
72
|
+
# statement (a repeated `prepend` is a no-op in Ruby, the first statement winning), and the resolution chain
|
|
73
|
+
# reads that raw list.
|
|
74
|
+
def discovered_prepends = @discovery.discovered_prepends.transform_values { |names| names.uniq.freeze }.freeze
|
|
70
75
|
def discovered_extends = @discovery.discovered_extends
|
|
71
76
|
def discovered_class_sources = @discovery.discovered_class_sources
|
|
72
77
|
# Issue #644 — `{qualified constant name => Set[declaring file]}`; seeded only on an ADR-46 recording run.
|
|
@@ -85,9 +90,13 @@ module Rigor
|
|
|
85
90
|
# Issue #1290 — the census names sharing `name`'s last segment that some write other than a memo `||=`
|
|
86
91
|
# assigns: the ones that shadow an outer constant of the name, so the lexical ladder stops at them
|
|
87
92
|
# (`Reflection.resolve_constant_type`). That asks once per constant reference, so a bare name is looked up
|
|
88
|
-
# as it stands rather than split
|
|
93
|
+
# as it stands rather than split, and a qualified one is sliced after its last `::` rather than split into
|
|
94
|
+
# an Array of every segment ([#1502](https://github.com/rigortype/rigor/issues/1502)). The two agree on every
|
|
95
|
+
# name a parsed constant path spells. Only the `Foo::` Prism recovers from a syntax error differs, and no
|
|
96
|
+
# file with a parse error is typed.
|
|
89
97
|
def shadowing_constant_names(name)
|
|
90
|
-
|
|
98
|
+
separator = name.rindex("::")
|
|
99
|
+
segment = separator ? name[(separator + 2)..] : name
|
|
91
100
|
@discovery.constant_shadowers[segment] || EMPTY_BOUND_CONSTANT_NAMES
|
|
92
101
|
end
|
|
93
102
|
|
|
@@ -241,12 +250,21 @@ module Rigor
|
|
|
241
250
|
# analysis cannot see runs, so a call or block that may run project or unresolved code restores each recorded
|
|
242
251
|
# binding to the union of this pre-guard type and its narrowed one ({#forget_guard_narrowings}). A write drops
|
|
243
252
|
# the record ({#with_global}), and a name the frame-local special-variable machinery owns is never recorded.
|
|
253
|
+
# Issue #1446 — a class guard's narrowing of an instance variable is recorded too, keyed `[:ivar, :@name]`
|
|
254
|
+
# ({#with_guarded_ivar}), since that code may reach `self` and rebind it; a write drops it ({#without_ivar_guard}).
|
|
244
255
|
EMPTY_GUARD_RECORDS = {}.freeze
|
|
256
|
+
# Issue #1446 — the classes a class guard narrowed a receiver to where the narrowing left it `bot`, keyed like
|
|
257
|
+
# {EMPTY_GUARD_RECORDS} with `[:local, :name]` too: `return unless @cb.is_a?(Proc)` binds `@cb` to `bot` when the
|
|
258
|
+
# guard is disjoint from its binding, but the value that passes it is a `Proc` (or a subclass of one), and a call
|
|
259
|
+
# on it dispatches on that class. `Inference::GuardRebinding` reads the class a `bot` receiver runs its methods
|
|
260
|
+
# on from here ({#bot_guard_classes_for}); a `bot` no class guard produced has no entry. A write or another
|
|
261
|
+
# narrowing of the name drops its entry, and a join keeps one only both arms hold.
|
|
262
|
+
EMPTY_BOT_GUARD_CLASSES = {}.freeze
|
|
245
263
|
private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
|
|
246
264
|
:EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED,
|
|
247
265
|
:EMPTY_FOLD_SAFE, :EMPTY_ORIGINS, :EMPTY_PUBLISHED_CONSTANT_SOURCED,
|
|
248
266
|
:EMPTY_PUBLISHED_CONSTANT_IVARS, :EMPTY_REPEATED_OR_WRITES,
|
|
249
|
-
:EMPTY_CONSTANT_NARROWINGS, :EMPTY_GUARD_RECORDS
|
|
267
|
+
:EMPTY_CONSTANT_NARROWINGS, :EMPTY_GUARD_RECORDS, :EMPTY_BOT_GUARD_CLASSES
|
|
250
268
|
|
|
251
269
|
class << self
|
|
252
270
|
def empty(environment: Environment.default, source_path: nil)
|
|
@@ -287,7 +305,7 @@ module Rigor
|
|
|
287
305
|
@plugin_typed_calls.key?(node)
|
|
288
306
|
end
|
|
289
307
|
|
|
290
|
-
def initialize(
|
|
308
|
+
def initialize( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- one keyword and assignment per field
|
|
291
309
|
environment:, locals:,
|
|
292
310
|
fact_store: Analysis::FactStore.empty,
|
|
293
311
|
self_type: nil,
|
|
@@ -315,7 +333,8 @@ module Rigor
|
|
|
315
333
|
repeated_or_writes: EMPTY_REPEATED_OR_WRITES,
|
|
316
334
|
match_frame: nil,
|
|
317
335
|
constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
|
|
318
|
-
guard_records: EMPTY_GUARD_RECORDS
|
|
336
|
+
guard_records: EMPTY_GUARD_RECORDS,
|
|
337
|
+
bot_guard_classes: EMPTY_BOT_GUARD_CLASSES
|
|
319
338
|
)
|
|
320
339
|
@environment = environment
|
|
321
340
|
@locals = locals
|
|
@@ -346,6 +365,7 @@ module Rigor
|
|
|
346
365
|
@match_frame = match_frame
|
|
347
366
|
@constant_narrowings = constant_narrowings
|
|
348
367
|
@guard_records = guard_records
|
|
368
|
+
@bot_guard_classes = bot_guard_classes
|
|
349
369
|
freeze
|
|
350
370
|
end
|
|
351
371
|
|
|
@@ -440,7 +460,8 @@ module Rigor
|
|
|
440
460
|
# rvalue is one.
|
|
441
461
|
published_constant_sourced: drop_published_constant_sourced_for(:local, name),
|
|
442
462
|
local_origins: drop_origin(@local_origins, name),
|
|
443
|
-
optimistic_locals: keep_marks ? @optimistic_locals : drop_origin(@optimistic_locals, name)
|
|
463
|
+
optimistic_locals: keep_marks ? @optimistic_locals : drop_origin(@optimistic_locals, name),
|
|
464
|
+
bot_guard_classes: drop_bot_guard_class(:local, name))
|
|
444
465
|
end
|
|
445
466
|
private :bind_local
|
|
446
467
|
|
|
@@ -557,6 +578,11 @@ module Rigor
|
|
|
557
578
|
end
|
|
558
579
|
|
|
559
580
|
def with_ivar(name, type)
|
|
581
|
+
bind_ivar(name, type, @guard_records)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# The one body behind {#with_ivar} and {#with_guarded_ivar}, which differ only in the guard records they leave.
|
|
585
|
+
def bind_ivar(name, type, guard_records)
|
|
560
586
|
new_indexed_narrowings = drop_indexed_narrowings_for(:ivar, name)
|
|
561
587
|
new_chain_narrowings = drop_chain_narrowings_for(:ivar, name)
|
|
562
588
|
# ADR-58 WD1 — a method-local ivar write or narrowing is flow-live: drop any declaration-sourced mark so
|
|
@@ -568,8 +594,11 @@ module Rigor
|
|
|
568
594
|
declaration_sourced: drop_declaration_sourced_for(:ivar, name),
|
|
569
595
|
published_constant_sourced: drop_published_constant_sourced_for(:ivar, name),
|
|
570
596
|
ivar_origins: drop_origin(@ivar_origins, name),
|
|
571
|
-
optimistic_ivars: drop_origin(@optimistic_ivars, name)
|
|
597
|
+
optimistic_ivars: drop_origin(@optimistic_ivars, name),
|
|
598
|
+
guard_records: guard_records,
|
|
599
|
+
bot_guard_classes: drop_bot_guard_class(:ivar, name))
|
|
572
600
|
end
|
|
601
|
+
private :bind_ivar
|
|
573
602
|
|
|
574
603
|
# ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only provenance is the class-ivar index.
|
|
575
604
|
# Unlike `with_ivar` this binds the type AND records the declaration-sourced mark in one transition.
|
|
@@ -680,7 +709,8 @@ module Rigor
|
|
|
680
709
|
name = name.to_sym
|
|
681
710
|
written = rebuild(globals: @globals.merge(name => type).freeze,
|
|
682
711
|
declaration_sourced: drop_declaration_sourced_for(:global, name),
|
|
683
|
-
guard_records: drop_guard_record(:global, name)
|
|
712
|
+
guard_records: drop_guard_record(:global, name),
|
|
713
|
+
bot_guard_classes: drop_bot_guard_class(:global, name))
|
|
684
714
|
alias_name = STDOUT_ALIASES[name]
|
|
685
715
|
alias_name ? written.forget_guard_narrowing_of(:global, alias_name) : written
|
|
686
716
|
end
|
|
@@ -717,7 +747,28 @@ module Rigor
|
|
|
717
747
|
records = record ? add_guard_record([:global, name].freeze, pre_guard) : drop_guard_record(:global, name)
|
|
718
748
|
rebuild(globals: @globals.merge(name => type).freeze,
|
|
719
749
|
declaration_sourced: drop_declaration_sourced_for(:global, name),
|
|
720
|
-
guard_records: records
|
|
750
|
+
guard_records: records,
|
|
751
|
+
bot_guard_classes: drop_bot_guard_class(:global, name))
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# Issue #1446 — binds the instance variable `name` to `type` on a class guard's edge, recording `pre_guard` as
|
|
755
|
+
# {#with_guarded_global} does. The truthiness, `nil?`, `&.` and `respond_to?` guards narrow an instance variable
|
|
756
|
+
# through {#with_ivar} and record nothing.
|
|
757
|
+
def with_guarded_ivar(name, type, pre_guard)
|
|
758
|
+
name = name.to_sym
|
|
759
|
+
bind_ivar(name, type, add_guard_record([:ivar, name].freeze, pre_guard))
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# Issue #1446 — this scope with no guard record for the instance variable `name`: what a write to it leaves.
|
|
763
|
+
# {#with_ivar} keeps the record, since a narrowing binds through it too.
|
|
764
|
+
def without_ivar_guard(name)
|
|
765
|
+
records = drop_guard_record(:ivar, name.to_sym)
|
|
766
|
+
records.equal?(@guard_records) ? self : rebuild(guard_records: records)
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
# Issue #1446 — true when a class guard's narrowing of the instance variable `name` is live.
|
|
770
|
+
def guard_narrowed_ivar?(name)
|
|
771
|
+
!@guard_records.empty? && @guard_records.key?([:ivar, name.to_sym])
|
|
721
772
|
end
|
|
722
773
|
|
|
723
774
|
# Issue #1429 — the type a guard narrowed the constant reference `key` to, or nil.
|
|
@@ -731,7 +782,23 @@ module Rigor
|
|
|
731
782
|
# {#with_guarded_global} does.
|
|
732
783
|
def with_constant_narrowing(key, type, pre_guard)
|
|
733
784
|
rebuild(constant_narrowings: @constant_narrowings.merge(key => type).freeze,
|
|
734
|
-
guard_records: add_guard_record([:constant, key].freeze, pre_guard)
|
|
785
|
+
guard_records: add_guard_record([:constant, key].freeze, pre_guard),
|
|
786
|
+
bot_guard_classes: drop_bot_guard_class(:constant, key))
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# Issue #1446 — this scope with `class_names` recorded as the classes a class guard narrowed the receiver `[kind,
|
|
790
|
+
# name]` to, where the guard left it `bot` ({EMPTY_BOT_GUARD_CLASSES}). Call it after the narrowing, which drops
|
|
791
|
+
# any earlier entry.
|
|
792
|
+
def with_bot_guard_classes(kind, name, class_names)
|
|
793
|
+
key = [kind, kind == :constant ? name : name.to_sym].freeze
|
|
794
|
+
rebuild(bot_guard_classes: @bot_guard_classes.merge(key => class_names.dup.freeze).freeze)
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
# Issue #1446 — the classes a class guard narrowed the `bot` receiver `[kind, name]` to, or nil.
|
|
798
|
+
def bot_guard_classes_for(kind, name)
|
|
799
|
+
return nil if @bot_guard_classes.empty?
|
|
800
|
+
|
|
801
|
+
@bot_guard_classes[[kind, name]]
|
|
735
802
|
end
|
|
736
803
|
|
|
737
804
|
# Issue #1429 — every constant reference whose last segment is `name` with no narrowing: what a write to a constant
|
|
@@ -741,7 +808,9 @@ module Rigor
|
|
|
741
808
|
return self if keys.empty?
|
|
742
809
|
|
|
743
810
|
records = @guard_records.reject { |(kind, key), _| kind == :constant && keys.include?(key) }
|
|
744
|
-
|
|
811
|
+
classes = @bot_guard_classes.except(*keys.map { |key| [:constant, key] }).freeze
|
|
812
|
+
rebuild(constant_narrowings: @constant_narrowings.except(*keys).freeze, guard_records: records.freeze,
|
|
813
|
+
bot_guard_classes: classes)
|
|
745
814
|
end
|
|
746
815
|
|
|
747
816
|
# Issue #1429 — the constant reference `key` with no narrowing.
|
|
@@ -749,11 +818,12 @@ module Rigor
|
|
|
749
818
|
return self unless @constant_narrowings.key?(key)
|
|
750
819
|
|
|
751
820
|
rebuild(constant_narrowings: @constant_narrowings.except(key).freeze,
|
|
752
|
-
guard_records: @guard_records.except([:constant, key]).freeze
|
|
821
|
+
guard_records: @guard_records.except([:constant, key]).freeze,
|
|
822
|
+
bot_guard_classes: drop_bot_guard_class(:constant, key))
|
|
753
823
|
end
|
|
754
824
|
|
|
755
|
-
# True when a guard's narrowing of a global or
|
|
756
|
-
# so the gate on every scan that decides whether to.
|
|
825
|
+
# True when a guard's narrowing of a global, constant or instance variable is live, the state
|
|
826
|
+
# {#forget_guard_narrowings} drops and so the gate on every scan that decides whether to.
|
|
757
827
|
def guard_narrowed?
|
|
758
828
|
!@guard_records.empty?
|
|
759
829
|
end
|
|
@@ -762,23 +832,19 @@ module Rigor
|
|
|
762
832
|
# the union of its narrowed type and the binding the guard narrowed, and no record is left. The union, not the
|
|
763
833
|
# pre-guard binding alone, because the code may leave the value as it was: `$stdout.is_a?(StringIO)`, then a
|
|
764
834
|
# helper, reads `IO | StringIO`, which keeps `$stdout.string` quiet as the guard intended.
|
|
835
|
+
#
|
|
836
|
+
# Issue #1446 — an instance variable a class guard narrowed is restored the same way.
|
|
765
837
|
def forget_guard_narrowings
|
|
766
838
|
return self if @guard_records.empty?
|
|
767
839
|
|
|
768
|
-
|
|
769
|
-
constants = @constant_narrowings
|
|
840
|
+
tables = { global: @globals, constant: @constant_narrowings, ivar: @ivars }
|
|
770
841
|
@guard_records.each do |(kind, name), pre_guard|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
else
|
|
775
|
-
current = constants[name]
|
|
776
|
-
constants = constants.merge(name => Type::Combinator.union(pre_guard, current)) if current
|
|
777
|
-
end
|
|
842
|
+
table = tables.fetch(kind)
|
|
843
|
+
current = table[name]
|
|
844
|
+
tables[kind] = table.merge(name => Type::Combinator.union(pre_guard, current)) if current
|
|
778
845
|
end
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
guard_records: EMPTY_GUARD_RECORDS)
|
|
846
|
+
globals, constants, ivars = tables.values_at(:global, :constant, :ivar).map { |t| t.frozen? ? t : t.freeze }
|
|
847
|
+
rebuild(globals: globals, constant_narrowings: constants, ivars: ivars, guard_records: EMPTY_GUARD_RECORDS)
|
|
782
848
|
end
|
|
783
849
|
|
|
784
850
|
# Issue #1362 (ADR-58 parity, ADR-117 Decision point 2) — used by the method-entry and top-level seeds to bind a
|
|
@@ -998,6 +1064,18 @@ module Rigor
|
|
|
998
1064
|
@self_type.nil?
|
|
999
1065
|
end
|
|
1000
1066
|
|
|
1067
|
+
# ADR-119 WD1 — the RAW slot of the def tables for `class_name` and `method_name`: the instance side by default,
|
|
1068
|
+
# the singleton side for `kind: :singleton`. The slot is a live `Prism::DefNode` or a {Inference::DefHandle}
|
|
1069
|
+
# (ADR-85 WD3), returned as stored: no dependency recording and no handle resolution. ADR-119 C1d/C2 decide how
|
|
1070
|
+
# a contested slot answers here. Callers that need a node go through {#user_def_for} or {#singleton_def_for}.
|
|
1071
|
+
# The `same_slot` census reads of this slot stay justified: certainty reaches them through the declaration
|
|
1072
|
+
# signature the producer fingerprints (C1d-a), not through this accessor.
|
|
1073
|
+
def def_node_slot(class_name, method_name, kind = :instance)
|
|
1074
|
+
table = kind == :singleton ? discovered_singleton_def_nodes : discovered_def_nodes
|
|
1075
|
+
per_class = table[class_name]
|
|
1076
|
+
per_class && per_class[method_name]
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1001
1079
|
# v0.0.2 #5 — per-class table mapping `method_name (Symbol) → Prism::DefNode`. Populated by `ScopeIndexer`
|
|
1002
1080
|
# alongside `discovered_methods` for instance-side defs only (singleton-side and `define_method`-introduced
|
|
1003
1081
|
# methods do not contribute a static body the engine can re-type). Consumed by `ExpressionTyper` to do
|
|
@@ -1085,12 +1163,15 @@ module Rigor
|
|
|
1085
1163
|
same_file_top_level_def?(method_name) ? node : nil
|
|
1086
1164
|
end
|
|
1087
1165
|
|
|
1166
|
+
# ADR-119 WD1 errata, deliberately unchanged: this compares the recorded site's FILE with the call's file, and
|
|
1167
|
+
# which file a def sits in does not depend on whether it executes. The `<toplevel>` slots are contested
|
|
1168
|
+
# truthfully (ADR-119 C1d-a) but unread until C2 makes the top-level reader decline on them.
|
|
1088
1169
|
def same_file_top_level_def?(method_name)
|
|
1089
1170
|
key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
1090
1171
|
site = discovered_def_sources.dig(key, method_name.to_sym)
|
|
1091
1172
|
return true if site.nil? || @source_path.nil?
|
|
1092
1173
|
|
|
1093
|
-
File.
|
|
1174
|
+
File.absolute_path(site.sub(/:\d+\z/, "")) == File.absolute_path(@source_path)
|
|
1094
1175
|
end
|
|
1095
1176
|
private :same_file_top_level_def?
|
|
1096
1177
|
|
|
@@ -1272,32 +1353,6 @@ module Rigor
|
|
|
1272
1353
|
@discovery.discovered_includes[class_name.to_s] || []
|
|
1273
1354
|
end
|
|
1274
1355
|
|
|
1275
|
-
# Issue #1123 — the modules `class_name` `prepend`s, in instance-ancestor SEARCH order: the nearest
|
|
1276
|
-
# prepend first, so `prepend A; prepend B` answers `["B", "A"]` and `prepend A, B` keeps `["A", "B"]`
|
|
1277
|
-
# (each Ruby statement's argument list lands as one unit, ahead of the earlier statements'). Read by
|
|
1278
|
-
# {#user_def_through_ancestors}'s prepend wedge — Ruby inserts a prepended module, and its own ancestry,
|
|
1279
|
-
# IMMEDIATELY BEFORE the class that prepends it, so these names are searched ahead of the class's own
|
|
1280
|
-
# `def`s. Empty for a class the project never prepends, which is the behaviour every class had before
|
|
1281
|
-
# `prepend` was ordered at all.
|
|
1282
|
-
#
|
|
1283
|
-
# The same names ALSO appear in {#includes_of} — that table answers "which modules does this class
|
|
1284
|
-
# carry", which is what the arity, visibility, reflection and constant-scope consumers ask, and a
|
|
1285
|
-
# prepended module does contribute instance methods. This table adds the ORDER and the KIND; it never
|
|
1286
|
-
# removes a name from the other one.
|
|
1287
|
-
#
|
|
1288
|
-
# The class dependency is recorded only on a HIT, as {#data_member_layout} does: the wedge asks this
|
|
1289
|
-
# for every class the walk visits, so recording on a miss would give a caller file an ancestry edge on
|
|
1290
|
-
# a class that prepends nothing — an edge where ADR-46 slice 4 keeps method-body reads at
|
|
1291
|
-
# symbol granularity.
|
|
1292
|
-
def prepends_of(class_name)
|
|
1293
|
-
names = @discovery.discovered_prepends[class_name.to_s]
|
|
1294
|
-
return EMPTY_MIXIN_NAMES if names.nil?
|
|
1295
|
-
|
|
1296
|
-
record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
|
|
1297
|
-
names
|
|
1298
|
-
end
|
|
1299
|
-
private :prepends_of
|
|
1300
|
-
|
|
1301
1356
|
# Issue #898 — the module names `extend`ed onto `class_name`'s SINGLETON, as written, gathered up the
|
|
1302
1357
|
# as-written superclass chain because a singleton class inherits its superclass's singleton class
|
|
1303
1358
|
# (`class Base; extend Comparable; end; class Widget < Base; end` leaves `Widget.is_a?(Comparable)`
|
|
@@ -1326,224 +1381,145 @@ module Rigor
|
|
|
1326
1381
|
names.uniq
|
|
1327
1382
|
end
|
|
1328
1383
|
|
|
1329
|
-
# ADR-24
|
|
1330
|
-
#
|
|
1331
|
-
#
|
|
1332
|
-
#
|
|
1384
|
+
# ADR-24, amended for #1567 / #1568 / #1570 / #1571 — every reader below answers in Ruby's linearised
|
|
1385
|
+
# ancestor order, read from {ResolutionChain} (an internal class: `Scope`'s own surface is the plugin API
|
|
1386
|
+
# `spec/rigor/public_api_drift_spec.rb` pins, and the chain is not part of it). None walks `includes_of` /
|
|
1387
|
+
# `superclass_of` / the extends table itself; `spec/rigor/scope/ancestry_walker_detection_spec.rb` fails on
|
|
1388
|
+
# a method that does. Each keeps its signature and its return type exactly: only WHICH definer it answers
|
|
1389
|
+
# moved. `name_memo:` is still accepted and no longer needed — the chain memoises name resolution itself.
|
|
1390
|
+
UNUSED_NAME_MEMO = {}.freeze
|
|
1391
|
+
private_constant :UNUSED_NAME_MEMO
|
|
1333
1392
|
#
|
|
1334
|
-
#
|
|
1335
|
-
#
|
|
1336
|
-
#
|
|
1337
|
-
|
|
1338
|
-
#
|
|
1339
|
-
#
|
|
1340
|
-
#
|
|
1393
|
+
# Every reader below asks {ResolutionChain#settle} whether its chain answer stands, and where it does not
|
|
1394
|
+
# answers what the walk it replaced answered ({ResolutionChain::MasterOrder}) — the tables cannot say which
|
|
1395
|
+
# world ran, and a disagreement is no reason to answer anything new.
|
|
1396
|
+
|
|
1397
|
+
# ADR-24 slice 2 — the user-side method lookup: the first project `def` of `method_name` along
|
|
1398
|
+
# `class_name`'s instance chain, as `[def_node, owner]`, or `[nil, nil]` when nothing the project declares
|
|
1399
|
+
# defines it. An external ancestor is passed over, as before: a caller asking "does the project define
|
|
1400
|
+
# this" must not read an RBS module's declaration as absence (#1572 is the typing read that will stop
|
|
1401
|
+
# there).
|
|
1341
1402
|
#
|
|
1342
|
-
#
|
|
1343
|
-
#
|
|
1344
|
-
#
|
|
1403
|
+
# It lives HERE, not in a consumer, because it reads nothing but this scope's frozen discovery index — the
|
|
1404
|
+
# property that lets `ExpressionTyper` memoise its results run-wide. `ExpressionTyper#compute_user_def_with_owner`
|
|
1405
|
+
# wraps it in that memo for the dispatch hot path, and {Inference::MethodDispatcher::StructMaterialization}'s
|
|
1406
|
+
# `.with` guard reads the same answer (#598 review).
|
|
1345
1407
|
#
|
|
1346
|
-
#
|
|
1347
|
-
#
|
|
1348
|
-
#
|
|
1349
|
-
#
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
until queue.empty?
|
|
1359
|
-
current = queue.shift
|
|
1360
|
-
next if current.nil? || seen[current]
|
|
1361
|
-
|
|
1362
|
-
seen[current] = true
|
|
1363
|
-
visited += 1
|
|
1364
|
-
return ancestor_walk_gave_up if visited > ANCESTOR_WALK_LIMIT
|
|
1408
|
+
# #1567 — the walk was breadth-first until the chain replaced it: `class C < Base; include A` with `A`
|
|
1409
|
+
# including `M` answered `Base#foo` where Ruby calls `M#foo`. The prepend wedge #1123 added (a prepended
|
|
1410
|
+
# module searched ahead of the class's own `def`s) and the include search order #1173 fixed are both
|
|
1411
|
+
# positions on the chain now, at every class on it.
|
|
1412
|
+
def user_def_through_ancestors(class_name, method_name, name_memo: UNUSED_NAME_MEMO) # rubocop:disable Lint/UnusedMethodArgument
|
|
1413
|
+
chain = ResolutionChain.for(self, class_name.to_s, :instance, :methods)
|
|
1414
|
+
found = first_user_def(chain, method_name)
|
|
1415
|
+
owner = found&.last
|
|
1416
|
+
if chain.settle(self, owner, owner: owner) { |retro| first_user_def(retro, method_name)&.last } == :master
|
|
1417
|
+
found = master_user_def(class_name.to_s, method_name)
|
|
1418
|
+
end
|
|
1419
|
+
return found if found
|
|
1365
1420
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1421
|
+
chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
|
|
1422
|
+
end
|
|
1368
1423
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1424
|
+
def first_user_def(chain, method_name)
|
|
1425
|
+
chain.search(self) do |entry|
|
|
1426
|
+
next if entry.external?
|
|
1371
1427
|
|
|
1372
|
-
|
|
1428
|
+
node = user_def_for(entry.name, method_name)
|
|
1429
|
+
[node, entry.name] if node
|
|
1373
1430
|
end
|
|
1374
|
-
[nil, nil]
|
|
1375
1431
|
end
|
|
1376
1432
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
# the walk (a prepended module reached again from somewhere else) from being expanded twice.
|
|
1384
|
-
def search_prepend_wedge(class_name, method_name, name_memo, seen)
|
|
1385
|
-
prepend_wedge_names(class_name, name_memo).each do |name|
|
|
1386
|
-
next if seen[name]
|
|
1387
|
-
|
|
1388
|
-
seen[name] = true
|
|
1389
|
-
found = user_def_for(name, method_name)
|
|
1390
|
-
return [found, name] if found
|
|
1433
|
+
def master_user_def(class_name, method_name)
|
|
1434
|
+
names = ResolutionChain::MasterOrder.definer_sequence(self, class_name)
|
|
1435
|
+
names.each { |name| record_class_dependency(name) } if Analysis::DependencyRecorder.active?
|
|
1436
|
+
names.each do |name|
|
|
1437
|
+
node = user_def_for(name, method_name)
|
|
1438
|
+
return [node, name] if node
|
|
1391
1439
|
end
|
|
1392
1440
|
nil
|
|
1393
1441
|
end
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
#
|
|
1397
|
-
#
|
|
1398
|
-
#
|
|
1399
|
-
#
|
|
1400
|
-
#
|
|
1401
|
-
# `
|
|
1402
|
-
#
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
seen[resolved] = true
|
|
1411
|
-
out.concat(subchain_names(resolved, name_memo, seen))
|
|
1412
|
-
end
|
|
1413
|
-
out
|
|
1414
|
-
end
|
|
1415
|
-
|
|
1416
|
-
# Issue #1123 — appends `node`'s own instance search order to `out`: its prepend wedge, the node
|
|
1417
|
-
# itself, then its includes and superclass, each expanded the same way. Since #1173 the include
|
|
1418
|
-
# half is `includes_of`'s search order — the ORDER inside a prepended module's own include list is
|
|
1419
|
-
# not re-derived here, only read.
|
|
1420
|
-
def subchain_names(node, name_memo, seen)
|
|
1421
|
-
out = prepend_wedge_names(node, name_memo, seen)
|
|
1422
|
-
out << node
|
|
1423
|
-
(includes_of(node) + [superclass_of(node)].compact).each do |raw|
|
|
1424
|
-
resolved = resolve_ancestor_class_name(node, raw, name_memo)
|
|
1425
|
-
next if resolved.nil? || seen[resolved]
|
|
1426
|
-
|
|
1427
|
-
seen[resolved] = true
|
|
1428
|
-
out.concat(subchain_names(resolved, name_memo, seen))
|
|
1442
|
+
private :first_user_def, :master_user_def
|
|
1443
|
+
|
|
1444
|
+
# Issue #731 — the singleton-side twin of {#user_def_through_ancestors}: the first `def self.` /
|
|
1445
|
+
# `class << self` body of `method_name` along `class_name`'s singleton chain, as `[def_node, owner]`, or
|
|
1446
|
+
# `[nil, nil]` — each class object's own table, then the modules it extends in Ruby's order, then the
|
|
1447
|
+
# superclass's. `ScopeIndexer` folds an extended module's own `def`s into the extender's table, so those
|
|
1448
|
+
# answer at the extender; a module the extended one includes is its own entry (#1567's singleton shape:
|
|
1449
|
+
# `class C < Base; extend A` with `A` including `M` is `M#foo`, not `Base.foo`). Where the chain does
|
|
1450
|
+
# not stand, the answer is the class objects' alone, as the superclass-only walk this replaced gave.
|
|
1451
|
+
def singleton_def_through_ancestors(class_name, method_name, name_memo: UNUSED_NAME_MEMO) # rubocop:disable Lint/UnusedMethodArgument
|
|
1452
|
+
chain = ResolutionChain.for(self, class_name.to_s, :singleton, :methods)
|
|
1453
|
+
found = first_singleton_def(chain, method_name)
|
|
1454
|
+
owner = found&.last
|
|
1455
|
+
if chain.settle(self, owner, owner: owner) { |retro| first_singleton_def(retro, method_name)&.last } == :master
|
|
1456
|
+
found = first_singleton_def(chain, method_name, :singleton)
|
|
1429
1457
|
end
|
|
1430
|
-
|
|
1431
|
-
end
|
|
1432
|
-
private :search_prepend_wedge, :prepend_wedge_names, :subchain_names
|
|
1458
|
+
return found if found
|
|
1433
1459
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
# `[def_node, owner]`, or `[nil, nil]`.
|
|
1437
|
-
#
|
|
1438
|
-
# Superclasses only, and that is the whole shape of class-method inheritance in Ruby: a subclass
|
|
1439
|
-
# inherits its parent's class methods, while an `include`d module contributes INSTANCE methods and its
|
|
1440
|
-
# own `def self.x` is not callable on the includer. `extend M` needs no walk either — `ScopeIndexer`
|
|
1441
|
-
# folds an extend into the extender's own singleton entries before the table is frozen — so the one
|
|
1442
|
-
# edge this misses is a module's singleton surface reached through some other route, which nothing
|
|
1443
|
-
# resolves today.
|
|
1444
|
-
def singleton_def_through_ancestors(class_name, method_name, name_memo: {})
|
|
1445
|
-
queue = [class_name.to_s]
|
|
1446
|
-
seen = {}
|
|
1447
|
-
visited = 0
|
|
1448
|
-
until queue.empty?
|
|
1449
|
-
current = queue.shift
|
|
1450
|
-
next if current.nil? || seen[current]
|
|
1451
|
-
|
|
1452
|
-
seen[current] = true
|
|
1453
|
-
visited += 1
|
|
1454
|
-
return ancestor_walk_gave_up if visited > ANCESTOR_WALK_LIMIT
|
|
1460
|
+
chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
|
|
1461
|
+
end
|
|
1455
1462
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1463
|
+
def first_singleton_def(chain, method_name, side = nil)
|
|
1464
|
+
chain.search(self, side: side) do |entry|
|
|
1465
|
+
next if entry.external?
|
|
1458
1466
|
|
|
1459
|
-
|
|
1467
|
+
name = entry.name
|
|
1468
|
+
node = entry.side == :singleton ? singleton_def_for(name, method_name) : user_def_for(name, method_name)
|
|
1469
|
+
[node, entry.name] if node
|
|
1460
1470
|
end
|
|
1461
|
-
[nil, nil]
|
|
1462
1471
|
end
|
|
1472
|
+
private :first_singleton_def
|
|
1463
1473
|
|
|
1464
1474
|
# Issue #633 — the ancestors a project class reaches that the project itself does NOT declare: the
|
|
1465
|
-
# `< StandardError` / `< Array` superclasses and the `include Comparable` mixins the
|
|
1466
|
-
#
|
|
1467
|
-
#
|
|
1468
|
-
#
|
|
1469
|
-
#
|
|
1475
|
+
# `< StandardError` / `< Array` superclasses and the `include Comparable` mixins, as the CANDIDATE LIST for
|
|
1476
|
+
# each as-written name ({#ancestor_name_candidates}: the nesting spellings first, the bare name last), in
|
|
1477
|
+
# Ruby's order along the instance chain — the order the caller that adopts the FIRST answering one (#1173)
|
|
1478
|
+
# depends on. Resolving a candidate means asking the RBS environment, which this frozen-index read does
|
|
1479
|
+
# not do; the caller takes the first candidate its own oracle knows. Where the chain does not stand, the
|
|
1480
|
+
# groups are the depth-first ones the walk this replaced emitted.
|
|
1470
1481
|
#
|
|
1471
|
-
#
|
|
1472
|
-
#
|
|
1473
|
-
#
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1482
|
+
# Issue #527 slice 1 — `mixins: false` answers the superclass edge only: a consumer resolving one KIND of
|
|
1483
|
+
# inheritance edge at a time (the dispatch arm lands `< Hash` before `include Enumerable`) narrows it, and
|
|
1484
|
+
# records only the superclass chain it read.
|
|
1485
|
+
def external_ancestor_name_candidates(class_name, name_memo: UNUSED_NAME_MEMO, mixins: true) # rubocop:disable Lint/UnusedMethodArgument
|
|
1486
|
+
chain = ResolutionChain.for(self, class_name.to_s, :instance, :methods)
|
|
1487
|
+
groups = external_groups(chain, mixins)
|
|
1488
|
+
if chain.settle(self, groups) { |retro| external_groups(retro, mixins) } == :master
|
|
1489
|
+
groups = ResolutionChain::MasterOrder.external_groups(self, class_name.to_s, mixins)
|
|
1490
|
+
end
|
|
1491
|
+
if Analysis::DependencyRecorder.active?
|
|
1492
|
+
mixins ? chain.record(self) : chain.level_classes.each { |name| record_class_dependency(name) if name }
|
|
1493
|
+
end
|
|
1494
|
+
# Issue #527 — the cut is a budget event, not an answer: consumers read the groups as "the ancestors
|
|
1495
|
+
# this class reaches", so a short list must be visible in `--stats` beside the other walks' exhaustions.
|
|
1496
|
+
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT) if chain.truncated?
|
|
1483
1497
|
groups
|
|
1484
1498
|
end
|
|
1485
1499
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
#
|
|
1489
|
-
# Issue #1173 — the walk is depth-first pre-order, not breadth-first: a resolved ancestor's own
|
|
1490
|
-
# edges are followed BEFORE the next sibling edge, because Ruby inserts an included module's whole
|
|
1491
|
-
# sub-chain ahead of the includer's superclass (`class C < Hash; include M` where `M` itself
|
|
1492
|
-
# includes `Widen` chains `C → M → Widen → Hash`; BFS would report `Hash` ahead of `Widen`). Each
|
|
1493
|
-
# node's edges still go includes-then-superclass, and `includes_of` is already search order, so the
|
|
1494
|
-
# emitted groups arrive in MRO order — which the #1173 include arm depends on when two ancestors
|
|
1495
|
-
# both declare the name.
|
|
1496
|
-
def collect_external_ancestors(current, groups, name_memo, mixins, seen, visited)
|
|
1497
|
-
return if current.nil? || seen[current]
|
|
1498
|
-
|
|
1499
|
-
seen[current] = true
|
|
1500
|
-
visited[0] += 1
|
|
1501
|
-
if visited[0] > ANCESTOR_WALK_LIMIT
|
|
1502
|
-
# Issue #527 — the give-up is a budget event, not an answer. Consumers read the groups as
|
|
1503
|
-
# "the ancestors this class reaches", so a truncated list must be visible in `--stats`
|
|
1504
|
-
# alongside the other walks' exhaustions rather than silently short. Hit once: every node
|
|
1505
|
-
# visited past the limit returns here too, and only the first should count.
|
|
1506
|
-
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT) if visited[0] == ANCESTOR_WALK_LIMIT + 1
|
|
1507
|
-
return
|
|
1508
|
-
end
|
|
1509
|
-
|
|
1510
|
-
raw_names = mixins ? includes_of(current).dup : []
|
|
1511
|
-
raw_super = superclass_of(current)
|
|
1512
|
-
raw_names << raw_super if raw_super
|
|
1513
|
-
raw_names.each do |raw|
|
|
1514
|
-
resolved = resolve_ancestor_class_name(current, raw, name_memo)
|
|
1515
|
-
if resolved
|
|
1516
|
-
collect_external_ancestors(resolved, groups, name_memo, mixins, seen, visited)
|
|
1517
|
-
else
|
|
1518
|
-
groups << ancestor_name_candidates(current, raw)
|
|
1519
|
-
end
|
|
1520
|
-
end
|
|
1500
|
+
def external_groups(chain, mixins)
|
|
1501
|
+
chain.entries.filter_map { |entry| entry.candidates if entry.external? && (mixins || entry.superclass_edge) }
|
|
1521
1502
|
end
|
|
1522
|
-
private :
|
|
1503
|
+
private :external_groups
|
|
1523
1504
|
|
|
1524
|
-
# The
|
|
1525
|
-
|
|
1505
|
+
# The budget {ResolutionChain::LIMIT} enforces; a hierarchy past it gives up rather than reading an answer
|
|
1506
|
+
# off a cut chain (ADR-41 WD4).
|
|
1507
|
+
ANCESTOR_WALK_LIMIT = ResolutionChain::LIMIT
|
|
1526
1508
|
|
|
1527
1509
|
EMPTY_HEADER_NESTING = [].freeze
|
|
1528
1510
|
private_constant :EMPTY_HEADER_NESTING
|
|
1529
1511
|
|
|
1530
|
-
# The answer for a class that prepends nothing — the common case, so the wedge does not allocate a
|
|
1531
|
-
# fresh empty list per visited node. Shared and frozen, and never handed to a caller that could
|
|
1532
|
-
# mutate it ({#prepends_of} is private to this walk).
|
|
1533
|
-
EMPTY_MIXIN_NAMES = [].freeze
|
|
1534
|
-
private_constant :EMPTY_MIXIN_NAMES
|
|
1535
|
-
|
|
1536
1512
|
def ancestor_walk_gave_up
|
|
1537
1513
|
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
1538
1514
|
[nil, nil]
|
|
1539
1515
|
end
|
|
1516
|
+
private :ancestor_walk_gave_up
|
|
1540
1517
|
|
|
1541
|
-
# Issue #723 — the
|
|
1542
|
-
#
|
|
1543
|
-
#
|
|
1544
|
-
# `
|
|
1545
|
-
#
|
|
1546
|
-
# needs the broader one.
|
|
1518
|
+
# Issue #723 — the question {#user_def_through_ancestors} asks, asked of the DISCOVERY table rather than
|
|
1519
|
+
# the def-node table: does the project define `method_name` on `class_name` or on any ancestor the project
|
|
1520
|
+
# itself declares? The two tables are not interchangeable — `discovered_methods` also carries
|
|
1521
|
+
# `define_method`, `attr_*` and the whole singleton side, none of which contributes a `Prism::DefNode` —
|
|
1522
|
+
# and the suppression probe shared by the `call.*` check rules needs the broader one.
|
|
1547
1523
|
#
|
|
1548
1524
|
# Why it must walk at all: `Analysis::CheckRules#source_declared_method?` asked `discovered_method?`,
|
|
1549
1525
|
# which is keyed on the receiver's OWN name, while the typer resolves the same call through this
|
|
@@ -1551,72 +1527,47 @@ module Rigor
|
|
|
1551
1527
|
# `call.undefined-method` on a method `dump_type` resolved on the same line of the same run — writing
|
|
1552
1528
|
# MORE RBS made the run worse, the incentive #653 removed for plugin-typed calls.
|
|
1553
1529
|
#
|
|
1554
|
-
# `kind: :singleton`
|
|
1555
|
-
#
|
|
1556
|
-
#
|
|
1557
|
-
|
|
1558
|
-
def discovered_method_through_ancestors?(class_name, method_name, kind, name_memo: {})
|
|
1530
|
+
# `kind: :singleton` reads the singleton chain: the class objects' own methods (an extended module's are
|
|
1531
|
+
# folded into its extender's) and the instance methods of the modules they extend and those modules'
|
|
1532
|
+
# includes. Existence is a union, so the chain's two worlds, which hold the same modules, agree on it.
|
|
1533
|
+
def discovered_method_through_ancestors?(class_name, method_name, kind, name_memo: UNUSED_NAME_MEMO) # rubocop:disable Lint/UnusedMethodArgument
|
|
1559
1534
|
return false if class_name.nil?
|
|
1560
1535
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
until queue.empty?
|
|
1565
|
-
current = queue.shift
|
|
1566
|
-
next if current.nil? || seen[current]
|
|
1536
|
+
chain = ResolutionChain.for(self, class_name.to_s, kind == :singleton ? :singleton : :instance, :methods)
|
|
1537
|
+
found = chain.search(self) do |entry|
|
|
1538
|
+
next false if entry.external?
|
|
1567
1539
|
|
|
1568
|
-
|
|
1569
|
-
visited += 1
|
|
1570
|
-
# Budget exhaustion is uncertainty, not absence: answering "not declared" here would hand a
|
|
1571
|
-
# `call.undefined-method` a fired verdict it has no evidence for. Suppress and record the hit.
|
|
1572
|
-
if visited > ANCESTOR_WALK_LIMIT
|
|
1573
|
-
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
1574
|
-
return true
|
|
1575
|
-
end
|
|
1576
|
-
|
|
1577
|
-
return true if discovered_method?(current, method_name, kind)
|
|
1578
|
-
|
|
1579
|
-
enqueue_ancestors(current, queue, name_memo, mixins: kind != :singleton)
|
|
1580
|
-
end
|
|
1581
|
-
false
|
|
1582
|
-
end
|
|
1583
|
-
|
|
1584
|
-
# Pushes `current`'s direct ancestors onto the BFS queue: included / prepended modules first (Ruby places
|
|
1585
|
-
# mixins nearer than the superclass), then the superclass. Each as-written name is resolved against the
|
|
1586
|
-
# nesting `current`'s declaration header is written in ({#ancestor_name_candidates}); names that resolve
|
|
1587
|
-
# to no project class/module are dropped. `mixins: false` walks the superclass chain alone, for the
|
|
1588
|
-
# singleton-side question where an `include` contributes nothing.
|
|
1589
|
-
def enqueue_ancestors(current, queue, name_memo, mixins: true)
|
|
1590
|
-
if mixins
|
|
1591
|
-
includes_of(current).each do |raw|
|
|
1592
|
-
resolved = resolve_ancestor_class_name(current, raw, name_memo)
|
|
1593
|
-
queue.push(resolved) if resolved
|
|
1594
|
-
end
|
|
1540
|
+
discovered_method?(entry.name, method_name, entry.side == :singleton ? :singleton : kind_for(kind))
|
|
1595
1541
|
end
|
|
1596
|
-
|
|
1597
|
-
|
|
1542
|
+
return true if found
|
|
1543
|
+
# Budget exhaustion is uncertainty, not absence: answering "not declared" here would hand a
|
|
1544
|
+
# `call.undefined-method` a fired verdict it has no evidence for. Suppress and record the hit.
|
|
1545
|
+
return false unless chain.truncated?
|
|
1598
1546
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1547
|
+
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
1548
|
+
true
|
|
1601
1549
|
end
|
|
1602
1550
|
|
|
1603
|
-
#
|
|
1604
|
-
#
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
by_subclass = (name_memo[subclass_qualified] ||= {})
|
|
1608
|
-
return by_subclass[raw_ancestor] if by_subclass.key?(raw_ancestor)
|
|
1551
|
+
# A module entry on the singleton chain answers with its INSTANCE methods; on the instance chain, the
|
|
1552
|
+
# caller's kind stands.
|
|
1553
|
+
def kind_for(kind) = kind == :singleton ? :instance : kind
|
|
1554
|
+
private :kind_for
|
|
1609
1555
|
|
|
1610
|
-
|
|
1556
|
+
# Pushes `current`'s direct ancestors onto a breadth-first queue: included / prepended modules first, then
|
|
1557
|
+
# the superclass, each resolved against the nesting `current`'s declaration header is written in; names
|
|
1558
|
+
# that resolve to no project class/module are dropped. `mixins: false` gives the superclass alone. No
|
|
1559
|
+
# reader in the engine walks this way any more (they read {ResolutionChain}); it stays for the plugin
|
|
1560
|
+
# surface, with the ADR-46 class edge the walk it served always filed.
|
|
1561
|
+
def enqueue_ancestors(current, queue, name_memo, mixins: true) # rubocop:disable Lint/UnusedMethodArgument
|
|
1562
|
+
record_class_dependency(current) if Analysis::DependencyRecorder.active?
|
|
1563
|
+
queue.concat(ResolutionChain.direct_ancestors(self, current.to_s, mixins))
|
|
1611
1564
|
end
|
|
1612
|
-
private :ancestor_walk_gave_up, :resolve_ancestor_class_name
|
|
1613
1565
|
|
|
1614
1566
|
# Issue #682 — the candidate names an ancestor spelled `raw_ancestor` can denote in `subclass_qualified`,
|
|
1615
1567
|
# in Ruby's lookup order, most-qualified first and the bare name last. The single owner of the question:
|
|
1616
|
-
# three walks resolved a superclass / include name apiece (
|
|
1617
|
-
# `
|
|
1618
|
-
#
|
|
1619
|
-
# subclass's qualified name one `::` segment at a time.
|
|
1568
|
+
# three walks resolved a superclass / include name apiece (the method walk, the constant ladder's
|
|
1569
|
+
# ancestor rung, and `Analysis::CheckRules`' override-visibility rule — all three now {ResolutionChain}),
|
|
1570
|
+
# and each derived the order by PEELING the subclass's qualified name one `::` segment at a time.
|
|
1620
1571
|
#
|
|
1621
1572
|
# That peel is the NESTED spelling's answer, given to both spellings. Ruby evaluates a superclass
|
|
1622
1573
|
# expression before entering the body, so the cref that governs it is the one the declaration's HEADER
|
|
@@ -1628,9 +1579,9 @@ module Rigor
|
|
|
1628
1579
|
# The header nesting is a property of the DECLARATION, not of the reader, so it is read from the
|
|
1629
1580
|
# discovery table `Inference::ScopeIndexer` records it in rather than from this scope's own
|
|
1630
1581
|
# `#lexical_nesting`. Answering off the reader's chain would be wrong past the first hop of the ancestor
|
|
1631
|
-
#
|
|
1632
|
-
# corrupt the
|
|
1633
|
-
# `Reflection.ancestor_constant_scopes
|
|
1582
|
+
# chain (the chain describes the reader's class, not the ancestor being resolved) and would silently
|
|
1583
|
+
# corrupt the {ResolutionChain} memo and the ones layered on it (`Inference::ExpressionTyper#class_graph_buckets`,
|
|
1584
|
+
# `Reflection.ancestor_constant_scopes`), all of which key on the class name alone because this walk is
|
|
1634
1585
|
# a pure function of the frozen discovery tables.
|
|
1635
1586
|
#
|
|
1636
1587
|
# The peel survives as the FALLBACK, for a class no declaration walk recorded — a scope seeded without
|
|
@@ -1737,10 +1688,6 @@ module Rigor
|
|
|
1737
1688
|
end
|
|
1738
1689
|
private :peeled_header_nesting
|
|
1739
1690
|
|
|
1740
|
-
def compute_ancestor_class_name(subclass_qualified, raw_ancestor)
|
|
1741
|
-
ancestor_name_candidates(subclass_qualified, raw_ancestor).find { |c| known_user_class?(c) }
|
|
1742
|
-
end
|
|
1743
|
-
|
|
1744
1691
|
# Issue #723 — `discovered_methods` is in the list because the other three miss a class whose only
|
|
1745
1692
|
# project-side content is CLASS methods: `class Base; def self.build = :built; end` records no instance
|
|
1746
1693
|
# def node, no superclass and no include, so the ancestor-name resolver did not recognise `Base` as a
|
|
@@ -1755,13 +1702,13 @@ module Rigor
|
|
|
1755
1702
|
# narrower predicate (`discovered_classes` alone) would read a project class as having left the project
|
|
1756
1703
|
# and attribute its root name to a same-named locked gem — a WRONG provenance label, which is the one
|
|
1757
1704
|
# direction ADR-82 forbids.
|
|
1758
|
-
private :ancestor_walk_gave_up, :compute_ancestor_class_name
|
|
1759
1705
|
|
|
1760
1706
|
# Records, for a resolved cross-class ancestry read, every file that declares `class_name` (its declaration /
|
|
1761
|
-
# reopening / superclass / include sites). The `discovered_class_sources` table it reads is populated
|
|
1762
|
-
#
|
|
1763
|
-
#
|
|
1764
|
-
# never appear in the source map). Gated by the caller on the recorder being
|
|
1707
|
+
# reopening / superclass / include sites). The `discovered_class_sources` table it reads is populated by the
|
|
1708
|
+
# cross-file project pre-pass ({Inference::ScopeIndexer.discovered_def_index_for_paths}); a scope built
|
|
1709
|
+
# without one holds none, and the read then files nothing. No-op when the class is not a project class
|
|
1710
|
+
# (core / stdlib / gem names never appear in the source map). Gated by the caller on the recorder being
|
|
1711
|
+
# active.
|
|
1765
1712
|
def record_class_dependency(class_name)
|
|
1766
1713
|
sites = @discovery.discovered_class_sources[class_name.to_s]
|
|
1767
1714
|
return if sites.nil?
|
|
@@ -1781,8 +1728,7 @@ module Rigor
|
|
|
1781
1728
|
# so a bare referent is re-checked when the class appears or disappears and at no other time. A file edge
|
|
1782
1729
|
# would have re-checked every bare referent of every class the file declares on any edit to it.
|
|
1783
1730
|
def record_class_existence(class_name)
|
|
1784
|
-
|
|
1785
|
-
Analysis::DependencyRecorder.read_name(:class, segment) if segment
|
|
1731
|
+
Analysis::DependencyRecorder.read_last_segment(:class, class_name.to_s)
|
|
1786
1732
|
end
|
|
1787
1733
|
|
|
1788
1734
|
# Issue #644 — the positive ADR-46 edge for a cross-file VALUE constant. `Reflection.constant_type_at`
|
|
@@ -1938,17 +1884,17 @@ module Rigor
|
|
|
1938
1884
|
|
|
1939
1885
|
# Issue #1429 — a join keeps a guard record while the joined scope still narrows its name: a global both arms
|
|
1940
1886
|
# bind, a constant both arms narrow (one an arm does not narrow reads its resolved type after the join, so its
|
|
1941
|
-
# narrowing is gone). An arm without a record contributes nothing
|
|
1942
|
-
# joined binding {#forget_guard_narrowings} takes, so it still covers
|
|
1943
|
-
|
|
1887
|
+
# narrowing is gone), and an instance variable both arms bind (#1446). An arm without a record contributes nothing
|
|
1888
|
+
# to the restore target, whose union with the joined binding {#forget_guard_narrowings} takes, so it still covers
|
|
1889
|
+
# that arm's binding.
|
|
1890
|
+
# `joined` maps each record kind to the joined scope's table of that kind.
|
|
1891
|
+
def join_guard_records(other, joined)
|
|
1944
1892
|
mine = @guard_records
|
|
1945
1893
|
theirs = other.guard_records
|
|
1946
1894
|
return EMPTY_GUARD_RECORDS if mine.empty? && theirs.empty?
|
|
1947
1895
|
|
|
1948
1896
|
merged = mine.merge(theirs) { |_key, left, right| Type::Combinator.union(left, right) }
|
|
1949
|
-
kept = merged.select
|
|
1950
|
-
kind == :global ? joined_globals.key?(name) : joined_constants.key?(name)
|
|
1951
|
-
end
|
|
1897
|
+
kept = merged.select { |(kind, name), _| joined.fetch(kind).key?(name) }
|
|
1952
1898
|
kept.empty? ? EMPTY_GUARD_RECORDS : kept.freeze
|
|
1953
1899
|
end
|
|
1954
1900
|
private :join_guard_records
|
|
@@ -1993,7 +1939,8 @@ module Rigor
|
|
|
1993
1939
|
end
|
|
1994
1940
|
|
|
1995
1941
|
def same_guard_state?(other)
|
|
1996
|
-
@constant_narrowings == other.constant_narrowings && @guard_records == other.guard_records
|
|
1942
|
+
@constant_narrowings == other.constant_narrowings && @guard_records == other.guard_records &&
|
|
1943
|
+
@bot_guard_classes == other.bot_guard_classes
|
|
1997
1944
|
end
|
|
1998
1945
|
|
|
1999
1946
|
def rebuild(
|
|
@@ -2020,7 +1967,8 @@ module Rigor
|
|
|
2020
1967
|
repeated_or_writes: @repeated_or_writes,
|
|
2021
1968
|
match_frame: @match_frame,
|
|
2022
1969
|
constant_narrowings: @constant_narrowings,
|
|
2023
|
-
guard_records: @guard_records
|
|
1970
|
+
guard_records: @guard_records,
|
|
1971
|
+
bot_guard_classes: @bot_guard_classes
|
|
2024
1972
|
)
|
|
2025
1973
|
self.class.new(
|
|
2026
1974
|
environment: environment, locals: locals,
|
|
@@ -2047,7 +1995,8 @@ module Rigor
|
|
|
2047
1995
|
repeated_or_writes: repeated_or_writes,
|
|
2048
1996
|
match_frame: match_frame,
|
|
2049
1997
|
constant_narrowings: constant_narrowings,
|
|
2050
|
-
guard_records: guard_records
|
|
1998
|
+
guard_records: guard_records,
|
|
1999
|
+
bot_guard_classes: bot_guard_classes
|
|
2051
2000
|
)
|
|
2052
2001
|
end
|
|
2053
2002
|
|
|
@@ -2137,15 +2086,32 @@ module Rigor
|
|
|
2137
2086
|
# of a merge inside one body carry the same one; `||` keeps it should either arm lack it, since dropping
|
|
2138
2087
|
# it only loses the frame's resets.
|
|
2139
2088
|
match_frame: @match_frame || other.match_frame,
|
|
2140
|
-
# Issue #1429 — the guard narrowings of globals and constants and their pre-guard records
|
|
2141
|
-
|
|
2089
|
+
# Issue #1429 — the guard narrowings of globals and constants and their pre-guard records, and of instance
|
|
2090
|
+
# variables (#1446).
|
|
2091
|
+
**join_guard_narrowings(other, joined_globals, joined_ivars)
|
|
2142
2092
|
)
|
|
2143
2093
|
end
|
|
2144
2094
|
|
|
2145
|
-
def join_guard_narrowings(other, joined_globals)
|
|
2095
|
+
def join_guard_narrowings(other, joined_globals, joined_ivars)
|
|
2146
2096
|
joined_constants = join_constant_narrowings(other)
|
|
2147
|
-
{
|
|
2148
|
-
|
|
2097
|
+
joined = { global: joined_globals, ivar: joined_ivars, constant: joined_constants }
|
|
2098
|
+
{ constant_narrowings: joined_constants, guard_records: join_guard_records(other, joined),
|
|
2099
|
+
bot_guard_classes: join_bot_guard_classes(other) }
|
|
2100
|
+
end
|
|
2101
|
+
|
|
2102
|
+
# Issue #1446 — an entry both arms hold, with the classes of either: the joined receiver reads `bot` only where
|
|
2103
|
+
# both arms do. One only an arm holds is dropped, so a call on the joined `bot` reads as on an unknown one.
|
|
2104
|
+
def join_bot_guard_classes(other)
|
|
2105
|
+
mine = @bot_guard_classes
|
|
2106
|
+
theirs = other.bot_guard_classes
|
|
2107
|
+
return mine if mine.equal?(theirs)
|
|
2108
|
+
return EMPTY_BOT_GUARD_CLASSES if mine.empty? || theirs.empty?
|
|
2109
|
+
|
|
2110
|
+
joined = mine.each_with_object({}) do |(key, names), acc|
|
|
2111
|
+
other_names = theirs[key]
|
|
2112
|
+
acc[key] = (names | other_names).freeze if other_names
|
|
2113
|
+
end
|
|
2114
|
+
joined.empty? ? EMPTY_BOT_GUARD_CLASSES : joined.freeze
|
|
2149
2115
|
end
|
|
2150
2116
|
|
|
2151
2117
|
# Issue #1429 — a constant reference both arms narrow reads the union; one only an arm narrows reads its resolved
|
|
@@ -2359,6 +2325,13 @@ module Rigor
|
|
|
2359
2325
|
@guard_records.merge(key => pre_guard).freeze
|
|
2360
2326
|
end
|
|
2361
2327
|
|
|
2328
|
+
def drop_bot_guard_class(kind, name)
|
|
2329
|
+
return @bot_guard_classes if @bot_guard_classes.empty?
|
|
2330
|
+
|
|
2331
|
+
key = [kind, kind == :constant ? name : name.to_sym]
|
|
2332
|
+
@bot_guard_classes.key?(key) ? @bot_guard_classes.except(key).freeze : @bot_guard_classes
|
|
2333
|
+
end
|
|
2334
|
+
|
|
2362
2335
|
def drop_guard_record(kind, name)
|
|
2363
2336
|
return @guard_records if @guard_records.empty?
|
|
2364
2337
|
|