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
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module RbsExtended
5
+ # ADR-13 slice 3b — per-run accumulator for `RBS::Extended`
6
+ # diagnostic events that the parser / resolver cannot surface
7
+ # at the point of failure (the parsers are fail-soft, returning
8
+ # `nil` so call sites fall back to the RBS-declared type).
9
+ #
10
+ # Owns two event streams:
11
+ #
12
+ # - `#unresolved_payloads` — `rigor:v1:*` directive payloads
13
+ # the resolver could not turn into a {Rigor::Type}. Surface
14
+ # as `dynamic.rbs-extended.unresolved` `:info` diagnostics.
15
+ # - `#lossy_projections` — shape-projection type functions
16
+ # (`pick_of` / `omit_of` / `partial_of` / `required_of` /
17
+ # `readonly_of`) applied to a carrier that does not preserve
18
+ # shape information (anything other than `Type::HashShape`
19
+ # / `Type::Tuple`). Surface as
20
+ # `dynamic.shape.lossy-projection` `:info` diagnostics.
21
+ #
22
+ # Mutable through the run; consumed once by
23
+ # {Rigor::Analysis::Runner} at end-of-run. Each event is
24
+ # deduplicated by `(payload, source_location)` for unresolved
25
+ # and `(head, source_location)` for lossy-projection so a
26
+ # single annotation read from many call sites yields one
27
+ # diagnostic.
28
+ #
29
+ # The reporter is intentionally thread-safe via a coarse
30
+ # `Mutex` because the inference engine may read the same
31
+ # method definition from multiple files in parallel; the
32
+ # critical sections are short (Array#include? + Array#<<) so
33
+ # the lock contention is negligible.
34
+ class Reporter
35
+ UnresolvedEntry = Data.define(:payload, :source_location)
36
+ LossyProjectionEntry = Data.define(:head, :source_location)
37
+
38
+ def initialize
39
+ @unresolved_payloads = []
40
+ @lossy_projections = []
41
+ @mutex = Mutex.new
42
+ end
43
+
44
+ # @return [Array<UnresolvedEntry>] frozen snapshot of the
45
+ # accumulated unresolved-payload events.
46
+ def unresolved_payloads
47
+ @mutex.synchronize { @unresolved_payloads.dup.freeze }
48
+ end
49
+
50
+ # @return [Array<LossyProjectionEntry>] frozen snapshot of
51
+ # the accumulated lossy-projection events.
52
+ def lossy_projections
53
+ @mutex.synchronize { @lossy_projections.dup.freeze }
54
+ end
55
+
56
+ # Records a `dynamic.rbs-extended.unresolved` event. The
57
+ # `source_location` argument is the {RBS::Location} attached
58
+ # to the source annotation (or `nil` when the caller doesn't
59
+ # have one — the diagnostic falls back to a generic
60
+ # location in that case).
61
+ def record_unresolved(payload:, source_location: nil)
62
+ entry = UnresolvedEntry.new(payload: payload.to_s, source_location: source_location)
63
+ @mutex.synchronize do
64
+ return if @unresolved_payloads.include?(entry)
65
+
66
+ @unresolved_payloads << entry
67
+ end
68
+ end
69
+
70
+ # Records a `dynamic.shape.lossy-projection` event for one
71
+ # of the five shape-projection heads. `head` MUST be a
72
+ # String (`"pick_of"`, `"omit_of"`, …); the diagnostic
73
+ # message identifies which projection degraded.
74
+ def record_lossy_projection(head:, source_location: nil)
75
+ entry = LossyProjectionEntry.new(head: head.to_s, source_location: source_location)
76
+ @mutex.synchronize do
77
+ return if @lossy_projections.include?(entry)
78
+
79
+ @lossy_projections << entry
80
+ end
81
+ end
82
+
83
+ # True when no events have accumulated. Used by callers
84
+ # that want to skip the diagnostic-emission pass entirely
85
+ # on the common no-event path.
86
+ def empty?
87
+ @mutex.synchronize { @unresolved_payloads.empty? && @lossy_projections.empty? }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative "type"
4
4
  require_relative "builtins/imported_refinements"
5
5
  require_relative "flow_contribution"
6
+ require_relative "rbs_extended/reporter"
6
7
 
