rigortype 0.2.0 → 0.2.1
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 +41 -6
- data/data/core_overlay/numeric.rbs +33 -0
- data/data/core_overlay/pathname.rbs +25 -0
- data/data/core_overlay/string_scanner.rbs +28 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
- data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
- data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
- data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
- data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
- data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
- data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
- data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
- data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
- data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
- data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
- data/data/vendored_gem_sigs/redis/future.rbs +5 -0
- data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
- data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
- data/lib/rigor/cli/check_command.rb +25 -2
- data/lib/rigor/cli/coverage_command.rb +67 -92
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
- data/lib/rigor/cli/fused_protection_report.rb +76 -0
- data/lib/rigor/config_audit.rb +152 -0
- data/lib/rigor/configuration.rb +12 -0
- data/lib/rigor/environment/rbs_loader.rb +27 -0
- data/lib/rigor/environment.rb +49 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +17 -7
- data/lib/rigor/inference/statement_evaluator.rb +27 -0
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +98 -38
- data/lib/rigor/protection/mutator.rb +21 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/signature_path_audit.rb +92 -0
- data/lib/rigor/version.rb +1 -1
- metadata +31 -1
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
-
require_relative "../analysis/runner"
|
|
6
5
|
require_relative "mutator"
|
|
6
|
+
require_relative "diagnostic_oracle"
|
|
7
7
|
|
|
8
8
|
module Rigor
|
|
9
9
|
module Protection
|
|
@@ -15,17 +15,16 @@ module Rigor
|
|
|
15
15
|
# Mechanism (the ADR-62 warm loop, narrowed to per-file measurement):
|
|
16
16
|
# generate the type-visible mutations ({Mutator}), keep only those whose
|
|
17
17
|
# receiver Rigor holds a concrete type for (the type-aware filter — the
|
|
18
|
-
# FP-safe meaning-maker; an unresolved receiver is kept), then for each
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
18
|
+
# FP-safe meaning-maker; an unresolved receiver is kept), then for each ask
|
|
19
|
+
# the **kill oracle** whether the mutant is caught. The oracle is the ADR-69
|
|
20
|
+
# seam: {#scan_file} uses the {DiagnosticOracle} (a *new Rigor diagnostic* =
|
|
21
|
+
# a kill); {#scan_file_fused} additionally consults a {TestSuiteOracle} on the
|
|
22
|
+
# type-survivors (ADR-70 — the dynamic protection axis).
|
|
22
23
|
#
|
|
23
24
|
# The expensive builds (RBS environment + the whole-project pre-pass scan)
|
|
24
|
-
# are paid ONCE by the caller and threaded
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# Passing `prebuilt:` disables the run-result cache (whose key digests the
|
|
28
|
-
# *disk* file), so a mutant is never served a stale clean hit.
|
|
25
|
+
# are paid ONCE by the caller and threaded into the {DiagnosticOracle}; each
|
|
26
|
+
# mutant reuses them through `Runner.new(prebuilt:)#run_source` (in-memory
|
|
27
|
+
# overlay, no disk write).
|
|
29
28
|
class MutationScanner
|
|
30
29
|
# A surviving mutation site — a breakage Rigor did not catch.
|
|
31
30
|
SurvivingSite = Data.define(:line, :receiver, :method_name, :operator)
|
|
@@ -39,18 +38,46 @@ module Rigor
|
|
|
39
38
|
def ratio = total.zero? ? 1.0 : killed.to_f / total
|
|
40
39
|
end
|
|
41
40
|
|
|
41
|
+
# ADR-70 — one type-survivor classified by the dynamic (test) axis.
|
|
42
|
+
# `protection` is `:test` (a test caught it) or `:none` (unprotected — the
|
|
43
|
+
# "add a type OR a test here" sites).
|
|
44
|
+
FusedSite = Data.define(:line, :receiver, :method_name, :operator, :protection)
|
|
45
|
+
|
|
46
|
+
# ADR-70 — the per-file fused classification. The gradual short-circuit
|
|
47
|
+
# collapses the conceptual "doubly-protected" bucket into `type_killed`:
|
|
48
|
+
# a mutant the type checker already kills never reaches the suite, because
|
|
49
|
+
# the static net already suffices and re-running the suite to learn a test
|
|
50
|
+
# *would also* catch it is wasted work. So the observed buckets are three.
|
|
51
|
+
FusedFileResult = Data.define(:path, :type_killed, :test_killed, :sites) do
|
|
52
|
+
# The unprotected sites (neither a type nor a test caught the breakage).
|
|
53
|
+
def unprotected = sites.size
|
|
54
|
+
def total = type_killed + test_killed + unprotected
|
|
55
|
+
|
|
56
|
+
# Fused protected ratio — caught by *either* axis.
|
|
57
|
+
def ratio = total.zero? ? 1.0 : (type_killed + test_killed).to_f / total
|
|
58
|
+
end
|
|
59
|
+
|
|
42
60
|
# @param configuration [Rigor::Configuration]
|
|
43
61
|
# @param environment [Rigor::Environment] pre-built once by the caller
|
|
44
62
|
# @param project_scan [Rigor::Analysis::ProjectScan] pre-built once
|
|
45
63
|
# @param limit [Integer, nil] optional per-file mutation cap (sampled with
|
|
46
64
|
# `seed`); nil analyses every type-relevant mutation (deterministic).
|
|
47
65
|
# @param seed [Integer] RNG seed for the optional sample.
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
# @param oracle [#baseline, #killed?, nil] the kill oracle (ADR-69 Seam 1);
|
|
67
|
+
# defaults to the {DiagnosticOracle} (the ADR-62/63 behaviour).
|
|
68
|
+
# @param site_selector [:biteable, :all] which sites to mutate (ADR-69
|
|
69
|
+
# Seam 2). `:biteable` (default) keeps only concrete-type sites Rigor can
|
|
70
|
+
# bite; `:all` also mutates Dynamic-receiver dispatch sites — use only
|
|
71
|
+
# with a {TestSuiteOracle} (the fused overlay), never the diagnostic path.
|
|
72
|
+
def initialize(configuration:, environment:, project_scan:, limit: nil, seed: 1, oracle: nil,
|
|
73
|
+
site_selector: :biteable)
|
|
50
74
|
@environment = environment
|
|
51
|
-
@project_scan = project_scan
|
|
52
75
|
@limit = limit
|
|
53
76
|
@seed = seed
|
|
77
|
+
@site_selector = site_selector
|
|
78
|
+
@oracle = oracle || DiagnosticOracle.new(
|
|
79
|
+
configuration: configuration, environment: environment, project_scan: project_scan
|
|
80
|
+
)
|
|
54
81
|
end
|
|
55
82
|
|
|
56
83
|
# @param path [String] the file to measure (used as the in-memory bind path)
|
|
@@ -58,21 +85,13 @@ module Rigor
|
|
|
58
85
|
# @return [FileResult]
|
|
59
86
|
def scan_file(path, source: nil)
|
|
60
87
|
source ||= File.read(path, encoding: Encoding::UTF_8)
|
|
61
|
-
|
|
62
|
-
kept, = mutator.filter_by_type(mutator.mutations, environment: @environment, path: path)
|
|
63
|
-
kept = sample(kept)
|
|
88
|
+
kept = kept_mutations(source, path)
|
|
64
89
|
return FileResult.new(path: path, killed: 0, survived: 0, sites: []) if kept.empty?
|
|
65
90
|
|
|
66
|
-
baseline =
|
|
67
|
-
measure(source, path, kept, baseline)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
|
|
72
|
-
def measure(source, path, mutations, baseline)
|
|
91
|
+
baseline = @oracle.baseline(source: source, path: path)
|
|
73
92
|
killed = 0
|
|
74
93
|
sites = []
|
|
75
|
-
|
|
94
|
+
kept.each do |mut|
|
|
76
95
|
case classify(source, path, mut, baseline)
|
|
77
96
|
when :killed then killed += 1
|
|
78
97
|
when :survived then sites << surviving_site(mut)
|
|
@@ -82,39 +101,80 @@ module Rigor
|
|
|
82
101
|
FileResult.new(path: path, killed: killed, survived: sites.size, sites: sites)
|
|
83
102
|
end
|
|
84
103
|
|
|
104
|
+
# ADR-70 — the fused static∪dynamic measurement. Runs the type pass
|
|
105
|
+
# (the {DiagnosticOracle}); for every mutant the type checker did **not**
|
|
106
|
+
# kill, asks `test_oracle` whether the project's test suite catches it.
|
|
107
|
+
# The expensive suite run is paid only for type-survivors (the gradual
|
|
108
|
+
# short-circuit), so the cost is proportional to the protection hole.
|
|
109
|
+
# @param test_oracle [TestSuiteOracle]
|
|
110
|
+
# @return [FusedFileResult]
|
|
111
|
+
def scan_file_fused(path, test_oracle:, source: nil)
|
|
112
|
+
source ||= File.read(path, encoding: Encoding::UTF_8)
|
|
113
|
+
kept = kept_mutations(source, path)
|
|
114
|
+
return FusedFileResult.new(path: path, type_killed: 0, test_killed: 0, sites: []) if kept.empty?
|
|
115
|
+
|
|
116
|
+
baseline = @oracle.baseline(source: source, path: path)
|
|
117
|
+
type_killed = 0
|
|
118
|
+
test_killed = 0
|
|
119
|
+
sites = []
|
|
120
|
+
kept.each do |mut|
|
|
121
|
+
case classify(source, path, mut, baseline)
|
|
122
|
+
when :killed then type_killed += 1
|
|
123
|
+
when :survived
|
|
124
|
+
if test_oracle.killed?(path: path, original: source, mutant_source: mut.apply(source))
|
|
125
|
+
test_killed += 1
|
|
126
|
+
else
|
|
127
|
+
sites << fused_site(mut, :none)
|
|
128
|
+
end
|
|
129
|
+
# :invalid — a parse-broken mutant; not a measurement, skip it.
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
FusedFileResult.new(path: path, type_killed: type_killed, test_killed: test_killed, sites: sites)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# The mutations to measure: the biteable filter (concrete-type sites only;
|
|
138
|
+
# the FP-safe default — an unresolved receiver is kept) or, under the
|
|
139
|
+
# `:all` selector (ADR-69 Seam 2), every dispatch site including Dynamic
|
|
140
|
+
# receivers. Optionally sampled.
|
|
141
|
+
def kept_mutations(source, path)
|
|
142
|
+
mutator = Mutator.new(source)
|
|
143
|
+
muts = mutator.mutations
|
|
144
|
+
kept =
|
|
145
|
+
if @site_selector == :all
|
|
146
|
+
mutator.dispatch_site_mutations(muts, environment: @environment, path: path)
|
|
147
|
+
else
|
|
148
|
+
mutator.filter_by_type(muts, environment: @environment, path: path).first
|
|
149
|
+
end
|
|
150
|
+
sample(kept)
|
|
151
|
+
end
|
|
152
|
+
|
|
85
153
|
def classify(source, path, mut, baseline)
|
|
86
154
|
mutant_source = mut.apply(source)
|
|
87
155
|
return :invalid unless Prism.parse(mutant_source).success?
|
|
88
156
|
|
|
89
|
-
|
|
90
|
-
new_diagnostics.empty? ? :survived : :killed
|
|
157
|
+
@oracle.killed?(mutant_source: mutant_source, path: path, baseline: baseline) ? :killed : :survived
|
|
91
158
|
rescue StandardError
|
|
92
159
|
# A harness-level failure on one mutant must not abort the file.
|
|
93
160
|
:invalid
|
|
94
161
|
end
|
|
95
162
|
|
|
96
|
-
# cache_store: nil + prebuilt: scan ⇒ the run cache is bypassed and the
|
|
97
|
-
# mutant is always re-analysed against the in-memory bytes.
|
|
98
|
-
def analyse(source, path)
|
|
99
|
-
Rigor::Analysis::Runner.new(
|
|
100
|
-
configuration: @configuration, environment: @environment, prebuilt: @project_scan,
|
|
101
|
-
cache_store: nil, collect_stats: false
|
|
102
|
-
).run_source(source: source, path: path).diagnostics
|
|
103
|
-
end
|
|
104
|
-
|
|
105
163
|
def sample(mutations)
|
|
106
164
|
return mutations unless @limit
|
|
107
165
|
|
|
108
166
|
mutations.sample(@limit, random: Random.new(@seed))
|
|
109
167
|
end
|
|
110
168
|
|
|
111
|
-
def signatures(diagnostics) = diagnostics.to_set { |d| sig(d) }
|
|
112
|
-
def sig(diagnostic) = [diagnostic.rule, diagnostic.path, diagnostic.line, diagnostic.column, diagnostic.message]
|
|
113
|
-
|
|
114
169
|
def surviving_site(mut)
|
|
115
170
|
SurvivingSite.new(line: mut.line, receiver: mut.anchor_type,
|
|
116
171
|
method_name: mut.method_name, operator: mut.operator.to_s)
|
|
117
172
|
end
|
|
173
|
+
|
|
174
|
+
def fused_site(mut, protection)
|
|
175
|
+
FusedSite.new(line: mut.line, receiver: mut.anchor_type, method_name: mut.method_name,
|
|
176
|
+
operator: mut.operator.to_s, protection: protection)
|
|
177
|
+
end
|
|
118
178
|
end
|
|
119
179
|
end
|
|
120
180
|
end
|
|
@@ -98,6 +98,27 @@ module Rigor
|
|
|
98
98
|
[kept, mutations.size - kept.size]
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
# ADR-69 Seam 2 (AllSites) — keep every *dispatch-site* mutation (a method
|
|
102
|
+
# call or a call-argument literal), Dynamic receiver included, annotating
|
|
103
|
+
# the anchor type where Rigor holds one. Drops only non-dispatch literals
|
|
104
|
+
# (a literal outside any call — no receiver contract to violate). The
|
|
105
|
+
# biteable {#filter_by_type} hides exactly the Dynamic sites a test-suite
|
|
106
|
+
# consumer most wants to probe: where Rigor cannot bite, a test is the only
|
|
107
|
+
# protection. Use only with a {TestSuiteOracle} — at a Dynamic site the
|
|
108
|
+
# type pass can never kill, so without the test axis these are all noise.
|
|
109
|
+
def dispatch_site_mutations(mutations, environment:, path:)
|
|
110
|
+
base = Rigor::Scope.empty(environment: environment, source_path: path)
|
|
111
|
+
index = Rigor::Inference::ScopeIndexer.index(@parse.value, default_scope: base)
|
|
112
|
+
cache = {}
|
|
113
|
+
mutations.select do |mut|
|
|
114
|
+
next false if mut.method_name.nil?
|
|
115
|
+
|
|
116
|
+
_keep, type = anchor_decision(mut.anchor, index, cache)
|
|
117
|
+
mut.anchor_type = type
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
101
122
|
private
|
|
102
123
|
|
|
103
124
|
def walk(node, &blk)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Protection
|
|
5
|
+
# ADR-70 — the **test-suite** kill oracle, the dynamic sibling of
|
|
6
|
+
# {DiagnosticOracle} on the ADR-69 seam. A mutant is killed iff applying its
|
|
7
|
+
# bytes to the file under test turns the project's test suite **red**. This is
|
|
8
|
+
# the dynamic half of the fused static∪dynamic protection map: a `Dynamic`
|
|
9
|
+
# site Rigor cannot bite (a type survivor) may still be fully guarded by a
|
|
10
|
+
# test.
|
|
11
|
+
#
|
|
12
|
+
# The suite command is the **runner hook** (`--test-command`, e.g.
|
|
13
|
+
# `bundle exec rake`). The runner is injectable so the decision logic is
|
|
14
|
+
# unit-testable without shelling out; the default shells out and reads the
|
|
15
|
+
# process exit status (0 = green / passed).
|
|
16
|
+
#
|
|
17
|
+
# I/O policy: {#killed?} writes the mutant to disk, runs the suite, and
|
|
18
|
+
# **always restores** the original bytes in an `ensure` — a normal exception
|
|
19
|
+
# never leaves a mutant on disk. (A hard interrupt mid-suite is the standard
|
|
20
|
+
# mutation-testing hazard the `ensure` cannot cover; callers running this in
|
|
21
|
+
# CI accept that, as `mutant` / Stryker do.)
|
|
22
|
+
class TestSuiteOracle
|
|
23
|
+
# @param command [Array<String>] the test command (the runner hook)
|
|
24
|
+
# @param runner [#call, nil] `runner.call(command) -> true iff the suite
|
|
25
|
+
# passed`. Defaults to shelling out via `system`.
|
|
26
|
+
def initialize(command:, runner: nil)
|
|
27
|
+
@command = command
|
|
28
|
+
@runner = runner || method(:shell_run)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The baseline: the suite must pass on clean code, else "a mutant survived"
|
|
32
|
+
# is meaningless (every mutant would look killed, or none would). Run once
|
|
33
|
+
# before measuring.
|
|
34
|
+
def green?
|
|
35
|
+
@runner.call(@command)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Killed iff the mutant turns the suite red. Restores `original` afterward.
|
|
39
|
+
# @param path [String] the file to (temporarily) overwrite with the mutant
|
|
40
|
+
# @param original [String] the clean bytes to restore
|
|
41
|
+
# @param mutant_source [String] the mutated bytes to test against
|
|
42
|
+
def killed?(path:, original:, mutant_source:)
|
|
43
|
+
File.write(path, mutant_source)
|
|
44
|
+
!@runner.call(@command)
|
|
45
|
+
ensure
|
|
46
|
+
File.write(path, original)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Run the suite with Bundler's environment stripped, so a `bundle exec`
|
|
52
|
+
# test command resolves the **target** project's Gemfile — not whatever
|
|
53
|
+
# bundle Rigor itself was launched under. Running Rigor via `bundle exec`
|
|
54
|
+
# leaks `RUBYOPT=-rbundler/setup` + `GEM_HOME` / `BUNDLE_*` into a plain
|
|
55
|
+
# `system` subprocess, which then resolves the target's Gemfile against
|
|
56
|
+
# Rigor's gems and fails — so a green suite looks red and the run aborts.
|
|
57
|
+
# `with_unbundled_env` restores the pre-bundler env (a bare `env -u
|
|
58
|
+
# BUNDLE_GEMFILE` is not enough — the `BUNDLER_ORIG_*` preservers defeat
|
|
59
|
+
# it). Found validating ADR-70 on real projects (2026-06-17).
|
|
60
|
+
def shell_run(command)
|
|
61
|
+
run = -> { system(*command, out: File::NULL, err: File::NULL) }
|
|
62
|
+
return run.call unless defined?(Bundler) && Bundler.respond_to?(:with_unbundled_env)
|
|
63
|
+
|
|
64
|
+
Bundler.with_unbundled_env(&run)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# Classifies each configured `signature_paths:` entry by what it
|
|
5
|
+
# actually contributes to the RBS environment, so a caller can warn
|
|
6
|
+
# when a configured path resolves to nothing.
|
|
7
|
+
#
|
|
8
|
+
# The failure this guards against is silent. {Environment::RbsLoader}
|
|
9
|
+
# `add`s a `signature_paths:` entry only when `path.directory?`, and
|
|
10
|
+
# only the `.rbs` files under it carry signatures — so a typo'd or
|
|
11
|
+
# moved path (or a directory holding no `.rbs`) loads zero signatures
|
|
12
|
+
# with no trace on stderr or in the run summary. The downstream symptom
|
|
13
|
+
# is the most authoritative diagnostics: every call into the extensions
|
|
14
|
+
# the missing RBS was meant to describe fires `call.undefined-method` at
|
|
15
|
+
# `evidence_tier: high`. A one-character path typo can manufacture
|
|
16
|
+
# hundreds of plausible-looking false positives; surfacing the empty
|
|
17
|
+
# entry makes the real cause visible.
|
|
18
|
+
#
|
|
19
|
+
# The audit deliberately mirrors the loader's own acceptance test
|
|
20
|
+
# (`path.directory?` + a recursive `**/*.rbs` glob) so a `:ok` verdict
|
|
21
|
+
# means the loader did load from it and a warning means it did not.
|
|
22
|
+
module SignaturePathAudit
|
|
23
|
+
# One configured `signature_paths:` entry's resolution status.
|
|
24
|
+
#
|
|
25
|
+
# `status` is one of:
|
|
26
|
+
# - `:ok` — a directory containing at least one `.rbs`.
|
|
27
|
+
# - `:missing` — the path does not exist.
|
|
28
|
+
# - `:not_directory` — the path exists but is not a directory (the
|
|
29
|
+
# loader only `add`s directories, so a `.rbs` file passed directly
|
|
30
|
+
# is silently ignored).
|
|
31
|
+
# - `:empty` — a directory with no `.rbs` file (recursive).
|
|
32
|
+
Entry = Data.define(:path, :status, :rbs_file_count) do
|
|
33
|
+
def ok?
|
|
34
|
+
status == :ok
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def warning?
|
|
38
|
+
!ok?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# One-line, human-facing reason. The wording matches the loader's
|
|
42
|
+
# actual behaviour ("loaded nothing from it") rather than the
|
|
43
|
+
# filesystem error, so the message points at the consequence.
|
|
44
|
+
def message
|
|
45
|
+
case status
|
|
46
|
+
when :missing
|
|
47
|
+
"signature_paths: #{path.inspect} does not exist (no signatures loaded from it)"
|
|
48
|
+
when :not_directory
|
|
49
|
+
"signature_paths: #{path.inspect} is not a directory (no signatures loaded from it)"
|
|
50
|
+
when :empty
|
|
51
|
+
"signature_paths: #{path.inspect} matched 0 signature files"
|
|
52
|
+
else
|
|
53
|
+
"signature_paths: #{path.inspect} loaded #{rbs_file_count} signature file(s)"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_h
|
|
58
|
+
{ "path" => path, "status" => status.to_s, "rbs_file_count" => rbs_file_count, "message" => message }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Audits each configured entry. `signature_paths` is the
|
|
63
|
+
# {Configuration#signature_paths} array (absolute paths, already
|
|
64
|
+
# resolved against the config file's directory). Pass `nil` — the
|
|
65
|
+
# unset default, where Rigor auto-detects `<root>/sig` — to get an
|
|
66
|
+
# empty result: an absent auto-detected `sig/` is a normal setup, not
|
|
67
|
+
# a misconfiguration, so it is never audited.
|
|
68
|
+
#
|
|
69
|
+
# @param signature_paths [Array<String, Pathname>, nil]
|
|
70
|
+
# @return [Array<Entry>]
|
|
71
|
+
def self.audit(signature_paths)
|
|
72
|
+
Array(signature_paths).map { |path| classify(path.to_s) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The subset of {audit} that resolved to nothing — the entries worth
|
|
76
|
+
# warning about.
|
|
77
|
+
#
|
|
78
|
+
# @param signature_paths [Array<String, Pathname>, nil]
|
|
79
|
+
# @return [Array<Entry>]
|
|
80
|
+
def self.warnings(signature_paths)
|
|
81
|
+
audit(signature_paths).select(&:warning?)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.classify(path)
|
|
85
|
+
return Entry.new(path: path, status: :missing, rbs_file_count: 0) unless File.exist?(path)
|
|
86
|
+
return Entry.new(path: path, status: :not_directory, rbs_file_count: 0) unless File.directory?(path)
|
|
87
|
+
|
|
88
|
+
count = Dir.glob(File.join(path, "**", "*.rbs")).size
|
|
89
|
+
Entry.new(path: path, status: count.zero? ? :empty : :ok, rbs_file_count: count)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/rigor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rigortype
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rigor contributors
|
|
@@ -281,6 +281,29 @@ files:
|
|
|
281
281
|
- data/builtins/ruby_core/string.yml
|
|
282
282
|
- data/builtins/ruby_core/struct.yml
|
|
283
283
|
- data/builtins/ruby_core/time.yml
|
|
284
|
+
- data/core_overlay/numeric.rbs
|
|
285
|
+
- data/core_overlay/pathname.rbs
|
|
286
|
+
- data/core_overlay/string_scanner.rbs
|
|
287
|
+
- data/gem_overlay/activesupport/core_ext.rbs
|
|
288
|
+
- data/vendored_gem_sigs/ast/ast.rbs
|
|
289
|
+
- data/vendored_gem_sigs/bcrypt/bcrypt.rbs
|
|
290
|
+
- data/vendored_gem_sigs/bundler/bundler.rbs
|
|
291
|
+
- data/vendored_gem_sigs/cgi/cgi_extras.rbs
|
|
292
|
+
- data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs
|
|
293
|
+
- data/vendored_gem_sigs/idn-ruby/idn.rbs
|
|
294
|
+
- data/vendored_gem_sigs/mysql2/client.rbs
|
|
295
|
+
- data/vendored_gem_sigs/mysql2/error.rbs
|
|
296
|
+
- data/vendored_gem_sigs/mysql2/result.rbs
|
|
297
|
+
- data/vendored_gem_sigs/mysql2/statement.rbs
|
|
298
|
+
- data/vendored_gem_sigs/nokogiri/nokogiri.rbs
|
|
299
|
+
- data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs
|
|
300
|
+
- data/vendored_gem_sigs/pg/pg.rbs
|
|
301
|
+
- data/vendored_gem_sigs/prism/prism_supplement.rbs
|
|
302
|
+
- data/vendored_gem_sigs/redis/errors.rbs
|
|
303
|
+
- data/vendored_gem_sigs/redis/future.rbs
|
|
304
|
+
- data/vendored_gem_sigs/redis/redis.rbs
|
|
305
|
+
- data/vendored_gem_sigs/redis/redis_extras.rbs
|
|
306
|
+
- data/vendored_gem_sigs/rubygems/rubygems_extras.rbs
|
|
284
307
|
- exe/rigor
|
|
285
308
|
- lib/rigor.rb
|
|
286
309
|
- lib/rigor/analysis/baseline.rb
|
|
@@ -343,12 +366,15 @@ files:
|
|
|
343
366
|
- lib/rigor/cli/ci_detector.rb
|
|
344
367
|
- lib/rigor/cli/command.rb
|
|
345
368
|
- lib/rigor/cli/coverage_command.rb
|
|
369
|
+
- lib/rigor/cli/coverage_mutation.rb
|
|
346
370
|
- lib/rigor/cli/coverage_renderer.rb
|
|
347
371
|
- lib/rigor/cli/coverage_report.rb
|
|
348
372
|
- lib/rigor/cli/coverage_scan.rb
|
|
349
373
|
- lib/rigor/cli/diagnostic_formats.rb
|
|
350
374
|
- lib/rigor/cli/diff_command.rb
|
|
351
375
|
- lib/rigor/cli/explain_command.rb
|
|
376
|
+
- lib/rigor/cli/fused_protection_renderer.rb
|
|
377
|
+
- lib/rigor/cli/fused_protection_report.rb
|
|
352
378
|
- lib/rigor/cli/lsp_command.rb
|
|
353
379
|
- lib/rigor/cli/mcp_command.rb
|
|
354
380
|
- lib/rigor/cli/mutation_protection_renderer.rb
|
|
@@ -373,6 +399,7 @@ files:
|
|
|
373
399
|
- lib/rigor/cli/type_scan_command.rb
|
|
374
400
|
- lib/rigor/cli/type_scan_renderer.rb
|
|
375
401
|
- lib/rigor/cli/type_scan_report.rb
|
|
402
|
+
- lib/rigor/config_audit.rb
|
|
376
403
|
- lib/rigor/configuration.rb
|
|
377
404
|
- lib/rigor/configuration/dependencies.rb
|
|
378
405
|
- lib/rigor/configuration/severity_profile.rb
|
|
@@ -519,8 +546,10 @@ files:
|
|
|
519
546
|
- lib/rigor/plugin/source_rbs_synthesis_reporter.rb
|
|
520
547
|
- lib/rigor/plugin/trust_policy.rb
|
|
521
548
|
- lib/rigor/plugin/type_node_resolver.rb
|
|
549
|
+
- lib/rigor/protection/diagnostic_oracle.rb
|
|
522
550
|
- lib/rigor/protection/mutation_scanner.rb
|
|
523
551
|
- lib/rigor/protection/mutator.rb
|
|
552
|
+
- lib/rigor/protection/test_suite_oracle.rb
|
|
524
553
|
- lib/rigor/rbs_extended.rb
|
|
525
554
|
- lib/rigor/rbs_extended/conformance_checker.rb
|
|
526
555
|
- lib/rigor/rbs_extended/hkt_directives.rb
|
|
@@ -540,6 +569,7 @@ files:
|
|
|
540
569
|
- lib/rigor/sig_gen/type_elaborator.rb
|
|
541
570
|
- lib/rigor/sig_gen/write_result.rb
|
|
542
571
|
- lib/rigor/sig_gen/writer.rb
|
|
572
|
+
- lib/rigor/signature_path_audit.rb
|
|
543
573
|
- lib/rigor/source.rb
|
|
544
574
|
- lib/rigor/source/constant_path.rb
|
|
545
575
|
- lib/rigor/source/literals.rb
|