rigortype 0.1.18 → 0.2.0

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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -224
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
  8. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  9. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
  10. data/lib/rigor/analysis/check_rules.rb +756 -132
  11. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  12. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  13. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  14. data/lib/rigor/analysis/diagnostic.rb +8 -0
  15. data/lib/rigor/analysis/fact_store.rb +5 -4
  16. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  17. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
  18. data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
  19. data/lib/rigor/analysis/runner.rb +75 -27
  20. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  21. data/lib/rigor/analysis/worker_session.rb +31 -25
  22. data/lib/rigor/bleeding_edge.rb +123 -0
  23. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  24. data/lib/rigor/cache/descriptor.rb +86 -8
  25. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  26. data/lib/rigor/cache/store.rb +5 -3
  27. data/lib/rigor/cli/annotate_command.rb +122 -16
  28. data/lib/rigor/cli/baseline_command.rb +4 -3
  29. data/lib/rigor/cli/check_command.rb +118 -16
  30. data/lib/rigor/cli/coverage_command.rb +148 -16
  31. data/lib/rigor/cli/coverage_scan.rb +57 -0
  32. data/lib/rigor/cli/explain_command.rb +2 -0
  33. data/lib/rigor/cli/lsp_command.rb +3 -7
  34. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  35. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  36. data/lib/rigor/cli/options.rb +9 -0
  37. data/lib/rigor/cli/plugins_command.rb +4 -5
  38. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  39. data/lib/rigor/cli/protection_renderer.rb +63 -0
  40. data/lib/rigor/cli/protection_report.rb +68 -0
  41. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  42. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  43. data/lib/rigor/cli/trace_command.rb +2 -1
  44. data/lib/rigor/cli/triage_command.rb +8 -4
  45. data/lib/rigor/cli/triage_renderer.rb +15 -1
  46. data/lib/rigor/cli/type_of_command.rb +1 -1
  47. data/lib/rigor/cli/type_scan_command.rb +2 -1
  48. data/lib/rigor/cli.rb +12 -3
  49. data/lib/rigor/configuration/dependencies.rb +2 -4
  50. data/lib/rigor/configuration/severity_profile.rb +13 -1
  51. data/lib/rigor/configuration.rb +100 -6
  52. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  53. data/lib/rigor/environment/class_registry.rb +4 -3
  54. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  55. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  56. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  57. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  58. data/lib/rigor/environment/rbs_loader.rb +74 -5
  59. data/lib/rigor/environment.rb +17 -7
  60. data/lib/rigor/flow_contribution/fact.rb +1 -1
  61. data/lib/rigor/flow_contribution.rb +3 -5
  62. data/lib/rigor/inference/acceptance.rb +17 -9
  63. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  64. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  65. data/lib/rigor/inference/budget_trace.rb +29 -2
  66. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  67. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  68. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  69. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  70. data/lib/rigor/inference/expression_typer.rb +1072 -71
  71. data/lib/rigor/inference/hkt_body.rb +8 -11
  72. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  73. data/lib/rigor/inference/hkt_registry.rb +10 -11
  74. data/lib/rigor/inference/macro_block_self_type.rb +2 -2
  75. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  76. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  77. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
  78. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  79. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  80. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  81. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  82. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  83. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  84. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  85. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  86. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  87. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
  88. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  89. data/lib/rigor/inference/method_dispatcher.rb +112 -49
  90. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  91. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  92. data/lib/rigor/inference/mutation_widening.rb +147 -11
  93. data/lib/rigor/inference/narrowing.rb +284 -53
  94. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  95. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  96. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  97. data/lib/rigor/inference/protection_scanner.rb +86 -0
  98. data/lib/rigor/inference/scope_indexer.rb +821 -76
  99. data/lib/rigor/inference/statement_evaluator.rb +1179 -102
  100. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  101. data/lib/rigor/inference/synthetic_method.rb +7 -7
  102. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  103. data/lib/rigor/language_server/completion_provider.rb +6 -12
  104. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  105. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  106. data/lib/rigor/language_server/hover_provider.rb +2 -3
  107. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  108. data/lib/rigor/language_server/server.rb +9 -17
  109. data/lib/rigor/language_server.rb +4 -5
  110. data/lib/rigor/plugin/base.rb +245 -87
  111. data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
  112. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  113. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  114. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  115. data/lib/rigor/plugin/macro.rb +6 -8
  116. data/lib/rigor/plugin/manifest.rb +49 -90
  117. data/lib/rigor/plugin/node_rule_walk.rb +59 -14
  118. data/lib/rigor/plugin/registry.rb +18 -18
  119. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  120. data/lib/rigor/protection/mutation_scanner.rb +120 -0
  121. data/lib/rigor/protection/mutator.rb +246 -0
  122. data/lib/rigor/rbs_extended.rb +24 -36
  123. data/lib/rigor/reflection.rb +4 -7
  124. data/lib/rigor/scope/discovery_index.rb +16 -2
  125. data/lib/rigor/scope.rb +185 -16
  126. data/lib/rigor/sig_gen/generator.rb +8 -0
  127. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  128. data/lib/rigor/sig_gen/writer.rb +40 -2
  129. data/lib/rigor/source/constant_path.rb +62 -0
  130. data/lib/rigor/source.rb +1 -0
  131. data/lib/rigor/triage/catalogue.rb +4 -19
  132. data/lib/rigor/triage.rb +69 -1
  133. data/lib/rigor/type/bound_method.rb +2 -11
  134. data/lib/rigor/type/combinator.rb +45 -3
  135. data/lib/rigor/type/constant.rb +2 -11
  136. data/lib/rigor/type/data_class.rb +2 -11
  137. data/lib/rigor/type/data_instance.rb +2 -11
  138. data/lib/rigor/type/hash_shape.rb +2 -11
  139. data/lib/rigor/type/integer_range.rb +2 -11
  140. data/lib/rigor/type/intersection.rb +2 -11
  141. data/lib/rigor/type/nominal.rb +2 -11
  142. data/lib/rigor/type/plain_lattice.rb +37 -0
  143. data/lib/rigor/type/refined.rb +72 -13
  144. data/lib/rigor/type/singleton.rb +2 -11
  145. data/lib/rigor/type/struct_class.rb +75 -0
  146. data/lib/rigor/type/struct_instance.rb +93 -0
  147. data/lib/rigor/type/tuple.rb +5 -15
  148. data/lib/rigor/type.rb +2 -0
  149. data/lib/rigor/version.rb +1 -1
  150. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  151. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  152. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  154. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
  157. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  158. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  159. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  160. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
  161. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  162. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
  163. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  164. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  165. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  166. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  167. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  168. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  169. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  170. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
  171. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  172. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  173. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
  174. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  175. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  176. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  177. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  178. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
  179. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  180. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  181. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
  182. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  183. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  184. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  185. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  186. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +21 -27
  187. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  188. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  189. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  190. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  191. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  192. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  193. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  194. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  195. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  196. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  197. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
  198. data/sig/rigor/analysis/fact_store.rbs +3 -0
  199. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  200. data/sig/rigor/plugin/base.rbs +5 -2
  201. data/sig/rigor/plugin/manifest.rbs +1 -2
  202. data/sig/rigor/scope.rbs +18 -1
  203. data/sig/rigor/type.rbs +37 -1
  204. data/sig/rigor.rbs +1 -1
  205. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  206. data/skills/rigor-plugin-author/SKILL.md +6 -4
  207. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  208. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  209. metadata +25 -2
  210. data/lib/rigor/plugin/macro/external_file.rb +0 -143
