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
@@ -16,6 +16,15 @@ module Rigor
16
16
  KEY = :__rigor_dependency_recorder__
17
17
  private_constant :KEY
18
18
 
19
+ # ADR-84 WD2 — thread-local Array of active {Capture}s (nil when none). A capture observes-and-forwards:
20
+ # every read recorded while it is active flows into the current consumer's accumulator exactly as before
21
+ # AND into every active capture, so a caller gets a replayable read-set without disturbing recording.
22
+ # It is a STACK because captures nest (a memoised callee body evaluating another memo-miss callee): an
23
+ # outer capture must also see the inner window's reads, or its read-set would be non-transitive and a
24
+ # later replay would drop the nested callee's edges.
25
+ CAPTURE_KEY = :__rigor_dependency_capture_stack__
26
+ private_constant :CAPTURE_KEY
27
+
19
28
  # Mutable per-consumer accumulator. Frozen into a {Record} snapshot when `record_for` returns.
20
29
  class Accumulator
21
30
  attr_reader :consumer, :sources, :missing, :symbol_sources, :ancestry_sources
@@ -49,6 +58,27 @@ module Rigor
49
58
  # `ancestry_sources`: frozen Set<source_path> (class-ancestry edges, file-granularity).
50
59
  Record = Data.define(:consumer, :sources, :missing, :symbol_sources, :ancestry_sources)
51
60
 
61
+ # ADR-84 WD2 — a replayable, consumer-independent read-set: `reads` is a frozen Set of frozen
62
+ # `[path, symbol_or_nil]` pairs (symbol nil = ancestry edge), `missing` a frozen Set of `"kind:name"`
63
+ # strings. Deliberately UNFILTERED by consumer: a read of the capturing consumer's own file is included
64
+ # because it is a genuine cross-file edge for every OTHER consumer a replay may serve — the per-consumer
65
+ # self-read filter is applied at {replay} time, mirroring {read_site}.
66
+ ReadSet = Data.define(:reads, :missing)
67
+
68
+ # Mutable capture accumulator; snapshot into a frozen {ReadSet} when the capture window closes.
69
+ class Capture
70
+ attr_reader :reads, :missing
71
+
72
+ def initialize
73
+ @reads = Set.new
74
+ @missing = Set.new
75
+ end
76
+
77
+ def snapshot
78
+ ReadSet.new(reads: reads.dup.freeze, missing: missing.dup.freeze)
79
+ end
80
+ end
81
+
52
82
  # Module-level activation count so the disabled fast path ({active?}) is a plain integer read rather
53
83
  # than a `Thread.current` hash lookup — `user_def_for` (the instrumented accessor) is on the
54
84
  # per-dispatch hot path, so a normal (non-recording) run must pay as little as possible. The per-thread
@@ -79,32 +109,86 @@ module Rigor
79
109
  @active_count.positive?
80
110
  end
81
111
 
112
+ # ADR-84 WD2 — runs the block under an observe-and-forward capture window and returns
113
+ # `[block_result, read_set]`. Reads recorded during the window reach the current consumer's accumulator
114
+ # unchanged (forwarding) and are additionally collected into the returned frozen {ReadSet} (observing),
115
+ # which {replay} can later apply to a different consumer. Nests (see CAPTURE_KEY). Callers guard on
116
+ # {active?}; opening a capture with no accumulator active would collect reads no run is recording.
117
+ def capture
118
+ captures = (Thread.current[CAPTURE_KEY] ||= [])
119
+ capture = Capture.new
120
+ captures.push(capture)
121
+ begin
122
+ result = yield
123
+ ensure
124
+ captures.pop
125
+ end
126
+ [result, capture.snapshot]
127
+ end
128
+
129
+ # ADR-84 WD2 — replays a {ReadSet} into the current consumer's accumulator as if each read happened
130
+ # here, applying the same per-consumer self-read filter as {read_site}, and tees into every active
131
+ # capture so an enclosing capture window stays transitive when a memo hit substitutes for a body walk.
132
+ def replay(read_set)
133
+ accumulator = Thread.current[KEY]
134
+ return if accumulator.nil? || read_set.nil?
135
+
136
+ captures = Thread.current[CAPTURE_KEY]
137
+ read_set.reads.each do |pair|
138
+ captures&.each { |c| c.reads.add(pair) }
139
+ path, symbol = pair
140
+ next if path == accumulator.consumer
141
+
142
+ accumulate_read(accumulator, path, symbol)
143
+ end
144
+ read_set.missing.each do |entry|
145
+ captures&.each { |c| c.missing.add(entry) }
146
+ accumulator.missing << entry
147
+ end
148
+ end
149
+
82
150
  # Records that the current consumer read a declaration / body whose definition site is `path_line` (a
