rigortype 0.0.9 → 0.1.0

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +40 -2
  3. data/lib/rigor/analysis/check_rules.rb +228 -40
  4. data/lib/rigor/analysis/diagnostic.rb +15 -1
  5. data/lib/rigor/analysis/runner.rb +183 -4
  6. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  7. data/lib/rigor/cache/rbs_class_type_param_names.rb +1 -1
  8. data/lib/rigor/cache/rbs_constant_table.rb +2 -2
  9. data/lib/rigor/cache/rbs_descriptor.rb +2 -0
  10. data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
  11. data/lib/rigor/cache/store.rb +2 -0
  12. data/lib/rigor/cli.rb +9 -3
  13. data/lib/rigor/configuration/severity_profile.rb +109 -0
  14. data/lib/rigor/configuration.rb +110 -6
  15. data/lib/rigor/environment/rbs_loader.rb +89 -13
  16. data/lib/rigor/flow_contribution/conflict.rb +81 -0
  17. data/lib/rigor/flow_contribution/element.rb +53 -0
  18. data/lib/rigor/flow_contribution/fact.rb +88 -0
  19. data/lib/rigor/flow_contribution/merge_result.rb +67 -0
  20. data/lib/rigor/flow_contribution/merger.rb +275 -0
  21. data/lib/rigor/flow_contribution.rb +51 -0
  22. data/lib/rigor/inference/block_parameter_binder.rb +15 -0
  23. data/lib/rigor/inference/expression_typer.rb +84 -5
  24. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +95 -9
  25. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
  26. data/lib/rigor/inference/multi_target_binder.rb +2 -0
  27. data/lib/rigor/inference/narrowing.rb +105 -130
  28. data/lib/rigor/inference/scope_indexer.rb +75 -1
  29. data/lib/rigor/inference/statement_evaluator.rb +380 -40
  30. data/lib/rigor/plugin/access_denied_error.rb +24 -0
  31. data/lib/rigor/plugin/base.rb +241 -0
  32. data/lib/rigor/plugin/io_boundary.rb +102 -0
  33. data/lib/rigor/plugin/load_error.rb +23 -0
  34. data/lib/rigor/plugin/loader.rb +191 -0
  35. data/lib/rigor/plugin/manifest.rb +134 -0
  36. data/lib/rigor/plugin/registry.rb +50 -0
  37. data/lib/rigor/plugin/services.rb +65 -0
  38. data/lib/rigor/plugin/trust_policy.rb +99 -0
  39. data/lib/rigor/plugin.rb +61 -0
  40. data/lib/rigor/rbs_extended.rb +57 -9
  41. data/lib/rigor/reflection.rb +2 -2
  42. data/lib/rigor/version.rb +1 -1
  43. data/lib/rigor.rb +7 -0
  44. data/sig/rigor/environment.rbs +7 -1
  45. data/sig/rigor/inference.rbs +1 -0
  46. data/sig/rigor/rbs_extended.rbs +2 -0
  47. data/sig/rigor/scope.rbs +1 -0
  48. data/sig/rigor/type.rbs +7 -0
  49. metadata +18 -1
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class FlowContribution
5
+ # Tagged element flattening of a {FlowContribution} bundle —
6
+ # the analyzer-internal representation [ADR-2 § "Flow
7
+ # Contribution Bundle"](../../../docs/adr/2-extension-api.md)
8
+ # routes through the {Merger}.
9
+ #
10
+ # The flattening is **mechanical, deterministic, and round-
11
+ # trippable** with the bundle: every non-empty slot expands
12
+ # into one or more elements keyed by `(target, edge, kind)`,
13
+ # and an array of elements rebuilds an equivalent bundle when
14
+ # routed through `Merger.merge`.
15
+ #
16
+ # Plugin authors should not depend on the element shape — the
17
+ # bundle is the public contract; the element list is the
18
+ # implementation surface the merge policy operates over.
19
+ ELEMENT_VALID_EDGES = %i[normal truthy falsey post_return exceptional].freeze
20
+ ELEMENT_VALID_KINDS = %i[
21
+ return_type
22
+ truthy_fact
23
+ falsey_fact
24
+ post_return_fact
25
+ mutation
26
+ invalidation
27
+ exception
28
+ role
29
+ ].freeze
30
+
31
+ Element = Data.define(:target, :edge, :kind, :payload, :provenance) do
32
+ def initialize(target:, edge:, kind:, payload:, provenance:)
33
+ unless ELEMENT_VALID_EDGES.include?(edge)
34
+ raise ArgumentError,
35
+ "FlowContribution::Element edge must be one of " \
36
+ "#{ELEMENT_VALID_EDGES.inspect}, got #{edge.inspect}"
37
+ end
38
+
39
+ unless ELEMENT_VALID_KINDS.include?(kind)
40
+ raise ArgumentError,
41
+ "FlowContribution::Element kind must be one of " \
42
+ "#{ELEMENT_VALID_KINDS.inspect}, got #{kind.inspect}"
43
+ end
44
+
45
+ super
46
+ end
47
+
48
+ def merge_key
49
+ [target, edge, kind]
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class FlowContribution
5
+ # Canonical slot payload for the four edge-aware fact slots
6
+ # (`truthy_facts`, `falsey_facts`, `post_return_facts`, plus
7
+ # the equivalent under `mutations` / `invalidations` /
8
+ # `role_conformance` once those carriers grow Fact-shaped
9
+ # variants).
10
+ #
11
+ # ADR-7 § "Slice 4-A" pins this object as the **single
12
+ # canonical translation target** for the four parallel
13
+ # contribution carriers the engine has carried so far:
14
+ #
15
+ # 1. Built-in narrowing rules' direct fact emission
16
+ # (Inference::Narrowing#predicate_scopes).
17
+ # 2. RBS::Extended `predicate-if-*` directives
18
+ # (`Rigor::RbsExtended::PredicateEffect`).
19
+ # 3. RBS::Extended `assert*` directives
20
+ # (`Rigor::RbsExtended::AssertEffect`).
21
+ # 4. Future plugin contributions (slice 5 emission protocol).
22
+ #
23
+ # Each of those four carriers translates to / from Fact at
24
+ # its boundary; downstream of {Rigor::FlowContribution#to_element_list}
25
+ # and {Rigor::FlowContribution::Merger.merge}, every slot
26
+ # payload is a Fact (or a value that the merger compares by
27
+ # equality and never inspects). The typed `RbsExtended::*Effect`
28
+ # carriers stay internal to the parser side — they hold the
29
+ # source-text shape, but lose their identity at the
30
+ # `read_flow_contribution` boundary.
31
+ #
32
+ # ## Field set
33
+ #
34
+ # - `target_kind`: `:parameter` (call-site argument) or
35
+ # `:self` (receiver). Future slices may extend the set
36
+ # (`:local`, `:ivar`, `:result`); the merger is agnostic
37
+ # to the concrete kinds and only requires equality.
38
+ # - `target_name`: a `Symbol`. For `:parameter` it's the
39
+ # declared parameter name. For `:self` it is the literal
40
+ # `:self` symbol so the field stays non-nil and the merge
41
+ # key is well-defined.
42
+ # - `type`: a `Rigor::Type::*` (Nominal, Refined,
43
+ # IntegerRange, Difference, …) the fact narrows the
44
+ # target toward (when `negative` is false) or away from
45
+ # (when `negative` is true).
46
+ # - `negative`: `true` for the `~T` negation form
47
+ # (`predicate-if-true x is ~Integer`), `false` for the
48
+ # plain positive form. Mirrors the `negative` field on
49
+ # `PredicateEffect` / `AssertEffect`.
50
+ #
51
+ # The `target` accessor returns `:self` for self-targeted
52
+ # facts and `[:parameter, name]` otherwise — that's the
53
+ # value {Element#target} keys on, so two facts that narrow
54
+ # the same parameter from different contribution sources
55
+ # land in the same merge bucket.
56
+ FACT_VALID_TARGET_KINDS = %i[parameter self].freeze
57
+
58
+ Fact = Data.define(:target_kind, :target_name, :type, :negative) do
59
+ def initialize(target_kind:, target_name:, type:, negative: false)
60
+ unless FACT_VALID_TARGET_KINDS.include?(target_kind)
61
+ raise ArgumentError,
62
+ "FlowContribution::Fact target_kind must be one of " \
63
+ "#{FACT_VALID_TARGET_KINDS.inspect}, got #{target_kind.inspect}"
64
+ end
65
+
66
+ unless target_name.is_a?(Symbol)
67
+ raise ArgumentError,
68
+ "FlowContribution::Fact target_name must be a Symbol, got #{target_name.inspect}"
69
+ end
70
+
71
+ super
72
+ end
73
+
74
+ # Composite target identifier the merger keys on. `:self`
75
+ # for self-targeted facts; otherwise `[:parameter, name]`
76
+ # so two contributions that narrow the same parameter
77
+ # (regardless of source family) land in the same merge
78
+ # bucket.
79
+ def target
80
+ target_kind == :self ? :self : [target_kind, target_name]
81
+ end
82
+
83
+ def negative?
84
+ negative == true
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class FlowContribution
5
+ # Result of folding any number of {FlowContribution} bundles
6
+ # through {Merger.merge}. Surfaces the merged content slot-by-
7
+ # slot, the ordered list of contributing provenances, and the
8
+ # {Conflict} list collected along the way.
9
+ #
10
+ # The merge result is a sibling shape of {FlowContribution} —
11
+ # the analyzer reads from it to drive narrowing / dispatch /
12
+ # diagnostics, and the formatter reads from it to surface
13
+ # plugin / RBS::Extended provenance. The shape is derived per
14
+ # ADR-2 § "Plugin Contribution Merging"; see
15
+ # [`docs/internal-spec/flow-contribution-merger.md`](../../../docs/internal-spec/flow-contribution-merger.md)
16
+ # for the slice-3 normative description.
17
+ class MergeResult
18
+ attr_reader :return_type, :truthy_facts, :falsey_facts, :post_return_facts,
19
+ :mutations, :invalidations, :exceptional, :role_conformance,
20
+ :provenances, :conflicts
21
+
22
+ # rubocop:disable Metrics/ParameterLists
23
+ def initialize(return_type: nil, truthy_facts: [], falsey_facts: [],
24
+ post_return_facts: [], mutations: [], invalidations: [],
25
+ exceptional: nil, role_conformance: [],
26
+ provenances: [], conflicts: [])
27
+ # rubocop:enable Metrics/ParameterLists
28
+ @return_type = return_type
29
+ @truthy_facts = truthy_facts.dup.freeze
30
+ @falsey_facts = falsey_facts.dup.freeze
31
+ @post_return_facts = post_return_facts.dup.freeze
32
+ @mutations = mutations.dup.freeze
33
+ @invalidations = invalidations.dup.freeze
34
+ @exceptional = exceptional
35
+ @role_conformance = role_conformance.dup.freeze
36
+ @provenances = provenances.dup.freeze
37
+ @conflicts = conflicts.dup.freeze
38
+ freeze
39
+ end
40
+
41
+ def conflict?
42
+ !@conflicts.empty?
43
+ end
44
+
45
+ def empty? # rubocop:disable Metrics/CyclomaticComplexity
46
+ @return_type.nil? && @truthy_facts.empty? && @falsey_facts.empty? &&
47
+ @post_return_facts.empty? && @mutations.empty? && @invalidations.empty? &&
48
+ @exceptional.nil? && @role_conformance.empty?
49
+ end
50
+
51
+ def to_h
52
+ {
53
+ "return_type" => return_type,
54
+ "truthy_facts" => truthy_facts,
55
+ "falsey_facts" => falsey_facts,
56
+ "post_return_facts" => post_return_facts,
57
+ "mutations" => mutations,
58
+ "invalidations" => invalidations,
59
+ "exceptional" => exceptional,
60
+ "role_conformance" => role_conformance,
61
+ "provenances" => provenances.map { |p| p.respond_to?(:to_h) ? p.to_h : p },
62
+ "conflicts" => conflicts.map(&:to_h)
63
+ }
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class FlowContribution
5
+ # Composes any number of {FlowContribution} bundles into a
6
+ # single {MergeResult} per ADR-2 § "Plugin Contribution
7
+ # Merging". The merger is the **single point of integration**
8
+ # the analyzer uses to combine contributions from built-in
9
+ # narrowing rules, `RBS::Extended` annotations, and plugins;
10
+ # slice 4 routes the existing internal narrowing through it
11
+ # and slice 6 wires plugin-side cache producers around it.
12
+ #
13
+ # ## Authority tiers
14
+ #
15
+ # - Tier 0: `:builtin` — Core Ruby semantics and accepted RBS
16
+ # contracts. Authoritative; lower tiers may not contradict.
17
+ # - Tier 1: `:rbs_extended` (`RBS::Extended` directive bundles,
18
+ # v0.0.9 group D reference impl) and `:generated` (generated
19
+ # signatures / metadata).
20
+ # - Tier 2: `:plugin` and `plugin.<id>` source families.
21
+ # - Tier 3: anything else — treated as the lowest tier.
22
+ #
23
+ # Within a tier, contributions are merged in deterministic
24
+ # order: provenance-supplied `plugin_id` alphabetical (nil
25
+ # plugin ids sort first to keep `:rbs_extended` / `:generated`
26
+ # pre-plugin contributions stable), then by their original
27
+ # input position as the final tie-break.
28
+ #
29
+ # ## Composition rules (ADR-2)
30
+ #
31
+ # - `:return_type` — Intersect via `Type::Combinator.intersection`;
32
+ # collapse to bot raises `:return_type_collapse`.
33
+ # - `:truthy_fact` / `:falsey_fact` / `:post_return_fact` —
34
+ # Edge-local; accumulate while deduping by payload equality.
35
+ # - `:mutation` / `:invalidation` / `:role` — Union; dedupe by
36
+ # equality.
37
+ # - `:exception` — Single-valued. Two non-`nil` non-equal
38
+ # exceptional effects raise `:exceptional_disagreement`.
39
+ #
40
+ # ## Cross-tier contradictions
41
+ #
42
+ # Lower tiers may refine higher tiers but must not weaken them.
43
+ # Slice 3 surfaces contradictions through `:lower_tier_contradiction`
44
+ # when:
45
+ #
46
+ # - the higher tier already pinned a `return_type` and a lower
47
+ # tier's intersection collapses to `bot`;
48
+ # - the higher tier set `exceptional` to a non-`nil` value and a
49
+ # lower tier disagrees.
50
+ #
51
+ # In every conflict case the result keeps the higher-tier value
52
+ # for that slot, records a {Conflict} with both provenances, and
53
+ # continues processing the remaining slots / contributions.
54
+ module Merger # rubocop:disable Metrics/ModuleLength
55
+ AUTHORITY_TIERS = {
56
+ builtin: 0,
57
+ rbs_extended: 1,
58
+ generated: 1
59
+ }.freeze
60
+
61
+ module_function
62
+
63
+ # @param contributions [Array<FlowContribution>]
64
+ # @return [MergeResult]
65
+ def merge(contributions)
66
+ contributions = Array(contributions)
67
+ return MergeResult.new if contributions.empty?
68
+
69
+ ordered = order_contributions(contributions)
70
+ state = MergeState.new
71
+ ordered.each do |contribution|
72
+ tier = tier_for(contribution.provenance)
73
+ fold_into(state, contribution, tier)
74
+ end
75
+ state.to_result
76
+ end
77
+
78
+ def tier_for(provenance)
79
+ family = provenance.respond_to?(:source_family) ? provenance.source_family : nil
80
+ return AUTHORITY_TIERS[family] if AUTHORITY_TIERS.key?(family)
81
+ return 2 if family == :plugin || family.to_s.start_with?("plugin.")
82
+
83
+ 3
84
+ end
85
+
86
+ class << self
87
+ private
88
+
89
+ def order_contributions(contributions)
90
+ # Stable sort: tier ascending, then provenance plugin_id
91
+ # alphabetical (nil first), then original input position.
92
+ contributions.each_with_index
93
+ .sort_by { |c, i| [tier_for(c.provenance), plugin_id_key(c.provenance), i] }
94
+ .map { |c, _| c }
95
+ end
96
+
97
+ def plugin_id_key(provenance)
98
+ id = provenance.respond_to?(:plugin_id) ? provenance.plugin_id : nil
99
+ [id.nil? ? 0 : 1, id.to_s]
100
+ end
101
+
102
+ def fold_into(state, contribution, tier)
103
+ state.add_provenance(contribution.provenance)
104
+ fold_return_type(state, contribution, tier)
105
+ fold_facts(state, contribution)
106
+ fold_effects(state, contribution)
107
+ fold_exceptional(state, contribution, tier)
108
+ fold_role_conformance(state, contribution)
109
+ end
110
+
111
+ def fold_return_type(state, contribution, tier) # rubocop:disable Metrics/AbcSize
112
+ incoming = contribution.return_type
113
+ return if incoming.nil?
114
+
115
+ if state.return_type.nil?
116
+ state.return_type = incoming
117
+ state.return_type_tier = tier
118
+ state.return_type_provenance = contribution.provenance
119
+ return
120
+ end
121
+
122
+ if intersection_empty?(state.return_type, incoming)
123
+ reason = tier > state.return_type_tier ? :lower_tier_contradiction : :return_type_collapse
124
+ state.conflicts << build_conflict(
125
+ target: :return,
126
+ edge: :normal,
127
+ kind: :return_type,
128
+ reason: reason,
129
+ provenances: [state.return_type_provenance, contribution.provenance],
130
+ message: return_type_conflict_message(reason, state.return_type, incoming)
131
+ )
132
+ return
133
+ end
134
+
135
+ state.return_type = Rigor::Type::Combinator.intersection(state.return_type, incoming)
136
+ state.return_type_tier = [state.return_type_tier, tier].min
137
+ end
138
+
139
+ # Two types' intersection collapses to bot when neither
140
+ # accepts the other under gradual mode — i.e. the value
141
+ # domains are disjoint. `Rigor::Type::Combinator.intersection`
142
+ # itself does not collapse incompatible nominals (`String ∩
143
+ # Integer` builds a structurally-empty `Intersection`
144
+ # carrier), so the merger checks the disjointness condition
145
+ # directly via the `accepts` trinary.
146
+ def intersection_empty?(lhs, rhs)
147
+ return false if lhs.equal?(rhs)
148
+
149
+ lhs_no = lhs.accepts(rhs).no?
150
+ rhs_no = rhs.accepts(lhs).no?
151
+ lhs_no && rhs_no
152
+ rescue StandardError
153
+ false
154
+ end
155
+
156
+ def fold_facts(state, contribution)
157
+ accumulate(state.truthy_facts, contribution.truthy_facts)
158
+ accumulate(state.falsey_facts, contribution.falsey_facts)
159
+ accumulate(state.post_return_facts, contribution.post_return_facts)
160
+ end
161
+
162
+ def fold_effects(state, contribution)
163
+ accumulate(state.mutations, contribution.mutations)
164
+ accumulate(state.invalidations, contribution.invalidations)
165
+ end
166
+
167
+ def fold_exceptional(state, contribution, tier)
168
+ incoming = contribution.exceptional
169
+ return if incoming.nil?
170
+
171
+ if state.exceptional.nil?
172
+ state.exceptional = incoming
173
+ state.exceptional_tier = tier
174
+ state.exceptional_provenance = contribution.provenance
175
+ return
176
+ end
177
+
178
+ return if state.exceptional == incoming
179
+
180
+ reason = tier > state.exceptional_tier ? :lower_tier_contradiction : :exceptional_disagreement
181
+ state.conflicts << build_conflict(
182
+ target: :raise,
183
+ edge: :exceptional,
184
+ kind: :exception,
185
+ reason: reason,
186
+ provenances: [state.exceptional_provenance, contribution.provenance],
187
+ message: "exceptional effect disagreement: #{state.exceptional.inspect} vs #{incoming.inspect}"
188
+ )
189
+ end
190
+
191
+ def fold_role_conformance(state, contribution)
192
+ accumulate(state.role_conformance, contribution.role_conformance)
193
+ end
194
+
195
+ def accumulate(target, incoming)
196
+ Array(incoming).each do |item|
197
+ target << item unless target.include?(item)
198
+ end
199
+ end
200
+
201
+ def build_conflict(target:, edge:, kind:, reason:, provenances:, message:) # rubocop:disable Metrics/ParameterLists
202
+ Conflict.new(target: target, edge: edge, kind: kind, reason: reason,
203
+ provenances: provenances, message: message)
204
+ end
205
+
206
+ def return_type_conflict_message(reason, lhs, rhs)
207
+ case reason
208
+ when :return_type_collapse
209
+ "return-type intersection collapses to bot: #{describe(lhs)} vs #{describe(rhs)}"
210
+ when :lower_tier_contradiction
211
+ "lower-tier return-type #{describe(rhs)} contradicts higher-tier proof #{describe(lhs)}"
212
+ end
213
+ end
214
+
215
+ def describe(type)
216
+ if type.respond_to?(:describe)
217
+ type.describe(:short)
218
+ else
219
+ type.inspect
220
+ end
221
+ rescue StandardError
222
+ type.inspect
223
+ end
224
+ end
225
+
226
+ # Internal accumulator carried through a single merge call.
227
+ # Not part of the public API; folds into a {MergeResult} at
228
+ # the end via {#to_result}.
229
+ class MergeState
230
+ attr_accessor :return_type, :return_type_tier, :return_type_provenance,
231
+ :exceptional, :exceptional_tier, :exceptional_provenance
232
+ attr_reader :truthy_facts, :falsey_facts, :post_return_facts,
233
+ :mutations, :invalidations, :role_conformance,
234
+ :provenances, :conflicts
235
+
236
+ def initialize
237
+ @return_type = nil
238
+ @return_type_tier = nil
239
+ @return_type_provenance = nil
240
+ @truthy_facts = []
241
+ @falsey_facts = []
242
+ @post_return_facts = []
243
+ @mutations = []
244
+ @invalidations = []
245
+ @exceptional = nil
246
+ @exceptional_tier = nil
247
+ @exceptional_provenance = nil
248
+ @role_conformance = []
249
+ @provenances = []
250
+ @conflicts = []
251
+ end
252
+
253
+ def add_provenance(provenance)
254
+ @provenances << provenance
255
+ end
256
+
257
+ def to_result
258
+ MergeResult.new(
259
+ return_type: @return_type,
260
+ truthy_facts: @truthy_facts,
261
+ falsey_facts: @falsey_facts,
262
+ post_return_facts: @post_return_facts,
263
+ mutations: @mutations,
264
+ invalidations: @invalidations,
265
+ exceptional: @exceptional,
266
+ role_conformance: @role_conformance,
267
+ provenances: @provenances,
268
+ conflicts: @conflicts
269
+ )
270
+ end
271
+ end
272
+ private_constant :MergeState
273
+ end
274
+ end
275
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "flow_contribution/element"
4
+
3
5
  module Rigor
