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
data/lib/rigor/scope.rb CHANGED
@@ -22,7 +22,8 @@ module Rigor
22
22
  attr_reader :environment, :locals, :fact_store, :self_type,
23
23
  :ivars, :cvars, :globals,
24
24
  :indexed_narrowings, :method_chain_narrowings,
25
- :source_path, :discovery
25
+ :declaration_sourced,
26
+ :source_path, :discovery, :struct_fold_safe_locals
26
27
 
27
28
  # ADR-53 Track A — the seed-time discovery tables live on the
28
29
  # {DiscoveryIndex} the scope carries by a single reference; the
@@ -43,12 +44,20 @@ module Rigor
43
44
  def in_source_constants = @discovery.in_source_constants
44
45
  def discovered_methods = @discovery.discovered_methods
45
46
  def discovered_def_nodes = @discovery.discovered_def_nodes
47
+ def discovered_singleton_def_nodes = @discovery.discovered_singleton_def_nodes
46
48
  def discovered_def_sources = @discovery.discovered_def_sources
47
49
  def discovered_method_visibilities = @discovery.discovered_method_visibilities
48
50
  def discovered_superclasses = @discovery.discovered_superclasses
49
51
  def discovered_includes = @discovery.discovered_includes
50
52
  def discovered_class_sources = @discovery.discovered_class_sources
51
53
  def data_member_layouts = @discovery.data_member_layouts
54
+ def struct_member_layouts = @discovery.struct_member_layouts
55
+ # ADR-67 WD3 — call-site-inferred parameter types, keyed by
56
+ # `[class_name, method_name, kind]`. `build_method_entry_scope` consults
57
+ # this to seed an undeclared `def` parameter with the union of its
58
+ # resolved call-site argument types (precision-additive; an RBS-declared
59
+ # parameter always wins). Empty unless a collection pass seeded it.
60
+ def param_inferred_types = @discovery.param_inferred_types
52
61
 
53
62
  # Narrowing key for an indexed read `receiver[key]` where both
54
63
  # the receiver and the key are stable enough to address. The
@@ -86,8 +95,25 @@ module Rigor
86
95
  EMPTY_VAR_BINDINGS = {}.freeze
87
96
  EMPTY_INDEXED_NARROWINGS = {}.freeze
88
97
  EMPTY_CHAIN_NARROWINGS = {}.freeze
98
+ # ADR-58 WD1 — the set of variable references whose binding's `nil`
99
+ # constituent is *declaration-sourced*: it arrives only via the
100
+ # class-ivar index seed (a ctor `@x = nil` written in another method),
101
+ # never through a method-local write, narrowing, or parameter. Members
102
+ # are frozen `[kind, name]` pairs (`[:ivar, :@x]`, `[:local, :r]`).
103
+ # `possible-nil-receiver` consults this set and declines to fire when the
104
+ # receiver's optionality is purely declaration-sourced — the working
105
+ # program's cross-method invariant is assumed per the robustness
106
+ # principle. Any flow-live touch (write / narrowing) drops the mark, so
107
+ # the diagnostic keeps firing exactly as before on flow-observed nil.
108
+ EMPTY_DECLARATION_SOURCED = Set.new.freeze
109
+ # ADR-48 Struct slice 3 — the per-body set of local names whose struct
110
+ # member reads are fold-safe (provably never mutated / aliased / escaped).
111
+ # A static per-scope context like {#source_path}: inherited unchanged
112
+ # through flow transitions and ignored by `==` / `hash`.
113
+ EMPTY_FOLD_SAFE = Set.new.freeze
89
114
  private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
90
- :EMPTY_CHAIN_NARROWINGS
115
+ :EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED,
116
+ :EMPTY_FOLD_SAFE
91
117
 
92
118
  class << self
93
119
  def empty(environment: Environment.default, source_path: nil)
@@ -106,7 +132,9 @@ module Rigor
106
132
  discovery: DiscoveryIndex::EMPTY,
107
133
  indexed_narrowings: EMPTY_INDEXED_NARROWINGS,
108
134
  method_chain_narrowings: EMPTY_CHAIN_NARROWINGS,
109
- source_path: nil
135
+ declaration_sourced: EMPTY_DECLARATION_SOURCED,
136
+ source_path: nil,
137
+ struct_fold_safe_locals: EMPTY_FOLD_SAFE
110
138
  )
