rigortype 0.0.9 → 0.1.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -2
  3. data/data/builtins/ruby_core/array.yml +6 -6
  4. data/data/builtins/ruby_core/hash.yml +1 -1
  5. data/data/builtins/ruby_core/io.yml +3 -3
  6. data/data/builtins/ruby_core/numeric.yml +1 -1
  7. data/data/builtins/ruby_core/pathname.yml +100 -100
  8. data/data/builtins/ruby_core/proc.yml +1 -1
  9. data/data/builtins/ruby_core/time.yml +3 -3
  10. data/lib/rigor/analysis/check_rules.rb +228 -40
  11. data/lib/rigor/analysis/diagnostic.rb +15 -1
  12. data/lib/rigor/analysis/runner.rb +269 -7
  13. data/lib/rigor/builtins/regex_refinement.rb +104 -0
  14. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  15. data/lib/rigor/cache/rbs_class_type_param_names.rb +1 -1
  16. data/lib/rigor/cache/rbs_constant_table.rb +2 -2
  17. data/lib/rigor/cache/rbs_descriptor.rb +2 -0
  18. data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
  19. data/lib/rigor/cache/store.rb +2 -0
  20. data/lib/rigor/cli/type_of_command.rb +3 -3
  21. data/lib/rigor/cli/type_scan_command.rb +4 -4
  22. data/lib/rigor/cli.rb +20 -7
  23. data/lib/rigor/configuration/severity_profile.rb +109 -0
  24. data/lib/rigor/configuration.rb +286 -15
  25. data/lib/rigor/environment/rbs_loader.rb +89 -13
  26. data/lib/rigor/environment.rb +12 -4
  27. data/lib/rigor/flow_contribution/conflict.rb +81 -0
  28. data/lib/rigor/flow_contribution/element.rb +53 -0
  29. data/lib/rigor/flow_contribution/fact.rb +88 -0
  30. data/lib/rigor/flow_contribution/merge_result.rb +67 -0
  31. data/lib/rigor/flow_contribution/merger.rb +275 -0
  32. data/lib/rigor/flow_contribution.rb +51 -0
  33. data/lib/rigor/inference/block_parameter_binder.rb +15 -0
  34. data/lib/rigor/inference/expression_typer.rb +87 -6
  35. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +31 -0
  36. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +136 -9
  37. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
  38. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +68 -2
  39. data/lib/rigor/inference/method_dispatcher.rb +50 -1
  40. data/lib/rigor/inference/multi_target_binder.rb +2 -0
  41. data/lib/rigor/inference/narrowing.rb +246 -127
  42. data/lib/rigor/inference/scope_indexer.rb +124 -16
  43. data/lib/rigor/inference/statement_evaluator.rb +406 -37
  44. data/lib/rigor/plugin/access_denied_error.rb +24 -0
  45. data/lib/rigor/plugin/base.rb +284 -0
  46. data/lib/rigor/plugin/fact_store.rb +92 -0
  47. data/lib/rigor/plugin/io_boundary.rb +102 -0
  48. data/lib/rigor/plugin/load_error.rb +35 -0
  49. data/lib/rigor/plugin/loader.rb +307 -0
  50. data/lib/rigor/plugin/manifest.rb +203 -0
  51. data/lib/rigor/plugin/registry.rb +50 -0
  52. data/lib/rigor/plugin/services.rb +77 -0
  53. data/lib/rigor/plugin/trust_policy.rb +99 -0
  54. data/lib/rigor/plugin.rb +62 -0
  55. data/lib/rigor/rbs_extended.rb +57 -9
  56. data/lib/rigor/reflection.rb +2 -2
  57. data/lib/rigor/trinary.rb +1 -1
  58. data/lib/rigor/type/integer_range.rb +6 -2
  59. data/lib/rigor/version.rb +1 -1
  60. data/lib/rigor.rb +7 -0
  61. data/sig/rigor/environment.rbs +10 -3
  62. data/sig/rigor/inference.rbs +1 -0
  63. data/sig/rigor/rbs_extended.rbs +2 -0
  64. data/sig/rigor/scope.rbs +1 -0
  65. data/sig/rigor/type.rbs +7 -0
  66. data/sig/rigor.rbs +8 -2
  67. metadata +20 -1
@@ -125,19 +125,42 @@ module Rigor
125
125
 
126
126
  env.class_decls.each_key { |rbs_name| yield rbs_name.to_s }
127
127
  env.class_alias_decls.each_key { |rbs_name| yield rbs_name.to_s }
