rigortype 0.2.9 → 0.3.1

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 (222) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/builtins/ruby_core/array.yml +416 -392
  4. data/data/builtins/ruby_core/file.yml +42 -42
  5. data/data/builtins/ruby_core/hash.yml +302 -302
  6. data/data/builtins/ruby_core/io.yml +191 -191
  7. data/data/builtins/ruby_core/numeric.yml +321 -366
  8. data/data/builtins/ruby_core/proc.yml +124 -124
  9. data/data/builtins/ruby_core/range.yml +21 -21
  10. data/data/builtins/ruby_core/rational.yml +39 -39
  11. data/data/builtins/ruby_core/re.yml +65 -65
  12. data/data/builtins/ruby_core/set.yml +106 -106
  13. data/data/builtins/ruby_core/struct.yml +14 -14
  14. data/data/core_overlay/string_scanner.rbs +6 -5
  15. data/docs/handbook/01-getting-started.md +22 -34
  16. data/docs/handbook/03-narrowing.md +2 -1
  17. data/docs/handbook/04-tuples-and-shapes.md +7 -3
  18. data/docs/handbook/06-classes.md +19 -10
  19. data/docs/handbook/07-rbs-and-extended.md +76 -101
  20. data/docs/handbook/08-understanding-errors.md +114 -246
  21. data/docs/handbook/09-plugins.md +54 -144
  22. data/docs/handbook/11-sig-gen.md +11 -1
  23. data/docs/handbook/README.md +5 -3
  24. data/docs/handbook/appendix-liskov.md +4 -2
  25. data/docs/handbook/appendix-mypy.md +4 -3
  26. data/docs/handbook/appendix-phpstan.md +14 -7
  27. data/docs/handbook/appendix-steep.md +4 -2
  28. data/docs/handbook/appendix-type-theory.md +3 -1
  29. data/docs/install.md +14 -6
  30. data/docs/manual/01-installation.md +98 -4
  31. data/docs/manual/02-cli-reference.md +81 -12
  32. data/docs/manual/03-configuration.md +34 -2
  33. data/docs/manual/04-diagnostics.md +59 -4
  34. data/docs/manual/06-baseline.md +35 -1
  35. data/docs/manual/07-plugins.md +4 -4
  36. data/docs/manual/08-skills.md +6 -1
  37. data/docs/manual/09-editor-integration.md +7 -6
  38. data/docs/manual/11-ci.md +51 -0
  39. data/docs/manual/12-caching.md +65 -0
  40. data/docs/manual/plugins/rigor-actioncable.md +32 -0
  41. data/docs/manual/plugins/rigor-devise.md +4 -2
  42. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
  43. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
  45. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
  46. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  47. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
  48. data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
  49. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  50. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
  52. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
  53. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +112 -0
  54. data/lib/rigor/analysis/check_rules.rb +581 -98
  55. data/lib/rigor/analysis/dependency_recorder.rb +93 -9
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  57. data/lib/rigor/analysis/incremental_session.rb +456 -51
  58. data/lib/rigor/analysis/path_expansion.rb +42 -0
  59. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
  60. data/lib/rigor/analysis/rule_catalog.rb +226 -1
  61. data/lib/rigor/analysis/run_cache_key.rb +78 -0
  62. data/lib/rigor/analysis/run_cache_probe.rb +72 -0
  63. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
  64. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
  65. data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
  66. data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
  67. data/lib/rigor/analysis/runner.rb +344 -68
  68. data/lib/rigor/analysis/severity_stamp.rb +43 -0
  69. data/lib/rigor/analysis/worker_session.rb +30 -2
  70. data/lib/rigor/bleeding_edge.rb +37 -8
  71. data/lib/rigor/builtins/regex_refinement.rb +36 -1
  72. data/lib/rigor/cache/descriptor.rb +88 -28
  73. data/lib/rigor/cache/file_digest.rb +158 -0
  74. data/lib/rigor/cache/incremental_snapshot.rb +41 -5
  75. data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
  76. data/lib/rigor/cache/rbs_descriptor.rb +56 -10
  77. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
  78. data/lib/rigor/cache/store.rb +132 -27
  79. data/lib/rigor/ci_detector.rb +90 -0
  80. data/lib/rigor/cli/annotate_command.rb +15 -9
  81. data/lib/rigor/cli/baseline_command.rb +4 -1
  82. data/lib/rigor/cli/check_command.rb +223 -32
  83. data/lib/rigor/cli/check_invocation.rb +84 -0
  84. data/lib/rigor/cli/coverage_command.rb +6 -1
  85. data/lib/rigor/cli/docs_command.rb +0 -30
  86. data/lib/rigor/cli/doctor_command.rb +169 -31
  87. data/lib/rigor/cli/lsp_command.rb +5 -0
  88. data/lib/rigor/cli/mcp_command.rb +5 -0
  89. data/lib/rigor/cli/plugins_command.rb +36 -6
  90. data/lib/rigor/cli/plugins_renderer.rb +44 -12
  91. data/lib/rigor/cli/probe_environment.rb +85 -0
  92. data/lib/rigor/cli/sig_gen_command.rb +36 -9
  93. data/lib/rigor/cli/skill_command.rb +22 -33
  94. data/lib/rigor/cli/skill_deep_probe.rb +172 -0
  95. data/lib/rigor/cli/skill_describe.rb +75 -9
  96. data/lib/rigor/cli/trace_command.rb +5 -6
  97. data/lib/rigor/cli/type_of_command.rb +9 -8
  98. data/lib/rigor/cli/type_scan_command.rb +8 -8
  99. data/lib/rigor/cli.rb +15 -4
  100. data/lib/rigor/config_audit.rb +48 -2
  101. data/lib/rigor/configuration/severity_profile.rb +31 -0
  102. data/lib/rigor/configuration.rb +156 -8
  103. data/lib/rigor/environment/default_libraries.rb +36 -0
  104. data/lib/rigor/environment/rbs_loader.rb +164 -5
  105. data/lib/rigor/environment.rb +29 -28
  106. data/lib/rigor/inference/budget_trace.rb +77 -2
  107. data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
  108. data/lib/rigor/inference/def_handle.rb +23 -0
  109. data/lib/rigor/inference/def_node_resolver.rb +90 -0
  110. data/lib/rigor/inference/def_return_typer.rb +2 -1
  111. data/lib/rigor/inference/expression_typer.rb +361 -113
  112. data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
  113. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
  114. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
  115. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
  116. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +17 -1
  117. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
  118. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
  119. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
  120. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
  121. data/lib/rigor/inference/method_dispatcher.rb +57 -1
  122. data/lib/rigor/inference/mutation_widening.rb +42 -8
  123. data/lib/rigor/inference/narrowing.rb +127 -17
  124. data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
  125. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  126. data/lib/rigor/inference/rbs_type_translator.rb +8 -1
  127. data/lib/rigor/inference/scope_indexer.rb +451 -36
  128. data/lib/rigor/inference/statement_evaluator.rb +64 -15
  129. data/lib/rigor/inference/struct_fold_safety.rb +48 -11
  130. data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
  131. data/lib/rigor/inference/void_origin.rb +25 -0
  132. data/lib/rigor/inference/void_tail_summary.rb +220 -0
  133. data/lib/rigor/language_server/buffer_resolution.rb +6 -3
  134. data/lib/rigor/language_server/buffer_table.rb +46 -6
  135. data/lib/rigor/language_server/completion_provider.rb +2 -1
  136. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
  137. data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
  138. data/lib/rigor/language_server/folding_range_provider.rb +2 -1
  139. data/lib/rigor/language_server/incremental_sync.rb +159 -0
  140. data/lib/rigor/language_server/selection_range_provider.rb +2 -1
  141. data/lib/rigor/language_server/server.rb +19 -9
  142. data/lib/rigor/language_server/signature_help_provider.rb +2 -1
  143. data/lib/rigor/language_server.rb +1 -0
  144. data/lib/rigor/plugin/base.rb +38 -24
  145. data/lib/rigor/plugin/inflector.rb +12 -3
  146. data/lib/rigor/plugin/io_boundary.rb +5 -1
  147. data/lib/rigor/plugin/isolation.rb +81 -11
  148. data/lib/rigor/plugin/load_error.rb +10 -1
  149. data/lib/rigor/plugin/loader.rb +96 -14
  150. data/lib/rigor/plugin/node_rule_walk.rb +5 -3
  151. data/lib/rigor/plugin/registry.rb +32 -23
  152. data/lib/rigor/plugin.rb +26 -0
  153. data/lib/rigor/protection/mutator.rb +3 -2
  154. data/lib/rigor/reflection.rb +64 -0
  155. data/lib/rigor/runtime/jit.rb +128 -0
  156. data/lib/rigor/scope/discovery_index.rb +12 -2
  157. data/lib/rigor/scope.rb +122 -28
  158. data/lib/rigor/sig_gen/classification.rb +5 -1
  159. data/lib/rigor/sig_gen/generator.rb +38 -6
  160. data/lib/rigor/sig_gen/observation_collector.rb +4 -3
  161. data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
  162. data/lib/rigor/sig_gen/renderer.rb +10 -0
  163. data/lib/rigor/sig_gen/write_result.rb +9 -4
  164. data/lib/rigor/sig_gen/writer.rb +207 -49
  165. data/lib/rigor/source/node_children.rb +116 -0
  166. data/lib/rigor/source/node_locator.rb +3 -1
  167. data/lib/rigor/source/node_walker.rb +4 -2
  168. data/lib/rigor/source.rb +1 -0
  169. data/lib/rigor/type/difference.rb +28 -24
  170. data/lib/rigor/type/hash_shape.rb +34 -10
  171. data/lib/rigor/version.rb +1 -1
  172. data/lib/rigortype.rb +24 -0
  173. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
  174. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
  175. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
  176. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
  177. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
  178. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
  179. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
  180. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
  181. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
  182. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
  183. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
  184. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
  185. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
  186. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
  187. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
  188. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
  189. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
  190. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
  191. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
  192. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
  193. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
  194. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
  195. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
  196. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
  197. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
  198. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
  199. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
  200. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
  201. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
  202. data/sig/prism_node_children.rbs +9 -0
  203. data/sig/rigor/cache.rbs +6 -0
  204. data/sig/rigor/inference/void_origin.rbs +18 -0
  205. data/sig/rigor/inference.rbs +7 -0
  206. data/sig/rigor/plugin/base.rbs +6 -6
  207. data/sig/rigor/reflection.rbs +1 -0
  208. data/sig/rigor/scope.rbs +12 -1
  209. data/skills/rigor-ci-setup/SKILL.md +10 -0
  210. data/skills/rigor-editor-setup/SKILL.md +11 -0
  211. data/skills/rigor-next-steps/SKILL.md +23 -2
  212. data/skills/rigor-plugin-author/SKILL.md +3 -3
  213. data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
  214. data/skills/rigor-plugin-review/SKILL.md +2 -2
  215. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
  216. data/skills/rigor-project-init/SKILL.md +29 -3
  217. data/skills/rigor-project-init/references/01-detect.md +1 -1
  218. data/skills/rigor-project-init/references/02-configure.md +39 -5
  219. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
  220. data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
  221. metadata +48 -26
  222. data/lib/rigor/cli/ci_detector.rb +0 -89
