rigortype 0.1.17 → 0.1.18
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 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules.rb +34 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +160 -1190
- data/lib/rigor/analysis/worker_session.rb +47 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/check_command.rb +705 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli.rb +15 -614
- data/lib/rigor/configuration.rb +9 -6
- data/lib/rigor/environment/rbs_loader.rb +53 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/expression_typer.rb +28 -62
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher.rb +115 -54
- data/lib/rigor/inference/narrowing.rb +60 -0
- data/lib/rigor/inference/scope_indexer.rb +75 -15
- data/lib/rigor/inference/statement_evaluator.rb +35 -52
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +282 -41
- data/lib/rigor/plugin/node_rule_walk.rb +147 -0
- data/lib/rigor/plugin/registry.rb +263 -35
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
- data/lib/rigor/scope/discovery_index.rb +58 -0
- data/lib/rigor/scope.rb +67 -198
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/type/combinator.rb +5 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +1 -2
- data/sig/rigor/scope.rbs +41 -29
- data/sig/rigor/source.rbs +1 -0
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- metadata +15 -2
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
|
@@ -6,6 +6,7 @@ require_relative "../flow_contribution"
|
|
|
6
6
|
require_relative "../flow_contribution/merger"
|
|
7
7
|
require_relative "../builtins/hkt_builtins"
|
|
8
8
|
require_relative "../builtins/static_return_refinements"
|
|
9
|
+
require_relative "flow_tracer"
|
|
9
10
|
require_relative "method_dispatcher/call_context"
|
|
10
11
|
require_relative "method_dispatcher/constant_folding"
|
|
11
12
|
require_relative "method_dispatcher/literal_string_folding"
|
|
@@ -73,9 +74,28 @@ module Rigor
|
|
|
73
74
|
# @param environment [Rigor::Environment, nil] required for
|
|
74
75
|
# RBS-backed dispatch; when nil only constant folding can fire.
|
|
75
76
|
# @return [Rigor::Type, nil] inferred result type, or `nil` for "no rule".
|
|
76
|
-
def dispatch(receiver_type:, method_name:, arg_types:,
|
|
77
|
+
def dispatch(receiver_type:, method_name:, arg_types:,
|
|
77
78
|
block_type: nil, environment: nil,
|
|
78
79
|
call_node: nil, scope: nil)
|
|
80
|
+
result = resolve(
|
|
81
|
+
receiver_type: receiver_type, method_name: method_name, arg_types: arg_types,
|
|
82
|
+
block_type: block_type, environment: environment,
|
|
83
|
+
call_node: call_node, scope: scope
|
|
84
|
+
)
|
|
85
|
+
# `rigor trace` — record the dispatch outcome (resolved type, or
|
|
86
|
+
# the fail-soft `nil` the caller widens to `Dynamic[Top]`).
|
|
87
|
+
if FlowTracer.active?
|
|
88
|
+
FlowTracer.dispatch(
|
|
89
|
+
receiver: receiver_type, method_name: method_name, args: arg_types,
|
|
90
|
+
result: result, location: call_node&.location
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
result
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def resolve(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
97
|
+
block_type: nil, environment: nil,
|
|
98
|
+
call_node: nil, scope: nil)
|
|
79
99
|
return nil if receiver_type.nil?
|
|
80
100
|
|
|
81
101
|
# Build the call context once and thread it — unchanged —
|
|
@@ -217,7 +237,7 @@ module Rigor
|
|
|
217
237
|
# introspection (`attr_reader`, `private`, ...) on
|
|
218
238
|
# user classes without requiring the user to author
|
|
219
239
|
# their own RBS.
|
|
220
|
-
try_user_class_fallback(receiver_type,
|
|
240
|
+
try_user_class_fallback(receiver_type, environment, call_node, context)
|
|
221
241
|
end
|
|
222
242
|
|
|
223
243
|
# v0.1.3 — discovered-method dispatch tier. `scope` carries
|
|
@@ -298,12 +318,12 @@ module Rigor
|
|
|
298
318
|
end
|
|
299
319
|
|
|
300
320
|
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
301
|
-
# slice 7
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
#
|
|
321
|
+
# slice 7; ADR-52 WD3 — consults each loaded plugin's gated
|
|
322
|
+
# `dynamic_return` rules, wraps the contributed types as
|
|
323
|
+
# `FlowContribution` bundles, merges them through
|
|
324
|
+
# `FlowContribution::Merger`, and returns the merged
|
|
325
|
+
# `return_type` slot (or nil when no plugin contributed a
|
|
326
|
+
# return type).
|
|
307
327
|
#
|
|
308
328
|
# Plugins whose hook raises have their contribution
|
|
309
329
|
# silently dropped for this call so the dispatch chain
|
|
@@ -643,23 +663,15 @@ module Rigor
|
|
|
643
663
|
Type::Combinator.untyped
|
|
644
664
|
end
|
|
645
665
|
|
|
666
|
+
# ADR-52 WD1 — the per-dispatch plugins × owns_receivers ×
|
|
667
|
+
# `class_ordering` walk moved into the compiled contribution
|
|
668
|
+
# table: the union is built once per registry (almost always
|
|
669
|
+
# empty → O(1) false) and per-class verdicts memoise per run.
|
|
646
670
|
def plugin_owns_receiver?(class_name, environment)
|
|
647
671
|
registry = environment&.plugin_registry
|
|
648
672
|
return false if registry.nil? || registry.empty?
|
|
649
673
|
|
|
650
|
-
registry.
|
|
651
|
-
owns = plugin.manifest.owns_receivers # rigor:disable undefined-method
|
|
652
|
-
owns.any? { |owner| receiver_matches_owner?(class_name, owner, environment) }
|
|
653
|
-
end
|
|
654
|
-
end
|
|
655
|
-
|
|
656
|
-
def receiver_matches_owner?(class_name, owner, environment)
|
|
657
|
-
return true if class_name == owner
|
|
658
|
-
|
|
659
|
-
ordering = environment.class_ordering(class_name, owner)
|
|
660
|
-
%i[equal subclass].include?(ordering)
|
|
661
|
-
rescue StandardError
|
|
662
|
-
false
|
|
674
|
+
registry.contribution_index.owns_receiver?(class_name, environment)
|
|
663
675
|
end
|
|
664
676
|
|
|
665
677
|
def dep_source_class_name(receiver_type)
|
|
@@ -668,11 +680,11 @@ module Rigor
|
|
|
668
680
|
end
|
|
669
681
|
end
|
|
670
682
|
|
|
671
|
-
# ADR-37 slice 2 — gathers each plugin's return-type
|
|
672
|
-
# from
|
|
673
|
-
#
|
|
674
|
-
# `flow_contribution_for` escape valve
|
|
675
|
-
#
|
|
683
|
+
# ADR-37 slice 2 / ADR-52 WD3 — gathers each plugin's return-type
|
|
684
|
+
# contribution from the gated `dynamic_return` DSL, wrapped as a
|
|
685
|
+
# return-only `FlowContribution` for the shared merger. (The legacy
|
|
686
|
+
# ungated `flow_contribution_for` escape valve was deleted once its
|
|
687
|
+
# five users migrated.)
|
|
676
688
|
EMPTY_CONTRIBUTIONS = [].freeze
|
|
677
689
|
private_constant :EMPTY_CONTRIBUTIONS
|
|
678
690
|
|
|
@@ -682,31 +694,48 @@ module Rigor
|
|
|
682
694
|
#
|
|
683
695
|
# 1. Only the plugins that *structurally* implement a per-call path
|
|
684
696
|
# are visited — `registry.contribution_index.for_method_dispatch`
|
|
685
|
-
# is the registry-ordered subset
|
|
686
|
-
#
|
|
687
|
-
#
|
|
688
|
-
#
|
|
689
|
-
#
|
|
690
|
-
#
|
|
691
|
-
# per dispatch inside `dynamic_return_type`.
|
|
697
|
+
# is the registry-ordered subset declaring a `dynamic_return`.
|
|
698
|
+
# Iterating the subset in registry order, and gating each path by
|
|
699
|
+
# membership, yields the exact same contributions in the same
|
|
700
|
+
# order as visiting every plugin would (a skipped plugin's call
|
|
701
|
+
# returns nil/[] anyway). The receiver-class ancestry match still
|
|
702
|
+
# happens per dispatch inside `dynamic_return_type`.
|
|
692
703
|
# 2. Contributions accumulate lazily — allocate only when one
|
|
693
704
|
# actually appears, and share a frozen empty array otherwise. The
|
|
694
705
|
# caller treats the result as read-only (`.empty?` / `Merger.merge`).
|
|
706
|
+
# 3. ADR-52 WD1 — method-name gates compiled at registry build. The
|
|
707
|
+
# global gate makes the common "no plugin cares about this call"
|
|
708
|
+
# case a single Set probe; the per-plugin gate skips a plugin
|
|
709
|
+
# whose `dynamic_return` rules are all `methods:`-gated on other
|
|
710
|
+
# names. A pruned consultation could only have returned nil, so
|
|
711
|
+
# contribution order and content are unchanged.
|
|
695
712
|
def collect_plugin_contributions(registry, call_node, scope, receiver_type)
|
|
696
713
|
index = registry.contribution_index
|
|
697
714
|
relevant = index.for_method_dispatch
|
|
698
715
|
return EMPTY_CONTRIBUTIONS if relevant.empty?
|
|
699
716
|
|
|
717
|
+
# `call_node` is not always a CallNode — the `&:symbol` block
|
|
718
|
+
# path dispatches with the `Prism::BlockArgumentNode` itself
|
|
719
|
+
# (`ExpressionTyper#symbol_block_return_type`). A bare `.name`
|
|
720
|
+
# here raised, and the raise was silently absorbed by
|
|
721
|
+
# `block_return_type_for`'s rescue, nil-ing the block type and
|
|
722
|
+
# flipping `select(&:p)`-style calls onto their no-block
|
|
723
|
+
# Enumerator overloads (caught by the GitLab corpus gate).
|
|
724
|
+
name = call_node.respond_to?(:name) ? call_node.name : nil
|
|
725
|
+
return EMPTY_CONTRIBUTIONS unless index.dispatch_candidate?(name)
|
|
726
|
+
|
|
727
|
+
collect_gated_contributions(index, relevant, name, call_node, scope, receiver_type)
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
# The post-gate walk, in registry order — the same order the
|
|
731
|
+
# ungated walk used.
|
|
732
|
+
def collect_gated_contributions(index, relevant, name, call_node, scope, receiver_type)
|
|
700
733
|
result = nil
|
|
701
734
|
relevant.each do |plugin|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if index.dynamic?(plugin)
|
|
707
|
-
dynamic = plugin.dynamic_return_type(call_node: call_node, scope: scope, receiver_type: receiver_type)
|
|
708
|
-
(result ||= []) << FlowContribution.new(return_type: dynamic) if dynamic
|
|
709
|
-
end
|
|
735
|
+
next unless index.dynamic_candidate_for?(plugin, name)
|
|
736
|
+
|
|
737
|
+
dynamic = plugin.dynamic_return_type(call_node: call_node, scope: scope, receiver_type: receiver_type)
|
|
738
|
+
(result ||= []) << FlowContribution.new(return_type: dynamic) if dynamic
|
|
710
739
|
rescue StandardError
|
|
711
740
|
next
|
|
712
741
|
end
|
|
@@ -735,13 +764,37 @@ module Rigor
|
|
|
735
764
|
# filter the common cases first. Adding a precise tier is a
|
|
736
765
|
# one-line append here rather than another link in a hand-written
|
|
737
766
|
# `||` ladder.
|
|
738
|
-
|
|
739
|
-
ConstantFolding, LiteralStringFolding, ShapeDispatch
|
|
740
|
-
|
|
741
|
-
|
|
767
|
+
PRECISE_TIERS_HEAD = Ractor.make_shareable([
|
|
768
|
+
ConstantFolding, LiteralStringFolding, ShapeDispatch
|
|
769
|
+
].freeze)
|
|
770
|
+
private_constant :PRECISE_TIERS_HEAD
|
|
771
|
+
|
|
772
|
+
# ADR-53 re-review follow-up (gate-by-held-key applied to the
|
|
773
|
+
# built-in tiers): the eight stdlib singleton folders are mutually
|
|
774
|
+
# exclusive — each fires only on `Singleton[<its class>]`, the
|
|
775
|
+
# first check in every `try_dispatch` — so at most one can match a
|
|
776
|
+
# given receiver and their relative trial order was never
|
|
777
|
+
# observable. Compiling them into a class-name table turns eight
|
|
778
|
+
# no-op trials per call into one Hash read, skipped entirely when
|
|
779
|
+
# the receiver is not a `Singleton` (the overwhelmingly common
|
|
780
|
+
# case). The table sits where the eight sat in the old flat list:
|
|
781
|
+
# after ShapeDispatch, before KernelDispatch.
|
|
782
|
+
STDLIB_SINGLETON_FOLDERS = Ractor.make_shareable({
|
|
783
|
+
"File" => FileFolding,
|
|
784
|
+
"Shellwords" => ShellwordsFolding,
|
|
785
|
+
"Math" => MathFolding,
|
|
786
|
+
"Time" => TimeFolding,
|
|
787
|
+
"Regexp" => RegexpFolding,
|
|
788
|
+
"CGI" => CGIFolding,
|
|
789
|
+
"URI" => URIFolding,
|
|
790
|
+
"Set" => SetFolding
|
|
791
|
+
}.freeze)
|
|
792
|
+
private_constant :STDLIB_SINGLETON_FOLDERS
|
|
793
|
+
|
|
794
|
+
PRECISE_TIERS_TAIL = Ractor.make_shareable([
|
|
742
795
|
KernelDispatch, MethodFolding, BlockFolding
|
|
743
796
|
].freeze)
|
|
744
|
-
private_constant :
|
|
797
|
+
private_constant :PRECISE_TIERS_TAIL
|
|
745
798
|
|
|
746
799
|
def dispatch_precise_tiers(context)
|
|
747
800
|
# ADR-48 — Data value folding runs ahead of meta-introspection:
|
|
@@ -756,14 +809,25 @@ module Rigor
|
|
|
756
809
|
meta_result = try_meta_introspection(context.receiver, context.method_name, context.args)
|
|
757
810
|
return meta_result if meta_result
|
|
758
811
|
|
|
759
|
-
|
|
812
|
+
PRECISE_TIERS_HEAD.each do |tier|
|
|
813
|
+
result = tier.try_dispatch(context)
|
|
814
|
+
return result if result
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
receiver = context.receiver
|
|
818
|
+
if receiver.is_a?(Type::Singleton) && (folder = STDLIB_SINGLETON_FOLDERS[receiver.class_name])
|
|
819
|
+
result = folder.try_dispatch(context)
|
|
820
|
+
return result if result
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
PRECISE_TIERS_TAIL.each do |tier|
|
|
760
824
|
result = tier.try_dispatch(context)
|
|
761
825
|
return result if result
|
|
762
826
|
end
|
|
763
827
|
nil
|
|
764
828
|
end
|
|
765
829
|
|
|
766
|
-
def try_user_class_fallback(receiver_type,
|
|
830
|
+
def try_user_class_fallback(receiver_type, environment, call_node, context)
|
|
767
831
|
return nil if environment.nil?
|
|
768
832
|
|
|
769
833
|
fallback_receiver = user_class_fallback_receiver(receiver_type, environment)
|
|
@@ -788,14 +852,11 @@ module Rigor
|
|
|
788
852
|
# self / `self.`-receiver calls (`puts`, `raise`, `require`)
|
|
789
853
|
# keep resolving — those are the fallback's intended targets.
|
|
790
854
|
RbsDispatch.try_dispatch(
|
|
791
|
-
|
|
855
|
+
context.with(
|
|
792
856
|
receiver: fallback_receiver,
|
|
793
|
-
method_name: method_name,
|
|
794
|
-
args: arg_types,
|
|
795
|
-
environment: environment,
|
|
796
|
-
block_type: block_type,
|
|
797
857
|
self_type_override: receiver_type,
|
|
798
|
-
public_only: explicit_non_self_receiver?(call_node)
|
|
858
|
+
public_only: explicit_non_self_receiver?(call_node),
|
|
859
|
+
call_node: nil, scope: nil
|
|
799
860
|
)
|
|
800
861
|
)
|
|
801
862
|
end
|
|
@@ -123,6 +123,66 @@ module Rigor
|
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
# Three-valued truthiness certainty of a predicate's type,
|
|
127
|
+
# derived from the truthy / falsey fragments above: `:truthy`
|
|
128
|
+
# when no inhabitant is falsey (the falsey fragment is `Bot`),
|
|
129
|
+
# `:falsey` when no inhabitant is truthy, nil when both
|
|
130
|
+
# fragments are inhabited — or when the type itself is nil /
|
|
131
|
+
# `Bot` (dead code is not a certainty claim). This is the single
|
|
132
|
+
# owner of the judgment both branch-elision consumers read
|
|
133
|
+
# (`ExpressionTyper#elide_or_union` on the value side,
|
|
134
|
+
# `StatementEvaluator#live_branch_for_if` on the scope side), so
|
|
135
|
+
# the type a dead branch is elided from and the scope that stops
|
|
136
|
+
# flowing through it can never disagree.
|
|
137
|
+
def predicate_certainty(type)
|
|
138
|
+
return nil if type.nil? || type.is_a?(Type::Bot)
|
|
139
|
+
|
|
140
|
+
truthy_bot = narrow_truthy(type).is_a?(Type::Bot)
|
|
141
|
+
falsey_bot = narrow_falsey(type).is_a?(Type::Bot)
|
|
142
|
+
|
|
143
|
+
return :falsey if truthy_bot && !falsey_bot
|
|
144
|
+
return :truthy if !truthy_bot && falsey_bot
|
|
145
|
+
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Three-valued certainty of `C === subject` for a class / module
|
|
150
|
+
# `when` pattern, derived from {.narrow_class} /
|
|
151
|
+
# {.narrow_not_class}: `:no` when no inhabitant of the subject
|
|
152
|
+
# matches, `:yes` when every inhabitant matches, `:maybe`
|
|
153
|
+
# otherwise. The value-side counterpart of the scope narrowing
|
|
154
|
+
# {.case_when_scopes} performs for the same condition shape, kept
|
|
155
|
+
# here so the branch a `case` expression's type drops and the
|
|
156
|
+
# clause whose body scope goes dead derive from one judgment.
|
|
157
|
+
def class_pattern_certainty(subject_type, class_name, environment:)
|
|
158
|
+
truthy_bot = narrow_class(subject_type, class_name, environment: environment).is_a?(Type::Bot)
|
|
159
|
+
falsey_bot = narrow_not_class(subject_type, class_name, environment: environment).is_a?(Type::Bot)
|
|
160
|
+
|
|
161
|
+
return :no if truthy_bot && !falsey_bot
|
|
162
|
+
return :yes if !truthy_bot && falsey_bot
|
|
163
|
+
|
|
164
|
+
:maybe
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Classes whose `===` is plain value equality, so a literal
|
|
168
|
+
# `when` pattern against a pinned `Constant` subject is exact in
|
|
169
|
+
# both directions. Anything else keeps custom-`===` semantics
|
|
170
|
+
# and stays `:maybe` in {.value_pattern_certainty}.
|
|
171
|
+
VALUE_EQUALITY_CLASSES = [Integer, Float, Rational, Complex, String, Symbol,
|
|
172
|
+
TrueClass, FalseClass, NilClass].freeze
|
|
173
|
+
|
|
174
|
+
# Three-valued certainty of `<literal> === subject` for a
|
|
175
|
+
# value-equality literal pattern: exact (`:yes` / `:no`) only
|
|
176
|
+
# when the subject is itself a pinned `Constant` of a
|
|
177
|
+
# value-equality class; `:maybe` otherwise (the runtime value
|
|
178
|
+
# isn't pinned, or `===` may be user-defined).
|
|
179
|
+
def value_pattern_certainty(subject_type, pattern_value)
|
|
180
|
+
return :maybe unless subject_type.is_a?(Type::Constant)
|
|
181
|
+
return :maybe unless VALUE_EQUALITY_CLASSES.any? { |klass| subject_type.value.is_a?(klass) }
|
|
182
|
+
|
|
183
|
+
pattern_value == subject_type.value ? :yes : :no
|
|
184
|
+
end
|
|
185
|
+
|
|
126
186
|
# Equality fragment of `type` against a trusted literal.
|
|
127
187
|
#
|
|
128
188
|
# String/Symbol/Integer equality narrows only when the current
|
|
@@ -68,16 +68,17 @@ module Rigor
|
|
|
68
68
|
# collision — same-file declarations are the most
|
|
69
69
|
# specific authority.
|
|
70
70
|
merged_classes = default_scope.discovered_classes.merge(discovered_classes)
|
|
71
|
-
seeded_scope = default_scope
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
seeded_scope = default_scope.with_discovery(
|
|
72
|
+
default_scope.discovery.with(declared_types: declared_types,
|
|
73
|
+
discovered_classes: merged_classes)
|
|
74
|
+
)
|
|
74
75
|
|
|
75
76
|
# Slice 7 phase 2. Pre-pass over every class/module body
|
|
76
77
|
# to collect the per-class ivar accumulator. Seeded after
|
|
77
78
|
# declared_types so the rvalue typer in the pre-pass can
|
|
78
79
|
# see declaration overrides.
|
|
79
80
|
class_ivars = build_class_ivar_index(root, seeded_scope)
|
|
80
|
-
seeded_scope = seeded_scope.
|
|
81
|
+
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(class_ivars: class_ivars))
|
|
81
82
|
|
|
82
83
|
# Slice 7 phase 6. Same pre-pass shape for cvars (per
|
|
83
84
|
# class) and globals (program-wide). Globals are also
|
|
@@ -86,9 +87,9 @@ module Rigor
|
|
|
86
87
|
# not enter a method body) observe the precise type
|
|
87
88
|
# without consulting the accumulator on every lookup.
|
|
88
89
|
class_cvars = build_class_cvar_index(root, seeded_scope)
|
|
89
|
-
seeded_scope = seeded_scope.
|
|
90
|
+
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(class_cvars: class_cvars))
|
|
90
91
|
program_globals = build_program_global_index(root, seeded_scope)
|
|
91
|
-
seeded_scope = seeded_scope.
|
|
92
|
+
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(program_globals: program_globals))
|
|
92
93
|
program_globals.each { |name, type| seeded_scope = seeded_scope.with_global(name, type) }
|
|
93
94
|
|
|
94
95
|
# Slice 7 phase 9. In-source constant value tracking.
|
|
@@ -99,7 +100,9 @@ module Rigor
|
|
|
99
100
|
# references resolve correctly. Multiple writes to the
|
|
100
101
|
# same qualified name union via `Type::Combinator.union`.
|
|
101
102
|
in_source_constants = build_in_source_constants(root, seeded_scope)
|
|
102
|
-
seeded_scope = seeded_scope.
|
|
103
|
+
seeded_scope = seeded_scope.with_discovery(
|
|
104
|
+
seeded_scope.discovery.with(in_source_constants: in_source_constants)
|
|
105
|
+
)
|
|
103
106
|
|
|
104
107
|
# Slice 7 phase 12. In-source method discovery. Walks
|
|
105
108
|
# every class/module body for `Prism::DefNode` and
|
|
@@ -115,7 +118,7 @@ module Rigor
|
|
|
115
118
|
discovered_methods = deep_merge_class_methods(
|
|
116
119
|
default_scope.discovered_methods, build_discovered_methods(root)
|
|
117
120
|
)
|
|
118
|
-
seeded_scope = seeded_scope.
|
|
121
|
+
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(discovered_methods: discovered_methods))
|
|
119
122
|
|
|
120
123
|
# v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
|
|
121
124
|
# def nodes, the class -> superclass map, and the
|
|
@@ -179,12 +182,15 @@ module Rigor
|
|
|
179
182
|
build_data_member_layouts(root)
|
|
180
183
|
)
|
|
181
184
|
|
|
182
|
-
seeded_scope
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
seeded_scope.with_discovery(
|
|
186
|
+
seeded_scope.discovery.with(
|
|
187
|
+
discovered_def_nodes: def_nodes,
|
|
188
|
+
discovered_superclasses: superclasses,
|
|
189
|
+
discovered_includes: includes,
|
|
190
|
+
discovered_method_visibilities: method_visibilities,
|
|
191
|
+
data_member_layouts: data_member_layouts
|
|
192
|
+
)
|
|
193
|
+
)
|
|
188
194
|
end
|
|
189
195
|
|
|
190
196
|
# Slice 7 phase 2. Builds the class-level ivar accumulator
|
|
@@ -328,7 +334,7 @@ module Rigor
|
|
|
328
334
|
end
|
|
329
335
|
end
|
|
330
336
|
|
|
331
|
-
def walk_class_ivars(node, qualified_prefix, default_scope, accumulator, mutated_ivars,
|
|
337
|
+
def walk_class_ivars(node, qualified_prefix, default_scope, accumulator, mutated_ivars, # rubocop:disable Metrics/CyclomaticComplexity
|
|
332
338
|
read_before_write = nil, init_writes = nil)
|
|
333
339
|
return unless node.is_a?(Prism::Node)
|
|
334
340
|
|
|
@@ -363,6 +369,13 @@ module Rigor
|
|
|
363
369
|
collect_def_ivar_writes(node, qualified_prefix, default_scope, accumulator,
|
|
364
370
|
mutated_ivars, read_before_write, init_writes)
|
|
365
371
|
return
|
|
372
|
+
when Prism::CallNode
|
|
373
|
+
if init_writes && !qualified_prefix.empty? &&
|
|
374
|
+
node.block.is_a?(Prism::BlockNode) &&
|
|
375
|
+
block_initializer?(qualified_prefix.join("::"), node.name, default_scope)
|
|
376
|
+
collect_block_ivar_writes(node.block, qualified_prefix, default_scope,
|
|
377
|
+
accumulator, mutated_ivars, init_writes)
|
|
378
|
+
end
|
|
366
379
|
end
|
|
367
380
|
|
|
368
381
|
node.compact_child_nodes.each do |child|
|
|
@@ -399,6 +412,53 @@ module Rigor
|
|
|
399
412
|
collect_read_before_write_evidence(def_node, class_name, read_before_write, init_writes, default_scope)
|
|
400
413
|
end
|
|
401
414
|
|
|
415
|
+
# ADR-38 block-form: collects ivar writes from a CallNode's
|
|
416
|
+
# block body (e.g. RSpec `before { @x = … }` / `let(:x) { … }`)
|
|
417
|
+
# and folds them into `init_writes`, suppressing the
|
|
418
|
+
# read-before-write nil contribution the same way a def-form
|
|
419
|
+
# initializer does. The block body is always treated as an
|
|
420
|
+
# initializer (the caller has already verified the method name
|
|
421
|
+
# is declared as a block_method initializer), so there is no
|
|
422
|
+
# read-before-write evidence collection step here.
|
|
423
|
+
def collect_block_ivar_writes(block_node, qualified_prefix, default_scope, accumulator,
|
|
424
|
+
mutated_ivars, init_writes)
|
|
425
|
+
return if block_node.body.nil? || qualified_prefix.empty?
|
|
426
|
+
|
|
427
|
+
class_name = qualified_prefix.join("::")
|
|
428
|
+
self_type = Type::Combinator.nominal_of(class_name)
|
|
429
|
+
body_scope = default_scope.with_self_type(self_type)
|
|
430
|
+
|
|
431
|
+
gather_ivar_writes(block_node.body, body_scope, class_name, accumulator,
|
|
432
|
+
EMPTY_GUARDED_IVARS, mutated_ivars)
|
|
433
|
+
|
|
434
|
+
seen_writes = Set.new
|
|
435
|
+
read_first = Set.new
|
|
436
|
+
detect_read_before_write(block_node.body, seen_writes, read_first)
|
|
437
|
+
init_set = (init_writes[class_name] ||= Set.new)
|
|
438
|
+
seen_writes.each { |name| init_set << name }
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# ADR-38 block-form gate: true when a loaded plugin declares
|
|
442
|
+
# `method_name` a block-form initializer for `class_name` (or
|
|
443
|
+
# an ancestor). Mirrors `additional_initializer?` but queries
|
|
444
|
+
# `covers_block_method?` instead of `covers_method?`.
|
|
445
|
+
def block_initializer?(class_name, method_name, default_scope)
|
|
446
|
+
return false if class_name.nil? || default_scope.nil?
|
|
447
|
+
|
|
448
|
+
environment = default_scope.environment
|
|
449
|
+
registry = environment&.plugin_registry
|
|
450
|
+
return false if registry.nil?
|
|
451
|
+
return false if registry.respond_to?(:empty?) && registry.empty?
|
|
452
|
+
return false unless registry.respond_to?(:additional_initializers)
|
|
453
|
+
|
|
454
|
+
registry.additional_initializers.any? do |entry|
|
|
455
|
+
entry.covers_block_method?(method_name) &&
|
|
456
|
+
class_matches_constraint?(class_name, entry.receiver_constraint, environment)
|
|
457
|
+
end
|
|
458
|
+
rescue StandardError
|
|
459
|
+
false
|
|
460
|
+
end
|
|
461
|
+
|
|
402
462
|
# Walks the method body in AST (== execution) order
|
|
403
463
|
# tracking ivar names whose first reference is a read.
|
|
404
464
|
# The set is unioned into the class-wide
|
|
@@ -476,31 +476,19 @@ module Rigor
|
|
|
476
476
|
# carriers like `Nominal[Integer]` (Integer is always truthy
|
|
477
477
|
# in Ruby — including 0) also collapse the dead else.
|
|
478
478
|
def live_branch_for_if(node, pred_type, post_pred)
|
|
479
|
-
case predicate_certainty(pred_type)
|
|
480
|
-
when :
|
|
481
|
-
when :
|
|
479
|
+
case Narrowing.predicate_certainty(pred_type)
|
|
480
|
+
when :truthy then eval_branch_or_nil(node.statements, post_pred)
|
|
481
|
+
when :falsey then eval_branch_or_nil(node.subsequent, post_pred)
|
|
482
482
|
end
|
|
483
483
|
end
|
|
484
484
|
|
|
485
485
|
def live_branch_for_unless(node, pred_type, post_pred)
|
|
486
|
-
case predicate_certainty(pred_type)
|
|
487
|
-
when :
|
|
488
|
-
when :
|
|
486
|
+
case Narrowing.predicate_certainty(pred_type)
|
|
487
|
+
when :truthy then eval_branch_or_nil(node.else_clause, post_pred)
|
|
488
|
+
when :falsey then eval_branch_or_nil(node.statements, post_pred)
|
|
489
489
|
end
|
|
490
490
|
end
|
|
491
491
|
|
|
492
|
-
def predicate_certainty(pred_type)
|
|
493
|
-
return nil if pred_type.nil? || pred_type.is_a?(Type::Bot)
|
|
494
|
-
|
|
495
|
-
truthy_bot = Narrowing.narrow_truthy(pred_type).is_a?(Type::Bot)
|
|
496
|
-
falsey_bot = Narrowing.narrow_falsey(pred_type).is_a?(Type::Bot)
|
|
497
|
-
|
|
498
|
-
return :always_falsey if truthy_bot && !falsey_bot
|
|
499
|
-
return :always_truthy if !truthy_bot && falsey_bot
|
|
500
|
-
|
|
501
|
-
nil
|
|
502
|
-
end
|
|
503
|
-
|
|
504
492
|
def eval_else(node)
|
|
505
493
|
return [Type::Combinator.constant_of(nil), scope] if node.statements.nil?
|
|
506
494
|
|
|
@@ -1421,10 +1409,9 @@ module Rigor
|
|
|
1421
1409
|
end
|
|
1422
1410
|
end
|
|
1423
1411
|
|
|
1424
|
-
# ADR-37 slice 2 — gathers each plugin's post-return
|
|
1425
|
-
#
|
|
1426
|
-
# facts-only `FlowContribution
|
|
1427
|
-
# `flow_contribution_for` escape valve, swallowing per-plugin
|
|
1412
|
+
# ADR-37 slice 2 / ADR-52 WD3 — gathers each plugin's post-return
|
|
1413
|
+
# narrowing from the method-gated `type_specifier` DSL, wrapped as
|
|
1414
|
+
# a facts-only `FlowContribution`, swallowing per-plugin
|
|
1428
1415
|
# exceptions so a buggy plugin can't abort the assertion path.
|
|
1429
1416
|
EMPTY_CONTRIBUTIONS = [].freeze
|
|
1430
1417
|
private_constant :EMPTY_CONTRIBUTIONS
|
|
@@ -1432,8 +1419,11 @@ module Rigor
|
|
|
1432
1419
|
# Per-dispatch collection of plugin narrowing contributions. Mirrors
|
|
1433
1420
|
# `MethodDispatcher#collect_plugin_contributions`: visit only the
|
|
1434
1421
|
# registry-ordered subset of plugins that implement a per-call path
|
|
1435
|
-
# (`for_statement` =
|
|
1436
|
-
#
|
|
1422
|
+
# (`for_statement` = declares a `type_specifier`), gate each path
|
|
1423
|
+
# by membership AND by the ADR-52 WD1 method-name gates (every
|
|
1424
|
+
# `type_specifier` rule is `methods:`-gated, so the common
|
|
1425
|
+
# no-candidate case is a single Set probe; a pruned
|
|
1426
|
+
# consultation could only have returned `[]`), and accumulate
|
|
1437
1427
|
# lazily (shared frozen empty array otherwise). Same contributions in
|
|
1438
1428
|
# the same order as visiting every plugin; the caller is read-only.
|
|
1439
1429
|
def collect_plugin_contributions(registry, call_node, current_scope)
|
|
@@ -1441,16 +1431,21 @@ module Rigor
|
|
|
1441
1431
|
relevant = index.for_statement
|
|
1442
1432
|
return EMPTY_CONTRIBUTIONS if relevant.empty?
|
|
1443
1433
|
|
|
1434
|
+
name = call_node.respond_to?(:name) ? call_node.name : nil
|
|
1435
|
+
return EMPTY_CONTRIBUTIONS unless index.statement_candidate?(name)
|
|
1436
|
+
|
|
1437
|
+
collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
|
|
1438
|
+
end
|
|
1439
|
+
|
|
1440
|
+
# The post-gate walk, in registry order — the same order the
|
|
1441
|
+
# ungated walk used.
|
|
1442
|
+
def collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
|
|
1444
1443
|
result = nil
|
|
1445
1444
|
relevant.each do |plugin|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
if index.type_specifier?(plugin)
|
|
1451
|
-
facts = plugin.type_specifier_facts(call_node: call_node, scope: current_scope)
|
|
1452
|
-
(result ||= []) << Rigor::FlowContribution.new(post_return_facts: facts) if facts && !facts.empty?
|
|
1453
|
-
end
|
|
1445
|
+
next unless index.type_specifier_candidate_for?(plugin, name)
|
|
1446
|
+
|
|
1447
|
+
facts = plugin.type_specifier_facts(call_node: call_node, scope: current_scope)
|
|
1448
|
+
(result ||= []) << Rigor::FlowContribution.new(post_return_facts: facts) if facts && !facts.empty?
|
|
1454
1449
|
rescue StandardError
|
|
1455
1450
|
next
|
|
1456
1451
|
end
|
|
@@ -1834,30 +1829,18 @@ module Rigor
|
|
|
1834
1829
|
# ScopeIndexer-populated declaration overrides
|
|
1835
1830
|
# (`Prism::ConstantReadNode` for `module Foo` headers, etc.)
|
|
1836
1831
|
# remain reachable from inside nested bodies.
|
|
1837
|
-
def build_fresh_body_scope
|
|
1838
|
-
# Single allocation instead of a
|
|
1839
|
-
# per class/method body on the main walk, so the chain's
|
|
1840
|
-
#
|
|
1841
|
-
# Local-empty by design;
|
|
1842
|
-
#
|
|
1843
|
-
#
|
|
1832
|
+
def build_fresh_body_scope
|
|
1833
|
+
# Single allocation instead of a deep `with_*` chain — this runs
|
|
1834
|
+
# per class/method body on the main walk, so the chain's throwaway
|
|
1835
|
+
# intermediate Scopes were a top `Scope#rebuild` source (ADR-44).
|
|
1836
|
+
# Local-empty by design; the discovery index is inherited whole by
|
|
1837
|
+
# reference (ADR-53 Track A), so a table added to the index can no
|
|
1838
|
+
# longer be dropped here by a missed per-field copy.
|
|
1844
1839
|
Scope.new(
|
|
1845
1840
|
environment: scope.environment,
|
|
1846
1841
|
locals: {}.freeze,
|
|
1847
1842
|
source_path: scope.source_path,
|
|
1848
|
-
|
|
1849
|
-
discovered_classes: scope.discovered_classes,
|
|
1850
|
-
in_source_constants: scope.in_source_constants,
|
|
1851
|
-
class_ivars: scope.class_ivars,
|
|
1852
|
-
class_cvars: scope.class_cvars,
|
|
1853
|
-
program_globals: scope.program_globals,
|
|
1854
|
-
discovered_methods: scope.discovered_methods,
|
|
1855
|
-
discovered_def_nodes: scope.discovered_def_nodes,
|
|
1856
|
-
discovered_def_sources: scope.discovered_def_sources,
|
|
1857
|
-
discovered_superclasses: scope.discovered_superclasses,
|
|
1858
|
-
discovered_includes: scope.discovered_includes,
|
|
1859
|
-
discovered_class_sources: scope.discovered_class_sources,
|
|
1860
|
-
discovered_method_visibilities: scope.discovered_method_visibilities
|
|
1843
|
+
discovery: scope.discovery
|
|
1861
1844
|
)
|
|
1862
1845
|
end
|
|
1863
1846
|
|