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.
- checksums.yaml +4 -4
- data/README.md +45 -2
- data/data/builtins/ruby_core/array.yml +6 -6
- data/data/builtins/ruby_core/hash.yml +1 -1
- data/data/builtins/ruby_core/io.yml +3 -3
- data/data/builtins/ruby_core/numeric.yml +1 -1
- data/data/builtins/ruby_core/pathname.yml +100 -100
- data/data/builtins/ruby_core/proc.yml +1 -1
- data/data/builtins/ruby_core/time.yml +3 -3
- data/lib/rigor/analysis/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +269 -7
- data/lib/rigor/builtins/regex_refinement.rb +104 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +1 -1
- data/lib/rigor/cache/rbs_constant_table.rb +2 -2
- data/lib/rigor/cache/rbs_descriptor.rb +2 -0
- data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
- data/lib/rigor/cache/store.rb +2 -0
- data/lib/rigor/cli/type_of_command.rb +3 -3
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli.rb +20 -7
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +286 -15
- data/lib/rigor/environment/rbs_loader.rb +89 -13
- data/lib/rigor/environment.rb +12 -4
- data/lib/rigor/flow_contribution/conflict.rb +81 -0
- data/lib/rigor/flow_contribution/element.rb +53 -0
- data/lib/rigor/flow_contribution/fact.rb +88 -0
- data/lib/rigor/flow_contribution/merge_result.rb +67 -0
- data/lib/rigor/flow_contribution/merger.rb +275 -0
- data/lib/rigor/flow_contribution.rb +51 -0
- data/lib/rigor/inference/block_parameter_binder.rb +15 -0
- data/lib/rigor/inference/expression_typer.rb +87 -6
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +31 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +136 -9
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +68 -2
- data/lib/rigor/inference/method_dispatcher.rb +50 -1
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +246 -127
- data/lib/rigor/inference/scope_indexer.rb +124 -16
- data/lib/rigor/inference/statement_evaluator.rb +406 -37
- data/lib/rigor/plugin/access_denied_error.rb +24 -0
- data/lib/rigor/plugin/base.rb +284 -0
- data/lib/rigor/plugin/fact_store.rb +92 -0
- data/lib/rigor/plugin/io_boundary.rb +102 -0
- data/lib/rigor/plugin/load_error.rb +35 -0
- data/lib/rigor/plugin/loader.rb +307 -0
- data/lib/rigor/plugin/manifest.rb +203 -0
- data/lib/rigor/plugin/registry.rb +50 -0
- data/lib/rigor/plugin/services.rb +77 -0
- data/lib/rigor/plugin/trust_policy.rb +99 -0
- data/lib/rigor/plugin.rb +62 -0
- data/lib/rigor/rbs_extended.rb +57 -9
- data/lib/rigor/reflection.rb +2 -2
- data/lib/rigor/trinary.rb +1 -1
- data/lib/rigor/type/integer_range.rb +6 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +7 -0
- data/sig/rigor/environment.rbs +10 -3
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +1 -0
- data/sig/rigor/type.rbs +7 -0
- data/sig/rigor.rbs +8 -2
- 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
|
|
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] =
|
|
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] =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
437
|
+
rescue ::RBS::BaseError
|
|
362
438
|
false
|
|
363
439
|
end
|
|
364
440
|
end
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -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
|
-
|
|
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
|