111
139
  @environment = environment
112
140
  @locals = locals
@@ -118,7 +146,9 @@ module Rigor
118
146
  @discovery = discovery
119
147
  @indexed_narrowings = indexed_narrowings
120
148
  @method_chain_narrowings = method_chain_narrowings
149
+ @declaration_sourced = declaration_sourced
121
150
  @source_path = source_path
151
+ @struct_fold_safe_locals = struct_fold_safe_locals
122
152
  freeze
123
153
  end
124
154
 
@@ -141,9 +171,15 @@ module Rigor
141
171
  # narrowing keyed on `(local, :x, :last)` no longer holds.
142
172
  new_indexed_narrowings = drop_indexed_narrowings_for(:local, name)
143
173
  new_chain_narrowings = drop_chain_narrowings_for(:local, name)
174
+ # ADR-58 WD1 — rebinding a local is a flow-live touch: any prior
175
+ # declaration-sourced mark on `name` no longer holds (the new value
176
+ # may carry a method-local nil). `with_declaration_sourced_local`
177
+ # re-establishes the mark afterward when the RHS is a pure copy of a
178
+ # declaration-sourced ivar read; the default is to drop it.
144
179
  rebuild(locals: new_locals, fact_store: new_fact_store,
145
180
  indexed_narrowings: new_indexed_narrowings,
146
- method_chain_narrowings: new_chain_narrowings)
181
+ method_chain_narrowings: new_chain_narrowings,
182
+ declaration_sourced: drop_declaration_sourced_for(:local, name))
147
183
  end
148
184
 
149
185
  def with_fact(fact)
@@ -159,17 +195,29 @@ module Rigor
159
195
  rebuild(self_type: type)
160
196
  end
161
197
 
162
- # ADR-11 per-call-site assertion gating prerequisite. The
163
- # analyzer's per-file boundary stamps the current source
164
- # file's path onto the seed scope; nested rebuilds carry
165
- # the value through so plugin rules (`dynamic_return`'s
166
- # `file_methods:` gate, sigil checks) can resolve "which
167
- # file does this call site belong to?" without
198
+ # ADR-28 / ADR-52 slice 5a — per-file source path carried on
199
+ # the scope. The analyzer stamps the current file's path onto
200
+ # the seed scope; nested rebuilds propagate it so plugin rules
201
+ # (`dynamic_return`'s `file_methods:` gate, sigil checks) can
202
+ # resolve "which file does this call site belong to?" without
168
203
  # thread-locals.
169
204
  def with_source_path(path)
170
205
  rebuild(source_path: path)
171
206
  end
172
207
 
208
+ # ADR-48 Struct slice 3 — installs the per-body fold-safe-local set
209
+ # ({Inference::StructFoldSafety}). Set once at body entry; inherited
210
+ # unchanged through subsequent flow transitions.
211
+ def with_struct_fold_safe(locals)
212
+ rebuild(struct_fold_safe_locals: locals)
213
+ end
214
+
215
+ # True when `name`'s `Struct` member reads are fold-safe in this body
216
+ # (the local is provably never mutated / aliased / escaped).
217
+ def struct_fold_safe?(name)
218
+ @struct_fold_safe_locals.include?(name.to_sym)
219
+ end
220
+
173
221
  # ADR-53 Track A — swaps the whole discovery index in one transition.
174
222
  # The sole seeding path; the per-table writers it replaced are derived
175
223
  # off-`Scope` through `scope.discovery.with(table_name: table)`.
@@ -201,9 +249,46 @@ module Rigor
201
249
  def with_ivar(name, type)
202
250
  new_indexed_narrowings = drop_indexed_narrowings_for(:ivar, name)
203
251
  new_chain_narrowings = drop_chain_narrowings_for(:ivar, name)
