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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +125 -31
  3. data/lib/rigor/analysis/check_rules.rb +10 -18
  4. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
  5. data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
  9. data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
  10. data/lib/rigor/analysis/diagnostic.rb +0 -2
  11. data/lib/rigor/analysis/fact_store.rb +11 -3
  12. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  13. data/lib/rigor/analysis/runner.rb +114 -3
  14. data/lib/rigor/builtins/imported_refinements.rb +360 -55
  15. data/lib/rigor/cache/descriptor.rb +1 -1
  16. data/lib/rigor/cache/store.rb +1 -1
  17. data/lib/rigor/cli/diff_command.rb +1 -1
  18. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  19. data/lib/rigor/cli/type_of_command.rb +1 -1
  20. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  21. data/lib/rigor/cli/type_scan_report.rb +2 -2
  22. data/lib/rigor/cli.rb +9 -1
  23. data/lib/rigor/configuration/dependencies.rb +2 -2
  24. data/lib/rigor/configuration.rb +2 -2
  25. data/lib/rigor/environment.rb +35 -4
  26. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  27. data/lib/rigor/flow_contribution/element.rb +1 -1
  28. data/lib/rigor/flow_contribution/fact.rb +1 -1
  29. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  30. data/lib/rigor/flow_contribution/merger.rb +3 -3
  31. data/lib/rigor/flow_contribution.rb +2 -2
  32. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  33. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  34. data/lib/rigor/inference/expression_typer.rb +67 -11
  35. data/lib/rigor/inference/fallback.rb +1 -1
  36. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  37. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
  38. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  39. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  40. data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
  41. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
  42. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  43. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
  44. data/lib/rigor/inference/method_dispatcher.rb +146 -2
  45. data/lib/rigor/inference/method_parameter_binder.rb +1 -3
  46. data/lib/rigor/inference/narrowing.rb +2 -4
  47. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  48. data/lib/rigor/inference/scope_indexer.rb +14 -9
  49. data/lib/rigor/inference/statement_evaluator.rb +7 -7
  50. data/lib/rigor/plugin/io_boundary.rb +0 -2
  51. data/lib/rigor/plugin/loader.rb +2 -2
  52. data/lib/rigor/plugin/manifest.rb +30 -9
  53. data/lib/rigor/plugin/registry.rb +11 -0
  54. data/lib/rigor/plugin/services.rb +1 -1
  55. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  56. data/lib/rigor/plugin.rb +1 -0
  57. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  58. data/lib/rigor/rbs_extended.rb +131 -32
  59. data/lib/rigor/scope.rb +25 -8
  60. data/lib/rigor/sig_gen/classification.rb +36 -0
  61. data/lib/rigor/sig_gen/generator.rb +1048 -0
  62. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  63. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  64. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  65. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  66. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  67. data/lib/rigor/sig_gen/renderer.rb +157 -0
  68. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  69. data/lib/rigor/sig_gen/write_result.rb +48 -0
  70. data/lib/rigor/sig_gen/writer.rb +530 -0
  71. data/lib/rigor/sig_gen.rb +25 -0
  72. data/lib/rigor/type/bound_method.rb +79 -0
  73. data/lib/rigor/type/combinator.rb +195 -2
  74. data/lib/rigor/type/constant.rb +13 -0
  75. data/lib/rigor/type/hash_shape.rb +0 -2
  76. data/lib/rigor/type/union.rb +20 -1
  77. data/lib/rigor/type.rb +1 -0
  78. data/lib/rigor/type_node/generic.rb +62 -0
  79. data/lib/rigor/type_node/identifier.rb +30 -0
  80. data/lib/rigor/type_node/indexed_access.rb +41 -0
  81. data/lib/rigor/type_node/integer_literal.rb +29 -0
  82. data/lib/rigor/type_node/name_scope.rb +52 -0
  83. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  84. data/lib/rigor/type_node/string_literal.rb +29 -0
  85. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  86. data/lib/rigor/type_node/union.rb +42 -0
  87. data/lib/rigor/type_node.rb +29 -0
  88. data/lib/rigor/version.rb +1 -1
  89. data/lib/rigor.rb +2 -0
  90. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  91. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  92. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  93. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  94. data/sig/rigor/cli/diff_command.rbs +4 -0
  95. data/sig/rigor/cli/explain_command.rbs +4 -0
  96. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  97. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  98. data/sig/rigor/environment.rbs +5 -2
  99. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  100. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  101. data/sig/rigor/inference/builtins.rbs +2 -0
  102. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  103. data/sig/rigor/plugin/base.rbs +6 -0
  104. data/sig/rigor/plugin/fact_store.rbs +11 -0
  105. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  106. data/sig/rigor/plugin/load_error.rbs +6 -0
  107. data/sig/rigor/plugin/loader.rbs +20 -0
  108. data/sig/rigor/plugin/manifest.rbs +9 -0
  109. data/sig/rigor/plugin/registry.rbs +3 -0
  110. data/sig/rigor/plugin/services.rbs +3 -0
  111. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  112. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  113. data/sig/rigor/plugin.rbs +8 -0
  114. data/sig/rigor/scope.rbs +4 -2
  115. data/sig/rigor/type.rbs +28 -6
  116. metadata +52 -1