4
6
  # The public packaging of a flow contribution at a single call edge.
5
7
  # Plugins, `RBS::Extended` annotations, and built-in narrowing rules
@@ -101,6 +103,40 @@ module Rigor
101
103
  end
102
104
  end
103
105
 
106
+ # Flattens this bundle into a tagged element list keyed by
107
+ # `(target, edge, kind)`. The flattening is mechanical and
108
+ # round-trippable through {Merger.merge}: feeding the result
109
+ # back through the merger produces an equivalent bundle.
110
+ #
111
+ # Layout:
112
+ #
113
+ # | slot | edge | kind | target |
114
+ # | --------------------|---------------|---------------------|-------------------------|
115
+ # | return_type | normal | return_type | :return |
116
+ # | truthy_facts | truthy | truthy_fact | (per-fact target) |
117
+ # | falsey_facts | falsey | falsey_fact | (per-fact target) |
118
+ # | post_return_facts | post_return | post_return_fact | (per-fact target) |
119
+ # | mutations | normal | mutation | (per-mutation target) |
120
+ # | invalidations | normal | invalidation | (per-fact target) |
121
+ # | exceptional | exceptional | exception | :raise |
122
+ # | role_conformance | normal | role | (per-role target) |
123
+ #
124
+ # @return [Array<Element>]
125
+ def to_element_list # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
126
+ elements = []
127
+ elements << element_for(:return, :normal, :return_type, return_type) unless return_type.nil?
128
+ Array(truthy_facts).each { |fact| elements << element_for(fact_target(fact), :truthy, :truthy_fact, fact) }
129
+ Array(falsey_facts).each { |fact| elements << element_for(fact_target(fact), :falsey, :falsey_fact, fact) }
130
+ Array(post_return_facts).each do |fact|
131
+ elements << element_for(fact_target(fact), :post_return, :post_return_fact, fact)
132
+ end
133
+ Array(mutations).each { |m| elements << element_for(fact_target(m), :normal, :mutation, m) }
134
+ Array(invalidations).each { |i| elements << element_for(fact_target(i), :normal, :invalidation, i) }
135
+ elements << element_for(:raise, :exceptional, :exception, exceptional) unless exceptional.nil?
136
+ Array(role_conformance).each { |r| elements << element_for(fact_target(r), :normal, :role, r) }
137
+ elements.freeze
138
+ end
139
+
104
140
  def ==(other)