7
8
  module Rigor
8
9
  # Slice 7 phase 15 — first-preview reader for the
@@ -59,7 +60,7 @@ module Rigor
59
60
  # current local type; `class_name` is then nil and
60
61
  # `negative` is false (refinement-form directives do not
61
62
  # support `~T` negation in v0.0.4).
62
- PredicateEffect = Data.define(:edge, :target_kind, :target_name, :class_name, :negative, :refinement_type) do
63
+ class PredicateEffect < Data.define(:edge, :target_kind, :target_name, :class_name, :negative, :refinement_type)
63
64
  def truthy_only? = edge == :truthy_only
64
65
  def falsey_only? = edge == :falsey_only
65
66
  def negative? = negative == true
@@ -98,7 +99,7 @@ module Rigor
98
99
  #
99
100
  # `negative` mirrors `PredicateEffect`: true when the
100
101
  # directive uses `~ClassName` syntax.
101
- AssertEffect = Data.define(:condition, :target_kind, :target_name, :class_name, :negative, :refinement_type) do
102
+ class AssertEffect < Data.define(:condition, :target_kind, :target_name, :class_name, :negative, :refinement_type)
102
103
  def always? = condition == :always
103
104
  def if_truthy_return? = condition == :if_truthy_return
104
105
  def if_falsey_return? = condition == :if_falsey_return
@@ -129,15 +130,30 @@ module Rigor
129
130
  # `rigor:v1:` directives are dropped. Returns an empty
130
131
  # array (NEVER `nil`) for a method with no recognised
131
132
  # annotations so callers can iterate unconditionally.
132
- def read_predicate_effects(method_def)
133
+ #
134
+ # @param environment [Rigor::Environment, nil] ADR-13 slice
135
+ # 3b. When provided, threads the plugin-supplied
136
+ # `name_scope:` and the per-run reporter through the
137
+ # annotation-parse path. `nil` (default) preserves the
138
+ # pre-slice-3b behaviour — no plugin resolvers consulted
139
+ # and no diagnostics accumulated.
140
+ def read_predicate_effects(method_def, environment: nil)
133
141
  return [] if method_def.nil?
134
142
 
135
143
  annotations = method_def.annotations
136
144
  return [] if annotations.nil? || annotations.empty?
137
145
 
146
+ name_scope = environment&.name_scope
147
+ reporter = environment&.rbs_extended_reporter
148
+
138
149
  effects = []
139
150
  annotations.each do |annotation|
140
- effect = parse_predicate_annotation(annotation.string)
151
+ effect = parse_predicate_annotation(
152
+ annotation.string,
153
+ name_scope: name_scope,
154
+ reporter: reporter,
155
+ source_location: annotation.location
156
+ )
141
157
  effects << effect if effect
142
158
  end
143
159
  effects.uniq
@@ -168,7 +184,7 @@ module Rigor
168
184
  /x
169
185
  private_constant :PREDICATE_DIRECTIVE_PATTERN
170
186
 
171
- def parse_predicate_annotation(string)
187
+ def parse_predicate_annotation(string, name_scope: nil, reporter: nil, source_location: nil)
172
188
  match = PREDICATE_DIRECTIVE_PATTERN.match(string)
173
189
  return nil if match.nil?
174
190
 
@@ -176,8 +192,16 @@ module Rigor
176
192
  target = match[:target].to_s
177
193
  edge = directive == "predicate-if-true" ? :truthy_only : :falsey_only
178
194
  target_kind, target_name = target_fields(target)
179
- class_name, refinement_type, negative = resolve_directive_rhs(match)
180
- return nil if class_name.nil? && refinement_type.nil?
195
+ class_name, refinement_type, negative = resolve_directive_rhs(
196
+ match,
197
+ name_scope: name_scope,
198
+ reporter: reporter,
199
+ source_location: source_location
200
+ )
201
+ if class_name.nil? && refinement_type.nil?
202
+ record_unresolved(reporter, string, source_location)
203
+ return nil
204
+ end
181
205
 
