rigortype 0.0.8 → 0.0.9
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 +195 -21
- data/data/builtins/ruby_core/encoding.yml +210 -0
- data/data/builtins/ruby_core/exception.yml +641 -0
- data/data/builtins/ruby_core/numeric.yml +3 -2
- data/data/builtins/ruby_core/proc.yml +731 -0
- data/data/builtins/ruby_core/random.yml +166 -0
- data/data/builtins/ruby_core/re.yml +689 -0
- data/data/builtins/ruby_core/struct.yml +449 -0
- data/lib/rigor/analysis/runner.rb +19 -3
- data/lib/rigor/builtins/imported_refinements.rb +6 -1
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +63 -0
- data/lib/rigor/cache/rbs_class_type_param_names.rb +60 -0
- data/lib/rigor/cache/rbs_constant_table.rb +15 -51
- data/lib/rigor/cache/rbs_descriptor.rb +53 -0
- data/lib/rigor/cache/rbs_environment.rb +52 -0
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +40 -0
- data/lib/rigor/cache/rbs_known_class_names.rb +43 -0
- data/lib/rigor/cache/store.rb +79 -15
- data/lib/rigor/cli.rb +36 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
- data/lib/rigor/environment/rbs_loader.rb +137 -25
- data/lib/rigor/environment.rb +11 -2
- data/lib/rigor/flow_contribution.rb +128 -0
- data/lib/rigor/inference/builtins/encoding_catalog.rb +67 -0
- data/lib/rigor/inference/builtins/exception_catalog.rb +92 -0
- data/lib/rigor/inference/builtins/proc_catalog.rb +122 -0
- data/lib/rigor/inference/builtins/random_catalog.rb +58 -0
- data/lib/rigor/inference/builtins/re_catalog.rb +81 -0
- data/lib/rigor/inference/builtins/struct_catalog.rb +55 -0
- data/lib/rigor/inference/expression_typer.rb +26 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +29 -14
- data/lib/rigor/rbs_extended.rb +55 -0
- data/lib/rigor/type/combinator.rb +72 -0
- data/lib/rigor/type/refined.rb +50 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +6 -0
- data/sig/rigor.rbs +3 -1
- metadata +21 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "rbs_descriptor"
|
|
4
4
|
|
|
5
5
|
module Rigor
|
|
6
6
|
module Cache
|
|
@@ -12,16 +12,9 @@ module Rigor
|
|
|
12
12
|
# `Marshal`-clean (RBS-native objects carry `RBS::Location`,
|
|
13
13
|
# which lacks `_dump_data`).
|
|
14
14
|
#
|
|
15
|
-
# Cache descriptor
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# upgrade invalidates the table — bundled core + stdlib
|
|
19
|
-
# signatures live inside the gem.
|
|
20
|
-
# - `files`: the digest of every `.rbs` file under the loader's
|
|
21
|
-
# `signature_paths` (project-supplied signatures that the
|
|
22
|
-
# gem's locked version cannot cover).
|
|
23
|
-
# - `configs`: the SHA-256 of the loader's libraries list so
|
|
24
|
-
# adding/removing a stdlib library invalidates.
|
|
15
|
+
# Cache descriptor shape is shared with every other cache
|
|
16
|
+
# producer that depends on the RBS environment — see
|
|
17
|
+
# {RbsDescriptor.build} for the slot definitions.
|
|
25
18
|
class RbsConstantTable
|
|
26
19
|
PRODUCER_ID = "rbs.constant_type_table"
|
|
27
20
|
|
|
@@ -29,55 +22,26 @@ module Rigor
|
|
|
29
22
|
# @param store [Rigor::Cache::Store]
|
|
30
23
|
# @return [Hash{String => Rigor::Type}]
|
|
31
24
|
def self.fetch(loader:, store:)
|
|
32
|
-
descriptor =
|
|
25
|
+
descriptor = RbsDescriptor.build(loader)
|
|
33
26
|
store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
|
|
34
27
|
compute(loader)
|
|
35
28
|
end
|
|
36
29
|
end
|
|
37
30
|
|
|
38
|
-
def self.build_descriptor(loader)
|
|
39
|
-
Descriptor.new(
|
|
40
|
-
gems: [rbs_gem_entry],
|
|
41
|
-
files: file_entries(loader),
|
|
42
|
-
configs: [libraries_entry(loader)]
|
|
43
|
-
)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
31
|
def self.compute(loader)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
table = {}
|
|
33
|
+
loader.each_constant_decl do |name, entry|
|
|
34
|
+
translated = Inference::RbsTypeTranslator.translate(entry.decl.type)
|
|
35
|
+
table[name] = translated unless translated.is_a?(Type::Bot)
|
|
36
|
+
rescue StandardError
|
|
37
|
+
# Skip entries whose RBS type fails to translate; the cache
|
|
38
|
+
# stays robust to a broken signature rather than corrupting
|
|
39
|
+
# the whole table.
|
|
50
40
|
end
|
|
41
|
+
table
|
|
51
42
|
end
|
|
52
43
|
|
|
53
|
-
|
|
54
|
-
Descriptor::GemEntry.new(name: "rbs", requirement: ">= 0", locked: ::RBS::VERSION.to_s)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def self.file_entries(loader)
|
|
58
|
-
loader.signature_paths.flat_map do |root|
|
|
59
|
-
next [] unless root.directory?
|
|
60
|
-
|
|
61
|
-
Dir.glob(root.join("**", "*.rbs")).map do |path|
|
|
62
|
-
Descriptor::FileEntry.new(
|
|
63
|
-
path: path,
|
|
64
|
-
comparator: :digest,
|
|
65
|
-
value: Digest::SHA256.file(path).hexdigest
|
|
66
|
-
)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def self.libraries_entry(loader)
|
|
72
|
-
sorted = loader.libraries.map(&:to_s).sort
|
|
73
|
-
Descriptor::ConfigEntry.new(
|
|
74
|
-
key: "rbs.libraries",
|
|
75
|
-
value_hash: Digest::SHA256.hexdigest(sorted.join("\n"))
|
|
76
|
-
)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
private_class_method :build_descriptor, :compute,
|
|
80
|
-
:rbs_gem_entry, :file_entries, :libraries_entry
|
|
44
|
+
private_class_method :compute
|
|
81
45
|
end
|
|
82
46
|
end
|
|
83
47
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Cache
|
|
7
|
+
# Shared descriptor builder for cache producers that depend on the
|
|
8
|
+
# RBS environment (constant table, known-class set, future
|
|
9
|
+
# Marshal-clean reflection artefacts). Every consumer attaches the
|
|
10
|
+
# same three slots, so factoring the construction here keeps the
|
|
11
|
+
# producers small and ensures invalidation behaves identically
|
|
12
|
+
# across them.
|
|
13
|
+
module RbsDescriptor
|
|
14
|
+
# @param loader [Rigor::Environment::RbsLoader]
|
|
15
|
+
# @return [Rigor::Cache::Descriptor]
|
|
16
|
+
def self.build(loader)
|
|
17
|
+
Descriptor.new(
|
|
18
|
+
gems: [rbs_gem_entry],
|
|
19
|
+
files: file_entries(loader),
|
|
20
|
+
configs: [libraries_entry(loader)]
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.rbs_gem_entry
|
|
25
|
+
Descriptor::GemEntry.new(name: "rbs", requirement: ">= 0", locked: ::RBS::VERSION.to_s)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.file_entries(loader)
|
|
29
|
+
loader.signature_paths.flat_map do |root|
|
|
30
|
+
next [] unless root.directory?
|
|
31
|
+
|
|
32
|
+
Dir.glob(root.join("**", "*.rbs")).map do |path|
|
|
33
|
+
Descriptor::FileEntry.new(
|
|
34
|
+
path: path,
|
|
35
|
+
comparator: :digest,
|
|
36
|
+
value: Digest::SHA256.file(path).hexdigest
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.libraries_entry(loader)
|
|
43
|
+
sorted = loader.libraries.map(&:to_s).sort
|
|
44
|
+
Descriptor::ConfigEntry.new(
|
|
45
|
+
key: "rbs.libraries",
|
|
46
|
+
value_hash: Digest::SHA256.hexdigest(sorted.join("\n"))
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private_class_method :rbs_gem_entry, :file_entries, :libraries_entry
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rbs_descriptor"
|
|
4
|
+
require_relative "rbs_environment_marshal_patch"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Cache
|
|
8
|
+
# Cache producer that materialises the entire
|
|
9
|
+
# `RBS::Environment` (the loader's `build_env` result) and
|
|
10
|
+
# round-trips it through `Marshal` against the patched
|
|
11
|
+
# `RBS::Location` (see {RbsEnvironmentMarshalPatch}).
|
|
12
|
+
#
|
|
13
|
+
# Cold runs pay the full
|
|
14
|
+
# `RBS::EnvironmentLoader#load + RBS::Environment.from_loader
|
|
15
|
+
# + resolve_type_names` cost once; warm runs (and a separate
|
|
16
|
+
# loader sharing the same Store) load the marshalled blob and
|
|
17
|
+
# skip the parse / resolve stages entirely. The
|
|
18
|
+
# `RbsConstantTable`, `RbsKnownClassNames`,
|
|
19
|
+
# `RbsClassAncestorTable`, and `RbsClassTypeParamNames`
|
|
20
|
+
# caches still live alongside this producer — their cached
|
|
21
|
+
# values are reached without re-touching env, but when an
|
|
22
|
+
# uncached lookup happens (`instance_method`,
|
|
23
|
+
# `singleton_method`, …) the env produced here is what
|
|
24
|
+
# answers it.
|
|
25
|
+
#
|
|
26
|
+
# Cache descriptor shape is shared with every other cache
|
|
27
|
+
# producer that depends on the RBS environment — see
|
|
28
|
+
# {RbsDescriptor.build}.
|
|
29
|
+
class RbsEnvironment
|
|
30
|
+
PRODUCER_ID = "rbs.environment"
|
|
31
|
+
|
|
32
|
+
# @param loader [Rigor::Environment::RbsLoader]
|
|
33
|
+
# @param store [Rigor::Cache::Store]
|
|
34
|
+
# @return [::RBS::Environment]
|
|
35
|
+
def self.fetch(loader:, store:)
|
|
36
|
+
descriptor = RbsDescriptor.build(loader)
|
|
37
|
+
store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
|
|
38
|
+
compute(loader)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.compute(loader)
|
|
43
|
+
Rigor::Environment::RbsLoader.build_env_for(
|
|
44
|
+
libraries: loader.libraries,
|
|
45
|
+
signature_paths: loader.signature_paths
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private_class_method :compute
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbs"
|
|
4
|
+
|
|
5
|
+
# Adds `_dump` / `_load` to {RBS::Location} so an
|
|
6
|
+
# `RBS::Environment` (and its transitive AST nodes, all of
|
|
7
|
+
# which carry Locations) round-trips through `Marshal`. The
|
|
8
|
+
# rbs gem's C-extension `RBS::Location` ships without the
|
|
9
|
+
# Marshal hooks; until rbs grows them upstream this patch is
|
|
10
|
+
# the minimal monkey-patch the v0.0.9 RBS::Environment cache
|
|
11
|
+
# relies on.
|
|
12
|
+
#
|
|
13
|
+
# Patch policy (purely additive):
|
|
14
|
+
#
|
|
15
|
+
# - `_dump` returns an empty string. The cached env loses
|
|
16
|
+
# per-node source-position info, but Rigor does not consult
|
|
17
|
+
# `RBS::Location` from any analysis code path (every
|
|
18
|
+
# diagnostic uses Prism's own location), so the loss is
|
|
19
|
+
# inert in practice.
|
|
20
|
+
# - `_load` reconstructs a sentinel Location backed by an
|
|
21
|
+
# empty `<cached>` Buffer. Code paths that DID consult
|
|
22
|
+
# Location after a cache hit see a benign zero-range value
|
|
23
|
+
# rather than crashing.
|
|
24
|
+
#
|
|
25
|
+
# Idempotent: the guard checks `method_defined?(:_dump)` so
|
|
26
|
+
# requiring this file twice (or against an upstream rbs that
|
|
27
|
+
# adds Marshal hooks itself) is a no-op.
|
|
28
|
+
module RBS
|
|
29
|
+
class Location
|
|
30
|
+
unless method_defined?(:_dump)
|
|
31
|
+
def _dump(_)
|
|
32
|
+
""
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self._load(_)
|
|
36
|
+
new(buffer: ::RBS::Buffer.new(name: "<cached>", content: ""), start_pos: 0, end_pos: 0)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rbs_descriptor"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Cache
|
|
7
|
+
# Cache producer that materialises the set of every RBS-declared
|
|
8
|
+
# class / module / alias name (top-level prefixed, e.g.
|
|
9
|
+
# `"::Math"`) currently loaded into the environment. Marshal-
|
|
10
|
+
# clean — the cache value is a `Set<String>`.
|
|
11
|
+
#
|
|
12
|
+
# The set lets `RbsLoader#class_known?` answer point lookups in
|
|
13
|
+
# O(1) without re-parsing the name on each call, and lets a warm
|
|
14
|
+
# process skip the env walk entirely (the env still has to be
|
|
15
|
+
# built to enumerate decls on cold misses; subsequent processes
|
|
16
|
+
# sharing the Store load the set straight from disk).
|
|
17
|
+
#
|
|
18
|
+
# Cache descriptor shape is shared with {RbsConstantTable} via
|
|
19
|
+
# {RbsDescriptor.build}; a single signature change or rbs gem
|
|
20
|
+
# bump invalidates both producers in lockstep.
|
|
21
|
+
class RbsKnownClassNames
|
|
22
|
+
PRODUCER_ID = "rbs.known_class_names"
|
|
23
|
+
|
|
24
|
+
# @param loader [Rigor::Environment::RbsLoader]
|
|
25
|
+
# @param store [Rigor::Cache::Store]
|
|
26
|
+
# @return [Set<String>]
|
|
27
|
+
def self.fetch(loader:, store:)
|
|
28
|
+
descriptor = RbsDescriptor.build(loader)
|
|
29
|
+
store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
|
|
30
|
+
compute(loader)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.compute(loader)
|
|
35
|
+
names = Set.new
|
|
36
|
+
loader.each_known_class_name { |name| names << name }
|
|
37
|
+
names
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private_class_method :compute
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -30,10 +30,27 @@ module Rigor
|
|
|
30
30
|
|
|
31
31
|
def initialize(root:)
|
|
32
32
|
@root = root.to_s.dup.freeze
|
|
33
|
+
@hits = 0
|
|
34
|
+
@misses = 0
|
|
35
|
+
@writes = 0
|
|
36
|
+
@by_producer = Hash.new { |h, k| h[k] = { hits: 0, misses: 0, writes: 0 } }
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
attr_reader :root
|
|
36
40
|
|
|
41
|
+
# Returns a frozen snapshot of this Store's per-run hit / miss /
|
|
42
|
+
# write counters. The bookkeeping is in-memory only — every new
|
|
43
|
+
# `Store.new` starts at zero — so the counters reflect activity
|
|
44
|
+
# against this specific instance rather than the on-disk cache
|
|
45
|
+
# state. Disk-level state is reported separately by
|
|
46
|
+
# {.disk_inventory}.
|
|
47
|
+
#
|
|
48
|
+
# @return [Hash] `{ hits:, misses:, writes:, by_producer: { id => { hits:, misses:, writes: } } }`
|
|
49
|
+
def stats
|
|
50
|
+
per_producer = @by_producer.transform_values { |counts| counts.dup.freeze }.freeze
|
|
51
|
+
{ hits: @hits, misses: @misses, writes: @writes, by_producer: per_producer }.freeze
|
|
52
|
+
end
|
|
53
|
+
|
|
37
54
|
# Walks the on-disk cache rooted at `root` and reports a
|
|
38
55
|
# producer-level inventory. Used by `rigor check --cache-stats`
|
|
39
56
|
# to surface cache size and per-producer entry counts without
|
|
@@ -84,21 +101,43 @@ module Rigor
|
|
|
84
101
|
# via {Descriptor#cache_key_for}.
|
|
85
102
|
# @param descriptor [Rigor::Cache::Descriptor] the invalidation
|
|
86
103
|
# descriptor for the value being cached.
|
|
87
|
-
# @
|
|
104
|
+
# @param serialize [#call, nil] optional callable that turns the
|
|
105
|
+
# producer's return value into a binary `String`. Defaults to
|
|
106
|
+
# `Marshal.dump(value).b`. Producers whose return values are
|
|
107
|
+
# not `Marshal`-clean (RBS-native objects with `RBS::Location`
|
|
108
|
+
# members, raw `IO`, …) MUST provide a serialiser. The pair
|
|
109
|
+
# `(serialize, deserialize)` MUST round-trip — a producer that
|
|
110
|
+
# reads with one strategy and writes with another corrupts
|
|
111
|
+
# its own cache slice.
|
|
112
|
+
# @param deserialize [#call, nil] optional callable that turns
|
|
113
|
+
# bytes back into the producer's value. Defaults to
|
|
114
|
+
# `Marshal.load`. Any exception (`StandardError`) raised by
|
|
115
|
+
# the deserialiser is treated as a cache miss — the entry is
|
|
116
|
+
# considered corrupt, the producer block reruns, and the
|
|
117
|
+
# next write overwrites it. This is consistent with the
|
|
118
|
+
# fault-tolerance contract for the default `Marshal.load`
|
|
119
|
+
# path.
|
|
120
|
+
# @yieldreturn the value to cache.
|
|
88
121
|
# @return the cached value (loaded from disk on hit; produced by
|
|
89
122
|
# the block on miss).
|
|
90
|
-
def fetch_or_compute(producer_id:, params:, descriptor:,
|
|
123
|
+
def fetch_or_compute(producer_id:, params:, descriptor:,
|
|
124
|
+
serialize: nil, deserialize: nil, &block)
|
|
91
125
|
validate_producer_id!(producer_id)
|
|
92
126
|
ensure_schema_version!
|
|
93
127
|
|
|
94
128
|
key = descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
95
129
|
path = entry_path(producer_id, key)
|
|
96
130
|
|
|
97
|
-
cached = read_entry(path)
|
|
98
|
-
|
|
131
|
+
cached = read_entry(path, deserialize: deserialize)
|
|
132
|
+
unless cached.nil?
|
|
133
|
+
record(:hits, producer_id)
|
|
134
|
+
return cached.value
|
|
135
|
+
end
|
|
99
136
|
|
|
137
|
+
record(:misses, producer_id)
|
|
100
138
|
value = block.call
|
|
101
|
-
write_entry(path, descriptor, value)
|
|
139
|
+
write_entry(path, descriptor, value, serialize: serialize)
|
|
140
|
+
record(:writes, producer_id)
|
|
102
141
|
value
|
|
103
142
|
end
|
|
104
143
|
|
|
@@ -107,6 +146,15 @@ module Rigor
|
|
|
107
146
|
Entry = Data.define(:descriptor_bytes, :value)
|
|
108
147
|
private_constant :Entry
|
|
109
148
|
|
|
149
|
+
def record(counter, producer_id)
|
|
150
|
+
case counter
|
|
151
|
+
when :hits then @hits += 1
|
|
152
|
+
when :misses then @misses += 1
|
|
153
|
+
when :writes then @writes += 1
|
|
154
|
+
end
|
|
155
|
+
@by_producer[producer_id][counter] += 1
|
|
156
|
+
end
|
|
157
|
+
|
|
110
158
|
def validate_producer_id!(producer_id)
|
|
111
159
|
return if producer_id.is_a?(String) && producer_id.match?(VALID_PRODUCER_ID)
|
|
112
160
|
|
|
@@ -121,7 +169,7 @@ module Rigor
|
|
|
121
169
|
# Reads and validates one entry file. Any failure (missing,
|
|
122
170
|
# short, bad magic, bad version, bad checksum, unmarshal-able)
|
|
123
171
|
# returns nil so the caller treats it as a cache miss.
|
|
124
|
-
def read_entry(path)
|
|
172
|
+
def read_entry(path, deserialize: nil)
|
|
125
173
|
return nil unless File.file?(path)
|
|
126
174
|
|
|
127
175
|
bytes = File.binread(path)
|
|
@@ -131,8 +179,8 @@ module Rigor
|
|
|
131
179
|
descriptor_bytes, value_bytes = parse_body(body)
|
|
132
180
|
return nil if descriptor_bytes.nil?
|
|
133
181
|
|
|
134
|
-
value =
|
|
135
|
-
return nil if value.equal?(
|
|
182
|
+
value = safe_load(value_bytes, deserialize)
|
|
183
|
+
return nil if value.equal?(LOAD_FAILED)
|
|
136
184
|
|
|
137
185
|
Entry.new(descriptor_bytes, value)
|
|
138
186
|
end
|
|
@@ -164,20 +212,24 @@ module Rigor
|
|
|
164
212
|
[descriptor_bytes, value_bytes]
|
|
165
213
|
end
|
|
166
214
|
|
|
167
|
-
|
|
168
|
-
private_constant :
|
|
215
|
+
LOAD_FAILED = Object.new.freeze
|
|
216
|
+
private_constant :LOAD_FAILED
|
|
169
217
|
|
|
170
|
-
def
|
|
171
|
-
|
|
218
|
+
def safe_load(bytes, deserialize)
|
|
219
|
+
if deserialize
|
|
220
|
+
deserialize.call(bytes)
|
|
221
|
+
else
|
|
222
|
+
Marshal.load(bytes) # rubocop:disable Security/MarshalLoad
|
|
223
|
+
end
|
|
172
224
|
rescue StandardError
|
|
173
|
-
|
|
225
|
+
LOAD_FAILED
|
|
174
226
|
end
|
|
175
227
|
|
|
176
|
-
def write_entry(path, descriptor, value)
|
|
228
|
+
def write_entry(path, descriptor, value, serialize: nil)
|
|
177
229
|
FileUtils.mkdir_p(File.dirname(path))
|
|
178
230
|
|
|
179
231
|
descriptor_bytes = descriptor.to_canonical_bytes
|
|
180
|
-
value_bytes =
|
|
232
|
+
value_bytes = serialize_value(value, serialize)
|
|
181
233
|
|
|
182
234
|
body = +"".b
|
|
183
235
|
body << HEADER
|
|
@@ -190,6 +242,18 @@ module Rigor
|
|
|
190
242
|
atomically_replace(path, body)
|
|
191
243
|
end
|
|
192
244
|
|
|
245
|
+
def serialize_value(value, serialize)
|
|
246
|
+
return Marshal.dump(value).b if serialize.nil?
|
|
247
|
+
|
|
248
|
+
bytes = serialize.call(value)
|
|
249
|
+
unless bytes.is_a?(String)
|
|
250
|
+
raise TypeError,
|
|
251
|
+
"custom serialize must return a String, got #{bytes.class}"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
bytes.b
|
|
255
|
+
end
|
|
256
|
+
|
|
193
257
|
def atomically_replace(path, body)
|
|
194
258
|
File.open(path, File::RDWR | File::CREAT, 0o644) do |lock_fd|
|
|
195
259
|
lock_fd.flock(File::LOCK_EX)
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -72,13 +72,19 @@ module Rigor
|
|
|
72
72
|
|
|
73
73
|
cache_root = ".rigor/cache"
|
|
74
74
|
handle_clear_cache(cache_root) if options.fetch(:clear_cache)
|
|
75
|
+
cache_store = options.fetch(:no_cache) ? nil : Cache::Store.new(root: cache_root)
|
|
75
76
|
|
|
76
77
|
configuration = Configuration.load(options.fetch(:config))
|
|
77
78
|
paths = @argv.empty? ? configuration.paths : @argv
|
|
78
|
-
|
|
79
|
+
runner = Analysis::Runner.new(
|
|
80
|
+
configuration: configuration,
|
|
81
|
+
explain: options.fetch(:explain),
|
|
82
|
+
cache_store: cache_store
|
|
83
|
+
)
|
|
84
|
+
result = runner.run(paths)
|
|
79
85
|
|
|
80
86
|
write_result(result, options.fetch(:format))
|
|
81
|
-
write_cache_stats(cache_root) if options.fetch(:cache_stats)
|
|
87
|
+
write_cache_stats(cache_root, runner.cache_store) if options.fetch(:cache_stats)
|
|
82
88
|
result.success? ? 0 : 1
|
|
83
89
|
end
|
|
84
90
|
|
|
@@ -88,7 +94,8 @@ module Rigor
|
|
|
88
94
|
format: "text",
|
|
89
95
|
explain: false,
|
|
90
96
|
cache_stats: false,
|
|
91
|
-
clear_cache: false
|
|
97
|
+
clear_cache: false,
|
|
98
|
+
no_cache: false
|
|
92
99
|
}
|
|
93
100
|
parser = OptionParser.new do |opts|
|
|
94
101
|
opts.banner = "Usage: rigor check [options] [paths]"
|
|
@@ -97,6 +104,7 @@ module Rigor
|
|
|
97
104
|
opts.on("--explain", "Surface fail-soft fallback events as :info diagnostics") { options[:explain] = true }
|
|
98
105
|
opts.on("--cache-stats", "Print on-disk cache inventory at end of run") { options[:cache_stats] = true }
|
|
99
106
|
opts.on("--clear-cache", "Remove the .rigor/cache directory before running") { options[:clear_cache] = true }
|
|
107
|
+
opts.on("--no-cache", "Disable the persistent cache for this run") { options[:no_cache] = true }
|
|
100
108
|
end
|
|
101
109
|
parser.parse!(@argv)
|
|
102
110
|
options
|
|
@@ -111,13 +119,18 @@ module Rigor
|
|
|
111
119
|
end
|
|
112
120
|
end
|
|
113
121
|
|
|
114
|
-
def write_cache_stats(cache_root)
|
|
122
|
+
def write_cache_stats(cache_root, runtime_store)
|
|
115
123
|
inv = Cache::Store.disk_inventory(root: cache_root)
|
|
116
124
|
|
|
117
125
|
@out.puts("")
|
|
118
126
|
@out.puts("Cache (root: #{inv.fetch(:root)})")
|
|
119
127
|
schema = inv.fetch(:schema_version)
|
|
120
128
|
@out.puts(" schema_version: #{schema.nil? ? 'absent' : schema}")
|
|
129
|
+
write_disk_inventory(inv)
|
|
130
|
+
write_runtime_stats(runtime_store) if runtime_store
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def write_disk_inventory(inv)
|
|
121
134
|
if inv.fetch(:total_entries).zero?
|
|
122
135
|
@out.puts(" (empty)")
|
|
123
136
|
return
|
|
@@ -130,6 +143,25 @@ module Rigor
|
|
|
130
143
|
end
|
|
131
144
|
end
|
|
132
145
|
|
|
146
|
+
def write_runtime_stats(store)
|
|
147
|
+
stats = store.stats
|
|
148
|
+
hits = stats.fetch(:hits)
|
|
149
|
+
misses = stats.fetch(:misses)
|
|
150
|
+
writes = stats.fetch(:writes)
|
|
151
|
+
@out.puts(" this run: #{hits} #{plural(hits, 'hit')}, " \
|
|
152
|
+
"#{misses} #{plural(misses, 'miss', 'misses')}, " \
|
|
153
|
+
"#{writes} #{plural(writes, 'write')}")
|
|
154
|
+
stats.fetch(:by_producer).each do |id, counts|
|
|
155
|
+
@out.puts(" #{id}: #{counts.fetch(:hits)} #{plural(counts.fetch(:hits), 'hit')}, " \
|
|
156
|
+
"#{counts.fetch(:misses)} #{plural(counts.fetch(:misses), 'miss', 'misses')}, " \
|
|
157
|
+
"#{counts.fetch(:writes)} #{plural(counts.fetch(:writes), 'write')}")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def plural(count, singular, plural = "#{singular}s")
|
|
162
|
+
count == 1 ? singular : plural
|
|
163
|
+
end
|
|
164
|
+
|
|
133
165
|
def format_bytes(bytes)
|
|
134
166
|
return "#{bytes} B" if bytes < 1024
|
|
135
167
|
return format("%.1f KiB", bytes / 1024.0) if bytes < 1024 * 1024
|
|
@@ -45,15 +45,28 @@ module Rigor
|
|
|
45
45
|
key = normalize_name(class_name)
|
|
46
46
|
return @ancestor_names_cache[key] if @ancestor_names_cache.key?(key)
|
|
47
47
|
|
|
48
|
-
definition = loader.instance_definition(key)
|
|
49
48
|
@ancestor_names_cache[key] =
|
|
50
|
-
if
|
|
51
|
-
|
|
49
|
+
if loader.cache_store
|
|
50
|
+
ancestor_table.fetch(key, [].freeze)
|
|
52
51
|
else
|
|
53
|
-
|
|
52
|
+
compute_ancestor_names(key)
|
|
54
53
|
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def compute_ancestor_names(key)
|
|
57
|
+
definition = loader.instance_definition(key)
|
|
58
|
+
return [].freeze if definition.nil?
|
|
59
|
+
|
|
60
|
+
definition.ancestors.ancestors.map { |ancestor| normalize_name(ancestor.name.to_s) }.uniq.freeze
|
|
55
61
|
rescue StandardError
|
|
56
|
-
|
|
62
|
+
[].freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def ancestor_table
|
|
66
|
+
@ancestor_table ||= begin
|
|
67
|
+
require_relative "../cache/rbs_class_ancestor_table"
|
|
68
|
+
Cache::RbsClassAncestorTable.fetch(loader: loader, store: loader.cache_store)
|
|
69
|
+
end
|
|
57
70
|
end
|
|
58
71
|
|
|
59
72
|
def normalize_name(name)
|