128
- rescue StandardError
129
- # fail-soft: a broken environment yields no names.
128
+ rescue ::RBS::BaseError
129
+ # fail-soft: a broken RBS environment yields no names.
130
+ # Analyzer-internal errors (NameError, NoMethodError,
131
+ # LoadError) are NOT swallowed — those are bugs and
132
+ # must surface so they don't hide silently the way the
133
+ # v0.0.9 cache `Cache::Descriptor` regression did.
130
134
  end
131
135
 
132
136
  # @return [RBS::Definition, nil] the resolved instance definition
133
137
  # for `class_name`, or nil when the class is unknown or its
134
138
  # definition cannot be built (RBS may raise on broken hierarchies;
135
139
  # we fail-soft and return nil so the caller can fall back).
140
+ #
141
+ # When `cache_store` is set, the loader fetches the per-class
142
+ # definition through {Cache::RbsInstanceDefinitions.fetch} so
143
+ # subsequent runs (and other loaders sharing the same Store)
144
+ # skip the `RBS::DefinitionBuilder.build_instance` step.
145
+ # In-memory `@instance_definition_cache` keeps the per-process
146
+ # short-circuit on top.
136
147
  def instance_definition(class_name)
137
148
  key = class_name.to_s
138
149
  return @instance_definition_cache[key] if @instance_definition_cache.key?(key)
139
150
 
140
- @instance_definition_cache[key] = build_instance_definition(class_name)
151
+ @instance_definition_cache[key] = if cache_store
152
+ cached_instance_definition(class_name)
153
+ else
154
+ build_instance_definition(class_name)
155
+ end
156
+ end
157
+
158
+ # Public uncached accessor used by the cache producer
159
+ # ({Rigor::Cache::RbsInstanceDefinitions}). Avoids the
160
+ # `private_method_called` round-trip a `loader.send(...)`
161
+ # callsite would require.
162
+ def uncached_instance_definition(class_name)
163
+ build_instance_definition(class_name)
141
164
  end
142
165
 
143
166
  # @return [RBS::Definition::Method, nil]
@@ -153,11 +176,25 @@ module Rigor
153
176
  # definition are the *class methods* of `class_name`, including
154
177
  # those inherited from `Class` and `Module` for class types.
155
178
  # Returns nil for unknown names and on RBS build errors (fail-soft).
179
+ #
180
+ # When `cache_store` is set, the loader fetches the per-class
181
+ # singleton definition through
182
+ # {Cache::RbsSingletonDefinitions.fetch}; the same caching
183
+ # discipline as {#instance_definition}.
156
184
  def singleton_definition(class_name)
157
185
  key = class_name.to_s
158
186
  return @singleton_definition_cache[key] if @singleton_definition_cache.key?(key)
159
187
 
160
- @singleton_definition_cache[key] = build_singleton_definition(class_name)
188
+ @singleton_definition_cache[key] = if cache_store
189
+ cached_singleton_definition(class_name)
190
+ else
191
+ build_singleton_definition(class_name)
192
+ end
193
+ end
194
+
195
+ # Public uncached accessor used by the cache producer.
196
+ def uncached_singleton_definition(class_name)
197
+ build_singleton_definition(class_name)
161
198
  end
162
199
 
163
200
  # @return [RBS::Definition::Method, nil] the class method on
@@ -214,7 +251,7 @@ module Rigor
214
251
  # should keep using {#constant_type} for point lookups.
215
252
  def constant_names
216
253
  env.constant_decls.keys.map(&:to_s)
217
- rescue StandardError
254
+ rescue ::RBS::BaseError
218
255
  []
219
256
  end
220
257
 
@@ -229,8 +266,8 @@ module Rigor
229
266
  env.constant_decls.each do |rbs_name, entry|
230
267
  yield rbs_name.to_s, entry
231
268
  end
232
- rescue StandardError
233
- # fail-soft: a broken environment yields no entries.
269
+ rescue ::RBS::BaseError
270
+ # fail-soft: a broken RBS environment yields no entries.
234
271
  end
235
272
 
236
273
  # Slice A constant-value lookup. Returns the translated
@@ -258,7 +295,7 @@ module Rigor
258
295
  else
259
296
  translate_constant_decl(rbs_name)
260
297
  end
261
- rescue StandardError
298
+ rescue ::RBS::BaseError
262
299
  nil
263
300
  end
264
301
 
@@ -290,7 +327,7 @@ module Rigor
290
327
  return false unless rbs_name
291
328
 
292
329
  known_class_names_set.include?(rbs_name.to_s)
293
- rescue StandardError
330
+ rescue ::RBS::BaseError
294
331
  false
295
332
  end
296
333
 
@@ -311,6 +348,45 @@ module Rigor
311
348
  Cache::RbsEnvironment.fetch(loader: self, store: cache_store)