252
+ # ADR-58 WD1 — a method-local ivar write or narrowing is flow-live:
253
+ # drop any declaration-sourced mark so subsequent reads of `@name`
254
+ # observe flow-live provenance and fire as before. The seed path uses
255
+ # `seed_declaration_sourced_ivar` to (re-)establish the mark.
204
256
  rebuild(ivars: @ivars.merge(name.to_sym => type).freeze,
205
257
  indexed_narrowings: new_indexed_narrowings,
206
- method_chain_narrowings: new_chain_narrowings)
258
+ method_chain_narrowings: new_chain_narrowings,
259
+ declaration_sourced: drop_declaration_sourced_for(:ivar, name))
260
+ end
261
+
262
+ # ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only
263
+ # provenance is the class-ivar index. Unlike `with_ivar` this binds the
264
+ # type AND records the declaration-sourced mark in one transition.
265
+ def seed_declaration_sourced_ivar(name, type)
266
+ rebuild(ivars: @ivars.merge(name.to_sym => type).freeze,
267
+ declaration_sourced: add_declaration_sourced(:ivar, name))
268
+ end
269
+
270
+ # ADR-58 WD1 — a local assignment `r = @right` whose RHS is a pure read
271
+ # of a declaration-sourced ivar inherits the mark, so the survey's exact
272
+ # rotation/traversal shape (`r = @right; r.key`) does not fire. Binds the
273
+ # type and stamps the local's mark in one transition (the plain
274
+ # `with_local` would have dropped it).
275
+ def with_declaration_sourced_local(name, type)
276
+ written = with_local(name, type)
277
+ written.with_local_declaration_mark(name)
278
+ end
279
+
280
+ # ADR-58 WD1 — re-stamp the local mark on a scope produced by
281
+ # `with_local` (which always drops it). Public so the sibling
282
+ # `with_declaration_sourced_local` can call it across the new
283
+ # post-write receiver without reaching into a private method.
284
+ def with_local_declaration_mark(name)
285
+ rebuild(declaration_sourced: add_declaration_sourced(:local, name))
286
+ end
287
+
288
+ # ADR-58 WD1 — true when `(kind, name)`'s binding optionality is purely
289
+ # declaration-sourced (no flow-live write/narrowing has touched it).
290
+ def declaration_sourced?(kind, name)
291
+ @declaration_sourced.include?([kind.to_sym, name.to_sym])
207
292
  end
208
293
 
209
294
  def with_cvar(name, type)
@@ -214,6 +299,23 @@ module Rigor
214
299
  rebuild(globals: @globals.merge(name.to_sym => type).freeze)
215
300
  end
216
301
 
