rigortype 0.3.2 → 0.3.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/builtins/ruby_core/array.yml +11 -24
- data/data/builtins/ruby_core/enumerable.yml +4 -8
- data/data/builtins/ruby_core/exception.yml +6 -9
- data/data/builtins/ruby_core/hash.yml +13 -20
- data/data/builtins/ruby_core/io.yml +6 -9
- data/data/builtins/ruby_core/numeric.yml +2 -3
- data/data/builtins/ruby_core/proc.yml +4 -8
- data/data/builtins/ruby_core/re.yml +6 -9
- data/data/builtins/ruby_core/set.yml +2 -3
- data/data/builtins/ruby_core/string.yml +4 -7
- data/data/builtins/ruby_core/struct.yml +0 -6
- data/data/builtins/ruby_core/time.yml +3 -6
- data/data/effects/core.yml +843 -0
- data/data/effects/registry.yml +67 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +311 -1
- data/docs/manual/03-configuration.md +84 -1
- data/docs/manual/04-diagnostics.md +14 -2
- data/docs/manual/16-rbs-extended-annotations.md +77 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/README.md +5 -0
- data/docs/manual/plugins/rigor-actionmailer.md +5 -0
- data/docs/manual/plugins/rigor-activejob.md +47 -0
- data/docs/manual/plugins/rigor-factorybot.md +16 -0
- data/docs/manual/plugins/rigor-pundit.md +26 -0
- data/docs/manual/plugins/rigor-rails-routes.md +41 -2
- data/docs/manual/plugins/rigor-rspec-rails.md +5 -0
- data/docs/manual/plugins/rigor-rspec.md +8 -0
- data/docs/manual/plugins/rigor-sidekiq.md +54 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
- data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +62 -22
- data/lib/rigor/analysis/incremental_session.rb +140 -6
- data/lib/rigor/analysis/reachability/graph.rb +244 -0
- data/lib/rigor/analysis/reachability/plugin_roots.rb +164 -0
- data/lib/rigor/analysis/reachability/project_files.rb +58 -0
- data/lib/rigor/analysis/reachability/scan.rb +241 -0
- data/lib/rigor/analysis/reachability/signature_scan.rb +134 -0
- data/lib/rigor/analysis/rule_catalog.rb +163 -1
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +54 -0
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
- data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +68 -10
- data/lib/rigor/analysis/runner.rb +420 -10
- data/lib/rigor/analysis/severity_stamp.rb +2 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/bleeding_edge.rb +22 -0
- data/lib/rigor/cache/incremental_snapshot.rb +24 -5
- data/lib/rigor/cli/check_command.rb +13 -1
- data/lib/rigor/cli/check_runner_factory.rb +4 -1
- data/lib/rigor/cli/effects_command.rb +131 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +123 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +54 -0
- data/lib/rigor/cli/effects_report.rb +53 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +256 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +16 -0
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +296 -14
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +182 -0
- data/lib/rigor/effects/config_envelopes.rb +185 -0
- data/lib/rigor/effects/discharge.rb +69 -0
- data/lib/rigor/effects/effect_table.rb +92 -0
- data/lib/rigor/effects/entry_points.rb +93 -0
- data/lib/rigor/effects/envelope.rb +86 -0
- data/lib/rigor/effects/envelope_check.rb +172 -0
- data/lib/rigor/effects/envelope_index.rb +157 -0
- data/lib/rigor/effects/file_collection.rb +160 -0
- data/lib/rigor/effects/framework_units.rb +167 -0
- data/lib/rigor/effects/identity.rb +104 -0
- data/lib/rigor/effects/label.rb +77 -0
- data/lib/rigor/effects/label_intent.rb +73 -0
- data/lib/rigor/effects/label_set.rb +136 -0
- data/lib/rigor/effects/liskov_check.rb +167 -0
- data/lib/rigor/effects/local_ownership.rb +132 -0
- data/lib/rigor/effects/method_key.rb +40 -0
- data/lib/rigor/effects/mutation_classifier.rb +92 -0
- data/lib/rigor/effects/narrowing.rb +202 -0
- data/lib/rigor/effects/origin.rb +66 -0
- data/lib/rigor/effects/path_finder.rb +89 -0
- data/lib/rigor/effects/plugin_facts.rb +335 -0
- data/lib/rigor/effects/propagator.rb +275 -0
- data/lib/rigor/effects/registry.rb +193 -0
- data/lib/rigor/effects/scanner.rb +290 -0
- data/lib/rigor/effects/signature_sources.rb +74 -0
- data/lib/rigor/effects/snapshot.rb +380 -0
- data/lib/rigor/effects/snapshot_diff.rb +265 -0
- data/lib/rigor/effects/summary.rb +154 -0
- data/lib/rigor/effects/taint_cause.rb +38 -0
- data/lib/rigor/effects/unit_scan.rb +572 -0
- data/lib/rigor/effects/unknown_label_check.rb +86 -0
- data/lib/rigor/effects/unknown_label_report.rb +59 -0
- data/lib/rigor/effects.rb +47 -0
- data/lib/rigor/environment/rbs_loader.rb +34 -0
- data/lib/rigor/environment.rb +8 -1
- data/lib/rigor/flow_contribution/element.rb +1 -0
- data/lib/rigor/flow_contribution/merge_result.rb +5 -3
- data/lib/rigor/flow_contribution/merger.rb +16 -1
- data/lib/rigor/flow_contribution.rb +20 -4
- data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +74 -17
- data/lib/rigor/inference/fork_map.rb +45 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +147 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +97 -2
- data/lib/rigor/inference/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/optimistic_origin.rb +68 -3
- data/lib/rigor/inference/pre_eval_constants.rb +189 -0
- data/lib/rigor/inference/scope_indexer.rb +134 -31
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +25 -12
- data/lib/rigor/plugin/base.rb +24 -0
- data/lib/rigor/plugin/effect_attribution.rb +208 -0
- data/lib/rigor/plugin/effect_edge.rb +101 -0
- data/lib/rigor/plugin/effect_entry_points.rb +51 -0
- data/lib/rigor/plugin/first_party.rb +57 -0
- data/lib/rigor/plugin/isolation.rb +4 -1
- data/lib/rigor/plugin/manifest.rb +103 -3
- data/lib/rigor/plugin/node_rule_walk.rb +6 -0
- data/lib/rigor/plugin/registry.rb +48 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +20 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +168 -0
- data/lib/rigor/reflection.rb +131 -30
- data/lib/rigor/scope.rb +62 -2
- data/lib/rigor/source/node_walker.rb +12 -0
- data/lib/rigor/type/anonymous_class_name.rb +40 -0
- data/lib/rigor/type/nominal.rb +5 -2
- data/lib/rigor/type/singleton.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +385 -28
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
- data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
- data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
- data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +56 -3
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor/rbs_extended.rbs +6 -0
- data/sig/rigor/scope.rbs +4 -0
- data/sig/rigor.rbs +21 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +83 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
class Sidekiq < Rigor::Plugin::Base
|
|
8
|
+
# ADR-102 WD3 / #367 — which workers does this project enqueue by NAME, from schedule configuration?
|
|
9
|
+
#
|
|
10
|
+
# This is the one genuinely Sidekiq-specific reachability root, and the reason it is worth reading YAML
|
|
11
|
+
# for: a cron-scheduled worker is enqueued from `config/schedule.yml` as the string `"HardWorker"`, so
|
|
12
|
+
# the repository may contain no `HardWorker.perform_async` anywhere. The constant scan sees nothing, and
|
|
13
|
+
# `rigor unused` reports a worker that runs every night as dead code.
|
|
14
|
+
#
|
|
15
|
+
# Two layouts, one key. `sidekiq-cron` writes the schedule as the whole document
|
|
16
|
+
# (`{ name => { "cron" =>, "class" => } }`); `sidekiq-scheduler` nests the same entries under
|
|
17
|
+
# `config/sidekiq.yml`'s `:scheduler: :schedule:` block. Both name the worker under `class:`, and
|
|
18
|
+
# `class:` is the ONLY key read here.
|
|
19
|
+
#
|
|
20
|
+
# **What this refuses to read is the queue list.** `:queues:` in `sidekiq.yml` holds queue names, and a
|
|
21
|
+
# queue name is not a class name — inflecting `report_worker` into `ReportWorker` would manufacture a
|
|
22
|
+
# root out of a naming coincidence, which is precisely the over-supply this plugin declined in #350. An
|
|
23
|
+
# over-claiming root source silently hides real dead code (ADR-102 § Consequences), and nothing
|
|
24
|
+
# downstream can tell you it happened. So a queue contributes nothing, deliberately.
|
|
25
|
+
#
|
|
26
|
+
# Names are not trusted either: the caller intersects them with the workers {WorkerDiscoverer} actually
|
|
27
|
+
# found, so a typo or an out-of-tree class costs coverage rather than manufacturing a root, and the
|
|
28
|
+
# report's `matched no declaration` counter stays meaningful.
|
|
29
|
+
#
|
|
30
|
+
# Fail-soft throughout, because this reads user-authored config that Rigor does not own: an absent file,
|
|
31
|
+
# an unreadable one, a YAML syntax error, or a document that is not a Hash contributes nothing rather
|
|
32
|
+
# than raising. `rigor unused` is a report a human reads, and refusing to print it because one
|
|
33
|
+
# `sidekiq.yml` has a stray tab is a bad trade.
|
|
34
|
+
class ScheduleScan
|
|
35
|
+
# The key naming the worker, in both layouts. Symbol keys are accepted because `sidekiq.yml` is
|
|
36
|
+
# conventionally written with them (`:scheduler:`, `:schedule:`), and Psych parses `:class:` as the
|
|
37
|
+
# Symbol `:class`.
|
|
38
|
+
CLASS_KEY = "class"
|
|
39
|
+
|
|
40
|
+
# Where a schedule block hides inside a document. `[]` is the document itself — `sidekiq-cron`'s
|
|
41
|
+
# `schedule.yml` IS the schedule map — and the nested paths are `sidekiq-scheduler`'s, whose entries
|
|
42
|
+
# live under `:scheduler: :schedule:` (with the bare `:schedule:` form kept for pre-3.0 layouts).
|
|
43
|
+
SCHEDULE_BLOCK_KEY_PATHS = [[], %w[scheduler schedule], %w[schedule]].freeze
|
|
44
|
+
|
|
45
|
+
# Errno classes that mean "this path is not readable as a schedule" — swallowed so one bad path does
|
|
46
|
+
# not cost the roots the other paths supply.
|
|
47
|
+
IO_ERRORS = [Errno::ENOENT, Errno::EACCES, Errno::EISDIR].freeze
|
|
48
|
+
|
|
49
|
+
def initialize(io_boundary:, schedule_paths:)
|
|
50
|
+
@io_boundary = io_boundary
|
|
51
|
+
@schedule_paths = schedule_paths
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Array<String>] the class names named by a `class:` key in a schedule entry, sorted and
|
|
55
|
+
# unique. NOT yet intersected with the discovered workers — the caller does that.
|
|
56
|
+
def worker_names
|
|
57
|
+
names = Set.new
|
|
58
|
+
@schedule_paths.each do |path|
|
|
59
|
+
document = load_document(path)
|
|
60
|
+
next unless document.is_a?(Hash)
|
|
61
|
+
|
|
62
|
+
SCHEDULE_BLOCK_KEY_PATHS.each { |keys| collect_entries(dig_block(document, keys), names) }
|
|
63
|
+
end
|
|
64
|
+
names.to_a.sort
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def load_document(path)
|
|
70
|
+
absolute = File.expand_path(path.to_s)
|
|
71
|
+
return nil unless File.file?(absolute)
|
|
72
|
+
|
|
73
|
+
contents = read_safely(absolute)
|
|
74
|
+
contents && parse_safely(contents)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def read_safely(path)
|
|
78
|
+
@io_boundary.read_file(path)
|
|
79
|
+
rescue Plugin::AccessDeniedError, *IO_ERRORS
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# `safe_load` with no permitted classes beyond Symbol: a schedule file is data, and Rigor never loads
|
|
84
|
+
# the Rails environment or the sidekiq runtime to read it. `aliases: true` because YAML anchors are
|
|
85
|
+
# ordinary style in a hand-maintained schedule.
|
|
86
|
+
def parse_safely(contents)
|
|
87
|
+
YAML.safe_load(contents, aliases: true, permitted_classes: [Symbol])
|
|
88
|
+
rescue Psych::Exception
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Walks one of {SCHEDULE_BLOCK_KEY_PATHS} into the document. String and Symbol keys are both tried because
|
|
93
|
+
# `sidekiq.yml` is written with Symbol keys and `schedule.yml` with String ones.
|
|
94
|
+
def dig_block(document, keys)
|
|
95
|
+
keys.reduce(document) do |node, key|
|
|
96
|
+
return nil unless node.is_a?(Hash)
|
|
97
|
+
|
|
98
|
+
fetch_either(node, key)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# A schedule block maps an arbitrary job NAME to an entry Hash. Only the entry's own `class:` is read
|
|
103
|
+
# — no recursion into the value, so a nested option that happens to be called `class` cannot enter,
|
|
104
|
+
# and neither can anything under `:queues:`, whose values are Strings and Arrays rather than entries.
|
|
105
|
+
def collect_entries(block, names)
|
|
106
|
+
return unless block.is_a?(Hash)
|
|
107
|
+
|
|
108
|
+
block.each_value do |entry|
|
|
109
|
+
next unless entry.is_a?(Hash)
|
|
110
|
+
|
|
111
|
+
value = fetch_either(entry, CLASS_KEY)
|
|
112
|
+
names << value if value.is_a?(String) && !value.empty?
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def fetch_either(hash, key)
|
|
117
|
+
hash.fetch(key) { hash[key.to_sym] }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -4,6 +4,7 @@ require "rigor/plugin"
|
|
|
4
4
|
|
|
5
5
|
require_relative "sidekiq/worker_index"
|
|
6
6
|
require_relative "sidekiq/worker_discoverer"
|
|
7
|
+
require_relative "sidekiq/schedule_scan"
|
|
7
8
|
require_relative "sidekiq/analyzer"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
@@ -22,6 +23,7 @@ module Rigor
|
|
|
22
23
|
# config:
|
|
23
24
|
# worker_search_paths: ["app/workers", "app/sidekiq"] # default; optional
|
|
24
25
|
# worker_marker_modules: ["Sidekiq::Job", "Sidekiq::Worker"] # default; optional
|
|
26
|
+
# schedule_paths: ["config/schedule.yml", "config/sidekiq.yml"] # default; optional
|
|
25
27
|
#
|
|
26
28
|
# ## What it checks
|
|
27
29
|
#
|
|
@@ -30,6 +32,11 @@ module Rigor
|
|
|
30
32
|
# schedule and forward the rest. Mismatches emit `wrong-arity`.
|
|
31
33
|
# 2. **Missing schedule** — `perform_in()` / `perform_at()` with zero arguments emit `missing-schedule`.
|
|
32
34
|
#
|
|
35
|
+
# ## What it contributes to `rigor unused`
|
|
36
|
+
#
|
|
37
|
+
# The workers a schedule file names under `class:` and nothing else (ADR-102 WD3 / #367) — see
|
|
38
|
+
# {#prepare} and {ScheduleScan}.
|
|
39
|
+
#
|
|
33
40
|
# ## Limitations (v0.1.0)
|
|
34
41
|
#
|
|
35
42
|
# - Direct `include` matches only against the configured marker modules. Indirect includes via a
|
|
@@ -41,12 +48,21 @@ module Rigor
|
|
|
41
48
|
class Sidekiq < Rigor::Plugin::Base
|
|
42
49
|
manifest(
|
|
43
50
|
id: "sidekiq",
|
|
44
|
-
|
|
51
|
+
# Bumped 2026-08-16 — publishes `:reachability_roots` for `rigor unused` (ADR-102 WD3): the workers
|
|
52
|
+
# a schedule file enqueues by name, which no `perform_async` call site writes down.
|
|
53
|
+
version: "0.2.0",
|
|
45
54
|
description: "Validates Sidekiq `Worker.perform_async` argument arity.",
|
|
46
55
|
config_schema: {
|
|
47
56
|
"worker_search_paths" => { kind: :array, default: ["app/workers", "app/sidekiq"] },
|
|
48
|
-
"worker_marker_modules" => { kind: :array, default: %w[Sidekiq::Job Sidekiq::Worker] }
|
|
49
|
-
|
|
57
|
+
"worker_marker_modules" => { kind: :array, default: %w[Sidekiq::Job Sidekiq::Worker] },
|
|
58
|
+
# `schedule_paths` — the schedule configuration {ScheduleScan} reads for the reachability roots
|
|
59
|
+
# below. The two defaults are the conventional locations of the two schedule layouts in the wild:
|
|
60
|
+
# `sidekiq-cron`'s `config/schedule.yml` and `sidekiq-scheduler`'s `:scheduler: :schedule:` block
|
|
61
|
+
# inside `config/sidekiq.yml`. A project that keeps its schedule elsewhere lists the file itself;
|
|
62
|
+
# these are file paths, not directories, because a schedule is a named document rather than a tree.
|
|
63
|
+
"schedule_paths" => { kind: :array, default: ["config/schedule.yml", "config/sidekiq.yml"] }
|
|
64
|
+
},
|
|
65
|
+
produces: [:reachability_roots]
|
|
50
66
|
)
|
|
51
67
|
|
|
52
68
|
producer :worker_index, watch: -> { [[@worker_search_paths, "**/*.rb"]] } do |_params|
|
|
@@ -57,9 +73,46 @@ module Rigor
|
|
|
57
73
|
).discover
|
|
58
74
|
end
|
|
59
75
|
|
|
76
|
+
# Cached separately from `:worker_index` because the two invalidate on different files: editing a
|
|
77
|
+
# schedule changes which workers are reached without touching `app/workers` at all. The `watch:` roots
|
|
78
|
+
# the glob at the working directory so that CREATING a schedule file — not just editing one — is seen.
|
|
79
|
+
producer :scheduled_workers, watch: -> { [[".", *@schedule_paths]] } do |_params|
|
|
80
|
+
ScheduleScan.new(
|
|
81
|
+
io_boundary: io_boundary,
|
|
82
|
+
schedule_paths: @schedule_paths
|
|
83
|
+
).worker_names
|
|
84
|
+
end
|
|
85
|
+
|
|
60
86
|
def init(_services)
|
|
61
87
|
@worker_search_paths = Array(config.fetch("worker_search_paths")).map(&:to_s)
|
|
62
88
|
@worker_marker_modules = Array(config.fetch("worker_marker_modules")).map(&:to_s)
|
|
89
|
+
@schedule_paths = Array(config.fetch("schedule_paths")).map(&:to_s)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# ADR-102 WD3 — publishes the workers a schedule file enqueues BY NAME, for `rigor unused`.
|
|
93
|
+
#
|
|
94
|
+
# `MyWorker.perform_async(...)` writes the worker's name as an ordinary constant, so the report's scan
|
|
95
|
+
# already records that edge and a root would add nothing. A worker named only as the string
|
|
96
|
+
# `class: "MyWorker"` in `config/schedule.yml` is the opposite case: it runs every night and the
|
|
97
|
+
# constant appears nowhere, so it reads as dead.
|
|
98
|
+
#
|
|
99
|
+
# The intersection is what keeps the contribution honest, exactly as in `rigor-pundit`. {ScheduleScan}
|
|
100
|
+
# says which names the schedule WRITES; {WorkerDiscoverer} says which workers EXIST. A `class:` value
|
|
101
|
+
# matching no discovered worker — a typo, a renamed class, a job living outside `worker_search_paths` —
|
|
102
|
+
# is dropped rather than published, so the failure mode is a missing root (a candidate row a human can
|
|
103
|
+
# judge) instead of a spurious one (silence where dead code used to be).
|
|
104
|
+
#
|
|
105
|
+
# Publishing the discovered worker set instead would have been one line and is what this refuses to do:
|
|
106
|
+
# "a file exists under `app/workers`" is not evidence that anything enqueues it.
|
|
107
|
+
def prepare(services)
|
|
108
|
+
index = producer_value(:worker_index)
|
|
109
|
+
scheduled = producer_value(:scheduled_workers)
|
|
110
|
+
return if index.nil? || scheduled.nil?
|
|
111
|
+
|
|
112
|
+
roots = scheduled.select { |name| index.known?(name) }
|
|
113
|
+
return if roots.empty?
|
|
114
|
+
|
|
115
|
+
services.fact_store.publish(plugin_id: manifest.id, name: :reachability_roots, value: roots)
|
|
63
116
|
end
|
|
64
117
|
|
|
65
118
|
# File-level only: the load-error emission. The per-call arity validation runs over the engine-owned
|
|
@@ -5,6 +5,7 @@ module Rigor
|
|
|
5
5
|
def self?.build_node_collectors: (String path, untyped scope_index) -> Hash[Symbol, untyped]
|
|
6
6
|
def self?.node_collector_driver: (Hash[Symbol, untyped] collectors) -> untyped
|
|
7
7
|
def self?.shadow_verify_converged_collectors: (String path, untyped root, untyped scope_index, Hash[Symbol, untyped]? collectors) -> void
|
|
8
|
+
def self?.filter_suppressed: (Array[Diagnostic] diagnostics, comments: untyped, disabled_rules: Array[String]) -> Array[Diagnostic]
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
class FactStore
|
data/sig/rigor/inference.rbs
CHANGED
|
@@ -163,6 +163,8 @@ module Rigor
|
|
|
163
163
|
module ScopeIndexer
|
|
164
164
|
def self?.index: (untyped root, default_scope: Scope) -> Hash[untyped, Scope]
|
|
165
165
|
def self?.build_declaration_overrides: (untyped root) -> Hash[untyped, Type::t]
|
|
166
|
+
def self?.build_declaration_artifacts: (untyped root) -> [Hash[untyped, Type::t], Hash[String, Type::t]]
|
|
167
|
+
def self?.build_in_source_constants: (untyped root, Scope default_scope) -> Hash[String, Type::t]
|
|
166
168
|
def self?.record_declarations: (untyped node, Array[String] qualified_prefix, Hash[untyped, Type::t] identity_table, Hash[String, Type::t] discovered) -> void
|
|
167
169
|
def self?.discovered_classes_for_paths: (Array[String] paths, ?buffer: untyped) -> Hash[String, Type::t]
|
|
168
170
|
def self?.discovered_def_index_for_paths: (Array[String] paths, ?buffer: untyped) -> Hash[Symbol, untyped]
|
data/sig/rigor/rbs_extended.rbs
CHANGED
|
@@ -47,6 +47,12 @@ module Rigor
|
|
|
47
47
|
|
|
48
48
|
def self?.parse_conforms_to_annotation: (String? string) -> String?
|
|
49
49
|
|
|
50
|
+
# ADR-103 #383 — the effect-envelope reader. `annotations` is any list of RBS annotation nodes
|
|
51
|
+
# (`RBS::Definition::Method#annotations`, or a declaration / member's own `#annotations`); the result
|
|
52
|
+
# is a `Rigor::Effects::Envelope`, or nil when the list carries neither purity spelling.
|
|
53
|
+
def self?.pure_annotation?: (String? string) -> bool
|
|
54
|
+
def self?.read_effect_envelope: (untyped annotations, owner_key: String, ?source: Symbol, ?registry: untyped, ?reporter: untyped) -> untyped?
|
|
55
|
+
|
|
50
56
|
class ParamOverride
|
|
51
57
|
attr_reader param_name: Symbol
|
|
52
58
|
attr_reader type: Type::t
|
data/sig/rigor/scope.rbs
CHANGED
|
@@ -19,6 +19,7 @@ module Rigor
|
|
|
19
19
|
attr_reader method_chain_narrowings: Hash[ChainKey, Type::t]
|
|
20
20
|
attr_reader source_path: String?
|
|
21
21
|
attr_reader struct_fold_safe_locals: Set[Symbol]
|
|
22
|
+
attr_reader opaque_block_self: bool
|
|
22
23
|
|
|
23
24
|
# ADR-53 Track A — the seed-time discovery tables live on the
|
|
24
25
|
# DiscoveryIndex; Scope keeps per-table readers as delegates.
|
|
@@ -90,6 +91,8 @@ module Rigor
|
|
|
90
91
|
def with_source_path: (String? path) -> Scope
|
|
91
92
|
def with_struct_fold_safe: (Set[Symbol] locals) -> Scope
|
|
92
93
|
def struct_fold_safe?: (String | Symbol name) -> bool
|
|
94
|
+
def entering_opaque_block: () -> Scope
|
|
95
|
+
def opaque_block_self?: () -> bool
|
|
93
96
|
def with_discovery: (DiscoveryIndex index) -> Scope
|
|
94
97
|
def local: (String | Symbol name) -> Type::t?
|
|
95
98
|
def ivar: (String | Symbol name) -> Type::t?
|
|
@@ -123,6 +126,7 @@ module Rigor
|
|
|
123
126
|
def singleton_def_for: (String | Symbol class_name, String | Symbol method_name) -> untyped?
|
|
124
127
|
def user_def_site_for: (String | Symbol class_name, String | Symbol method_name) -> String?
|
|
125
128
|
def top_level_def_for: (String | Symbol method_name) -> untyped?
|
|
129
|
+
def bindable_top_level_def_for: (String | Symbol method_name) -> untyped?
|
|
126
130
|
def toplevel?: () -> bool
|
|
127
131
|
def discovered_method_visibility: (String | Symbol class_name, String | Symbol method_name) -> Symbol?
|
|
128
132
|
def superclass_of: (String | Symbol class_name) -> String?
|
data/sig/rigor.rbs
CHANGED
|
@@ -14,8 +14,12 @@ module Rigor
|
|
|
14
14
|
def self.load: (?String? path) -> Configuration
|
|
15
15
|
def self.discover: () -> String?
|
|
16
16
|
def self.load_with_includes: (String path, ?visited: Set[String]) -> Hash[String, untyped]
|
|
17
|
-
def initialize: (?Hash[String, untyped] data) -> void
|
|
17
|
+
def initialize: (?Hash[String, untyped] data, ?bool effects_key_present) -> void
|
|
18
18
|
def to_h: () -> Hash[String, untyped]
|
|
19
|
+
# ADR-103 WD13 — the `effects:` block, nil when the key was absent. Presence is the opt-in.
|
|
20
|
+
attr_reader effects: Hash[String, untyped]?
|
|
21
|
+
def effects_enabled?: () -> bool
|
|
22
|
+
def with_effects_enabled: () -> Rigor::Configuration
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
class CLI
|
|
@@ -58,6 +62,10 @@ module Rigor
|
|
|
58
62
|
# `node` / `location` are Prism values, received as `untyped`).
|
|
59
63
|
def self.from_node: (untyped node, path: String, message: String, ?severity: Symbol, ?rule: String?, ?source_family: String, ?receiver_type: untyped, ?method_name: untyped, ?project_definition_site: untyped) -> Rigor::Analysis::Diagnostic
|
|
60
64
|
def self.from_location: (untyped location, path: String, message: String, ?severity: Symbol, ?rule: String?, ?source_family: String, ?receiver_type: untyped, ?method_name: untyped, ?project_definition_site: untyped) -> Rigor::Analysis::Diagnostic
|
|
65
|
+
# `from_message_loc` / `from_name_loc` forward `**` to `from_location`, so their keyword surface is
|
|
66
|
+
# `from_location`'s; only the positional node differs.
|
|
67
|
+
def self.from_message_loc: (untyped node, path: String, message: String, ?severity: Symbol, ?rule: String?, ?source_family: String, ?receiver_type: untyped, ?method_name: untyped, ?project_definition_site: untyped) -> Rigor::Analysis::Diagnostic
|
|
68
|
+
def self.from_name_loc: (untyped node, path: String, message: String, ?severity: Symbol, ?rule: String?, ?source_family: String, ?receiver_type: untyped, ?method_name: untyped, ?project_definition_site: untyped) -> Rigor::Analysis::Diagnostic
|
|
61
69
|
end
|
|
62
70
|
|
|
63
71
|
class Result
|
|
@@ -121,6 +129,18 @@ module Rigor
|
|
|
121
129
|
def analysis_file_set: (?Array[String] paths) -> Array[String]
|
|
122
130
|
def file_dependents: () -> Hash[String, untyped]
|
|
123
131
|
def prepare_project_scan: (?paths: Array[String]) -> untyped
|
|
132
|
+
# ADR-103 — the propagated effect graph and the merged per-file collections behind it. A report
|
|
133
|
+
# surface: never a diagnostic, empty when the configuration carries no `effects:` block. Typed
|
|
134
|
+
# `untyped` for the same reason `cache_store` is — `Rigor::Effects` is not sig-covered yet
|
|
135
|
+
# (`spec/rigor/public_api_drift_spec.rb`), and a named reference would raise `RBS::UnknownTypeName`.
|
|
136
|
+
def effect_table: () -> untyped
|
|
137
|
+
def effect_collection: () -> untyped
|
|
138
|
+
# #381 — `{ "Class#m" => [path] }`, which the snapshot's `reach:` globs match against. Same
|
|
139
|
+
# `untyped` reasoning as its two siblings above.
|
|
140
|
+
def effect_sources: () -> untyped
|
|
141
|
+
# #387 — the loaded plugins' compiled effect contributions, read by `rigor effects` so the snapshot's
|
|
142
|
+
# vocabulary is the one the collection window scanned under. Same `untyped` reasoning again.
|
|
143
|
+
def effect_plugin_facts: () -> untyped
|
|
124
144
|
end
|
|
125
145
|
end
|
|
126
146
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rigor-unused-adjudicate
|
|
3
|
+
description: |
|
|
4
|
+
Find dead code in a Ruby project with `rigor unused` — establish what the report can see on THIS project first, then adjudicate every row before proposing any deletion. Use this whenever someone asks to find or remove dead code, unused classes, unused constants, or "code nobody calls", whenever they ask what a `rigor unused` report means or which rows are safe to delete, and whenever a dead-code cleanup, codebase inventory, or legacy audit comes up — even if they never say "rigor". The report is a review queue and not a defect list; on an adjudicated corpus target only 4 of 57 rows were genuinely dead, so acting on it directly produces mostly wrong deletions. NOT for deleting a specific class you already know is dead, and NOT for `rigor check` diagnostics (those are ordinary type errors).
|
|
5
|
+
license: MPL-2.0
|
|
6
|
+
metadata:
|
|
7
|
+
version: 0.2.0
|
|
8
|
+
homepage: https://github.com/rigortype/rigor
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Adjudicating `rigor unused`
|
|
12
|
+
|
|
13
|
+
`rigor unused` answers *which project classes and modules does no reachable
|
|
14
|
+
code name?* — not *what can I delete*. Closing that gap is the work, and the
|
|
15
|
+
manual chapter has the method:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
rigor docs 18-removing-dead-code
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Read it before adjudicating anything. It carries the false-positive shapes with
|
|
22
|
+
their measured frequencies, the pre-deletion checklist, and how to report the
|
|
23
|
+
result. This skill exists for the part the chapter cannot do for you: **work out
|
|
24
|
+
what the report can and cannot see on this particular project, before you trust
|
|
25
|
+
a single row.**
|
|
26
|
+
|
|
27
|
+
Do that first, because every check below changes how the output should be read —
|
|
28
|
+
and skipping one is how a live class ends up on a deletion list.
|
|
29
|
+
|
|
30
|
+
## Establish the ground first
|
|
31
|
+
|
|
32
|
+
Run the report and answer these against the project in front of you.
|
|
33
|
+
|
|
34
|
+
**Are framework roots actually supplied?** The summary prints
|
|
35
|
+
`roots: N (M from plugins, …)`. On a framework application `0 from plugins`
|
|
36
|
+
means nothing is naming your controllers, jobs or policies, so most of the
|
|
37
|
+
report is noise. Stop and fix the plugin configuration — `rigor docs
|
|
38
|
+
07-plugins` — rather than adjudicating hundreds of rows.
|
|
39
|
+
|
|
40
|
+
**Does the project ship signatures?** If `signature_paths:` is configured, also
|
|
41
|
+
run with it emptied and compare:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
rigor unused # normal
|
|
45
|
+
rigor unused --config /tmp/no-sig.yml # a copy of your config with signature_paths: []
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Adjudicate the union. On one application the signature-free run surfaced three
|
|
49
|
+
genuinely dead classes that the default run never showed. Rows that appear only
|
|
50
|
+
without signatures are still worth checking — signatures reference classes, and
|
|
51
|
+
a reference the report counts is not always one a human would.
|
|
52
|
+
|
|
53
|
+
**What do `paths:` actually cover?** Only Ruby under those paths is analysed.
|
|
54
|
+
Views are the common gap: a helper called from `app/views/**/*.erb` has no Ruby
|
|
55
|
+
caller at all, and will land in the report looking unused. Before believing any
|
|
56
|
+
helper or presenter row, grep the view tree for it.
|
|
57
|
+
|
|
58
|
+
**Is the git history usable?** The chapter's checklist uses "when did this file
|
|
59
|
+
last change" as evidence. In a shallow clone, or one whose history is all
|
|
60
|
+
dependency bumps, that signal is empty — notice it and say so rather than
|
|
61
|
+
reporting a bot commit as the file's age.
|
|
62
|
+
|
|
63
|
+
## Then hand off to the chapter
|
|
64
|
+
|
|
65
|
+
With that established, follow the chapter. Two things worth holding onto as you
|
|
66
|
+
go, because they are what the report's own design is built around:
|
|
67
|
+
|
|
68
|
+
**Framework conventions and configuration reach code without naming it.** The
|
|
69
|
+
chapter lists the shapes; the ones that recur hardest are a class named as a
|
|
70
|
+
*string* in `config/*.yml` (a recurring-job schedule, a queue definition), a
|
|
71
|
+
convention that derives one name from another (`FooHelper` from
|
|
72
|
+
`FooController`, a decorator from a model, a join model from
|
|
73
|
+
`has_many :speakers_talks`), and a registration DSL called inside the class
|
|
74
|
+
body. None of these appear as a constant anywhere.
|
|
75
|
+
|
|
76
|
+
**Wrong rows cascade.** A class used only by a wrongly-classified class follows
|
|
77
|
+
it into the report. When a row turns out to be live, re-check what it
|
|
78
|
+
references before treating those rows as independent findings.
|
|
79
|
+
|
|
80
|
+
## Two habits that keep the answer honest
|
|
81
|
+
|
|
82
|
+
**A search that finds nothing proves nothing until it has found something.**
|
|
83
|
+
Before concluding a name appears nowhere, run the same search against a name you
|
|
84
|
+
know is used. Silence from a broken command and silence from dead code look
|
|
85
|
+
identical.
|
|
86
|
+
|
|
87
|
+
**Over-claiming hides dead code; under-claiming does not.** If you cannot settle
|
|
88
|
+
a row, leave it in the report and say you could not settle it. A row left on the
|
|
89
|
+
list is visible to a human; a row you dismissed on a hunch is invisible to
|
|
90
|
+
everyone.
|