@@ -3,6 +3,7 @@
3
3
  require "strscan"
4
4
 
5
5
  require_relative "../type"
6
+ require_relative "../type_node"
6
7
 
7
8
  module Rigor
8
9
  module Builtins
@@ -118,6 +119,38 @@ module Rigor
118
119
  return nil unless args.size == 1
119
120
 
120
121
  Type::Combinator.int_mask_of(args.first)
122
+ },
123
+ # ADR-13 § "Canonical type-function additions" — five
124
+ # shape-projection type functions that the
125
+ # `rigor-typescript-utility-types` plugin (and any other
126
+ # plugin that ships a shape-projection vocabulary) maps
127
+ # onto. Phase A handles `HashShape` carriers; non-shape
128
+ # inputs return the input unchanged (the lossy-projection
129
+ # diagnostic lands in slice 5).
130
+ "pick_of" => lambda { |args|
131
+ return nil unless args.size == 2
132
+
133
+ Type::Combinator.pick_of(args[0], args[1])
134
+ },
135
+ "omit_of" => lambda { |args|
136
+ return nil unless args.size == 2
137
+
138
+ Type::Combinator.omit_of(args[0], args[1])
139
+ },
140
+ "partial_of" => lambda { |args|
141
+ return nil unless args.size == 1
142
+
143
+ Type::Combinator.partial_of(args.first)
144
+ },
145
+ "required_of" => lambda { |args|
146
+ return nil unless args.size == 1
147
+
148
+ Type::Combinator.required_of(args.first)
149
+ },
150
+ "readonly_of" => lambda { |args|
151
+ return nil unless args.size == 1
152
+
153
+ Type::Combinator.readonly_of(args.first)
121
154
  }
122
155
  }.freeze
123
156
  private_constant :PARAMETERISED_TYPE_BUILDERS
@@ -151,11 +184,46 @@ module Rigor
151
184
  # `rigor:v1:return:` (or sibling) directive. Accepts
152
185
  # the bare-name forms `lookup` already handles plus the
153
186
  # parameterised forms documented on {Parser}.
187
+ # @param name_scope [Rigor::TypeNode::NameScope, nil]
188
+ # ADR-13 slice 3 — when provided, the parser consults the
189
+ # scope's `#resolver` chain after the built-in registry
190
+ # and built-in parametric forms but before the RBS Nominal
191
+ # fallback. `nil` (default) preserves the slice-1 / slice-2
192
+ # behaviour of consulting only built-ins + RBS.
193
+ # @param reporter [Rigor::RbsExtended::Reporter, nil]
194
+ # ADR-13 slice 3b — collector that the Resolver feeds
195
+ # `dynamic.shape.lossy-projection` events into when a
196
+ # shape-projection head (`pick_of`, `omit_of`,
197
+ # `partial_of`, `required_of`, `readonly_of`) is applied
198
+ # to a carrier that does not preserve shape information.
199
+ # `nil` (default) suppresses event accumulation; legacy
200
+ # call sites that have no reporter to thread keep the
201
+ # pre-slice-3b silent fall-through.
202
+ # @param source_location [RBS::Location, nil] location
203
+ # attribution for the events the Resolver records. Carries
204
+ # the annotation's filename / line / column so the runner
205
+ # can stamp diagnostics with the user-visible source site.
154
206
  # @return [Rigor::Type, nil] the resolved refinement