@@ -63,7 +63,7 @@ module Rigor
63
63
  }
64
64
  )
65
65
 
66
- producer :worker_index do |_params|
66
+ producer :worker_index, watch: -> { [[@worker_search_paths, "**/*.rb"]] } do |_params|
67
67
  WorkerDiscoverer.new(
68
68
  io_boundary: io_boundary,
69
69
  search_paths: @worker_search_paths,
@@ -74,46 +74,33 @@ module Rigor
74
74
  def init(_services)
75
75
  @worker_search_paths = Array(config.fetch("worker_search_paths")).map(&:to_s)
76
76
  @worker_marker_modules = Array(config.fetch("worker_marker_modules")).map(&:to_s)
77
- @worker_index = nil
78
- @load_error = nil
79
77
  end
80
78
 
81
79
  # File-level only: the load-error emission. The per-call arity
82
80
  # validation runs over the engine-owned walk via the node_rule
83
81
  # below (ADR-37). The worker index is lazily loaded + memoised by
84
- # worker_index_or_nil, shared by both surfaces.
82
+ # `producer_value`, shared by both surfaces.
85
83
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
86
- index = worker_index_or_nil
87
- return [load_error_diagnostic(path)] if index.nil? && @load_error
84
+ index = producer_value(:worker_index)
85
+ return [load_error_diagnostic(path)] if index.nil? && producer_error(:worker_index)
88
86
 
89
87
  []
90
88
  end
91
89
 
92
90
  node_rule Prism::CallNode do |node, _scope, path|
93
- index = worker_index_or_nil
91
+ index = producer_value(:worker_index)
94
92
  next [] if index.nil? || index.empty?
95
93
 
96
- Analyzer.violations_for(call_node: node, worker_index: index).map do |violation|
97
- diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
98
- end
94
+ diagnostics_for(Analyzer.violations_for(call_node: node, worker_index: index), path: path, node: node)
99
95
  end
100
96
 
101
97
  private
102
98
 
103
- def worker_index_or_nil
104
- return @worker_index if @worker_index
105
-
106
- descriptor = glob_descriptor(@worker_search_paths, "**/*.rb")
107
- @worker_index = cache_for(:worker_index, params: {}, descriptor: descriptor).call
108
- rescue StandardError => e
109
- @load_error = "rigor-sidekiq: failed to discover workers: #{e.class}: #{e.message}"
110
- nil
111
- end
112
-
113
99
  def load_error_diagnostic(path)
100
+ error = producer_error(:worker_index)
114
101
  Rigor::Analysis::Diagnostic.new(
115
102
  path: path, line: 1, column: 1,
116
- message: @load_error,
103
+ message: "rigor-sidekiq: failed to discover workers: #{error.class}: #{error.message}",
117
104
  severity: :warning,
118
105
  rule: "load-error"
119
106
  )
@@ -74,7 +74,7 @@ module Rigor
74
74
  block_as_methods: [
75
75
  Rigor::Plugin::Macro::BlockAsMethod.new(
76
76
  receiver_constraint: "Sinatra::Base",
77
- verbs: %i[get post put delete head options patch link unlink]
77
+ method_names: %i[get post put delete head options patch link unlink]
78
78
  )
79
79
  ]
80
80
  )
@@ -168,9 +168,8 @@ module Rigor
168
168
  # contribution mirrors `T.must` minus the nil-stripping:
169
169
  # the call's return type is the inner expression's
170
170
  # inferred type. The companion diagnostic is emitted by
171
- # the plugin's `diagnostics_for_file` hook through
172
- # {RevealTypeRecognizer}; the recogniser here is
173
- # contribution-only.
171
+ # the plugin's `diagnostics_for_file` hook; this
172
+ # method handles the contribution only.
174
173
  def resolve_reveal_type(call_node, scope)
