rigortype 0.3.9 → 0.4.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 (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -63,6 +63,7 @@ module Rigor
63
63
  ["oauth_authorized_application_url", 1, "/oauth/authorized_applications/:id", :delete, :destroy]
64
64
  ]
65
65
  }.freeze
66
+ Ractor.make_shareable(CONTROLLER_HELPERS)
66
67
 
67
68
  module_function
68
69
 
@@ -192,18 +192,12 @@ module Rigor
192
192
  publish_reachability_roots(services, table)
193
193
  end
194
194
 
195
- # File-level only: the once-per-run load-error emission. Per-call helper validation runs over the
196
- # engine-owned walk via the node_rule below (ADR-37), with the same-file `shadowing` set built once
197
- # as the node-rule file context.
195
+ # File-level only: the run-scoped load-error disclosure (#1051). Per-call helper validation runs over
196
+ # the engine-owned walk via the node_rule below (ADR-37), with the same-file `shadowing` set built
197
+ # once as the node-rule file context.
198
198
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
199
199
  table = helper_table_or_nil
200
- if table.nil? && @load_error
201
- return [] if @load_error_emitted
202
-
203
- @load_error_emitted = true
204
- return [load_error_diagnostic(path)]
205
- end
206
-
200
+ disclose_load_error if table.nil? && @load_error
207
201
  []
208
202
  end
209
203
 
@@ -265,13 +259,11 @@ module Rigor
265
259
  nil
266
260
  end
267
261
 
268
- def load_error_diagnostic(path)
269
- Rigor::Analysis::Diagnostic.new(
270
- path: path, line: 1, column: 1,
271
- message: @load_error,
272
- severity: :warning,
273
- rule: "load-error"
274
- )
262
+ # #1051 — "the routes file did not load" is a fact about the project, not about the file that
263
+ # happened to be analysed first by this instance, so it goes through the run-scoped channel and the
264
+ # engine positions it at `.rigor.yml:1:1`, once per run rather than once per fork-pool worker.
265
+ def disclose_load_error
266
+ disclose_once(:routes_load_failed, message: @load_error, severity: :warning, rule: "load-error")
275
267
  end
276
268
  end
