rigortype 0.2.8 → 0.3.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 (207) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/csv.rbs +28 -0
  4. data/data/core_overlay/psych.rbs +22 -0
  5. data/docs/handbook/01-getting-started.md +9 -1
  6. data/docs/handbook/02-everyday-types.md +4 -1
  7. data/docs/handbook/03-narrowing.md +2 -1
  8. data/docs/handbook/04-tuples-and-shapes.md +7 -3
  9. data/docs/handbook/06-classes.md +18 -9
  10. data/docs/handbook/08-understanding-errors.md +4 -3
  11. data/docs/handbook/11-sig-gen.md +11 -1
  12. data/docs/handbook/appendix-mypy.md +4 -3
  13. data/docs/install.md +14 -6
  14. data/docs/manual/01-installation.md +99 -4
  15. data/docs/manual/02-cli-reference.md +65 -19
  16. data/docs/manual/03-configuration.md +34 -2
  17. data/docs/manual/04-diagnostics.md +23 -0
  18. data/docs/manual/07-plugins.md +5 -5
  19. data/docs/manual/09-editor-integration.md +4 -4
  20. data/docs/manual/11-ci.md +51 -0
  21. data/docs/manual/12-caching.md +65 -0
  22. data/docs/manual/14-rails-quickstart.md +4 -2
  23. data/docs/manual/15-type-protection-coverage.md +21 -0
  24. data/docs/manual/plugins/rigor-actionpack.md +1 -1
  25. data/docs/manual/plugins/rigor-activerecord.md +12 -5
  26. data/docs/manual/plugins/rigor-rails-routes.md +11 -0
  27. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
  28. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
  29. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
  30. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
  31. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  32. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
  33. data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
  34. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  35. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
  36. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
  37. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
  38. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
  39. data/lib/rigor/analysis/check_rules.rb +557 -95
  40. data/lib/rigor/analysis/dependency_recorder.rb +93 -9
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  42. data/lib/rigor/analysis/incremental_session.rb +456 -51
  43. data/lib/rigor/analysis/path_expansion.rb +42 -0
  44. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
  45. data/lib/rigor/analysis/rule_catalog.rb +226 -1
  46. data/lib/rigor/analysis/run_cache_key.rb +68 -0
  47. data/lib/rigor/analysis/run_cache_probe.rb +72 -0
  48. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
  49. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
  50. data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
  51. data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
  52. data/lib/rigor/analysis/runner.rb +343 -68
  53. data/lib/rigor/analysis/severity_stamp.rb +43 -0
  54. data/lib/rigor/analysis/worker_session.rb +30 -2
  55. data/lib/rigor/bleeding_edge.rb +37 -8
  56. data/lib/rigor/builtins/regex_refinement.rb +36 -1
  57. data/lib/rigor/cache/descriptor.rb +88 -28
  58. data/lib/rigor/cache/file_digest.rb +158 -0
  59. data/lib/rigor/cache/incremental_snapshot.rb +41 -5
  60. data/lib/rigor/cache/rbs_descriptor.rb +56 -10
  61. data/lib/rigor/cache/store.rb +207 -60
  62. data/lib/rigor/ci_detector.rb +90 -0
  63. data/lib/rigor/cli/annotate_command.rb +15 -9
  64. data/lib/rigor/cli/baseline_command.rb +4 -1
  65. data/lib/rigor/cli/check_command.rb +212 -27
  66. data/lib/rigor/cli/coverage_command.rb +56 -30
  67. data/lib/rigor/cli/diagnostic_formats.rb +4 -1
  68. data/lib/rigor/cli/docs_command.rb +0 -30
  69. data/lib/rigor/cli/doctor_command.rb +163 -23
  70. data/lib/rigor/cli/lsp_command.rb +5 -0
  71. data/lib/rigor/cli/mcp_command.rb +5 -0
  72. data/lib/rigor/cli/plugins_command.rb +36 -6
  73. data/lib/rigor/cli/plugins_renderer.rb +44 -12
  74. data/lib/rigor/cli/probe_environment.rb +85 -0
  75. data/lib/rigor/cli/protection_fork_scan.rb +55 -0
  76. data/lib/rigor/cli/protection_report.rb +7 -1
  77. data/lib/rigor/cli/sig_gen_command.rb +36 -9
  78. data/lib/rigor/cli/skill_command.rb +1 -32
  79. data/lib/rigor/cli/trace_command.rb +5 -6
  80. data/lib/rigor/cli/type_of_command.rb +9 -8
  81. data/lib/rigor/cli/type_scan_command.rb +8 -8
  82. data/lib/rigor/cli.rb +15 -4
  83. data/lib/rigor/config_audit.rb +48 -2
  84. data/lib/rigor/configuration/severity_profile.rb +31 -0
  85. data/lib/rigor/configuration.rb +156 -8
  86. data/lib/rigor/environment/default_libraries.rb +35 -0
  87. data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
  88. data/lib/rigor/environment/rbs_loader.rb +164 -5
  89. data/lib/rigor/environment.rb +71 -43
  90. data/lib/rigor/inference/budget_trace.rb +77 -2
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
  92. data/lib/rigor/inference/def_handle.rb +23 -0
  93. data/lib/rigor/inference/def_node_resolver.rb +90 -0
  94. data/lib/rigor/inference/def_return_typer.rb +2 -1
  95. data/lib/rigor/inference/expression_typer.rb +379 -113
  96. data/lib/rigor/inference/fork_map.rb +87 -0
  97. data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
  98. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
  99. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
  100. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
  101. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
  102. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
  103. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
  104. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
  105. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
  106. data/lib/rigor/inference/method_dispatcher.rb +57 -1
  107. data/lib/rigor/inference/mutation_widening.rb +42 -8
  108. data/lib/rigor/inference/narrowing.rb +245 -17
  109. data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
  110. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  111. data/lib/rigor/inference/rbs_type_translator.rb +8 -1
  112. data/lib/rigor/inference/scope_indexer.rb +460 -49
  113. data/lib/rigor/inference/statement_evaluator.rb +111 -17
  114. data/lib/rigor/inference/struct_fold_safety.rb +48 -11
  115. data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
  116. data/lib/rigor/inference/void_origin.rb +25 -0
  117. data/lib/rigor/inference/void_tail_summary.rb +220 -0
  118. data/lib/rigor/language_server/completion_provider.rb +2 -1
  119. data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
  120. data/lib/rigor/language_server/folding_range_provider.rb +2 -1
  121. data/lib/rigor/language_server/selection_range_provider.rb +2 -1
  122. data/lib/rigor/language_server/signature_help_provider.rb +2 -1
  123. data/lib/rigor/plugin/base.rb +9 -22
  124. data/lib/rigor/plugin/inflector.rb +12 -3
  125. data/lib/rigor/plugin/io_boundary.rb +5 -1
  126. data/lib/rigor/plugin/isolation.rb +81 -11
  127. data/lib/rigor/plugin/load_error.rb +10 -1
  128. data/lib/rigor/plugin/loader.rb +96 -14
  129. data/lib/rigor/plugin/node_rule_walk.rb +5 -3
  130. data/lib/rigor/plugin/registry.rb +32 -23
  131. data/lib/rigor/plugin.rb +26 -0
  132. data/lib/rigor/protection/mutator.rb +3 -2
  133. data/lib/rigor/reflection.rb +64 -0
  134. data/lib/rigor/runtime/jit.rb +128 -0
  135. data/lib/rigor/scope/discovery_index.rb +12 -2
  136. data/lib/rigor/scope.rb +122 -28
  137. data/lib/rigor/sig_gen/classification.rb +5 -1
  138. data/lib/rigor/sig_gen/generator.rb +38 -6
  139. data/lib/rigor/sig_gen/observation_collector.rb +4 -3
  140. data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
  141. data/lib/rigor/sig_gen/renderer.rb +10 -0
  142. data/lib/rigor/sig_gen/write_result.rb +9 -4
  143. data/lib/rigor/sig_gen/writer.rb +24 -2
  144. data/lib/rigor/source/node_children.rb +116 -0
  145. data/lib/rigor/source/node_locator.rb +3 -1
  146. data/lib/rigor/source/node_walker.rb +4 -2
  147. data/lib/rigor/source.rb +1 -0
  148. data/lib/rigor/type/difference.rb +28 -24
  149. data/lib/rigor/type/hash_shape.rb +34 -10
  150. data/lib/rigor/version.rb +1 -1
  151. data/lib/rigortype.rb +24 -0
  152. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
  154. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +23 -8
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
  157. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
  158. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
  159. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
  160. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
  161. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
  162. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
  163. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
  164. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
  165. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
  166. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
  167. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
  168. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
  169. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
  170. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
  171. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
  172. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
  173. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
  174. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +191 -0
  175. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
  176. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +19 -1
  177. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
  178. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
  179. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
  180. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
  181. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
  182. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
  183. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
  184. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
  185. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
  186. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
  187. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
  188. data/sig/prism_node_children.rbs +9 -0
  189. data/sig/rigor/environment.rbs +1 -0
  190. data/sig/rigor/inference.rbs +7 -0
  191. data/sig/rigor/plugin/base.rbs +2 -3
  192. data/sig/rigor/reflection.rbs +1 -0
  193. data/sig/rigor/scope.rbs +12 -1
  194. data/skills/rigor-ci-setup/SKILL.md +10 -0
  195. data/skills/rigor-editor-setup/SKILL.md +11 -0
  196. data/skills/rigor-next-steps/SKILL.md +23 -2
  197. data/skills/rigor-plugin-author/SKILL.md +3 -3
  198. data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
  199. data/skills/rigor-plugin-review/SKILL.md +2 -2
  200. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
  201. data/skills/rigor-project-init/SKILL.md +29 -3
  202. data/skills/rigor-project-init/references/01-detect.md +1 -1
  203. data/skills/rigor-project-init/references/02-configure.md +39 -5
  204. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
  205. data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
  206. metadata +49 -24
  207. data/lib/rigor/cli/ci_detector.rb +0 -89