@@ -62,6 +62,17 @@ module Rigor
62
62
  narrowing_facts methods: AssertionAnalyzer::SUPPORTED_METHODS do |call_node, scope|
63
63
  AssertionAnalyzer.contribution_for(call_node, environment: scope&.environment)&.post_return_facts
64
64
  end
65
+
66
+ # ADR-88 WD1 — the narrowing facts this plugin contributes are derived purely from each assertion call's
67
+ # own AST at its call site (`assert_kind_of(String, x)` ⇒ `x` is `String` on the continuation); there is
68
+ # no cross-file catalog a cached diagnostic could depend on. A change to a file's assertions changes that
69
+ # file's own content (re-analysed by the incremental graph already). A stable sentinel declares "no
70
+ # cross-file fact surface", keeping the plugin incremental-capable (a contributing plugin with NO
71
+ # fact / producer / hook makes the incremental snapshot un-reusable every run); `--verify-incremental`
72
+ # backstops the claim.
73
+ def incremental_state_fingerprint
74
+ "per-file-assertions"
75
+ end
65
76
  end
66
77
 
67
78
  Rigor::Plugin.register(Minitest)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rigor/source/node_children"
4
+
3
5
  require "prism"
4
6
 
5
7
  require_relative "policy_index"
@@ -69,7 +71,7 @@ module Rigor
69
71
  when Prism::ClassNode then visit_class(node, lexical_path, &)
