rigortype 0.1.4 → 0.1.6
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 +69 -56
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +11 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
- data/lib/rigor/analysis/fact_store.rb +15 -3
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +681 -19
- data/lib/rigor/analysis/worker_session.rb +339 -0
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/imported_refinements.rb +6 -2
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/builtins/static_return_refinements.rb +120 -0
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +72 -9
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +122 -10
- data/lib/rigor/configuration.rb +168 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- 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 +238 -7
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +179 -10
- data/lib/rigor/inference/acceptance.rb +83 -4
- 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 +59 -2
- data/lib/rigor/inference/hkt_body.rb +171 -0
- data/lib/rigor/inference/hkt_body_parser.rb +363 -0
- data/lib/rigor/inference/hkt_reducer.rb +256 -0
- data/lib/rigor/inference/hkt_registry.rb +223 -0
- 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 +126 -31
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
- data/lib/rigor/inference/method_dispatcher.rb +282 -6
- data/lib/rigor/inference/method_parameter_binder.rb +21 -11
- data/lib/rigor/inference/narrowing.rb +127 -8
- data/lib/rigor/inference/project_patched_methods.rb +70 -0
- data/lib/rigor/inference/project_patched_scanner.rb +210 -0
- data/lib/rigor/inference/scope_indexer.rb +156 -12
- data/lib/rigor/inference/statement_evaluator.rb +106 -6
- 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 +599 -0
- data/lib/rigor/language_server/buffer_table.rb +63 -0
- data/lib/rigor/language_server/completion_provider.rb +438 -0
- data/lib/rigor/language_server/debouncer.rb +86 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
- data/lib/rigor/language_server/folding_range_provider.rb +75 -0
- data/lib/rigor/language_server/hover_provider.rb +74 -0
- data/lib/rigor/language_server/hover_renderer.rb +312 -0
- data/lib/rigor/language_server/loop.rb +71 -0
- data/lib/rigor/language_server/project_context.rb +145 -0
- data/lib/rigor/language_server/selection_range_provider.rb +93 -0
- data/lib/rigor/language_server/server.rb +384 -0
- data/lib/rigor/language_server/signature_help_provider.rb +249 -0
- data/lib/rigor/language_server/synchronized_writer.rb +28 -0
- data/lib/rigor/language_server/uri.rb +40 -0
- data/lib/rigor/language_server.rb +29 -0
- data/lib/rigor/plugin/base.rb +63 -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 +315 -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 +127 -9
- data/lib/rigor/plugin/registry.rb +51 -2
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
- data/lib/rigor/rbs_extended.rb +82 -2
- data/lib/rigor/sig_gen/generator.rb +12 -3
- data/lib/rigor/trinary.rb +15 -11
- data/lib/rigor/type/app.rb +107 -0
- 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.rb +1 -0
- 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 +11 -4
- data/sig/rigor/inference.rbs +2 -0
- 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 +37 -2
- metadata +92 -1
|
@@ -4,6 +4,8 @@ require_relative "../reflection"
|
|
|
4
4
|
require_relative "../type"
|
|
5
5
|
require_relative "../flow_contribution"
|
|
6
6
|
require_relative "../flow_contribution/merger"
|
|
7
|
+
require_relative "../builtins/hkt_builtins"
|
|
8
|
+
require_relative "../builtins/static_return_refinements"
|
|
7
9
|
require_relative "method_dispatcher/constant_folding"
|
|
8
10
|
require_relative "method_dispatcher/literal_string_folding"
|
|
9
11
|
require_relative "method_dispatcher/shape_dispatch"
|
|
@@ -62,7 +64,7 @@ module Rigor
|
|
|
62
64
|
# @param environment [Rigor::Environment, nil] required for
|
|
63
65
|
# RBS-backed dispatch; when nil only constant folding can fire.
|
|
64
66
|
# @return [Rigor::Type, nil] inferred result type, or `nil` for "no rule".
|
|
65
|
-
def dispatch(receiver_type:, method_name:, arg_types:,
|
|
67
|
+
def dispatch(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/MethodLength
|
|
66
68
|
block_type: nil, environment: nil,
|
|
67
69
|
call_node: nil, scope: nil)
|
|
68
70
|
return nil if receiver_type.nil?
|
|
@@ -88,6 +90,32 @@ module Rigor
|
|
|
88
90
|
plugin_result = try_plugin_contribution(call_node, scope)
|
|
89
91
|
return plugin_result if plugin_result
|
|
90
92
|
|
|
93
|
+
# ADR-20 slice 3 — Rigor-bundled HKT-builtin return-
|
|
94
|
+
# type tier. Sits ABOVE `RbsDispatch.try_dispatch` so
|
|
95
|
+
# the handful of stdlib methods whose upstream RBS
|
|
96
|
+
# signature is `untyped` but whose runtime shape Rigor
|
|
97
|
+
# models via a Lightweight HKT (`json::value`,
|
|
98
|
+
# eventually `dry_monads::result`, …) get the reduced
|
|
99
|
+
# type instead of `Dynamic[Top]`. The table that
|
|
100
|
+
# populates this tier lives in
|
|
101
|
+
# `Rigor::Builtins::HktBuiltins::METHOD_RETURN_OVERRIDES`;
|
|
102
|
+
# plugin-supplied per-method overrides are out of
|
|
103
|
+
# scope for slice 3 and continue to flow through the
|
|
104
|
+
# `try_plugin_contribution` tier above.
|
|
105
|
+
hkt_builtin_result = try_hkt_builtin_return(receiver_type, method_name, arg_types, environment)
|
|
106
|
+
return hkt_builtin_result if hkt_builtin_result
|
|
107
|
+
|
|
108
|
+
# Rigor-bundled static refinement tier. Sits between HKT
|
|
109
|
+
# and RBS so stdlib methods whose upstream RBS is broader
|
|
110
|
+
# than the documented behaviour (e.g. `Kernel#__dir__`
|
|
111
|
+
# declared `() -> String?` when the documented return is
|
|
112
|
+
# `non-empty-string | nil`) get the tightened type
|
|
113
|
+
# without modifying the vendored `ruby/rbs` submodule.
|
|
114
|
+
# The override table lives in
|
|
115
|
+
# `Rigor::Builtins::StaticReturnRefinements::OVERRIDES`.
|
|
116
|
+
static_refinement = try_static_refinement(receiver_type, method_name, arg_types)
|
|
117
|
+
return static_refinement if static_refinement
|
|
118
|
+
|
|
91
119
|
rbs_result = RbsDispatch.try_dispatch(
|
|
92
120
|
receiver: receiver_type, method_name: method_name, args: arg_types,
|
|
93
121
|
environment: environment, block_type: block_type
|
|
@@ -97,6 +125,34 @@ module Rigor
|
|
|
97
125
|
return rbs_result
|
|
98
126
|
end
|
|
99
127
|
|
|
128
|
+
# ADR-16 Tier B / Tier C — synthetic-method tier. Sits
|
|
129
|
+
# BELOW RBS dispatch (per WD13: user-authored RBS overrides
|
|
130
|
+
# substrate synthesis) and ABOVE the dependency-source
|
|
131
|
+
# inference tier so a plugin's declared emit table beats
|
|
132
|
+
# the generic gem-source fallback for the same class. Slice
|
|
133
|
+
# 6a-TierB (origin_module dispatch) lands precise return
|
|
134
|
+
# types for Tier B emissions; Tier C emissions still return
|
|
135
|
+
# `Dynamic[T]` at this tier (slice 6b is the Tier C
|
|
136
|
+
# promotion via ADR-13's resolver chain).
|
|
137
|
+
synthetic_result = try_synthetic_method(
|
|
138
|
+
receiver_type, method_name, arg_types, block_type, environment
|
|
139
|
+
)
|
|
140
|
+
return synthetic_result if synthetic_result
|
|
141
|
+
|
|
142
|
+
# ADR-17 slice 2 — project-side patched-method tier.
|
|
143
|
+
# Sits BELOW the substrate / plugin tiers and ABOVE
|
|
144
|
+
# dependency-source inference per ADR-17 § "Inference
|
|
145
|
+
# contract". When the user's `pre_eval:` list named a
|
|
146
|
+
# file that re-opens a class (e.g.,
|
|
147
|
+
# `lib/core_ext/string_extensions.rb` declaring
|
|
148
|
+
# `class String; def to_url; end; end`), the pre-pass
|
|
149
|
+
# populated `ProjectPatchedMethods` with the `(class,
|
|
150
|
+
# method, kind)` triple; this tier surfaces it as
|
|
151
|
+
# `Dynamic[top]` so the patched call resolves
|
|
152
|
+
# cross-file without `call.undefined-method`.
|
|
153
|
+
patched_result = try_project_patched_method(receiver_type, method_name, environment)
|
|
154
|
+
return patched_result if patched_result
|
|
155
|
+
|
|
100
156
|
# ADR-10 slice 2b-ii — dependency-source inference tier.
|
|
101
157
|
# Sits BELOW RBS dispatch (RBS / RBS::Inline / generated
|
|
102
158
|
# stubs / plugin contracts always win) and ABOVE the
|
|
@@ -203,6 +259,80 @@ module Rigor
|
|
|
203
259
|
# keeps moving — the run-level diagnostic envelope (per
|
|
204
260
|
# ADR-2 § "Plugin Trust and I/O Policy") is owned by
|
|
205
261
|
# `Analysis::Runner#plugin_emitted_diagnostics`.
|
|
262
|
+
# ADR-20 slice 3 — looks up the receiver / method pair
|
|
263
|
+
# in {Rigor::Builtins::HktBuiltins::METHOD_RETURN_OVERRIDES}
|
|
264
|
+
# and returns the reduced HKT type. Only fires when the
|
|
265
|
+
# receiver is a {Rigor::Type::Singleton} (the
|
|
266
|
+
# `JSON.parse` shape) and the registry-backed reduction
|
|
267
|
+
# succeeds; returns `nil` otherwise so the dispatcher
|
|
268
|
+
# falls through to RBS.
|
|
269
|
+
def try_hkt_builtin_return(receiver_type, method_name, arg_types, environment)
|
|
270
|
+
return nil if environment.nil?
|
|
271
|
+
return nil unless receiver_type.is_a?(Type::Singleton)
|
|
272
|
+
|
|
273
|
+
Rigor::Builtins::HktBuiltins.method_return_override(
|
|
274
|
+
class_name: receiver_type.class_name,
|
|
275
|
+
method_name: method_name,
|
|
276
|
+
kind: :singleton,
|
|
277
|
+
arg_types: arg_types,
|
|
278
|
+
hkt_registry: environment.hkt_registry
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Consults the Rigor-bundled static refinement table for a
|
|
283
|
+
# (owner-class, method-name, kind) entry. Kernel methods
|
|
284
|
+
# are mixed into every non-BasicObject class, so an
|
|
285
|
+
# implicit-self `__dir__` call (receiver_type =
|
|
286
|
+
# Nominal[ClassName]) is matched by looking up Kernel as
|
|
287
|
+
# the owner. Explicit `Kernel.__dir__` (receiver_type =
|
|
288
|
+
# Singleton[Kernel]) and instance-side calls
|
|
289
|
+
# (receiver_type = Nominal[Klass]) share the `:both` row.
|
|
290
|
+
#
|
|
291
|
+
# The receiver-side ancestor check is intentionally cheap:
|
|
292
|
+
# any non-BasicObject Nominal / Singleton matches every
|
|
293
|
+
# Kernel-owned override. BasicObject explicitly excludes
|
|
294
|
+
# Kernel and is therefore rejected. The narrow risk of a
|
|
295
|
+
# user-defined `def __dir__` shadowing Kernel's method
|
|
296
|
+
# would also alter the runtime answer; users with that
|
|
297
|
+
# configuration opt out via a `signature_paths` overlay
|
|
298
|
+
# declaring their own return type.
|
|
299
|
+
def try_static_refinement(receiver_type, method_name, arg_types)
|
|
300
|
+
candidates = Rigor::Builtins::StaticReturnRefinements.owners_for(method_name)
|
|
301
|
+
return nil if candidates.empty?
|
|
302
|
+
|
|
303
|
+
owner = static_refinement_owner_for(receiver_type, candidates)
|
|
304
|
+
return nil unless owner
|
|
305
|
+
|
|
306
|
+
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
307
|
+
Rigor::Builtins::StaticReturnRefinements.lookup(
|
|
308
|
+
owner_class_name: owner,
|
|
309
|
+
method_name: method_name,
|
|
310
|
+
kind: kind,
|
|
311
|
+
arg_types: arg_types
|
|
312
|
+
)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Picks the most specific override owner the receiver
|
|
316
|
+
# honours. For Kernel-owned overrides the receiver simply
|
|
317
|
+
# needs to be a real-class Nominal / Singleton (i.e. not
|
|
318
|
+
# BasicObject and not a Dynamic / Constant / shape carrier
|
|
319
|
+
# — those carriers go through their own narrower tiers).
|
|
320
|
+
def static_refinement_owner_for(receiver_type, candidates)
|
|
321
|
+
receiver_class = static_refinement_class_for(receiver_type)
|
|
322
|
+
return nil unless receiver_class
|
|
323
|
+
|
|
324
|
+
return "Kernel" if candidates.include?("Kernel") && receiver_class != "BasicObject"
|
|
325
|
+
|
|
326
|
+
candidates.find { |owner| owner == receiver_class }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def static_refinement_class_for(receiver_type)
|
|
330
|
+
case receiver_type
|
|
331
|
+
when Type::Singleton, Type::Nominal
|
|
332
|
+
receiver_type.class_name
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
206
336
|
def try_plugin_contribution(call_node, scope)
|
|
207
337
|
return nil if call_node.nil? || scope.nil?
|
|
208
338
|
|
|
@@ -225,6 +355,125 @@ module Rigor
|
|
|
225
355
|
# publish as ground-truth `T`). Returns `nil` when the
|
|
226
356
|
# environment carries no index, the index has no entry, or
|
|
227
357
|
# the receiver has no nominal class to look up.
|
|
358
|
+
# ADR-16 synthetic-method tier. Slice 2b shipped the floor —
|
|
359
|
+
# a match short-circuits at the right precedence (above
|
|
360
|
+
# dep-source / discovered / user-class-fallback; below RBS)
|
|
361
|
+
# and returns `Dynamic[T]`. Slice 6 (precision promotion):
|
|
362
|
+
# - Tier B path (slice 6a, `provenance[:origin_module]`
|
|
363
|
+
# recorded by the slice-3b scanner): redispatch on
|
|
364
|
+
# `Nominal[origin_module]` via `RbsDispatch` so the
|
|
365
|
+
# module's authored RBS return type wins. Devise's
|
|
366
|
+
# `valid_password?` returns `bool`, not `Dynamic[T]`.
|
|
367
|
+
# - Tier C path (slice 6b, plain `return_type:` string from
|
|
368
|
+
# the manifest's emit table): look up
|
|
369
|
+
# `environment.nominal_for_name(return_type)` so
|
|
370
|
+
# `attribute :avatar, Types::String` emits a synthetic
|
|
371
|
+
# reader returning `Nominal[ActiveStorage::Attached::One]`
|
|
372
|
+
# (when the class is in RBS). Unparameterised class names
|
|
373
|
+
# only — parameterised forms (`Array[String]`,
|
|
374
|
+
# `Hash[K, V]`) and plugin-supplied utility-type names
|
|
375
|
+
# (`Pick<T, K>`) require routing through the full ADR-13
|
|
376
|
+
# `Plugin::TypeNodeResolver` chain, which slice 6 does
|
|
377
|
+
# not yet wire in (the resolver chain is consulted only
|
|
378
|
+
# for `%a{rigor:v1:…}` payloads as of ADR-13 slice 3).
|
|
379
|
+
def try_synthetic_method(receiver_type, method_name, arg_types, block_type, environment)
|
|
380
|
+
index = environment&.synthetic_method_index
|
|
381
|
+
return nil if index.nil? || index.empty?
|
|
382
|
+
|
|
383
|
+
class_name = synthetic_method_class_name(receiver_type)
|
|
384
|
+
return nil if class_name.nil?
|
|
385
|
+
|
|
386
|
+
matches = case receiver_type
|
|
387
|
+
when Type::Singleton then index.lookup_singleton(class_name, method_name)
|
|
388
|
+
else index.lookup_instance(class_name, method_name)
|
|
389
|
+
end
|
|
390
|
+
return nil if matches.empty?
|
|
391
|
+
|
|
392
|
+
promoted = promote_synthetic_match(matches, method_name, arg_types, block_type, environment)
|
|
393
|
+
promoted || Type::Combinator.untyped
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# First non-nil promotion wins. Tier B (origin_module) and
|
|
397
|
+
# Tier C (return_type nominal lookup) are tried in the
|
|
398
|
+
# same registration-order pass per WD11 first-wins —
|
|
399
|
+
# the slice-3b scanner sets `origin_module` for Tier B
|
|
400
|
+
# entries and leaves it absent for Tier C, so the two
|
|
401
|
+
# paths self-route per match.
|
|
402
|
+
def promote_synthetic_match(matches, method_name, arg_types, block_type, environment)
|
|
403
|
+
return nil if environment.nil?
|
|
404
|
+
|
|
405
|
+
matches.each do |synthetic|
|
|
406
|
+
promoted =
|
|
407
|
+
promote_via_origin_module(synthetic, method_name, arg_types, block_type, environment) ||
|
|
408
|
+
promote_via_return_type(synthetic, environment)
|
|
409
|
+
return promoted if promoted
|
|
410
|
+
end
|
|
411
|
+
nil
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Slice 6a-TierB. For Tier B emissions (origin_module
|
|
415
|
+
# recorded in provenance), redispatch the call on the
|
|
416
|
+
# included module's `Nominal[...]` type via `RbsDispatch`.
|
|
417
|
+
# Returns nil when the SyntheticMethod is not a Tier B
|
|
418
|
+
# entry or when the origin_module is not in the RBS env.
|
|
419
|
+
def promote_via_origin_module(synthetic, method_name, arg_types, block_type, environment)
|
|
420
|
+
module_name = synthetic.provenance[:origin_module]
|
|
421
|
+
return nil unless module_name
|
|
422
|
+
|
|
423
|
+
module_type = Type::Combinator.nominal_of(module_name)
|
|
424
|
+
RbsDispatch.try_dispatch(
|
|
425
|
+
receiver: module_type, method_name: method_name, args: arg_types,
|
|
426
|
+
environment: environment, block_type: block_type
|
|
427
|
+
)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Slice 6b-TierC. For Tier C emissions, look up the
|
|
431
|
+
# manifest-declared `return_type:` string via
|
|
432
|
+
# `environment.nominal_for_name`. Skips the placeholder
|
|
433
|
+
# `"untyped"` (Tier B's record-but-do-not-resolve marker
|
|
434
|
+
# from the slice-3b scanner) and the `"void"` keyword
|
|
435
|
+
# (RBS-style absent return). Falls back to nil when the
|
|
436
|
+
# class is not in the env — caller then returns Dynamic[T].
|
|
437
|
+
TIER_C_PLACEHOLDER_RETURNS = %w[untyped void].freeze
|
|
438
|
+
private_constant :TIER_C_PLACEHOLDER_RETURNS
|
|
439
|
+
|
|
440
|
+
def promote_via_return_type(synthetic, environment)
|
|
441
|
+
return_type = synthetic.return_type
|
|
442
|
+
return nil if return_type.nil? || TIER_C_PLACEHOLDER_RETURNS.include?(return_type)
|
|
443
|
+
|
|
444
|
+
environment.nominal_for_name(return_type)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def synthetic_method_class_name(receiver_type)
|
|
448
|
+
case receiver_type
|
|
449
|
+
when Type::Nominal, Type::Singleton then receiver_type.class_name
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# ADR-17 slice 2 — project-side patched-method tier.
|
|
454
|
+
# Slice 3a uses the registry's heuristic-extracted
|
|
455
|
+
# `return_type` (populated via the same
|
|
456
|
+
# `Analysis::DependencySourceInference::ReturnTypeHeuristic`
|
|
457
|
+
# the ADR-10 walker uses): a `def to_url; "hello"; end`
|
|
458
|
+
# patched onto `String` now resolves `s.to_url` to
|
|
459
|
+
# `Dynamic[Nominal[String]]` instead of the pre-3a
|
|
460
|
+
# `Dynamic[Top]`. Falls back to `Dynamic[Top]` when the
|
|
461
|
+
# heuristic declined (non-literal tail expression).
|
|
462
|
+
def try_project_patched_method(receiver_type, method_name, environment)
|
|
463
|
+
registry = environment&.project_patched_methods
|
|
464
|
+
return nil if registry.nil? || registry.empty?
|
|
465
|
+
|
|
466
|
+
class_name = synthetic_method_class_name(receiver_type)
|
|
467
|
+
return nil if class_name.nil?
|
|
468
|
+
|
|
469
|
+
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
470
|
+
entry = registry.lookup(class_name: class_name, method_name: method_name, kind: kind)
|
|
471
|
+
return nil if entry.nil?
|
|
472
|
+
return Type::Combinator.untyped if entry.return_type.nil?
|
|
473
|
+
|
|
474
|
+
Type::Combinator.dynamic(entry.return_type)
|
|
475
|
+
end
|
|
476
|
+
|
|
228
477
|
def try_dependency_source(receiver_type, method_name, environment)
|
|
229
478
|
index = environment&.dependency_source_index
|
|
230
479
|
return nil if index.nil? || index.empty?
|
|
@@ -241,8 +490,8 @@ module Rigor
|
|
|
241
490
|
# inference must not contribute behind their backs.
|
|
242
491
|
return nil if plugin_owns_receiver?(class_name, environment)
|
|
243
492
|
|
|
244
|
-
|
|
245
|
-
return
|
|
493
|
+
contribution = index.contribution_for(class_name: class_name, method_name: method_name)
|
|
494
|
+
return dependency_source_return_type(contribution) if contribution
|
|
246
495
|
|
|
247
496
|
# ADR-10 5b — β budget semantics. On a catalog miss,
|
|
248
497
|
# if the receiver class belongs to a budget-exceeded
|
|
@@ -294,6 +543,17 @@ module Rigor
|
|
|
294
543
|
)
|
|
295
544
|
end
|
|
296
545
|
|
|
546
|
+
# Maps a {DependencySourceInference::Walker::CatalogEntry}
|
|
547
|
+
# to the Type the dispatcher returns at the call site.
|
|
548
|
+
# When the heuristic recovered a static facet, wrap it in
|
|
549
|
+
# `Dynamic[T]` per ADR-10's gem-boundary contract;
|
|
550
|
+
# otherwise fall back to the pre-heuristic `Dynamic[top]`.
|
|
551
|
+
def dependency_source_return_type(contribution)
|
|
552
|
+
return Type::Combinator.untyped if contribution.return_type.nil?
|
|
553
|
+
|
|
554
|
+
Type::Combinator.dynamic(contribution.return_type)
|
|
555
|
+
end
|
|
556
|
+
|
|
297
557
|
# Composite preflight for {#record_boundary_cross_if_applicable}.
|
|
298
558
|
# Returns the receiver class name only when every prerequisite
|
|
299
559
|
# for emitting the diagnostic is satisfied (environment carries
|
|
@@ -469,9 +729,25 @@ module Rigor
|
|
|
469
729
|
Type::Combinator.nominal_of(receiver_type.class_name)
|
|
470
730
|
end
|
|
471
731
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
732
|
+
# ADR-15 Phase 4b.x — `Ractor.make_shareable` on both the
|
|
733
|
+
# outer Hash and each lambda value. A plain `.freeze` leaves
|
|
734
|
+
# the Procs unshareable; reading `CONSTANT_CONSTRUCTORS[class]`
|
|
735
|
+
# from a worker Ractor would raise `Ractor::IsolationError`,
|
|
736
|
+
# which the `rescue StandardError` in
|
|
737
|
+
# `constant_constructor_lift` silently swallows — `meta_new`
|
|
738
|
+
# then falls back to `Nominal[Pathname]` in pool mode while
|
|
739
|
+
# sequential builds the `Constant<Pathname>` lift. The
|
|
740
|
+
# divergence surfaces downstream as a spurious
|
|
741
|
+
# `call.argument-type-mismatch` (sequential's
|
|
742
|
+
# `argument_type_diagnostic` short-circuits on Constant<Pathname>
|
|
743
|
+
# because Pathname is not in its CONSTANT_CLASSES table; pool's
|
|
744
|
+
# Nominal[Pathname] doesn't short-circuit). Surfaced on GitLab
|
|
745
|
+
# FOSS via `lib/gitlab/mail_room.rb:17`.
|
|
746
|
+
CONSTANT_CONSTRUCTORS = Ractor.make_shareable({
|
|
747
|
+
"Pathname" => Ractor.make_shareable(lambda { |arg|
|
|
748
|
+
Pathname.new(arg)
|
|
749
|
+
})
|
|
750
|
+
})
|
|
475
751
|
private_constant :CONSTANT_CONSTRUCTORS
|
|
476
752
|
|
|
477
753
|
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,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# ADR-17 § "Inference contract" — project-wide patched-method
|
|
6
|
+
# registry populated by the pre-eval pre-pass (slice 2) from
|
|
7
|
+
# the user's `.rigor.yml` `pre_eval:` list.
|
|
8
|
+
#
|
|
9
|
+
# Each entry records one `def` declaration the pre-pass
|
|
10
|
+
# observed inside a class / module body. The dispatcher's
|
|
11
|
+
# `try_project_patched_method` tier consults this registry
|
|
12
|
+
# between the plugin tier and the dependency-source tier so
|
|
13
|
+
# project-side `lib/core_ext/string_extensions.rb` patches
|
|
14
|
+
# are visible to cross-file dispatch.
|
|
15
|
+
#
|
|
16
|
+
# Slice 2 ships the registry at the **floor**: the dispatcher
|
|
17
|
+
# answers `Type::Combinator.untyped` (Dynamic[Top]) on a hit;
|
|
18
|
+
# return-type inference for patched methods stays deferred
|
|
19
|
+
# (a separate slice when concrete demand surfaces — most
|
|
20
|
+
# real-world `core_ext` patches return shapes the analyzer
|
|
21
|
+
# could heuristically extract via the same machinery the
|
|
22
|
+
# ADR-10 walker uses, but slice 2 keeps the surface narrow).
|
|
23
|
+
class ProjectPatchedMethods
|
|
24
|
+
# Frozen value-object recording one `def` observed by the
|
|
25
|
+
# pre-pass. `class_name` is the qualified prefix
|
|
26
|
+
# (`"String"`, `"Foo::Bar"`); `method_name` is the
|
|
27
|
+
# declared name; `kind` is `:instance` or `:singleton`;
|
|
28
|
+
# `source_path` / `source_line` carry attribution for
|
|
29
|
+
# diagnostics; `return_type` is the
|
|
30
|
+
# {Analysis::DependencySourceInference::ReturnTypeHeuristic}-
|
|
31
|
+
# extracted static facet (a `Rigor::Type::*`) or `nil`
|
|
32
|
+
# when the heuristic declined. The dispatcher wraps a
|
|
33
|
+
# non-nil `return_type` in `Dynamic[T]`; a `nil`
|
|
34
|
+
# `return_type` falls back to `Dynamic[top]`.
|
|
35
|
+
Entry = Data.define(:class_name, :method_name, :kind, :source_path, :source_line, :return_type) do
|
|
36
|
+
def initialize(class_name:, method_name:, kind:, source_path:, source_line:, return_type: nil)
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attr_reader :by_key
|
|
42
|
+
|
|
43
|
+
# @param entries [Array<Entry>] flat list of declarations
|
|
44
|
+
# observed during the pre-pass. First-write-wins on
|
|
45
|
+
# `(class_name, method_name, kind)` duplicates so the
|
|
46
|
+
# `pre-eval.duplicate-declaration` diagnostic emission
|
|
47
|
+
# stays decoupled from registry behaviour.
|
|
48
|
+
def initialize(entries: [])
|
|
49
|
+
@by_key = entries.each_with_object({}) do |entry, acc|
|
|
50
|
+
key = [entry.class_name, entry.method_name, entry.kind]
|
|
51
|
+
acc[key] ||= entry
|
|
52
|
+
end.freeze
|
|
53
|
+
freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Entry, nil] the recorded entry for the given
|
|
57
|
+
# `(class_name, method_name, kind)` triple, or `nil`
|
|
58
|
+
# when no pre-eval file declared it.
|
|
59
|
+
def lookup(class_name:, method_name:, kind:)
|
|
60
|
+
@by_key[[class_name, method_name, kind]]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def empty?
|
|
64
|
+
@by_key.empty?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
EMPTY = new.freeze
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|