rigortype 0.4.2 → 0.4.4
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 +1 -1
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +26 -9
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +21 -1
|
@@ -377,7 +377,7 @@ module Rigor
|
|
|
377
377
|
# Parse a SKILL.md's leading `---`-delimited YAML frontmatter. Returns {} on any missing or malformed block so the
|
|
378
378
|
# catalogue degrades to a name-only entry rather than raising.
|
|
379
379
|
def frontmatter(path)
|
|
380
|
-
text = File.read(path)
|
|
380
|
+
text = File.read(path, encoding: Encoding::UTF_8)
|
|
381
381
|
return {} unless text.start_with?("---\n")
|
|
382
382
|
|
|
383
383
|
closing = text.index("\n---\n", 4)
|
|
@@ -220,7 +220,8 @@ module Rigor
|
|
|
220
220
|
# so probes against files outside the current process's CWD still see the right `sig/` tree.
|
|
221
221
|
# Issue #1700 — an editor buffer is scanned for required features in place of the file it stands for.
|
|
222
222
|
def project_environment(files, configuration, buffer = nil)
|
|
223
|
-
ProjectEnvironment.
|
|
223
|
+
patched = ProjectEnvironment.project_patched_methods(configuration, buffer: buffer)
|
|
224
|
+
ProjectEnvironment.build(configuration: configuration, source_files: files, project_patched_methods: patched,
|
|
224
225
|
feature_sources: ProjectEnvironment.buffer_sources(buffer))
|
|
225
226
|
end
|
|
226
227
|
|
|
@@ -49,8 +49,8 @@ module Rigor
|
|
|
49
49
|
|
|
50
50
|
configuration = Configuration.load(options.fetch(:config))
|
|
51
51
|
paths = @argv.empty? ? configuration.paths : @argv
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
establish_root(configuration)
|
|
53
|
+
scan_cache = open_scan_cache(configuration)
|
|
54
54
|
declarations, references, dynamic_uses, shadows = scan(paths, configuration, scan_cache)
|
|
55
55
|
references.concat(signature_references(configuration))
|
|
56
56
|
dynamic_uses.concat(template_mentions(declarations, scan_cache))
|
|
@@ -68,6 +68,47 @@ module Rigor
|
|
|
68
68
|
|
|
69
69
|
private
|
|
70
70
|
|
|
71
|
+
# A scan's role and the spelling it records depend on the root and the working directory (#1760): a role
|
|
72
|
+
# reads every spelling below either ({#role_of}), and a relative path argument is recorded as written
|
|
73
|
+
# relative to the working directory.
|
|
74
|
+
def open_scan_cache(configuration)
|
|
75
|
+
cwd = canonical(Dir.pwd)
|
|
76
|
+
spelled = cwd == @root ? "" : ":#{cwd}"
|
|
77
|
+
Analysis::Reachability::ScanCache.open(configuration.cache_path, target_ruby: configuration.target_ruby,
|
|
78
|
+
root: "#{@root}#{spelled}")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The root, and its spellings: the realpath and the logical directory it was named by.
|
|
82
|
+
def establish_root(configuration)
|
|
83
|
+
@root = project_root(configuration)
|
|
84
|
+
logical = @root == canonical(Dir.pwd) ? Dir.pwd : configuration.exclude_base
|
|
85
|
+
@root_spellings = [@root, File.absolute_path(logical)].uniq
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The project root (#1760), realpath'd: the config file's directory, unless the config sits below the
|
|
89
|
+
# working directory (`--config=ci/rigor.yml` from the project root), where the working directory is the
|
|
90
|
+
# project. Run from a subdirectory, or from a directory unrelated to the config, it is the config's.
|
|
91
|
+
# The root never rises above the config's directory or the working directory, so the role rules, which
|
|
92
|
+
# anchor `qa/`, `e2e/` and `features/` at the root, keep reading the project; an analysed path outside
|
|
93
|
+
# it reads as `../…`.
|
|
94
|
+
def project_root(configuration)
|
|
95
|
+
base = canonical(configuration.exclude_base)
|
|
96
|
+
cwd = canonical(Dir.pwd)
|
|
97
|
+
inside?(base, cwd) && base != cwd ? cwd : base
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def inside?(path, dir)
|
|
101
|
+
path.start_with?("#{dir.chomp('/')}/")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The directories the wider corpus is read from: the root, and the working directory when it lies
|
|
105
|
+
# outside the root. Reading the working directory too keeps the corpus no narrower than it was when
|
|
106
|
+
# the working directory alone was read; a reference only ever makes a declaration more reachable.
|
|
107
|
+
def corpus_dirs
|
|
108
|
+
cwd = canonical(Dir.pwd)
|
|
109
|
+
cwd == @root || inside?(cwd, @root) ? [@root] : [@root, cwd]
|
|
110
|
+
end
|
|
111
|
+
|
|
71
112
|
def build_graph(configuration, options, contribution, declarations:, references:, dynamic_uses:, shadows:)
|
|
72
113
|
Analysis::Reachability::Graph.new(
|
|
73
114
|
declarations: declarations, references: references, dynamic_uses: dynamic_uses, shadows: shadows,
|
|
@@ -111,21 +152,52 @@ module Rigor
|
|
|
111
152
|
# references resolve to first (#1732).
|
|
112
153
|
# Every file is consulted every run — an unchanged one contributes its cached scan, so the
|
|
113
154
|
# whole-project completeness the `--incremental` refusal protects is unaffected.
|
|
155
|
+
#
|
|
156
|
+
# A file is parsed once however it is spelled (#1760), but every spelling of it keeps what it referred
|
|
157
|
+
# to under its own path and role, exactly as when each spelling was scanned separately: dropping a
|
|
158
|
+
# spelling lost its role, and with it a production reference a `spec/` symlink to the same file would
|
|
159
|
+
# otherwise have hidden. Only the declarations are taken once, from the first analysed spelling, which
|
|
160
|
+
# is the one the report prints; any other spelling's declarations are shadows, as a wider-corpus
|
|
161
|
+
# file's are (#1732).
|
|
114
162
|
def scan(paths, configuration, cache)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
dynamic_uses = []
|
|
120
|
-
(declaration_files + reference_files(paths, configuration)).sort.each do |file|
|
|
121
|
-
result = cache.serve(:scan, file) { read_and_scan(file, configuration) }
|
|
163
|
+
out = { declarations: [], references: [], dynamic_uses: [], shadows: [] }
|
|
164
|
+
file_spellings(paths, configuration).sort.each do |real, list|
|
|
165
|
+
primary, analysed = list.find { |_, declared| declared } || list.first
|
|
166
|
+
result = cache.serve(:scan, primary) { read_and_scan(primary, real, configuration) }
|
|
122
167
|
next if result.nil?
|
|
123
168
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
dynamic_uses.concat(result.dynamic_uses)
|
|
169
|
+
out[analysed ? :declarations : :shadows].concat(result.declarations)
|
|
170
|
+
list.map(&:first).each { |file| add_spelling(out, result, file, real, primary) }
|
|
127
171
|
end
|
|
128
|
-
|
|
172
|
+
out.values_at(:declarations, :references, :dynamic_uses, :shadows)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def add_spelling(out, result, file, real, primary)
|
|
176
|
+
out[:shadows].concat(result.declarations.map { |d| d.with(path: file) }) unless file == primary
|
|
177
|
+
role = role_of(file, real)
|
|
178
|
+
out[:references].concat(result.references.map { |r| r.with(path: file, role: role) })
|
|
179
|
+
out[:dynamic_uses].concat(result.dynamic_uses.map { |u| respelled(u, file, role) })
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# `{realpath => [[spelling, analysed?], ...]}`: the analysed spellings first, in `paths:` order, then
|
|
183
|
+
# the wider corpus's.
|
|
184
|
+
def file_spellings(paths, configuration)
|
|
185
|
+
analysed = Analysis::PathExpansion.ruby_files(paths, configuration.exclude_patterns,
|
|
186
|
+
base: configuration.exclude_base)
|
|
187
|
+
spellings = Hash.new { |hash, real| hash[real] = [] }
|
|
188
|
+
analysed.each { |file| spellings[canonical(file)] << [file, true] }
|
|
189
|
+
reference_files(paths, configuration).each { |file| spellings[canonical(file)] << [file, false] }
|
|
190
|
+
spellings = spellings.transform_values { |list| list.uniq(&:first) }
|
|
191
|
+
@analysed_spellings = spellings.transform_values do |list|
|
|
192
|
+
list.filter_map do |file, analysed|
|
|
193
|
+
file if analysed
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
spellings
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def respelled(use, file, role)
|
|
200
|
+
use.with(path: file, role: role, site: use.site && "#{file}:#{use.line}")
|
|
129
201
|
end
|
|
130
202
|
|
|
131
203
|
# The reference corpus is the PROJECT, not the analysed paths. A constant declared in `lib/` is commonly
|
|
@@ -133,21 +205,80 @@ module Rigor
|
|
|
133
205
|
# treating those as absent is what manufactured artifacts on two of three corpus targets in #345.
|
|
134
206
|
# Widening the *declaration* set the same way would cancel the gain (measured: +1 / −3 / +2 candidates,
|
|
135
207
|
# ADR-102 WD2), which is exactly why the two corpora are separated rather than both widened.
|
|
208
|
+
#
|
|
209
|
+
# The files are read from every {#corpus_dirs} entry (#1760): running from a subdirectory or through a
|
|
210
|
+
# symlinked spelling reads the same project.
|
|
136
211
|
def reference_files(_paths, configuration)
|
|
137
|
-
|
|
138
|
-
absolute
|
|
139
|
-
|
|
212
|
+
absolute = project_files(REFERENCE_GLOB)
|
|
213
|
+
Analysis::PathExpansion.reject_excluded(absolute, configuration.exclude_patterns,
|
|
214
|
+
base: configuration.exclude_base)
|
|
140
215
|
end
|
|
141
216
|
|
|
142
|
-
|
|
217
|
+
# The role is read from the file's realpath relative to the project root (#1760), so `test/helper.rb`
|
|
218
|
+
# is test code whichever directory the command runs from. A file outside the root reads as `../…`,
|
|
219
|
+
# which names no directory above the root.
|
|
220
|
+
def read_and_scan(file, real, configuration)
|
|
143
221
|
Analysis::Reachability::Scan.call(path: file, source: File.read(file),
|
|
144
222
|
target_ruby: configuration.target_ruby,
|
|
145
|
-
|
|
223
|
+
role: role_of(file, real))
|
|
146
224
|
rescue SystemCallError
|
|
147
225
|
nil
|
|
148
226
|
end
|
|
149
227
|
|
|
150
|
-
#
|
|
228
|
+
# Absolute paths of the project's own files matching `glob`, under every corpus directory.
|
|
229
|
+
def project_files(glob)
|
|
230
|
+
corpus_dirs.flat_map do |dir|
|
|
231
|
+
Analysis::Reachability::ProjectFiles.own(Dir.glob(glob, base: dir), dir).map { |rel| File.join(dir, rel) }
|
|
232
|
+
end.uniq
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def canonical(path)
|
|
236
|
+
File.realpath(path)
|
|
237
|
+
rescue SystemCallError
|
|
238
|
+
File.absolute_path(path)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# A file's role (#1760), read from its spellings as written below the project root or the working
|
|
242
|
+
# directory: the first one below the root decides, and any of them reading as test code makes it test
|
|
243
|
+
# code, so a `features/` tree below a subdirectory run's working directory stays test code as it was when
|
|
244
|
+
# the working directory was the only root. The realpath is read only when no written spelling lies below
|
|
245
|
+
# either: a symlink whose target runs through a `test/` directory must not turn the production files
|
|
246
|
+
# reached through it into test code.
|
|
247
|
+
def role_of(file, real)
|
|
248
|
+
spellings = below(file, @root_spellings) + below(file, cwd_spellings)
|
|
249
|
+
spellings = below(real, @root_spellings) + below(real, cwd_spellings) if spellings.empty?
|
|
250
|
+
role_for = Analysis::Reachability::Scan.method(:role_for)
|
|
251
|
+
return :test if spellings.uniq.any? { |path| role_for.call(path) == :test }
|
|
252
|
+
|
|
253
|
+
role_for.call(spellings.first || relative_to_root(real))
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# `path` relative to every directory in `dirs` it lies below.
|
|
257
|
+
def below(path, dirs)
|
|
258
|
+
absolute = File.absolute_path(path)
|
|
259
|
+
dirs.filter_map { |dir| absolute.delete_prefix("#{dir.chomp('/')}/") if inside?(absolute, dir) }.uniq
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# The working directory as its realpath and, when the shell's `PWD` names it, as that logical spelling.
|
|
263
|
+
def cwd_spellings
|
|
264
|
+
@cwd_spellings ||= begin
|
|
265
|
+
logical = ENV.fetch("PWD", nil)
|
|
266
|
+
real = canonical(Dir.pwd)
|
|
267
|
+
logical && canonical(logical) == real ? [real, Dir.pwd, logical].uniq : [real, Dir.pwd].uniq
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def relative_to_root(real)
|
|
272
|
+
prefix = "#{@root.chomp('/')}/"
|
|
273
|
+
return real.delete_prefix(prefix) if real.start_with?(prefix)
|
|
274
|
+
|
|
275
|
+
Pathname.new(real).relative_path_from(Pathname.new(@root)).to_s
|
|
276
|
+
rescue ArgumentError
|
|
277
|
+
real
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# A glob is matched against every analysed spelling of the declaration's file, and each of them and its
|
|
281
|
+
# realpath relative to the project root (#1760) and to the working directory:
|
|
151
282
|
# `configuration.paths` are expanded to absolute, so a user-written `--entry-point=lib/entry.rb` would
|
|
152
283
|
# otherwise never match anything and silently produce a root set of zero.
|
|
153
284
|
# A constant named only from the project's own `sig/` is referenced — the #345 probe reported exactly this
|
|
@@ -181,8 +312,8 @@ module Rigor
|
|
|
181
312
|
names = declarations.map(&:fqn)
|
|
182
313
|
return [] if names.empty?
|
|
183
314
|
|
|
184
|
-
|
|
185
|
-
|
|
315
|
+
project_files(TEMPLATE_GLOB).flat_map do |absolute|
|
|
316
|
+
rel = relative_to_root(absolute)
|
|
186
317
|
haystack = cache.serve(:runs, absolute) { constant_bearing_text(File.read(absolute).scrub) }
|
|
187
318
|
names.filter_map do |fqn|
|
|
188
319
|
next unless haystack.include?(fqn)
|
|
@@ -213,10 +344,12 @@ module Rigor
|
|
|
213
344
|
return [] if globs.empty?
|
|
214
345
|
|
|
215
346
|
matcher = Analysis::Reachability::ProjectFiles.method(:entry_point_match?)
|
|
216
|
-
cwd = "#{File.absolute_path(Dir.pwd)}/"
|
|
217
347
|
declarations.filter_map do |d|
|
|
218
|
-
|
|
219
|
-
|
|
348
|
+
real = canonical(d.path)
|
|
349
|
+
dirs = @root_spellings + cwd_spellings
|
|
350
|
+
written = @analysed_spellings.fetch(real, [d.path]) | [d.path]
|
|
351
|
+
spellings = [*written, relative_to_root(real), *written.flat_map { |w| below(w, dirs) }, *below(real, dirs)]
|
|
352
|
+
d.fqn if globs.any? { |g| spellings.any? { |path| matcher.call(g, path) } }
|
|
220
353
|
end
|
|
221
354
|
end
|
|
222
355
|
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -51,6 +51,7 @@ module Rigor
|
|
|
51
51
|
"init" => :run_init,
|
|
52
52
|
"annotate" => :run_annotate,
|
|
53
53
|
"type-of" => :run_type_of,
|
|
54
|
+
"lens" => :run_lens,
|
|
54
55
|
"trace" => :run_trace,
|
|
55
56
|
"type-scan" => :run_type_scan,
|
|
56
57
|
"effects" => :run_effects,
|
|
@@ -291,18 +292,36 @@ module Rigor
|
|
|
291
292
|
|
|
292
293
|
# The settings `rigor init` writes: the defaults, with `test_paths:` spelled out as the test roots found now, so
|
|
293
294
|
# the project's config says where its tests are rather than leaving it to auto-detection. The roots are written
|
|
294
|
-
# relative to the config file's directory, which is what {Configuration.load} resolves them against.
|
|
295
|
+
# relative to the config file's directory, which is what {Configuration.load} resolves them against. `paths:`
|
|
296
|
+
# is written the same way: the default `lib` is resolved against the physical cwd, so a config written to
|
|
297
|
+
# `config/rigor.yml` must say `../lib`, or it loads back as `config/lib` (#1399).
|
|
295
298
|
#
|
|
296
299
|
# `target_ruby` is left out (the template writes it commented): a written key is a statement about the runtime
|
|
297
300
|
# (#1692), and the starter must not make one the user never chose.
|
|
298
301
|
def init_settings(path)
|
|
302
|
+
config_dir = config_dir_of(path)
|
|
299
303
|
settings = Configuration::DEFAULTS.except("target_ruby")
|
|
304
|
+
settings = settings.merge("paths" => roots_relative_to(settings.fetch("paths"), config_dir))
|
|
300
305
|
test_paths = Configuration.new.resolved_test_paths
|
|
301
306
|
return settings if test_paths.empty?
|
|
302
307
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
308
|
+
settings.merge("test_paths" => roots_relative_to(test_paths, config_dir))
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# The directory the config file's roots are written relative to. An absolute `--path` is taken through its
|
|
312
|
+
# physical directory: the roots are resolved against the physical cwd (`Dir.pwd` is getcwd), so a logical
|
|
313
|
+
# spelling through a symlink makes the lexical `..` climb a different tree than the kernel does (#1399 review).
|
|
314
|
+
# A relative `--path` stays lexical, as {Configuration.load} resolves it.
|
|
315
|
+
def config_dir_of(path)
|
|
316
|
+
dir = File.dirname(path)
|
|
317
|
+
Pathname(Pathname(path).absolute? ? File.realdirpath(dir) : File.absolute_path(dir))
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Each root is resolved against the physical cwd (where `rigor` reads it from), then written relative to the config
|
|
321
|
+
# dir. With the config at the project root, both sides are the same physical directory, so the roots come out as
|
|
322
|
+
# they are spelled from it (`lib`, `spec`), which is master's output for the default config.
|
|
323
|
+
def roots_relative_to(roots, config_dir)
|
|
324
|
+
roots.map { |root| Pathname(File.absolute_path(root)).relative_path_from(config_dir).to_s }
|
|
306
325
|
end
|
|
307
326
|
|
|
308
327
|
def run_annotate
|
|
@@ -317,6 +336,12 @@ module Rigor
|
|
|
317
336
|
TypeOfCommand.new(argv: @argv, out: @out, err: @err).run
|
|
318
337
|
end
|
|
319
338
|
|
|
339
|
+
def run_lens
|
|
340
|
+
require_relative "cli/lens_command"
|
|
341
|
+
|
|
342
|
+
LensCommand.new(argv: @argv, out: @out, err: @err).run
|
|
343
|
+
end
|
|
344
|
+
|
|
320
345
|
def run_trace
|
|
321
346
|
require_relative "cli/trace_command"
|
|
322
347
|
|
|
@@ -463,6 +488,7 @@ module Rigor
|
|
|
463
488
|
init Create a starter .rigor.yml
|
|
464
489
|
annotate Print FILE with each line's last-expression type
|
|
465
490
|
type-of Print inferred types at FILE:LINE[:COL] positions
|
|
491
|
+
lens Map a file's declarations with their types and provenance (experimental)
|
|
466
492
|
trace Replay how the engine typed FILE as a terminal animation
|
|
467
493
|
type-scan Report Scope#type_of coverage across PATHs
|
|
468
494
|
effects Report each method's effect labels, and the committed effect snapshot
|
data/lib/rigor/config_audit.rb
CHANGED
|
@@ -26,11 +26,12 @@ module Rigor
|
|
|
26
26
|
#
|
|
27
27
|
# Every check is held to the same bar that {SignaturePathAudit} set: it mirrors the
|
|
28
28
|
# loader's own acceptance test, so a warning means the loader really did load nothing, and
|
|
29
|
-
# it never fires on a setup that works. In particular the
|
|
30
|
-
#
|
|
31
|
-
# (whose family Rigor cannot statically enumerate, since a `node_rule` block emits
|
|
32
|
-
# `rule:` string it likes) is left alone
|
|
33
|
-
# a
|
|
29
|
+
# it never fires on a setup that works. In particular the `severity_overrides:` check only
|
|
30
|
+
# flags a key under a built-in family (`call`, `flow`, …) — a plugin- or `rbs_extended.*`
|
|
31
|
+
# rule id (whose family Rigor cannot statically enumerate, since a `node_rule` block emits
|
|
32
|
+
# any `rule:` string it likes) is left alone. The `disable:` check goes further because the
|
|
33
|
+
# list's reach is enumerable (issue #1953): a plugin's row answers only to its qualified
|
|
34
|
+
# `plugin.<id>.<rule>` id, so a bare plugin rule there is inert (see {.disable_token_warning}).
|
|
34
35
|
module ConfigAudit
|
|
35
36
|
# One config-level finding. `kind` discriminates the source key (`:signature_path`,
|
|
36
37
|
# `:bundled_plugin_signature_path`, `:library`, `:disabled_rule`, `:severity_override`,
|
|
@@ -146,18 +147,13 @@ module Rigor
|
|
|
146
147
|
[]
|
|
147
148
|
end
|
|
148
149
|
|
|
149
|
-
# `disable:` tokens
|
|
150
|
-
#
|
|
151
|
-
# docstring).
|
|
150
|
+
# `disable:` tokens that reach no row ({.disable_token_warning}) and `severity_overrides:` keys
|
|
151
|
+
# that name no rule. An override key is flagged only under a built-in family, so a plugin
|
|
152
|
+
# rule id is never mis-flagged (see the module docstring).
|
|
152
153
|
def self.rule_token_warnings(configuration)
|
|
154
|
+
plugin_ids = configured_plugin_ids(configuration.plugins)
|
|
153
155
|
disable = Array(configuration.disabled_rules).filter_map do |token|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
Warning.new(
|
|
157
|
-
kind: :disabled_rule,
|
|
158
|
-
message: "disable: #{token.to_s.inspect} is not a recognized rule id; the suppression has no effect",
|
|
159
|
-
fields: { "token" => token.to_s }
|
|
160
|
-
)
|
|
156
|
+
disable_token_warning(token.to_s, plugin_ids)
|
|
161
157
|
end
|
|
162
158
|
overrides = configuration.severity_overrides.keys.filter_map do |key|
|
|
163
159
|
next unless inert_builtin_token?(key.to_s)
|
|
@@ -171,6 +167,106 @@ module Rigor
|
|
|
171
167
|
disable + overrides
|
|
172
168
|
end
|
|
173
169
|
|
|
170
|
+
# Issue #1953 — the run-level rows the diagnostic policy keeps on `severity_overrides:` and the
|
|
171
|
+
# baseline. `disable:` does not reach them by any spelling, so naming one there is inert.
|
|
172
|
+
RESERVED_RUN_LEVEL_FAMILIES = %w[plugin_loader plugin_trust rbs.coverage dynamic.dependency-source].freeze
|
|
173
|
+
RESERVED_RUN_LEVEL_IDS = %w[load-error runtime-error pool-degraded].freeze
|
|
174
|
+
private_constant :RESERVED_RUN_LEVEL_FAMILIES, :RESERVED_RUN_LEVEL_IDS
|
|
175
|
+
|
|
176
|
+
# A warning for a `disable:` token that can reach no row, or nil. Since issue #1953 the list's
|
|
177
|
+
# reach is exactly enumerable for three shapes, and those are what this flags:
|
|
178
|
+
#
|
|
179
|
+
# - a token naming a reserved run-level row ({RESERVED_RUN_LEVEL_FAMILIES} /
|
|
180
|
+
# {RESERVED_RUN_LEVEL_IDS}), which the list never reaches;
|
|
181
|
+
# - a `plugin.`-qualified token naming no plugin `plugins:` configures — checked only when
|
|
182
|
+
# every entry's manifest id is known without loading it ({.configured_plugin_ids});
|
|
183
|
+
# - any other token that is not `all`, a family, a legacy alias or a catalogue / `pre-eval.*`
|
|
184
|
+
# id. A bare id never reaches a plugin's row, so a plugin's bare rule is inert here too.
|
|
185
|
+
#
|
|
186
|
+
# A token under a non-check engine family (`rbs_extended.*`, `rbs.*`, `dynamic.*`) or a known
|
|
187
|
+
# non-catalogue engine id stays unflagged: under-warning is this module's FP-safe direction.
|
|
188
|
+
def self.disable_token_warning(token, plugin_ids)
|
|
189
|
+
return nil if effective_disable_token?(token)
|
|
190
|
+
|
|
191
|
+
reason = disable_token_problem(token, plugin_ids)
|
|
192
|
+
return nil if reason.nil?
|
|
193
|
+
|
|
194
|
+
Warning.new(kind: :disabled_rule, message: "disable: #{token.inspect} #{reason}",
|
|
195
|
+
fields: { "token" => token })
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.disable_token_problem(token, plugin_ids)
|
|
199
|
+
if reserved_run_level_token?(token)
|
|
200
|
+
"names a run-level row, which `disable:` does not reach; use `severity_overrides:` or a baseline"
|
|
201
|
+
elsif token.start_with?(Analysis::CheckRules::PLUGIN_FAMILY_PREFIX)
|
|
202
|
+
unless plugin_ids.nil? || plausible_plugin_token?(token, plugin_ids)
|
|
203
|
+
"names no plugin `plugins:` configures; the suppression has no effect"
|
|
204
|
+
end
|
|
205
|
+
elsif inert_builtin_token?(token)
|
|
206
|
+
"is not a recognized rule id; the suppression has no effect"
|
|
207
|
+
elsif !non_check_engine_token?(token)
|
|
208
|
+
"is not a recognized rule id; the suppression has no effect " \
|
|
209
|
+
"(if this is a plugin's rule, write it as `plugin.<id>.<rule>`)"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
private_class_method :disable_token_problem
|
|
213
|
+
|
|
214
|
+
def self.effective_disable_token?(token)
|
|
215
|
+
return true if token == "all"
|
|
216
|
+
|
|
217
|
+
!Array(Analysis::CheckRules.resolve_rule_token(token)).empty?
|
|
218
|
+
end
|
|
219
|
+
private_class_method :effective_disable_token?
|
|
220
|
+
|
|
221
|
+
def self.reserved_run_level_token?(token)
|
|
222
|
+
RESERVED_RUN_LEVEL_IDS.include?(token) ||
|
|
223
|
+
RESERVED_RUN_LEVEL_FAMILIES.any? { |family| token == family || token.start_with?("#{family}.") }
|
|
224
|
+
end
|
|
225
|
+
private_class_method :reserved_run_level_token?
|
|
226
|
+
|
|
227
|
+
def self.non_check_engine_token?(token)
|
|
228
|
+
Analysis::CheckRules::NON_CHECK_DIAGNOSTIC_IDS.include?(token) ||
|
|
229
|
+
Analysis::CheckRules::NON_CHECK_DIAGNOSTIC_FAMILIES.include?(token.split(".").first)
|
|
230
|
+
end
|
|
231
|
+
private_class_method :non_check_engine_token?
|
|
232
|
+
|
|
233
|
+
# `plugin.<id>` or `plugin.<id>.<rule>` for a configured id. A manifest id may itself hold a
|
|
234
|
+
# dot, so the token is matched against each id rather than split.
|
|
235
|
+
def self.plausible_plugin_token?(token, plugin_ids)
|
|
236
|
+
rest = token.delete_prefix(Analysis::CheckRules::PLUGIN_FAMILY_PREFIX)
|
|
237
|
+
plugin_ids.any? { |id| rest == id || rest.start_with?("#{id}.") }
|
|
238
|
+
end
|
|
239
|
+
private_class_method :plausible_plugin_token?
|
|
240
|
+
|
|
241
|
+
# The manifest id of every `plugins:` entry, or nil when one cannot be known without loading
|
|
242
|
+
# the plugin. An explicit `id:` is the id; a bundled plugin's id is its gem name minus the
|
|
243
|
+
# `rigor-` prefix ({Plugin::FirstParty}); a third-party gem named without `id:` may register
|
|
244
|
+
# any id, so its presence turns the plugin-token check off. An `enabled: false` entry counts:
|
|
245
|
+
# the user has found the plugin, and under-warning is the FP-safe direction.
|
|
246
|
+
def self.configured_plugin_ids(plugins)
|
|
247
|
+
Array(plugins).each_with_object(Set.new) do |raw, ids|
|
|
248
|
+
entry = raw.is_a?(Hash) ? raw.to_h { |key, value| [key.to_s, value] } : { "gem" => raw.to_s }
|
|
249
|
+
if entry["id"]
|
|
250
|
+
ids << entry["id"].to_s
|
|
251
|
+
elsif bundled_plugin_gem?(entry["gem"].to_s)
|
|
252
|
+
ids << entry["gem"].to_s.delete_prefix(BUNDLED_GEM_PREFIX)
|
|
253
|
+
else
|
|
254
|
+
return nil
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
private_class_method :configured_plugin_ids
|
|
259
|
+
|
|
260
|
+
BUNDLED_GEM_PREFIX = "rigor-"
|
|
261
|
+
private_constant :BUNDLED_GEM_PREFIX
|
|
262
|
+
|
|
263
|
+
def self.bundled_plugin_gem?(gem)
|
|
264
|
+
return false unless gem.start_with?(BUNDLED_GEM_PREFIX)
|
|
265
|
+
|
|
266
|
+
File.file?(File.join(SignaturePathAudit::BUNDLED_PLUGINS_ROOT, gem, "lib", "#{gem}.rb"))
|
|
267
|
+
end
|
|
268
|
+
private_class_method :bundled_plugin_gem?
|
|
269
|
+
|
|
174
270
|
# True when `token` looks like a built-in rule id but matches none — its first segment
|
|
175
271
|
# is a built-in family yet it is neither a bare family wildcard nor a known canonical id.
|
|
176
272
|
# A token whose family is not built-in (a plugin / `rbs_extended.*` rule, or a bare
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../configuration_error"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
class Configuration
|
|
5
7
|
# Parsed `dependencies:` section of `.rigor.yml`. Per
|
|
@@ -56,7 +58,7 @@ module Rigor
|
|
|
56
58
|
# Hash with `source_inference:` and / or `budget_per_gem:` / `budget_overrun_strategy:` present.
|
|
57
59
|
def self.from_h(data)
|
|
58
60
|
return new([]) if data.nil?
|
|
59
|
-
raise
|
|
61
|
+
raise ConfigurationError, "dependencies: must be a Hash, got #{data.inspect}" unless data.is_a?(Hash)
|
|
60
62
|
|
|
61
63
|
raw_entries = coerce_source_inference(data["source_inference"])
|
|
62
64
|
entries, warnings = dedupe_entries(raw_entries)
|
|
@@ -139,7 +141,7 @@ module Rigor
|
|
|
139
141
|
return [] if value.nil? || value == false
|
|
140
142
|
|
|
141
143
|
unless value.is_a?(Array)
|
|
142
|
-
raise
|
|
144
|
+
raise ConfigurationError,
|
|
143
145
|
"dependencies.source_inference: must be a list of entries " \
|
|
144
146
|
"(or false / omitted to disable), got #{value.inspect}"
|
|
145
147
|
end
|
|
@@ -149,7 +151,7 @@ module Rigor
|
|
|
149
151
|
|
|
150
152
|
def coerce_entry(raw)
|
|
151
153
|
unless raw.is_a?(Hash)
|
|
152
|
-
raise
|
|
154
|
+
raise ConfigurationError,
|
|
153
155
|
"dependencies.source_inference[] entry must be a Hash, got #{raw.inspect}"
|
|
154
156
|
end
|
|
155
157
|
|
|
@@ -162,7 +164,7 @@ module Rigor
|
|
|
162
164
|
|
|
163
165
|
def coerce_gem(value)
|
|
164
166
|
unless value.is_a?(String) && !value.empty?
|
|
165
|
-
raise
|
|
167
|
+
raise ConfigurationError,
|
|
166
168
|
"dependencies.source_inference[].gem must be a non-empty String, got #{value.inspect}"
|
|
167
169
|
end
|
|
168
170
|
|
|
@@ -170,28 +172,34 @@ module Rigor
|
|
|
170
172
|
end
|
|
171
173
|
|
|
172
174
|
def coerce_mode(value)
|
|
173
|
-
mode = (value || "when_missing")
|
|
175
|
+
mode = config_symbol(value || "when_missing")
|
|
174
176
|
return mode if VALID_MODES.include?(mode)
|
|
175
177
|
|
|
176
|
-
raise
|
|
178
|
+
raise ConfigurationError,
|
|
177
179
|
"dependencies.source_inference[].mode must be one of " \
|
|
178
180
|
"#{VALID_MODES.inspect}, got #{value.inspect}"
|
|
179
181
|
end
|
|
180
182
|
|
|
183
|
+
# The Symbol a String / Symbol value names, or nil for any other YAML shape (a number, a list, a mapping), which
|
|
184
|
+
# the caller then reports as not one of its valid values rather than crashing on `#to_sym`.
|
|
185
|
+
def config_symbol(value)
|
|
186
|
+
value.to_sym if value.is_a?(String) || value.is_a?(Symbol)
|
|
187
|
+
end
|
|
188
|
+
|
|
181
189
|
def coerce_roots(raw)
|
|
182
190
|
roots = Array(raw.fetch("roots", DEFAULT_ROOTS)).map(&:to_s).freeze
|
|
183
191
|
return roots unless roots.empty?
|
|
184
192
|
|
|
185
|
-
raise
|
|
193
|
+
raise ConfigurationError,
|
|
186
194
|
"dependencies.source_inference[].roots must not be empty when supplied " \
|
|
187
195
|
"(omit the key to fall back to the default #{DEFAULT_ROOTS.inspect})"
|
|
188
196
|
end
|
|
189
197
|
|
|
190
198
|
def coerce_budget_overrun_strategy(value)
|
|
191
|
-
symbol = value
|
|
199
|
+
symbol = config_symbol(value)
|
|
192
200
|
return symbol if VALID_BUDGET_OVERRUN_STRATEGIES.include?(symbol)
|
|
193
201
|
|
|
194
|
-
raise
|
|
202
|
+
raise ConfigurationError,
|
|
195
203
|
"dependencies.budget_overrun_strategy must be one of " \
|
|
196
204
|
"#{VALID_BUDGET_OVERRUN_STRATEGIES.inspect}, got #{value.inspect}"
|
|
197
205
|
end
|
|
@@ -201,13 +209,13 @@ module Rigor
|
|
|
201
209
|
# MIN_BUDGET_PER_GEM .. MAX_BUDGET_PER_GEM (i.e. 0.25× – 4× of the default).
|
|
202
210
|
def coerce_budget_per_gem(value)
|
|
203
211
|
unless value.is_a?(Integer)
|
|
204
|
-
raise
|
|
212
|
+
raise ConfigurationError,
|
|
205
213
|
"dependencies.budget_per_gem must be an Integer, " \
|
|
206
214
|
"got #{value.inspect}"
|
|
207
215
|
end
|
|
208
216
|
|
|
209
217
|
unless value.between?(MIN_BUDGET_PER_GEM, MAX_BUDGET_PER_GEM)
|
|
210
|
-
raise
|
|
218
|
+
raise ConfigurationError,
|
|
211
219
|
"dependencies.budget_per_gem must be in the range " \
|
|
212
220
|
"#{MIN_BUDGET_PER_GEM}..#{MAX_BUDGET_PER_GEM}, " \
|
|
213
221
|
"got #{value.inspect}"
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -356,6 +356,13 @@ module Rigor
|
|
|
356
356
|
data = DEFAULTS.merge(raw)
|
|
357
357
|
end
|
|
358
358
|
new(autowire_default_plugins(data), effects_key_present, target_ruby_key_present || false)
|
|
359
|
+
.with_exclude_base(resolved && File.exist?(resolved) ? File.dirname(resolved) : nil)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# #1576 — the directory relative `exclude:` patterns are matched against: the directory of the config file in
|
|
363
|
+
# use (the base `paths:` resolve against), or the working directory when no config file is used.
|
|
364
|
+
def exclude_base
|
|
365
|
+
@base_dir || Dir.pwd
|
|
359
366
|
end
|
|
360
367
|
|
|
361
368
|
# ADR-93 WD2 — the one bundled plugin default-wired without a `plugins:` entry. `rigor-rbs-inline` is the
|
|
@@ -746,6 +753,14 @@ module Rigor
|
|
|
746
753
|
copy.freeze
|
|
747
754
|
end
|
|
748
755
|
|
|
756
|
+
# #1576 — a sibling Configuration whose relative `exclude:` patterns match against `dir` (nil: the working
|
|
757
|
+
# directory). {Configuration.load} sets it to the loaded config file's directory.
|
|
758
|
+
def with_exclude_base(dir)
|
|
759
|
+
copy = dup
|
|
760
|
+
copy.instance_variable_set(:@base_dir, dir && File.absolute_path(dir).freeze)
|
|
761
|
+
copy.freeze
|
|
762
|
+
end
|
|
763
|
+
|
|
749
764
|
# #1777 — a sibling Configuration with only `baseline:` unset, every other field (including the
|
|
750
765
|
# explicitness of `target_ruby`) shared with the receiver. `rigor baseline generate` analyses with
|
|
751
766
|
# this so the generating run sees exactly the configuration `rigor check` will, minus the filter it
|
|
@@ -118,15 +118,19 @@ module Rigor
|
|
|
118
118
|
# `{name => LockedGem}` → set of canonical bundler gem directory basenames. Pure-Ruby gems install as
|
|
119
119
|
# `<name>-<version>`; platform-specific gems install as `<name>-<version>-<platform>` (e.g.
|
|
120
120
|
# `ffi-1.17.4-aarch64-linux-gnu`). Lockfile platform `"ruby"` is the pure-Ruby case; any other value is
|
|
121
|
-
# treated as a platform tag.
|
|
121
|
+
# treated as a platform tag. Every locked variant contributes a directory (#1844): the set only selects
|
|
122
|
+
# among directories that are actually installed, so admitting another platform's name costs nothing,
|
|
123
|
+
# while dropping the host's would discard its `sig/`.
|
|
122
124
|
def self.expected_gem_dirs(locked_gems)
|
|
123
125
|
locked_gems.each_value.with_object(Set.new) do |locked, set|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
locked.variants.each do |version, platform|
|
|
127
|
+
base = "#{locked.name}-#{version}"
|
|
128
|
+
set << if platform == "ruby" || platform.empty?
|
|
129
|
+
base
|
|
130
|
+
else
|
|
131
|
+
"#{base}-#{platform}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
130
134
|
end
|
|
131
135
|
end
|
|
132
136
|
private_class_method :expected_gem_dirs
|