@@ -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
@@ -29,6 +29,7 @@ module Rigor
29
29
  def rbs_module?: (String | Symbol name) -> bool
30
30
  def class_ordering: (String | Symbol lhs, String | Symbol rhs) -> ordering
31
31
  def reflection: () -> untyped?
32
+ def missing_rbs_gem_owner: (String | Symbol root_constant_name) -> String?
32
33
 
33
34
  class ClassRegistry
34
35
  def self.default: () -> ClassRegistry
@@ -3,8 +3,10 @@ module Rigor
3
3
  type closure_classification = :non_escaping | :escaping | :unknown
4
4
 
5
5
  class ExpressionTyper
6
+ def self.harvest_return_memo: () -> Hash[untyped, Array[untyped]]
6
7
  def initialize: (scope: Scope, ?tracer: FallbackTracer?) -> void
7
8
  def type_of: (untyped node) -> Type::t
9
+ def return_type_for: (untyped def_node, Type::t receiver, Array[Type::t] arg_types) -> Type::t?
8
10
  def scope: () -> Scope
9
11
  def tracer: () -> FallbackTracer?
10
12
  def dynamic_top: () -> Type::Dynamic
@@ -27,6 +29,7 @@ module Rigor
27
29
 
28
30
  def initialize: (scope: Scope, ?tracer: FallbackTracer?, ?on_enter: (^(untyped, Scope) -> void)?, ?class_context: Array[ClassFrame]) -> void