175
174
  inner = nth_argument(call_node, 0)
176
175
  return Rigor::Type::Combinator.untyped if inner.nil? || scope.nil?
@@ -4,22 +4,18 @@ module Rigor
4
4
  module Plugin
5
5
  class Sorbet < Rigor::Plugin::Base
6
6
  # Frozen description of one Sorbet `sig` block as parsed by
7
- # {SigParser}. Holds enough to reconstruct the method's
8
- # call-site return type (slice 1's deliverable) plus the
9
- # parameter shape and modifier list (kept for slice 2+ when
10
- # we begin checking call-site argument types and override
11
- # compatibility).
7
+ # {SigParser}. Holds the return type, parameter shape, and
8
+ # modifier list. Call-site argument-type checking and
9
+ # override-compatibility validation are deferred; only the
10
+ # return-type contribution is active.
12
11
  #
13
12
  # `kind` distinguishes `def foo` (`:instance`) from
14
13
  # `def self.foo` / `class << self; def foo; end`
15
14
  # (`:singleton`).
16
15
  #
17
- # `modifiers` is the set of `sig`-level modifiers we
18
- # observed: `:abstract`, `:override`, `:overridable`,
19
- # `:final`. Slice 1 records them but does not act on them;
20
- # later slices wire `:abstract` into the existing
21
- # `def.return-type-mismatch` check and `:override` into
22
- # override-compatibility validation.
16
+ # `modifiers` records the `sig`-level flags observed:
17
+ # `:abstract`, `:override`, `:overridable`, `:final`.
18
+ # Currently stored but not acted on.
23
19
  MethodSignature = Data.define(
24
20
  :class_name, :method_name, :kind, :params, :return_type, :modifiers
25
21
  )
@@ -26,9 +26,8 @@ module Rigor
26
26
  # whatever it recognises (`params` / `returns` / `void` /
27
27
  # `abstract` / `override` / `overridable` / `final` /
28
28
  # `type_parameters` / `checked` / `on_failure`) into a
29
- # frozen result hash. Slice 1 wires the parsed structure
30
- # into {MethodSignature}; later slices will start *acting*
31
- # on the modifiers and `type_parameters`.
29
+ # frozen result hash stored in {MethodSignature}. Modifiers
30
+ # and `type_parameters` are recorded but not yet acted on.
32
31
  #
33
32
  # The parser is intentionally tolerant — unknown chain
34
33
  # nodes degrade to "the rest of the chain is opaque" rather
@@ -93,8 +92,8 @@ module Rigor
93
92
  when :params
94
93
  accumulator[:params].merge!(parse_params(current))
95
94
  when :type_parameters
96
- # Slice 1: recognise to suppress the degraded
97
- # path; widen translation in slice 3.
95
+ # Recognised to suppress the degraded path;
96
+ # payload intentionally discarded (deferred).
98
97
  when *RECOGNISED_MODIFIERS
99
98
  accumulator[:modifiers] << current.name
100
99
  when *RUNTIME_ONLY_STEPS
@@ -17,15 +17,12 @@ module Rigor
17
17
  # default) when no sigil is present, matching how Sorbet
18
18
  # treats sigil-less files.
19
19
  #
20
- # Slice 5 of ADR-11 uses this purely at catalog-harvest
21
- # time: `# typed: ignore` files are skipped entirely (the
22
- # plugin records no sigs from them). The other levels are
23
- # detected for forward compatibility but treated
24
- # identically per-call-site sigil honouring (e.g. only
25
- # firing `T.let` recognition in `# typed: true`+ files)
26
- # requires threading the file path through
27
- # the per-call recognition path, which lives behind a
28
- # future plugin-contract widening slice.
20
+ # ADR-11 slice 5 uses this at catalog-harvest time:
21
+ # `# typed: ignore` files are skipped entirely; other
22
+ # levels gate both sig contributions (`:true`/`:strict`/
23
+ # `:strong` only) and per-call assertion recognition
24
+ # (`T.let` / `T.cast` / `T.must` / etc.) via
25
+ # `Sorbet#assertion_enforced_here?`.
29
26
  module SigilDetector
30
27
  # Sorbet's strictness-level names. Stored as symbols to
31
28
  # match the analyzer's existing convention for level
@@ -9,14 +9,6 @@ module Rigor
9
9
  # block's `params(...)` and `returns(...)` clauses) into
10
10
  # Rigor's internal type carriers.
11
11
  #
12
- # Slice 1 covered the minimum vocabulary that lets a
13
- # typical `sig { params(x: Integer).returns(String) }`
14
- # round-trip; slice 3 widens it to cover the dense middle
15
- # of Sorbet's surface — generic class applications
16
- # (`T::Array[E]`, `T::Hash[K, V]`, etc.), class-object
17
- # types (`T.class_of(C)`, `T::Class[T]`), tuples, and
18
- # shapes:
19
- #
20
12
  # | Sorbet form | Rigor carrier |
21
13
  # | ------------------------ | ---------------------------------------- |
22
14
  # | `Integer` etc. | `Nominal["Integer"]` |
@@ -42,10 +34,9 @@ module Rigor
42
34
  #
43
35
  # Anything else (`T.proc`, `T.attached_class`,
44
36
  # `T.self_type`, `T.type_parameter`, `T::Struct` / `T::Enum`
45
- # subclasses, …) degrades to `Dynamic[top]`. The degraded
46
- # path stays silent for now per ADR-11's slice plan; a
47
- # later slice surfaces the gap as a `dynamic.sorbet.unsupported`
48
- # diagnostic.
37
+ # subclasses, …) degrades silently to `Dynamic[top]`. The
38
+ # `dynamic.sorbet.unsupported` diagnostic for degraded
39
+ # forms is deferred.
49
40
  module TypeTranslator