83
151
  # `"path:line"` String, or nil). When `symbol` is given (a `"ClassName#method"` String), the read is a
84
152
  # method-call edge and is recorded at symbol granularity in `symbol_sources` in addition to the coarse
85
153
  # `sources` set. Without `symbol` the read is a class-ancestry edge (file-granularity) and is added to
86
- # `ancestry_sources` only. Self-reads and nil sites are ignored in all cases.
154
+ # `ancestry_sources` only. Self-reads and nil sites are ignored for the accumulator; active captures
155
+ # (ADR-84 WD2) observe every valid-path read UNfiltered — the self-read filter is per-consumer and is
156
+ # re-applied by {replay} against whichever consumer a replay serves.
87
157
  def read_site(path_line, symbol = nil)
88
158
  accumulator = Thread.current[KEY]
89
159
  return if accumulator.nil? || path_line.nil?
90
160
 
91
161
  path = path_line.split(":", 2).first
92
- return unless path && path != accumulator.consumer
162
+ return unless path
93
163
 
94
- accumulator.sources << path
95
- if symbol
96
- accumulator.symbol_sources[path] << symbol
97
- else
98
- accumulator.ancestry_sources << path
99
- end
164
+ captures = Thread.current[CAPTURE_KEY]
165
+ captures&.each { |c| c.reads.add([path, symbol].freeze) }
166
+ return if path == accumulator.consumer
167
+
168
+ accumulate_read(accumulator, path, symbol)
100
169
  end
101
170
 
102
171
  # Records a cross-file lookup of `name` (kind `:method` / `:class` / `:const` / …) that resolved to
103
172
  # nothing — a negative dependency.
104
173
  def read_missing(kind, name)
105
174
  accumulator = Thread.current[KEY]
106
- accumulator&.missing&.add("#{kind}:#{name}")
175
+ return if accumulator.nil?
176
+
177
+ entry = "#{kind}:#{name}"
178
+ Thread.current[CAPTURE_KEY]&.each { |c| c.missing.add(entry) }
179
+ accumulator.missing.add(entry)
180
+ end
181
+
182
+ # The positive-edge aggregation shared by {read_site} and {replay}.
183
+ def accumulate_read(accumulator, path, symbol)
184
+ accumulator.sources << path
185
+ if symbol
186
+ accumulator.symbol_sources[path] << symbol
187
+ else
188
+ accumulator.ancestry_sources << path
189
+ end
107
190
  end
191
+ private_class_method :accumulate_read
108
192
  end
109
193
  end
110
194
  end
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "return_type_heuristic"
6
6
  require_relative "../../source/constant_path"
7
+ require_relative "../../source/node_children"
7
8
 
8
9
  module Rigor
9
10
  module Analysis
@@ -125,7 +126,7 @@ module Rigor
125
126
  end
126
127
 
127
128
  def walk_children(node, qualified_prefix, in_singleton_class, accumulator, budget)
128
- node.compact_child_nodes.each do |child|
129
+ node.rigor_each_child do |child|
129
130
  break if accumulator.size >= budget
130
131
 
131
132
  walk_node(child, qualified_prefix, in_singleton_class, accumulator, budget)