rigortype 0.1.16 → 0.1.18

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
  4. data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
  5. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
  6. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +226 -0
  7. data/lib/rigor/analysis/check_rules.rb +180 -73
  8. data/lib/rigor/analysis/dependency_recorder.rb +122 -0
  9. data/lib/rigor/analysis/diagnostic.rb +18 -0
  10. data/lib/rigor/analysis/incremental.rb +162 -0
  11. data/lib/rigor/analysis/incremental_session.rb +337 -0
  12. data/lib/rigor/analysis/rule_catalog.rb +48 -0
  13. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
  14. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  15. data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
  16. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  17. data/lib/rigor/analysis/runner.rb +477 -1110
  18. data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
  19. data/lib/rigor/analysis/worker_session.rb +47 -8
  20. data/lib/rigor/builtins/static_return_refinements.rb +7 -1
  21. data/lib/rigor/cache/descriptor.rb +50 -49
  22. data/lib/rigor/cache/incremental_snapshot.rb +153 -0
  23. data/lib/rigor/cache/rbs_cache_producer.rb +34 -0
  24. data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
  25. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
  26. data/lib/rigor/cache/rbs_constant_table.rb +2 -8
  27. data/lib/rigor/cache/rbs_environment.rb +2 -8
  28. data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
  29. data/lib/rigor/cache/store.rb +145 -14
  30. data/lib/rigor/cli/annotate_command.rb +2 -7
  31. data/lib/rigor/cli/baseline_command.rb +2 -7
  32. data/lib/rigor/cli/check_command.rb +705 -0
  33. data/lib/rigor/cli/ci_detector.rb +94 -0
  34. data/lib/rigor/cli/command.rb +47 -0
  35. data/lib/rigor/cli/coverage_command.rb +3 -23
  36. data/lib/rigor/cli/coverage_renderer.rb +3 -8
  37. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  38. data/lib/rigor/cli/diff_command.rb +3 -7
  39. data/lib/rigor/cli/explain_command.rb +2 -7
  40. data/lib/rigor/cli/lsp_command.rb +3 -7
  41. data/lib/rigor/cli/mcp_command.rb +3 -7
  42. data/lib/rigor/cli/options.rb +57 -0
  43. data/lib/rigor/cli/plugin_command.rb +3 -7
  44. data/lib/rigor/cli/plugins_command.rb +2 -7
  45. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  46. data/lib/rigor/cli/renderable.rb +26 -0
  47. data/lib/rigor/cli/sig_gen_command.rb +2 -7
  48. data/lib/rigor/cli/skill_command.rb +3 -7
  49. data/lib/rigor/cli/trace_command.rb +143 -0
  50. data/lib/rigor/cli/trace_renderer.rb +310 -0
  51. data/lib/rigor/cli/triage_command.rb +2 -7
  52. data/lib/rigor/cli/type_of_command.rb +5 -38
  53. data/lib/rigor/cli/type_of_renderer.rb +4 -9
  54. data/lib/rigor/cli/type_scan_command.rb +3 -23
  55. data/lib/rigor/cli/type_scan_renderer.rb +4 -9
  56. data/lib/rigor/cli.rb +15 -532
  57. data/lib/rigor/configuration/dependencies.rb +18 -1
  58. data/lib/rigor/configuration/severity_profile.rb +22 -3
  59. data/lib/rigor/configuration.rb +16 -3
  60. data/lib/rigor/environment/rbs_loader.rb +129 -71
  61. data/lib/rigor/environment.rb +1 -1
  62. data/lib/rigor/inference/acceptance.rb +10 -0
  63. data/lib/rigor/inference/block_parameter_binder.rb +1 -2
  64. data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
  65. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
  66. data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
  67. data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
  68. data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
  69. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
  70. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
  71. data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
  72. data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
  73. data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
  74. data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
  75. data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
  76. data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
  77. data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
  78. data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
  79. data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
  80. data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
  81. data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
  82. data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
  83. data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
  84. data/lib/rigor/inference/expression_typer.rb +149 -63
  85. data/lib/rigor/inference/flow_tracer.rb +180 -0
  86. data/lib/rigor/inference/macro_block_self_type.rb +10 -11
  87. data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
  88. data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
  89. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
  90. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
  91. data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
  92. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
  93. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  94. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
  95. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
  96. data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
  97. data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
  98. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  99. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
  100. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
  101. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
  102. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
  103. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
  104. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
  105. data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
  106. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
  107. data/lib/rigor/inference/method_dispatcher.rb +185 -84
  108. data/lib/rigor/inference/narrowing.rb +262 -5
  109. data/lib/rigor/inference/scope_indexer.rb +208 -21
  110. data/lib/rigor/inference/statement_evaluator.rb +110 -48
  111. data/lib/rigor/language_server/buffer_resolution.rb +33 -0
  112. data/lib/rigor/language_server/completion_provider.rb +4 -4
  113. data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
  114. data/lib/rigor/language_server/folding_range_provider.rb +4 -4
  115. data/lib/rigor/language_server/hover_provider.rb +4 -4
  116. data/lib/rigor/language_server/selection_range_provider.rb +4 -4
  117. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  118. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  119. data/lib/rigor/plugin/base.rb +302 -45
  120. data/lib/rigor/plugin/node_rule_walk.rb +147 -0
  121. data/lib/rigor/plugin/registry.rb +281 -15
  122. data/lib/rigor/plugin.rb +1 -0
  123. data/lib/rigor/rbs_extended/conformance_checker.rb +293 -0
  124. data/lib/rigor/rbs_extended.rb +39 -0
  125. data/lib/rigor/scope/discovery_index.rb +58 -0
  126. data/lib/rigor/scope.rb +150 -167
  127. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  128. data/lib/rigor/source/literals.rb +14 -0
  129. data/lib/rigor/type/acceptance_router.rb +19 -0
  130. data/lib/rigor/type/accepts_result.rb +3 -10
  131. data/lib/rigor/type/app.rb +3 -7
  132. data/lib/rigor/type/bot.rb +2 -3
  133. data/lib/rigor/type/bound_method.rb +5 -12
  134. data/lib/rigor/type/combinator.rb +22 -0
  135. data/lib/rigor/type/constant.rb +2 -3
  136. data/lib/rigor/type/data_class.rb +80 -0
  137. data/lib/rigor/type/data_instance.rb +100 -0
  138. data/lib/rigor/type/difference.rb +5 -10
  139. data/lib/rigor/type/dynamic.rb +5 -10
  140. data/lib/rigor/type/hash_shape.rb +5 -15
  141. data/lib/rigor/type/integer_range.rb +5 -10
  142. data/lib/rigor/type/intersection.rb +5 -10
  143. data/lib/rigor/type/nominal.rb +5 -10
  144. data/lib/rigor/type/refined.rb +5 -10
  145. data/lib/rigor/type/singleton.rb +5 -10
  146. data/lib/rigor/type/top.rb +2 -3
  147. data/lib/rigor/type/tuple.rb +5 -10
  148. data/lib/rigor/type/union.rb +5 -10
  149. data/lib/rigor/type.rb +2 -0
  150. data/lib/rigor/value_semantics.rb +77 -0
  151. data/lib/rigor/version.rb +1 -1
  152. data/lib/rigor.rb +1 -1
  153. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
  156. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  157. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
  158. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  159. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  160. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  161. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  163. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  164. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  166. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  167. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  168. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
  169. data/sig/rigor/cache.rbs +19 -0
  170. data/sig/rigor/environment.rbs +0 -2
  171. data/sig/rigor/inference.rbs +27 -0
  172. data/sig/rigor/plugin/base.rbs +1 -2
  173. data/sig/rigor/rbs_extended.rbs +2 -0
  174. data/sig/rigor/scope.rbs +42 -25
  175. data/sig/rigor/source.rbs +1 -0
  176. data/sig/rigor/type.rbs +58 -1
  177. data/sig/rigor.rbs +6 -1
  178. data/skills/rigor-ci-setup/SKILL.md +319 -0
  179. metadata +36 -2
  180. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -79