155
207
  # carrier, or `nil` when the payload is unparseable or
156
- # names a refinement / class not in the registry.
157
- def parse(payload)
158
- Parser.new(payload.to_s).parse
208
+ # names a refinement / class no registered source resolved.
209
+ def parse(payload, name_scope: nil, reporter: nil, source_location: nil)
210
+ Parser.new(
211
+ payload.to_s,
212
+ name_scope: name_scope,
213
+ reporter: reporter,
214
+ source_location: source_location
215
+ ).parse
216
+ end
217
+
218
+ # Builder helpers reachable from the Resolver. They live on
219
+ # the module so the Resolver does not have to import the
220
+ # `private_constant` builder hashes.
221
+ def parametric_type_builder(name)
222
+ PARAMETERISED_TYPE_BUILDERS[name]
223
+ end
224
+
225
+ def parametric_int_builder(name)
226
+ PARAMETERISED_INT_BUILDERS[name]
159
227
  end
160
228
 
161
229
  def known?(name)
@@ -185,23 +253,38 @@ module Rigor
185
253
  # soft (returns `nil` from `parse`) on any deviation so the
186
254
  # `RBS::Extended` directive site can fall back to the
187
255
  # RBS-declared type rather than crash on a typo.
188
- class Parser # rubocop:disable Metrics/ClassLength
189
- def initialize(input)
256
+ #
257
+ # ADR-13 slice 3 split the original "scan + resolve" loop
258
+ # into two passes: the parser emits a {Rigor::TypeNode} AST,
259
+ # and a sibling {Resolver} walks the AST to produce a
260
+ # {Rigor::Type} carrier — consulting the built-in registry,
261
+ # the plugin {Rigor::TypeNode::ResolverChain}, and finally
262
+ # the RBS Nominal fallback in that order. Plugin resolvers
263
+ # never see partial parses.
264
+ class Parser
265
+ def initialize(input, name_scope: nil, reporter: nil, source_location: nil)
190
266
  @scanner = StringScanner.new(input.strip)
267
+ @resolver = Resolver.new(
268
+ name_scope: name_scope,
269
+ reporter: reporter,
270
+ source_location: source_location
271
+ )
191
272
  end
192
273
 
193
274
  def parse
194
- type = parse_type
195
- return nil if type.nil?
196
-
197
- # v0.0.7 — trailing `[K]` indexed-access projects
198
- # into the parsed type. Multiple `[K]` segments
199
- # chain (`Tuple[A, B, C][1][0]`).
200
- type = parse_indexed_access_chain(type)
201
- return nil if type.nil?
275
+ ast = parse_type_ast
276
+ return nil if ast.nil?
277
+
278
+ # v0.0.7 — trailing `[K]` indexed-access projects into
279
+ # the parsed type. Multiple `[K]` segments chain
280
+ # (`Tuple[A, B, C][1][0]`). Each segment wraps the
281
+ # previous AST in an {IndexedAccess} node so the chain
282
+ # composes cleanly through the resolver pass.
283
+ ast = parse_indexed_access_chain_ast(ast)
284
+ return nil if ast.nil?
202
285
  return nil unless @scanner.eos?
203
286
 
204
- type
287
+ @resolver.resolve_ast(ast)
205
288
  end
206
289
 
207
290
  private
@@ -214,68 +297,70 @@ module Rigor
214
297
  SIMPLE_NAME = /[a-z][a-z0-9_-]*/
215
298
  CLASS_NAME = /[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*/
216
299
  SIGNED_INT = /-?\d+/
217
- private_constant :SIMPLE_NAME, :CLASS_NAME, :SIGNED_INT
218
-
219
- def parse_type
300
+ # ADR-13 follow-up — literal-key tokens at type-arg
301
+ # position. Symbol literals match Ruby's bare-symbol
302
+ # identifier shape (`:name`); string literals are
303
+ # double-quoted without escape sequences (the most
304
+ # common TS-style key-union shape). The `?<value>`
305
+ # capture lets the parser pull the inner text without
306
+ # post-stripping the delimiter.
307
+ SYMBOL_LITERAL = /:(?<value>[a-zA-Z_][a-zA-Z0-9_]*[?!=]?)/
308
+ STRING_LITERAL = /"(?<value>[^"\\]*)"/
309
+ private_constant :SIMPLE_NAME, :CLASS_NAME, :SIGNED_INT, :SYMBOL_LITERAL, :STRING_LITERAL
310
+
311
+ def parse_type_ast
220
312
  if (class_name = @scanner.scan(CLASS_NAME))