302
+ # Regex match-data globals (`$~`, `$&`, `$1..$9`, the pre/post-match
303
+ # and last-paren back-references). Narrowed on a successful-`=~` /
304
+ # `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`);
305
+ # any subsequent method call could run another match and rebind every
306
+ # one of them, so `eval_call` forgets the narrowed facts here. Always
307
+ # safe — only drops facts, so a subsequent read falls back to the
308
+ # default `String | nil`. Program-level `$GLOBAL = ...` seeds use other
309
+ # names and are untouched.
310
+ MATCH_DATA_GLOBALS = %i[$~ $& $` $' $+ $1 $2 $3 $4 $5 $6 $7 $8 $9].freeze
311
+ private_constant :MATCH_DATA_GLOBALS
312
+
313
+ def forget_match_globals
314
+ return self unless @globals.keys.any? { |k| MATCH_DATA_GLOBALS.include?(k) }
315
+
316
+ rebuild(globals: @globals.except(*MATCH_DATA_GLOBALS).freeze)
317
+ end
318
+
217
319
  # Slice 7 phase 2 — class-level ivar accumulator. Keyed by
218
320
  # the qualified class name (e.g. `"Rigor::Scope"`); the
219
321
  # value is a `Hash[Symbol, Type::t]` of every ivar that
@@ -263,8 +365,9 @@ module Rigor
263
365
  # scope (no enclosing `class` / `module` body). True at the top
264
366
  # of a file AND inside a top-level `def` body (since toplevel
265
367
  # defs leave `self_type` nil per the existing scope-construction
266
- # contract, mirroring how ADR-24's `adoptable_self_call_result?`
267
- # also keys on `self_type.nil?` for the same context). Used by
368
+ # contract the same nil-`self_type` signal ADR-24's self-call
369
+ # return adoption historically keyed on before ADR-57 opened the
370
+ # gate unconditionally). Used by
268
371
  # `CheckRules#unresolved_toplevel_diagnostic` to gate the
269
372
  # `call.unresolved-toplevel` rule so it fires only outside
270
373
  # class / module bodies, where Rails-DSL metaprogramming
@@ -289,6 +392,22 @@ module Rigor
289
392
  node
290
393
  end
291
394
 
395
+ # Module-singleton call resolution (ADR-57 follow-up) — companion of
396
+ # {#user_def_for} for SINGLETON-side defs (`def self.x`, `def Foo.x`,
397
+ # `class << self` bodies, and `module_function` defs). Returns the
398
+ # `Prism::DefNode` for `class_name.method_name` invoked on the
399
+ # module/class constant itself, or nil. The `discovered_def_nodes`
400
+ # table is deliberately instance-side only (its ancestor walk binds
401
+ # `self` as `Nominal`), so singleton bodies live in a parallel table
402
+ # the `ScopeIndexer` populates alongside it. Records the same
403
+ # cross-file dependency edge as the instance path (ADR-46).
404
+ def singleton_def_for(class_name, method_name)
405
+ table = @discovery.discovered_singleton_def_nodes[class_name.to_s]
406
+ node = table && table[method_name.to_sym]
407
+ record_cross_file_method(class_name, method_name, node) if Analysis::DependencyRecorder.active?
408
+ node
409
+ end
410
+
292
411
  # ADR-46 slice 1 — note the cross-file dependency this resolution
293
412
  # creates: the file defining `class_name#method_name` (the consumer's
294
413
  # analysis reads its body via `infer_user_method_return`), or, when
@@ -392,6 +511,20 @@ module Rigor
392
511
  layout
393
512
  end
394
513
 
514
+ # ADR-48 Struct follow-up — the `{ members:, keyword_init: }` layout
515
+ # recorded for a `Struct.new(...)`-defined class, in the constant form
516
+ # (`Point = Struct.new(:x, :y)`) and the named-subclass form
517
+ # (`class Point < Struct.new(:x, :y)`). Consumed by
518
+ # {Inference::MethodDispatcher::StructFolding} so `Point.new(...)` on a
519
+ # `Singleton[Point]` receiver materialises a member instance. Returns nil
520
+ # when the class has no recorded struct layout. Mirrors
521
+ # {#data_member_layout}'s dependency-recording contract.
522
+ def struct_member_layout(class_name)
523
+ layout = @discovery.struct_member_layouts[class_name.to_s]
524
+ record_class_dependency(class_name) if layout && Analysis::DependencyRecorder.active?
525
+ layout
526
+ end
527
+
395
528
  # ADR-24 slice 2 — per-class/module table mapping a fully
396
529
  # qualified user class or module to the list of module
397
530
  # names it `include`s / `prepend`s, AS WRITTEN at the
@@ -564,7 +697,8 @@ module Rigor
564
697
  @cvars == other.cvars &&
565
698
  @globals == other.globals &&
566
699
  @indexed_narrowings == other.indexed_narrowings &&
567
- @method_chain_narrowings == other.method_chain_narrowings
700
+ @method_chain_narrowings == other.method_chain_narrowings &&
701
+ @declaration_sourced == other.declaration_sourced
568
702
  end
569
703
  alias eql? ==
570
704
 
@@ -580,7 +714,9 @@ module Rigor
580
714
  discovery: @discovery,
581
715
  indexed_narrowings: @indexed_narrowings,
582
716
  method_chain_narrowings: @method_chain_narrowings,
583
- source_path: @source_path
717
+ declaration_sourced: @declaration_sourced,
718
+ source_path: @source_path,
719
+ struct_fold_safe_locals: @struct_fold_safe_locals
584
720
  )
585
721
  self.class.new(
586
722
  environment: environment, locals: locals,
@@ -589,7 +725,9 @@ module Rigor
589
725
  discovery: discovery,
590
726
  indexed_narrowings: indexed_narrowings,
591
727
  method_chain_narrowings: method_chain_narrowings,
592
- source_path: source_path
728
+ declaration_sourced: declaration_sourced,
729
+ source_path: source_path,
730
+ struct_fold_safe_locals: struct_fold_safe_locals
593
731
  )
594
732
  end
595
733
 
@@ -621,10 +759,22 @@ module Rigor
621
759
  discovery: @discovery,
622
760
  indexed_narrowings: join_bindings(@indexed_narrowings, other.indexed_narrowings),