182
206
  PredicateEffect.new(
183
207
  edge: edge,
@@ -194,15 +218,26 @@ module Rigor
194
218
  # `RBS::Definition::Method#annotations`. Returns an empty
195
219
  # array when no recognised assertion directives are
196
220
  # attached to the method.
197
- def read_assert_effects(method_def)
221
+ #
222
+ # See {.read_predicate_effects} for the `environment:`
223
+ # keyword contract.
224
+ def read_assert_effects(method_def, environment: nil)
198
225
  return [] if method_def.nil?
199
226
 
200
227
  annotations = method_def.annotations
201
228
  return [] if annotations.nil? || annotations.empty?
202
229
 
230
+ name_scope = environment&.name_scope
231
+ reporter = environment&.rbs_extended_reporter
232
+
203
233
  effects = []
204
234
  annotations.each do |annotation|
205
- effect = parse_assert_annotation(annotation.string)
235
+ effect = parse_assert_annotation(
236
+ annotation.string,
237
+ name_scope: name_scope,
238
+ reporter: reporter,
239
+ source_location: annotation.location
240
+ )
206
241
  effects << effect if effect
207
242
  end
208
243
  effects.uniq
@@ -232,7 +267,7 @@ module Rigor
232
267
  }.freeze
233
268
  private_constant :ASSERT_CONDITIONS
234
269
 
235
- def parse_assert_annotation(string)
270
+ def parse_assert_annotation(string, name_scope: nil, reporter: nil, source_location: nil)
236
271
  match = ASSERT_DIRECTIVE_PATTERN.match(string)
237
272
  return nil if match.nil?
238
273
 
@@ -242,8 +277,16 @@ module Rigor
242
277
 
243
278
  target = match[:target].to_s
244
279
  target_kind, target_name = target_fields(target)
245
- class_name, refinement_type, negative = resolve_directive_rhs(match)
246
- return nil if class_name.nil? && refinement_type.nil?
280
+ class_name, refinement_type, negative = resolve_directive_rhs(
281
+ match,
282
+ name_scope: name_scope,
283
+ reporter: reporter,
284
+ source_location: source_location
285
+ )
286
+ if class_name.nil? && refinement_type.nil?
287
+ record_unresolved(reporter, string, source_location)
288
+ return nil
289
+ end
247
290
 