50
41
  BOOLEAN_NAME = "Boolean"
51
42
 
@@ -235,9 +226,8 @@ module Rigor
235
226
  # analogue (Singleton names a specific class); the
236
227
  # closest faithful translation is `Singleton[name]`
237
228
  # when `T` is a constant, or `Singleton[Object]` for
238
- # broader applications. Lossy translation; emitted as
239
- # `dynamic.sorbet.degraded` once slice 3's diagnostic
240
- # surface lands.
229
+ # broader applications. Lossy the `dynamic.sorbet.degraded`
230
+ # diagnostic for this case is deferred.
241
231
  def translate_t_class_subscript(args)
242
232
  inner = args.first
243
233
  return Rigor::Type::Combinator.singleton_of("Class") if inner.nil?
@@ -29,11 +29,10 @@ module Rigor
29
29
  # nesting; `def self.foo` is recognised as a singleton
30
30
  # method.
31
31
  #
32
- # Slice 1 vocabulary is the bare minimum to round-trip the
33
- # most common sig shapes; the {TypeTranslator} table
34
- # documents what's covered. Anything else (T.proc / T::Array
35
- # / T.class_of / T::Struct) degrades silently to
36
- # `Dynamic[top]` for now — slice 3 widens the translator.
32
+ # The {TypeTranslator} table documents coverage. Most of
33
+ # Sorbet's vocabulary translates; remaining gaps (`T.proc`,
34
+ # `T::Struct` subclasses, `T.attached_class`, etc.) degrade
35
+ # silently to `Dynamic[top]`.
37
36
  #
38
37
  # Architecture: per-run `Catalog` is built lazily on first
39
38
  # access by walking every configured `paths:` entry's `.rb`
@@ -99,20 +98,15 @@ module Rigor
99
98
  # `false` to record every file's sigs regardless of
100
99
  # sigil (current behaviour pre-this-config).
101
100
  @enforce_sigil = config.fetch("enforce_sigil")
102
- # ADR-11 deferred follow-up — per-call-site assertion
103
- # gating. Catalog harvest's `@sigil_by_path` cache is
104
- # consulted at every per-call recognition so
105
- # `T.let` / `T.cast` / `T.must` / `T.bind` /
106
- # `T.assert_type!` only fire in files Sorbet itself
107
- # would enforce (`# typed: true` / `:strict` /
108
- # `:strong`). When `@enforce_sigil` is off (the user
109
- # opted out at harvest time), the gate also opens at
110
- # every call site — current behaviour. Files whose
111
- # sigil hasn't been observed yet (e.g. the catalog
112
- # hasn't run, or the call site is in a fixture /
113
- # synthetic path the harvest didn't see) treat
114
- # missing-info as enforced — failing-open is friendlier
115
- # for spec ergonomics than failing-closed.
101
+ # Per-call-site assertion gating: `@sigil_by_path` (built
102
+ # during catalog harvest) is consulted so `T.let` /
103
+ # `T.cast` / `T.must` / `T.bind` / `T.assert_type!` only
104
+ # fire in files Sorbet itself would enforce (`:true` /
105
+ # `:strict` / `:strong`). With `enforce_sigil: false` the
106
+ # gate is open everywhere. Missing-sigil paths (synthetic
107
+ # fixtures, out-of-tree call sites) default to enforced —
108
+ # failing-open suits spec ergonomics better than
109
+ # failing-closed. See `assertion_enforced_here?`.
116
110
  @sigil_by_path = {}
117
111
  @catalog = nil
118
112
  @parse_errors_by_path = {}
@@ -160,8 +154,7 @@ module Rigor
160
154
  diagnostics
161
155
  end
162
156
 
163
- # ADR-52 slice 4 — the per-call return-type path, migrated off
164
- # the legacy `flow_contribution_for` hook onto the
157
+ # ADR-52 slice 4 — per-call return-type path via the
165
158
  # method-name-gated `dynamic_return` DSL. The recognised name
166
159
  # set is only known at run time (the catalog's `def` names come
167
160
  # from the lazy catalog build), so it is declared as a
@@ -170,17 +163,15 @@ module Rigor
170
163
  # resolved Symbol Set. The gate is a safe over-approximation —
171
164
  # a project method merely *named* `cast` or `find` passes it
172
165
  # and is declined by the block's own `T.`-receiver / catalog
173
- # checks, exactly as the ungated hook used to decline.
166
+ # checks.
174
167
  dynamic_return methods: -> { recognised_method_names } do |call_node, scope|
175
168
  contribution_return_type(call_node, scope)
176
169
  end
177
170
 
178
171
  # ADR-52 slice 4 — `T.bind(self, T)`'s self-narrowing fact,
179
- # migrated off the legacy hook's `post_return_facts` slot onto
180
- # the method-gated `type_specifier` DSL (once
181
- # `flow_contribution_for` is gone, the statement evaluator
182
- # consults only this path for narrowing facts). The
183
- # return-type half (`Constant[nil]`) flows through the
172
+ # contributed via the method-gated `type_specifier` DSL. The
173
+ # statement evaluator consults this path for narrowing facts.
174
+ # The return-type half (`Constant[nil]`) flows through the
184
175
  # `dynamic_return` rule above; the block re-checks the `T.`
185
176
  # receiver via the recogniser, so an unrelated `bind` call
186
177
  # contributes nothing.
@@ -190,8 +181,8 @@ module Rigor
190
181
 
191
182
  private
192
183
 