277
269
 
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Rigor
6
+ module Plugin
7
+ class RbsInline < Base
8
+ # Issue #998 — the two same-line `%a{}` spellings rbs's built-in `RBS::InlineParser` and Steep's inline
9
+ # mode accept, repaired into the shape the `rbs-inline` gem's writer honours:
10
+ #
11
+ # - `# @rbs %a{…} () -> String` — the gem's `@rbs %a{}` rule lexes the annotations and stops, so the
12
+ # method type after them is discarded and the method renders `() -> untyped`;
13
+ # - `#: %a{…} () -> String` — the gem hands the whole payload to `RBS::Parser.parse_method_type`, which
14
+ # has no annotation prefix, so the line becomes a `SyntaxErrorAssertion` and nothing of it survives.
15
+ #
16
+ # Both were silent (ADR-32 WD12), and the second reached users as #997's "did not parse … DROPPED"
17
+ # notice on input that is valid RBS for the other two readers.
18
+ #
19
+ # This is a post-parse repair of the gem's annotation list, not a rewrite of the Ruby source. A source
20
+ # rewrite would have to put the annotation and the method type on two comment lines — the gem has no
21
+ # one-line spelling for the pair, which is the defect — and inserting a line moves every later line,
22
+ # including the ones a `#:` notice reports and the one a `def` uses to find its comment block.
23
+ #
24
+ # The repair is exact rather than heuristic because every step is the gem's own reading:
25
+ #
26
+ # - the leading `%a{…}` spellings are delimited by the gem's own `Tokenizer`, the lexer that already
27
+ # accepts them in the own-line form, so what counts as an annotation cannot drift from upstream;
28
+ # - each half is re-read by the gem's own `AnnotationParser` as the comment the author would have written
29
+ # in the own-line form (`# @rbs %a{…}`, then `# @rbs () -> String` or `#: () -> String`), on the same
30
+ # line and column, so the replacement annotations are indistinguishable from ones upstream built;
31
+ # - the split lands only when the remainder re-reads as a well-formed method type. Anything else keeps
32
+ # the gem's original annotation untouched, so a genuinely malformed line still reaches the WD12
33
+ # notices, and a split can never trade one silent loss for another.
34
+ module SameLineAnnotations
35
+ module_function
36
+
37
+ # The `@rbs` marker as the gem's `annotation_comment?` detects it, with the whitespace before it.
38
+ RBS_MARKER = /\A\s*@rbs\b/
39
+
40
+ # Replaces, in `parsing_result.annotations`, every same-line form that splits cleanly with the
41
+ # annotation and the method type it spells. Idempotent: a repaired list has no same-line form left.
42
+ def split!(parsing_result)
43
+ return parsing_result unless parsing_result.is_a?(::RBS::Inline::AnnotationParser::ParsingResult)
44
+
45
+ parsing_result.annotations.replace(parsing_result.annotations.flat_map { |annotation| expand(annotation) })
46
+ parsing_result
47
+ end
48
+
49
+ # Every `ParsingResult` a parsed declaration tree holds, which are the objects the writer reads.
50
+ def each_parsing_result(nodes, &)
51
+ nodes.each do |node|
52
+ comments = node.respond_to?(:comments) ? node.comments : nil
53
+ yield comments if comments.is_a?(::RBS::Inline::AnnotationParser::ParsingResult)
54
+ each_parsing_result(node.members, &) if node.respond_to?(:members)
55
+ end
56
+ end
57
+
58
+ # The text an `@rbs %a{…}` line carries after its annotations that the gem discarded — `nil` when
59
+ # there is none. Call it on an annotation {.split!} has already seen: a remainder that survives the
60
+ # split is one that did not read as a method type.
61
+ def dropped_remainder(annotation)
62
+ return nil unless annotation.is_a?(::RBS::Inline::AST::Annotations::RBSAnnotation)
63
+
64
+ parts = same_line_parts(annotation)
65
+ parts && parts[2].strip
66
+ end
67
+
68
+ def expand(annotation)
69
+ parts = same_line_parts(annotation)
70
+ return [annotation] if parts.nil?
71
+
72
+ prefix, spellings, rest = parts
73
+ method_type = reparse(annotation, "##{prefix}#{rest}")
74
+ return [annotation] unless well_formed_method_type?(method_type)
75
+
76
+ # Re-read even when `annotation` already is the `@rbs %a{}` carrier: the original still spans the
77
+ # method type in its source, and {.dropped_remainder} reads exactly that to report a failed split.
78
+ carrier = reparse(annotation, "# @rbs #{spellings.join(' ')}")
79
+ return [annotation] unless carrier.is_a?(::RBS::Inline::AST::Annotations::RBSAnnotation)
80
+
81
+ [carrier, method_type]
82
+ end
83
+
84
+ # `[prefix, spellings, rest]` for an annotation whose payload opens with one or more `%a{…}` and
85
+ # continues with something else, or `nil`. `prefix` is the marker as written (`" @rbs"` or `":"`),
86
+ # so the re-read keeps the gem's continuation offset for a multi-line comment block.
87
+ def same_line_parts(annotation)
88
+ # `annotations` also holds the `CommentLines` of plain prose, which has no `source` to read.
89
+ prefix =
90
+ case annotation
91
+ when ::RBS::Inline::AST::Annotations::RBSAnnotation
92
+ annotation.source.string[RBS_MARKER]
93
+ when ::RBS::Inline::AST::Annotations::SyntaxErrorAssertion
94
+ ":" if annotation.source.string.start_with?(":")
95
+ end
96
+ return nil if prefix.nil?
97
+
98
+ spellings, rest = leading_annotations(annotation.source.string.delete_prefix(prefix))
99
+ return nil if spellings.empty? || rest.strip.empty?
100
+
101
+ [prefix, spellings, rest]
102
+ end
103
+
104
+ # The `%a{…}` tokens the gem's tokenizer reads at the head of `body`, and the unread text after them.
105
+ def leading_annotations(body)
106
+ tokenizer = ::RBS::Inline::AnnotationParser::Tokenizer.new(StringScanner.new(body))
107
+ trivia = ::RBS::Inline::AST::Tree.new(:rigor_same_line_annotations)
108
+ tokenizer.advance(trivia)
109
+ tokenizer.advance(trivia)
110
+
111
+ spellings = []
112
+ while tokenizer.type?(::RBS::Inline::AnnotationParser::Tokens::T_ANNOTATION)
113
+ spellings << tokenizer.lookahead1[1]
114
+ tokenizer.advance(trivia)
115
+ end
116
+ [spellings, tokenizer.rest]
117
+ end
118
+
119
+ # Reads `comment_text` through the gem's `AnnotationParser` as if it stood where `annotation`'s comment
120
+ # block does — same first line, same column, continuation lines re-prefixed with the `#` that
121
+ # `CommentLines#string` strips — and returns the one annotation it yields, or `nil`.
122
+ def reparse(annotation, comment_text)
123
+ location = annotation.source.comments.first&.location
124
+ return nil if location.nil?
125
+
126
+ indent = " " * location.start_column
127
+ padding = "\n" * (location.start_line - 1)
128
+ source = "#{padding}#{indent}#{comment_text.gsub("\n") { "\n#{indent}#" }}\n"
129
+ results = ::RBS::Inline::AnnotationParser.parse(::Prism.parse(source).comments)
130
+ return nil unless results.size == 1
131
+
132
+ annotations = results.first.annotations
133
+ annotations.size == 1 && annotations.first.is_a?(::RBS::Inline::AST::Annotations::Base) ? annotations.first : nil
134
+ end
135
+
136
+ def well_formed_method_type?(annotation)
137
+ case annotation
138
+ when ::RBS::Inline::AST::Annotations::MethodTypeAssertion
139
+ true
140
+ when ::RBS::Inline::AST::Annotations::Method
141
+ annotation.method_types.is_a?(Array) && annotation.error_source.nil?
142
+ else
143
+ false
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rigor/plugin"
4
+ require_relative "rbs_inline/same_line_annotations"
4
5
 