70
72
  when Prism::ModuleNode then visit_module(node, lexical_path, &)
71
73
  else
72
- node.compact_child_nodes.each { |child| walk_for_policies(child, lexical_path, &) }
74
+ node.rigor_each_child { |child| walk_for_policies(child, lexical_path, &) }
73
75
  end
74
76
  end
75
77
 
@@ -1,19 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Convenience entry point. `require "rigor-rails"` requires every Tier 1+2 Rails ecosystem plugin in one go,
4
- # so projects that prefer a single require statement (some `spec_helper` patterns, ad-hoc scripts) do not
5
- # have to list seven require lines.
3
+ # The Tier 1+2 Rails set, as a single require. Requiring this file registers every member class with
4
+ # `Rigor::Plugin`; the loader's lookup phase then finds them by id when `.rigor.yml` enumerates them.
6
5
  #
7
- # Note: requiring this entry point does NOT mark every plugin as active. The Rigor plugin loader walks
8
- # `.rigor.yml`'s `plugins:` list and instantiates only the plugins enumerated there. This is per ADR-12
9
- # WD1's "Gemfile-convenience meta-gem" pattern users still control which plugins participate in analysis
10
- # via `.rigor.yml`.
6
+ # UNWIRED this entry point has no supported use today, and is kept deliberately (ADR-96 WD5) rather
7
+ # than removed. It was ADR-12 WD1's "Gemfile-convenience meta-gem": each plugin was to be its own gem, and
8
+ # `gem "rigor-rails"` was to pull the Rails set into a project's Gemfile in one line. Commit `9769f5fa`
9
+ # dropped the per-plugin gemspecs and ADR-31 settled distribution on the single bundled `rigortype` gem, so
10
+ # there is no `rigor-rails` gem to add to a Gemfile — and `plugins: [rigor-rails]` is rejected by the
11
+ # loader, which requires the members to be enumerated (or one selected with an explicit `id:`).
11
12
  #
12
- # Sub-plugins ARE registered with `Rigor::Plugin` when this file loads (each gem's entry point side-effects
13
- # a `Plugin.register` call); the loader's lookup phase finds them by id when listed.
14
- #
15
- # Adding the gem to a project's Gemfile without listing any plugin in `.rigor.yml` is harmless: the requires
16
- # happen on `Bundler.require`, but no plugin's `init` / `prepare` / hooks run.
13
+ # ADR-96 WD3 proposes the role this file would fill: `plugins: [rigor-rails]` activating those members
14
+ # whose `target_gems:` are actually in the project's `Gemfile.lock`. If that is rejected, ADR-60 WD1's
15
+ # never-wired-surface criterion applies and this directory goes.
17
16
 
18
17
  require "rigor-rails-routes"
19
18
  require "rigor-rails-i18n"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rigor/source/node_children"
4
+
3
5
  require "prism"
4
6
 
5
7
  module Rigor
@@ -106,7 +108,7 @@ module Rigor
106
108
  end
107
109
  end
108
110
 
109
- node.compact_child_nodes.each { |child| walk(child, prefix_path, declarations, superclasses) }
111
+ node.rigor_each_child { |child| walk(child, prefix_path, declarations, superclasses) }
110
112
  end
111
113
 
112
114
  def record_class(node, qualified, declarations, superclasses)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rigor/source/node_children"
4
+
3
5
  require "prism"
4
6
 
5
7
  module Rigor
@@ -87,7 +89,7 @@ module Rigor
87
89
  def visit_program_children(node)
88
90
  return unless node.respond_to?(:compact_child_nodes)
89
91
 
90
- node.compact_child_nodes.each do |child|
92
+ node.rigor_each_child do |child|
91
93
  case child
92
94
  when Prism::ModuleNode, Prism::ClassNode
93
95
  visit_module_or_class(child)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rigor/source/node_children"
4
+
3
5
  require "prism"
4
6
  require "rigor/source/literals"
5
7
 
@@ -371,7 +373,7 @@ module Rigor
371
373
  when Prism::CallNode
372
374
  interpret_call(node, context)
373
375
  else
374
- node.compact_child_nodes.each { |child| interpret(child, context) }
376
+ node.rigor_each_child { |child| interpret(child, context) }
375
377
  end
376
378
  end
377
379
 
@@ -526,7 +528,7 @@ module Rigor
526
528
  return unless node.is_a?(Prism::Node)
527
529
 
528
530
  yield node
529
- node.compact_child_nodes.each { |child| walk_for_alias_pattern(child, &) }
531
+ node.rigor_each_child { |child| walk_for_alias_pattern(child, &) }
530
532
  end
531
533
 
532
534
  def first_block_parameter_name(block_node)