623
761
  method_chain_narrowings: join_bindings(@method_chain_narrowings, other.method_chain_narrowings),
762
+ # ADR-58 WD1 — a ref is declaration-sourced after a join only when
763
+ # BOTH branches agree it is. If either path made the binding
764
+ # flow-live (a method-local nil write / failed-guard narrowing), the
765
+ # merge is flow-live and `possible-nil-receiver` fires as before.
766
+ declaration_sourced: join_declaration_sourced(other),
624
767
  source_path: source_path
625
768
  )
626
769
  end
627
770
 
771
+ def join_declaration_sourced(other)
772
+ return @declaration_sourced if @declaration_sourced.equal?(other.declaration_sourced)
773
+ return EMPTY_DECLARATION_SOURCED if @declaration_sourced.empty? || other.declaration_sourced.empty?
774
+
775
+ (@declaration_sourced & other.declaration_sourced).freeze
776
+ end
777
+
628
778
  def indexed_key(receiver_kind, receiver_name, key)
629
779
  IndexedKey.new(
630
780
  receiver_kind: receiver_kind.to_sym,
@@ -652,6 +802,25 @@ module Rigor
652
802
  filtered.size == @indexed_narrowings.size ? @indexed_narrowings : filtered.freeze
653
803
  end
654
804
 
805
+ # ADR-58 WD1 — set/clear the declaration-sourced provenance mark.
806
+ def add_declaration_sourced(kind, name)
807
+ ref = [kind.to_sym, name.to_sym]
808
+ return @declaration_sourced if @declaration_sourced.include?(ref)
809
+
810
+ (@declaration_sourced.dup << ref).freeze
811
+ end
812
+
813
+ def drop_declaration_sourced_for(kind, name)
814
+ return @declaration_sourced if @declaration_sourced.empty?
815
+
816
+ ref = [kind.to_sym, name.to_sym]
817
+ return @declaration_sourced unless @declaration_sourced.include?(ref)
818
+
819
+ dropped = @declaration_sourced.dup
820
+ dropped.delete(ref)
821
+ dropped.freeze
822
+ end
823
+
655
824
  def drop_chain_narrowings_for(receiver_kind, receiver_name)
656
825
  return @method_chain_narrowings if @method_chain_narrowings.empty?
657
826
 
@@ -118,7 +118,15 @@ module Rigor
118
118
  @class_shells = Set.new
119
119
  defs = collect_method_definitions(parse_result.value)
120
120
  candidates_from_defs = defs.filter_map do |def_node, class_name, kind|
121
+ # An analyzer bug typing one def's body must cost only that
122
+ # def's candidate, never the whole `rigor sig-gen` run. The
123
+ # `check` path recovers each *file* this way
124
+ # (worker_session.rb); sig-gen recovers per-def so the rest of
125
+ # the file's candidates still emit.
126
+
121
127
  classify_def(path, def_node, class_name, kind, scope_index)
128
+ rescue StandardError
129
+ nil
122
130
  end
123
131
  obs_ivar_map = build_observed_ivar_map(parse_result.value)
124
132
  candidates_from_defs + collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map)
@@ -5,9 +5,9 @@ module Rigor
5
5
  # Per-call-site argument observation produced by
6
6
  # {ObservationCollector}. ADR-14 follow-up: the earlier
7
7
  # MVP shape (`Array[Type]` of positional types only)
8
- # could not represent keyword arguments — every call like
9
- # `MethodCatalog.new(path: ..., mutating_selectors: ...)`
10
- # discarded the whole observation via `non_positional?`.
8
+ # could not represent keyword arguments — keyword calls
9
+ # like `MethodCatalog.new(path: ..., mutating_selectors: ...)`
10
+ # were silently skipped in that shape.
11
11
  # The new shape carries positional and keyword arg types
12
12
  # in parallel so the per-position / per-keyword unions
13
13
  # can each be reconstructed independently.
@@ -46,6 +46,11 @@ module Rigor
46
46
  def initialize(path_mapper:, overwrite: false)
47
47
  @path_mapper = path_mapper
48
48
  @overwrite = overwrite
49
+ # Run-level (cross-file) namespace-kind view, populated
50
+ # per `#write_all` from every candidate's per-file map.
51
+ # Empty until then so the single-target `#write` path
52
+ # falls back to per-candidate kinds only.
53
+ @global_namespace_kinds = {}
49
54
  end
