rigortype 0.1.4 → 0.1.5
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 +40 -13
- data/lib/rigor/analysis/fact_store.rb +15 -3
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +387 -12
- data/lib/rigor/analysis/worker_session.rb +327 -0
- data/lib/rigor/builtins/imported_refinements.rb +6 -2
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +40 -7
- data/lib/rigor/cli.rb +52 -2
- data/lib/rigor/configuration.rb +131 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/lockfile_resolver.rb +125 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
- data/lib/rigor/environment/rbs_loader.rb +194 -6
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment.rb +78 -6
- data/lib/rigor/inference/acceptance.rb +35 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
- data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
- data/lib/rigor/inference/expression_typer.rb +12 -2
- data/lib/rigor/inference/macro_block_self_type.rb +96 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
- data/lib/rigor/inference/method_dispatcher.rb +128 -3
- data/lib/rigor/inference/method_parameter_binder.rb +21 -11
- data/lib/rigor/inference/narrowing.rb +127 -8
- data/lib/rigor/inference/synthetic_method.rb +86 -0
- data/lib/rigor/inference/synthetic_method_index.rb +82 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
- data/lib/rigor/plugin/blueprint.rb +60 -0
- data/lib/rigor/plugin/loader.rb +3 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
- data/lib/rigor/plugin/macro/external_file.rb +143 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
- data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
- data/lib/rigor/plugin/macro.rb +31 -0
- data/lib/rigor/plugin/manifest.rb +78 -7
- data/lib/rigor/plugin/registry.rb +32 -2
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/trinary.rb +15 -11
- data/lib/rigor/type/bot.rb +6 -3
- data/lib/rigor/type/combinator.rb +12 -1
- data/lib/rigor/type/integer_range.rb +7 -7
- data/lib/rigor/type/refined.rb +18 -12
- data/lib/rigor/type/top.rb +4 -3
- data/lib/rigor/type_node/generic.rb +7 -1
- data/lib/rigor/type_node/identifier.rb +9 -1
- data/lib/rigor/type_node/string_literal.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +5 -2
- data/sig/rigor/plugin/blueprint.rbs +7 -0
- data/sig/rigor/plugin/manifest.rbs +1 -1
- data/sig/rigor/plugin/registry.rbs +14 -1
- data/sig/rigor.rbs +35 -2
- metadata +39 -1
|
@@ -97,6 +97,20 @@ module Rigor
|
|
|
97
97
|
return rbs_result
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
# ADR-16 Tier B / Tier C — synthetic-method tier. Sits
|
|
101
|
+
# BELOW RBS dispatch (per WD13: user-authored RBS overrides
|
|
102
|
+
# substrate synthesis) and ABOVE the dependency-source
|
|
103
|
+
# inference tier so a plugin's declared emit table beats
|
|
104
|
+
# the generic gem-source fallback for the same class. Slice
|
|
105
|
+
# 6a-TierB (origin_module dispatch) lands precise return
|
|
106
|
+
# types for Tier B emissions; Tier C emissions still return
|
|
107
|
+
# `Dynamic[T]` at this tier (slice 6b is the Tier C
|
|
108
|
+
# promotion via ADR-13's resolver chain).
|
|
109
|
+
synthetic_result = try_synthetic_method(
|
|
110
|
+
receiver_type, method_name, arg_types, block_type, environment
|
|
111
|
+
)
|
|
112
|
+
return synthetic_result if synthetic_result
|
|
113
|
+
|
|
100
114
|
# ADR-10 slice 2b-ii — dependency-source inference tier.
|
|
101
115
|
# Sits BELOW RBS dispatch (RBS / RBS::Inline / generated
|
|
102
116
|
# stubs / plugin contracts always win) and ABOVE the
|
|
@@ -225,6 +239,101 @@ module Rigor
|
|
|
225
239
|
# publish as ground-truth `T`). Returns `nil` when the
|
|
226
240
|
# environment carries no index, the index has no entry, or
|
|
227
241
|
# the receiver has no nominal class to look up.
|
|
242
|
+
# ADR-16 synthetic-method tier. Slice 2b shipped the floor —
|
|
243
|
+
# a match short-circuits at the right precedence (above
|
|
244
|
+
# dep-source / discovered / user-class-fallback; below RBS)
|
|
245
|
+
# and returns `Dynamic[T]`. Slice 6 (precision promotion):
|
|
246
|
+
# - Tier B path (slice 6a, `provenance[:origin_module]`
|
|
247
|
+
# recorded by the slice-3b scanner): redispatch on
|
|
248
|
+
# `Nominal[origin_module]` via `RbsDispatch` so the
|
|
249
|
+
# module's authored RBS return type wins. Devise's
|
|
250
|
+
# `valid_password?` returns `bool`, not `Dynamic[T]`.
|
|
251
|
+
# - Tier C path (slice 6b, plain `return_type:` string from
|
|
252
|
+
# the manifest's emit table): look up
|
|
253
|
+
# `environment.nominal_for_name(return_type)` so
|
|
254
|
+
# `attribute :avatar, Types::String` emits a synthetic
|
|
255
|
+
# reader returning `Nominal[ActiveStorage::Attached::One]`
|
|
256
|
+
# (when the class is in RBS). Unparameterised class names
|
|
257
|
+
# only — parameterised forms (`Array[String]`,
|
|
258
|
+
# `Hash[K, V]`) and plugin-supplied utility-type names
|
|
259
|
+
# (`Pick<T, K>`) require routing through the full ADR-13
|
|
260
|
+
# `Plugin::TypeNodeResolver` chain, which slice 6 does
|
|
261
|
+
# not yet wire in (the resolver chain is consulted only
|
|
262
|
+
# for `%a{rigor:v1:…}` payloads as of ADR-13 slice 3).
|
|
263
|
+
def try_synthetic_method(receiver_type, method_name, arg_types, block_type, environment)
|
|
264
|
+
index = environment&.synthetic_method_index
|
|
265
|
+
return nil if index.nil? || index.empty?
|
|
266
|
+
|
|
267
|
+
class_name = synthetic_method_class_name(receiver_type)
|
|
268
|
+
return nil if class_name.nil?
|
|
269
|
+
|
|
270
|
+
matches = case receiver_type
|
|
271
|
+
when Type::Singleton then index.lookup_singleton(class_name, method_name)
|
|
272
|
+
else index.lookup_instance(class_name, method_name)
|
|
273
|
+
end
|
|
274
|
+
return nil if matches.empty?
|
|
275
|
+
|
|
276
|
+
promoted = promote_synthetic_match(matches, method_name, arg_types, block_type, environment)
|
|
277
|
+
promoted || Type::Combinator.untyped
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# First non-nil promotion wins. Tier B (origin_module) and
|
|
281
|
+
# Tier C (return_type nominal lookup) are tried in the
|
|
282
|
+
# same registration-order pass per WD11 first-wins —
|
|
283
|
+
# the slice-3b scanner sets `origin_module` for Tier B
|
|
284
|
+
# entries and leaves it absent for Tier C, so the two
|
|
285
|
+
# paths self-route per match.
|
|
286
|
+
def promote_synthetic_match(matches, method_name, arg_types, block_type, environment)
|
|
287
|
+
return nil if environment.nil?
|
|
288
|
+
|
|
289
|
+
matches.each do |synthetic|
|
|
290
|
+
promoted =
|
|
291
|
+
promote_via_origin_module(synthetic, method_name, arg_types, block_type, environment) ||
|
|
292
|
+
promote_via_return_type(synthetic, environment)
|
|
293
|
+
return promoted if promoted
|
|
294
|
+
end
|
|
295
|
+
nil
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Slice 6a-TierB. For Tier B emissions (origin_module
|
|
299
|
+
# recorded in provenance), redispatch the call on the
|
|
300
|
+
# included module's `Nominal[...]` type via `RbsDispatch`.
|
|
301
|
+
# Returns nil when the SyntheticMethod is not a Tier B
|
|
302
|
+
# entry or when the origin_module is not in the RBS env.
|
|
303
|
+
def promote_via_origin_module(synthetic, method_name, arg_types, block_type, environment)
|
|
304
|
+
module_name = synthetic.provenance[:origin_module]
|
|
305
|
+
return nil unless module_name
|
|
306
|
+
|
|
307
|
+
module_type = Type::Combinator.nominal_of(module_name)
|
|
308
|
+
RbsDispatch.try_dispatch(
|
|
309
|
+
receiver: module_type, method_name: method_name, args: arg_types,
|
|
310
|
+
environment: environment, block_type: block_type
|
|
311
|
+
)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Slice 6b-TierC. For Tier C emissions, look up the
|
|
315
|
+
# manifest-declared `return_type:` string via
|
|
316
|
+
# `environment.nominal_for_name`. Skips the placeholder
|
|
317
|
+
# `"untyped"` (Tier B's record-but-do-not-resolve marker
|
|
318
|
+
# from the slice-3b scanner) and the `"void"` keyword
|
|
319
|
+
# (RBS-style absent return). Falls back to nil when the
|
|
320
|
+
# class is not in the env — caller then returns Dynamic[T].
|
|
321
|
+
TIER_C_PLACEHOLDER_RETURNS = %w[untyped void].freeze
|
|
322
|
+
private_constant :TIER_C_PLACEHOLDER_RETURNS
|
|
323
|
+
|
|
324
|
+
def promote_via_return_type(synthetic, environment)
|
|
325
|
+
return_type = synthetic.return_type
|
|
326
|
+
return nil if return_type.nil? || TIER_C_PLACEHOLDER_RETURNS.include?(return_type)
|
|
327
|
+
|
|
328
|
+
environment.nominal_for_name(return_type)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def synthetic_method_class_name(receiver_type)
|
|
332
|
+
case receiver_type
|
|
333
|
+
when Type::Nominal, Type::Singleton then receiver_type.class_name
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
228
337
|
def try_dependency_source(receiver_type, method_name, environment)
|
|
229
338
|
index = environment&.dependency_source_index
|
|
230
339
|
return nil if index.nil? || index.empty?
|
|
@@ -469,9 +578,25 @@ module Rigor
|
|
|
469
578
|
Type::Combinator.nominal_of(receiver_type.class_name)
|
|
470
579
|
end
|
|
471
580
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
581
|
+
# ADR-15 Phase 4b.x — `Ractor.make_shareable` on both the
|
|
582
|
+
# outer Hash and each lambda value. A plain `.freeze` leaves
|
|
583
|
+
# the Procs unshareable; reading `CONSTANT_CONSTRUCTORS[class]`
|
|
584
|
+
# from a worker Ractor would raise `Ractor::IsolationError`,
|
|
585
|
+
# which the `rescue StandardError` in
|
|
586
|
+
# `constant_constructor_lift` silently swallows — `meta_new`
|
|
587
|
+
# then falls back to `Nominal[Pathname]` in pool mode while
|
|
588
|
+
# sequential builds the `Constant<Pathname>` lift. The
|
|
589
|
+
# divergence surfaces downstream as a spurious
|
|
590
|
+
# `call.argument-type-mismatch` (sequential's
|
|
591
|
+
# `argument_type_diagnostic` short-circuits on Constant<Pathname>
|
|
592
|
+
# because Pathname is not in its CONSTANT_CLASSES table; pool's
|
|
593
|
+
# Nominal[Pathname] doesn't short-circuit). Surfaced on GitLab
|
|
594
|
+
# FOSS via `lib/gitlab/mail_room.rb:17`.
|
|
595
|
+
CONSTANT_CONSTRUCTORS = Ractor.make_shareable({
|
|
596
|
+
"Pathname" => Ractor.make_shareable(lambda { |arg|
|
|
597
|
+
Pathname.new(arg)
|
|
598
|
+
})
|
|
599
|
+
})
|
|
475
600
|
private_constant :CONSTANT_CONSTRUCTORS
|
|
476
601
|
|
|
477
602
|
def constant_constructor_lift(class_name, arg_types)
|
|
@@ -211,20 +211,30 @@ module Rigor
|
|
|
211
211
|
# (`?by:`) while the Ruby `def` lists it as required (or vice
|
|
212
212
|
# versa); the binding is by-name regardless of which side
|
|
213
213
|
# defines it.
|
|
214
|
-
KEYWORD_PROVIDER = lambda do |fn, slot|
|
|
214
|
+
KEYWORD_PROVIDER = Ractor.make_shareable(lambda do |fn, slot|
|
|
215
215
|
fn.required_keywords[slot.name]&.type || fn.optional_keywords[slot.name]&.type
|
|
216
|
-
end
|
|
216
|
+
end)
|
|
217
217
|
private_constant :KEYWORD_PROVIDER
|
|
218
218
|
|
|
219
|
-
RBS_TYPE_PROVIDERS = {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
219
|
+
RBS_TYPE_PROVIDERS = Ractor.make_shareable({
|
|
220
|
+
required_positional: Ractor.make_shareable(lambda { |fn, slot|
|
|
221
|
+
fn.required_positionals[slot.index]&.type
|
|
222
|
+
}),
|
|
223
|
+
optional_positional: Ractor.make_shareable(lambda { |fn, slot|
|
|
224
|
+
fn.optional_positionals[slot.index]&.type
|
|
225
|
+
}),
|
|
226
|
+
rest_positional: Ractor.make_shareable(lambda { |fn, _slot|
|
|
227
|
+
fn.rest_positionals&.type
|
|
228
|
+
}),
|
|
229
|
+
trailing_positional: Ractor.make_shareable(lambda { |fn, slot|
|
|
230
|
+
fn.trailing_positionals[slot.index]&.type
|
|
231
|
+
}),
|
|
232
|
+
required_keyword: KEYWORD_PROVIDER,
|
|
233
|
+
optional_keyword: KEYWORD_PROVIDER,
|
|
234
|
+
rest_keyword: Ractor.make_shareable(lambda { |fn, _slot|
|
|
235
|
+
fn.rest_keywords&.type
|
|
236
|
+
})
|
|
237
|
+
})
|
|
228
238
|
private_constant :RBS_TYPE_PROVIDERS
|
|
229
239
|
|
|
230
240
|
def rbs_type_for_slot(function, slot)
|
|
@@ -384,6 +384,14 @@ module Rigor
|
|
|
384
384
|
analyse_statements(node, scope)
|
|
385
385
|
when Prism::LocalVariableReadNode
|
|
386
386
|
analyse_local_read(node, scope)
|
|
387
|
+
when Prism::LocalVariableWriteNode
|
|
388
|
+
analyse_local_write(node, scope)
|
|
389
|
+
when Prism::InstanceVariableWriteNode
|
|
390
|
+
analyse_ivar_write(node, scope)
|
|
391
|
+
when Prism::ClassVariableWriteNode
|
|
392
|
+
analyse_cvar_write(node, scope)
|
|
393
|
+
when Prism::GlobalVariableWriteNode
|
|
394
|
+
analyse_global_write(node, scope)
|
|
387
395
|
when Prism::CallNode
|
|
388
396
|
analyse_call(node, scope)
|
|
389
397
|
when Prism::AndNode
|
|
@@ -736,6 +744,59 @@ module Rigor
|
|
|
736
744
|
]
|
|
737
745
|
end
|
|
738
746
|
|
|
747
|
+
# Assignment-in-condition: `if name = expr` and the more
|
|
748
|
+
# frequent `if cond && (name = expr)` / `if cond && name =
|
|
749
|
+
# expr` Redmine-style guard. By the time narrowing runs,
|
|
750
|
+
# `StatementEvaluator#eval_local_write` has already bound
|
|
751
|
+
# the assigned local in `scope` to the rvalue type. The
|
|
752
|
+
# write's own truthiness IS the assigned value's
|
|
753
|
+
# truthiness, so the truthy edge narrows the local by
|
|
754
|
+
# `narrow_truthy(current)` and the falsey edge by
|
|
755
|
+
# `narrow_falsey(current)`. Mirrors `analyse_local_read`
|
|
756
|
+
# because the only meaningful difference between
|
|
757
|
+
# "predicate is `var`" and "predicate is `var = expr`" is
|
|
758
|
+
# which scope holds the just-bound value; the narrowing
|
|
759
|
+
# contract on the surrounding `if` is the same.
|
|
760
|
+
def analyse_local_write(node, scope)
|
|
761
|
+
current = scope.local(node.name)
|
|
762
|
+
return nil if current.nil?
|
|
763
|
+
|
|
764
|
+
[
|
|
765
|
+
scope.with_local(node.name, narrow_truthy(current)),
|
|
766
|
+
scope.with_local(node.name, narrow_falsey(current))
|
|
767
|
+
]
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def analyse_ivar_write(node, scope)
|
|
771
|
+
current = scope.ivar(node.name)
|
|
772
|
+
return nil if current.nil?
|
|
773
|
+
|
|
774
|
+
[
|
|
775
|
+
scope.with_ivar(node.name, narrow_truthy(current)),
|
|
776
|
+
scope.with_ivar(node.name, narrow_falsey(current))
|
|
777
|
+
]
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
def analyse_cvar_write(node, scope)
|
|
781
|
+
current = scope.cvar(node.name)
|
|
782
|
+
return nil if current.nil?
|
|
783
|
+
|
|
784
|
+
[
|
|
785
|
+
scope.with_cvar(node.name, narrow_truthy(current)),
|
|
786
|
+
scope.with_cvar(node.name, narrow_falsey(current))
|
|
787
|
+
]
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def analyse_global_write(node, scope)
|
|
791
|
+
current = scope.global(node.name)
|
|
792
|
+
return nil if current.nil?
|
|
793
|
+
|
|
794
|
+
[
|
|
795
|
+
scope.with_global(node.name, narrow_truthy(current)),
|
|
796
|
+
scope.with_global(node.name, narrow_falsey(current))
|
|
797
|
+
]
|
|
798
|
+
end
|
|
799
|
+
|
|
739
800
|
# `if /(?<x>...)/ =~ str` — Prism wraps the `=~` call in a
|
|
740
801
|
# `MatchWriteNode` listing the named-capture targets. The
|
|
741
802
|
# parent `eval_match_write` has already bound each target
|
|
@@ -916,12 +977,12 @@ module Rigor
|
|
|
916
977
|
# zero-arg predicates on `Numeric`. We model them as
|
|
917
978
|
# comparisons against the literal 0 so the existing range
|
|
918
979
|
# narrowing handles them uniformly.
|
|
919
|
-
ZERO_CLASS_PREDICATE_RULES = {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
980
|
+
ZERO_CLASS_PREDICATE_RULES = Ractor.make_shareable({
|
|
981
|
+
positive?: { truthy: [:>, 0], falsey: [:<=, 0] },
|
|
982
|
+
negative?: { truthy: [:<, 0], falsey: [:>=, 0] },
|
|
983
|
+
zero?: { truthy: [:eq, 0], falsey: [:ne, 0] },
|
|
984
|
+
nonzero?: { truthy: [:ne, 0], falsey: [:eq, 0] }
|
|
985
|
+
})
|
|
925
986
|
private_constant :ZERO_CLASS_PREDICATE_RULES
|
|
926
987
|
|
|
927
988
|
def analyse_zero_class_predicate(node, scope, predicate:)
|
|
@@ -1202,15 +1263,73 @@ module Rigor
|
|
|
1202
1263
|
return nil if node.arguments.nil?
|
|
1203
1264
|
return nil unless node.arguments.arguments.size == 1
|
|
1204
1265
|
|
|
1205
|
-
|
|
1206
|
-
return nil if
|
|
1266
|
+
bare_name = static_class_name(node.arguments.arguments.first)
|
|
1267
|
+
return nil if bare_name.nil?
|
|
1207
1268
|
|
|
1208
1269
|
current = scope.local(node.receiver.name)
|
|
1209
1270
|
return nil if current.nil?
|
|
1210
1271
|
|
|
1272
|
+
# Resolve `bare_name` through the lexical-scope chain
|
|
1273
|
+
# so a name shadowed by the current class / enclosing
|
|
1274
|
+
# module wins over the top-level constant. Mirrors
|
|
1275
|
+
# Ruby's `Module.nesting`-driven constant lookup. The
|
|
1276
|
+
# canonical motivating case: inside
|
|
1277
|
+
# `Rigor::Type::Singleton#==`, `is_a?(Singleton)`
|
|
1278
|
+
# should resolve to `Rigor::Type::Singleton`, not the
|
|
1279
|
+
# top-level stdlib `Singleton` mixin (which would
|
|
1280
|
+
# surface as a spurious `undefined-method` on
|
|
1281
|
+
# subsequent `other.class_name` calls).
|
|
1282
|
+
class_name = resolve_class_name_lexically(bare_name, scope)
|
|
1211
1283
|
class_predicate_scopes(scope, node.receiver.name, current, class_name, exact: exact)
|
|
1212
1284
|
end
|
|
1213
1285
|
|
|
1286
|
+
# Walks the lexical-nesting chain derived from
|
|
1287
|
+
# `scope.self_type` and returns the first
|
|
1288
|
+
# `<prefix>::<bare_name>` (or bare `<bare_name>` at the
|
|
1289
|
+
# top level) that the environment recognises. Falls back
|
|
1290
|
+
# to `bare_name` itself when nothing in the chain
|
|
1291
|
+
# resolves; the downstream `narrow_class` then yields
|
|
1292
|
+
# the conservative answer for unknown receivers.
|
|
1293
|
+
def resolve_class_name_lexically(bare_name, scope)
|
|
1294
|
+
return bare_name if bare_name.include?("::") # Already qualified.
|
|
1295
|
+
|
|
1296
|
+
chain = lexical_nesting_for(scope)
|
|
1297
|
+
chain.each do |prefix|
|
|
1298
|
+
candidate = "#{prefix}::#{bare_name}"
|
|
1299
|
+
return candidate if class_known_to_scope?(scope, candidate)
|
|
1300
|
+
end
|
|
1301
|
+
bare_name
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
# Combines the environment's RBS-known set with the
|
|
1305
|
+
# scope's in-source `discovered_classes` table so a
|
|
1306
|
+
# lexical-nesting candidate matches a class the project
|
|
1307
|
+
# declares but has no RBS for.
|
|
1308
|
+
def class_known_to_scope?(scope, candidate)
|
|
1309
|
+
return true if scope.environment.class_known?(candidate)
|
|
1310
|
+
|
|
1311
|
+
scope.discovered_classes.key?(candidate)
|
|
1312
|
+
end
|
|
1313
|
+
|
|
1314
|
+
# Approximates `Module.nesting` from the inferable
|
|
1315
|
+
# `self_type`. Today's implementation handles the common
|
|
1316
|
+
# case: when the surrounding method is a regular
|
|
1317
|
+
# instance method (`self_type = Nominal[T]`) or a
|
|
1318
|
+
# class-body / singleton (`self_type = Singleton[T]`),
|
|
1319
|
+
# the chain is `T`'s namespace path — `Foo::Bar::Baz`
|
|
1320
|
+
# → `["Foo::Bar::Baz", "Foo::Bar", "Foo"]`. Returns an
|
|
1321
|
+
# empty array when `self_type` is unknown.
|
|
1322
|
+
def lexical_nesting_for(scope)
|
|
1323
|
+
self_type = scope.self_type
|
|
1324
|
+
base = case self_type
|
|
1325
|
+
when Type::Nominal, Type::Singleton then self_type.class_name
|
|
1326
|
+
end
|
|
1327
|
+
return [] if base.nil? || base.empty?
|
|
1328
|
+
|
|
1329
|
+
parts = base.split("::")
|
|
1330
|
+
parts.each_index.map { |i| parts[0..-(i + 1)].join("::") }
|
|
1331
|
+
end
|
|
1332
|
+
|
|
1214
1333
|
def class_predicate_scopes(scope, name, current, class_name, exact:)
|
|
1215
1334
|
[
|
|
1216
1335
|
scope.with_local(
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# ADR-16 Tier C output — one synthetic method declared by a
|
|
6
|
+
# plugin's `Plugin::Macro::HeredocTemplate` entry, after the
|
|
7
|
+
# pre-pass has interpolated the call-site literal symbol into
|
|
8
|
+
# the template name. Stored in {SyntheticMethodIndex} and
|
|
9
|
+
# consulted by {MethodDispatcher} below the RBS dispatch tier.
|
|
10
|
+
#
|
|
11
|
+
# Per ADR-16 § WD13 (cost-bounded best-effort): the v0.1.x
|
|
12
|
+
# delivery commitment is the floor — method names emit; their
|
|
13
|
+
# return types degrade to `Dynamic[T]` until slice 6
|
|
14
|
+
# (precision promotion) routes the recorded `return_type`
|
|
15
|
+
# string through ADR-13's `Plugin::TypeNodeResolver` chain.
|
|
16
|
+
# The string is preserved so the ceiling slice can resolve it
|
|
17
|
+
# without re-walking.
|
|
18
|
+
#
|
|
19
|
+
# The `provenance` Hash carries debug / `--explain` metadata:
|
|
20
|
+
# plugin id, the template's call shape, and the source
|
|
21
|
+
# location of the originating DSL call. Surfaced through the
|
|
22
|
+
# dispatcher's `macro.tier_c.*` provenance markers.
|
|
23
|
+
class SyntheticMethod
|
|
24
|
+
INSTANCE = :instance
|
|
25
|
+
SINGLETON = :singleton
|
|
26
|
+
VALID_KINDS = [INSTANCE, SINGLETON].freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :class_name, :method_name, :return_type, :kind, :provenance
|
|
29
|
+
|
|
30
|
+
def initialize(class_name:, method_name:, return_type:, kind: INSTANCE, provenance: {})
|
|
31
|
+
validate!(class_name, method_name, return_type, kind, provenance)
|
|
32
|
+
@class_name = class_name.dup.freeze
|
|
33
|
+
@method_name = method_name.to_sym
|
|
34
|
+
@return_type = return_type.dup.freeze
|
|
35
|
+
@kind = kind
|
|
36
|
+
@provenance = provenance.transform_keys(&:to_sym).transform_values do |v|
|
|
37
|
+
v.is_a?(String) ? v.dup.freeze : v
|
|
38
|
+
end.freeze
|
|
39
|
+
freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def instance? = kind == INSTANCE
|
|
43
|
+
def singleton? = kind == SINGLETON
|
|
44
|
+
|
|
45
|
+
def to_h
|
|
46
|
+
{
|
|
47
|
+
"class_name" => class_name,
|
|
48
|
+
"method_name" => method_name.to_s,
|
|
49
|
+
"return_type" => return_type,
|
|
50
|
+
"kind" => kind.to_s,
|
|
51
|
+
"provenance" => provenance.transform_keys(&:to_s)
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ==(other)
|
|
56
|
+
other.is_a?(SyntheticMethod) && to_h == other.to_h
|
|
57
|
+
end
|
|
58
|
+
alias eql? ==
|
|
59
|
+
|
|
60
|
+
def hash
|
|
61
|
+
to_h.hash
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def validate!(class_name, method_name, return_type, kind, provenance)
|
|
67
|
+
unless class_name.is_a?(String) && !class_name.empty?
|
|
68
|
+
raise ArgumentError, "SyntheticMethod#class_name must be non-empty String, got #{class_name.inspect}"
|
|
69
|
+
end
|
|
70
|
+
unless method_name.is_a?(Symbol) || (method_name.is_a?(String) && !method_name.empty?)
|
|
71
|
+
raise ArgumentError, "SyntheticMethod#method_name must be Symbol/non-empty String, got #{method_name.inspect}"
|
|
72
|
+
end
|
|
73
|
+
unless return_type.is_a?(String) && !return_type.empty?
|
|
74
|
+
raise ArgumentError, "SyntheticMethod#return_type must be non-empty String, got #{return_type.inspect}"
|
|
75
|
+
end
|
|
76
|
+
unless VALID_KINDS.include?(kind)
|
|
77
|
+
raise ArgumentError, "SyntheticMethod#kind must be one of #{VALID_KINDS.inspect}, got #{kind.inspect}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
return if provenance.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
raise ArgumentError, "SyntheticMethod#provenance must be a Hash, got #{provenance.inspect}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "synthetic_method"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Frozen, Ractor-shareable lookup table for the synthetic
|
|
8
|
+
# methods emitted by ADR-16 Tier C declarations during a
|
|
9
|
+
# single `Analysis::Runner#run`. Constructed by the pre-pass
|
|
10
|
+
# scanner (see {SyntheticMethodScanner}) and consulted by
|
|
11
|
+
# {MethodDispatcher} below `RbsDispatch.try_dispatch` (per WD13:
|
|
12
|
+
# user-authored RBS overrides substrate synthesis).
|
|
13
|
+
#
|
|
14
|
+
# The index is keyed by `(class_name, method_name, kind)`. A
|
|
15
|
+
# single key may resolve to multiple {SyntheticMethod} records
|
|
16
|
+
# if two plugins emit the same name (e.g. `rigor-dry-struct`
|
|
17
|
+
# and a hypothetical `rigor-dry-struct-extras` both registering
|
|
18
|
+
# the same attribute). Per ADR-16 WD11 / the WD-discussion in
|
|
19
|
+
# `## Open questions` the dispatcher uses first-wins by
|
|
20
|
+
# registration order; this index preserves that order in
|
|
21
|
+
# `lookup`'s return.
|
|
22
|
+
#
|
|
23
|
+
# ## Slice 2b — return-type precision posture
|
|
24
|
+
#
|
|
25
|
+
# The recorded `SyntheticMethod#return_type` is a String
|
|
26
|
+
# (e.g. `"ActiveStorage::Attached::One"`), preserved verbatim
|
|
27
|
+
# from the manifest's emit table. Slice 2b's engine wiring
|
|
28
|
+
# treats every match as returning `Dynamic[T]` per WD13's
|
|
29
|
+
# floor — the recorded string is the input to a later slice's
|
|
30
|
+
# precision promotion via ADR-13's `Plugin::TypeNodeResolver`.
|
|
31
|
+
class SyntheticMethodIndex
|
|
32
|
+
attr_reader :entries
|
|
33
|
+
|
|
34
|
+
def initialize(entries: [])
|
|
35
|
+
unless entries.is_a?(Array) && entries.all?(SyntheticMethod)
|
|
36
|
+
raise ArgumentError,
|
|
37
|
+
"SyntheticMethodIndex#entries must be an Array of SyntheticMethod, got #{entries.inspect}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@entries = Ractor.make_shareable(entries.dup)
|
|
41
|
+
@by_instance = Ractor.make_shareable(bucket(entries, SyntheticMethod::INSTANCE))
|
|
42
|
+
@by_singleton = Ractor.make_shareable(bucket(entries, SyntheticMethod::SINGLETON))
|
|
43
|
+
freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def empty?
|
|
47
|
+
entries.empty?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns an Array of matching {SyntheticMethod} records in
|
|
51
|
+
# plugin-registration order. Empty Array when no plugin has
|
|
52
|
+
# declared a Tier C entry that interpolates to this name.
|
|
53
|
+
def lookup_instance(class_name, method_name)
|
|
54
|
+
@by_instance.fetch([class_name, method_name.to_sym], EMPTY_ROW)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def lookup_singleton(class_name, method_name)
|
|
58
|
+
@by_singleton.fetch([class_name, method_name.to_sym], EMPTY_ROW)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_h
|
|
62
|
+
{ "entries" => entries.map(&:to_h) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
EMPTY_ROW = [].freeze
|
|
66
|
+
|
|
67
|
+
def bucket(entries, kind)
|
|
68
|
+
h = {}
|
|
69
|
+
entries.each do |entry|
|
|
70
|
+
next unless entry.kind == kind
|
|
71
|
+
|
|
72
|
+
key = [entry.class_name, entry.method_name]
|
|
73
|
+
(h[key] ||= []) << entry
|
|
74
|
+
end
|
|
75
|
+
h.each_value(&:freeze).freeze
|
|
76
|
+
end
|
|
77
|
+
private :bucket
|
|
78
|
+
|
|
79
|
+
EMPTY = new(entries: []).freeze
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|