@@ -545,7 +547,7 @@ module Rigor
545
547
  body = node.block&.body
546
548
  return if body.nil?
547
549
 
548
- body.compact_child_nodes.each { |child| interpret(child, context) }
550
+ body.rigor_each_child { |child| interpret(child, context) }
549
551
  end
550
552
 
551
553
  def handle_namespace(node, context)
@@ -625,7 +627,7 @@ module Rigor
625
627
  return [] if body.nil?
626
628
 
627
629
  skips = []
628
- body.compact_child_nodes.each do |child|
630
+ body.rigor_each_child do |child|
629
631
  next unless child.is_a?(Prism::CallNode) && child.name == :skip_controllers
630
632
  next if child.receiver
631
633
 
@@ -748,7 +750,7 @@ module Rigor
748
750
  body = context.concern_body(concern_name)
749
751
  next if body.nil?
750
752
 
751
- body.compact_child_nodes.each { |child| interpret(child, context) }
753
+ body.rigor_each_child { |child| interpret(child, context) }
752
754
  end
753
755
  end
754
756
 
@@ -8,9 +8,10 @@ require "rigor/plugin"
8
8
  # name: T`, `# @rbs return: T`, attribute `#:`, …) and contributes the result to the analysis environment
9
9
  # through the `source_rbs_synthesizer:` manifest hook.
10
10
  #
11
- # By default (WD2) only files starting with the upstream `# rbs_inline: enabled` magic comment are
12
- # processed; a host context can flip this off by setting `require_magic_comment: false` in the plugin
13
- # config (WD10).
11
+ # Since ADR-93 WD1 the default is `require_magic_comment: false`: a file is processed whenever it actually
12
+ # carries an annotation (see {Synthesizer#annotated?}), with only the upstream `# rbs_inline: disabled`
13
+ # directive opting a file out. Set `require_magic_comment: true` to restore the old ADR-32 WD2 gate that
14
+ # processed only files opening with `# rbs_inline: enabled`.
14
15
  module Rigor
15
16
  module Plugin
16
17
  # The plugin gem requires `rbs/inline` at load time; without the upstream library the synthesizer can't
@@ -35,9 +36,24 @@ module Rigor
35
36
  # env-build time. Returns the synthesised RBS source as a String, or `nil` when the file contributes
36
37
  # nothing (no magic comment in the default mode, empty annotation set, parse error per WD6).
37
38
  class Synthesizer
38
- # @param require_magic_comment [Boolean] when `true` (the default, WD2), only files with
39
- # `# rbs_inline: enabled` at the top are processed. When `false` (WD10 host-context override),
40
- # every file is treated as if it carried the magic comment.
39
+ # An RDoc directive comment: `#:` followed by a bare word (RDoc allows a hyphen, as in `:call-seq:`)
40
+ # and a closing colon. RDoc predates rbs-inline by about two decades and `#:<name>:` is
41
+ # indistinguishable from an rbs-inline `#: <type>` assertion to upstream's parser, which reads the
42
+ # directive name as a type alias — `def f #:nodoc:` becomes `def f: () -> nodoc`, a type nothing
43
+ # declares (upstream issue: https://github.com/soutaro/rbs-inline/issues/248).
44
+ #
45
+ # Matching on shape rather than a name list covers all 17 directives the Ruby docs list
46
+ # (https://docs.ruby-lang.org/ja/latest/library/rdoc.html) with nothing to maintain, and none of
47
+ # `#: () -> void`, `#: String`, `#:Integer`, `#: bool`, `#:my_alias`, `#: { name: String }`,
48
+ # `#: (name: String) -> void`, `#: Integer?`, `#[Integer]` or `# @rbs x: String` matches it: a valid
49
+ # RBS type never starts with a bare lowercase word that is immediately closed by a colon. Only the
50
+ # space-free spelling is affected; `# :nodoc:` never reaches upstream's annotation grammar.
51
+ RDOC_DIRECTIVE_COMMENT = /\A#:[a-z_][\w-]*:/
52
+
53
+ # @param require_magic_comment [Boolean] when `false` (the default since ADR-93 WD1), the magic
54
+ # comment is not required and the file is processed only if it actually carries an annotation — see
55
+ # {#annotated?}. When `true` (the old ADR-32 WD2 gate), only files opening with
56
+ # `# rbs_inline: enabled` are processed, and upstream's opt-in semantics apply verbatim.
41
57
  def initialize(require_magic_comment:)
42
58
  @require_magic_comment = require_magic_comment
43
59
  freeze
@@ -55,6 +71,9 @@ module Rigor
55
71
  return nil if source.empty?
56
72
 
57
73
  result = ::Prism.parse(source)
74
+ _, result = neutralize_rdoc_directives(source, result)
75
+ return nil if !@require_magic_comment && !annotated?(result)
76
+
58
77
  # `opt_in: true` is rbs-inline's "require the magic comment" mode (per upstream parser.rb:62).
59
78
  # The plugin's `require_magic_comment:` config knob maps directly onto it.
60
79
  parsed = ::RBS::Inline::Parser.parse(result, opt_in: @require_magic_comment)
@@ -71,6 +90,56 @@ module Rigor
71
90
  # message, without crashing analysis.
72
91
  [:error, "#{e.class}: #{e.message.to_s.lines.first.to_s.strip}"]
73
92
  end
