rigortype 0.1.0 → 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 +7 -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/runner.rb +88 -5
- data/lib/rigor/builtins/regex_refinement.rb +104 -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 +11 -4
- data/lib/rigor/configuration.rb +177 -10
- data/lib/rigor/environment.rb +12 -4
- data/lib/rigor/inference/expression_typer.rb +3 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +31 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +43 -2
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +68 -2
- data/lib/rigor/inference/method_dispatcher.rb +50 -1
- data/lib/rigor/inference/narrowing.rb +150 -6
- data/lib/rigor/inference/scope_indexer.rb +49 -15
- data/lib/rigor/inference/statement_evaluator.rb +29 -0
- data/lib/rigor/plugin/base.rb +43 -0
- data/lib/rigor/plugin/fact_store.rb +92 -0
- data/lib/rigor/plugin/load_error.rb +14 -2
- data/lib/rigor/plugin/loader.rb +116 -0
- data/lib/rigor/plugin/manifest.rb +75 -6
- data/lib/rigor/plugin/services.rb +14 -2
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/trinary.rb +1 -1
- data/lib/rigor/type/integer_range.rb +6 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor.rbs +8 -2
- metadata +3 -1
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -111,6 +111,49 @@ module Rigor
|
|
|
111
111
|
nil
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
+
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
115
|
+
# slice 7 — per-call return-type contribution hook. When
|
|
116
|
+
# the inference engine dispatches a `Prism::CallNode` and
|
|
117
|
+
# neither the precision tiers nor RBS resolve a result,
|
|
118
|
+
# `MethodDispatcher` consults each loaded plugin via this
|
|
119
|
+
# hook ahead of `RbsDispatch`. Plugins that override the
|
|
120
|
+
# default return a {Rigor::FlowContribution} bundle whose
|
|
121
|
+
# `return_type` slot pins the call site's result type.
|
|
122
|
+
#
|
|
123
|
+
# Default returns nil — plugins that don't refine return
|
|
124
|
+
# types skip the override. Failures are isolated: a hook
|
|
125
|
+
# that raises gets its contribution dropped silently for
|
|
126
|
+
# this call so the rest of the dispatch chain continues.
|
|
127
|
+
def flow_contribution_for(call_node:, scope:) # rubocop:disable Lint/UnusedMethodArgument
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# ADR-9 slice 3 — per-run preparation hook. The runner
|
|
132
|
+
# invokes `#prepare(services)` on every loaded plugin once
|
|
133
|
+
# per `Analysis::Runner.run`, after `#init` has run on every
|
|
134
|
+
# plugin and before any `#diagnostics_for_file` call.
|
|
135
|
+
# Plugins use this hook to compute and publish facts other
|
|
136
|
+
# plugins consume:
|
|
137
|
+
#
|
|
138
|
+
# def prepare(services)
|
|
139
|
+
# services.fact_store.publish(
|
|
140
|
+
# plugin_id: manifest.id, name: :model_index, value: model_index
|
|
141
|
+
# )
|
|
142
|
+
# end
|
|
143
|
+
#
|
|
144
|
+
# Default no-op so plugins without facts to publish leave
|
|
145
|
+
# `#prepare` unimplemented. Failures isolate as
|
|
146
|
+
# `:plugin_loader runtime-error` diagnostics; a plugin that
|
|
147
|
+
# raises in `#prepare` has its facts considered un-published
|
|
148
|
+
# and downstream consumers see `nil` from `fact_store.read`.
|
|
149
|
+
#
|
|
150
|
+
# Slice 3 calls plugins in registration order. ADR-9 slice 5
|
|
151
|
+
# introduces topological ordering by `consumes:` so producers
|
|
152
|
+
# always run before consumers.
|
|
153
|
+
def prepare(services) # rubocop:disable Lint/UnusedMethodArgument
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
114
157
|
# ADR-7 § "Slice 5-A" — per-file diagnostic emission hook.
|
|
115
158
|
# Override in plugin subclasses to return an array of
|
|
116
159
|
# `Rigor::Analysis::Diagnostic` rows for the analysed file.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
# Per-run cross-plugin fact store. ADR-9 § "Plugin::FactStore".
|
|
6
|
+
#
|
|
7
|
+
# A plugin publishes typed `(plugin_id, name) -> value` tuples
|
|
8
|
+
# in its `Plugin::Base#prepare(services)` hook (slice 3); other
|
|
9
|
+
# plugins read them in `#diagnostics_for_file` via
|
|
10
|
+
# `services.fact_store.read(plugin_id:, name:)`. The store is
|
|
11
|
+
# constructed fresh at the start of every `Analysis::Runner.run`
|
|
12
|
+
# and discarded at the end — caching the underlying expensive
|
|
13
|
+
# computation is the producer's job (`Plugin::Base.producer`);
|
|
14
|
+
# the FactStore just publishes a *reference* to that
|
|
15
|
+
# already-cached result.
|
|
16
|
+
#
|
|
17
|
+
# `(plugin_id, name)` is a unique key. A second `publish` with
|
|
18
|
+
# the same value is a no-op (`==` comparison); a second
|
|
19
|
+
# `publish` with a different value raises {Conflict}. Since
|
|
20
|
+
# `plugin_id` namespaces the key, a real conflict only happens
|
|
21
|
+
# when a single plugin publishes twice with differing values —
|
|
22
|
+
# the conflict signals a plugin-author bug, never a load-time
|
|
23
|
+
# interaction between unrelated plugins.
|
|
24
|
+
class FactStore
|
|
25
|
+
Fact = Data.define(:plugin_id, :name, :value)
|
|
26
|
+
|
|
27
|
+
class Conflict < StandardError
|
|
28
|
+
attr_reader :plugin_id, :name, :existing, :incoming
|
|
29
|
+
|
|
30
|
+
def initialize(plugin_id:, name:, existing:, incoming:)
|
|
31
|
+
@plugin_id = plugin_id
|
|
32
|
+
@name = name
|
|
33
|
+
@existing = existing
|
|
34
|
+
@incoming = incoming
|
|
35
|
+
super(
|
|
36
|
+
"fact store conflict: plugin #{plugin_id.inspect} published " \
|
|
37
|
+
"two different values for #{name.inspect} " \
|
|
38
|
+
"(existing: #{existing.inspect}, incoming: #{incoming.inspect})"
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize
|
|
44
|
+
@facts = {}
|
|
45
|
+
@mutex = Mutex.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Writes a `(plugin_id, name) -> value` triple. Idempotent if
|
|
49
|
+
# the same value is published twice (`==`); raises
|
|
50
|
+
# {Conflict} if the values differ.
|
|
51
|
+
#
|
|
52
|
+
# @param plugin_id [String] producing plugin's manifest id.
|
|
53
|
+
# @param name [Symbol, String] fact name (canonicalised to
|
|
54
|
+
# Symbol for lookup).
|
|
55
|
+
# @param value [Object] frozen-shape value object the
|
|
56
|
+
# producer chose to publish. The value is stored as-is.
|
|
57
|
+
def publish(plugin_id:, name:, value:)
|
|
58
|
+
plugin_id = plugin_id.to_s
|
|
59
|
+
name = name.to_sym
|
|
60
|
+
@mutex.synchronize do
|
|
61
|
+
existing = @facts[[plugin_id, name]]
|
|
62
|
+
if existing && existing.value != value
|
|
63
|
+
raise Conflict.new(plugin_id: plugin_id, name: name, existing: existing.value, incoming: value)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@facts[[plugin_id, name]] = Fact.new(plugin_id: plugin_id, name: name, value: value)
|
|
67
|
+
end
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [Object, nil] the published value, or `nil` when no
|
|
72
|
+
# fact is registered. Reads do NOT establish a dependency —
|
|
73
|
+
# `manifest(consumes:)` (slice 4) is the dependency
|
|
74
|
+
# declaration mechanism.
|
|
75
|
+
def read(plugin_id:, name:)
|
|
76
|
+
fact = @mutex.synchronize { @facts[[plugin_id.to_s, name.to_sym]] }
|
|
77
|
+
fact&.value
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [Boolean] whether a fact is registered.
|
|
81
|
+
def published?(plugin_id:, name:)
|
|
82
|
+
@mutex.synchronize { @facts.key?([plugin_id.to_s, name.to_sym]) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @yield [Fact] every published fact in publication order.
|
|
86
|
+
def each_fact(&)
|
|
87
|
+
snapshot = @mutex.synchronize { @facts.values }
|
|
88
|
+
snapshot.each(&)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -11,12 +11,24 @@ module Rigor
|
|
|
11
11
|
# to be isolated at the analyzer boundary; this class is the
|
|
12
12
|
# carrier for that contract on the loading side.
|
|
13
13
|
class LoadError < StandardError
|
|
14
|
-
attr_reader :plugin_ref, :cause_class
|
|
14
|
+
attr_reader :plugin_ref, :cause_class, :reason
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# ADR-9 slice 5 introduces two new reason codes alongside the
|
|
17
|
+
# implicit "load failure" used for require / configuration /
|
|
18
|
+
# init failures:
|
|
19
|
+
#
|
|
20
|
+
# - `:missing-producer` — a non-optional `manifest(consumes:)`
|
|
21
|
+
# entry names a `(plugin_id, name)` no loaded plugin
|
|
22
|
+
# produces.
|
|
23
|
+
# - `:dependency-cycle` — the consumes graph forms a cycle.
|
|
24
|
+
#
|
|
25
|
+
# Older callers omit `reason:` and the field defaults to nil
|
|
26
|
+
# (the legacy "load failure" envelope).
|
|
27
|
+
def initialize(message, plugin_ref:, cause: nil, reason: nil)
|
|
17
28
|
super(message)
|
|
18
29
|
@plugin_ref = plugin_ref
|
|
19
30
|
@cause_class = cause&.class
|
|
31
|
+
@reason = reason&.to_sym
|
|
20
32
|
end
|
|
21
33
|
end
|
|
22
34
|
end
|
data/lib/rigor/plugin/loader.rb
CHANGED
|
@@ -63,6 +63,15 @@ module Rigor
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# ADR-9 slice 5 — topological sort by `manifest(consumes:)`
|
|
67
|
+
# so producers run before consumers, plus early
|
|
68
|
+
# `missing-producer` validation. Cycles surface as
|
|
69
|
+
# `dependency-cycle` LoadErrors. When validation fails, the
|
|
70
|
+
# offending plugin(s) drop from the returned plugins list
|
|
71
|
+
# and the LoadError surfaces alongside any earlier failure.
|
|
72
|
+
plugins, sort_errors = topo_sort_plugins(plugins)
|
|
73
|
+
load_errors.concat(sort_errors)
|
|
74
|
+
|
|
66
75
|
Registry.new(plugins: plugins, load_errors: load_errors)
|
|
67
76
|
end
|
|
68
77
|
|
|
@@ -186,6 +195,113 @@ module Rigor
|
|
|
186
195
|
rescue StandardError
|
|
187
196
|
plugin_class.to_s
|
|
188
197
|
end
|
|
198
|
+
|
|
199
|
+
# ADR-9 slice 5 — topological sort of plugins by their
|
|
200
|
+
# `manifest(consumes:)` declarations. Returns `[sorted_plugins,
|
|
201
|
+
# load_errors]`. Determinism: when no dependency relation
|
|
202
|
+
# forces an order, plugins are visited alphabetically by
|
|
203
|
+
# manifest id. A non-optional consume of a `(plugin_id, name)`
|
|
204
|
+
# whose producer is missing emits a `:missing-producer`
|
|
205
|
+
# LoadError and drops the consumer; cycles emit a
|
|
206
|
+
# `:dependency-cycle` LoadError naming the offending chain.
|
|
207
|
+
def topo_sort_plugins(plugins)
|
|
208
|
+
# If no plugin opts into the cross-plugin API the loader's
|
|
209
|
+
# legacy configuration-order contract is preserved
|
|
210
|
+
# unchanged. Topo sort and missing-producer validation only
|
|
211
|
+
# run when at least one plugin declares `consumes:`.
|
|
212
|
+
return [plugins, []] unless plugins.any? { |p| p.manifest.consumes.any? }
|
|
213
|
+
|
|
214
|
+
index = plugins.to_h { |plugin| [plugin.manifest.id, plugin] }
|
|
215
|
+
errors = validate_missing_producers(plugins, index)
|
|
216
|
+
sortable = plugins.reject { |p| errors.any? { |e| e.plugin_ref == p.manifest.id } }
|
|
217
|
+
config_order = plugins.each_with_index.to_h { |plugin, i| [plugin.manifest.id, i] }
|
|
218
|
+
|
|
219
|
+
sort_in_topo_order(sortable, index, errors, config_order)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def validate_missing_producers(plugins, index)
|
|
223
|
+
errors = []
|
|
224
|
+
plugins.each do |plugin|
|
|
225
|
+
plugin.manifest.consumes.each do |consume|
|
|
226
|
+
next if consume.optional
|
|
227
|
+
next if index.key?(consume.plugin_id) && producer_provides?(index[consume.plugin_id], consume.name)
|
|
228
|
+
|
|
229
|
+
errors << LoadError.new(
|
|
230
|
+
"plugin #{plugin.manifest.id.inspect} consumes " \
|
|
231
|
+
"#{consume.plugin_id.inspect}/#{consume.name} but no loaded plugin " \
|
|
232
|
+
"with that id declares `produces: [#{consume.name.inspect}]`",
|
|
233
|
+
plugin_ref: plugin.manifest.id,
|
|
234
|
+
reason: :"missing-producer"
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
errors
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def producer_provides?(producer, name)
|
|
242
|
+
producer.manifest.produces.include?(name)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Kahn's algorithm with `Configuration#plugins`-order
|
|
246
|
+
# tie-break. Edges go from producer -> consumer (producer
|
|
247
|
+
# must visit first). When two plugins are simultaneously
|
|
248
|
+
# ready, the configuration-order index decides the visit
|
|
249
|
+
# order — preserves the v0.1.0 legacy contract for plugins
|
|
250
|
+
# without dependencies.
|
|
251
|
+
def sort_in_topo_order(plugins, index, errors, config_order)
|
|
252
|
+
in_degree, forward = build_consumes_graph(plugins, index, errors)
|
|
253
|
+
ordered, cycle_errors = kahn_walk(plugins, in_degree, forward, config_order)
|
|
254
|
+
[ordered, errors + cycle_errors]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def build_consumes_graph(plugins, index, errors)
|
|
258
|
+
in_degree = Hash.new(0)
|
|
259
|
+
forward = Hash.new { |h, k| h[k] = [] }
|
|
260
|
+
plugins.each do |consumer|
|
|
261
|
+
consumer.manifest.consumes.each do |consume|
|
|
262
|
+
next unless index.key?(consume.plugin_id)
|
|
263
|
+
next if errors.any? { |e| e.plugin_ref == consume.plugin_id }
|
|
264
|
+
|
|
265
|
+
forward[consume.plugin_id] << consumer.manifest.id
|
|
266
|
+
in_degree[consumer.manifest.id] += 1
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
[in_degree, forward]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def kahn_walk(plugins, in_degree, forward, config_order)
|
|
273
|
+
order = ->(plugin) { config_order.fetch(plugin.manifest.id, Float::INFINITY) }
|
|
274
|
+
ready = plugins.select { |p| in_degree[p.manifest.id].zero? }.sort_by(&order)
|
|
275
|
+
result = kahn_collect(plugins, in_degree, forward, ready, order)
|
|
276
|
+
|
|
277
|
+
return [result, []] if result.size == plugins.size
|
|
278
|
+
|
|
279
|
+
cycled = plugins - result
|
|
280
|
+
[result, [dependency_cycle_error(cycled)]]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def kahn_collect(plugins, in_degree, forward, ready, order)
|
|
284
|
+
result = []
|
|
285
|
+
until ready.empty?
|
|
286
|
+
plugin = ready.shift
|
|
287
|
+
result << plugin
|
|
288
|
+
forward[plugin.manifest.id].each do |consumer_id|
|
|
289
|
+
in_degree[consumer_id] -= 1
|
|
290
|
+
ready << plugins.find { |p| p.manifest.id == consumer_id } if in_degree[consumer_id].zero?
|
|
291
|
+
end
|
|
292
|
+
ready.sort_by!(&order)
|
|
293
|
+
end
|
|
294
|
+
result
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def dependency_cycle_error(cycled)
|
|
298
|
+
ids = cycled.map { |p| p.manifest.id }.sort
|
|
299
|
+
LoadError.new(
|
|
300
|
+
"plugin dependency cycle through `manifest(consumes:)`: #{ids.inspect}",
|
|
301
|
+
plugin_ref: ids.first,
|
|
302
|
+
reason: :"dependency-cycle"
|
|
303
|
+
)
|
|
304
|
+
end
|
|
189
305
|
end
|
|
190
306
|
end
|
|
191
307
|
end
|
|
@@ -11,7 +11,7 @@ module Rigor
|
|
|
11
11
|
# The fields are pinned by ADR-2 § "Registration, Configuration,
|
|
12
12
|
# and Caching"; the v0.1.0 plugin contract surface treats this
|
|
13
13
|
# struct as the public manifest shape.
|
|
14
|
-
class Manifest
|
|
14
|
+
class Manifest # rubocop:disable Metrics/ClassLength
|
|
15
15
|
# Same regex {Rigor::Cache::Store::VALID_PRODUCER_ID} uses,
|
|
16
16
|
# so plugin ids round-trip through cache producer ids and
|
|
17
17
|
# `plugin.<id>.<rule>` diagnostic identifiers without escape.
|
|
@@ -23,23 +23,51 @@ module Rigor
|
|
|
23
23
|
# the v0.1.0 protocol slices need them.
|
|
24
24
|
VALID_VALUE_KINDS = %i[string boolean integer array hash any].freeze
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# ADR-9 slice 4 — declared cross-plugin fact dependencies.
|
|
27
|
+
# `produces:` lists the names this plugin publishes through
|
|
28
|
+
# its `#prepare(services)` hook. `consumes:` lists the
|
|
29
|
+
# `(plugin_id, name)` pairs this plugin reads from
|
|
30
|
+
# `services.fact_store`. The loader uses both for
|
|
31
|
+
# topological sort + missing-producer detection (slice 5);
|
|
32
|
+
# slice 4 carries the declarations on the manifest but the
|
|
33
|
+
# loader does not yet enforce them.
|
|
34
|
+
Consumption = Data.define(:plugin_id, :name, :optional) do
|
|
35
|
+
def initialize(plugin_id:, name:, optional: false)
|
|
36
|
+
super(plugin_id: plugin_id.to_s, name: name.to_sym, optional: optional ? true : false)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
attr_reader :id, :version, :description, :protocols, :config_schema, :produces, :consumes
|
|
41
|
+
|
|
42
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
43
|
+
id:, version:,
|
|
44
|
+
description: nil, protocols: [], config_schema: {},
|
|
45
|
+
produces: [], consumes: []
|
|
46
|
+
)
|
|
29
47
|
validate_id!(id)
|
|
30
48
|
validate_version!(version)
|
|
31
49
|
validate_protocols!(protocols)
|
|
32
50
|
validate_config_schema!(config_schema)
|
|
51
|
+
validate_produces!(produces)
|
|
52
|
+
|
|
53
|
+
assign_fields(id, version, description, protocols, config_schema, produces, consumes)
|
|
54
|
+
freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
33
58
|
|
|
59
|
+
def assign_fields(id, version, description, protocols, config_schema, produces, consumes) # rubocop:disable Metrics/ParameterLists
|
|
34
60
|
@id = id.dup.freeze
|
|
35
61
|
@version = version.dup.freeze
|
|
36
62
|
@description = description.nil? ? nil : description.to_s.dup.freeze
|
|
37
63
|
@protocols = protocols.map(&:to_sym).freeze
|
|
38
64
|
@config_schema = config_schema.to_h { |k, v| [k.to_s.dup.freeze, v.to_sym] }.freeze
|
|
39
|
-
|
|
40
|
-
|
|
65
|
+
@produces = produces.map(&:to_sym).freeze
|
|
66
|
+
@consumes = coerce_consumes(consumes)
|
|
41
67
|
end
|
|
42
68
|
|
|
69
|
+
public
|
|
70
|
+
|
|
43
71
|
# Validates the user-supplied plugin config block against this
|
|
44
72
|
# manifest's `config_schema`. Returns an array of human-readable
|
|
45
73
|
# error strings (empty when the config is valid). Slice 1 checks
|
|
@@ -69,7 +97,9 @@ module Rigor
|
|
|
69
97
|
"version" => version,
|
|
70
98
|
"description" => description,
|
|
71
99
|
"protocols" => protocols.map(&:to_s),
|
|
72
|
-
"config_schema" => config_schema.to_h { |k, v| [k, v.to_s] }
|
|
100
|
+
"config_schema" => config_schema.to_h { |k, v| [k, v.to_s] },
|
|
101
|
+
"produces" => produces.map(&:to_s),
|
|
102
|
+
"consumes" => consumes.map { |c| consumption_hash(c) }
|
|
73
103
|
}
|
|
74
104
|
end
|
|
75
105
|
|
|
@@ -129,6 +159,45 @@ module Rigor
|
|
|
129
159
|
else false
|
|
130
160
|
end
|
|
131
161
|
end
|
|
162
|
+
|
|
163
|
+
def validate_produces!(produces)
|
|
164
|
+
return if produces.is_a?(Array) && produces.all? { |p| p.is_a?(Symbol) || p.is_a?(String) }
|
|
165
|
+
|
|
166
|
+
raise ArgumentError, "plugin manifest produces must be an Array of Symbol/String, got #{produces.inspect}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def coerce_consumes(consumes)
|
|
170
|
+
unless consumes.is_a?(Array)
|
|
171
|
+
raise ArgumentError, "plugin manifest consumes must be an Array, got #{consumes.inspect}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
consumes.map { |entry| coerce_consumption(entry) }.freeze
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def coerce_consumption(entry)
|
|
178
|
+
case entry
|
|
179
|
+
when Consumption then entry
|
|
180
|
+
when Hash then build_consumption_from_hash(entry)
|
|
181
|
+
else raise ArgumentError,
|
|
182
|
+
"plugin manifest consumes entry must be a Hash or Consumption, got #{entry.inspect}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def consumption_hash(consumption)
|
|
187
|
+
{ "plugin_id" => consumption.plugin_id, "name" => consumption.name.to_s, "optional" => consumption.optional }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def build_consumption_from_hash(entry)
|
|
191
|
+
plugin_id = entry[:plugin_id] || entry["plugin_id"]
|
|
192
|
+
name = entry[:name] || entry["name"]
|
|
193
|
+
optional = entry.key?(:optional) ? entry[:optional] : entry["optional"]
|
|
194
|
+
if plugin_id.nil? || name.nil?
|
|
195
|
+
raise ArgumentError,
|
|
196
|
+
"plugin manifest consumes entry missing plugin_id/name: #{entry.inspect}"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
Consumption.new(plugin_id: plugin_id, name: name, optional: optional || false)
|
|
200
|
+
end
|
|
132
201
|
end
|
|
133
202
|
end
|
|
134
203
|
end
|
|
@@ -31,15 +31,27 @@ module Rigor
|
|
|
31
31
|
# raw `File.read` so reads stay within the trusted scope and
|
|
32
32
|
# feed cache invalidation; ADR-2 § "Plugin Trust and I/O
|
|
33
33
|
# Policy" documents the trust model the boundary enforces.
|
|
34
|
+
#
|
|
35
|
+
# ADR-9 slice 2 adds `fact_store`: the per-run cross-plugin
|
|
36
|
+
# `Plugin::FactStore`. Producer plugins publish their facts
|
|
37
|
+
# in `#prepare(services)` (slice 3); consumer plugins read in
|
|
38
|
+
# `#diagnostics_for_file` via `services.fact_store.read(...)`.
|
|
39
|
+
# A fresh `FactStore` instance is constructed per Services
|
|
40
|
+
# when none is supplied — the runner threads its own instance
|
|
41
|
+
# in once slice 3 wires `#prepare` invocation.
|
|
34
42
|
class Services
|
|
35
|
-
attr_reader :reflection, :type, :configuration, :cache_store, :trust_policy
|
|
43
|
+
attr_reader :reflection, :type, :configuration, :cache_store, :trust_policy, :fact_store
|
|
36
44
|
|
|
37
|
-
def initialize(
|
|
45
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
46
|
+
reflection:, type:, configuration:,
|
|
47
|
+
cache_store: nil, trust_policy: nil, fact_store: nil
|
|
48
|
+
)
|
|
38
49
|
@reflection = reflection
|
|
39
50
|
@type = type
|
|
40
51
|
@configuration = configuration
|
|
41
52
|
@cache_store = cache_store
|
|
42
53
|
@trust_policy = trust_policy || default_trust_policy
|
|
54
|
+
@fact_store = fact_store || FactStore.new
|
|
43
55
|
freeze
|
|
44
56
|
end
|
|
45
57
|
|
data/lib/rigor/plugin.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "plugin/manifest"
|
|
|
4
4
|
require_relative "plugin/access_denied_error"
|
|
5
5
|
require_relative "plugin/trust_policy"
|
|
6
6
|
require_relative "plugin/io_boundary"
|
|
7
|
+
require_relative "plugin/fact_store"
|
|
7
8
|
require_relative "plugin/services"
|
|
8
9
|
require_relative "plugin/base"
|
|
9
10
|
require_relative "plugin/registry"
|
data/lib/rigor/trinary.rb
CHANGED
|
@@ -66,12 +66,16 @@ module Rigor
|
|
|
66
66
|
# `:neg_infinity` directly with an `Integer`.
|
|
67
67
|
def lower
|
|
68
68
|
m = min
|
|
69
|
-
m.is_a?(
|
|
69
|
+
return m if m.is_a?(Integer)
|
|
70
|
+
|
|
71
|
+
-Float::INFINITY
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def upper
|
|
73
75
|
m = max
|
|
74
|
-
m.is_a?(
|
|
76
|
+
return m if m.is_a?(Integer)
|
|
77
|
+
|
|
78
|
+
Float::INFINITY
|
|
75
79
|
end
|
|
76
80
|
|
|
77
81
|
ALIAS_NAMES = {
|
data/lib/rigor/version.rb
CHANGED
data/sig/rigor/environment.rbs
CHANGED
|
@@ -6,11 +6,12 @@ module Rigor
|
|
|
6
6
|
|
|
7
7
|
attr_reader class_registry: ClassRegistry
|
|
8
8
|
attr_reader rbs_loader: RbsLoader?
|
|
9
|
+
attr_reader plugin_registry: untyped?
|
|
9
10
|
|
|
10
11
|
def self.default: () -> Environment
|
|
11
|
-
def self.for_project: (?root: String, ?libraries: Array[String], ?signature_paths: Array[String | _ToPath]?) -> Environment
|
|
12
|
+
def self.for_project: (?root: String, ?libraries: Array[String], ?signature_paths: Array[String | _ToPath]?, ?cache_store: untyped?, ?plugin_registry: untyped?) -> Environment
|
|
12
13
|
|
|
13
|
-
def initialize: (?class_registry: ClassRegistry, ?rbs_loader: RbsLoader?) -> void
|
|
14
|
+
def initialize: (?class_registry: ClassRegistry, ?rbs_loader: RbsLoader?, ?plugin_registry: untyped?) -> void
|
|
14
15
|
def nominal_for_name: (String | Symbol name) -> Type::Nominal?
|
|
15
16
|
def singleton_for_name: (String | Symbol name) -> Type::Singleton?
|
|
16
17
|
def constant_for_name: (String | Symbol name) -> Type::t?
|
data/sig/rigor.rbs
CHANGED
|
@@ -64,8 +64,14 @@ module Rigor
|
|
|
64
64
|
class Runner
|
|
65
65
|
RUBY_GLOB: String
|
|
66
66
|
DEFAULT_CACHE_ROOT: String
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
# `Rigor::Cache::Store` itself is not yet sig-covered (the
|
|
68
|
+
# cache namespace is in `UNSIGNED_NAMESPACES` per
|
|
69
|
+
# `spec/rigor/public_api_drift_spec.rb`), so reference it as
|
|
70
|
+
# `untyped` until the full Cache::Store sig lands. Steep
|
|
71
|
+
# otherwise raises `RBS::UnknownTypeName` for the named type.
|
|
72
|
+
attr_reader cache_store: untyped
|
|
73
|
+
attr_reader plugin_registry: untyped
|
|
74
|
+
def initialize: (configuration: Configuration, ?explain: bool, ?cache_store: untyped, ?plugin_requirer: untyped) -> void
|
|
69
75
|
def run: (?Array[String] paths) -> Result
|
|
70
76
|
end
|
|
71
77
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rigortype
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rigor contributors
|
|
@@ -191,6 +191,7 @@ files:
|
|
|
191
191
|
- lib/rigor/ast.rb
|
|
192
192
|
- lib/rigor/ast/type_node.rb
|
|
193
193
|
- lib/rigor/builtins/imported_refinements.rb
|
|
194
|
+
- lib/rigor/builtins/regex_refinement.rb
|
|
194
195
|
- lib/rigor/cache/descriptor.rb
|
|
195
196
|
- lib/rigor/cache/rbs_class_ancestor_table.rb
|
|
196
197
|
- lib/rigor/cache/rbs_class_type_param_names.rb
|
|
@@ -265,6 +266,7 @@ files:
|
|
|
265
266
|
- lib/rigor/plugin.rb
|
|
266
267
|
- lib/rigor/plugin/access_denied_error.rb
|
|
267
268
|
- lib/rigor/plugin/base.rb
|
|
269
|
+
- lib/rigor/plugin/fact_store.rb
|
|
268
270
|
- lib/rigor/plugin/io_boundary.rb
|
|
269
271
|
- lib/rigor/plugin/load_error.rb
|
|
270
272
|
- lib/rigor/plugin/loader.rb
|