rigortype 0.1.3 → 0.1.4
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 +125 -31
- data/lib/rigor/analysis/check_rules.rb +10 -18
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
- data/lib/rigor/analysis/diagnostic.rb +0 -2
- data/lib/rigor/analysis/fact_store.rb +11 -3
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/runner.rb +114 -3
- data/lib/rigor/builtins/imported_refinements.rb +360 -55
- data/lib/rigor/cache/descriptor.rb +1 -1
- data/lib/rigor/cache/store.rb +1 -1
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +173 -0
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_renderer.rb +1 -1
- data/lib/rigor/cli/type_scan_report.rb +2 -2
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/dependencies.rb +2 -2
- data/lib/rigor/configuration.rb +2 -2
- data/lib/rigor/environment.rb +35 -4
- data/lib/rigor/flow_contribution/conflict.rb +2 -2
- data/lib/rigor/flow_contribution/element.rb +1 -1
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution/merge_result.rb +1 -1
- data/lib/rigor/flow_contribution/merger.rb +3 -3
- data/lib/rigor/flow_contribution.rb +2 -2
- data/lib/rigor/inference/block_parameter_binder.rb +0 -2
- data/lib/rigor/inference/coverage_scanner.rb +1 -1
- data/lib/rigor/inference/expression_typer.rb +67 -11
- data/lib/rigor/inference/fallback.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
- data/lib/rigor/inference/method_dispatcher.rb +146 -2
- data/lib/rigor/inference/method_parameter_binder.rb +1 -3
- data/lib/rigor/inference/narrowing.rb +2 -4
- data/lib/rigor/inference/rbs_type_translator.rb +0 -2
- data/lib/rigor/inference/scope_indexer.rb +14 -9
- data/lib/rigor/inference/statement_evaluator.rb +7 -7
- data/lib/rigor/plugin/io_boundary.rb +0 -2
- data/lib/rigor/plugin/loader.rb +2 -2
- data/lib/rigor/plugin/manifest.rb +30 -9
- data/lib/rigor/plugin/registry.rb +11 -0
- data/lib/rigor/plugin/services.rb +1 -1
- data/lib/rigor/plugin/type_node_resolver.rb +52 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/reporter.rb +91 -0
- data/lib/rigor/rbs_extended.rb +131 -32
- data/lib/rigor/scope.rb +25 -8
- data/lib/rigor/sig_gen/classification.rb +36 -0
- data/lib/rigor/sig_gen/generator.rb +1048 -0
- data/lib/rigor/sig_gen/layout_index.rb +108 -0
- data/lib/rigor/sig_gen/method_candidate.rb +62 -0
- data/lib/rigor/sig_gen/observation_collector.rb +391 -0
- data/lib/rigor/sig_gen/observed_call.rb +62 -0
- data/lib/rigor/sig_gen/path_mapper.rb +116 -0
- data/lib/rigor/sig_gen/renderer.rb +157 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
- data/lib/rigor/sig_gen/write_result.rb +48 -0
- data/lib/rigor/sig_gen/writer.rb +530 -0
- data/lib/rigor/sig_gen.rb +25 -0
- data/lib/rigor/type/bound_method.rb +79 -0
- data/lib/rigor/type/combinator.rb +195 -2
- data/lib/rigor/type/constant.rb +13 -0
- data/lib/rigor/type/hash_shape.rb +0 -2
- data/lib/rigor/type/union.rb +20 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +62 -0
- data/lib/rigor/type_node/identifier.rb +30 -0
- data/lib/rigor/type_node/indexed_access.rb +41 -0
- data/lib/rigor/type_node/integer_literal.rb +29 -0
- data/lib/rigor/type_node/name_scope.rb +52 -0
- data/lib/rigor/type_node/resolver_chain.rb +56 -0
- data/lib/rigor/type_node/string_literal.rb +29 -0
- data/lib/rigor/type_node/symbol_literal.rb +28 -0
- data/lib/rigor/type_node/union.rb +42 -0
- data/lib/rigor/type_node.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
- data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
- data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
- data/sig/rigor/cli/diff_command.rbs +4 -0
- data/sig/rigor/cli/explain_command.rbs +4 -0
- data/sig/rigor/cli/sig_gen_command.rbs +4 -0
- data/sig/rigor/cli/type_scan_command.rbs +3 -0
- data/sig/rigor/environment.rbs +5 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
- data/sig/rigor/inference/builtins.rbs +2 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -0
- data/sig/rigor/plugin/base.rbs +6 -0
- data/sig/rigor/plugin/fact_store.rbs +11 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/load_error.rbs +6 -0
- data/sig/rigor/plugin/loader.rbs +20 -0
- data/sig/rigor/plugin/manifest.rbs +9 -0
- data/sig/rigor/plugin/registry.rbs +3 -0
- data/sig/rigor/plugin/services.rbs +3 -0
- data/sig/rigor/plugin/trust_policy.rbs +4 -0
- data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
- data/sig/rigor/plugin.rbs +8 -0
- data/sig/rigor/scope.rbs +4 -2
- data/sig/rigor/type.rbs +28 -6
- metadata +52 -1
|
@@ -12,6 +12,7 @@ require_relative "method_dispatcher/iterator_dispatch"
|
|
|
12
12
|
require_relative "method_dispatcher/block_folding"
|
|
13
13
|
require_relative "method_dispatcher/file_folding"
|
|
14
14
|
require_relative "method_dispatcher/kernel_dispatch"
|
|
15
|
+
require_relative "method_dispatcher/method_folding"
|
|
15
16
|
|
|
16
17
|
module Rigor
|
|
17
18
|
module Inference
|
|
@@ -61,11 +62,18 @@ module Rigor
|
|
|
61
62
|
# @param environment [Rigor::Environment, nil] required for
|
|
62
63
|
# RBS-backed dispatch; when nil only constant folding can fire.
|
|
63
64
|
# @return [Rigor::Type, nil] inferred result type, or `nil` for "no rule".
|
|
64
|
-
def dispatch(receiver_type:, method_name:, arg_types:,
|
|
65
|
+
def dispatch(receiver_type:, method_name:, arg_types:,
|
|
65
66
|
block_type: nil, environment: nil,
|
|
66
67
|
call_node: nil, scope: nil)
|
|
67
68
|
return nil if receiver_type.nil?
|
|
68
69
|
|
|
70
|
+
bound_method_result = MethodFolding.try_backward(
|
|
71
|
+
receiver: receiver_type, method_name: method_name, args: arg_types,
|
|
72
|
+
block_type: block_type, environment: environment,
|
|
73
|
+
call_node: call_node, scope: scope
|
|
74
|
+
)
|
|
75
|
+
return bound_method_result if bound_method_result
|
|
76
|
+
|
|
69
77
|
precise = dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type)
|
|
70
78
|
return precise if precise
|
|
71
79
|
|
|
@@ -84,7 +92,10 @@ module Rigor
|
|
|
84
92
|
receiver: receiver_type, method_name: method_name, args: arg_types,
|
|
85
93
|
environment: environment, block_type: block_type
|
|
86
94
|
)
|
|
87
|
-
|
|
95
|
+
if rbs_result
|
|
96
|
+
record_boundary_cross_if_applicable(receiver_type, method_name, rbs_result, environment)
|
|
97
|
+
return rbs_result
|
|
98
|
+
end
|
|
88
99
|
|
|
89
100
|
# ADR-10 slice 2b-ii — dependency-source inference tier.
|
|
90
101
|
# Sits BELOW RBS dispatch (RBS / RBS::Inline / generated
|
|
@@ -98,6 +109,21 @@ module Rigor
|
|
|
98
109
|
dep_source_result = try_dependency_source(receiver_type, method_name, environment)
|
|
99
110
|
return dep_source_result if dep_source_result
|
|
100
111
|
|
|
112
|
+
# v0.1.3 — discovered-method dispatch tier. When the
|
|
113
|
+
# receiver class has no RBS BUT scope_indexer recorded
|
|
114
|
+
# `def method_name` for that class (or singleton), the
|
|
115
|
+
# call dispatches to `Dynamic[top]` rather than falling
|
|
116
|
+
# through to the user-class fallback. Sits below RBS /
|
|
117
|
+
# dependency-source so authoritative signatures still win.
|
|
118
|
+
# The scope-indexer-built table records every project-side
|
|
119
|
+
# `def`, `define_method`, and `alias_method`; the
|
|
120
|
+
# `discovered_method?` consult here closes the
|
|
121
|
+
# fail-soft-event hot spot on implicit-self calls
|
|
122
|
+
# (`sibling_private(...)`) inside `lib/rigor/`'s own
|
|
123
|
+
# internals (analyser private helpers don't have RBS).
|
|
124
|
+
discovered_result = try_discovered_method(receiver_type, method_name, scope)
|
|
125
|
+
return discovered_result if discovered_result
|
|
126
|
+
|
|
101
127
|
# Slice 7 phase 10 — user-class ancestor fallback. When
|
|
102
128
|
# the receiver is `Nominal[T]` or `Singleton[T]` for a
|
|
103
129
|
# class not in the RBS environment (typically a
|
|
@@ -112,6 +138,58 @@ module Rigor
|
|
|
112
138
|
try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
113
139
|
end
|
|
114
140
|
|
|
141
|
+
# v0.1.3 — discovered-method dispatch tier. `scope` carries
|
|
142
|
+
# the `discovered_methods` table built once per program by
|
|
143
|
+
# `ScopeIndexer` (a `Hash[String, Hash[Symbol, :instance |
|
|
144
|
+
# :singleton]]`). When the receiver names a discovered
|
|
145
|
+
# class AND the requested method is recorded for that
|
|
146
|
+
# class's appropriate kind, return `Type::Combinator.untyped`
|
|
147
|
+
# — the dispatcher cannot infer a more precise return type
|
|
148
|
+
# from the bare `def` shape, but the call site stops being a
|
|
149
|
+
# fail-soft hot spot.
|
|
150
|
+
#
|
|
151
|
+
# Returns `nil` when scope / receiver class is unavailable,
|
|
152
|
+
# when the method is not in the discovered table, OR when
|
|
153
|
+
# `discovered_def_nodes` carries a re-typable body for the
|
|
154
|
+
# method (so the downstream
|
|
155
|
+
# `ExpressionTyper#try_user_method_inference` tier can
|
|
156
|
+
# re-type the body for a precise return type rather than
|
|
157
|
+
# collapsing to `Dynamic[top]` here).
|
|
158
|
+
#
|
|
159
|
+
# The tier does NOT gate on `rbs_class_known?`. RBS dispatch
|
|
160
|
+
# already had its turn upstream and returned `nil` (otherwise
|
|
161
|
+
# we wouldn't be here). When RBS knows the class but the
|
|
162
|
+
# particular method is missing from the sig — common for
|
|
163
|
+
# internal helpers and for auto-generated stubs that emit
|
|
164
|
+
# `class X` without enumerating every method — falling
|
|
165
|
+
# through to the user-class fallback would mistakenly fire
|
|
166
|
+
# `call.undefined-method`. Honoring the discovered table
|
|
167
|
+
# here keeps the sibling-private call resolution working
|
|
168
|
+
# under partial RBS coverage.
|
|
169
|
+
def try_discovered_method(receiver_type, method_name, scope)
|
|
170
|
+
return nil if scope.nil?
|
|
171
|
+
|
|
172
|
+
class_name, kind = discovered_method_lookup(receiver_type)
|
|
173
|
+
return nil if class_name.nil?
|
|
174
|
+
return nil unless scope.discovered_method?(class_name, method_name, kind)
|
|
175
|
+
return nil if kind == :instance && scope.user_def_for(class_name, method_name)
|
|
176
|
+
|
|
177
|
+
Type::Combinator.untyped
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Resolves the `(class_name, kind)` pair scope_indexer keys
|
|
181
|
+
# its `discovered_methods` table on. `Nominal[X]` looks up
|
|
182
|
+
# instance methods on X; `Singleton[X]` looks up singleton
|
|
183
|
+
# methods on X. Other carriers return `[nil, nil]` so the
|
|
184
|
+
# tier declines.
|
|
185
|
+
def discovered_method_lookup(receiver_type)
|
|
186
|
+
case receiver_type
|
|
187
|
+
when Type::Nominal then [receiver_type.class_name, :instance]
|
|
188
|
+
when Type::Singleton then [receiver_type.class_name, :singleton]
|
|
189
|
+
else [nil, nil]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
115
193
|
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
116
194
|
# slice 7. Walks every loaded plugin's
|
|
117
195
|
# `#flow_contribution_for(call_node:, scope:)` hook,
|
|
@@ -177,6 +255,71 @@ module Rigor
|
|
|
177
255
|
budget_silence_result(class_name, index, environment)
|
|
178
256
|
end
|
|
179
257
|
|
|
258
|
+
# ADR-10 slice 5c — record a
|
|
259
|
+
# `dynamic.dependency-source.boundary-cross` event when
|
|
260
|
+
# RBS dispatch resolves a call AND the receiver class
|
|
261
|
+
# belongs to a `mode: :full` opt-in gem whose Walker
|
|
262
|
+
# also catalogued the same `(class_name, method_name)`.
|
|
263
|
+
# The dispatcher still returns the RBS answer (per
|
|
264
|
+
# ADR-10's tier order: authoritative-source wins), but
|
|
265
|
+
# the reporter accumulates the crossing for end-of-run
|
|
266
|
+
# audit diagnostics.
|
|
267
|
+
#
|
|
268
|
+
# Five honest fall-throughs keep the gate narrow:
|
|
269
|
+
#
|
|
270
|
+
# - environment / index / reporter missing — slice 5c
|
|
271
|
+
# needs all three.
|
|
272
|
+
# - receiver has no nominal class name (Dynamic-only
|
|
273
|
+
# carriers) — nothing to look up.
|
|
274
|
+
# - receiver class doesn't belong to a `mode: :full` gem
|
|
275
|
+
# — the user didn't opt this gem into the distinct
|
|
276
|
+
# dispatch path.
|
|
277
|
+
# - the gem-source catalog has no entry for the method —
|
|
278
|
+
# only RBS knows about it; nothing to cross.
|
|
279
|
+
# - the RBS-side result is itself `Dynamic[Top]` — the
|
|
280
|
+
# "agreement" is trivially `untyped ≈ untyped`, no
|
|
281
|
+
# meaningful divergence to flag.
|
|
282
|
+
def record_boundary_cross_if_applicable(receiver_type, method_name, rbs_result, environment)
|
|
283
|
+
class_name = boundary_cross_class_name(receiver_type, environment, rbs_result)
|
|
284
|
+
return if class_name.nil?
|
|
285
|
+
|
|
286
|
+
index = environment.dependency_source_index
|
|
287
|
+
return unless index.full_mode?(class_name)
|
|
288
|
+
return unless index.contribution_for(class_name: class_name, method_name: method_name)
|
|
289
|
+
|
|
290
|
+
environment.boundary_cross_reporter.record(
|
|
291
|
+
class_name: class_name, method_name: method_name,
|
|
292
|
+
gem_name: index.gem_for(class_name),
|
|
293
|
+
rbs_display: rbs_display_for(rbs_result)
|
|
294
|
+
)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Composite preflight for {#record_boundary_cross_if_applicable}.
|
|
298
|
+
# Returns the receiver class name only when every prerequisite
|
|
299
|
+
# for emitting the diagnostic is satisfied (environment carries
|
|
300
|
+
# an index + reporter, receiver is a nominal carrier, RBS-side
|
|
301
|
+
# result is not the trivial `Dynamic[Top]` envelope). Returns
|
|
302
|
+
# `nil` to short-circuit otherwise.
|
|
303
|
+
def boundary_cross_class_name(receiver_type, environment, rbs_result)
|
|
304
|
+
return nil if environment.nil?
|
|
305
|
+
return nil if environment.dependency_source_index.nil?
|
|
306
|
+
return nil if environment.dependency_source_index.empty?
|
|
307
|
+
return nil if environment.boundary_cross_reporter.nil?
|
|
308
|
+
return nil if rbs_result_untyped?(rbs_result)
|
|
309
|
+
|
|
310
|
+
dep_source_class_name(receiver_type)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def rbs_result_untyped?(rbs_result)
|
|
314
|
+
rbs_result.is_a?(Type::Dynamic) && rbs_result.static_facet.is_a?(Type::Top)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def rbs_display_for(rbs_result)
|
|
318
|
+
return "untyped" if rbs_result.nil?
|
|
319
|
+
|
|
320
|
+
rbs_result.respond_to?(:describe) ? rbs_result.describe : rbs_result.inspect
|
|
321
|
+
end
|
|
322
|
+
|
|
180
323
|
def budget_silence_result(class_name, index, _environment)
|
|
181
324
|
return nil unless index.budget_overrun_strategy == :dependency_silence
|
|
182
325
|
|
|
@@ -242,6 +385,7 @@ module Rigor
|
|
|
242
385
|
ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
243
386
|
FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
244
387
|
KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
388
|
+
MethodFolding.try_forward(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
245
389
|
BlockFolding.try_fold(
|
|
246
390
|
receiver: receiver_type, method_name: method_name, args: arg_types, block_type: block_type
|
|
247
391
|
)
|
|
@@ -32,7 +32,6 @@ module Rigor
|
|
|
32
32
|
# `#singleton_method`.
|
|
33
33
|
#
|
|
34
34
|
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
35
|
-
# rubocop:disable Metrics/ClassLength
|
|
36
35
|
class MethodParameterBinder
|
|
37
36
|
# @param environment [Rigor::Environment]
|
|
38
37
|
# @param class_path [String, nil] the qualified name of the class
|
|
@@ -175,7 +174,7 @@ module Rigor
|
|
|
175
174
|
# `non-empty-array[Integer]` describe the parameter binding
|
|
176
175
|
# they actually want, not its element type.
|
|
177
176
|
def apply_param_overrides(types, slots, rbs_method)
|
|
178
|
-
override_map = RbsExtended.param_type_override_map(rbs_method)
|
|
177
|
+
override_map = RbsExtended.param_type_override_map(rbs_method, environment: @environment)
|
|
179
178
|
return if override_map.empty?
|
|
180
179
|
|
|
181
180
|
slots.each do |slot|
|
|
@@ -275,6 +274,5 @@ module Rigor
|
|
|
275
274
|
end
|
|
276
275
|
end
|
|
277
276
|
end
|
|
278
|
-
# rubocop:enable Metrics/ClassLength
|
|
279
277
|
end
|
|
280
278
|
end
|
|
@@ -376,7 +376,7 @@ module Rigor
|
|
|
376
376
|
# the predicate shape is recognised, or `nil` to signal "no
|
|
377
377
|
# narrowing" so the public surface can fall back to the entry
|
|
378
378
|
# scope.
|
|
379
|
-
def analyse(node, scope)
|
|
379
|
+
def analyse(node, scope)
|
|
380
380
|
case node
|
|
381
381
|
when Prism::ParenthesesNode
|
|
382
382
|
analyse_parentheses(node, scope)
|
|
@@ -445,7 +445,6 @@ module Rigor
|
|
|
445
445
|
# intersects each half with the integer-domain parts of
|
|
446
446
|
# `current_type`. Non-integer parts of a Union receiver
|
|
447
447
|
# (nil, String, …) survive unchanged.
|
|
448
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
449
448
|
def complement_integer_range(current_type, range)
|
|
450
449
|
halves = integer_range_complement_halves(range)
|
|
451
450
|
parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
|
|
@@ -466,7 +465,6 @@ module Rigor
|
|
|
466
465
|
|
|
467
466
|
Type::Combinator.union(*survivors)
|
|
468
467
|
end
|
|
469
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
470
468
|
|
|
471
469
|
# Returns the two open halves of an IntegerRange's
|
|
472
470
|
# complement: the left half `int<-∞, a-1>` (when `a` is
|
|
@@ -1337,7 +1335,7 @@ module Rigor
|
|
|
1337
1335
|
method_def = resolve_rbs_extended_method(node, scope)
|
|
1338
1336
|
return nil if method_def.nil?
|
|
1339
1337
|
|
|
1340
|
-
contribution = RbsExtended.read_flow_contribution(method_def)
|
|
1338
|
+
contribution = RbsExtended.read_flow_contribution(method_def, environment: scope.environment)
|
|
1341
1339
|
return nil if contribution.nil?
|
|
1342
1340
|
|
|
1343
1341
|
result = Rigor::FlowContribution::Merger.merge([contribution])
|
|
@@ -41,7 +41,6 @@ module Rigor
|
|
|
41
41
|
# `Nominal[C]` regardless of which method body we are in.
|
|
42
42
|
# When either argument is omitted, the corresponding token degrades
|
|
43
43
|
# to Dynamic[Top].
|
|
44
|
-
# rubocop:disable Metrics/ModuleLength
|
|
45
44
|
module RbsTypeTranslator
|
|
46
45
|
# Hash-based dispatch keeps `translate` linear and dodges the
|
|
47
46
|
# bookkeeping costs of a 20-arm `case` (RuboCop AbcSize/CCN/Length
|
|
@@ -214,6 +213,5 @@ module Rigor
|
|
|
214
213
|
end
|
|
215
214
|
end
|
|
216
215
|
end
|
|
217
|
-
# rubocop:enable Metrics/ModuleLength
|
|
218
216
|
end
|
|
219
217
|
end
|
|
@@ -57,9 +57,19 @@ module Rigor
|
|
|
57
57
|
# `Scope#with_local` / `#with_fact` / `#with_self_type`
|
|
58
58
|
# propagates it across every derived scope.
|
|
59
59
|
declared_types, discovered_classes = build_declaration_artifacts(root)
|
|
60
|
+
# Merge the indexer's findings on top of whatever the
|
|
61
|
+
# base scope already carries so callers that seed
|
|
62
|
+
# cross-file class knowledge (e.g. the ADR-14
|
|
63
|
+
# `SigGen::ObservationCollector` pre-walking project
|
|
64
|
+
# `lib/` before scanning `spec/`) keep their seeds
|
|
65
|
+
# alongside the per-file declarations the indexer
|
|
66
|
+
# itself discovers. Indexer-found entries win on
|
|
67
|
+
# collision — same-file declarations are the most
|
|
68
|
+
# specific authority.
|
|
69
|
+
merged_classes = default_scope.discovered_classes.merge(discovered_classes)
|
|
60
70
|
seeded_scope = default_scope
|
|
61
71
|
.with_declared_types(declared_types)
|
|
62
|
-
.with_discovered_classes(
|
|
72
|
+
.with_discovered_classes(merged_classes)
|
|
63
73
|
|
|
64
74
|
# Slice 7 phase 2. Pre-pass over every class/module body
|
|
65
75
|
# to collect the per-class ivar accumulator. Seeded after
|
|
@@ -300,7 +310,7 @@ module Rigor
|
|
|
300
310
|
accumulator.freeze
|
|
301
311
|
end
|
|
302
312
|
|
|
303
|
-
def walk_constant_writes(node, qualified_prefix, default_scope, accumulator)
|
|
313
|
+
def walk_constant_writes(node, qualified_prefix, default_scope, accumulator)
|
|
304
314
|
return unless node.is_a?(Prism::Node)
|
|
305
315
|
|
|
306
316
|
case node
|
|
@@ -348,7 +358,7 @@ module Rigor
|
|
|
348
358
|
accumulator.transform_values(&:freeze).freeze
|
|
349
359
|
end
|
|
350
360
|
|
|
351
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
361
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
352
362
|
def walk_methods(node, qualified_prefix, in_singleton_class, accumulator)
|
|
353
363
|
return unless node.is_a?(Prism::Node)
|
|
354
364
|
|
|
@@ -385,7 +395,7 @@ module Rigor
|
|
|
385
395
|
walk_methods(child, qualified_prefix, in_singleton_class, accumulator)
|
|
386
396
|
end
|
|
387
397
|
end
|
|
388
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
398
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
389
399
|
|
|
390
400
|
# v0.1.2 — when a `Const = Data.define(*sym) do ... end`
|
|
391
401
|
# / `Const = Struct.new(*sym) do ... end` constant write
|
|
@@ -430,7 +440,6 @@ module Rigor
|
|
|
430
440
|
accumulator.transform_values(&:freeze).freeze
|
|
431
441
|
end
|
|
432
442
|
|
|
433
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
434
443
|
def walk_def_nodes(node, qualified_prefix, in_singleton_class, accumulator)
|
|
435
444
|
return unless node.is_a?(Prism::Node)
|
|
436
445
|
|
|
@@ -462,8 +471,6 @@ module Rigor
|
|
|
462
471
|
walk_def_nodes(child, qualified_prefix, in_singleton_class, accumulator)
|
|
463
472
|
end
|
|
464
473
|
end
|
|
465
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
466
|
-
|
|
467
474
|
# v0.0.3 A — sentinel key under which `record_def_node`
|
|
468
475
|
# files DefNodes that live outside any class / module
|
|
469
476
|
# body (top-level helpers, `def`s nested inside DSL
|
|
@@ -646,7 +653,6 @@ module Rigor
|
|
|
646
653
|
|
|
647
654
|
# Builds a map `{class_name => {new_name_sym => old_name_sym}}` by
|
|
648
655
|
# walking the tree for `AliasMethodNode` nodes inside class bodies.
|
|
649
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
|
650
656
|
def collect_class_alias_map(node, qualified_prefix, accumulator)
|
|
651
657
|
return accumulator unless node.is_a?(Prism::Node)
|
|
652
658
|
|
|
@@ -667,7 +673,6 @@ module Rigor
|
|
|
667
673
|
node.compact_child_nodes.each { |child| collect_class_alias_map(child, qualified_prefix, accumulator) }
|
|
668
674
|
accumulator
|
|
669
675
|
end
|
|
670
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
|
671
676
|
|
|
672
677
|
def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
|
|
673
678
|
return if qualified_prefix.empty?
|
|
@@ -889,7 +889,7 @@ module Rigor
|
|
|
889
889
|
# or nil otherwise. Centralised so each per-matcher
|
|
890
890
|
# decoder can short-circuit on a non-matching outer
|
|
891
891
|
# call.
|
|
892
|
-
def rspec_expectation_target(call_node)
|
|
892
|
+
def rspec_expectation_target(call_node)
|
|
893
893
|
receiver = call_node.receiver
|
|
894
894
|
return nil unless receiver.is_a?(Prism::CallNode) && receiver.name == :expect
|
|
895
895
|
return nil unless receiver.receiver.nil?
|
|
@@ -967,7 +967,7 @@ module Rigor
|
|
|
967
967
|
method_def = resolve_call_method(call_node, current_scope)
|
|
968
968
|
return current_scope if method_def.nil?
|
|
969
969
|
|
|
970
|
-
contribution = RbsExtended.read_flow_contribution(method_def)
|
|
970
|
+
contribution = RbsExtended.read_flow_contribution(method_def, environment: current_scope.environment)
|
|
971
971
|
return current_scope if contribution.nil?
|
|
972
972
|
|
|
973
973
|
result = Rigor::FlowContribution::Merger.merge([contribution])
|
|
@@ -1034,7 +1034,7 @@ module Rigor
|
|
|
1034
1034
|
end
|
|
1035
1035
|
end
|
|
1036
1036
|
|
|
1037
|
-
def resolve_call_method(call_node, current_scope)
|
|
1037
|
+
def resolve_call_method(call_node, current_scope)
|
|
1038
1038
|
receiver_node = call_node.receiver
|
|
1039
1039
|
receiver_type =
|
|
1040
1040
|
if receiver_node
|
|
@@ -1120,7 +1120,7 @@ module Rigor
|
|
|
1120
1120
|
end
|
|
1121
1121
|
end
|
|
1122
1122
|
|
|
1123
|
-
def lookup_post_return_arg(call_node, method_def, target_name)
|
|
1123
|
+
def lookup_post_return_arg(call_node, method_def, target_name)
|
|
1124
1124
|
# Plugin-source contributions arrive without an
|
|
1125
1125
|
# authoritative method_def (the plugin recognised the
|
|
1126
1126
|
# call shape directly). Parameter-targeting falls back
|
|
@@ -1394,6 +1394,8 @@ module Rigor
|
|
|
1394
1394
|
.with_class_ivars(scope.class_ivars)
|
|
1395
1395
|
.with_class_cvars(scope.class_cvars)
|
|
1396
1396
|
.with_program_globals(scope.program_globals)
|
|
1397
|
+
.with_discovered_methods(scope.discovered_methods)
|
|
1398
|
+
.with_discovered_method_visibilities(scope.discovered_method_visibilities)
|
|
1397
1399
|
end
|
|
1398
1400
|
|
|
1399
1401
|
def singleton_def?(def_node)
|
|
@@ -1501,7 +1503,7 @@ module Rigor
|
|
|
1501
1503
|
EXIT_CALL_NAMES = %i[raise throw exit abort fail].freeze
|
|
1502
1504
|
private_constant :EXIT_CALL_NAMES
|
|
1503
1505
|
|
|
1504
|
-
def branch_unconditionally_exits?(node)
|
|
1506
|
+
def branch_unconditionally_exits?(node)
|
|
1505
1507
|
return false if node.nil?
|
|
1506
1508
|
|
|
1507
1509
|
case node
|
|
@@ -1607,7 +1609,6 @@ module Rigor
|
|
|
1607
1609
|
# Returns an array of `[Symbol, Rigor::Type]` pairs for every
|
|
1608
1610
|
# variable captured by `pattern`. Unrecognised pattern nodes
|
|
1609
1611
|
# contribute no bindings (fail-soft).
|
|
1610
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
|
1611
1612
|
def collect_in_pattern_bindings(subject, pattern, scope)
|
|
1612
1613
|
case pattern
|
|
1613
1614
|
when Prism::CapturePatternNode
|
|
@@ -1629,7 +1630,6 @@ module Rigor
|
|
|
1629
1630
|
[]
|
|
1630
1631
|
end
|
|
1631
1632
|
end
|
|
1632
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
|
1633
1633
|
|
|
1634
1634
|
def collect_array_pattern_bindings(pattern, scope)
|
|
1635
1635
|
bindings = [*pattern.requireds, *pattern.posts].flat_map do |elem|
|
|
@@ -137,7 +137,6 @@ module Rigor
|
|
|
137
137
|
# to the same `#get(url, timeout:, max_bytes:)` shape so the
|
|
138
138
|
# tests don't require network access.
|
|
139
139
|
class DefaultHttpClient
|
|
140
|
-
# rubocop:disable Metrics/MethodLength
|
|
141
140
|
def get(url, timeout:, max_bytes:)
|
|
142
141
|
require "net/http"
|
|
143
142
|
require "uri"
|
|
@@ -169,7 +168,6 @@ module Rigor
|
|
|
169
168
|
end
|
|
170
169
|
body
|
|
171
170
|
end
|
|
172
|
-
# rubocop:enable Metrics/MethodLength
|
|
173
171
|
end
|
|
174
172
|
end
|
|
175
173
|
end
|
data/lib/rigor/plugin/loader.rb
CHANGED
|
@@ -81,7 +81,7 @@ module Rigor
|
|
|
81
81
|
# "rigor-rails"
|
|
82
82
|
# { "gem" => "rigor-rails", "id" => "rails", "config" => {...} }
|
|
83
83
|
# { gem: "rigor-rails", id: "rails", config: {...} }
|
|
84
|
-
def normalise_entry(raw, index)
|
|
84
|
+
def normalise_entry(raw, index)
|
|
85
85
|
case raw
|
|
86
86
|
when String
|
|
87
87
|
{ gem: raw, id: nil, config: {} }
|
|
@@ -136,7 +136,7 @@ module Rigor
|
|
|
136
136
|
)
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
-
def lookup_plugin_class!(entry, newly_registered)
|
|
139
|
+
def lookup_plugin_class!(entry, newly_registered)
|
|
140
140
|
if entry[:id]
|
|
141
141
|
plugin_class = Plugin.registered_for(entry[:id])
|
|
142
142
|
unless plugin_class
|
|
@@ -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
|
|
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.
|
|
@@ -31,19 +31,19 @@ module Rigor
|
|
|
31
31
|
# topological sort + missing-producer detection (slice 5);
|
|
32
32
|
# slice 4 carries the declarations on the manifest but the
|
|
33
33
|
# loader does not yet enforce them.
|
|
34
|
-
Consumption
|
|
34
|
+
class Consumption < Data.define(:plugin_id, :name, :optional)
|
|
35
35
|
def initialize(plugin_id:, name:, optional: false)
|
|
36
36
|
super(plugin_id: plugin_id.to_s, name: name.to_sym, optional: optional ? true : false)
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
attr_reader :id, :version, :description, :protocols, :config_schema, :produces, :consumes,
|
|
41
|
-
:owns_receivers
|
|
41
|
+
:owns_receivers, :type_node_resolvers
|
|
42
42
|
|
|
43
43
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
44
44
|
id:, version:,
|
|
45
45
|
description: nil, protocols: [], config_schema: {},
|
|
46
|
-
produces: [], consumes: [], owns_receivers: []
|
|
46
|
+
produces: [], consumes: [], owns_receivers: [], type_node_resolvers: []
|
|
47
47
|
)
|
|
48
48
|
validate_id!(id)
|
|
49
49
|
validate_version!(version)
|
|
@@ -51,15 +51,18 @@ module Rigor
|
|
|
51
51
|
validate_config_schema!(config_schema)
|
|
52
52
|
validate_produces!(produces)
|
|
53
53
|
validate_owns_receivers!(owns_receivers)
|
|
54
|
+
validate_type_node_resolvers!(type_node_resolvers)
|
|
54
55
|
|
|
55
|
-
assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers
|
|
56
|
+
assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
|
|
57
|
+
type_node_resolvers)
|
|
56
58
|
freeze
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
private
|
|
60
62
|
|
|
61
|
-
# rubocop:disable Metrics/ParameterLists
|
|
62
|
-
def assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers
|
|
63
|
+
# rubocop:disable Metrics/ParameterLists
|
|
64
|
+
def assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
|
|
65
|
+
type_node_resolvers)
|
|
63
66
|
@id = id.dup.freeze
|
|
64
67
|
@version = version.dup.freeze
|
|
65
68
|
@description = description.nil? ? nil : description.to_s.dup.freeze
|
|
@@ -68,8 +71,9 @@ module Rigor
|
|
|
68
71
|
@produces = produces.map(&:to_sym).freeze
|
|
69
72
|
@consumes = coerce_consumes(consumes)
|
|
70
73
|
@owns_receivers = owns_receivers.map { |c| c.to_s.dup.freeze }.freeze
|
|
74
|
+
@type_node_resolvers = type_node_resolvers.dup.freeze
|
|
71
75
|
end
|
|
72
|
-
# rubocop:enable Metrics/ParameterLists
|
|
76
|
+
# rubocop:enable Metrics/ParameterLists
|
|
73
77
|
|
|
74
78
|
public
|
|
75
79
|
|
|
@@ -105,7 +109,8 @@ module Rigor
|
|
|
105
109
|
"config_schema" => config_schema.to_h { |k, v| [k, v.to_s] },
|
|
106
110
|
"produces" => produces.map(&:to_s),
|
|
107
111
|
"consumes" => consumes.map { |c| consumption_hash(c) },
|
|
108
|
-
"owns_receivers" => owns_receivers
|
|
112
|
+
"owns_receivers" => owns_receivers,
|
|
113
|
+
"type_node_resolvers" => type_node_resolvers.map { |r| r.class.name }
|
|
109
114
|
}
|
|
110
115
|
end
|
|
111
116
|
|
|
@@ -187,6 +192,22 @@ module Rigor
|
|
|
187
192
|
"got #{owns_receivers.inspect}"
|
|
188
193
|
end
|
|
189
194
|
|
|
195
|
+
# ADR-13 slice 2 — `type_node_resolvers:` declares the
|
|
196
|
+
# plugin-supplied `TypeNodeResolver` instances the parser
|
|
197
|
+
# consults (in slice 3) when an RBS::Extended payload's
|
|
198
|
+
# named- or generic-type head misses the built-in registry.
|
|
199
|
+
# Slice 2 carries the declarations on the manifest and the
|
|
200
|
+
# registry exposes them in registration order; the parser
|
|
201
|
+
# integration that actually drives the chain lands in
|
|
202
|
+
# slice 3.
|
|
203
|
+
def validate_type_node_resolvers!(resolvers)
|
|
204
|
+
return if resolvers.is_a?(Array) && resolvers.all?(TypeNodeResolver)
|
|
205
|
+
|
|
206
|
+
raise ArgumentError,
|
|
207
|
+
"plugin manifest type_node_resolvers must be an Array of " \
|
|
208
|
+
"Rigor::Plugin::TypeNodeResolver instances, got #{resolvers.inspect}"
|
|
209
|
+
end
|
|
210
|
+
|
|
190
211
|
def coerce_consumes(consumes)
|
|
191
212
|
unless consumes.is_a?(Array)
|
|
192
213
|
raise ArgumentError, "plugin manifest consumes must be an Array, got #{consumes.inspect}"
|
|
@@ -44,6 +44,17 @@ module Rigor
|
|
|
44
44
|
!load_errors.empty?
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# ADR-13 slice 2 — flat ordered list of every loaded
|
|
48
|
+
# plugin's manifest-declared {TypeNodeResolver} instances,
|
|
49
|
+
# in plugin registration order. Slice 3 wires this into
|
|
50
|
+
# the parser's resolver chain; until then the method is a
|
|
51
|
+
# read-side aggregator only. The first non-nil
|
|
52
|
+
# `#resolve(node, scope)` return wins per ADR-13 WD3 / WD5
|
|
53
|
+
# — registration order is the user's lever.
|
|
54
|
+
def type_node_resolvers
|
|
55
|
+
plugins.flat_map { |plugin| plugin.manifest.type_node_resolvers }
|
|
56
|
+
end
|
|
57
|
+
|
|
47
58
|
EMPTY = new.freeze
|
|
48
59
|
end
|
|
49
60
|
end
|
|
@@ -42,7 +42,7 @@ module Rigor
|
|
|
42
42
|
class Services
|
|
43
43
|
attr_reader :reflection, :type, :configuration, :cache_store, :trust_policy, :fact_store
|
|
44
44
|
|
|
45
|
-
def initialize(
|
|
45
|
+
def initialize(
|
|
46
46
|
reflection:, type:, configuration:,
|
|
47
47
|
cache_store: nil, trust_policy: nil, fact_store: nil
|
|
48
48
|
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
# Plugin-supplied resolver for custom named / generic type
|
|
6
|
+
# vocabulary in RBS::Extended payloads. ADR-13 § "Decision".
|
|
7
|
+
#
|
|
8
|
+
# Subclasses override {#resolve} to return a
|
|
9
|
+
# {Rigor::Type::Base} when the node matches the vocabulary
|
|
10
|
+
# the resolver covers, or `nil` to fall through to the next
|
|
11
|
+
# resolver in the chain (and finally to the built-in / RBS
|
|
12
|
+
# fallback). The base implementation returns `nil` so an
|
|
13
|
+
# unimplemented subclass is a safe no-op.
|
|
14
|
+
#
|
|
15
|
+
# Resolvers are registered through their plugin's manifest
|
|
16
|
+
# under the `type_node_resolvers:` slot:
|
|
17
|
+
#
|
|
18
|
+
# class RigorTypescriptUtilityTypes < Rigor::Plugin::Base
|
|
19
|
+
# manifest(
|
|
20
|
+
# id: "typescript-utility-types",
|
|
21
|
+
# version: "0.1.0",
|
|
22
|
+
# type_node_resolvers: [Resolvers::Pick.new,
|
|
23
|
+
# Resolvers::Omit.new]
|
|
24
|
+
# )
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# Slice 2 of the ADR-13 envelope (this file) ships the base
|
|
28
|
+
# class + manifest hook + registry aggregation. The parser-
|
|
29
|
+
# side wiring that actually consults the resolver chain
|
|
30
|
+
# arrives in slice 3, when {Rigor::TypeNode::NameScope} and
|
|
31
|
+
# the dispatcher between {Rigor::Builtins::ImportedRefinements::Parser}
|
|
32
|
+
# and the chain land. Until then resolvers can be unit-tested
|
|
33
|
+
# in isolation but never run for a real `%a{rigor:v1:...}`
|
|
34
|
+
# payload.
|
|
35
|
+
#
|
|
36
|
+
# Resolvers SHOULD be stateless and re-entrant; the registry
|
|
37
|
+
# builds the chain once per `Analysis::Runner.run` and may
|
|
38
|
+
# consult any resolver multiple times for the same node.
|
|
39
|
+
class TypeNodeResolver
|
|
40
|
+
# @param node [Rigor::TypeNode::Identifier, Rigor::TypeNode::Generic]
|
|
41
|
+
# the parser-emitted node the chain is asking about.
|
|
42
|
+
# @param scope [Rigor::TypeNode::NameScope] companion
|
|
43
|
+
# value object (slice 3); slice 2 invocations MAY pass
|
|
44
|
+
# `nil` because the chain doesn't exist yet.
|
|
45
|
+
# @return [Rigor::Type::Base, nil] resolved type, or `nil`
|
|
46
|
+
# to fall through.
|
|
47
|
+
def resolve(node, scope) # rubocop:disable Lint/UnusedMethodArgument
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|