221
- return parse_class_arg_tail(class_name)
313
+ return parse_class_arg_tail_ast(class_name)
222
314
  end
223
315
 
224
316
  name = @scanner.scan(SIMPLE_NAME)
225
317
  return nil if name.nil?
226
318
 
227
319
  case @scanner.peek(1)
228
- when "[" then parse_parametric_type_args(name)
229
- when "<" then parse_parametric_int_bounds(name)
230
- else ImportedRefinements.lookup(name)
320
+ when "[" then parse_bracket_args_ast(name)
321
+ when "<" then parse_angle_bounds_ast(name)
322
+ else TypeNode::Identifier.new(name: name)
231
323
  end
232
324
  end
233
325
 
234
- # `T[K]` — keep applying `[K]` indexes until no more
235
- # opening brackets are present. Each index consumes one
236
- # type argument; multi-arg `[K1, K2]` fails (the spec
237
- # specifies a single key).
238
- def parse_indexed_access_chain(type)
326
+ def parse_indexed_access_chain_ast(ast)
239
327
  loop do
240
328
  skip_ws
241
329
  break unless @scanner.peek(1) == "["
242
330
 
243
331
  @scanner.getch
244
- args = parse_type_arg_list
332
+ args = parse_type_arg_list_ast
245
333
  return nil if args.nil? || args.size != 1
246
334
  return nil unless @scanner.getch == "]"
247
335
 
248
- type = Type::Combinator.indexed_access(type, args.first)
336
+ ast = TypeNode::IndexedAccess.new(receiver: ast, key: args.first)
249
337
  end
250
- type
338
+ ast
251
339
  end
252
340
 
253
- def parse_parametric_type_args(name)
254
- builder = PARAMETERISED_TYPE_BUILDERS[name]
255
- return nil if builder.nil?
256
-
341
+ def parse_bracket_args_ast(name)
257
342
  @scanner.getch # consume '['
258
- args = parse_type_arg_list
343
+ args = parse_type_arg_list_ast
259
344
  return nil if args.nil?
260
345
  return nil unless @scanner.getch == "]"
261
346
 
262
- builder.call(args)
347
+ TypeNode::Generic.new(head: name, args: args)
263
348
  end
264
349
 
265
- def parse_parametric_int_bounds(name)
266
- builder = PARAMETERISED_INT_BUILDERS[name]
267
- return nil if builder.nil?
268
-
350
+ def parse_angle_bounds_ast(name)
269
351
  @scanner.getch # consume '<'
270
352
  bounds = parse_int_bound_list
271
353
  return nil if bounds.nil?
272
354
  return nil unless @scanner.getch == ">"
273
355
 
274
- builder.call(bounds)
356
+ TypeNode::Generic.new(
357
+ head: name,
358
+ args: bounds.map { |b| TypeNode::IntegerLiteral.new(value: b) }
359
+ )
275
360
  end
276
361
 
277
- def parse_type_arg_list
278
- collect_separated_list { parse_type_arg }
362
+ def parse_type_arg_list_ast
363
+ collect_separated_list { parse_type_arg_ast }
279
364
  end
280
365
 
281
366
  def parse_int_bound_list
@@ -298,34 +383,63 @@ module Rigor
298
383
  items
299
384
  end
300
385
 
301
- def parse_type_arg
386
+ # ADR-13 follow-up — admits `:a | "b"` literal-union
387
+ # forms by parsing a single arg first, then optionally
388
+ # folding a chain of `| <single_arg>` into a
389
+ # {TypeNode::Union}. Leaves the existing single-arg
390
+ # path bit-for-bit untouched when no `|` follows.
391
+ def parse_type_arg_ast
302
392
  skip_ws