5
6
  # ADR-32 — bundled `rigor-rbs-inline` plugin.
6
7
  #
@@ -68,6 +69,18 @@ module Rigor
68
69
  # instead of adopting the placeholder. `Rigor::RbsExtended.inferred_return?` is the reader.
69
70
  INFERRED_RETURN_ANNOTATION = "rigor:v1:inferred-return"
70
71
 
72
+ # The RBS annotation the synthesizer writes on every member where upstream defaulted EVERY type
73
+ # slot — every parameter and the return alike — because the author's comment block asserted nothing
74
+ # about this member's own contract (issue #991). This is a strictly narrower condition than
75
+ # {INFERRED_RETURN_ANNOTATION}: that one fires whenever the return alone defaulted, which is also
76
+ # true of a member whose parameters WERE annotated (`# @rbs num: Float` with no `# @rbs return:`).
77
+ # This annotation distinguishes "nobody said anything about this member" from "the return half was
78
+ # left to inference" — the two collapse under return-only provenance, because no equivalent
79
+ # per-parameter survivor exists to separate them (see {DEFAULTED_TYPE_NAME}), so this constant
80
+ # exists precisely to carry that missing half. Its meaning is normative in
81
+ # `docs/type-specification/rbs-extended.md`. `Rigor::RbsExtended.inferred_signature?` is the reader.
82
+ INFERRED_SIGNATURE_ANNOTATION = "rigor:v1:inferred-signature"
83
+
71
84
  # The stand-in type handed to upstream's `Writer#default_type`, and the whole reason this plugin can
72
85
  # tell a defaulted type from an authored one. `RBS::Inline::Writer` substitutes `default_type`
73
86
  # wherever the author wrote nothing — `return_type || default_type` in `RubyDef#method_overloads`,
@@ -119,6 +132,9 @@ module Rigor
119
132
  return nil if parsed.nil?
120
133
 
121
134
  uses, decls, rbs_decls = parsed
135
+ # Issue #998 — before the writer reads the annotation lists, so a same-line `%a{…} () -> T` renders
136
+ # with its method type and its annotation both attached. See {SameLineAnnotations}.
137
+ SameLineAnnotations.each_parsing_result(decls) { |comments| SameLineAnnotations.split!(comments) }
122
138
  rendered = mark_inferred_returns(render_with_defaulted_marker(uses, decls, rbs_decls))
123
139
  rendered = reattach_declaration_annotations(rendered)
124
140
  return nil if rendered.nil? || rendered.strip.empty?
@@ -162,6 +178,15 @@ module Rigor
162
178
  # about the return. The engine reads the annotation in `RbsDispatch` and falls through to the same
163
179
  # body-inference tier an undeclared method takes.
164
180
  #
181
+ # A member additionally carries {INFERRED_SIGNATURE_ANNOTATION} when EVERY type slot on it —
182
+ # not only the return — defaulted: a fully bare `def`, or one whose only annotation lives on a
183
+ # sibling member the file also happens to carry. A member with a parameter annotated (`# @rbs num:
184
+ # Float`) but no `# @rbs return:` still gets {INFERRED_RETURN_ANNOTATION} (the return did default),
185
+ # but NOT this one — the author asserted something about the member, just not its return. Issue
186
+ # #991's `call.wrong-arity` reads this one rather than the return-only annotation, because the
187
+ # arity question is about the parameter list, and the parameter list is exactly what tells the two
188
+ # cases apart.
189
+ #
165
190
  # Two properties make the rewrite safe rather than clever:
166
191
  #
167
192
  # - the stand-in is a token this plugin injected, so replacing it is exact — it can only appear
@@ -181,53 +206,85 @@ module Rigor
181
206
  def mark_inferred_returns(rendered)
182
207
  return rendered if rendered.nil? || !rendered.b.include?(DEFAULTED_TYPE_NAME.to_s)
183
208
 
184
- marked_lines = rendered.valid_encoding? ? defaulted_member_lines(rendered) : Set.new
209
+ return_lines, signature_lines =
210
+ rendered.valid_encoding? ? defaulted_member_lines(rendered) : [Set.new, Set.new]
185
211
  scrubbed = rendered.b.gsub(DEFAULTED_TYPE_NAME.to_s, "untyped").force_encoding(rendered.encoding)
186
- return scrubbed if marked_lines.empty?
212
+ return scrubbed if return_lines.empty?
187
213
 
188
214
  scrubbed.lines.each_with_index.flat_map do |line, index|
189
- next line unless marked_lines.include?(index + 1)
215
+ line_number = index + 1
216
+ next line unless return_lines.include?(line_number)
190
217
 
191
- ["#{line[/\A[ \t]*/]}%a{#{INFERRED_RETURN_ANNOTATION}}\n", line]
218
+ indent = line[/\A[ \t]*/]
219
+ annotations = ["#{indent}%a{#{INFERRED_RETURN_ANNOTATION}}\n"]
220
+ annotations << "#{indent}%a{#{INFERRED_SIGNATURE_ANNOTATION}}\n" if signature_lines.include?(line_number)
221
+ [*annotations, line]
192
222
  end.join
193
223
  end
194
224
 
195
- # The 1-based line of every member in `rendered` whose return type (or, for an attribute, whose
196
- # type) is the defaulted stand-in. A member's `location.start_line` is the line its `def` /
197
- # `attr_*` keyword sits on, which is where the annotation has to go.
225
+ # The 1-based lines of every member in `rendered` whose return type (or, for an attribute, whose
226
+ # type) is the defaulted stand-in — and, of those, the subset where EVERY type slot on the member
227
+ # defaulted, not only the return. A member's `location.start_line` is the line its `def` /
228
+ # `attr_*` keyword sits on, which is where the annotation has to go. Returns `[return_lines,
229
+ # signature_lines]`; `signature_lines` is always a subset of `return_lines`, since a member cannot
230
+ # have every slot defaulted without its return being one of them.
198
231
  def defaulted_member_lines(rendered)
199
232
  buffer = ::RBS::Buffer.new(name: "(rigor: inline defaulted-type probe)", content: rendered)
200
233
  _, _directives, decls = ::RBS::Parser.parse_signature(buffer)
201
- collect_defaulted_member_lines(decls, Set.new)
234
+ collect_defaulted_member_lines(decls, Set.new, Set.new)
202
235
  rescue ::StandardError
203
236
  # The writer's own output failing to parse is a condition the loader already handles (ADR-32 WD6
204
237
  # fail-soft drops the entry). Declining to mark here keeps that the only failure.
205
- Set.new
238
+ [Set.new, Set.new]
206
239
  end
207
240
 
208
- def collect_defaulted_member_lines(nodes, lines)
241
+ def collect_defaulted_member_lines(nodes, return_lines, signature_lines)
209
242
  nodes.each do |node|
210
243
  next unless node.respond_to?(:members)
211
244
 
212
245
  node.members.each do |member|
213
- line = defaulted_member_line(member)
214
- lines << line if line
246
+ classify_defaulted_member(member, return_lines, signature_lines)
215
247
  end
216
- collect_defaulted_member_lines(node.members.grep(::RBS::AST::Declarations::Base), lines)
248
+ collect_defaulted_member_lines(
249
+ node.members.grep(::RBS::AST::Declarations::Base), return_lines, signature_lines
250
+ )
217
251
  end
218
- lines
252
+ [return_lines, signature_lines]
219
253
  end
220
254
 
221
- def defaulted_member_line(member)
255
+ def classify_defaulted_member(member, return_lines, signature_lines)
222
256
  case member
223
257
  when ::RBS::AST::Members::MethodDefinition