50
55
 
51
56
  # Process the full candidate list by resolving each
@@ -65,6 +70,7 @@ module Rigor
65
70
  emittable = candidates.select { |c| EMITTABLE.include?(c.classification) }
66
71
  return [] if emittable.empty?
67
72
 
73
+ @global_namespace_kinds = build_namespace_kinds(candidates)
68
74
  emittable.group_by { |c| @path_mapper.target_for(c.path, class_name: c.class_name) }
69
75
  .map { |target, group| write_target(target, group) }
70
76
  end
@@ -161,13 +167,45 @@ module Rigor
161
167
  end
162
168
 
163
169
  def merged_namespace_kinds(candidates)
164
- merged = {}
170
+ merged = @global_namespace_kinds.dup
165
171
  candidates.each do |c|
166
- (c.namespace_kinds || {}).each { |k, v| merged[k] = v }
172
+ (c.namespace_kinds || {}).each { |k, v| apply_namespace_kind(merged, k, v) }
167
173
  end
168
174
  merged
169
175
  end
170
176
 
177
+ # Folds every candidate's per-file namespace-kind map
178
+ # into one run-level view so a `class Foo` recorded
179
+ # while scanning `foo.rb` governs the wrapper keyword
180
+ # emitted for `Foo` in a *sibling* file's target — e.g.
181
+ # `foo/bar.rb` declaring `class Foo::Bar`, whose compact
182
+ # constant path never names `Foo`, so the walker records
183
+ # no kind for it. Without this view that sibling target
184
+ # wraps the nested class in `module Foo` while `foo.rbs`
185
+ # declares `class Foo`; loading both raises
186
+ # `RBS::DuplicatedDeclarationError`, aborting the whole
187
+ # RBS env build.
188
+ def build_namespace_kinds(candidates)
189
+ candidates.each_with_object({}) do |candidate, acc|
190
+ (candidate.namespace_kinds || {}).each { |name, kind| apply_namespace_kind(acc, name, kind) }
191
+ end
192
+ end
193
+
194
+ # A `class` declaration is authoritative and MUST win
195
+ # over the `:module` wrapper default: a compact
196
+ # `class Foo::Bar` never names `Foo`, so the only signal
197
+ # for `Foo`'s kind is an actual `class Foo` (or
198
+ # `Const = Data.define(...)` shell) seen elsewhere. This
199
+ # guarantees the generated tree never mixes `class` /
200
+ # `module` for the same constant.
201
+ def apply_namespace_kind(map, key, kind)
202
+ if kind == :class
203
+ map[key] = :class
204
+ else
205
+ map[key] ||= :module
206
+ end
207
+ end
208
+
171
209
  # Tree node: { name:, children: Hash{String => node},
172
210
  # methods: Array<MethodCandidate>, shell: Boolean }.
173
211
  # `shell` flags nodes that came in via `class_shells`
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Source
5
+ # Flattens a Prism constant-reference node (`ConstantReadNode` /
6
+ # `ConstantPathNode`) to its source-qualified `"A::B::C"` string.
7
+ #
8
+ # Two nil policies for the one edge case that distinguishes the call
9
+ # sites — a constant path rooted in a *dynamic* base (`expr::Bar`, where
10
+ # the left side is a runtime expression rather than a constant):
11
+ #
12
+ # * {.qualified_name} / {.render} are LENIENT — they drop the dynamic
13
+ # segment and render the trailing constant names (`expr::Bar` => "Bar").
14
+ # The scope indexer and statement evaluator feed only genuine
15
+ # class/module path nodes and want a best-effort name.
16
+ # * {.qualified_name_or_nil} is STRICT — a dynamic base anywhere in the
17
+ # chain yields `nil`, so a caller that statically names constants can
18
+ # treat the path as opaque rather than guessing.
19
+ #
20
+ # A leading `::` (absolute root, `::Foo`) renders as `"Foo"` under both
21
+ # policies. A node that is neither a `ConstantReadNode` nor a
22
+ # `ConstantPathNode` yields `nil` under both.
23
+ module ConstantPath
24
+ module_function
25
+
26
+ # Lenient dispatch over a constant-reference node.
27
+ def qualified_name(node)
28
+ case node
29
+ when Prism::ConstantReadNode then node.name.to_s
30
+ when Prism::ConstantPathNode then render(node)
31
+ end
32
+ end
33
+
34
+ # Lenient render of a `ConstantPathNode`; never nil for a path node.
35
+ def render(node)
36
+ prefix =
37
+ case node.parent
38
+ when Prism::ConstantReadNode then "#{node.parent.name}::"
39
+ when Prism::ConstantPathNode then "#{render(node.parent)}::"
40
+ else ""
41
+ end
42
+ "#{prefix}#{node.name}"
43
+ end
44
+
45
+ # Strict dispatch: a dynamic base anywhere in the path yields nil.
46
+ def qualified_name_or_nil(node)
47
+ case node
48
+ when Prism::ConstantReadNode
49
+ node.name.to_s
50
+ when Prism::ConstantPathNode
51
+ parent = node.parent
52
+ return node.name.to_s if parent.nil?
53
+
54
+ parent_name = qualified_name_or_nil(parent)
55
+ return nil if parent_name.nil?
56
+
57
+ "#{parent_name}::#{node.name}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/rigor/source.rb CHANGED
@@ -14,3 +14,4 @@ end
14
14
  require_relative "source/node_locator"