29
31
  def evaluate: (untyped node) -> [Type::t, Scope]
32
+ def content_mutated_parameter_positions: (untyped def_node) -> Array[Integer]
30
33
  def scope: () -> Scope
31
34
  def tracer: () -> FallbackTracer?
32
35
  def sub_eval: (untyped node, Scope with_scope, ?class_context: Array[ClassFrame]) -> [Type::t, Scope]
@@ -163,6 +166,10 @@ module Rigor
163
166
  def self?.record_declarations: (untyped node, Array[String] qualified_prefix, Hash[untyped, Type::t] identity_table, Hash[String, Type::t] discovered) -> void
164
167
  def self?.discovered_classes_for_paths: (Array[String] paths, ?buffer: untyped) -> Hash[String, Type::t]
165
168
  def self?.discovered_def_index_for_paths: (Array[String] paths, ?buffer: untyped) -> Hash[Symbol, untyped]
169
+ def self?.scan_summary_for_paths: (Array[String] paths, ?buffer: untyped) -> Hash[Symbol, untyped]
170
+ def self?.code_fingerprint: (String source, untyped comments) -> String
171
+ def self?.discovered_project_index_for_paths: (Array[String] paths, ?buffer: untyped) -> Hash[Symbol, untyped]
172
+ def self?.discovered_project_index_incremental: (Array[String] paths, seed_bundles: Hash[String, untyped], ?buffer: untyped) -> Hash[Symbol, untyped]
166
173
  def self?.collect_class_decls: (untyped node, Array[String] qualified_prefix, Hash[String, Type::t] accumulator) -> void