393
+ first = parse_single_type_arg_ast
394
+ return nil if first.nil?
395
+
396
+ members = [first]
397
+ loop do
398
+ skip_ws
399
+ break unless @scanner.peek(1) == "|"
400
+
401
+ @scanner.getch
402
+ skip_ws
403
+ following = parse_single_type_arg_ast
404
+ return nil if following.nil?
405
+
406
+ members << following
407
+ end
408
+
409
+ # Engine cannot see the `members << following`
410
+ # mutation inside the loop, so it folds the
411
+ # `size == 1` guard to a constant; the loop body
412
+ # actually grows the tuple, so the guard is real.
413
+ members.size == 1 ? first : TypeNode::Union.new(nodes: members) # rigor:disable flow.always-truthy-condition
414
+ end
415
+
416
+ def parse_single_type_arg_ast
303
417
  if (class_name = @scanner.scan(CLASS_NAME))
304
- parse_class_arg_tail(class_name)
418
+ parse_class_arg_tail_ast(class_name)
305
419
  elsif (literal = @scanner.scan(SIGNED_INT))
306
- # Integer-literal arg, used by `int_mask[1, 2, 4]`.
307
- # Wrapped as `Constant<Integer>` so type-arg builders
308
- # see a uniform `Array<Type::t>`.
309
- Type::Combinator.constant_of(Integer(literal))
420
+ TypeNode::IntegerLiteral.new(value: Integer(literal))
421
+ elsif @scanner.scan(SYMBOL_LITERAL)
422
+ TypeNode::SymbolLiteral.new(value: @scanner[:value].to_sym)
423
+ elsif @scanner.scan(STRING_LITERAL)
424
+ TypeNode::StringLiteral.new(value: @scanner[:value])
310
425
  else
311
- parse_type
426
+ parse_type_ast
312
427
  end
313
428
  end
314
429
 
315
430
  # Class-name-headed type argument with optional `[T_1,
316
431
  # …]` type-args tail. Used so `key_of[Hash[Symbol,
317
432
  # Integer]]` parses as the projection of a parameterised
318
- # nominal carrier rather than rejecting the inner
319
- # brackets.
320
- def parse_class_arg_tail(class_name)
321
- return Type::Combinator.nominal_of(class_name) unless @scanner.peek(1) == "["
433
+ # nominal carrier rather than rejecting the inner brackets.
434
+ def parse_class_arg_tail_ast(class_name)
435
+ return TypeNode::Identifier.new(name: class_name) unless @scanner.peek(1) == "["
322
436
 
323
437
  @scanner.getch # consume '['
324
- args = parse_type_arg_list
438
+ args = parse_type_arg_list_ast
325
439
  return nil if args.nil?
326
440
  return nil unless @scanner.getch == "]"
327
441
 
328
- Type::Combinator.nominal_of(class_name, type_args: args)
442
+ TypeNode::Generic.new(head: class_name, args: args)
329
443
  end
330
444
 
331
445
  def parse_int_bound
@@ -341,6 +455,197 @@ module Rigor
341
455
  end
342
456
  end
343
457
  private_constant :Parser