93
+
94
+ private
95
+
96
+ # True when the file carries at least one rbs-inline annotation. Gates the magic-comment-free mode,
97
+ # and is the difference between "honour annotations wherever they are" and "fabricate signatures for
98
+ # code nobody annotated" — upstream's opt-out mode does the latter, emitting a full
99
+ # `def f: (untyped x) -> untyped` skeleton for EVERY unannotated def. Rigor trusts an accepted
100
+ # signature over body inference, so those skeletons would replace real inferred types with `untyped`:
101
+ # measured on mail (zero annotations) as 26 → 42 diagnostics, i.e. the mode actively fought the
102
+ # analysis on exactly the projects that write no annotations. The spec binds Rigor to treat
103
+ # annotations as type sources "whenever present" (`overview.md` § "Compatibility hierarchy"); it does
104
+ # not ask for untyped shadows of unannotated code.
105
+ #
106
+ # Detection delegates to upstream's own `AnnotationParser` rather than scanning for `#:` / `@rbs`
107
+ # with a regexp: the annotation grammar is upstream's to define (ADR-32 WD3), and this keeps a doc
108
+ # comment that merely mentions `@rbs`, or a URL containing `#:`, from opting a file in. A file with
109
+ # the magic comment keeps upstream's semantics verbatim — the author opted that file in explicitly,
110
+ # skeletons and all, which is what `rbs-inline --output` would generate for it.
111
+ def annotated?(prism_result)
112
+ ::RBS::Inline::AnnotationParser.parse(prism_result.comments)
113
+ .any? { |parsed| parsed.each_annotation.any? }
114
+ end
115
+
116
+ # Rewrite every RDoc directive comment to its spaced spelling (`#:nodoc:` -> `# :nodoc:`) so
117
+ # upstream's annotation grammar never sees it, and re-parse. Two reasons this happens here rather
118
+ # than in {#annotated?}: the directive must not gate a file in, AND it must not reach the synthesis,
119
+ # because one mis-parsed directive takes the whole class down with it. `def f #:nodoc:` renders as
120
+ # `def f: (untyped x) -> nodoc`; `nodoc` resolves to nothing, `RBS::DefinitionBuilder` raises
121
+ # `NoTypeFoundError` for the class, and every real annotation in that class is silently lost —
122
+ # measured on a class whose `#: (String) -> Integer` method fell back to body inference because a
123
+ # sibling method carried `#:nodoc:`. `fileutils.rb` alone carries 29 of these.
124
+ #
125
+ # The rewrite touches comment text only, so it cannot change what Ruby does; it shifts columns
126
+ # within the comment, which nothing downstream reads (the synthesized RBS is fresh text with its own
127
+ # buffer). Prism decides what is a comment, so a `#:nodoc:` inside a string literal stays put. Files
128
+ # without a directive re-parse nothing.
129
+ def neutralize_rdoc_directives(source, prism_result)
130
+ offsets = prism_result.comments.filter_map do |comment|
131
+ location = comment.location
132
+ # `start_character_offset`, not `start_offset`: the latter counts BYTES while `String#insert`
133
+ # indexes CHARACTERS, so on a file with any multi-byte content the space lands mid-word and
134
+ # rewrites `#:nodoc:` to `#:n odoc:` — still a directive to upstream, and now a corrupted one.
135
+ location.start_character_offset if RDOC_DIRECTIVE_COMMENT.match?(location.slice)
136
+ end
137
+ return [source, prism_result] if offsets.empty?
138
+
139
+ # Back to front, so each insertion leaves the earlier offsets valid.
140
+ rewritten = offsets.sort.reverse.inject(source.dup) { |acc, offset| acc.insert(offset + 1, " ") }
141
+ [rewritten, ::Prism.parse(rewritten)]
142
+ end
74
143
  end
75
144
 