167
174
  def self?.qualified_name_for: (untyped constant_path_node) -> String?
168
175
  def self?.render_constant_path: (untyped node) -> String
@@ -28,8 +28,7 @@ class Rigor::Plugin::Base
28
28
 
29
29
  def self.narrowing_facts: (methods: Array[untyped]) { (untyped call_node, untyped scope) -> untyped } -> nil
30
30
  # Deprecating alias for `narrowing_facts` (ADR-80); removed in 0.3.0.
31
- def self.type_specifier: (methods: Array[untyped]) { (untyped call_node, untyped scope) -> untyped } -> nil
32
- def self.type_specifiers: () -> Array[untyped]
31
+ def self.narrowing_facts_rules: () -> Array[untyped]
33
32
 
34
33
  def self.suggest: (untyped name, untyped candidates) -> String?
35
34
 
@@ -48,7 +47,7 @@ class Rigor::Plugin::Base
48
47
  # Engine-executed dispatch over the declared DSLs.
49
48
  def node_rule_diagnostics: (path: untyped, scope: untyped, root: untyped) -> Array[untyped]
50
49
  def dynamic_return_type: (call_node: untyped, scope: untyped, receiver_type: untyped) -> untyped
51
- def type_specifier_facts: (call_node: untyped, scope: untyped) -> Array[untyped]
50
+ def narrowing_facts_for: (call_node: untyped, scope: untyped) -> Array[untyped]
52
51
 
53
52
  # Authoring helpers.
54
53
  def diagnostic: (untyped node, path: untyped, message: untyped, ?severity: untyped, ?rule: untyped, ?location: untyped) -> untyped
@@ -6,6 +6,7 @@ module Rigor
6
6
  def self?.nominal_for_name: (String | Symbol class_name, ?scope: Scope) -> Type::Nominal?
7
7
  def self?.singleton_for_name: (String | Symbol class_name, ?scope: Scope) -> Type::Singleton?
8
8
  def self?.constant_type_for: (String | Symbol constant_name, ?scope: Scope) -> Type::t?
9
+ def self?.resolve_constant_type: (String | Symbol name, ?scope: Scope) -> Type::t?
9
10
  def self?.instance_method_definition: (String | Symbol class_name, String | Symbol method_name, ?scope: Scope) -> untyped
10
11
  def self?.singleton_method_definition: (String | Symbol class_name, String | Symbol method_name, ?scope: Scope) -> untyped
11
12
  def self?.instance_definition: (String | Symbol class_name, ?scope: Scope) -> untyped
data/sig/rigor/scope.rbs CHANGED
@@ -11,6 +11,7 @@ module Rigor
11
11
  attr_reader dynamic_origins: Hash[untyped, Symbol]
12
12
  attr_reader local_origins: Hash[Symbol, Symbol]
13
13
  attr_reader ivar_origins: Hash[Symbol, Symbol]