312
349
  end
313
350
 
351
+ # Per-process Hash<String, RBS::Definition> for the instance
352
+ # side. Loaded once on first miss through the
353
+ # {Cache::RbsInstanceDefinitions} producer (single Marshal
354
+ # blob); subsequent calls are pure Hash lookups. Cold runs
355
+ # build every known class once and persist; warm runs (and
356
+ # other loaders sharing the same Store) skip the
357
+ # `RBS::DefinitionBuilder.build_instance` work entirely.
358
+ def cached_instance_definition(class_name)
359
+ instance_definitions_table[normalise_class_key(class_name)]
360
+ end
361
+
362
+ def instance_definitions_table
363
+ @state[:instance_definitions_table] ||= begin
364
+ require_relative "../cache/rbs_instance_definitions"
365
+ Cache::RbsInstanceDefinitions.fetch(loader: self, store: cache_store)
366
+ end
367
+ end
368
+
369
+ def cached_singleton_definition(class_name)
370
+ singleton_definitions_table[normalise_class_key(class_name)]
371
+ end
372
+
373
+ def singleton_definitions_table
374
+ @state[:singleton_definitions_table] ||= begin
375
+ require_relative "../cache/rbs_instance_definitions"
376
+ Cache::RbsSingletonDefinitions.fetch(loader: self, store: cache_store)
377
+ end
378
+ end
379
+
380
+ # The cache producers persist class names in
381
+ # `RBS::TypeName#to_s` form (top-level prefixed
382
+ # `"::Hash"`); plain-name lookups (`"Hash"`) normalise
383
+ # before the Hash query so callers stay agnostic to the
384
+ # prefix.
385
+ def normalise_class_key(class_name)
386
+ s = class_name.to_s
387
+ s.start_with?("::") ? s : "::#{s}"
388
+ end
389
+
314
390
  def builder
315
391
  @state[:builder] ||= RBS::DefinitionBuilder.new(env: env)
316
392
  end
@@ -325,7 +401,7 @@ module Rigor
325
401
  return nil unless env.class_decls.key?(rbs_name)
326
402
 
327
403
  builder.build_instance(rbs_name)
328
- rescue StandardError
404
+ rescue ::RBS::BaseError
329
405
  nil
330
406
  end
331
407
 
@@ -335,7 +411,7 @@ module Rigor
335
411
  return nil unless env.class_decls.key?(rbs_name)
336
412
 
337
413
  builder.build_singleton(rbs_name)
338
- rescue StandardError
414
+ rescue ::RBS::BaseError
339
415
  nil
340
416
  end
341
417
 
@@ -345,7 +421,7 @@ module Rigor
345
421
 
346
422
  s = "::#{s}" unless s.start_with?("::")
347
423
  RBS::TypeName.parse(s)
348
- rescue StandardError
424
+ rescue ::RBS::BaseError
349
425
  nil
350
426
  end
351
427
 
@@ -358,7 +434,7 @@ module Rigor
358
434
  # them under one map post-resolution. Aliases live in their
359
435
  # own table.
360
436
  env.class_decls.key?(rbs_name) || env.class_alias_decls.key?(rbs_name)
361
- rescue StandardError
437
+ rescue ::RBS::BaseError
362
438
  false
363
439
  end
364
440
  end
@@ -41,7 +41,7 @@ module Rigor
41
41
  prism rbs
42
42
  ].freeze
43
43
 
44
- attr_reader :class_registry, :rbs_loader
44
+ attr_reader :class_registry, :rbs_loader, :plugin_registry
45
45
 
46
46
  # @param class_registry [Rigor::Environment::ClassRegistry]
47
47
  # @param rbs_loader [Rigor::Environment::RbsLoader, nil] when nil the
@@ -50,9 +50,17 @@ module Rigor
50
50
  # wires the shared core loader, which is itself lazy: requesting an
51
51
  # environment instance does NOT load RBS until a method or class
52
52
  # query actually consults the loader.
53
- def initialize(class_registry: ClassRegistry.default, rbs_loader: nil)
53
+ # @param plugin_registry [Rigor::Plugin::Registry, nil] v0.1.1
54
+ # Track 2 slice 7. The per-run plugin registry the
55
+ # inference engine consults at call sites for plugin
56
+ # `#flow_contribution_for` overrides. When nil (the
57
+ # default), no plugin-level return-type contribution
58
+ # participates — useful for tests, the `Environment.default`
59
+ # facade, and analyses that don't load plugins.
60
+ def initialize(class_registry: ClassRegistry.default, rbs_loader: nil, plugin_registry: nil)
54
61
  @class_registry = class_registry