105
141
  other.is_a?(FlowContribution) && to_h == other.to_h
106
142
  end
@@ -112,6 +148,21 @@ module Rigor
112
148
 
113
149
  private
114
150
 
151
+ def element_for(target, edge, kind, payload)
152
+ Element.new(target: target, edge: edge, kind: kind, payload: payload, provenance: provenance)
153
+ end
154
+
155
+ # Best-effort target extraction. Payloads that expose a
156
+ # `#target` accessor (typed-fact carriers, mutation effects)
157
+ # provide their own; everything else falls through with the
158
+ # payload itself as the merge key, which keeps deduplication
159
+ # well-defined for opaque entries.
160
+ def fact_target(payload)
161
+ return payload.target if payload.respond_to?(:target)
162
+
163
+ payload
164
+ end
165
+
115
166
  def freeze_collection(value)
116
167
  return nil if value.nil?
117
168
 
@@ -34,6 +34,12 @@ module Rigor
34
34
  # `[1, 2, 3].each { _1 + _2 }` sees `_1`/`_2` typed identically
35
35
  # to their explicit `|x, y|` counterparts.
36
36
  #
37
+ # The `it` implicit parameter (Ruby 3.4+) is bound from
38
+ # `Prism::ItParametersNode`. It is the single-argument cousin of
39
+ # `_1`: the binder produces `{ it: expected_param_types[0] }` so
40
+ # the body's `Prism::ItLocalVariableReadNode` lookup sees the same
41
+ # type as the explicit `|x|` form would.
42
+ #
37
43
  # Block-local declarations after `;` (e.g., `|x; y, z|`) are
38
44
  # still skipped — they are explicitly block-local, so the outer
39
45
  # scope MUST NOT observe them and the binder leaves them unbound.
@@ -65,6 +71,8 @@ module Rigor
65
71
  case params_root
66
72
  when Prism::NumberedParametersNode
67
73
  bind_numbered_parameters(params_root)
74
+ when Prism::ItParametersNode
75
+ bind_it_parameter
68
76
  when Prism::BlockParametersNode
69
77
  bind_block_parameters(params_root)
70
78
  else
@@ -88,6 +96,13 @@ module Rigor
88
96
  bindings
89
97
  end
90
98
 
99
+ # `{ it.foo }` — Ruby 3.4 `it` is a single-argument implicit
100
+ # parameter. Always binds the symbol `:it`; `ItLocalVariableReadNode`
101
+ # in the body looks the binding up by name.
102
+ def bind_it_parameter
103
+ { it: positional_type_at(0) }
104
+ end
105
+
91
106
  def bind_block_parameters(params_root)
92
107
  params_node = params_root.parameters
93
108
  return {} if params_node.nil?