14
+ attr_reader void_origins: Hash[untyped, Inference::VoidOrigin]
14
15
  attr_reader indexed_narrowings: Hash[IndexedKey, Type::t]
15
16
  attr_reader method_chain_narrowings: Hash[ChainKey, Type::t]
16
17
  attr_reader source_path: String?
@@ -28,6 +29,7 @@ module Rigor
28
29
  def discovered_def_nodes: () -> Hash[String, Hash[Symbol, untyped]]
29
30
  def discovered_singleton_def_nodes: () -> Hash[String, Hash[Symbol, untyped]]
30
31
  def discovered_def_sources: () -> Hash[String, Hash[Symbol, String]]
32
+ def discovered_singleton_def_sources: () -> Hash[String, Hash[Symbol, String]]
31
33
  def discovered_method_visibilities: () -> Hash[String, Hash[Symbol, Symbol]]
32
34
  def discovered_superclasses: () -> Hash[String, String]
33
35
  def discovered_includes: () -> Hash[String, Array[String]]
@@ -35,8 +37,11 @@ module Rigor
35
37
  def data_member_layouts: () -> Hash[String, Array[Symbol]]
36
38
  def struct_member_layouts: () -> Hash[String, { members: Array[Symbol], keyword_init: bool }]
37
39
  def param_inferred_types: () -> Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]
40
+ def run_generation: () -> Object?
38
41
  def record_dynamic_origin: (untyped node, Symbol cause) -> Scope
39
42
 
43
+ def record_void_origin: (untyped node, Inference::VoidOrigin origin) -> Scope
44
+
40
45
  class DiscoveryIndex
41
46
  attr_reader declared_types: Hash[untyped, Type::t]
42
47
  attr_reader class_ivars: Hash[String, Hash[Symbol, Type::t]]
@@ -48,6 +53,7 @@ module Rigor
48
53
  attr_reader discovered_def_nodes: Hash[String, Hash[Symbol, untyped]]
49
54
  attr_reader discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]]
50
55
  attr_reader discovered_def_sources: Hash[String, Hash[Symbol, String]]
56
+ attr_reader discovered_singleton_def_sources: Hash[String, Hash[Symbol, String]]
51
57
  attr_reader discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]]
52
58
  attr_reader discovered_superclasses: Hash[String, String]
53
59
  attr_reader discovered_includes: Hash[String, Array[String]]
@@ -55,10 +61,11 @@ module Rigor
55
61
  attr_reader data_member_layouts: Hash[String, Array[Symbol]]
56
62
  attr_reader struct_member_layouts: Hash[String, { members: Array[Symbol], keyword_init: bool }]
57
63
  attr_reader param_inferred_types: Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]
64
+ attr_reader run_generation: Object?
58
65
 
59
66
  EMPTY: DiscoveryIndex
60
67
 
61
- def with: (?declared_types: Hash[untyped, Type::t], ?class_ivars: Hash[String, Hash[Symbol, Type::t]], ?class_cvars: Hash[String, Hash[Symbol, Type::t]], ?program_globals: Hash[Symbol, Type::t], ?discovered_classes: Hash[String, Type::Singleton], ?in_source_constants: Hash[String, Type::t], ?discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?discovered_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]], ?discovered_superclasses: Hash[String, String], ?discovered_includes: Hash[String, Array[String]], ?discovered_class_sources: Hash[String, Set[String]], ?data_member_layouts: Hash[String, Array[Symbol]], ?struct_member_layouts: Hash[String, { members: Array[Symbol], keyword_init: bool }], ?param_inferred_types: Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]) -> DiscoveryIndex
68
+ def with: (?declared_types: Hash[untyped, Type::t], ?class_ivars: Hash[String, Hash[Symbol, Type::t]], ?class_cvars: Hash[String, Hash[Symbol, Type::t]], ?program_globals: Hash[Symbol, Type::t], ?discovered_classes: Hash[String, Type::Singleton], ?in_source_constants: Hash[String, Type::t], ?discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?discovered_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_singleton_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]], ?discovered_superclasses: Hash[String, String], ?discovered_includes: Hash[String, Array[String]], ?discovered_class_sources: Hash[String, Set[String]], ?data_member_layouts: Hash[String, Array[Symbol]], ?struct_member_layouts: Hash[String, { members: Array[Symbol], keyword_init: bool }], ?param_inferred_types: Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]], ?run_generation: Object?) -> DiscoveryIndex
62
69
  end
