rigortype 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
  4. data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
  5. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +18 -1
  6. data/lib/rigor/analysis/check_rules.rb +34 -6
  7. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
  8. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  9. data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
  10. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  11. data/lib/rigor/analysis/runner.rb +160 -1190
  12. data/lib/rigor/analysis/worker_session.rb +47 -8
  13. data/lib/rigor/cache/incremental_snapshot.rb +10 -4
  14. data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
  15. data/lib/rigor/cache/store.rb +46 -13
  16. data/lib/rigor/cli/check_command.rb +705 -0
  17. data/lib/rigor/cli/ci_detector.rb +94 -0
  18. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  19. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  20. data/lib/rigor/cli/trace_command.rb +143 -0
  21. data/lib/rigor/cli/trace_renderer.rb +310 -0
  22. data/lib/rigor/cli.rb +15 -614
  23. data/lib/rigor/configuration.rb +9 -6
  24. data/lib/rigor/environment/rbs_loader.rb +53 -68
  25. data/lib/rigor/environment.rb +1 -1
  26. data/lib/rigor/inference/acceptance.rb +10 -0
  27. data/lib/rigor/inference/expression_typer.rb +28 -62
  28. data/lib/rigor/inference/flow_tracer.rb +180 -0
  29. data/lib/rigor/inference/macro_block_self_type.rb +10 -11
  30. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  31. data/lib/rigor/inference/method_dispatcher.rb +115 -54
  32. data/lib/rigor/inference/narrowing.rb +60 -0
  33. data/lib/rigor/inference/scope_indexer.rb +75 -15
  34. data/lib/rigor/inference/statement_evaluator.rb +35 -52
  35. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  36. data/lib/rigor/plugin/base.rb +282 -41
  37. data/lib/rigor/plugin/node_rule_walk.rb +147 -0
  38. data/lib/rigor/plugin/registry.rb +263 -35
  39. data/lib/rigor/plugin.rb +1 -0
  40. data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
  41. data/lib/rigor/scope/discovery_index.rb +58 -0
  42. data/lib/rigor/scope.rb +67 -198
  43. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  44. data/lib/rigor/source/literals.rb +14 -0
  45. data/lib/rigor/type/combinator.rb +5 -0
  46. data/lib/rigor/version.rb +1 -1
  47. data/lib/rigor.rb +0 -1
  48. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  49. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  50. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
  51. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  52. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
  53. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  54. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  55. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  56. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  57. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  58. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
  59. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  60. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  61. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  62. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
  63. data/sig/rigor/environment.rbs +0 -2
  64. data/sig/rigor/inference.rbs +5 -0
  65. data/sig/rigor/plugin/base.rbs +1 -2
  66. data/sig/rigor/scope.rbs +41 -29
  67. data/sig/rigor/source.rbs +1 -0
  68. data/skills/rigor-ci-setup/SKILL.md +319 -0
  69. metadata +15 -2
  70. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
@@ -39,12 +39,19 @@ module Rigor
39
39
  # - `plugin_blueprints` — Phase 3a
40
40
  # (`Array<Plugin::Blueprint>` is `Ractor.shareable?`).
41
41
  # - `explain` — Boolean.
42
- # - `synthetic_method_index` / `project_patched_methods`
43
- # optional (default `nil`). NOT `Ractor.shareable?`, so the
44
- # Ractor pool path leaves them unset; the fork backend
42
+ # - `synthetic_method_index` / `project_patched_methods` /
43
+ # `project_scope_seed` — optional (default `nil` / `{}`). NOT
44
+ # `Ractor.shareable?` (the seed tables carry Prism def nodes),
45
+ # so the Ractor pool path leaves them unset; the fork backend
45
46
  # (ADR-15 Amendment), which builds the session pre-fork on the
46
47
  # parent, threads the runner's project-scan results through so
47
48
  # per-file inference matches the sequential path exactly.
49
+ # `project_scope_seed` is `Runner#project_scope_seed_tables` —
50
+ # the cross-file discovery tables `seed_project_scope` applies
51
+ # to every per-file scope on the sequential path; without it a
52
+ # worker cannot resolve calls to methods defined in OTHER
53
+ # project files and emits `call.undefined-method` false
54
+ # positives the sequential path does not.
48
55
  #
49
56
  # Internally the session OWNS (and never shares):
50
57
  #