@@ -0,0 +1,580 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../diagnostic"
4
+
5
+ module Rigor
6
+ module Analysis
7
+ class Runner
8
+ # Builds and orders every project-level diagnostic stream the
9
+ # {Runner} surfaces — the pre-file streams (plugin load / prepare,
10
+ # ADR-10 dependency-source, pre-eval, RBS coverage, path errors),
11
+ # the post-analysis streams (synthesized namespaces, conforms-to,
12
+ # the three reporter drains), and the final severity stamp.
13
+ #
14
+ # Constraint: the relative order of every stream below is the
15
+ # diagnostic output contract — callers MUST NOT reorder the
16
+ # concatenation in `pre_file_diagnostics` or the post-analysis
17
+ # streams the {Runner} drains after `analyze_files`.
18
+ #
19
+ # The collaborator holds the immutable per-run inputs (the
20
+ # configuration and the three mutable reporter accumulators, which
21
+ # are shared instances the dispatcher records into). The per-run
22
+ # varying state produced by other passes (the plugin registry, the
23
+ # dependency-source index, and the four end-of-pass snapshots) is
24
+ # read through injected reader procs so this collaborator never
25
+ # calls back into the {Runner} and the read happens at the exact
26
+ # point in the run the original inline read did.
27
+ class DiagnosticAggregator # rubocop:disable Metrics/ClassLength
28
+ # @param configuration [Rigor::Configuration]
29
+ # @param rbs_extended_reporter [RbsExtended::Reporter]
30
+ # @param boundary_cross_reporter
31
+ # [DependencySourceInference::BoundaryCrossReporter]
32
+ # @param source_rbs_synthesis_reporter
33
+ # [Plugin::SourceRbsSynthesisReporter]
34
+ # @param plugin_registry [#call] reader returning the current
35
+ # {Plugin::Registry} (varies per run).
36
+ # @param dependency_source_index [#call] reader returning the
37
+ # current {DependencySourceInference::Index}.
38
+ # @param pool_mode [#call] reader returning the pool-mode flag.
39
+ # @param cached_plugin_prepare_diagnostics [#call] reader
40
+ # returning the prepare-diagnostic snapshot.
41
+ # @param pre_eval_diagnostics_from_scanner [#call] reader
42
+ # returning the pre-eval scanner diagnostics.
43
+ # @param synthesized_namespaces_snapshot [#call] reader.
44
+ # @param conformance_results_snapshot [#call] reader.
45
+ def initialize(configuration:, rbs_extended_reporter:, boundary_cross_reporter:, # rubocop:disable Metrics/ParameterLists
46
+ source_rbs_synthesis_reporter:, plugin_registry:, dependency_source_index:,
47
+ pool_mode:, cached_plugin_prepare_diagnostics:,
48
+ pre_eval_diagnostics_from_scanner:, synthesized_namespaces_snapshot:,
49
+ conformance_results_snapshot:)
50
+ @configuration = configuration
51
+ @rbs_extended_reporter = rbs_extended_reporter
52
+ @boundary_cross_reporter = boundary_cross_reporter
53
+ @source_rbs_synthesis_reporter = source_rbs_synthesis_reporter
54
+ @plugin_registry_reader = plugin_registry
55
+ @dependency_source_index_reader = dependency_source_index
56
+ @pool_mode_reader = pool_mode
57
+ @cached_plugin_prepare_diagnostics_reader = cached_plugin_prepare_diagnostics
58
+ @pre_eval_diagnostics_from_scanner_reader = pre_eval_diagnostics_from_scanner
59
+ @synthesized_namespaces_snapshot_reader = synthesized_namespaces_snapshot
60
+ @conformance_results_snapshot_reader = conformance_results_snapshot
61
+ end
62
+
63
+ # Pre-file diagnostic streams that fire once per run rather
64
+ # than per analyzed file: plugin load / prepare envelopes,
65
+ # the ADR-10 dependency-source resolution surface, and the
66
+ # `expand_paths` errors for `paths:` entries that don't
67
+ # exist or aren't `.rb`. Aggregated here so `#run` stays
68
+ # under the ABC budget.
69
+ #
70
+ # ADR-15 Phase 4b — `plugin_prepare_diagnostics` runs on
71
+ # the coordinator's plugin registry under sequential mode;
72
+ # under pool mode each worker re-runs `prepare` against
73
+ # its own plugin instances, so the pool path drains the
74
+ # first worker's prepare-diagnostic snapshot into the
75
+ # aggregated diagnostic stream instead (see
76
+ # {#analyze_files_in_pool}). Skipping the coordinator
77
+ # prepare in pool mode avoids double-running `#prepare`
78
+ # against the coordinator-side plugin instances (which
79
+ # the pool path never consults for per-file analysis).
80
+ def pre_file_diagnostics(expansion)
81
+ # ADR-18 slice 3 — prepare diagnostics are captured
82
+ # earlier in #run (before the synthetic-method scanner)
83
+ # so cross-plugin facts are available to the scanner.
84
+ # We re-surface the captured diagnostics here so the
85
+ # existing pre_file_diagnostics ordering is preserved.
86
+ prepare = pool_mode? ? [] : cached_plugin_prepare_diagnostics
87
+ plugin_load_diagnostics +
88
+ prepare +
89
+ pre_eval_diagnostics +
90
+ dependency_source_diagnostics +
91
+ dependency_source_budget_diagnostics +
92
+ dependency_source_config_conflict_diagnostics +
93
+ rbs_coverage_diagnostics +
94
+ expansion.fetch(:errors)
95
+ end
96
+
97
+ # ADR-17 slice 1 — surface a `:error` diagnostic for each
98
+ # `pre_eval:` entry whose resolved path doesn't exist on
99
+ # disk. Loud failure mode (`:error`, not `:warning`):
100
+ # a missing pre_eval path is a configuration mistake the
101
+ # user must fix before analysis is meaningful.
102
+ #
103
+ # Slice 2 adds the `:warning` `pre-eval.parse-error`
104
+ # stream from the pre-pass scanner — accumulated as
105
+ # `@pre_eval_diagnostics_from_scanner` during {#run} and
106
+ # merged here so both diagnostics flow through the same
107
+ # severity / ordering pipeline.
108
+ def pre_eval_diagnostics
109
+ not_found = @configuration.pre_eval.filter_map do |path|
110
+ next if File.file?(path)
111
+
112
+ Diagnostic.new(
113
+ path: ".rigor.yml", line: 1, column: 1,
114
+ message: "pre_eval entry not found: #{path.inspect}. " \
115
+ "Pre-evaluation requires the file to exist on disk; remove the entry " \
116
+ "or create the file before re-running analysis.",
117
+ severity: :error,
118
+ rule: "pre-eval.file-not-found",
119
+ source_family: :builtin
120
+ )
121
+ end
122
+ not_found + Array(pre_eval_diagnostics_from_scanner).map { |hash| diagnostic_from_hash(hash) }
123
+ end
124
+
125
+ def diagnostic_from_hash(hash)
126
+ Diagnostic.new(
127
+ path: hash.fetch(:path), line: hash.fetch(:line), column: hash.fetch(:column),
128
+ message: hash.fetch(:message), severity: hash.fetch(:severity),
129
+ rule: hash.fetch(:rule), source_family: :builtin
130
+ )
131
+ end
132
+
133
+ def plugin_load_diagnostics
134
+ plugin_registry.load_errors.map do |error|
135
+ Diagnostic.new(
136
+ path: ".rigor.yml",
137
+ line: 1,
138
+ column: 1,
139
+ message: error.message,
140
+ severity: :error,
141
+ rule: "load-error",
142
+ source_family: :plugin_loader
143
+ )
144
+ end
145
+ end
146
+
147
+ # ADR-10 § "Diagnostic prefix family" — surfaces gems
148
+ # listed in `dependencies.source_inference` that RubyGems
149
+ # could not resolve. The run continues; the gem simply
150
+ # contributes nothing this session, mirroring the
151
+ # plugin-load error envelope. Authored `:warning` because
152
+ # an unresolvable gem usually means a typo or a missing
153
+ # `bundle install` rather than a project-blocking problem;
154
+ # the severity profile still re-stamps it.
155
+ def dependency_source_diagnostics
156
+ dependency_source_index.unresolvable.map do |entry|
157
+ Diagnostic.new(
158
+ path: ".rigor.yml",
159
+ line: 1,
160
+ column: 1,
161
+ message: "dependencies.source_inference[].gem #{entry.gem_name.inspect} could not be " \
162
+ "resolved (#{entry.reason}); skipping",
163
+ severity: :warning,
164
+ rule: "dynamic.dependency-source.gem-not-found",
165
+ source_family: :builtin
166
+ )
167
+ end
168
+ end
169
+
170
+ # ADR-10 § "Budget interaction" / slice 4 — emits one
171
+ # `:warning` per gem whose Walker run hit the
172
+ # `dependencies.budget_per_gem` cap. The cap is a Walker-
173
+ # side guard rail (slice 4 picks the (α) semantics from
174
+ # ADR-10 WD4: harvesting stops, the dispatcher behaves
175
+ # exactly as before for unrecorded methods). The
176
+ # diagnostic names the gem and points the user at the
177
+ # three remediations: ship RBS, reduce `mode:` from
178
+ # `full` to `when_missing`, or de-list the gem.
179
+ # ADR-10 § "config-conflict diagnostic" / 5d — surfaces
180
+ # `Configuration::Dependencies` warnings accumulated
181
+ # during `from_h` deduplication of the `includes:`-chain
182
+ # source_inference array. Each warning describes a
183
+ # per-gem mode conflict that the merge resolved
184
+ # right-wins; the user sees one diagnostic per conflict.
185
+ # `:warning` matches the user's "warn but don't block"
186
+ # preference per the design discussion.
187
+ def dependency_source_config_conflict_diagnostics
188
+ @configuration.dependencies.warnings.map do |message|
189
+ Diagnostic.new(
190
+ path: ".rigor.yml",
191
+ line: 1,
192
+ column: 1,
193
+ message: message,
194
+ severity: :warning,
195
+ rule: "dynamic.dependency-source.config-conflict",
196
+ source_family: :builtin
197
+ )
198
+ end
199
+ end
200
+
201
+ def dependency_source_budget_diagnostics
202
+ budget = @configuration.dependencies.budget_per_gem
203
+ dependency_source_index.budget_exceeded.map do |gem_name|
204
+ Diagnostic.new(
205
+ path: ".rigor.yml",
206
+ line: 1,
207
+ column: 1,
208
+ message: "dependencies.source_inference[].gem #{gem_name.inspect} exceeded the per-gem " \
209
+ "catalog cap (#{budget} method definitions); the remaining methods fall back " \
210
+ "to the existing RBS-or-Dynamic[top] boundary. Ship RBS for the gem, set " \
211
+ "`mode: when_missing` instead of `full`, or de-list the gem.",
212
+ severity: :warning,
213
+ rule: "dynamic.dependency-source.budget-exceeded",
214
+ source_family: :builtin
215
+ )
216
+ end
217
+ end
218
+
219
+ # O4 Layer 3 slice 3 — graceful-degradation coverage
220
+ # report. When the project has a `Gemfile.lock` (slice 1)
221
+ # and one or more locked gems are not covered by ANY of
222
+ # the four RBS resolution paths (`DEFAULT_LIBRARIES`,
223
+ # `data/vendored_gem_sigs/`, slice-1 bundle-shipped
224
+ # `sig/`, slice-2 `rbs_collection.lock.yaml`), emit a
225
+ # single `:info` diagnostic summarising the uncovered set
226
+ # so the user can act on it (run `rbs collection install`,
227
+ # opt the gem into `dependencies.source_inference:`, or
228
+ # accept the `Dynamic[T]` fallback).
229
+ #
230
+ # Suppressed when the lockfile is empty, when every gem
231
+ # is covered, or when slice 1's `bundler.lockfile`
232
+ # discovery returned nothing (no lockfile to read).
233
+ def rbs_coverage_diagnostics
234
+ locked = Environment::LockfileResolver.locked_gems(
235
+ lockfile_path: @configuration.bundler_lockfile,
236
+ project_root: Dir.pwd,
237
+ auto_detect: @configuration.bundler_auto_detect
238
+ )
239
+ return [] if locked.empty?
240
+
241
+ bundle_sig_paths = Environment::BundleSigDiscovery.discover(
242
+ bundle_path: @configuration.bundler_bundle_path,
243
+ project_root: Dir.pwd,
244
+ auto_detect: @configuration.bundler_auto_detect,
245
+ locked_gems: locked
246
+ )
247
+ collection_paths = Environment::RbsCollectionDiscovery.discover(
248
+ lockfile_path: @configuration.rbs_collection_lockfile,
249
+ project_root: Dir.pwd,
250
+ auto_detect: @configuration.rbs_collection_auto_detect
251
+ )
252
+ rows = Environment::RbsCoverageReport.classify(
253
+ locked_gems: locked,
254
+ default_libraries: Environment::DEFAULT_LIBRARIES,
255
+ bundle_sig_paths: bundle_sig_paths,
256
+ rbs_collection_paths: collection_paths
257
+ )
258
+ missing = Environment::RbsCoverageReport.missing(rows)
259
+ return [] if missing.empty?
260
+
261
+ [build_rbs_coverage_missing_diagnostic(missing)]
262
+ end
263
+
264
+ # Robustness uplift companion (ADR-5) — when the project's
265
+ # `signature_paths:` RBS declared qualified names without their
266
+ # enclosing namespace, `RbsLoader` synthesizes the missing
267
+ # `module`s so the otherwise-inert signatures resolve. Surface a
268
+ # single `:info` diagnostic naming them so the user knows their
269
+ # sig set is malformed (`rbs validate` rejects it) and can fix it
270
+ # at the source. Authored `:info`: the analysis already succeeded;
271
+ # this is advisory, never a gate. Empty for a well-formed sig set.
272
+ def rbs_synthesized_namespace_diagnostics
273
+ synthesized = synthesized_namespaces_snapshot
274
+ return [] if synthesized.nil? || synthesized.empty?
275
+
276
+ [build_rbs_synthesized_namespace_diagnostic(synthesized)]
277
+ end
278
+
279
+ # Maps the per-run `rigor:v1:conforms-to` scan results into
280
+ # diagnostics (spec: `rbs-extended.md` § "Explicit conformance
281
+ # directive"). A class that declares `conforms-to _Interface`
282
+ # but is missing a required interface method surfaces as
283
+ # `rbs_extended.unsatisfied-conformance`; an unresolvable
284
+ # interface name surfaces as `dynamic.rbs-extended.unresolved`
285
+ # `:info` (the same fail-soft channel the other directive
286
+ # parsers use). Empty for a project with no directive, a
287
+ # well-formed conformance, or a non-sequential pool run (the
288
+ # snapshot mirrors `synthesized_namespaces`).
289
+ def conforms_to_diagnostics
290
+ results = conformance_results_snapshot
291
+ return [] if results.nil? || results.empty?
292
+
293
+ results.map { |record| build_conformance_diagnostic(record) }
294
+ end
295
+
296
+ def build_conformance_diagnostic(record)
297
+ case record
298
+ when RbsExtended::ConformanceChecker::Unsatisfied
299
+ build_unsatisfied_conformance_diagnostic(record)
300
+ when RbsExtended::ConformanceChecker::IncompatibleSignature
301
+ build_incompatible_signature_diagnostic(record)
302
+ else # UnresolvedInterface
303
+ build_reporter_diagnostic(
304
+ record.location,
305
+ rule: "dynamic.rbs-extended.unresolved",
306
+ message: "`#{record.class_name}` declares `conforms-to #{record.interface_name}` but " \
307
+ "interface `#{record.interface_name}` is not loaded. Check for a typo or add " \
308
+ "the `sig`/library that declares it to the RBS load path."
309
+ )
310
+ end
311
+ end
312
+
313
+ def build_unsatisfied_conformance_diagnostic(record)
314
+ path, line, column = location_fields(record.location)
315
+ Diagnostic.new(
316
+ path: path, line: line, column: column,
317
+ message: "`#{record.class_name}` declares `conforms-to #{record.interface_name}` " \
318
+ "but does not provide #{pluralize_methods(record.missing_methods)}: " \
319
+ "#{record.missing_methods.map { |m| "`##{m}`" }.join(', ')}. Implement the " \
320
+ "missing method(s) or remove the directive.",
321
+ severity: :warning,
322
+ rule: "rbs_extended.unsatisfied-conformance",
323
+ source_family: :builtin
324
+ )
325
+ end
326
+
327
+ def build_incompatible_signature_diagnostic(record)
328
+ path, line, column = location_fields(record.location)
329
+ Diagnostic.new(
330
+ path: path, line: line, column: column,
331
+ message: "`#{record.class_name}##{record.method_name}` does not satisfy " \
332
+ "`conforms-to #{record.interface_name}`: #{record.detail}. Adjust the " \
333
+ "signature to a subtype of the interface contract.",
334
+ severity: :warning,
335
+ rule: "rbs_extended.unsatisfied-conformance",
336
+ source_family: :builtin,
337
+ method_name: record.method_name
338
+ )
339
+ end
340
+
341
+ def pluralize_methods(methods)
342
+ methods.size == 1 ? "required method" : "#{methods.size} required methods"
343
+ end
344
+
345
+ def build_rbs_synthesized_namespace_diagnostic(synthesized)
346
+ sample_size = 5
347
+ sample = synthesized.first(sample_size)
348
+ suffix = synthesized.size > sample_size ? ", and #{synthesized.size - sample_size} more" : ""
349
+ Diagnostic.new(
350
+ path: ".rigor.yml",
351
+ line: 1,
352
+ column: 1,
353
+ message: "#{synthesized.size} RBS namespace(s) under `signature_paths:` are " \
354
+ "referenced by qualified declarations (e.g. `class Foo::Bar`) but never " \
355
+ "declared: #{sample.join(', ')}#{suffix}. `rbs validate` rejects this; " \
356
+ "Rigor synthesized the missing `module`(s) so the signatures still " \
357
+ "resolve. Declare each (`module <name>` / `class <name>`) in your RBS to " \
358
+ "make the sig set valid upstream.",
359
+ severity: :info,
360
+ rule: "rbs.coverage.synthesized-namespace",
361
+ source_family: :builtin
362
+ )
363
+ end
364
+
365
+ def build_rbs_coverage_missing_diagnostic(missing)
366
+ sample_size = 5
367
+ sample = missing.first(sample_size).map(&:gem_name)
368
+ suffix = missing.size > sample_size ? ", and #{missing.size - sample_size} more" : ""
369
+ Diagnostic.new(
370
+ path: ".rigor.yml",
371
+ line: 1,
372
+ column: 1,
373
+ message: "#{missing.size} gem(s) in Gemfile.lock have no RBS available: " \
374
+ "#{sample.join(', ')}#{suffix}. " \
375
+ "Consider `rbs collection install` to fetch community RBS from " \
376
+ "`ruby/gem_rbs_collection`, ship `sig/` in the gem itself, or " \
377
+ "opt the gem into `dependencies.source_inference:` in `.rigor.yml`.",
378
+ severity: :info,
379
+ rule: "rbs.coverage.missing-gem",
380
+ source_family: :builtin
381
+ )
382
+ end
383
+
384
+ # ADR-13 slice 3b — drains the per-run
385
+ # {RbsExtended::Reporter} into one diagnostic per accumulated
386
+ # event:
387
+ #
388
+ # - `dynamic.rbs-extended.unresolved` for every annotation
389
+ # payload the parser could not turn into a {Rigor::Type}.
390
+ # Surfaces typos and references to plugin-supplied names
391
+ # the project did not enable.
392
+ # - `dynamic.shape.lossy-projection` for every shape-projection
393
+ # type function (`pick_of`, …) applied to a carrier that
394
+ # loses precision (anything other than `HashShape` / `Tuple`).
395
+ #
396
+ # Both are authored `:info`; the severity profile re-stamps
397
+ # them per project taste. Path / line / column come from the
398
+ # annotation's `RBS::Location` when available, falling back
399
+ # to `.rigor.yml`-style file-level attribution otherwise.
400
+ def rbs_extended_reporter_diagnostics
401
+ return [] if @rbs_extended_reporter.empty?
402
+
403
+ unresolved = @rbs_extended_reporter.unresolved_payloads.map do |entry|
404
+ build_reporter_diagnostic(
405
+ entry.source_location,
406
+ rule: "dynamic.rbs-extended.unresolved",
407
+ message: "`RBS::Extended` directive payload could not be resolved: " \
408
+ "#{entry.payload.inspect}. Check for typos or enable a plugin " \
409
+ "that contributes the referenced type vocabulary."
410
+ )
411
+ end
412
+
413
+ lossy = @rbs_extended_reporter.lossy_projections.map do |entry|
414
+ build_reporter_diagnostic(
415
+ entry.source_location,
416
+ rule: "dynamic.shape.lossy-projection",
417
+ message: "Shape projection `#{entry.head}` applied to a carrier without a " \
418
+ "literal shape; the projection degrades to the input type. Author " \
419
+ "a `HashShape` / `Tuple` carrier or accept the unchanged result."
420
+ )
421
+ end
422
+
423
+ unresolved + lossy
424
+ end
425
+
426
+ # ADR-10 slice 5c — drains the per-run
427
+ # {DependencySourceInference::BoundaryCrossReporter} into
428
+ # `dynamic.dependency-source.boundary-cross` `:info`
429
+ # diagnostics. Each event flags a call site where RBS
430
+ # dispatch produced a concrete answer AND a `mode: :full`
431
+ # opt-in gem's source catalog ALSO contains an entry for
432
+ # the same `(class_name, method_name)` — i.e., both
433
+ # contracts have an opinion. RBS still wins on the
434
+ # dispatch result; the diagnostic is purely advisory so
435
+ # the user can verify the two contracts haven't drifted.
436
+ #
437
+ # Severity profile re-stamps the rule per project taste.
438
+ # The diagnostic carries no `path` / `line` / `column`
439
+ # because the crossing is per-method-per-gem, not
440
+ # per-call-site — the diagnostic anchors at `.rigor.yml`
441
+ # like the other `dependency-source.*` diagnostics that
442
+ # report on opt-in configuration.
443
+ # ADR-32 WD6 — drains the per-run
444
+ # {Plugin::SourceRbsSynthesisReporter} into
445
+ # `source-rbs-synthesis-failed` `:info` diagnostics. Each
446
+ # entry names the plugin that owns the synthesizer, the
447
+ # source file the rbs-inline parser couldn't process, and
448
+ # the upstream error message. The synthesizer-emitting
449
+ # plugin (currently only `rigor-rbs-inline`) treats a
450
+ # parse failure as a no-contribution event so analysis
451
+ # continues; this stream surfaces the failure so the user
452
+ # can see which files contributed nothing and why.
453
+ #
454
+ # Severity profile re-stamps the rule per project taste.
455
+ def source_rbs_synthesis_diagnostics
456
+ return [] if @source_rbs_synthesis_reporter.empty?
457
+
458
+ @source_rbs_synthesis_reporter.entries.map do |entry|
459
+ Diagnostic.new(
460
+ path: entry.path, line: 1, column: 1,
461
+ message: "plugin `#{entry.plugin_id}` failed to synthesise RBS from this file: " \
462
+ "#{entry.message}. The file's analysis falls back to no inline-RBS " \
463
+ "contribution. Fix the inline-RBS comment grammar or remove the " \
464
+ "annotation to silence this diagnostic.",
465
+ severity: :info,
466
+ rule: "source-rbs-synthesis-failed",
467
+ source_family: :builtin
468
+ )
469
+ end
470
+ end
471
+
472
+ def boundary_cross_diagnostics
473
+ return [] if @boundary_cross_reporter.empty?
474
+
475
+ @boundary_cross_reporter.entries.map do |entry|
476
+ Diagnostic.new(
477
+ path: ".rigor.yml", line: 1, column: 1,
478
+ message: "`#{entry.class_name}##{entry.method_name}` is contributed by both " \
479
+ "RBS (#{entry.rbs_display}) and the `mode: :full` opt-in gem " \
480
+ "`#{entry.gem_name}`. RBS wins on dispatch; verify the gem source " \
481
+ "has not drifted from its RBS contract.",
482
+ severity: :info,
483
+ rule: "dynamic.dependency-source.boundary-cross",
484
+ source_family: :builtin
485
+ )
486
+ end
487
+ end
488
+
489
+ def build_reporter_diagnostic(source_location, rule:, message:)
490
+ path, line, column = location_fields(source_location)
491
+ Diagnostic.new(
492
+ path: path, line: line, column: column,
493
+ message: message, severity: :info, rule: rule, source_family: :builtin
494
+ )
495
+ end
496
+
497
+ def location_fields(source_location)
498
+ return [".rigor.yml", 1, 1] if source_location.nil?
499
+
500
+ path = location_path(source_location)
501
+ line = source_location.respond_to?(:start_line) ? source_location.start_line : 1
502
+ column = source_location.respond_to?(:start_column) ? source_location.start_column + 1 : 1
503
+ [path, line, column]
504
+ rescue StandardError
505
+ [".rigor.yml", 1, 1]
506
+ end
507
+
508
+ def location_path(source_location)
509
+ buffer = source_location.respond_to?(:buffer) ? source_location.buffer : nil
510
+ return ".rigor.yml" if buffer.nil? || !buffer.respond_to?(:name)
511
+
512
+ name = buffer.name.to_s
513
+ name.empty? ? ".rigor.yml" : name
514
+ end
515
+
516
+ # ADR-8 § "Severity profile" — re-stamps each diagnostic's
517
+ # severity from the configured profile + per-rule
518
+ # overrides. Rules emit with their authored severity; the
519
+ # profile is the final filter. Diagnostics whose resolved
520
+ # severity is `:off` are dropped from the run result.
521
+ def apply_severity_profile(diagnostics)
522
+ diagnostics.filter_map { |diagnostic| stamp_severity(diagnostic) }
523
+ end
524
+
525
+ def stamp_severity(diagnostic)
526
+ return diagnostic if diagnostic.rule.nil?
527
+
528
+ resolved = Configuration::SeverityProfile.resolve(
529
+ rule: diagnostic.rule,
530
+ authored_severity: diagnostic.severity,
531
+ profile: @configuration.severity_profile,
532
+ overrides: @configuration.severity_overrides
533
+ )
534
+ return nil if resolved == :off
535
+ return diagnostic if resolved == diagnostic.severity
536
+
537
+ Diagnostic.new(
538
+ path: diagnostic.path,
539
+ line: diagnostic.line,
540
+ column: diagnostic.column,
541
+ message: diagnostic.message,
542
+ severity: resolved,
543
+ rule: diagnostic.rule,
544
+ source_family: diagnostic.source_family
545
+ )
546
+ end
547
+
548
+ private
549
+
550
+ def plugin_registry
551
+ @plugin_registry_reader.call
552
+ end
553
+
554
+ def dependency_source_index
555
+ @dependency_source_index_reader.call
556
+ end
557
+
558
+ def pool_mode?
559
+ @pool_mode_reader.call
560
+ end
561
+
562
+ def cached_plugin_prepare_diagnostics
563
+ @cached_plugin_prepare_diagnostics_reader.call
564
+ end
565
+
566
+ def pre_eval_diagnostics_from_scanner
567
+ @pre_eval_diagnostics_from_scanner_reader.call
568
+ end
569
+
570
+ def synthesized_namespaces_snapshot
571
+ @synthesized_namespaces_snapshot_reader.call
572
+ end
573
+
574
+ def conformance_results_snapshot
575
+ @conformance_results_snapshot_reader.call
576
+ end
577
+ end
578
+ end
579
+ end
580
+ end