63
70
 
64
71
  class IndexedKey
@@ -97,6 +104,9 @@ module Rigor
97
104
  def with_declaration_sourced_local: (String | Symbol name, Type::t type) -> Scope
98
105
  def with_local_declaration_mark: (String | Symbol name) -> Scope
99
106
  def declaration_sourced?: (Symbol kind, String | Symbol name) -> bool
107
+ def with_inferred_param_mark: (String | Symbol name) -> Scope
108
+ def without_inferred_param_mark: (String | Symbol name) -> Scope
109
+ def inferred_param?: (String | Symbol name) -> bool
100
110
  def forget_match_globals: () -> Scope
101
111
  def class_ivars_for: (String | Symbol? class_name) -> Hash[Symbol, Type::t]
102
112
  def class_cvars_for: (String | Symbol? class_name) -> Hash[Symbol, Type::t]
@@ -125,6 +135,7 @@ module Rigor
125
135
  def local_facts: (String | Symbol name, ?bucket: Symbol?) -> Array[Analysis::FactStore::Fact]
126
136
  def type_of: (untyped node, ?tracer: Inference::FallbackTracer?) -> Type::t
127
137
  def evaluate: (untyped node, ?tracer: Inference::FallbackTracer?) -> [Type::t, Scope]
138
+ def user_method_return: (untyped def_node, Type::t receiver, Array[Type::t] arg_types) -> Type::t?
128
139
  def join: (Scope other) -> Scope
129
140
  def ==: (untyped other) -> bool
130
141
  alias eql? ==
@@ -141,6 +141,16 @@ The exact recipe (the `BUNDLE_GEMFILE` wiring, the Dependabot entry) is in
141
141
  role.
142
142
  - **Determinism.** Add `--no-cache` in CI if you want each run independent
143
143
  of any persisted `.rigor/cache`.
144
+ - **Cache persistence (opposite trade).** When the Rigor job's runtime
145
+ matters, persist `.rigor/cache` with the CI's cache facility. Current
146
+ Rigor detects CI and validates the restored cache by content hash
147
+ automatically (`cache.validation: auto`); on versions before the
148
+ `auto` default, **also set `RIGOR_STRICT_VALIDATION=1` on the job** —
149
+ without it a fresh checkout's regenerated stat tuples make the plugin
150
+ watch-glob cache slots read as stale on every run. A self-hosted
151
+ runner that reuses its workspace opts back into the faster stat check
152
+ with `cache.validation: stat`. Snippets: `rigor docs ci`
153
+ § "Persisting the analysis cache across runs".
144
154
 
145
155
  ## Verify
146
156
 
@@ -89,6 +89,17 @@ The manual chapter covers:
89
89
  - **Helix** (`languages.toml`)
90
90
  - **Emacs** (Eglot and lsp-mode)
91
91
 
92
+ **The editors are not equally cheap to wire.** Neovim / Helix / Emacs
93
+ are plain config snippets. **VS Code has no first-party Rigor
94
+ extension yet** — its path means authoring and side-loading a minimal
95
+ extension (a `LanguageClient` wrapper), which is a development task,
96
+ not a `settings.json` edit. Say so *before* starting: the developer
97
+ may prefer to wait for a first-class extension, and an autonomous
98
+ agent should treat the VS Code path as a **user handoff** (report the
99
+ manual's recipe; do not scaffold an unrequested extension). Phase 4's
100
+ verification (live diagnostics, hover) also requires a human in front
101
+ of the editor — an agent stops there and reports, for every editor.
102
+
92
103
  ### Phase 3 — apply the matching config
93
104
 
94
105
  Open the manual's **Editor wiring** section and apply the snippet for