193
- # ADR-52 slice 4 — the run-time method-name gate for the
194
- # `dynamic_return` rule: the static assertion vocabulary
184
+ # Run-time method-name gate for the `dynamic_return` rule
185
+ # (ADR-52 slice 4): the static assertion vocabulary
195
186
  # (`T.let` / `T.cast` / …), `T.absurd`, and every method name
196
187
  # the catalog carries a sig for.
197
188
  def recognised_method_names
@@ -202,20 +193,17 @@ module Rigor
202
193
  names
203
194
  end
204
195
 
205
- # The migrated body of the legacy `flow_contribution_for`
206
- # same recognition order, but returns the bare `Rigor::Type`
207
- # the `dynamic_return` contract expects. Resolves the receiver
208
- # in two passes:
196
+ # Main contribution body for the `dynamic_return` rule.
197
+ # Returns the bare `Rigor::Type` the contract expects.
198
+ # Resolves the receiver in three passes:
209
199
  #
210
200
  # 1. Constant receiver (`User.find(...)`) → singleton-side
211
201
  # catalog lookup.
212
202
  # 2. Nominal receiver-type (`user.name` where `user`'s
213
203
  # inferred type is `Nominal["User"]`) → instance-side
214
204
  # catalog lookup.
215
- #
216
- # Implicit-self calls (no receiver, current-class method)
217
- # are deferred to slice 2 — slice 1 covers the common case
218
- # where the sig is on the called method's own class.
205
+ # 3. Implicit-self (receiver-less inside a method body) →
206
+ # current-class lookup via `implicit_self_lookup`.
219
207
  def contribution_return_type(call_node, scope)
220
208
  return nil unless call_node.is_a?(Prism::CallNode)
221
209
 
@@ -286,9 +274,8 @@ module Rigor
286
274
  contribution&.post_return_facts
287
275
  end
288
276
 
289
- # ADR-11 deferred follow-up — per-call-site assertion
290
- # gating. With `enforce_sigil: false`, the gate is fully
291
- # open (matches the pre-feature behaviour). With
277
+ # Per-call-site assertion gating (ADR-11). With
278
+ # `enforce_sigil: false` the gate is fully open. With
292
279
  # `enforce_sigil: true` (default), the caller file's
293
280
  # sigil must reach `:true` / `:strict` / `:strong` for
294
281
  # assertions to fire. Three honest fallbacks:
@@ -329,9 +316,34 @@ module Rigor
329
316
  chain_lookup(singleton_target, method_name, anchor_kind: :singleton, mixin_kind: :extend)
330
317
  elsif receiver
331
318
  instance_chain_lookup(receiver, method_name, scope)
319
+ else
320
+ implicit_self_lookup(method_name, scope)
332
321
  end
333
322
  end
334
323
 
324
+ # ADR-11 slice 2 — implicit-self calls.
325
+ # A receiver-less call inside a method body resolves against the
326
+ # engine's own `scope.self_type`: `Nominal[Foo]` inside an
327
+ # instance method (instance-side lookup), `Singleton[Foo]` inside
328
+ # a `def self.x` body (singleton-side lookup, `extend` mixins).
329
+ # Without this, an enforced sig on a sibling method was invisible
330
+ # to in-class calls — the engine's body-inference tiers then
331
+ # re-typed the sibling's body, overriding an explicit
332
+ # `T.untyped` opt-out (the dispatcher's plugin tier had already
333
+ # run and declined). Anything else (toplevel / Dynamic / DSL
334
+ # self) contributes nothing and the dispatcher continues.
335
+ def implicit_self_lookup(method_name, scope)
336
+ self_type = scope&.self_type
337
+ case self_type
338
+ when Rigor::Type::Singleton
339
+ chain_lookup(self_type.class_name, method_name, anchor_kind: :singleton, mixin_kind: :extend)
340
+ when Rigor::Type::Nominal
341
+ chain_lookup(self_type.class_name, method_name, anchor_kind: :instance, mixin_kind: :include)
342
+ end
343
+ rescue StandardError
344
+ nil
345
+ end
346
+
335
347
  def instance_chain_lookup(receiver_node, method_name, scope)
336
348
  return nil if scope.nil?
337
349
 
@@ -2,6 +2,9 @@ module Rigor
2
2
  module Analysis
3
3
  module CheckRules
4
4
  def self?.diagnose: (path: String, root: untyped, scope_index: Hash[untyped, Scope]) -> Array[Diagnostic]
5
+ def self?.build_node_collectors: (String path, untyped scope_index) -> Hash[Symbol, untyped]
6
+ def self?.node_collector_driver: (Hash[Symbol, untyped] collectors) -> untyped
7
+ def self?.shadow_verify_converged_collectors: (String path, untyped root, untyped scope_index, Hash[Symbol, untyped]? collectors) -> void
5
8
  end
6
9
 
7
10
  class FactStore
@@ -1,4 +1,4 @@
1
1
  class Rigor::Inference::Builtins::MethodCatalog
2
2
  def initialize: (path: String, ?mutating_selectors: Hash[String, Set[untyped]]) -> void
3
- def reset!: () -> nil
3
+ def reset!: () -> Hash[String, untyped]
4
4
  end
@@ -14,7 +14,7 @@ class Rigor::Plugin::Base
14
14
  # the no-arg `manifest` reads the cached value back.
15
15
  def self.manifest: (**untyped fields) -> Rigor::Plugin::Manifest
16
16
 
17
- def self.producer: (untyped id, ?serialize: untyped, ?deserialize: untyped) { (untyped params) -> untyped } -> Symbol
17
+ def self.producer: (untyped id, ?watch: untyped, ?serialize: untyped, ?deserialize: untyped) { (untyped params) -> untyped } -> Symbol
18
18
  def self.producers: () -> Hash[Symbol, untyped]