248
291
  AssertEffect.new(
249
292
  condition: condition,
@@ -273,7 +316,7 @@ module Rigor
273
316
  # - Refinement payload unparseable: returns
274
317
  # `[nil, nil, false]` so callers can drop the directive
275
318
  # silently (fail-soft policy).
276
- def resolve_directive_rhs(match)
319
+ def resolve_directive_rhs(match, name_scope: nil, reporter: nil, source_location: nil)
277
320
  negative = match[:negation].to_s == "~"
278
321
  class_capture = match[:class_name]
279
322
  return [class_capture.to_s.sub(/\A::/, ""), nil, negative] if class_capture
@@ -281,7 +324,12 @@ module Rigor
281
324
  refinement_capture = match[:refinement]
282
325
  return [nil, nil, false] if refinement_capture.nil?
283
326
 
284
- type = Builtins::ImportedRefinements.parse(refinement_capture)
327
+ type = Builtins::ImportedRefinements.parse(
328
+ refinement_capture,
329
+ name_scope: name_scope,
330
+ reporter: reporter,
331
+ source_location: source_location
332
+ )
285
333
  return [nil, nil, false] if type.nil?
286
334
 
287
335
  [nil, type, negative]
@@ -326,16 +374,25 @@ module Rigor
326
374
  # - the directive's payload names a refinement not
327
375
  # registered in `Rigor::Builtins::ImportedRefinements`
328
376
  # (the analyzer prefers a silent miss over crashing on a
329
- # typo; future slices MAY surface the miss as a
330
- # `:warning` self-diagnostic).
331
- def read_return_type_override(method_def)
377
+ # typo; ADR-13 slice 3b surfaces the miss as a
378
+ # `dynamic.rbs-extended.unresolved` `:info` diagnostic when
379
+ # an `environment:` is supplied).
380
+ def read_return_type_override(method_def, environment: nil)
332
381
  return nil if method_def.nil?
333
382
 
334
383
  annotations = method_def.annotations
335
384
  return nil if annotations.nil? || annotations.empty?
336
385
 
386
+ name_scope = environment&.name_scope
387
+ reporter = environment&.rbs_extended_reporter
388
+
337
389
  annotations.each do |annotation|
338
- type = parse_return_type_override(annotation.string)
390
+ type = parse_return_type_override(
391
+ annotation.string,
392
+ name_scope: name_scope,
393
+ reporter: reporter,
394
+ source_location: annotation.location
395
+ )
339
396
  return type if type
340
397
  end
341
398
  nil
@@ -360,11 +417,18 @@ module Rigor
360
417
  /x
361
418
  private_constant :RETURN_DIRECTIVE_PATTERN
362
419
 
363
- def parse_return_type_override(string)
420
+ def parse_return_type_override(string, name_scope: nil, reporter: nil, source_location: nil)
364
421
  match = RETURN_DIRECTIVE_PATTERN.match(string)
365
422
  return nil if match.nil?
366
423
 
367
- Builtins::ImportedRefinements.parse(match[:payload])
424
+ type = Builtins::ImportedRefinements.parse(
425
+ match[:payload],
426
+ name_scope: name_scope,
427
+ reporter: reporter,
428
+ source_location: source_location
429
+ )
430
+ record_unresolved(reporter, string, source_location) if type.nil?
431
+ type
368
432
  end
369
433
 
370
434
  # Returned for `rigor:v1:param: <name> <refinement>`. The
@@ -397,13 +461,23 @@ module Rigor
397
461
  # purposes; passing a too-wide `Nominal[String]` argument
398
462
  # is flagged as an argument-type mismatch at the call
399
463
  # site.
400
- def read_param_type_overrides(method_def)
464
+ def read_param_type_overrides(method_def, environment: nil)
401
465
  return [] if method_def.nil?
402
466
 
403
467
  annotations = method_def.annotations
404
468
  return [] if annotations.nil? || annotations.empty?
405
469
 
406
- annotations.filter_map { |annotation| parse_param_annotation(annotation.string) }
470
+ name_scope = environment&.name_scope
471
+ reporter = environment&.rbs_extended_reporter
472
+
473
+ annotations.filter_map do |annotation|
474
+ parse_param_annotation(
475
+ annotation.string,
476
+ name_scope: name_scope,
477
+ reporter: reporter,
478
+ source_location: annotation.location
479
+ )
480
+ end
407
481
  end
408
482
 
409
483
  # Convenience reader for call sites that want to look up
@@ -411,8 +485,10 @@ module Rigor
411
485
  # Hash<Symbol, Rigor::Type>; missing keys mean "use the
412
486
  # RBS-declared type". Callers MUST treat the hash as
413
487
  # read-only.
414
- def param_type_override_map(method_def)
415
- read_param_type_overrides(method_def).to_h { |o| [o.param_name, o.type] }.freeze
488
+ def param_type_override_map(method_def, environment: nil)
489
+ read_param_type_overrides(method_def, environment: environment)
490
+ .to_h { |o| [o.param_name, o.type] }
491
+ .freeze
416
492
  end
417
493
 
418
494
  # The `is` glue word is optional so authors can write
@@ -434,12 +510,20 @@ module Rigor
434
510
  /x
435
511
  private_constant :PARAM_DIRECTIVE_PATTERN
436
512
 
437
- def parse_param_annotation(string)
513
+ def parse_param_annotation(string, name_scope: nil, reporter: nil, source_location: nil)
438
514
  match = PARAM_DIRECTIVE_PATTERN.match(string)
439
515
  return nil if match.nil?
440
516
 
441
- type = Builtins::ImportedRefinements.parse(match[:payload])
442
- return nil if type.nil?
517
+ type = Builtins::ImportedRefinements.parse(
518
+ match[:payload],
519
+ name_scope: name_scope,
520
+ reporter: reporter,
521
+ source_location: source_location
522
+ )
523
+ if type.nil?
524
+ record_unresolved(reporter, string, source_location)
525
+ return nil
526
+ end
443
527
 
444
528
  ParamOverride.new(param_name: match[:param].to_sym, type: type)
445
529
  end
@@ -477,18 +561,21 @@ module Rigor
477
561
  # Returns `nil` when the method carries no recognised
478
562
  # contribution directives (callers can skip the merge step
479
563
  # without iterating an empty bundle).
480
- def read_flow_contribution(method_def)
564
+ #
565
+ # See {.read_predicate_effects} for the `environment:`
566
+ # keyword contract.
567
+ def read_flow_contribution(method_def, environment: nil)
481
568
  return nil if method_def.nil?
482
569
 
483
- predicate_effects = read_predicate_effects(method_def)
484
- assert_effects = read_assert_effects(method_def)
485
- return_override = read_return_type_override(method_def)
570
+ predicate_effects = read_predicate_effects(method_def, environment: environment)
571
+ assert_effects = read_assert_effects(method_def, environment: environment)
572
+ return_override = read_return_type_override(method_def, environment: environment)
486
573
  return nil if predicate_effects.empty? && assert_effects.empty? && return_override.nil?
487
574
 
488
575
  build_flow_contribution(predicate_effects, assert_effects, return_override)
489
576
  end
490
577
 
491
- def build_flow_contribution(predicate_effects, assert_effects, return_override) # rubocop:disable Metrics/CyclomaticComplexity
578
+ def build_flow_contribution(predicate_effects, assert_effects, return_override)
492
579
  truthy = predicate_effects.select(&:truthy_only?).map(&:to_fact)
493
580
  falsey = predicate_effects.select(&:falsey_only?).map(&:to_fact)
494
581
  post_return = []
@@ -513,5 +600,17 @@ module Rigor
513
600
  def nilable_slot(facts)
514
601
  facts.empty? ? nil : facts
515
602
  end
603
+
604
+ # ADR-13 slice 3b — guards every reporter call so the
605
+ # in-RbsExtended-module call sites can record events
606
+ # uniformly without nil-checking each time. When the
607
+ # reporter is nil (the v0.1.0 → v0.1.3 default for call
608
+ # sites that do not yet thread `environment:`), the call is
609
+ # a no-op and the parser stays fail-soft.
610
+ def record_unresolved(reporter, payload, source_location)
611
+ return if reporter.nil?
612
+
613
+ reporter.record_unresolved(payload: payload, source_location: source_location)
614
+ end
516
615
  end
517
616
  end
data/lib/rigor/scope.rb CHANGED
@@ -20,7 +20,8 @@ module Rigor
20
20
  :ivars, :cvars, :globals,
21
21
  :class_ivars, :class_cvars, :program_globals,
22
22
  :discovered_classes, :in_source_constants, :discovered_methods,
23
- :discovered_def_nodes, :discovered_method_visibilities
23
+ :discovered_def_nodes, :discovered_method_visibilities,
24
+ :source_path
24
25
 
25
26
  EMPTY_DECLARED_TYPES = {}.compare_by_identity.freeze
26
27
  EMPTY_VAR_BINDINGS = {}.freeze
@@ -28,8 +29,9 @@ module Rigor
28
29
  private_constant :EMPTY_DECLARED_TYPES, :EMPTY_VAR_BINDINGS, :EMPTY_CLASS_BINDINGS
29
30
 
30
31
  class << self
31
- def empty(environment: Environment.default)
32
- new(environment: environment, locals: {}.freeze, fact_store: Analysis::FactStore.empty)
32
+ def empty(environment: Environment.default, source_path: nil)
33
+ new(environment: environment, locals: {}.freeze,
34
+ fact_store: Analysis::FactStore.empty, source_path: source_path)
33
35
  end
34
36
  end
35
37
 
@@ -48,7 +50,8 @@ module Rigor
48
50
  in_source_constants: EMPTY_VAR_BINDINGS,
49
51
  discovered_methods: EMPTY_CLASS_BINDINGS,
50
52
  discovered_def_nodes: EMPTY_CLASS_BINDINGS,
51
- discovered_method_visibilities: EMPTY_CLASS_BINDINGS
53
+ discovered_method_visibilities: EMPTY_CLASS_BINDINGS,
54
+ source_path: nil
52
55
  )
