rigortype 0.4.4 → 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/docs/handbook/11-sig-gen.md +90 -17
- data/docs/manual/02-cli-reference.md +39 -9
- data/docs/manual/03-configuration.md +10 -8
- data/docs/manual/04-diagnostics.md +16 -5
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- 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 +3 -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 +228 -15
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +49 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +166 -24
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/rule_catalog.rb +17 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
- data/lib/rigor/analysis/runner.rb +69 -65
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +12 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +26 -8
- 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/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 +60 -15
- 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 +21 -1
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +87 -20
- data/lib/rigor/inference/expression_typer.rb +35 -5
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- 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 +137 -42
- data/lib/rigor/inference/narrowing.rb +54 -10
- 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 +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +675 -105
- data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
- 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 +446 -139
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +2 -2
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -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 +45 -2
- 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 +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- 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 +5 -3
- 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-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -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-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 +13 -1
|
@@ -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
|
|
@@ -172,7 +173,16 @@ module Rigor
|
|
|
172
173
|
# 38: issue #1898 — the payload gains `effect_plugin_digest`, the plugin effect-fact digest the collections were
|
|
173
174
|
# stamped under, which a recheck now checks before its served collections reach the envelope pass. A pre-38
|
|
174
175
|
# blob carries none, so the gate rejects it and the next run is cold.
|
|
175
|
-
|
|
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
|
|
176
186
|
|
|
177
187
|
# The persisted per-file state.
|
|
178
188
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -227,6 +237,9 @@ module Rigor
|
|
|
227
237
|
# Issues #796 / #794 — the run-level rows a narrowed run replays instead of re-deriving; see
|
|
228
238
|
# docs/internal-spec/cache.md § "Run-level rows the snapshot carries" for why exactly these two are
|
|
229
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.
|
|
230
243
|
# Issue #1898 — `effect_plugin_digest` is {Rigor::Effects::PluginFacts#digest} at the run that collected
|
|
231
244
|
# `effect_collections` (nil when collection was off). A plugin stamps its attributions and edges into a file's
|
|
232
245
|
# collection, and `effects_identity` is plugin-blind (it is compared before any plugin loads), so the session
|
|
@@ -236,10 +249,11 @@ module Rigor
|
|
|
236
249
|
:missing, :class_decls, :constant_decls, :seed_bundles, :plugin_fact_digest,
|
|
237
250
|
:return_summaries, :param_table,
|
|
238
251
|
:effect_collections, :effects_identity, :run_level_rows,
|
|
239
|
-
:effect_plugin_digest) do
|
|
240
|
-
#
|
|
241
|
-
# which the session treats as moved.
|
|
242
|
-
|
|
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)
|
|
243
257
|
super
|
|
244
258
|
end
|
|
245
259
|
end
|
|
@@ -367,7 +381,7 @@ module Rigor
|
|
|
367
381
|
# checkouts.
|
|
368
382
|
def self.digest_signature_paths(signature_paths)
|
|
369
383
|
globbed = Array(signature_paths).flat_map do |entry|
|
|
370
|
-
File.directory?(entry) ?
|
|
384
|
+
File.directory?(entry) ? RootedGlob.glob(entry, "**/*.rbs") : [entry]
|
|
371
385
|
end
|
|
372
386
|
files = globbed.select { |path| File.file?(path) }.sort
|
|
373
387
|
digest = Digest::SHA256.new
|
|
@@ -449,7 +463,9 @@ module Rigor
|
|
|
449
463
|
effect_collections: data[:effect_collections] || {},
|
|
450
464
|
effects_identity: data[:effects_identity],
|
|
451
465
|
run_level_rows: data[:run_level_rows],
|
|
452
|
-
effect_plugin_digest: data[:effect_plugin_digest]
|
|
466
|
+
effect_plugin_digest: data[:effect_plugin_digest],
|
|
467
|
+
plugin_reads: data[:plugin_reads] || {},
|
|
468
|
+
discovery_digests: data.fetch(:discovery_digests, {})
|
|
453
469
|
)
|
|
454
470
|
end
|
|
455
471
|
private :payload_from
|
|
@@ -475,7 +491,9 @@ module Rigor
|
|
|
475
491
|
effect_collections: payload.effect_collections,
|
|
476
492
|
effects_identity: payload.effects_identity,
|
|
477
493
|
run_level_rows: payload.run_level_rows,
|
|
478
|
-
effect_plugin_digest: payload.effect_plugin_digest
|
|
494
|
+
effect_plugin_digest: payload.effect_plugin_digest,
|
|
495
|
+
plugin_reads: payload.plugin_reads,
|
|
496
|
+
discovery_digests: payload.discovery_digests
|
|
479
497
|
)
|
|
480
498
|
# #1507 — every `--incremental` run that analysed anything rewrites this whole blob, so its deflate is
|
|
481
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("")
|
|
@@ -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
|
data/lib/rigor/config_audit.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "did_you_mean"
|
|
4
4
|
|
|
5
5
|
require_relative "config_path"
|
|
6
|
+
require_relative "flow_contribution/conflict"
|
|
6
7
|
require_relative "signature_path_audit"
|
|
7
8
|
# ADR-87 WD4 — only the pure rule-id table (`ALL_RULES` / `RULE_FAMILIES`) is needed, so require the light
|
|
8
9
|
# `rule_ids.rb` rather than the engine-pulling `check_rules.rb`; keeps `config_audit` (loaded by every check)
|
|
@@ -26,12 +27,13 @@ module Rigor
|
|
|
26
27
|
#
|
|
27
28
|
# Every check is held to the same bar that {SignaturePathAudit} set: it mirrors the
|
|
28
29
|
# 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.
|
|
30
|
-
#
|
|
31
|
-
# rule id (
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# `
|
|
30
|
+
# it never fires on a setup that works. The rule-token checks lean on the reach of `disable:`
|
|
31
|
+
# and `severity_overrides:` being enumerable (issues #1953, #1985): a plugin's row answers only
|
|
32
|
+
# to its qualified `plugin.<id>.<rule>` id (or `plugin.<id>`), so a bare plugin rule there is
|
|
33
|
+
# inert (see {.disable_token_warning}, {.override_key_warning}). A `plugin.`-qualified token is
|
|
34
|
+
# checked only against `plugins:`, and a token under a non-check engine family
|
|
35
|
+
# (`rbs_extended.*`, `rbs.*`, `dynamic.*`) is left alone, since the engine emits those ids
|
|
36
|
+
# outside the catalogue.
|
|
35
37
|
module ConfigAudit
|
|
36
38
|
# One config-level finding. `kind` discriminates the source key (`:signature_path`,
|
|
37
39
|
# `:bundled_plugin_signature_path`, `:library`, `:disabled_rule`, `:severity_override`,
|
|
@@ -148,25 +150,55 @@ module Rigor
|
|
|
148
150
|
end
|
|
149
151
|
|
|
150
152
|
# `disable:` tokens that reach no row ({.disable_token_warning}) and `severity_overrides:` keys
|
|
151
|
-
# that
|
|
152
|
-
# rule id is never mis-flagged (see the module docstring).
|
|
153
|
+
# that reach none ({.override_key_warning}).
|
|
153
154
|
def self.rule_token_warnings(configuration)
|
|
154
155
|
plugin_ids = configured_plugin_ids(configuration.plugins)
|
|
155
156
|
disable = Array(configuration.disabled_rules).filter_map do |token|
|
|
156
157
|
disable_token_warning(token.to_s, plugin_ids)
|
|
157
158
|
end
|
|
158
159
|
overrides = configuration.severity_overrides.keys.filter_map do |key|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
Warning.new(
|
|
162
|
-
kind: :severity_override,
|
|
163
|
-
message: "severity_overrides: #{key.to_s.inspect} is not a recognized rule id; the override has no effect",
|
|
164
|
-
fields: { "token" => key.to_s }
|
|
165
|
-
)
|
|
160
|
+
override_key_warning(key.to_s, plugin_ids)
|
|
166
161
|
end
|
|
167
162
|
disable + overrides
|
|
168
163
|
end
|
|
169
164
|
|
|
165
|
+
# Issue #1985 — the plugin loader's own rows, which a `severity_overrides:` key names by their qualified
|
|
166
|
+
# id; they also answer to the bare `load-error` / `runtime-error`.
|
|
167
|
+
PLUGIN_LOADER_ROWS = %w[plugin_loader.load-error plugin_loader.runtime-error].freeze
|
|
168
|
+
private_constant :PLUGIN_LOADER_ROWS
|
|
169
|
+
|
|
170
|
+
# A warning for a `severity_overrides:` key that can reach no row, or nil. Mirrors {.disable_token_warning}
|
|
171
|
+
# (issue #1985): a plugin's row answers only to its qualified `plugin.<id>.<rule>` or `plugin.<id>`, so a
|
|
172
|
+
# `plugin.`-qualified key naming an unconfigured plugin, and any key outside the built-in vocabulary that is
|
|
173
|
+
# not a qualified plugin id (a plugin's bare rule included), is inert. A reserved run-level row is the home of
|
|
174
|
+
# this map, so naming one is never flagged; `all` is not a family wildcard here, so it is.
|
|
175
|
+
def self.override_key_warning(key, plugin_ids)
|
|
176
|
+
reason = override_key_problem(key, plugin_ids)
|
|
177
|
+
return nil if reason.nil?
|
|
178
|
+
|
|
179
|
+
Warning.new(kind: :severity_override, message: "severity_overrides: #{key.inspect} #{reason}",
|
|
180
|
+
fields: { "token" => key })
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def self.override_key_problem(key, plugin_ids)
|
|
184
|
+
return "is not a recognized rule id; the override has no effect" if key == "all"
|
|
185
|
+
return nil unless Array(Analysis::CheckRules.resolve_rule_token(key)).empty?
|
|
186
|
+
|
|
187
|
+
if key.start_with?(Analysis::CheckRules::PLUGIN_FAMILY_PREFIX)
|
|
188
|
+
return nil if plugin_ids.nil? || plausible_plugin_token?(key, plugin_ids)
|
|
189
|
+
|
|
190
|
+
"names no plugin `plugins:` configures; the override has no effect"
|
|
191
|
+
elsif key.start_with?("plugin_loader.")
|
|
192
|
+
"is not a recognized rule id; the override has no effect" unless PLUGIN_LOADER_ROWS.include?(key)
|
|
193
|
+
elsif inert_builtin_token?(key)
|
|
194
|
+
"is not a recognized rule id; the override has no effect"
|
|
195
|
+
elsif !non_check_engine_token?(key) && !severity_only_id?(key)
|
|
196
|
+
"is not a recognized rule id; the override has no effect " \
|
|
197
|
+
"(if this is a plugin's rule, write it as `plugin.<id>.<rule>`)"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
private_class_method :override_key_problem
|
|
201
|
+
|
|
170
202
|
# Issue #1953 — the run-level rows the diagnostic policy keeps on `severity_overrides:` and the
|
|
171
203
|
# baseline. `disable:` does not reach them by any spelling, so naming one there is inert.
|
|
172
204
|
RESERVED_RUN_LEVEL_FAMILIES = %w[plugin_loader plugin_trust rbs.coverage dynamic.dependency-source].freeze
|
|
@@ -224,6 +256,19 @@ module Rigor
|
|
|
224
256
|
end
|
|
225
257
|
private_class_method :reserved_run_level_token?
|
|
226
258
|
|
|
259
|
+
# Bare ids that a `severity_overrides:` key re-stamps but a marker or `disable:` cannot suppress: the `--explain`
|
|
260
|
+
# `fallback` row and the `contribution_merge` conflict reasons (`FlowContribution::CONFLICT_VALID_REASONS`,
|
|
261
|
+
# kebab-cased). They are not in `CheckRules::NON_CHECK_DIAGNOSTIC_IDS`, which would silence the warnings for a
|
|
262
|
+
# `disable:` token or marker naming them. `spec/rigor/config_audit_spec.rb` pins this list's completeness.
|
|
263
|
+
CONFLICT_IDS = FlowContribution::CONFLICT_VALID_REASONS.map { |reason| reason.to_s.tr("_", "-").freeze }.freeze
|
|
264
|
+
SEVERITY_ONLY_IDS = (%w[fallback] + CONFLICT_IDS).freeze
|
|
265
|
+
private_constant :CONFLICT_IDS, :SEVERITY_ONLY_IDS
|
|
266
|
+
|
|
267
|
+
def self.severity_only_id?(key)
|
|
268
|
+
SEVERITY_ONLY_IDS.include?(key)
|
|
269
|
+
end
|
|
270
|
+
private_class_method :severity_only_id?
|
|
271
|
+
|
|
227
272
|
def self.non_check_engine_token?(token)
|
|
228
273
|
Analysis::CheckRules::NON_CHECK_DIAGNOSTIC_IDS.include?(token) ||
|
|
229
274
|
Analysis::CheckRules::NON_CHECK_DIAGNOSTIC_FAMILIES.include?(token.split(".").first)
|
|
@@ -202,11 +202,17 @@ module Rigor
|
|
|
202
202
|
# always wins) and *above* the profile table. Exact rule ids only — the overlay never carries family
|
|
203
203
|
# wildcards. Empty while the overlay is unpopulated, so the default leaves resolution bit-for-bit
|
|
204
204
|
# unchanged.
|
|
205
|
+
# @param qualified_rule — the row's `<source_family>.<rule>` id (`plugin_loader.load-error`); equal to `rule`
|
|
206
|
+
# for a built-in row. Issue #1985 — an override key may spell it.
|
|
207
|
+
# @param plugin_family — `plugin.<id>` for a row a plugin emitted, nil for every other row. Such a row answers
|
|
208
|
+
# to an override only by its `qualified_rule` or this whole-plugin key, never by its bare `rule` or a family
|
|
209
|
+
# wildcard: plugins share bare rules (`load-error`), and a bare key would re-stamp them all.
|
|
205
210
|
# @return the resolved severity. Returns `:off` to mean "drop the diagnostic entirely".
|
|
206
|
-
def resolve(rule:, authored_severity:, profile: DEFAULT_PROFILE, overrides: {}, bleeding_edge_overrides: {}
|
|
211
|
+
def resolve(rule:, authored_severity:, profile: DEFAULT_PROFILE, overrides: {}, bleeding_edge_overrides: {},
|
|
212
|
+
qualified_rule: rule, plugin_family: nil)
|
|
207
213
|
return authored_severity if rule.nil?
|
|
208
214
|
|
|
209
|
-
override =
|
|
215
|
+
override = row_override(rule, qualified_rule, plugin_family, overrides)
|
|
210
216
|
return override.to_sym if override
|
|
211
217
|
|
|
212
218
|
bleeding = bleeding_edge_overrides[rule]
|
|
@@ -216,6 +222,18 @@ module Rigor
|
|
|
216
222
|
profile_table.fetch(rule, authored_severity)
|
|
217
223
|
end
|
|
218
224
|
|
|
225
|
+
# Issue #1985 — the `severity_overrides:` entry a row answers to, or nil. A plugin's row: its qualified id, then
|
|
226
|
+
# its `plugin.<id>`. Any other row: its qualified id (the plugin loader's
|
|
227
|
+
# `plugin_loader.load-error`), then its bare rule, then a family wildcard. The qualified key is the more specific
|
|
228
|
+
# one, so it wins over the bare key, as it does for a plugin's row.
|
|
229
|
+
def row_override(rule, qualified_rule, plugin_family, overrides)
|
|
230
|
+
return overrides[qualified_rule] || overrides[plugin_family] unless plugin_family.nil?
|
|
231
|
+
|
|
232
|
+
overrides[qualified_rule] || overrides[rule] || family_override(rule, overrides)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private_class_method :row_override
|
|
236
|
+
|
|
219
237
|
# A family name has no `.` in it, so only a dot-free key can match one. `resolve` runs for every
|
|
220
238
|
# diagnostic in a run's stream, and most configurations carry no such key (usually no overrides at all),
|
|
221
239
|
# so the split below is skipped for them rather than allocating a name that nothing can match.
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "configuration/dependencies"
|
|
|
8
8
|
require_relative "configuration/severity_profile"
|
|
9
9
|
require_relative "config_path"
|
|
10
10
|
require_relative "configuration_error"
|
|
11
|
+
require_relative "rooted_glob"
|
|
11
12
|
require_relative "effects/entry_points"
|
|
12
13
|
require_relative "effects/label"
|
|
13
14
|
require_relative "effects/method_key"
|
|
@@ -995,9 +996,34 @@ module Rigor
|
|
|
995
996
|
# this entry" so a templated `**` pattern in a fresh project doesn't generate an error per match-less
|
|
996
997
|
# pattern.
|
|
997
998
|
def expand_pre_eval_entries(entries)
|
|
998
|
-
entries.flat_map
|
|
999
|
-
|
|
1000
|
-
|
|
999
|
+
entries.flat_map { |entry| expand_pre_eval_entry(entry) }.uniq.freeze
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
# #1956 — a relative entry arrives joined onto the config file's directory, so the metacharacters the
|
|
1003
|
+
# check above sees may be that directory's own (`ds [x]/`). The leading run of segments that name existing
|
|
1004
|
+
# directories is read literally and only the rest is globbed under it: a literal entry under such a
|
|
1005
|
+
# directory stays a literal path, and a pattern matches what it matches under a plain directory.
|
|
1006
|
+
def expand_pre_eval_entry(entry)
|
|
1007
|
+
return [entry] unless glob_pattern?(entry)
|
|
1008
|
+
|
|
1009
|
+
base, pattern = literal_directory_prefix(entry)
|
|
1010
|
+
return [entry] unless glob_pattern?(pattern)
|
|
1011
|
+
|
|
1012
|
+
base ? RootedGlob.glob(base, pattern) : Dir.glob(entry, sort: true)
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
def literal_directory_prefix(entry)
|
|
1016
|
+
segments = entry.split("/")
|
|
1017
|
+
count = (1...segments.size).take_while { |n| File.directory?(join_segments(segments.take(n))) }.size
|
|
1018
|
+
return [nil, entry] if count.zero?
|
|
1019
|
+
|
|
1020
|
+
[join_segments(segments.take(count)), segments.drop(count).join("/")]
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
# `"/a".split("/")` leads with an empty segment, which spells the filesystem root.
|
|
1024
|
+
def join_segments(segments)
|
|
1025
|
+
path = segments.join("/")
|
|
1026
|
+
path.empty? ? "/" : path
|
|
1001
1027
|
end
|
|
1002
1028
|
|
|
1003
1029
|
def glob_pattern?(path)
|