19
19
 
20
20
  def self.node_rule: (untyped node_type) { (*untyped) -> untyped } -> untyped
@@ -50,11 +50,14 @@ class Rigor::Plugin::Base
50
50
 
51
51
  # Authoring helpers.
52
52
  def diagnostic: (untyped node, path: untyped, message: untyped, ?severity: untyped, ?rule: untyped, ?location: untyped) -> untyped
53
+ def diagnostics_for: (untyped violations, path: untyped, ?node: untyped) -> Array[untyped]
54
+ def read_fact: (plugin_id: untyped, name: untyped) -> untyped
55
+ def producer_value: (untyped id, ?params: untyped) -> untyped
56
+ def producer_error: (untyped id) -> untyped
53
57
  def manifest: () -> Rigor::Plugin::Manifest
54
58
  def signature_paths: () -> Array[String]
55
59
  def protocol_contracts: () -> untyped
56
60
  def io_boundary: () -> Rigor::Plugin::IoBoundary
57
61
  def cache_for: (untyped producer_id, ?params: untyped, ?descriptor: untyped) -> untyped
58
- def glob_descriptor: (untyped roots, *untyped patterns) -> untyped
59
62
  def plugin_entry: () -> untyped
60
63
  end
@@ -3,7 +3,7 @@ class Rigor::Plugin::Manifest::Consumption
3
3
  end
4
4
 
5
5
  class Rigor::Plugin::Manifest
6
- def initialize: (id: untyped, version: untyped, ?description: untyped, ?config_schema: untyped, ?produces: untyped, ?consumes: untyped, ?owns_receivers: untyped, ?open_receivers: untyped, ?type_node_resolvers: untyped, ?block_as_methods: untyped, ?heredoc_templates: untyped, ?nested_class_templates: untyped, ?trait_registries: untyped, ?external_files: untyped, ?hkt_registrations: untyped, ?hkt_definitions: untyped, ?signature_paths: untyped, ?protocol_contracts: untyped, ?source_rbs_synthesizer: untyped, ?additional_initializers: untyped) -> void
6
+ def initialize: (id: untyped, version: untyped, ?description: untyped, ?config_schema: untyped, ?produces: untyped, ?consumes: untyped, ?owns_receivers: untyped, ?open_receivers: untyped, ?type_node_resolvers: untyped, ?block_as_methods: untyped, ?heredoc_templates: untyped, ?nested_class_templates: untyped, ?trait_registries: untyped, ?hkt_registrations: untyped, ?hkt_definitions: untyped, ?signature_paths: untyped, ?protocol_contracts: untyped, ?source_rbs_synthesizer: untyped, ?additional_initializers: untyped) -> void
7
7
 
8
8
  # Public attribute readers (the full `attr_reader` surface). Plugins
9
9
  # read `manifest.id` / `manifest.protocol_contracts` etc.; declaring
@@ -25,7 +25,6 @@ class Rigor::Plugin::Manifest
25
25
  def heredoc_templates: () -> untyped
26
26
  def nested_class_templates: () -> untyped
27
27
  def trait_registries: () -> untyped
28
- def external_files: () -> untyped
29
28
  def hkt_registrations: () -> untyped
30
29
  def hkt_definitions: () -> untyped
31
30
  def signature_paths: () -> untyped
data/sig/rigor/scope.rbs CHANGED
@@ -11,6 +11,7 @@ module Rigor
11
11
  attr_reader indexed_narrowings: Hash[IndexedKey, Type::t]
12
12
  attr_reader method_chain_narrowings: Hash[ChainKey, Type::t]
13
13
  attr_reader source_path: String?
14
+ attr_reader struct_fold_safe_locals: Set[Symbol]
14
15
 
15
16
  # ADR-53 Track A — the seed-time discovery tables live on the
16
17
  # DiscoveryIndex; Scope keeps per-table readers as delegates.
@@ -22,12 +23,15 @@ module Rigor
22
23
  def in_source_constants: () -> Hash[String, Type::t]
23
24
  def discovered_methods: () -> Hash[String, Hash[Symbol, Symbol]]
24
25
  def discovered_def_nodes: () -> Hash[String, Hash[Symbol, untyped]]
26
+ def discovered_singleton_def_nodes: () -> Hash[String, Hash[Symbol, untyped]]
25
27
  def discovered_def_sources: () -> Hash[String, Hash[Symbol, String]]
26
28
  def discovered_method_visibilities: () -> Hash[String, Hash[Symbol, Symbol]]
27
29
  def discovered_superclasses: () -> Hash[String, String]
28
30
  def discovered_includes: () -> Hash[String, Array[String]]
29
31
  def discovered_class_sources: () -> Hash[String, Set[String]]
30
32
  def data_member_layouts: () -> Hash[String, Array[Symbol]]
33
+ def struct_member_layouts: () -> Hash[String, { members: Array[Symbol], keyword_init: bool }]
34
+ def param_inferred_types: () -> Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]
31
35
 
32
36
  class DiscoveryIndex
33
37
  attr_reader declared_types: Hash[untyped, Type::t]
@@ -38,16 +42,19 @@ module Rigor
38
42
  attr_reader in_source_constants: Hash[String, Type::t]
39
43
  attr_reader discovered_methods: Hash[String, Hash[Symbol, Symbol]]
40
44
  attr_reader discovered_def_nodes: Hash[String, Hash[Symbol, untyped]]
45
+ attr_reader discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]]
41
46
  attr_reader discovered_def_sources: Hash[String, Hash[Symbol, String]]
42
47
  attr_reader discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]]