53
56
  @environment = environment
54
57
  @locals = locals
@@ -66,6 +69,7 @@ module Rigor
66
69
  @discovered_methods = discovered_methods
67
70
  @discovered_def_nodes = discovered_def_nodes
68
71
  @discovered_method_visibilities = discovered_method_visibilities
72
+ @source_path = source_path
69
73
  freeze
70
74
  end
71
75
 
@@ -92,6 +96,16 @@ module Rigor
92
96
  rebuild(self_type: type)
93
97
  end
94
98
 
99
+ # ADR-11 per-call-site assertion gating prerequisite. The
100
+ # analyzer's per-file boundary stamps the current source
101
+ # file's path onto the seed scope; nested rebuilds carry
102
+ # the value through so plugin hooks like
103
+ # `flow_contribution_for` can resolve "which file does
104
+ # this call site belong to?" without thread-locals.
105
+ def with_source_path(path)
106
+ rebuild(source_path: path)
107
+ end
108
+
95
109
  # Slice A-declarations. Returns a scope that carries an
96
110
  # identity-comparing Hash of `Prism::Node => Rigor::Type`
97
111
  # overrides. `ExpressionTyper#type_of(node)` MUST consult
@@ -333,7 +347,7 @@ module Rigor
333
347
  build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other)
334
348
  end