55
62
  @rbs_loader = rbs_loader
63
+ @plugin_registry = plugin_registry
56
64
  freeze
57
65
  end
58
66
 
@@ -82,7 +90,7 @@ module Rigor
82
90
  # reflection artefacts) consult the cache. Pass `nil` (the
83
91
  # default) to skip caching for this environment.
84
92
  # @return [Rigor::Environment]
85
- def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil)
93
+ def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil, plugin_registry: nil)
86
94
  resolved_paths = signature_paths || default_signature_paths(root)
87
95
  merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
88
96
  loader = RbsLoader.new(
@@ -90,7 +98,7 @@ module Rigor
90
98
  signature_paths: resolved_paths,
91
99
  cache_store: cache_store
92
100
  )
93
- new(rbs_loader: loader)
101
+ new(rbs_loader: loader, plugin_registry: plugin_registry)
94
102
  end
95
103
 
96
104
  private
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class FlowContribution
5
+ # Records a contradiction between two or more flow contributions
6
+ # detected during {Merger.merge}. Carried on {MergeResult#conflicts}
7
+ # so the analyzer / formatter can surface a `:contribution_merge`
8
+ # diagnostic per ADR-2 § "Plugin Contribution Merging".
9
+ #
10
+ # ADR-2 § "Plugin Contribution Merging" rules out first-wins /
11
+ # last-wins behaviour: when contributions conflict, both sources
12
+ # are reported and the merger falls back to the nearest non-
13
+ # conflicting higher-tier (or default) value for the affected
14
+ # `(target, edge, kind)` slot. The conflict object is the
15
+ # carrier of that report.
16
+ #
17
+ # Slice-3 conflict reasons:
18
+ #
19
+ # - `:return_type_collapse` — two return-type contributions
20
+ # intersect to `bot`.
21
+ # - `:exceptional_disagreement` — two contributions assert
22
+ # incompatible non-`nil` exceptional effects.
23
+ # - `:lower_tier_contradiction` — a lower-tier contribution
24
+ # would weaken or contradict a higher-tier proof.
25
+ CONFLICT_VALID_REASONS = %i[
26
+ return_type_collapse
27
+ exceptional_disagreement
28
+ lower_tier_contradiction
29
+ ].freeze
30
+
31
+ Conflict = Data.define(:target, :edge, :kind, :reason, :provenances, :message) do
32
+ def initialize(target:, edge:, kind:, reason:, provenances:, message:) # rubocop:disable Metrics/ParameterLists
33
+ unless CONFLICT_VALID_REASONS.include?(reason)
34
+ raise ArgumentError,
35
+ "FlowContribution::Conflict reason must be one of " \
36
+ "#{CONFLICT_VALID_REASONS.inspect}, got #{reason.inspect}"
37
+ end
38
+
39
+ super(target: target, edge: edge, kind: kind, reason: reason,
40
+ provenances: provenances.dup.freeze, message: message.to_s.dup.freeze)
41
+ end
42
+
43
+ def to_h
44
+ {
45
+ "target" => target.to_s,
46
+ "edge" => edge.to_s,
47
+ "kind" => kind.to_s,
48
+ "reason" => reason.to_s,
49
+ "sources" => provenances.map { |p| p.respond_to?(:to_h) ? p.to_h : p.to_s },
50
+ "message" => message
51
+ }
52
+ end
53
+
54
+ # ADR-7 § "Slice 5-C" — converts the conflict into a
55
+ # `Rigor::Analysis::Diagnostic` for the run result.
56
+ # Carries `source_family: :contribution_merge` so the
57
+ # qualified-rule formatter (slice 5 formatter half,
58
+ # `ef730b2`) prefixes the rule id with
59
+ # `contribution_merge.` and the JSON output side-bands
60
+ # `source_family` + `rule` for plugin attribution.
61
+ #
62
+ # The `rule` identifier is the kebab-case form of the
63
+ # conflict reason (`return_type_collapse` →
64
+ # `return-type-collapse`, etc.) so the qualified rule
65
+ # reads `[contribution_merge.return-type-collapse]` in
66
+ # the standard text stream.
67
+ def to_diagnostic(path:, line:, column:, severity: :error)
68
+ require_relative "../analysis/diagnostic" unless defined?(Rigor::Analysis::Diagnostic)
69
+ Rigor::Analysis::Diagnostic.new(
70
+ path: path,
71
+ line: line,
72
+ column: column,
73
+ message: message,
74
+ severity: severity,
75
+ rule: reason.to_s.tr("_", "-"),
76
+ source_family: :contribution_merge
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
@@ -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