43
48
  attr_reader discovered_superclasses: Hash[String, String]
44
49
  attr_reader discovered_includes: Hash[String, Array[String]]
45
50
  attr_reader discovered_class_sources: Hash[String, Set[String]]
46
51
  attr_reader data_member_layouts: Hash[String, Array[Symbol]]
52
+ attr_reader struct_member_layouts: Hash[String, { members: Array[Symbol], keyword_init: bool }]
53
+ attr_reader param_inferred_types: Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]
47
54
 
48
55
  EMPTY: DiscoveryIndex
49
56
 
50
- def with: (?declared_types: Hash[untyped, Type::t], ?class_ivars: Hash[String, Hash[Symbol, Type::t]], ?class_cvars: Hash[String, Hash[Symbol, Type::t]], ?program_globals: Hash[Symbol, Type::t], ?discovered_classes: Hash[String, Type::Singleton], ?in_source_constants: Hash[String, Type::t], ?discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?discovered_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]], ?discovered_superclasses: Hash[String, String], ?discovered_includes: Hash[String, Array[String]], ?discovered_class_sources: Hash[String, Set[String]], ?data_member_layouts: Hash[String, Array[Symbol]]) -> DiscoveryIndex
57
+ def with: (?declared_types: Hash[untyped, Type::t], ?class_ivars: Hash[String, Hash[Symbol, Type::t]], ?class_cvars: Hash[String, Hash[Symbol, Type::t]], ?program_globals: Hash[Symbol, Type::t], ?discovered_classes: Hash[String, Type::Singleton], ?in_source_constants: Hash[String, Type::t], ?discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?discovered_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]], ?discovered_superclasses: Hash[String, String], ?discovered_includes: Hash[String, Array[String]], ?discovered_class_sources: Hash[String, Set[String]], ?data_member_layouts: Hash[String, Array[Symbol]], ?struct_member_layouts: Hash[String, { members: Array[Symbol], keyword_init: bool }], ?param_inferred_types: Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]) -> DiscoveryIndex
51
58
  end
52
59
 
53
60
  class IndexedKey
@@ -66,6 +73,8 @@ module Rigor
66
73
 
67
74
  def initialize: (environment: Environment, locals: Hash[Symbol, Type::t], ?fact_store: Analysis::FactStore, ?self_type: Type::t?, ?ivars: Hash[Symbol, Type::t], ?cvars: Hash[Symbol, Type::t], ?globals: Hash[Symbol, Type::t], ?discovery: DiscoveryIndex, ?source_path: String?) -> void
68
75
  def with_source_path: (String? path) -> Scope
76
+ def with_struct_fold_safe: (Set[Symbol] locals) -> Scope
77
+ def struct_fold_safe?: (String | Symbol name) -> bool
69
78
  def with_discovery: (DiscoveryIndex index) -> Scope
70
79
  def local: (String | Symbol name) -> Type::t?
71
80
  def ivar: (String | Symbol name) -> Type::t?
@@ -75,16 +84,24 @@ module Rigor
75
84
  def with_ivar: (String | Symbol name, Type::t type) -> Scope
76
85
  def with_cvar: (String | Symbol name, Type::t type) -> Scope
77
86
  def with_global: (String | Symbol name, Type::t type) -> Scope
87
+ def declaration_sourced: () -> Set[[Symbol, Symbol]]
88
+ def seed_declaration_sourced_ivar: (String | Symbol name, Type::t type) -> Scope
89
+ def with_declaration_sourced_local: (String | Symbol name, Type::t type) -> Scope
90
+ def with_local_declaration_mark: (String | Symbol name) -> Scope
91
+ def declaration_sourced?: (Symbol kind, String | Symbol name) -> bool
92
+ def forget_match_globals: () -> Scope
78
93
  def class_ivars_for: (String | Symbol? class_name) -> Hash[Symbol, Type::t]
79
94
  def class_cvars_for: (String | Symbol? class_name) -> Hash[Symbol, Type::t]
80
95
  def discovered_method?: (String | Symbol class_name, String | Symbol method_name, Symbol kind) -> bool
81
96
  def user_def_for: (String | Symbol class_name, String | Symbol method_name) -> untyped?
97
+ def singleton_def_for: (String | Symbol class_name, String | Symbol method_name) -> untyped?
82
98
  def user_def_site_for: (String | Symbol class_name, String | Symbol method_name) -> String?
83
99
  def top_level_def_for: (String | Symbol method_name) -> untyped?
84
100
  def toplevel?: () -> bool
85
101
  def discovered_method_visibility: (String | Symbol class_name, String | Symbol method_name) -> Symbol?
86
102
  def superclass_of: (String | Symbol class_name) -> String?
87
103
  def data_member_layout: (String | Symbol class_name) -> Array[Symbol]?
104
+ def struct_member_layout: (String | Symbol class_name) -> { members: Array[Symbol], keyword_init: bool }?
88
105
  def includes_of: (String | Symbol class_name) -> Array[String]
89
106
  def indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key) -> Type::t?
90
107
  def with_indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key, Type::t type) -> Scope
data/sig/rigor/type.rbs CHANGED
@@ -1,6 +1,6 @@
1
1
  module Rigor
2
2
  module Type
3
- type t = Top | Bot | Dynamic | Constant | IntegerRange | Nominal | Singleton | Union | Difference | Tuple | HashShape | DataClass | DataInstance
3
+ type t = Top | Bot | Dynamic | Constant | IntegerRange | Nominal | Singleton | Union | Difference | Tuple | HashShape | DataClass | DataInstance | StructClass | StructInstance
4
4
 
5
5
  type accepts_mode = :strict | :gradual | :loose
6
6
 