224
- return nil unless member.overloads.any? { |o| defaulted_type?(o.method_type.type.return_type) }
258
+ return unless member.overloads.any? { |o| defaulted_type?(o.method_type.type.return_type) }
259
+
260
+ line = member.location&.start_line
261
+ return if line.nil?
262
+
263
+ return_lines << line
264
+ signature_lines << line if member.overloads.all? { |o| fully_defaulted_overload?(o) }
225
265
  when ::RBS::AST::Members::Attribute
226
- return nil unless defaulted_type?(member.type)
227
- else
228
- return nil
266
+ return unless defaulted_type?(member.type)
267
+
268
+ line = member.location&.start_line
269
+ return if line.nil?
270
+
271
+ # An attribute has exactly one type slot (the value type, shared by its reader and writer
272
+ # overloads), so "the type defaulted" and "every type slot defaulted" are the same fact.
273
+ return_lines << line
274
+ signature_lines << line
229
275
  end
230
- member.location&.start_line
276
+ end
277
+
278
+ # True when every parameter type and the return type of `overload` — everything
279
+ # `RBS::Types::Function#each_type` yields — is the defaulted stand-in, i.e. the author's comment
280
+ # block asserted nothing about this overload's own contract. Block-type provenance is
281
+ # deliberately not part of this: a block parameter's shape does not bear on the positional arity
282
+ # #991 exists to check, and upstream defaults it independently of the parameter list either way.
283
+ def fully_defaulted_overload?(overload)
284
+ function = overload.method_type.type
285
+ return false unless function.respond_to?(:each_type)
286
+
287
+ function.each_type.all? { |type| defaulted_type?(type) }
231
288
  end
232
289
 
233
290
  def defaulted_type?(type)
@@ -300,29 +357,153 @@ module Rigor
300
357
  # These are invisible without a report: synthesis succeeds, and the annotation comment is even echoed
301
358
  # into the generated RBS, so the omission shows up neither in the output nor at runtime.
302
359
  #
303
- # The one case today is `module-self`, where the two inline-RBS dialects disagree on spelling. rbs's
304
- # own `docs/inline.md` documents `# @rbs module-self: Foo`; the rbs-inline gem's grammar is
305
- # `# @rbs module-self Foo`, without the colon. Handed the colon form the gem still builds a
306
- # `ModuleSelf` annotation but extracts no types from it, so an empty `self_types` on a parsed
307
- # annotation is a precise signature for "the author asked for a constraint we did not apply".
308
- # Measured both ways in `docs/notes/20260730-inline-rbs-parser-grammar-diff.md`.
360
+ # Five cases today:
361
+ #
362
+ # - `module-self`, where the two inline-RBS dialects disagree on spelling. rbs's own `docs/inline.md`
363
+ # documents `# @rbs module-self: Foo`; the rbs-inline gem's grammar is `# @rbs module-self Foo`,
364
+ # without the colon. Handed the colon form the gem still builds a `ModuleSelf` annotation but
365
+ # extracts no types from it, so an empty `self_types` on a parsed annotation is a precise signature
366
+ # for "the author asked for a constraint we did not apply". Measured both ways in
367
+ # `docs/notes/20260730-inline-rbs-parser-grammar-diff.md`.
368
+ # - a `#:` line whose type does not parse (issue #997). Upstream's own tolerant parse
369
+ # ({#parse_type_method_type} in its `annotation_parser.rb`) never raises on this: it consumes the
370
+ # rest of the line into a `SyntaxErrorAssertion` and moves on, so nothing upstream ever surfaces
371
+ # the failure. Measured at 568138c2: `#: (finite-float) -> String` above a `def` synthesized no
372
+ # signature for it AT ALL, and `rigor check` on the file reported nothing beyond an unrelated
373
+ # `rbs.coverage.missing-gem` info — the annotation was indistinguishable from one never written,
374
+ # which ADR-93's "an inline annotation is a live contract" forbids.
375
+ # - an `# @rbs %a{…}` line whose text after the annotations is not a method type (issue #998). The
376
+ # gem's `@rbs %a{}` rule lexes the annotations and ignores the rest of the line, so
377
+ # `# @rbs %a{pure} (finite-float) -> String` keeps `%a{pure}` and discards the signature beside it
378
+ # without a word. A remainder that IS a method type is not reported, because
379
+ # {SameLineAnnotations.split!} has already made it bind — the notice is for what that repair
380
+ # could not read.
381
+ # - an `# @rbs name: T` parameter (or local-variable) annotation whose `T` does not parse (issue
382
+ # #1019, row H). The gem's grammar makes the colon-and-type optional — {#parse_var_decl} always
383
+ # builds a `VarType`, even when `parse_type` fell through — so a malformed type leaves `name`
384
+ # present and `type` `nil` rather than raising: `# @rbs n: Integer[1..10]` names `n` and leaves its
385
+ # type unset. `Writer#default_type` then fills the empty slot the same way an UNANNOTATED parameter
386
+ # is filled, so `VarType#type.nil?` is indistinguishable downstream from "the author wrote nothing"
387
+ # without this check — the same silent-drop shape #997 closed for `#:` lines and `return:`, now
388
+ # closed for `name:`.
389
+ # - an `@rbs`-prefixed comment paragraph whose body is not one of the gem's known annotation shapes
390
+ # (issue #1019, row B). {RBS::Inline::AnnotationParser#annotation_comment?} matches a `\b` word
391
+ # boundary right after `@rbs`, so `# @rbs-ext …` is inside its net — the gem treats it as an `@rbs`
392
+ # annotation attempt — but {RBS::Inline::AnnotationParser#parse_annotation}'s `case` has no branch
393
+ # for what follows `-ext`, falls through with no `else`, and returns `nil`. `AnnotationParser#parse`
394
+ # then folds the whole paragraph back into a `CommentLines` — the SAME class an ordinary comment
395
+ # parses to — so nothing distinguishes "the gem tried to read this as `@rbs` and failed" from
396
+ # "this was never meant to be one" except the marker the gem itself keyed on. {SameLineAnnotations::
397
+ # RBS_MARKER} already carries that exact marker (documented there as "the `@rbs` marker as the
398
+ # gem's `annotation_comment?` detects it"), reused here rather than duplicated, so a `CommentLines`
399
+ # paragraph is only reported when the gem's own detector would have called it an `@rbs` attempt.
400
+ #
401
+ # The list is repaired by {SameLineAnnotations.split!} first, exactly as the synthesis path repairs
402
+ # its own, so a valid same-line `#: %a{…} () -> T` is not reported as a `#:` line that failed to
403
+ # parse: it did not fail, it binds.
309
404
  #
