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.
- checksums.yaml +4 -4
- data/README.md +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules.rb +34 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +160 -1190
- data/lib/rigor/analysis/worker_session.rb +47 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/check_command.rb +705 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli.rb +15 -614
- data/lib/rigor/configuration.rb +9 -6
- data/lib/rigor/environment/rbs_loader.rb +53 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/expression_typer.rb +28 -62
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher.rb +115 -54
- data/lib/rigor/inference/narrowing.rb +60 -0
- data/lib/rigor/inference/scope_indexer.rb +75 -15
- data/lib/rigor/inference/statement_evaluator.rb +35 -52
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +282 -41
- data/lib/rigor/plugin/node_rule_walk.rb +147 -0
- data/lib/rigor/plugin/registry.rb +263 -35
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
- data/lib/rigor/scope/discovery_index.rb +58 -0
- data/lib/rigor/scope.rb +67 -198
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/type/combinator.rb +5 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +1 -2
- data/sig/rigor/scope.rbs +41 -29
- data/sig/rigor/source.rbs +1 -0
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- metadata +15 -2
- 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`
|
|
44
|
-
# Ractor
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -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)
|
|
21
|
-
# treated as cache misses; the producer block reruns
|
|
22
|
-
# next write replaces the bad entry. The trailing SHA-256
|
|
23
|
-
# accidental corruption (partial writes, FS errors); it is
|
|
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.
|
|
28
|
-
|
|
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(
|
|
381
|
+
deserialize.call(raw)
|
|
354
382
|
else
|
|
355
|
-
Marshal.load(
|
|
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 =
|
|
447
|
+
current = self.class.schema_marker_value
|
|
415
448
|
|
|
416
449
|
if File.file?(marker)
|
|
417
450
|
on_disk = File.read(marker).strip
|