76
145
  manifest(
@@ -87,11 +156,16 @@ module Rigor
87
156
  # override (returned by `#manifest`, which `Plugin::Registry#source_rbs_synthesizers` consults via
88
157
  # `plugin.manifest.source_rbs_synthesizer`).
89
158
  #
90
- # ADR-32 WD10 — `require_magic_comment` defaults to `true`. Setting it to `false` in `.rigor.yml`
91
- # flips the synthesizer into "process every file" mode.
159
+ # ADR-93 WD1 — `require_magic_comment` defaults to `false`: annotations are official type sources
160
+ # "always parsed whenever present" per the binding spec (overview.md § Compatibility hierarchy), so
161
+ # a file is processed when it actually carries an annotation, with the magic comment not required.
162
+ # The magic-comment-free mode is annotation-presence-gated (see {Synthesizer#annotated?}), so an
163
+ # unannotated file contributes nothing and the flip cannot regress it. Set `require_magic_comment:
164
+ # true` in `.rigor.yml` to restore the ADR-32 opt-in behaviour; `# rbs_inline: disabled` remains the
165
+ # per-file opt-out either way (honoured by upstream unconditionally).
92
166
  def initialize(services:, config: {})
93
167
  super
94
- @require_magic_comment = config.fetch("require_magic_comment", true) ? true : false
168
+ @require_magic_comment = config.fetch("require_magic_comment", false) ? true : false
95
169
  @synthesizer = Synthesizer.new(require_magic_comment: @require_magic_comment)
96
170
  # Build the per-instance manifest eagerly (before `freeze`) so the registry's repeated reads
97
171
  # return the same object and we don't need to mutate a frozen instance later.
@@ -3,16 +3,20 @@
3
3
  # rigor-rbs-inline — ingests rbs-inline-shaped comments (`# @rbs name: T`, `#: () -> T`, `# @rbs return: T`,
4
4
  # attribute `#:`, ivars, generics, override, …) as RBS contributions to the analysis environment.
5
5
  #
6
- # Per ADR-32 the plugin gates per file on the upstream `# rbs_inline: enabled` magic comment by default. A
7
- # host context that owns the entire analysis scope (e.g. the ADR-29 browser playground) can set
8
- # `require_magic_comment: false` in the plugin config to skip the gate — every file the synthesizer sees is
9
- # treated as if it carried the magic comment.
6
+ # Since ADR-93 WD1 the plugin defaults to `require_magic_comment: false`: annotations are official type
7
+ # sources wherever present, gated only on a file actually carrying one, with `# rbs_inline: disabled` the
8
+ # per-file opt-out. ADR-93 WD2 default-wires this plugin from `Configuration.load` when the upstream
9
+ # `rbs-inline` library is resolvable, so most projects need no `plugins:` entry at all. Restore the old
10
+ # ADR-32 magic-comment gate, or opt out of the auto-wire, through config:
10
11
  #
11
12
  # # .rigor.yml
12
13
  # plugins:
13
14
  # - id: rigor-rbs-inline
14
15
  # config:
15
- # require_magic_comment: false # default true
16
+ # require_magic_comment: true # default false — restore the ADR-32 magic-comment gate
17
+ # # or disable the auto-wired default entirely:
18
+ # # - gem: rigor-rbs-inline
19
+ # # enabled: false
16
20
  #
17
21
  # The plugin depends on the upstream `rbs-inline` gem; Rigor's core `rigortype` gemspec stays zero-dep per
18
22
  # ADR-0 / ADR-32 WD1.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rigor/source/node_children"
4
+
3
5
  require "prism"
4
6
  require "rigor/source/literals"
5
7
 
@@ -89,7 +91,7 @@ module Rigor
89
91
  return
90
92
  end
91
93
 
92
- node.compact_child_nodes.each do |child|
94
+ node.rigor_each_child do |child|
93
95
  collect(child, anchor: anchor, accumulator: accumulator)
94
96
  end
95
97
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rigor/source/node_children"
4
+
3
5
  require "prism"
4
6
  require "rigor/source/literals"
5
7
 
@@ -70,7 +72,7 @@ module Rigor
70
72
  if rspec_describe_call?(node)
71
73
  scopes << build_scope(node, kind: :rspec_describe)
72
74
  else
73
- node.compact_child_nodes.each { |child| walk_top_level(child, scopes) }
75
+ node.rigor_each_child { |child| walk_top_level(child, scopes) }
74
76
  end
75
77
  end
76
78
 
@@ -97,6 +97,17 @@ module Rigor
97
97
  let_binding_return_type(call_node, scope)
98
98
  end
99
99
 
100
+ # ADR-88 WD1 — both contributions are per-file: the matcher narrowing facts come from the call's own AST,
101
+ # and the `let`-binding return types come from each spec file's OWN `describe`/`let` structure (`let` is
102
+ # file-scoped). A spec file's edit changes its own content (re-analysed already). The one cross-file input
103
+ # — the optional factorybot `:factory_index` fact this plugin consumes — is itself an ADR-9 publication
104
+ # captured in the incremental fact-surface digest via factorybot's own channel, so it need not appear
105
+ # here. A stable sentinel declares "no OWN cross-file fact surface", keeping the plugin
106
+ # incremental-capable; `--verify-incremental` backstops the claim.
107
+ def incremental_state_fingerprint
108
+ "per-file-lets"
109
+ end
110
+
100
111
  private
101
112
 
102
113
  # The `file_methods:` gate set — every `let` / `subject` name the file declares anywhere. A safe
@@ -43,7 +43,10 @@ module Rigor
43
43
 
44
44
  require "rack/utils"
45
45
  @rack_status_symbols = Rack::Utils::SYMBOL_TO_STATUS_CODE.keys.to_set.freeze
46
- rescue LoadError, StandardError
46
+ rescue ::LoadError, ::StandardError
47
+ # `::`-qualified: in this lexical scope a bare `LoadError` resolves to `Rigor::Plugin::LoadError`
48
+ # (the plugin-loading error, a StandardError), so the real `::LoadError` (a ScriptError) from a
49
+ # failed require would escape instead of declining.
47
50
  @rack_status_symbols = nil
48
51
  end
49
52
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rigor/source/node_children"
4
+
3
5
  require "prism"
4
6
 
5
7
  require_relative "worker_index"
@@ -67,7 +69,7 @@ module Rigor
67
69
  when Prism::ClassNode then visit_class(node, lexical_path, &)
68
70
  when Prism::ModuleNode then visit_module(node, lexical_path, &)
69
71
  else
70
- node.compact_child_nodes.each { |child| walk_for_workers(child, lexical_path, &) }
72
+ node.rigor_each_child { |child| walk_for_workers(child, lexical_path, &) }
71
73
  end
72
74
  end
73
75
 
@@ -133,7 +135,7 @@ module Rigor
133
135
  def lookup_perform_def(body)
134
136
  return nil if body.nil?
135
137
 
136
- body.compact_child_nodes.each do |node|
138
+ body.rigor_each_child do |node|
137
139
  next unless node.is_a?(Prism::DefNode) && node.name == :perform
138
140
  next if node.receiver.is_a?(Prism::SelfNode)
139
141
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rigor/source/node_children"
4
+
3
5
  require "prism"
4
6
 
5
7
  require_relative "method_signature"
@@ -58,7 +60,7 @@ module Rigor
58
60
  # A `def` not preceded by a `sig` is fine; we just don't record anything for it. The
59
61
  # interesting case is in `walk_statements`, which pairs sig+def.
60
62
  else
61
- node.compact_child_nodes.each do |child|
63
+ node.rigor_each_child do |child|
62
64
  walk_node(child, state, lexical_path: lexical_path, in_singleton_class: in_singleton_class)
63
65
  end
64
66
  end
@@ -116,6 +116,18 @@ module Rigor
116
116
  errors.map { |error| parse_error_diagnostic(path, error) }
117
117
  end
118
118
 
119
+ # ADR-88 WD2 — the per-run catalog is an ADR-60 record-and-validate producer. Before this it was rebuilt
120
+ # unconditionally on every `--incremental` recheck (parse + walk every `.rb` / `.rbi` sig tree); as a
121
+ # producer it is cached to disk keyed on the plugin config, with `watch:` covering the scanned trees so an
122
+ # added / removed / edited sig file recomputes it. The producer VALUE (a Marshal-clean bundle of the
123
+ # catalog + the sigil map + the parse-error tuples) is what ADR-88 WD1 digests into the incremental
124
+ # fact-surface fingerprint, so a sig edit OUTSIDE `signature_paths:` invalidates the snapshot for free.
125
+ # The bundle is returned (not written to ivars) because a cache HIT does not run the block — the sigil map
126
+ # and parse errors would otherwise be lost on the warm path.
127
+ producer :catalog, watch: -> { catalog_watch_globs } do |_params|
128
+ build_catalog_bundle
129
+ end
130
+
119
131
  # ADR-52 slice 4 — per-call return-type path via the method-name-gated `dynamic_return` DSL. The
120
132
  # recognised name set is only known at run time (the catalog's `def` names come from the lazy
121
133
  # catalog build), so it is declared as a callable: the engine `instance_exec`s it once per run on
@@ -389,35 +401,69 @@ module Rigor
389
401
  end
390
402
  end
391
403
 
404
+ # ADR-88 WD2 — resolve the catalog from the `:catalog` producer (disk-cached) and unpack its Marshal-clean
405
+ # bundle into the ivars the return-type / sigil-gate / parse-error paths read. `producer_value` memoises
406
+ # per instance and rescues a producer failure to nil, hence the empty-bundle fallback.
392
407
  def ensure_catalog
393
408
  return @catalog if @catalog_built
394
409
 
410
+ bundle = producer_value(:catalog) || EMPTY_CATALOG_BUNDLE
411
+ @catalog = bundle[:catalog]
412
+ @sigil_by_path = bundle[:sigil_by_path]
413
+ @parse_errors_by_path = bundle[:parse_errors_by_path]
414
+ @catalog_built = true
415
+ @catalog
416
+ end
417
+
418
+ # ADR-88 WD2 — the `watch:` coverage for the `:catalog` producer: the `.rb` sig trees + the `.rbi` trees.
419
+ # An added / removed file under either invalidates the cached bundle even though the block globbed the
420
+ # tree itself. Evaluated at `cache_for` time (after `#init`), so `@configured_paths` / `@rbi_paths` are set.
421
+ def catalog_watch_globs
422
+ [[@configured_paths, "**/*.rb"], [@rbi_paths, "**/*.rbi"]]
423
+ end
424
+
425
+ # ADR-88 WD2 — build the Marshal-clean catalog bundle the `:catalog` producer caches. Harvests the `.rb`
426
+ # sig trees then the `.rbi` trees (last-wins ordering preserved from the ivar-mutating original) and
427
+ # returns `{ catalog:, sigil_by_path:, parse_errors_by_path: }`.
428
+ def build_catalog_bundle
395
429
  catalog = Catalog.new
430
+ sigil_by_path = {}
431
+ parse_errors_by_path = {}
396
432
  # Project source — `.rb` only.
397
- @configured_paths.each { |root| harvest_path(root, catalog, extensions: %w[.rb]) }
433
+ @configured_paths.each { |root| harvest_path(root, catalog, sigil_by_path, parse_errors_by_path, %w[.rb]) }
398
434
  # Sorbet RBI tree — `.rbi` only. Slice 4 of ADR-11.
399
- @rbi_paths.each { |root| harvest_path(root, catalog, extensions: %w[.rbi]) }
435
+ @rbi_paths.each { |root| harvest_path(root, catalog, sigil_by_path, parse_errors_by_path, %w[.rbi]) }
400
436
  catalog.freeze!
401
- @catalog = catalog
402
- @catalog_built = true
403
- catalog
437
+ { catalog: catalog, sigil_by_path: sigil_by_path, parse_errors_by_path: parse_errors_by_path }
404
438
  end
405
439
 
440
+ # Frozen empty bundle used when the `:catalog` producer failed (e.g. a project I/O error) so downstream
441
+ # reads see a well-formed shape rather than nil.
442
+ EMPTY_CATALOG_BUNDLE = { catalog: Catalog.new.freeze!, sigil_by_path: {}, parse_errors_by_path: {} }.freeze
443
+ private_constant :EMPTY_CATALOG_BUNDLE
444
+
406
445
  # @param root [String] directory or single file.
407
446
  # @param catalog [Catalog]
447
+ # @param sigil_by_path [Hash{String=>Symbol}] accumulator: harvested file → detected sigil level.
448
+ # @param parse_errors_by_path [Hash{String=>Array<Hash>}] accumulator: file → `{kind:,line:,column:}` tuples.
408
449
  # @param extensions [Array<String>] file extensions to accept (e.g. `[".rb"]` for project source,
409
450
  # `[".rbi"]` for Sorbet RBI tree).
410
- def harvest_path(root, catalog, extensions:)
451
+ def harvest_path(root, catalog, sigil_by_path, parse_errors_by_path, extensions)
411
452
  absolute = canonicalize(root)
412
453
  if File.directory?(absolute)
413
454
  extensions.each do |ext|
455
+ # ADR-88 WD2 — deterministic fold order matters now that the catalog VALUE is digested into the
456
+ # incremental fact-surface fingerprint: a duplicate `(class, method, kind)` sig's last-wins winner
457
+ # must not vary by machine / run. `Dir.glob` sorts its results by default on Ruby 3.0+, so the fold
458
+ # order is already stable (the slice-1 comment's "filesystem order" caveat predates that default);
459
+ # this walk relies on it rather than re-sorting.
414
460
  Dir.glob(File.join(absolute, "**", "*#{ext}")).each do |path|
415
- harvest_file(canonicalize(path), catalog)
461
+ harvest_file(canonicalize(path), catalog, sigil_by_path, parse_errors_by_path)
416
462
  end
417
463
  end
418
464
  elsif File.file?(absolute) && extensions.any? { |ext| absolute.end_with?(ext) }
419
465
  # `paths:` may list individual files (the demos do this); walk them directly rather than skipping.
420
- harvest_file(absolute, catalog)
466
+ harvest_file(absolute, catalog, sigil_by_path, parse_errors_by_path)
421
467
  end
422
468
  end
423
469
 
@@ -432,7 +478,7 @@ module Rigor
432
478
  expanded
433
479
  end
434
480
 
435
- def harvest_file(path, catalog)
481
+ def harvest_file(path, catalog, sigil_by_path, parse_errors_by_path)
436
482
  contents = io_boundary.read_file(path)
437
483
  return if contents.nil?
438
484
 
@@ -441,7 +487,7 @@ module Rigor
441
487
  # Per-call-site assertion gating consults this map at recognition time. Recorded BEFORE the
442
488
  # ignored short-circuit so a `# typed: ignore` file still reports its level to the gate (the gate
443
489
  # then chooses to suppress assertions there too — `ignore` is stricter than `false`).
444
- @sigil_by_path[path] = level
490
+ sigil_by_path[path] = level
445
491
  return if SigilDetector.ignored?(level)
446
492
 
447
493
  result = Prism.parse(contents)
@@ -460,13 +506,23 @@ module Rigor
460
506
  end
461
507
 
462
508
  errors = CatalogWalker.walk(root: result.value, catalog: sig_catalog, path: path)
463
- @parse_errors_by_path[path] = errors unless errors.empty?
509
+ # ADR-88 WD2 store Marshal-clean `{kind:, line:, column:}` tuples, not the `ParseError` (it holds a
510
+ # live Prism node the producer bundle could not serialise). `diagnostics_for_file` rebuilds the
511
+ # diagnostic from the tuple, at the same 1-based line / `start_column + 1` position `from_node` gave.
512
+ parse_errors_by_path[path] = errors.map { |error| parse_error_tuple(error) } unless errors.empty?
464
513
  rescue Plugin::AccessDeniedError, Errno::ENOENT
465
514
  # Skip files outside the trusted read scope or that vanished between glob and read; the plugin
466
515
  # produces no output for them.
467
516
  nil
468
517
  end
469
518
 
519
+ # ADR-88 WD2 — the Marshal-clean position tuple for a `CatalogWalker::ParseError`, capturing the node's
520
+ # location at harvest time so the producer bundle carries no live Prism node.
521
+ def parse_error_tuple(error)
522
+ location = error.node.location
523
+ { kind: error.kind, line: location.start_line, column: location.start_column + 1 }
524
+ end
525
+
470
526
  # Emits a `plugin.sorbet.absurd-reachable` warning for the `T.absurd(x)` call recorded in
471
527
  # `@reachable_absurd_nodes` during inference; the node rule above does the identity match and pop.
472
528
  def absurd_diagnostic(path, call_node)
@@ -535,11 +591,15 @@ module Rigor
535
591
  )