458
+
459
+ # AST → {Rigor::Type} resolver. ADR-13's resolution order
460
+ # for every named-type production:
461
+ #
462
+ # 1. Built-in `ImportedRefinements.lookup` (no-arg
463
+ # refinements like `non-empty-string`).
464
+ # 2. Built-in parametric builders
465
+ # (`PARAMETERISED_TYPE_BUILDERS` for `[...]` forms,
466
+ # `PARAMETERISED_INT_BUILDERS` for `<...>` forms).
467
+ # 3. Plugin resolver chain from the supplied
468
+ # {Rigor::TypeNode::NameScope}, if any.
469
+ # 4. RBS Nominal fallback for class-shaped names
470
+ # (PascalCase head, with or without type args).
471
+ #
472
+ # Returns `nil` when every step declined — preserves the
473
+ # parser's fail-soft contract so callers fall back to the
474
+ # RBS-declared type instead of raising.
475
+ class Resolver
476
+ # ADR-13 slice 3b — heads that consume a shape-bearing
477
+ # first argument. When the first arg is not a `HashShape`
478
+ # / `Tuple` (per {Rigor::Type::Combinator.shape_projection_lossy?}),
479
+ # the projection degrades to "input unchanged" and the
480
+ # Resolver records a `dynamic.shape.lossy-projection`
481
+ # event on the reporter (if any).
482
+ SHAPE_PROJECTION_HEADS = %w[pick_of omit_of partial_of required_of readonly_of].freeze
483
+ private_constant :SHAPE_PROJECTION_HEADS
484
+
485
+ def initialize(name_scope: nil, reporter: nil, source_location: nil)
486
+ @chain = name_scope&.resolver
487
+ @class_context = name_scope&.class_context
488
+ @type_alias_table = name_scope&.type_alias_table || {}
489
+ @reporter = reporter
490
+ @source_location = source_location
491
+ end
492
+
493
+ # ADR-13 follow-up — every leaf-literal AST node
494
+ # (`IntegerLiteral` / `SymbolLiteral` / `StringLiteral`)
495
+ # carries a Ruby value that lifts directly to a
496
+ # `Constant<value>` carrier through the same helper.
497
+ LITERAL_AST_NODES = [
498
+ TypeNode::IntegerLiteral, TypeNode::SymbolLiteral, TypeNode::StringLiteral
499
+ ].freeze
500
+ private_constant :LITERAL_AST_NODES
501
+
502
+ def resolve_ast(node)
503
+ case node
504
+ when TypeNode::Identifier then resolve_identifier(node)
505
+ when TypeNode::Generic then resolve_generic(node)
506
+ when TypeNode::IndexedAccess then resolve_indexed_access(node)
507
+ when TypeNode::Union then resolve_union(node)
508
+ when *LITERAL_AST_NODES then Type::Combinator.constant_of(node.value)
509
+ end
510
+ end
511
+
512
+ # ADR-13 follow-up — resolves each node recursively and
513
+ # folds into a `Type::Combinator.union(...)`. When any
514
+ # node resolves to `nil` (unknown name, plugin decline,
515
+ # RBS Nominal fallback miss), the whole union collapses
516
+ # to `nil` so the caller falls back to the underlying
517
+ # RBS-declared type rather than a half-resolved Union
518
+ # carrier.
519
+ def resolve_union(node)
520
+ resolved = node.nodes.map { |child| resolve_ast(child) }
521
+ return nil if resolved.any?(&:nil?)
522
+
523
+ Type::Combinator.union(*resolved)
524
+ end
525
+
526
+ # Public {Rigor::Plugin::TypeNodeResolver}-shaped interface
527
+ # so a {Rigor::TypeNode::NameScope} can point its
528
+ # `#resolver` at the Resolver itself. Plugin resolvers
529
+ # call `scope.resolver.resolve(arg, scope)` to recursively
530
+ # resolve a nested argument through the FULL pass
531
+ # (built-in registry → plugin chain → RBS fallback), not
532
+ # just back through the chain. The `_scope` argument is
533
+ # ignored — the Resolver owns the scope state internally.
534
+ def resolve(node, _scope)
535
+ resolve_ast(node)
536
+ end
537
+
538
+ private
539
+
540
+ CLASS_SHAPED_HEAD = /\A[A-Z]/
541
+ private_constant :CLASS_SHAPED_HEAD
542
+
543
+ def resolve_identifier(node)
544
+ if class_shaped?(node.name)
545
+ chain_type = consult_chain(node)
546
+ return chain_type unless chain_type.nil?
547
+
548
+ return Type::Combinator.nominal_of(node.name)
549
+ end
550
+
551
+ builtin = ImportedRefinements.lookup(node.name)
552
+ return builtin unless builtin.nil?
553
+
554
+ consult_chain(node)
555
+ end
556
+
557
+ def resolve_generic(node)
558
+ builtin = try_builtin_parametric(node)
559
+ return builtin unless builtin.nil?
560
+
561
+ chain_type = consult_chain(node)
562
+ return chain_type unless chain_type.nil?
563
+
564
+ return nil unless class_shaped?(node.head)
565
+
566
+ args = resolve_args(node.args)
567
+ return nil if args.nil?
568
+
569
+ Type::Combinator.nominal_of(node.head, type_args: args)
570
+ end
571
+
572
+ def resolve_indexed_access(node)
573
+ receiver = resolve_ast(node.receiver)
574
+ return nil if receiver.nil?
575
+
576
+ key = resolve_ast(node.key)
577
+ return nil if key.nil?
578
+
579
+ Type::Combinator.indexed_access(receiver, key)
580
+ end
581
+
582
+ def try_builtin_parametric(node)
583
+ try_parametric_type_builder(node) || try_parametric_int_builder(node)
584
+ end
585
+
586
+ def try_parametric_type_builder(node)
587
+ builder = ImportedRefinements.parametric_type_builder(node.head)
588
+ return nil if builder.nil?
589
+
590
+ args = resolve_args(node.args)
591
+ return nil if args.nil?
592
+
593
+ result = builder.call(args)
594
+ record_lossy_projection_if_applicable(node, args, result)
595
+ result
596
+ end
597
+
598
+ # ADR-13 slice 3b — record one `dynamic.shape.lossy-projection`
599
+ # event per (head, source_location) pair when the projection
600
+ # actually degraded. The builders return the source carrier
601
+ # unchanged on non-HashShape / non-Tuple receivers (see
602
+ # `Type::Combinator.pick_of` / `omit_of` and the
603
+ # HashShape-only `partial_of` / `required_of` /
604
+ # `readonly_of`), so detection is "first arg was lossy".
605
+ def record_lossy_projection_if_applicable(node, args, result)
606
+ return if @reporter.nil?
607
+ return if result.nil?
608
+ return unless SHAPE_PROJECTION_HEADS.include?(node.head)
609
+ return if args.empty?
610
+ return unless Type::Combinator.shape_projection_lossy?(args.first)
611
+
612
+ @reporter.record_lossy_projection(
613
+ head: node.head,
614
+ source_location: @source_location
615
+ )
616
+ end
617
+
618
+ def try_parametric_int_builder(node)
619
+ builder = ImportedRefinements.parametric_int_builder(node.head)
620
+ return nil if builder.nil?
621
+
622
+ bounds = node.args.map { |a| a.is_a?(TypeNode::IntegerLiteral) ? a.value : nil }
623
+ return nil if bounds.any?(&:nil?)
624
+
625
+ builder.call(bounds)
626
+ end
627
+
628
+ def resolve_args(args)
629
+ resolved = args.map { |a| resolve_ast(a) }
630
+ resolved.any?(&:nil?) ? nil : resolved
631
+ end
632
+
633
+ def consult_chain(node)
634
+ return nil if @chain.nil?
635
+
636
+ scope = TypeNode::NameScope.new(
637
+ resolver: self,
638
+ class_context: @class_context,
639
+ type_alias_table: @type_alias_table
640
+ )
641
+ @chain.resolve(node, scope)
642
+ end
643
+
644
+ def class_shaped?(name)
645
+ name.match?(CLASS_SHAPED_HEAD)
646
+ end
647
+ end
648
+ private_constant :Resolver
344
649
  end
