rigortype 0.4.3 → 0.4.5
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 +5 -5
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +56 -8
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +50 -35
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- 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-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -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/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +16 -1
|
@@ -4,6 +4,7 @@ require "digest"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require_relative "../value_semantics"
|
|
6
6
|
require_relative "file_digest"
|
|
7
|
+
require_relative "../rooted_glob"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Cache
|
|
@@ -82,7 +83,18 @@ module Rigor
|
|
|
82
83
|
# an unchanged file would fold as "no possible fact" where a cold walk records one.
|
|
83
84
|
# v18: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`; a pre-18 bundle
|
|
84
85
|
# carries it empty, so an unchanged file's conditional visibility would fold as certain.
|
|
85
|
-
|
|
86
|
+
# v19: #1555 / #1962 — the run-result dependency descriptor now carries a `:kind` row per analysis root,
|
|
87
|
+
# `pre_eval:` entry and project signature root (configured, or the auto-detected `sig`), so one that
|
|
88
|
+
# appears, vanishes or changes kind invalidates the warm entry. A
|
|
89
|
+
# pre-19 entry carries none of them and would validate fresh across exactly those edits, so cached
|
|
90
|
+
# entries must read as misses once and rebuild carrying the rows.
|
|
91
|
+
# v20: #1977 — the `--incremental` run-result slot records a `:kind` row, not an existence row, per analysis
|
|
92
|
+
# root, `pre_eval:` entry and project signature root. A pre-20 slot's existence row validates fresh across a
|
|
93
|
+
# directory replaced by a file of the same name, so it must read as a miss once and rebuild.
|
|
94
|
+
# v21: #1984 — the visibility walk records each class's singleton slots (`[:singleton, name]`) in the seed
|
|
95
|
+
# bundles' `method_visibilities`. A pre-21 bundle carries none for an unchanged file, so cached bundles must read
|
|
96
|
+
# as misses once and rebuild carrying them.
|
|
97
|
+
SCHEMA_VERSION = 21
|
|
86
98
|
|
|
87
99
|
# Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
|
|
88
100
|
# struct so no caller can mutate after the entry is in a Descriptor.
|
|
@@ -96,7 +108,7 @@ module Rigor
|
|
|
96
108
|
# It MUST NOT be placed in a descriptor used as a cache KEY (its value carries machine-local,
|
|
97
109
|
# per-run-nondeterministic stat data); {RbsDescriptor.build}'s env-cache `files` therefore stay
|
|
98
110
|
# `:digest`, while the {Runner} run-dependency descriptor and plugin {IoBoundary} reads use `:stat`.
|
|
99
|
-
VALID_COMPARATORS = %i[digest stat mtime exists].freeze
|
|
111
|
+
VALID_COMPARATORS = %i[digest stat mtime kind exists].freeze
|
|
100
112
|
|
|
101
113
|
# The two `:exists` values — `File.exist?`'s answer rendered with `to_s`, which is what
|
|
102
114
|
# {Descriptor#file_entry_fresh?} compares the live answer against.
|
|
@@ -163,6 +175,32 @@ module Rigor
|
|
|
163
175
|
new(path: path, comparator: :exists, value: PRESENT)
|
|
164
176
|
end
|
|
165
177
|
|
|
178
|
+
# The four `:kind` values, in the order {Analysis::Runner}'s path expansion asks: a directory, a regular
|
|
179
|
+
# file, something else that exists (a FIFO, a socket), nothing (a missing path or a dangling symlink).
|
|
180
|
+
# Each predicate follows symlinks, as the expansion's do.
|
|
181
|
+
KIND_DIRECTORY = "directory"
|
|
182
|
+
KIND_FILE = "file"
|
|
183
|
+
KIND_OTHER = "other"
|
|
184
|
+
KIND_ABSENT = "absent"
|
|
185
|
+
|
|
186
|
+
# #1962 — the kind row: fresh while `path` is still the same kind of thing, stale once it appears,
|
|
187
|
+
# vanishes, or a directory and a file trade places. It is the row for an analysis root, whose
|
|
188
|
+
# diagnostics (`no such file or directory`, `not a Ruby file`) answer to exactly that and to nothing
|
|
189
|
+
# else about the path, and for a `pre_eval:` entry and a signature root: an existence row would read a
|
|
190
|
+
# directory replaced by a file of the same name as fresh (ADR-45 WD1b's bound), and what the path holds
|
|
191
|
+
# has rows of its own (the analysed-file set the key carries, a signature root's `.rbs` rows).
|
|
192
|
+
def self.kind(path:)
|
|
193
|
+
new(path: path, comparator: :kind, value: kind_of(path))
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def self.kind_of(path)
|
|
197
|
+
if File.directory?(path) then KIND_DIRECTORY
|
|
198
|
+
elsif File.file?(path) then KIND_FILE
|
|
199
|
+
elsif File.exist?(path) then KIND_OTHER
|
|
200
|
+
else KIND_ABSENT
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
166
204
|
# @return whether this row records the path as absent (see {.absent}).
|
|
167
205
|
def absent?
|
|
168
206
|
comparator == :exists && value == ABSENT
|
|
@@ -294,7 +332,12 @@ module Rigor
|
|
|
294
332
|
# tuple moves, so a `git checkout`, a `bundle install` or a CI cache restored over a fresh checkout
|
|
295
333
|
# keeps the file rows fresh while it would move every glob signature, turning every such run into a
|
|
296
334
|
# full re-analysis. `:names` also reads no stat at all, so it costs a `Dir.glob` and nothing else.
|
|
297
|
-
|
|
335
|
+
#
|
|
336
|
+
# `:kinds` (#1959) is `:names` plus each match's `File.lstat` file type: the set moves it, and so does a
|
|
337
|
+
# path that keeps its name and changes kind (a file `gen` replaced by a directory `gen/`, a symlinked
|
|
338
|
+
# directory replaced by a real one). It reads one `lstat` per match and no content, so a `touch` or an
|
|
339
|
+
# edit leaves it fresh.
|
|
340
|
+
VALID_MODES = %i[stat names kinds].freeze
|
|
298
341
|
|
|
299
342
|
attr_reader :root, :pattern, :value
|
|
300
343
|
|
|
@@ -326,13 +369,15 @@ module Rigor
|
|
|
326
369
|
# The aggregate signature the entry's `value` carries: SHA-256 over the sorted per-file rows. In the
|
|
327
370
|
# default (`:stat`) mode a row is the file's `(path, size, mtime_ns, ctime_ns, inode)` tuple — NO
|
|
328
371
|
# content read; in strict mode a row is `(path, content-sha256)`, restoring the pre-ADR-87 authority.
|
|
329
|
-
# In `:names` mode a row is the path alone, stat-free, so only the matching SET moves the signature
|
|
372
|
+
# In `:names` mode a row is the path alone, stat-free, so only the matching SET moves the signature; in
|
|
373
|
+
# `:kinds` mode it is the path and its {.lstat_kind}.
|
|
330
374
|
# Per-file stat failures (a file vanishing between the glob and the stat) drop the row — same race
|
|
331
375
|
# posture as {Descriptor#file_entry_fresh?}. `Dir.glob` returns sorted entries by default so the row
|
|
332
376
|
# order — and therefore the signature — is stable.
|
|
333
377
|
def self.signature_for(root:, pattern:, mode: :stat)
|
|
334
|
-
paths =
|
|
378
|
+
paths = RootedGlob.glob(root, pattern)
|
|
335
379
|
return Digest::SHA256.hexdigest(paths.map { |path| "#{path}#{ROW_SEPARATOR}" }.join) if mode == :names
|
|
380
|
+
return kinds_signature(paths) if mode == :kinds
|
|
336
381
|
|
|
337
382
|
strict = FileDigest.strict_validation?
|
|
338
383
|
rows = paths.filter_map do |path|
|
|
@@ -351,10 +396,29 @@ module Rigor
|
|
|
351
396
|
Digest::SHA256.hexdigest(rows.join)
|
|
352
397
|
end
|
|
353
398
|
|
|
399
|
+
# #1959 — what `path` itself is, a symlink not followed: `:file`, `:directory`, `:symlink` or `:other`, and
|
|
400
|
+
# `:absent` for a path that vanished between the glob and the `lstat`.
|
|
401
|
+
def self.lstat_kind(path)
|
|
402
|
+
case File.lstat(path).ftype
|
|
403
|
+
when "file" then :file
|
|
404
|
+
when "directory" then :directory
|
|
405
|
+
when "link" then :symlink
|
|
406
|
+
else :other
|
|
407
|
+
end
|
|
408
|
+
rescue SystemCallError
|
|
409
|
+
:absent
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def self.kinds_signature(paths)
|
|
413
|
+
rows = paths.map { |path| "#{path}#{FIELD_SEPARATOR}#{lstat_kind(path)}#{ROW_SEPARATOR}" }
|
|
414
|
+
Digest::SHA256.hexdigest(rows.join)
|
|
415
|
+
end
|
|
416
|
+
private_class_method :kinds_signature
|
|
417
|
+
|
|
354
418
|
# ADR-87 WD2 — fresh iff re-globbing + re-stat-ing reproduces the recorded signature. Zero file-content
|
|
355
|
-
# bytes are read in the default mode,
|
|
356
|
-
# (recompute), never a crash. A stored signature recorded under the opposite
|
|
357
|
-
# mismatches and recomputes.
|
|
419
|
+
# bytes are read in the default mode, no stat at all in `:names`, and one `lstat` per match in `:kinds`.
|
|
420
|
+
# Any failure reads as stale (recompute), never a crash. A stored signature recorded under the opposite
|
|
421
|
+
# validation mode simply mismatches and recomputes.
|
|
358
422
|
def self.fresh?(entry)
|
|
359
423
|
signature_for(root: entry.root, pattern: entry.pattern, mode: entry.mode) == entry.value
|
|
360
424
|
rescue StandardError
|
|
@@ -413,10 +477,11 @@ module Rigor
|
|
|
413
477
|
|
|
414
478
|
# File-comparator strictness ordering. `:stat` is strictest (it carries the content digest AND a stat
|
|
415
479
|
# tuple, so it validates content-authoritatively while short-circuiting on an unmoved stat); `:digest` is
|
|
416
|
-
# deterministic across machines; `:mtime` is cheaper but local; `:
|
|
417
|
-
# `:
|
|
418
|
-
#
|
|
419
|
-
|
|
480
|
+
# deterministic across machines; `:mtime` is cheaper but local; `:kind` answers what the path is and
|
|
481
|
+
# `:exists` only whether it is there, the weakest signal. Ranking `:stat` and `:digest` apart guarantees a
|
|
482
|
+
# path contributed under both never raises a value {Conflict} (their `value` strings differ by
|
|
483
|
+
# construction) — the stricter one wins and its value is used.
|
|
484
|
+
COMPARATOR_STRICTNESS = { stat: 4, digest: 3, mtime: 2, kind: 1, exists: 0 }.freeze
|
|
420
485
|
private_constant :COMPARATOR_STRICTNESS
|
|
421
486
|
|
|
422
487
|
# Composes any number of descriptors into a single descriptor whose slots are the union of the inputs'
|
|
@@ -505,6 +570,8 @@ module Rigor
|
|
|
505
570
|
FileDigest.stat_fresh?(entry.path, entry.value)
|
|
506
571
|
when :mtime
|
|
507
572
|
File.exist?(entry.path) && File.mtime(entry.path).to_i.to_s == entry.value
|
|
573
|
+
when :kind
|
|
574
|
+
FileEntry.kind_of(entry.path) == entry.value
|
|
508
575
|
when :exists
|
|
509
576
|
File.exist?(entry.path).to_s == entry.value
|
|
510
577
|
else
|
|
@@ -4,6 +4,7 @@ require "digest"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../version"
|
|
6
6
|
require_relative "descriptor"
|
|
7
|
+
require_relative "../rooted_glob"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Cache
|
|
@@ -195,8 +196,8 @@ module Rigor
|
|
|
195
196
|
# Sorted, de-duplicated absolute paths of every engine `.rb` file and {DATA_GLOBS} data file, across
|
|
196
197
|
# the directories that exist. `plugins/*/sig/**/*.rbs` overlaps nothing else but is de-duplicated anyway.
|
|
197
198
|
def source_files(root)
|
|
198
|
-
sources = SOURCE_DIRECTORIES.flat_map { |relative|
|
|
199
|
-
data = DATA_GLOBS.flat_map { |pattern|
|
|
199
|
+
sources = SOURCE_DIRECTORIES.flat_map { |relative| RootedGlob.glob(root, File.join(relative, "**", "*.rb")) }
|
|
200
|
+
data = DATA_GLOBS.flat_map { |pattern| RootedGlob.glob(root, pattern) }
|
|
200
201
|
(sources + data).uniq.sort
|
|
201
202
|
end
|
|
202
203
|
end
|
|
@@ -10,6 +10,7 @@ require_relative "../analysis/shadow_harness"
|
|
|
10
10
|
require_relative "../analysis/run_cache_key"
|
|
11
11
|
require_relative "../environment"
|
|
12
12
|
require_relative "../analysis/path_expansion"
|
|
13
|
+
require_relative "../rooted_glob"
|
|
13
14
|
|
|
14
15
|
module Rigor
|
|
15
16
|
module Cache
|
|
@@ -169,7 +170,19 @@ module Rigor
|
|
|
169
170
|
# file's conditional `def` as certain and a warm run would answer a definer a cold run declines.
|
|
170
171
|
# 37: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`, which a pre-37 bundle
|
|
171
172
|
# carries empty.
|
|
172
|
-
|
|
173
|
+
# 38: issue #1898 — the payload gains `effect_plugin_digest`, the plugin effect-fact digest the collections were
|
|
174
|
+
# stamped under, which a recheck now checks before its served collections reach the envelope pass. A pre-38
|
|
175
|
+
# blob carries none, so the gate rejects it and the next run is cold.
|
|
176
|
+
# 39: issue #1984 — the visibility walk records the singleton slots (`[:singleton, name]`) and contests the ones
|
|
177
|
+
# two files disagree on, which a pre-39 bundle carries none of, so a warm run would answer a singleton call a
|
|
178
|
+
# cold run reports.
|
|
179
|
+
# 40: issue #1553 — the payload gains `plugin_reads`, the plugin reads each analysed file's last analysis made,
|
|
180
|
+
# which a recheck validates to put a reader of a changed input into the closure. A pre-40 blob carries none,
|
|
181
|
+
# so its readers would be served stale; the gate rejects it and the next run is cold.
|
|
182
|
+
# 41: issue #1583 — the payload gains `discovery_digests`, the packed digest of each file a run over path
|
|
183
|
+
# arguments discovered without analysing, which a recheck compares to re-baseline when one moved. A pre-41
|
|
184
|
+
# blob carries none; the gate rejects it and the next run is cold.
|
|
185
|
+
SCHEMA = 41
|
|
173
186
|
|
|
174
187
|
# The persisted per-file state.
|
|
175
188
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -224,11 +237,26 @@ module Rigor
|
|
|
224
237
|
# Issues #796 / #794 — the run-level rows a narrowed run replays instead of re-deriving; see
|
|
225
238
|
# docs/internal-spec/cache.md § "Run-level rows the snapshot carries" for why exactly these two are
|
|
226
239
|
# fresh under the fingerprint and why no other `.rigor.yml`-level row may join them.
|
|
240
|
+
# Issue #1583 — `discovery_digests` maps each file a run over path arguments discovered but did not analyse
|
|
241
|
+
# (#684's widening to the configured `paths:`) to its ADR-87 packed stat-digest entry; `{}` on a run over
|
|
242
|
+
# the configured paths. A recheck that finds one moved, or the set changed, re-baselines.
|
|
243
|
+
# Issue #1898 — `effect_plugin_digest` is {Rigor::Effects::PluginFacts#digest} at the run that collected
|
|
244
|
+
# `effect_collections` (nil when collection was off). A plugin stamps its attributions and edges into a file's
|
|
245
|
+
# collection, and `effects_identity` is plugin-blind (it is compared before any plugin loads), so the session
|
|
246
|
+
# compares this one after the recheck has loaded them.
|
|
227
247
|
Payload = Data.define(:cache, :sources, :digests, :analyzed,
|
|
228
248
|
:symbol_sources, :ancestry_sources, :symbol_fingerprints,
|
|
229
249
|
:missing, :class_decls, :constant_decls, :seed_bundles, :plugin_fact_digest,
|
|
230
250
|
:return_summaries, :param_table,
|
|
231
|
-
:effect_collections, :effects_identity, :run_level_rows
|
|
251
|
+
:effect_collections, :effects_identity, :run_level_rows,
|
|
252
|
+
:effect_plugin_digest, :plugin_reads, :discovery_digests) do
|
|
253
|
+
# `effect_plugin_digest` defaults to nil so a payload built without it (an older caller, a spec fixture)
|
|
254
|
+
# reads as "unknown", which the session treats as moved. `plugin_reads` defaults to no reads, and
|
|
255
|
+
# `discovery_digests` to no discovery-only files.
|
|
256
|
+
def initialize(effect_plugin_digest: nil, plugin_reads: {}, discovery_digests: {}, **rest)
|
|
257
|
+
super
|
|
258
|
+
end
|
|
259
|
+
end
|
|
232
260
|
|
|
233
261
|
# Issues #796 / #794 — the two run-level rows the snapshot carries. `definition_build_failures` is the
|
|
234
262
|
# reported per-class detail list ({Environment::RbsLoader#definition_build_failures}); `hkt_scan_failure`
|
|
@@ -353,7 +381,7 @@ module Rigor
|
|
|
353
381
|
# checkouts.
|
|
354
382
|
def self.digest_signature_paths(signature_paths)
|
|
355
383
|
globbed = Array(signature_paths).flat_map do |entry|
|
|
356
|
-
File.directory?(entry) ?
|
|
384
|
+
File.directory?(entry) ? RootedGlob.glob(entry, "**/*.rbs") : [entry]
|
|
357
385
|
end
|
|
358
386
|
files = globbed.select { |path| File.file?(path) }.sort
|
|
359
387
|
digest = Digest::SHA256.new
|
|
@@ -434,7 +462,10 @@ module Rigor
|
|
|
434
462
|
param_table: data[:param_table] || {},
|
|
435
463
|
effect_collections: data[:effect_collections] || {},
|
|
436
464
|
effects_identity: data[:effects_identity],
|
|
437
|
-
run_level_rows: data[:run_level_rows]
|
|
465
|
+
run_level_rows: data[:run_level_rows],
|
|
466
|
+
effect_plugin_digest: data[:effect_plugin_digest],
|
|
467
|
+
plugin_reads: data[:plugin_reads] || {},
|
|
468
|
+
discovery_digests: data.fetch(:discovery_digests, {})
|
|
438
469
|
)
|
|
439
470
|
end
|
|
440
471
|
private :payload_from
|
|
@@ -459,7 +490,10 @@ module Rigor
|
|
|
459
490
|
param_table: payload.param_table,
|
|
460
491
|
effect_collections: payload.effect_collections,
|
|
461
492
|
effects_identity: payload.effects_identity,
|
|
462
|
-
run_level_rows: payload.run_level_rows
|
|
493
|
+
run_level_rows: payload.run_level_rows,
|
|
494
|
+
effect_plugin_digest: payload.effect_plugin_digest,
|
|
495
|
+
plugin_reads: payload.plugin_reads,
|
|
496
|
+
discovery_digests: payload.discovery_digests
|
|
463
497
|
)
|
|
464
498
|
# #1507 — every `--incremental` run that analysed anything rewrites this whole blob, so its deflate is
|
|
465
499
|
# on the edit path; see {Store::DEFLATE_LEVEL}.
|
|
@@ -5,6 +5,7 @@ require "digest"
|
|
|
5
5
|
require_relative "descriptor"
|
|
6
6
|
require_relative "engine_source"
|
|
7
7
|
require_relative "file_digest"
|
|
8
|
+
require_relative "../rooted_glob"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Cache
|
|
@@ -110,7 +111,7 @@ module Rigor
|
|
|
110
111
|
roots.flat_map do |root|
|
|
111
112
|
next [] unless root.directory?
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
RootedGlob.glob(root, "**/*.rbs").map do |path|
|
|
114
115
|
digest = FileDigest.hexdigest(path)
|
|
115
116
|
if comparator == :stat
|
|
116
117
|
Descriptor::FileEntry.stat(path: path, digest: digest)
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "zlib"
|
|
|
9
9
|
|
|
10
10
|
require_relative "../version"
|
|
11
11
|
require_relative "descriptor"
|
|
12
|
+
require_relative "../rooted_glob"
|
|
12
13
|
|
|
13
14
|
module Rigor
|
|
14
15
|
module Cache
|
|
@@ -176,7 +177,7 @@ module Rigor
|
|
|
176
177
|
subdir = File.join(root, child)
|
|
177
178
|
next nil unless File.directory?(subdir)
|
|
178
179
|
|
|
179
|
-
entries =
|
|
180
|
+
entries = RootedGlob.glob(subdir, "**/*.entry")
|
|
180
181
|
next nil if entries.empty?
|
|
181
182
|
|
|
182
183
|
{ id: child, entries: entries.size, bytes: entries.sum { |e| File.size(e) } }
|
|
@@ -709,7 +710,7 @@ module Rigor
|
|
|
709
710
|
|
|
710
711
|
def cleanup_stale_temp_files
|
|
711
712
|
cutoff = Time.now - STALE_TEMP_FILE_AGE_SECONDS
|
|
712
|
-
|
|
713
|
+
RootedGlob.glob(@root, "**/*.tmp.*").each do |path|
|
|
713
714
|
next unless File.file?(path)
|
|
714
715
|
next if File.mtime(path) > cutoff
|
|
715
716
|
|
|
@@ -771,7 +772,7 @@ module Rigor
|
|
|
771
772
|
# Returns an array of `{ path:, producer:, mtime:, bytes: }` hashes for every `.entry` file under the
|
|
772
773
|
# cache root, skipping unreadable entries.
|
|
773
774
|
def collect_entry_stats
|
|
774
|
-
|
|
775
|
+
RootedGlob.glob(@root, "**/*.entry").filter_map do |path|
|
|
775
776
|
stat = File.stat(path)
|
|
776
777
|
producer = producer_id_for_entry(path)
|
|
777
778
|
next nil if producer.nil?
|
data/lib/rigor/cli/command.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../rooted_glob"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
class CLI
|
|
5
7
|
# Base class for the `rigor <subcommand>` command objects.
|
|
@@ -26,7 +28,7 @@ module Rigor
|
|
|
26
28
|
paths = []
|
|
27
29
|
args.each do |arg|
|
|
28
30
|
if File.directory?(arg)
|
|
29
|
-
paths.concat(
|
|
31
|
+
paths.concat(RootedGlob.glob(arg, "**/*.rb"))
|
|
30
32
|
elsif File.file?(arg)
|
|
31
33
|
paths << arg
|
|
32
34
|
else
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
4
|
require_relative "doc_links"
|
|
5
|
+
require_relative "../rooted_glob"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
class CLI
|
|
@@ -186,8 +187,8 @@ module Rigor
|
|
|
186
187
|
# `install.md` sits at the docs root (category `guide`); the rest are manual / handbook chapters.
|
|
187
188
|
def discover_docs
|
|
188
189
|
paths = [File.join(DOCS_ROOT, "install.md")]
|
|
189
|
-
paths +=
|
|
190
|
-
paths +=
|
|
190
|
+
paths += RootedGlob.glob(MANUAL_ROOT, "**/*.md") # Dir.glob is already sorted
|
|
191
|
+
paths += RootedGlob.glob(HANDBOOK_ROOT, "**/*.md")
|
|
191
192
|
paths.select { |path| File.file?(path) }.map { |path| doc_entry(path) }
|
|
192
193
|
end
|
|
193
194
|
|
|
@@ -364,7 +364,7 @@ module Rigor
|
|
|
364
364
|
status: :warn,
|
|
365
365
|
message: "#{gap.locked_gems.join(', ')} #{gap.locked_gems.one? ? 'is a dependency' : 'are dependencies'} " \
|
|
366
366
|
"of this project and #{gap.plugin_gem} models it, but it is not enabled",
|
|
367
|
-
hint: "Add `#{gap.plugin_gem}` to `.rigor.yml` `plugins:`, or
|
|
367
|
+
hint: "Add `#{gap.plugin_gem}` to `.rigor.yml` `plugins:`, or decline it with an `enabled: false` entry."
|
|
368
368
|
}
|
|
369
369
|
end
|
|
370
370
|
|
|
@@ -10,7 +10,7 @@ require_relative "command"
|
|
|
10
10
|
module Rigor
|
|
11
11
|
class CLI
|
|
12
12
|
# Executes `rigor explain <rule>`. Prints the catalog entry for one canonical rule id, a legacy alias, or a family
|
|
13
|
-
# wildcard (`call`, `flow`, `assert`, `dump`, `
|
|
13
|
+
# wildcard (`call`, `flow`, `def`, `assert`, `dump`, `global`, `suppression`, `static`, `effect`).
|
|
14
14
|
#
|
|
15
15
|
# Without arguments lists every rule's id and one-line summary.
|
|
16
16
|
#
|
|
@@ -74,7 +74,8 @@ module Rigor
|
|
|
74
74
|
end
|
|
75
75
|
@out.puts("")
|
|
76
76
|
@out.puts("Run `rigor explain <rule>` for the full description.")
|
|
77
|
-
@out.puts("Family wildcards (
|
|
77
|
+
@out.puts("Family wildcards (#{Analysis::CheckRules::RULE_FAMILIES.map { |f| "`#{f}`" }.join(', ')}) " \
|
|
78
|
+
"print every rule under that prefix.")
|
|
78
79
|
@out.puts("")
|
|
79
80
|
@out.puts("`rigor sig-gen` skip reasons:")
|
|
80
81
|
@out.puts("")
|
|
@@ -12,7 +12,7 @@ module Rigor
|
|
|
12
12
|
# Starts a long-running MCP (Model Context Protocol) server over stdio. The server exposes Rigor's analysis tools as
|
|
13
13
|
# MCP tool calls over a newline-delimited JSON-RPC 2.0 stream. See ADR-33.
|
|
14
14
|
#
|
|
15
|
-
#
|
|
15
|
+
# The stdio transport serves eight read-only tools: rigor_check, rigor_type_of, rigor_triage, rigor_lens,
|
|
16
16
|
# rigor_annotate, rigor_sig_gen, rigor_explain, rigor_coverage.
|
|
17
17
|
class McpCommand < Command
|
|
18
18
|
USAGE = "Usage: rigor mcp [options]"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class CLI
|
|
@@ -204,7 +205,7 @@ module Rigor
|
|
|
204
205
|
candidate = File.join(dir, "lib", "#{plugin.fetch(:name)}.rb")
|
|
205
206
|
return candidate if File.file?(candidate)
|
|
206
207
|
|
|
207
|
-
|
|
208
|
+
RootedGlob.glob(dir, "lib/*.rb").min
|
|
208
209
|
end
|
|
209
210
|
|
|
210
211
|
def docker_note
|
|
@@ -13,6 +13,7 @@ require_relative "../reflection"
|
|
|
13
13
|
require_relative "../type/combinator"
|
|
14
14
|
require_relative "plugins_renderer"
|
|
15
15
|
require_relative "command"
|
|
16
|
+
require_relative "../rooted_glob"
|
|
16
17
|
|
|
17
18
|
module Rigor
|
|
18
19
|
class CLI
|
|
@@ -297,7 +298,7 @@ module Rigor
|
|
|
297
298
|
def rbs_file_count(dir)
|
|
298
299
|
return 0 unless File.directory?(dir)
|
|
299
300
|
|
|
300
|
-
|
|
301
|
+
RootedGlob.glob(dir, "**/*.rbs").size
|
|
301
302
|
end
|
|
302
303
|
|
|
303
304
|
def entry_gem_name(entry)
|
|
@@ -55,7 +55,8 @@ module Rigor
|
|
|
55
55
|
configuration = Configuration.load(options.fetch(:config))
|
|
56
56
|
paths = @argv.empty? ? configuration.paths : @argv
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
observation_roots = observation_roots(configuration, options)
|
|
59
|
+
observations = collect_observations(configuration, options, observation_roots)
|
|
59
60
|
generator = SigGen::Generator.new(configuration: configuration, paths: paths,
|
|
60
61
|
observations: observations,
|
|
61
62
|
include_private: options.fetch(:include_private),
|
|
@@ -68,7 +69,7 @@ module Rigor
|
|
|
68
69
|
when :write then dispatch_write(candidates, configuration, options)
|
|
69
70
|
when :check then dispatch_check(candidates, configuration, options)
|
|
70
71
|
else
|
|
71
|
-
dispatch_print_or_diff(candidates, mode, options)
|
|
72
|
+
dispatch_print_or_diff(candidates, mode, options, observation_roots)
|
|
72
73
|
0
|
|
73
74
|
end
|
|
74
75
|
report_skipped(candidates, options)
|
|
@@ -219,12 +220,13 @@ module Rigor
|
|
|
219
220
|
unresolvable.sort.each { |class_name, superclass| @err.puts(" #{class_name} < #{superclass}") }
|
|
220
221
|
end
|
|
221
222
|
|
|
222
|
-
def dispatch_print_or_diff(candidates, mode, options)
|
|
223
|
+
def dispatch_print_or_diff(candidates, mode, options, observation_roots)
|
|
223
224
|
SigGen::Renderer.new(out: @out).render(
|
|
224
225
|
candidates: candidates,
|
|
225
226
|
mode: mode,
|
|
226
227
|
format: options.fetch(:format),
|
|
227
|
-
selection: options.fetch(:selection)
|
|
228
|
+
selection: options.fetch(:selection),
|
|
229
|
+
observation: observation_report(observation_roots, options)
|
|
228
230
|
)
|
|
229
231
|
end
|
|
230
232
|
|
|
@@ -289,11 +291,9 @@ module Rigor
|
|
|
289
291
|
# specified, observe the project's test roots: `test_paths:`, or whichever of `spec/` and `test/` exist. With no
|
|
290
292
|
# root at all the run still succeeds, but says so — otherwise every parameter stays `untyped` with nothing
|
|
291
293
|
# naming the reason.
|
|
292
|
-
def collect_observations(configuration, options)
|
|
294
|
+
def collect_observations(configuration, options, observe_paths)
|
|
293
295
|
return {} if options.fetch(:params) != "observed"
|
|
294
296
|
|
|
295
|
-
observe_paths = options.fetch(:observe)
|
|
296
|
-
observe_paths = configuration.resolved_test_paths if observe_paths.empty?
|
|
297
297
|
if observe_paths.empty?
|
|
298
298
|
warn_no_test_roots(configuration)
|
|
299
299
|
else
|
|
@@ -302,15 +302,36 @@ module Rigor
|
|
|
302
302
|
SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths).collect
|
|
303
303
|
end
|
|
304
304
|
|
|
305
|
-
#
|
|
306
|
-
|
|
305
|
+
# The roots `--params=observed` reads: `--observe=PATH...` when given, else the project's test roots.
|
|
306
|
+
def observation_roots(configuration, options)
|
|
307
|
+
return [] if options.fetch(:params) != "observed"
|
|
308
|
+
|
|
309
|
+
observe_paths = options.fetch(:observe)
|
|
310
|
+
observe_paths.empty? ? configuration.resolved_test_paths : observe_paths
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# #1398 — the same fact the stderr notes carry, as data for `--format=json` (and the MCP tool, whose
|
|
314
|
+
# stderr nobody reads). Absent under `--params=untyped`, where no root is read. Paths are spelled as
|
|
315
|
+
# {#observation_roots} yields them: `--observe=PATH` as typed, `test_paths:` as the configuration
|
|
316
|
+
# resolved them, an auto-detected `spec/` / `test/` absolute.
|
|
317
|
+
def observation_report(observe_paths, options)
|
|
318
|
+
return nil if options.fetch(:params) != "observed"
|
|
319
|
+
|
|
320
|
+
present, missing = observe_paths.uniq.partition { |path| SigGen::ObservationCollector.readable_root?(path) }
|
|
321
|
+
{ paths: present, missing: missing }
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# A declared root that does not exist (or is not a directory or `.rb` file) is read as empty, so name it:
|
|
325
|
+
# `sig-gen` is the only command that reads the test roots, and without this the run looks exactly like one
|
|
326
|
+
# whose tests pass no typed arguments.
|
|
307
327
|
def warn_missing_test_roots(observe_paths)
|
|
308
|
-
missing = observe_paths.reject { |path|
|
|
328
|
+
missing = observe_paths.reject { |path| SigGen::ObservationCollector.readable_root?(path) }
|
|
309
329
|
return if missing.empty?
|
|
310
330
|
|
|
311
331
|
consequence = missing.size == observe_paths.size ? "; every parameter stays untyped" : ""
|
|
312
332
|
@err.puts("rigor sig-gen: no call sites are observed from #{missing.map(&:inspect).join(', ')}, " \
|
|
313
|
-
"which does not exist#{consequence}.
|
|
333
|
+
"which does not exist or is not a directory or `.rb` file#{consequence}. " \
|
|
334
|
+
"Check `test_paths:` or --observe=PATH.")
|
|
314
335
|
end
|
|
315
336
|
|
|
316
337
|
def warn_no_test_roots(configuration)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class CLI
|
|
@@ -227,7 +228,7 @@ module Rigor
|
|
|
227
228
|
return [] unless File.directory?(dir)
|
|
228
229
|
|
|
229
230
|
# `Dir.glob` returns lexicographically sorted paths (Ruby 3.0+), so the `NN-` prefixes already drive read order.
|
|
230
|
-
|
|
231
|
+
RootedGlob.glob(dir, "*.md")
|
|
231
232
|
end
|
|
232
233
|
|
|
233
234
|
def name_error(name)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
5
|
require_relative "../plugin_gap_advisory"
|
|
6
|
+
require_relative "../rooted_glob"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
class CLI
|
|
@@ -78,7 +79,7 @@ module Rigor
|
|
|
78
79
|
vscode = File.join(@root, ".vscode")
|
|
79
80
|
return :none unless File.directory?(vscode)
|
|
80
81
|
|
|
81
|
-
files =
|
|
82
|
+
files = RootedGlob.glob(vscode, "*.json")
|
|
82
83
|
return :unwired if files.empty?
|
|
83
84
|
|
|
84
85
|
files.any? { |path| file_mentions_rigor?(path) } ? :wired : :unwired
|
|
@@ -103,7 +104,7 @@ module Rigor
|
|
|
103
104
|
end
|
|
104
105
|
|
|
105
106
|
def ci_config_files
|
|
106
|
-
files =
|
|
107
|
+
files = RootedGlob.glob(@root, ".github/workflows/*.{yml,yaml}")
|
|
107
108
|
gitlab = File.join(@root, ".gitlab-ci.yml")
|
|
108
109
|
files << gitlab if File.file?(gitlab)
|
|
109
110
|
files
|
|
@@ -15,6 +15,7 @@ require_relative "../cache/store"
|
|
|
15
15
|
require_relative "options"
|
|
16
16
|
require_relative "command"
|
|
17
17
|
require_relative "../project_environment"
|
|
18
|
+
require_relative "../rooted_glob"
|
|
18
19
|
|
|
19
20
|
module Rigor
|
|
20
21
|
class CLI
|
|
@@ -51,7 +52,7 @@ module Rigor
|
|
|
51
52
|
paths = @argv.empty? ? configuration.paths : @argv
|
|
52
53
|
establish_root(configuration)
|
|
53
54
|
scan_cache = open_scan_cache(configuration)
|
|
54
|
-
declarations, references, dynamic_uses, shadows = scan(paths, configuration, scan_cache)
|
|
55
|
+
declarations, references, dynamic_uses, shadows, ancestries = scan(paths, configuration, scan_cache)
|
|
55
56
|
references.concat(signature_references(configuration))
|
|
56
57
|
dynamic_uses.concat(template_mentions(declarations, scan_cache))
|
|
57
58
|
|
|
@@ -60,7 +61,7 @@ module Rigor
|
|
|
60
61
|
references.concat(plugin_references(contribution.references))
|
|
61
62
|
graph = build_graph(configuration, options, contribution,
|
|
62
63
|
declarations: declarations, references: references, dynamic_uses: dynamic_uses,
|
|
63
|
-
shadows: shadows)
|
|
64
|
+
shadows: shadows, ancestries: ancestries)
|
|
64
65
|
emit(graph.report, options, supply: root_supply(contribution.roots, declarations))
|
|
65
66
|
scan_cache.save
|
|
66
67
|
0
|
|
@@ -109,9 +110,9 @@ module Rigor
|
|
|
109
110
|
cwd == @root || inside?(cwd, @root) ? [@root] : [@root, cwd]
|
|
110
111
|
end
|
|
111
112
|
|
|
112
|
-
def build_graph(configuration, options, contribution, declarations:,
|
|
113
|
+
def build_graph(configuration, options, contribution, declarations:, **scanned)
|
|
113
114
|
Analysis::Reachability::Graph.new(
|
|
114
|
-
declarations: declarations,
|
|
115
|
+
declarations: declarations, **scanned,
|
|
115
116
|
root_fqns: root_fqns(declarations, options.fetch(:entry_points)) + contribution.roots,
|
|
116
117
|
foreign: foreign_predicate(configuration)
|
|
117
118
|
)
|
|
@@ -160,16 +161,18 @@ module Rigor
|
|
|
160
161
|
# is the one the report prints; any other spelling's declarations are shadows, as a wider-corpus
|
|
161
162
|
# file's are (#1732).
|
|
162
163
|
def scan(paths, configuration, cache)
|
|
163
|
-
out = { declarations: [], references: [], dynamic_uses: [], shadows: [] }
|
|
164
|
+
out = { declarations: [], references: [], dynamic_uses: [], shadows: [], ancestries: [] }
|
|
164
165
|
file_spellings(paths, configuration).sort.each do |real, list|
|
|
165
166
|
primary, analysed = list.find { |_, declared| declared } || list.first
|
|
166
167
|
result = cache.serve(:scan, primary) { read_and_scan(primary, real, configuration) }
|
|
167
168
|
next if result.nil?
|
|
168
169
|
|
|
169
170
|
out[analysed ? :declarations : :shadows].concat(result.declarations)
|
|
171
|
+
# An ancestry edge is a fact about the program, not about the file's role, so one spelling carries it.
|
|
172
|
+
out[:ancestries].concat(result.ancestries)
|
|
170
173
|
list.map(&:first).each { |file| add_spelling(out, result, file, real, primary) }
|
|
171
174
|
end
|
|
172
|
-
out.values_at(:declarations, :references, :dynamic_uses, :shadows)
|
|
175
|
+
out.values_at(:declarations, :references, :dynamic_uses, :shadows, :ancestries)
|
|
173
176
|
end
|
|
174
177
|
|
|
175
178
|
def add_spelling(out, result, file, real, primary)
|
|
@@ -289,7 +292,7 @@ module Rigor
|
|
|
289
292
|
Array(configuration.signature_paths).flat_map do |dir|
|
|
290
293
|
next [] unless File.directory?(dir)
|
|
291
294
|
|
|
292
|
-
|
|
295
|
+
RootedGlob.glob(dir, "**/*.rbs").flat_map { |file| Analysis::Reachability::SignatureScan.call(file) }
|
|
293
296
|
end
|
|
294
297
|
end
|
|
295
298
|
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -250,8 +250,9 @@ module Rigor
|
|
|
250
250
|
# ships (run `rigor explain <rule>` for what
|
|
251
251
|
# each one catches):
|
|
252
252
|
#{rule_ids_comment}
|
|
253
|
-
# A bare family token (`call`, `flow`,
|
|
254
|
-
# `assert`, `dump`, `
|
|
253
|
+
# A bare family token (`call`, `flow`, `def`,
|
|
254
|
+
# `assert`, `dump`, `global`, `suppression`,
|
|
255
|
+
# `static`, `effect`, `pre-eval`) wildcards every
|
|
255
256
|
# rule under that prefix. Legacy unprefixed
|
|
256
257
|
# names (`undefined-method`, …) still
|
|
257
258
|
# resolve. In-source
|