310
- # Deliberately narrow. A construct the gem's parser REJECTS already routes through WD6's error path,
311
- # and one it never recognised at all is upstream's grammar to define (WD3) — guessing at those would
312
- # make this a lint on comment prose, which is exactly the false-positive cost ADR-5 ranks first.
405
+ # Deliberately narrow otherwise. A construct the gem's parser REJECTS already routes through WD6's
406
+ # error path, and one it never recognised at all is upstream's grammar to define (WD3) — guessing at
407
+ # those would make this a lint on comment prose, which is exactly the false-positive cost ADR-5 ranks
408
+ # first. `SyntaxErrorAssertion` is neither: the gem's OWN parser recognised the `#:` shape and
409
+ # positively flagged its payload as unparseable, so reporting it is naming a fact rbs-inline already
410
+ # computed, not guessing at one. The row-B check is the same kind of fact, read off the gem's own
411
+ # marker rather than a fact rbs-inline names with a dedicated AST class — nothing else in the grammar
412
+ # collapses a recognised-but-unparseable paragraph into ordinary prose.
313
413
  def unhonoured_annotations(prism_result)
314
414
  ::RBS::Inline::AnnotationParser.parse(prism_result.comments).flat_map do |parsed|
315
- parsed.each_annotation.filter_map do |annotation|
316
- next unless annotation.is_a?(::RBS::Inline::AST::Annotations::ModuleSelf)
317
- next unless annotation.self_types.empty?
318
-
319
- "`@rbs module-self` contributed no self-type constraint. Rigor reads the " \
320
- "`# @rbs module-self Foo` spelling; `# @rbs module-self: Foo` (the spelling in rbs's own " \
321
- "inline documentation) is not honoured here."
415
+ repaired = SameLineAnnotations.split!(parsed)
416
+ unrecognised_tag_notices(repaired) + repaired.each_annotation.filter_map do |annotation|
417
+ case annotation
418
+ when ::RBS::Inline::AST::Annotations::ModuleSelf
419
+ next if annotation.self_types.any?
420
+
421
+ "`@rbs module-self` contributed no self-type constraint. Rigor reads the " \
422
+ "`# @rbs module-self Foo` spelling; `# @rbs module-self: Foo` (the spelling in rbs's own " \
423
+ "inline documentation) is not honoured here."
424
+ when ::RBS::Inline::AST::Annotations::SyntaxErrorAssertion
425
+ syntax_error_assertion_notice(annotation)
426
+ when ::RBS::Inline::AST::Annotations::RBSAnnotation
427
+ dropped_remainder_notice(annotation)
428
+ when ::RBS::Inline::AST::Annotations::VarType
429
+ var_type_notice(annotation)
430
+ end
322
431
  end