345
650
  end
346
651
  end
@@ -21,7 +21,7 @@ module Rigor
21
21
  # See ADR-2 § "Registration, Configuration, and Caching" for
22
22
  # the design rationale and ADR-6 for the storage backend
23
23
  # decisions that consume this schema.
24
- class Descriptor # rubocop:disable Metrics/ClassLength
24
+ class Descriptor
25
25
  # Bumped on incompatible schema changes. The storage layer
26
26
  # mixes this into the cache key, so a bump implicitly
27
27
  # invalidates every cached value. v2 added the
@@ -21,7 +21,7 @@ module Rigor
21
21
  # next write replaces the bad entry. The trailing SHA-256 catches
22
22
  # accidental corruption (partial writes, FS errors); it is **not**
23
23
  # a security boundary, per ADR-2's trusted-gem trust model.
24
- class Store # rubocop:disable Metrics/ClassLength
24
+ class Store
25
25
  # Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte
26
26
  # format version. Bumped on incompatible on-disk format changes
27
27
  # (independent of {Descriptor::SCHEMA_VERSION}, which covers
@@ -29,7 +29,7 @@ module Rigor
29
29
  # is `1` when any new diagnostic appears, `0` otherwise —
30
30
  # so adding new errors fails CI but legacy errors recorded
31
31
  # in the baseline don't.
32
- class DiffCommand # rubocop:disable Metrics/ClassLength
32
+ class DiffCommand
33
33
  USAGE = "Usage: rigor diff [options] <baseline.json> [paths...]"
34
34
 
35
35
  def initialize(argv:, out:, err:)