536
592
  end
537
593
 
594
+ # ADR-88 WD2 — `error` is a Marshal-clean `{kind:, line:, column:}` tuple (the pre-computed node
595
+ # location), not a Prism-node-bearing `ParseError`; build the diagnostic directly at that position (the
596
+ # `line` / `column` `from_node` would have derived).
538
597
  def parse_error_diagnostic(path, error)
539
- Rigor::Analysis::Diagnostic.from_node(
540
- error.node,
598
+ Rigor::Analysis::Diagnostic.new(
541
599
  path: path,
542
- message: parse_error_message(error.kind),
600
+ line: error[:line],
601
+ column: error[:column],
602
+ message: parse_error_message(error[:kind]),
543
603
  severity: :warning,
544
604
  rule: "parse-error"
545
605
  )
@@ -0,0 +1,9 @@
1
+ # Declares the reopening `Rigor::Source::NodeChildren` performs at load: a compiled `#rigor_each_child` is defined on
2
+ # every concrete `Prism::*Node` class (allocation-free equivalent of `compact_child_nodes.each`). Declared on the
3
+ # abstract base so every subclass resolves; the runtime definition is per-concrete-class, generated from
4
+ # `Prism::Reflection` in lib/rigor/source/node_children.rb.
5
+ module Prism
6
+ class Node
7
+ def rigor_each_child: () { (Prism::Node) -> void } -> void
8
+ end
9
+ end
data/sig/rigor/cache.rbs CHANGED
@@ -14,6 +14,12 @@ module Rigor
14
14
  # PRODUCER_ID constant and a private `self.compute(loader)`.
15
15
  class RbsCacheProducer
16
16
  def self.fetch: (loader: untyped, store: untyped) -> untyped
17
+
18
+ # The generation cap this producer declares to `Cache::Store#evict!`
19
+ # (issue #151). `rigor sig-gen` infers the literal `2` from the base
20
+ # body; the contract is the wider `Integer`, since a subclass may
21
+ # override it.
22
+ def self.generation_cap: () -> Integer
17
23
  end
18
24
  end
19
25
  end
@@ -0,0 +1,18 @@
1
+ module Rigor
2
+ module Inference
3
+ # `VoidOrigin = Data.define(:class_name, :method_name, :kind)`. Declared as a `::Data` subclass because
4
+ # that is the shape the runtime actually builds; `sig-gen` cannot yet read a `Data.define` assignment
5
+ # (it reports the block body's `#label` against the enclosing namespace and drops the members).
6
+ class VoidOrigin < ::Data
7
+ attr_reader class_name: String
8
+ attr_reader method_name: Symbol
9
+ attr_reader kind: Symbol
10
+
11
+ def self.new: (class_name: String, method_name: Symbol, kind: Symbol) -> instance
12
+ | (String class_name, Symbol method_name, Symbol kind) -> instance
13
+
14
+ # A human-facing `Class#method` / `Class.method` label for the diagnostic message.
15
+ def label: () -> String
16
+ end
17
+ end
18
+ end