323
432
  end.uniq
324
433
  end
325
434
 
435
+ # Issue #997 — the line comes off the annotation's OWN `Prism::Comment` location, not the
436
+ # synthesized RBS buffer: unlike {Environment::RbsLoader#unresolved_type_name_detail}'s position,
437
+ # this one is read straight from the `.rb` file this synthesis call is fresh over (the per-file
438
+ # synthesizer cache is content-keyed, so a changed file simply misses rather than replaying a stale
439
+ # line), so it is the real line the author wrote, not an approximation.
440
+ def syntax_error_assertion_notice(annotation)
441
+ line = annotation.source.comments.first&.location&.start_line
442
+ position = line ? " on line #{line}" : ""
443
+ "a `#:` annotation#{position} did not parse as an RBS method type or type " \
444
+ "(`#{annotation.error_string}`) and was DROPPED — the method types as if the annotation had " \
445
+ "never been written, not merely as if its type were wrong. Fix the RBS syntax to restore it."
446
+ end
447
+
448
+ # Issue #998 — the line is read off the annotation's own comment, as in {#syntax_error_assertion_notice}.
449
+ def dropped_remainder_notice(annotation)
450
+ remainder = SameLineAnnotations.dropped_remainder(annotation)
451
+ return nil if remainder.nil?
452
+
453
+ line = annotation.source.comments.first&.location&.start_line
454
+ position = line ? " on line #{line}" : ""
455
+ "the text after the `%a{…}` annotation#{position} (`#{remainder}`) did not parse as an RBS method " \
456
+ "type and was DROPPED — the annotation is kept, but the method types as if no signature had been " \
457
+ "written beside it. Fix the RBS syntax to restore it."
458
+ end
459
+
460
+ # Issue #1019, row H — a `# @rbs name: T` whose `T` the gem's `parse_type` could not build, read off
461
+ # {RBS::Inline::AST::Annotations::VarType#type} being `nil` rather than off any pattern over the
462
+ # comment text. Silent otherwise: {Writer#default_type} fills the slot exactly as it fills a
463
+ # parameter nobody annotated, so `(untyped n)` does not distinguish "wrote nothing" from "wrote a
464
+ # type RBS could not parse". `raw` is `nil` (and the notice withheld) for the colon-less `# @rbs
465
+ # name` shorthand — the grammar accepts it too, but with no text to name as the dropped type it is a
466
+ # bare mention, not a failed assertion.
467
+ def var_type_notice(annotation)
468
+ return nil if annotation.type
469
+
470
+ raw = annotation.source.string[/#{Regexp.escape(annotation.name.to_s)}\s*:\s*(.+)\z/, 1]&.strip
471
+ return nil if raw.nil? || raw.empty?
472
+
473
+ line = annotation.source.comments.first&.location&.start_line
474
+ position = line ? " on line #{line}" : ""
475
+ "a `# @rbs #{annotation.name}:` annotation#{position} (`#{raw}`) did not parse as an RBS type " \
476
+ "and was DROPPED — the parameter types as `untyped`, not merely as if its type were wrong. If " \
477
+ "`#{raw}` is a Rigor refinement, write it as an `%a{rigor:v1:param: #{annotation.name} is " \
478
+ "#{raw}}` annotation beside the plain RBS type instead — see " \
479
+ "docs/manual/16-rbs-extended-annotations.md — to restore type coverage."
480
+ end
481
+
482
+ # Issue #1019, row B — an `@rbs`-prefixed comment paragraph the gem's OWN detector
483
+ # ({RBS::Inline::AnnotationParser#annotation_comment?}) called an `@rbs` annotation attempt, that
484
+ # `#parse_annotation`'s `case` had no branch for. `# @rbs-ext return: non-empty-string` is the case
485
+ # in point: `annotation_comment?`'s `/\A#(\s*)@rbs(\b|!)/` matches the `\b` word boundary between
486
+ # `s` and `-`, so the gem tries to read it as `@rbs`, fails, and folds it back into a `CommentLines`
487
+ # — the same class a `# rigor: …` or `# @extrbs …` paragraph parses to, which never claimed to be
488
+ # `@rbs` and must stay silent (row X / row C). {SameLineAnnotations::RBS_MARKER} already carries the
489
+ # gem's own marker, so matching it here is reading the SAME fact the gem's detector computed, not
490
+ # inventing a new one — see {#unhonoured_annotations}'s row-B paragraph for why that is not the
491
+ # "regexp lint on comment prose" WD12 otherwise declines to run.
492
+ def unrecognised_tag_notices(parsing_result)
493
+ parsing_result.annotations.grep(::RBS::Inline::AST::CommentLines).filter_map do |lines|
494
+ first_line = lines.string.each_line.first.to_s.strip
495
+ next unless SameLineAnnotations::RBS_MARKER.match?(first_line)
496
+
497
+ line = lines.comments.first&.location&.start_line
498
+ position = line ? " on line #{line}" : ""
499
+ "a comment#{position} (`##{first_line}`) opens with the `@rbs` marker but its body did not " \
500
+ "parse as a recognised `@rbs` annotation, so the whole line was DROPPED — this is not a " \
501
+ "recognised tag, and nothing on it was applied. Use one of the documented `# @rbs` forms " \
502
+ "(`name: T`, `return: T`, `module-self T`, …), or rename the tag so it does not start with " \
503
+ "`@rbs`, to avoid this notice."
504
+ end
505
+ end
506
+
326
507
  # Rewrite every RDoc directive comment to its spaced spelling (`#:nodoc:` -> `# :nodoc:`) so
327
508
  # upstream's annotation grammar never sees it, and re-parse. Two reasons this happens here rather
328
509
  # than in {#annotated?}: the directive must not gate a file in, AND it must not reach the synthesis,
@@ -41,6 +41,7 @@ module Rigor
41
41
  # `schedule.yml` IS the schedule map — and the nested paths are `sidekiq-scheduler`'s, whose entries
42
42
  # live under `:scheduler: :schedule:` (with the bare `:schedule:` form kept for pre-3.0 layouts).
43
43
  SCHEDULE_BLOCK_KEY_PATHS = [[], %w[scheduler schedule], %w[schedule]].freeze
44
+ Ractor.make_shareable(SCHEDULE_BLOCK_KEY_PATHS)
44
45
 
45
46
  # Errno classes that mean "this path is not readable as a schedule" — swallowed so one bad path does
46
47
  # not cost the roots the other paths supply.
@@ -137,12 +137,12 @@ module Rigor
137
137
  services.fact_store.publish(plugin_id: manifest.id, name: :reachability_roots, value: roots)
138
138
  end
139
139
 
140
- # File-level only: the load-error emission. The per-call arity validation runs over the engine-owned
140
+ # File-level only: the load-error disclosure. The per-call arity validation runs over the engine-owned
141
141
  # walk via the node_rule below (ADR-37). The worker index is lazily loaded + memoised by
142
142
  # `producer_value`, shared by both surfaces.
143
143
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
144
144
  index = producer_value(:worker_index)
145
- return [load_error_diagnostic(path)] if index.nil? && producer_error(:worker_index)
145
+ disclose_load_error if index.nil? && producer_error(:worker_index)
146
146
 
147
147
  []
148
148
  end
@@ -206,10 +206,14 @@ module Rigor
206
206
 
207
207
  private
208
208
 
209
- def load_error_diagnostic(path)
209
+ # Issue #1056 — "the worker index did not load" is a fact about the run's INPUTS, not about the file being
210
+ # analysed, so it is registered rather than returned: see {Plugin::Base#disclose_once} for the
211
+ # channel, the `(plugin id, key)` de-duplication and why the engine positions it at `.rigor.yml:1:1`.
212
+ # Returned from the per-file hook it carried no once-guard at all and repeated on every file.
213
+ def disclose_load_error
210
214
  error = producer_error(:worker_index)
211
- Rigor::Analysis::Diagnostic.new(
212
- path: path, line: 1, column: 1,
215
+ disclose_once(
216
+ :worker_index_load_failed,
213
217
  message: "rigor-sidekiq: failed to discover workers: #{error.class}: #{error.message}",
214
218
  severity: :warning,
215
219
  rule: "load-error"
@@ -30,7 +30,6 @@ module Rigor
30
30
 
31
31
  module_function
32
32
 
33
- # @param contents — raw file contents.
34
33
  # @return one of {VALID_LEVELS}; defaults to {DEFAULT_LEVEL} for sigil-less or
35
34
  # malformed-sigil files.
36
35
  def detect(contents)