@@ -277,6 +277,39 @@ module Rigor
277
277
  def inspect: () -> String
278
278
  end
279
279
 
280
+ class StructClass
281
+ attr_reader members: Array[Symbol]
282
+ attr_reader class_name: String?
283
+ attr_reader keyword_init: bool
284
+ def initialize: (Array[Symbol] members, ?String? class_name, ?keyword_init: bool) -> void
285
+ def describe: (?Symbol verbosity) -> String
286
+ def erase_to_rbs: () -> String
287
+ def top: () -> Trinary
288
+ def bot: () -> Trinary
289
+ def dynamic: () -> Trinary
290
+ def accepts: (Type::t other, ?mode: accepts_mode) -> AcceptsResult
291
+ def ==: (untyped other) -> bool
292
+ def hash: () -> Integer
293
+ def inspect: () -> String
294
+ end
295
+
296
+ class StructInstance
297
+ attr_reader members: Hash[Symbol, Type::t]
298
+ attr_reader class_name: String?
299
+ def initialize: (Hash[Symbol, Type::t] members, ?String? class_name) -> void
300
+ def member_names: () -> Array[Symbol]
301
+ def member_type: (Symbol name) -> Type::t?
302
+ def describe: (?Symbol verbosity) -> String
303
+ def erase_to_rbs: () -> String
304
+ def top: () -> Trinary
305
+ def bot: () -> Trinary
306
+ def dynamic: () -> Trinary
307
+ def accepts: (Type::t other, ?mode: accepts_mode) -> AcceptsResult
308
+ def ==: (untyped other) -> bool
309
+ def hash: () -> Integer
310
+ def inspect: () -> String
311
+ end
312
+
280
313
  class AcceptsResult
281
314
  attr_reader trinary: Trinary
282
315
  attr_reader mode: accepts_mode
@@ -339,9 +372,12 @@ module Rigor
339
372
  def self?.hash_shape_of: (?Hash[untyped, Type::t]? pairs, **untyped options) -> HashShape
340
373
  def self?.data_class_of: (members: Array[Symbol], ?class_name: String?) -> DataClass
341
374
  def self?.data_instance_of: (members: Hash[Symbol, Type::t], ?class_name: String?) -> DataInstance
375
+ def self?.struct_class_of: (members: Array[Symbol], ?class_name: String?, ?keyword_init: bool) -> StructClass
376
+ def self?.struct_instance_of: (members: Hash[Symbol, Type::t], ?class_name: String?) -> StructInstance
342
377
  def self?.union: (*Type::t types) -> Type::t
343
378
  def self?.key_of: (Type::t type) -> Type::t
344
379
  def self?.value_of: (Type::t type) -> Type::t
380
+ def self?.widen_value_pinned: (Type::t type) -> Type::t
345
381
  def self?.int_mask: (Array[Integer] flags) -> Type::t?
346
382
  def self?.int_mask_of: (Type::t type) -> Type::t?
347
383
  def self?.indexed_access: (Type::t type, Type::t key) -> Type::t
data/sig/rigor.rbs CHANGED
@@ -11,7 +11,7 @@ module Rigor
11
11
  attr_reader cache_path: String
12
12
  attr_reader baseline_path: String?
13
13
 
14
- def self.load: (?String path) -> Configuration
14
+ def self.load: (?String? path) -> Configuration
15
15
  def self.discover: () -> String?
16
16
  def self.load_with_includes: (String path, ?visited: Set[String]) -> Hash[String, untyped]
17
17
  def initialize: (?Hash[String, untyped] data) -> void
@@ -28,6 +28,33 @@ signal — each hint has an `id`:
28
28
  | `project-monkey-patch` | A DSL / monkey-patch Rigor can't see. | Escalate — a `pre_eval:` entry or a plugin clears the whole cluster. |
29
29
  | `activerecord-relation-misinference` | Likely an engine gap. | Treat sites as candidate false positives (Phase 2). |
30
30
 
31
+ ### `rigor triage --format json` `.selectors` — the by-(class, method) axis
32
+
33
+ Beside `hints`, the triage JSON carries a `selectors` array: one row
34
+ per dispatch target the diagnostics cluster on, built from the
35
+ structured `receiver_type` / `method_name` fields (never message
36
+ parsing). Each row is `{receiver, method, count, files, rules}`. Use
37
+ it to pick *which sites within a rule* to sample first — and to tell a
38
+ systemic cause from a scatter of real bugs — with `jq`, not eyeballing
39
+ the stream:
40
+
41
+ ```sh
42
+ # the dispatch targets responsible for the most diagnostics
43
+ rigor triage --format json | jq -r '.selectors[:15][] | "\(.count)\t\(.files)f\t\(.receiver)#\(.method)"'
44
+ # one method, one receiver, spread across many files → systemic
45
+ # (a plugin / pre_eval clears it) rather than N independent bugs
46
+ rigor triage --format json | jq '.selectors[] | select(.files >= 4)'
47
+ # narrow to a rule you are about to work, ranked by concentration
48
+ rigor triage --format json \
49
+ | jq '[.selectors[] | select(.rules["call.possible-nil-receiver"])] | sort_by(-.count)'
50
+ ```
51
+
52
+ Read it as: **high `count` × high `files` = a systemic selector**
53
+ (escalate as a decision — one fix clears the cluster); **low `count` =
54
+ a candidate genuine bug** to sample directly in Phase 2. The `receiver`
55
+ is a normalised class (`"hi".squish` and `name.squish` both bucket
56
+ under `String#squish`), so a single idiom does not scatter across rows.
57
+
31
58
  ### `rigor baseline dump --format json` — the bucket list
32
59
 
33
60
  ```sh