15
15
  require_relative "source/node_walker"
16
16
  require_relative "source/literals"
17
+ require_relative "source/constant_path"
@@ -304,26 +304,11 @@ module Rigor
304
304
  m && [m[1], m[2]]
305
305
  end
306
306
 
307
- # Normalises a message receiver token to a class name.
308
- # Integer / string / symbol literals fold to their class;
309
- # `Foo[...]` keeps the `Array[...]` form (H4 needs it);
310
- # `singleton(Foo)` and bare `Foo` fold to `Foo`.
307
+ # Normalises a message receiver token to a class name. The fold
308
+ # logic is shared with the selector axis see
309
+ # {Triage.normalize_receiver}.
311
310
  def receiver_class(token)
312
- t = token.strip
313
- return "Integer" if t.match?(/\A-?\d+\z/)
314
- return "Float" if t.match?(/\A-?\d+\.\d+\z/)
315
- return "String" if t.start_with?('"', "'")
316
- return "Symbol" if t.start_with?(":")
317
-
318
- singleton = t[/\Asingleton\(([\w:]+)\)\z/, 1]
319
- return singleton if singleton
320
- return t if t.start_with?("Array[")
321
-
322
- nominal = t[/\A([\w:]+)\[/, 1]
323
- return nominal if nominal
324
- return t if t.match?(/\A[\w:]+\z/)
325
-
326
- nil
311
+ Triage.normalize_receiver(token)
327
312
  end
328
313
 
329
314
  def activesupport?(receiver, method)
data/lib/rigor/triage.rb CHANGED
@@ -18,7 +18,15 @@ module Rigor
18
18
  Summary = Data.define(:total, :error, :warning, :info)
19
19
  RuleCount = Data.define(:rule, :count)
20
20
  Hotspot = Data.define(:file, :count, :by_rule)
21
- Report = Data.define(:summary, :distribution, :hotspots, :hints)
21
+ # A (receiver-class, method) dispatch target the diagnostics
22
+ # cluster on, built from the structured `Diagnostic#receiver_type`
23
+ # / `#method_name` fields — never from message-string parsing.
24
+ # `receiver` is nil for method-only diagnostics (a toplevel call,
25
+ # a `def`-side return / override finding that has no call
26
+ # receiver); `files` is the distinct-file count (a systemic vs.
27
+ # localised signal); `rules` is the per-rule breakdown.
28
+ Selector = Data.define(:receiver, :method_name, :count, :files, :rules)
29
+ Report = Data.define(:summary, :distribution, :selectors, :hotspots, :hints)
22
30
 
23
31
  module_function
24
32
 
@@ -30,6 +38,7 @@ module Rigor
30
38
  Report.new(
31
39
  summary: build_summary(diagnostics),
32
40
  distribution: build_distribution(diagnostics),
41
+ selectors: build_selectors(diagnostics),
33
42
  hotspots: build_hotspots(diagnostics, top),
34
43
  hints: hints ? Catalogue.recognise(diagnostics) : []
35
44
  )
@@ -57,6 +66,61 @@ module Rigor
57
66
  .sort_by { |row| [-row.count, row.rule] }
58
67
  end
59
68
 
69
+ # The class/method aggregation axis (ADR-23 follow-up). Groups
70
+ # every diagnostic that carries a `method_name` by its
71
+ # `(receiver_type, method_name)` pair so a consumer can answer
72
+ # "which method / class concentrates the diagnostics?" with a
73
+ # `jq` query over the JSON instead of parsing message text.
74
+ # Method-only diagnostics (nil `receiver_type`) keep a null
75
+ # `receiver` and still group by method. The full list is
76
+ # returned uncapped — the JSON is the agent-facing surface; the
77
+ # text renderer caps its own rows.
78
+ def build_selectors(diagnostics)
79
+ diagnostics.select(&:method_name)
80
+ .group_by { |d| [normalize_receiver(d.receiver_type) || d.receiver_type, d.method_name.to_s] }
81
+ .map { |(receiver, method), group| selector_for(receiver, method, group) }
82
+ .sort_by { |s| [-s.count, s.receiver.to_s, s.method_name] }
83
+ end
84
+
85
+ # Folds a receiver token — a `Diagnostic#receiver_type` display
86
+ # string or a message-parsed token — to the class the diagnostics
87
+ # should bucket under, so the selector axis does not fragment one
88
+ # method across every distinct literal receiver. String / integer
89
+ # / float / symbol literals collapse to their class; `singleton(C)`
90
+ # and a bare `C` fold to `C`; a generic `C[...]` keeps the
91
+ # `Array[String]` element form (the AR-relation heuristic keys on
92
+ # it). Returns nil for a token it cannot reduce to a class (a
93
+ # union display, an inferred shape) — the caller keeps the raw
94
+ # string then, never losing the row. Shared with {Catalogue}.
95
+ def normalize_receiver(token)
96
+ return nil if token.nil?
97
+
98
+ t = token.to_s.strip
99
+ return "Integer" if t.match?(/\A-?\d+\z/)
100
+ return "Float" if t.match?(/\A-?\d+\.\d+\z/)
101
+ return "String" if t.start_with?('"', "'")
102
+ return "Symbol" if t.start_with?(":")
103
+
104
+ singleton = t[/\Asingleton\(([\w:]+)\)\z/, 1]
105
+ return singleton if singleton
106
+ return t if t.start_with?("Array[")
107
+
108
+ nominal = t[/\A([\w:]+)\[/, 1]
109
+ return nominal if nominal
110
+ return t if t.match?(/\A[\w:]+\z/)
111
+
112
+ nil
113
+ end
114
+
115
+ def selector_for(receiver, method, group)
116
+ rules = group.group_by { |d| rule_key(d) }
117
+ .transform_values(&:size)
118
+ .sort_by { |rule, count| [-count, rule] }
119
+ .to_h
120
+ Selector.new(receiver: receiver, method_name: method, count: group.size,
121
+ files: group.map(&:path).uniq.size, rules: rules)
122
+ end
123
+
60
124
  def build_hotspots(diagnostics, top)
61
125
  diagnostics.group_by(&:path)
62
126
  .map { |path, group| hotspot_for(path, group) }
@@ -79,6 +143,10 @@ module Rigor
79
143
  "warning" => report.summary.warning, "info" => report.summary.info
80
144
  },
81
145
  "distribution" => report.distribution.map { |r| { "rule" => r.rule, "count" => r.count } },
146
+ "selectors" => report.selectors.map do |s|
147
+ { "receiver" => s.receiver, "method" => s.method_name, "count" => s.count,
148
+ "files" => s.files, "rules" => s.rules }
149
+ end,
82
150
  "hotspots" => report.hotspots.map do |h|
83
151
  { "file" => h.file, "count" => h.count, "by_rule" => h.by_rule }
84
152
  end,
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -46,17 +47,7 @@ module Rigor
46
47
  "Method"
47
48
  end
48
49
 
49
- def top
50
- Trinary.no
51
- end
52
-
53
- def bot
54
- Trinary.no
55
- end
56
-
57
- def dynamic
58
- Trinary.no
59
- end
50
+ include Rigor::Type::PlainLattice
60
51
 
61
52
  include Rigor::Type::AcceptanceRouter
62
53