@@ -97,13 +104,14 @@ module Rigor
97
104
  def initialize(configuration:, cache_store: nil, # rubocop:disable Metrics/MethodLength,Metrics/ParameterLists
98
105
  plugin_blueprints: [], explain: false, buffer: nil,
99
106
  synthetic_method_index: nil, project_patched_methods: nil,
100
- source_files: [])
107
+ project_scope_seed: {}, source_files: [])
101
108
  @configuration = configuration
102
109
  @cache_store = cache_store
103
110
  @explain = explain
104
111
  @buffer = buffer
105
112
  @synthetic_method_index = synthetic_method_index
106
113
  @project_patched_methods = project_patched_methods
114
+ @project_scope_seed = project_scope_seed || {}
107
115
  # ADR-32 WD4 — full project file list (frozen
108
116
  # Array<String>) for env-build-time invocation of any
109
117
  # loaded plugin's `source_rbs_synthesizer` callable.
@@ -165,7 +173,7 @@ module Rigor
165
173
  return parse_diagnostics(path, parse_result)
166
174
  end
167
175
 
168
- scope = Scope.empty(environment: @environment, source_path: path)
176
+ scope = seed_project_scope(Scope.empty(environment: @environment, source_path: path))
169
177
  index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
170
178
  diagnostics = CheckRules.diagnose(
171
179
  path: path,
@@ -200,6 +208,17 @@ module Rigor
200
208
 
201
209
  private
202
210
 
211
+ # Mirrors {Runner#seed_project_scope}: applies the cross-file
212
+ # pre-pass discovery tables the constructor received (fork
213
+ # backend only — see the class comment) to a fresh per-file
214
+ # scope, so worker-side inference resolves project-internal
215
+ # cross-file calls exactly like the sequential path.
216
+ def seed_project_scope(scope)
217
+ return scope if @project_scope_seed.empty?
218
+
219
+ scope.with_discovery(scope.discovery.with(**@project_scope_seed))
220
+ end
221
+
203
222
  # See {Runner#parse_source}. Same contract: if `@buffer`
204
223
  # binds `path` to a physical file, read the physical bytes
205
224
  # but stamp the parse buffer's `filepath:` as the LOGICAL
@@ -278,14 +297,34 @@ module Rigor
278
297
  def plugin_emitted_diagnostics(path, root, scope)
279
298
  return [] if @plugin_registry.empty?
280
299
 
300
+ # ADR-52 WD4 — single engine-owned node-rule walk per file; the
301
+ # results are bucketed per plugin (registry order) so emission
302
+ # stays plugin-major and byte-identical with the per-plugin walk.
303
+ node_results = node_rule_results_by_plugin(path, root, scope)
304
+
281
305
  @plugin_registry.plugins.flat_map do |plugin|
282
- collect_plugin_diagnostics(plugin, path, root, scope)
306
+ collect_plugin_diagnostics(plugin, path, root, scope, node_results[plugin])
283
307
  end
284
308
  end
285
309
 
286
- def collect_plugin_diagnostics(plugin, path, root, scope)
310
+ def node_rule_results_by_plugin(path, root, scope)
311
+ walk = @plugin_registry.node_rule_walk
312
+ return {}.compare_by_identity if walk.empty?
313
+
314
+ results = walk.diagnostics_for_file(path: path, scope: scope, root: root)
315
+ results.each_with_object({}.compare_by_identity) do |result, by_plugin|
316
+ by_plugin[result.plugin] = result
317
+ end
318
+ end
319
+
320
+ def collect_plugin_diagnostics(plugin, path, root, scope, node_result)
287
321
  raw = Array(plugin.diagnostics_for_file(path: path, scope: scope, root: root))
288
- raw += plugin.node_rule_diagnostics(path: path, scope: scope, root: root)
322
+ # A node-rule context/rule raise isolates the whole plugin's
323
+ # node-rule contribution, matching the old combined per-plugin
324
+ # rescue (which discarded `diagnostics_for_file` output too).
325
+ raise node_result.error if node_result&.error
326
+
327
+ raw += node_result.diagnostics if node_result
289
328
  raw.map { |diagnostic| stamp_plugin_diagnostic(diagnostic, plugin.manifest.id) }
290
329
  rescue StandardError => e
291
330
  [plugin_runtime_error_diagnostic(path, plugin, e)]
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "fileutils"
4
4
  require "digest"
5
+ require "zlib"
5
6
 
6
7
  module Rigor
7
8
  module Cache
@@ -28,8 +29,12 @@ module Rigor
28
29
  # is cold). A cache must never break a run (the ADR-45 invariant).
29
30
  class IncrementalSnapshot
30
31
  # Bump when the on-disk shape changes so stale snapshots are ignored
31
- # rather than mis-deserialized.
32
- SCHEMA = 4
32
+ # rather than mis-deserialized. 5: the blob is zlib-deflated
33
+ # (ADR-54 WD2 parity with `Store` entries — the snapshot is the
34
+ # one cache artefact that does not go through `Store`); a raw
35
+ # pre-5 blob fails the inflate and loads as nil, the usual
36
+ # fault-tolerant cold-run path.
37
+ SCHEMA = 5
33
38
 
34
39
  # The persisted per-file state.
35
40
  # `cache` maps an analyzed file to its diagnostics.
@@ -103,7 +108,7 @@ module Rigor
103
108
  # The stored {Payload}, or nil when absent / unreadable / schema or
104
109
  # fingerprint mismatch / corrupt. Never raises.
105
110
  def load(fingerprint:)
106
- data = Marshal.load(File.binread(@path)) # rubocop:disable Security/MarshalLoad
111
+ data = Marshal.load(Zlib::Inflate.inflate(File.binread(@path))) # rubocop:disable Security/MarshalLoad
107
112
  return nil unless data.is_a?(Hash) && data[:schema] == SCHEMA && data[:fingerprint] == fingerprint
108
113
 
109
114
  Payload.new(
@@ -125,7 +130,7 @@ module Rigor
125
130
  # raises).
126
131
  def save(fingerprint:, payload:)
127
132
  FileUtils.mkdir_p(File.dirname(@path))
128
- blob = Marshal.dump(
133
+ raw = Marshal.dump(
129
134
  schema: SCHEMA, fingerprint: fingerprint,
130
135
  cache: payload.cache, sources: payload.sources,
131
136
  digests: payload.digests, analyzed: payload.analyzed,
@@ -135,6 +140,7 @@ module Rigor
135
140
  missing: payload.missing,
136
141
  class_decls: payload.class_decls
137
142
  )
143
+ blob = Zlib::Deflate.deflate(raw)
138
144
  tmp = "#{@path}.#{Process.pid}.tmp"
139
145
  File.binwrite(tmp, blob)
140
146
  File.rename(tmp, @path)
@@ -20,7 +20,11 @@ module Rigor
20
20
  # structural contract.
21
21
  class RbsCacheProducer
22
22
  def self.fetch(loader:, store:)
23
- descriptor = RbsDescriptor.build(loader)
23
+ # ADR-54 WD4 — the descriptor is identical for every producer
24
+ # consulting the same loader (same sig files, same libraries),
25
+ # so the loader memoises one build per process instead of
26
+ # re-digesting every .rbs file once per producer.
27
+ descriptor = loader.rbs_cache_descriptor
24
28
  store.fetch_or_compute(producer_id: self::PRODUCER_ID, params: {}, descriptor: descriptor) do
25
29
  compute(loader)
26
30
  end
@@ -5,6 +5,7 @@ require "fileutils"
5
5
  require "json"
6
6
  require "monitor"
7
7
  require "securerandom"
8
+ require "zlib"
8
9
 
9
10
  require_relative "descriptor"
10
11
 
@@ -17,17 +18,27 @@ module Rigor
17
18
  # and nothing else.
18
19
  #
19
20
  # Read failures (missing file, bad magic, format-version mismatch,
20
- # corrupt SHA-256 trailer, unmarshal-able payload) are silently
21
- # treated as cache misses; the producer block reruns and the
22
- # next write replaces the bad entry. The trailing SHA-256 catches
23
- # accidental corruption (partial writes, FS errors); it is **not**
24
- # a security boundary, per ADR-2's trusted-gem trust model.
21
+ # corrupt SHA-256 trailer, un-inflatable or unmarshal-able payload)
22
+ # are silently treated as cache misses; the producer block reruns
23
+ # and the next write replaces the bad entry. The trailing SHA-256
24
+ # catches accidental corruption (partial writes, FS errors); it is
25
+ # **not** a security boundary, per ADR-2's trusted-gem trust model.
25
26
  class Store # rubocop:disable Metrics/ClassLength
27
+ # On-disk byte-layout version. Bumped on incompatible format
28
+ # changes (independent of {Descriptor::SCHEMA_VERSION}, which
29
+ # covers the descriptor schema rather than the byte layout).
30
+ # v2 (ADR-54 WD2): the value payload is zlib-deflated on write
31
+ # and inflated on read — Marshal blobs compress to 13–16 % at
32
+ # an inflate cost an order of magnitude below their
33
+ # `Marshal.load`. v1 entries fail the header check and read as
34
+ # silent misses; the `schema_version.txt` marker additionally
35
+ # carries this version, so the first writable run after a bump
36
+ # clears the root and reclaims the unreadable bytes.
37
+ FORMAT_VERSION = 2
38
+
26
39
  # Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte
27
- # format version. Bumped on incompatible on-disk format changes
28
- # (independent of {Descriptor::SCHEMA_VERSION}, which covers
29
- # the descriptor schema rather than the byte layout).
30
- HEADER = "RIGOR\x00\x01".b.freeze
40
+ # format version.
41
+ HEADER = "RIGOR\x00#{FORMAT_VERSION.chr}".b.freeze
31
42
 
32
43
  VALID_PRODUCER_ID = /\A[a-z][a-z0-9._-]*\z/
33
44
 
@@ -48,6 +59,7 @@ module Rigor
48
59
  @root = root.to_s.dup.freeze
49
60
  @read_only = read_only
50
61
  @max_bytes = max_bytes&.then { |n| Integer(n) }
62
+ @schema_version_ensured = false
51
63
  @hits = 0
52
64
  @misses = 0
53
65
  @writes = 0
@@ -107,6 +119,18 @@ module Rigor
107
119
  # When the root does not exist or has no schema-version
108
120
  # marker, `schema_version` is nil and the producer list is
109
121
  # empty.
122
+ # The `schema_version.txt` marker content. Covers BOTH
123
+ # invalidation axes: the descriptor schema and the on-disk byte
124
+ # layout ({FORMAT_VERSION}, ADR-54). A format bump leaves the
125
+ # old entries permanently unreadable (header mismatch → miss)
126
+ # but, alone, would never reclaim their bytes — they can sit
127
+ # below the eviction cap forever. Folding the format version
128
+ # into the marker routes the bump through the established
129
+ # clear-the-root path instead.
130
+ def self.schema_marker_value
131
+ "#{Descriptor::SCHEMA_VERSION}.#{FORMAT_VERSION}"
132
+ end
133
+
110
134
  def self.disk_inventory(root:)
111
135
  root_s = root.to_s
112
136
  marker = File.join(root_s, "schema_version.txt")
@@ -348,11 +372,15 @@ module Rigor
348
372
  LOAD_FAILED = Object.new.freeze
349
373
  private_constant :LOAD_FAILED
350
374
 
375
+ # Inflates the stored value payload (ADR-54 WD2), then hands the
376
+ # raw bytes to the deserialiser. Any failure — corrupt deflate
377
+ # stream included — reads as a miss.
351
378
  def safe_load(bytes, deserialize)
379
+ raw = Zlib::Inflate.inflate(bytes)
352
380
  if deserialize
353
- deserialize.call(bytes)
381
+ deserialize.call(raw)
354
382
  else
355
- Marshal.load(bytes) # rubocop:disable Security/MarshalLoad
383
+ Marshal.load(raw) # rubocop:disable Security/MarshalLoad
356
384
  end
357
385
  rescue StandardError
358
386
  LOAD_FAILED
@@ -362,7 +390,7 @@ module Rigor
362
390
  FileUtils.mkdir_p(File.dirname(path))
363
391
 
364
392
  descriptor_bytes = descriptor.to_canonical_bytes
365
- value_bytes = serialize_value(value, serialize)
393
+ value_bytes = Zlib::Deflate.deflate(serialize_value(value, serialize))
366
394
 
367
395
  body = +"".b
368
396
  body << HEADER
@@ -408,10 +436,15 @@ module Rigor
408
436
  # never collides with a read under the old). The next
409
437
  # writable run will repair the cache.
410
438
  return if @read_only
439
+ # The marker is process-stable; one check per Store is
440
+ # enough (a benign double-check under a thread race just
441
+ # repeats idempotent work).
442
+ return if @schema_version_ensured
411
443
 
444
+ @schema_version_ensured = true
412
445
  FileUtils.mkdir_p(@root)
413
446
  marker = File.join(@root, "schema_version.txt")
414
- current = Descriptor::SCHEMA_VERSION.to_s
447
+ current = self.class.schema_marker_value
415
448
 
416
449
  if File.file?(marker)
417
450
  on_disk = File.read(marker).strip