335
349
 
336
- def ==(other) # rubocop:disable Metrics/CyclomaticComplexity
350
+ def ==(other)
337
351
  other.is_a?(Scope) &&
338
352
  environment.equal?(other.environment) &&
339
353
  @locals == other.locals &&
@@ -357,7 +371,8 @@ module Rigor
357
371
  class_ivars: @class_ivars, class_cvars: @class_cvars, program_globals: @program_globals,
358
372
  discovered_classes: @discovered_classes, in_source_constants: @in_source_constants,
359
373
  discovered_methods: @discovered_methods, discovered_def_nodes: @discovered_def_nodes,
360
- discovered_method_visibilities: @discovered_method_visibilities
374
+ discovered_method_visibilities: @discovered_method_visibilities,
375
+ source_path: @source_path
361
376
  )
362
377
  self.class.new(
363
378
  environment: environment, locals: locals,
@@ -370,7 +385,8 @@ module Rigor
370
385
  in_source_constants: in_source_constants,
371
386
  discovered_methods: discovered_methods,
372
387
  discovered_def_nodes: discovered_def_nodes,
373
- discovered_method_visibilities: discovered_method_visibilities
388
+ discovered_method_visibilities: discovered_method_visibilities,
389
+ source_path: source_path
374
390
  )
375
391
  end
376
392
 
@@ -396,7 +412,8 @@ module Rigor
396
412
  in_source_constants: in_source_constants,
397
413
  discovered_methods: discovered_methods,
398
414
  discovered_def_nodes: discovered_def_nodes,
399
- discovered_method_visibilities: discovered_method_visibilities
415
+ discovered_method_visibilities: discovered_method_visibilities,
416
+ source_path: source_path
400
417
  )
401
418
  end
402
419
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module SigGen
5
+ # The five classifications a candidate method falls into
6
+ # after the generator has compared the inferred return
7
+ # type against the project's existing RBS.
8
+ #
9
+ # The strings are the diagnostic-family identifiers ADR-14
10
+ # reserves under `sig.*`; the MVP carries them as plain
11
+ # symbols on the method candidate and renders the matching
12
+ # identifier in JSON / text output. They are added to the
13
+ # diagnostic family hierarchy in `docs/type-specification/
14
+ # diagnostic-policy.md` even though slice 1 does not yet
15
+ # emit them as diagnostics.
16
+ module Classification
17
+ NEW_FILE = :new_file
18
+ NEW_METHOD = :new_method
19
+ TIGHTER_RETURN = :tighter_return
20
+ EQUIVALENT = :equivalent
21
+ SKIPPED = :skipped
22
+
23
+ DIAGNOSTIC_IDS = {
24
+ NEW_FILE => "sig.generated.new-file",
25
+ NEW_METHOD => "sig.generated.new-method",
26
+ TIGHTER_RETURN => "sig.generated.tighter-return"
27
+ }.freeze
28
+
29
+ SKIP_DIAGNOSTIC_IDS = {
30
+ complex_shape: "sig.skipped.complex-shape",
31
+ user_authored: "sig.skipped.user-authored",
32
+ untyped_return: "sig.skipped.untyped-return"
33
+ }.freeze
34
+ end
35
+ end
36
+ end