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,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optionparser"
|
|
4
|
+
|
|
5
|
+
require_relative "../analysis/runner"
|
|
6
|
+
require_relative "../cache/store"
|
|
7
|
+
require_relative "../configuration"
|
|
8
|
+
require_relative "../effects/discharge"
|
|
9
|
+
require_relative "../effects/path_finder"
|
|
10
|
+
require_relative "../effects/snapshot"
|
|
11
|
+
require_relative "../effects/snapshot_diff"
|
|
12
|
+
require_relative "command"
|
|
13
|
+
require_relative "effects_diff_renderer"
|
|
14
|
+
require_relative "effects_explain_renderer"
|
|
15
|
+
require_relative "options"
|
|
16
|
+
|
|
17
|
+
module Rigor
|
|
18
|
+
class CLI
|
|
19
|
+
# ADR-103 WD7 — the snapshot verbs of `rigor effects`: `update`, `check`, `diff` and `explain`.
|
|
20
|
+
#
|
|
21
|
+
# rigor effects update # write .rigor-effects.yml; commit it
|
|
22
|
+
# rigor effects check # 0 fresh, 1 drift — the CI gate
|
|
23
|
+
# rigor effects diff --baseline PATH # the same comparison, never gating
|
|
24
|
+
# rigor effects explain --symbol KEY # the shortest edge path behind a reach change
|
|
25
|
+
#
|
|
26
|
+
# None of them emits a diagnostic or enters `rigor check`'s stream: drift between two observations by
|
|
27
|
+
# the same tool is 100 % precise, and whether it *matters* is the reviewer's judgment — which is
|
|
28
|
+
# exactly what makes it a review artefact rather than a finding
|
|
29
|
+
# ([ADR-102](../adr/102-unused-code-reachability-report.md)).
|
|
30
|
+
#
|
|
31
|
+
# **The verbs take no paths.** The report does (`rigor effects lib/foo.rb`), because a report is a
|
|
32
|
+
# view; a snapshot is a record of the whole project, and one written over a subset would read as a
|
|
33
|
+
# project where every other method vanished. The analysed set is always the configured `paths:`.
|
|
34
|
+
class EffectsSnapshotCommand < Command
|
|
35
|
+
VERBS = %w[update check diff explain].freeze
|
|
36
|
+
|
|
37
|
+
FORMATS = %w[text json].freeze
|
|
38
|
+
private_constant :FORMATS
|
|
39
|
+
|
|
40
|
+
# The verbs that read a committed record, and so accept `--baseline PATH` —
|
|
41
|
+
# `--baseline <(git show origin/main:.rigor-effects.yml)` is the bot form.
|
|
42
|
+
COMPARING_VERBS = %w[check diff explain].freeze
|
|
43
|
+
private_constant :COMPARING_VERBS
|
|
44
|
+
|
|
45
|
+
def initialize(argv:, verb:, out: $stdout, err: $stderr)
|
|
46
|
+
super(argv: argv, out: out, err: err)
|
|
47
|
+
@verb = verb
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Integer] 0 fresh / written, 1 on drift under `check`, 64 on a usage error.
|
|
51
|
+
def run
|
|
52
|
+
options = parse_options
|
|
53
|
+
return usage_error("unsupported format: #{options.fetch(:format)}") unless
|
|
54
|
+
FORMATS.include?(options[:format])
|
|
55
|
+
return usage_error("rigor effects #{@verb} takes no paths; it records the configured paths:") unless
|
|
56
|
+
@argv.empty?
|
|
57
|
+
|
|
58
|
+
execute(options)
|
|
59
|
+
rescue OptionParser::ParseError => e
|
|
60
|
+
usage_error(e.message)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def execute(options)
|
|
66
|
+
configuration = Configuration.load(options.fetch(:config)).with_effects_enabled
|
|
67
|
+
current = build_snapshot(configuration, full: options.fetch(:full))
|
|
68
|
+
return write(configuration, current) if @verb == "update"
|
|
69
|
+
|
|
70
|
+
compare(configuration, current, options)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ---- update -------------------------------------------------
|
|
74
|
+
|
|
75
|
+
# Always writes — there is no generate / regenerate split, because a snapshot has no "already
|
|
76
|
+
# exists" hazard: it records observations, so overwriting it is the whole point of running it.
|
|
77
|
+
def write(configuration, snapshot)
|
|
78
|
+
path = configuration.effects_snapshot_path
|
|
79
|
+
File.write(path, snapshot.to_yaml)
|
|
80
|
+
@err.puts("rigor: wrote #{path} (#{snapshot.methods.size} method(s), #{snapshot.reach.size} reach entr" \
|
|
81
|
+
"#{snapshot.reach.size == 1 ? 'y' : 'ies'})")
|
|
82
|
+
@err.puts("rigor: note — `effects.snapshot.reach:` is empty, so the snapshot records `methods:` only.") if
|
|
83
|
+
configuration.effects_snapshot_reach.empty?
|
|
84
|
+
0
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# ---- check / diff / explain ---------------------------------
|
|
88
|
+
|
|
89
|
+
def compare(configuration, current, options)
|
|
90
|
+
path = options.fetch(:baseline) || configuration.effects_snapshot_path
|
|
91
|
+
recorded = load_recorded(path)
|
|
92
|
+
return CLI::EXIT_USAGE if recorded == :error
|
|
93
|
+
|
|
94
|
+
discharge = options.fetch(:no_tolerated) ? Effects::Discharge.none : effect_discharge(configuration)
|
|
95
|
+
diff = Effects::SnapshotDiff.compare(
|
|
96
|
+
recorded: recorded, current: current,
|
|
97
|
+
tolerated: options.fetch(:no_tolerated) ? [] : configuration.effects_tolerated,
|
|
98
|
+
gate: configuration.effects_snapshot_gate,
|
|
99
|
+
strict_tolerated: options.fetch(:strict_tolerated),
|
|
100
|
+
undischarged: Effects::Snapshot.undischarged_index(
|
|
101
|
+
snapshot: current, table: @table, discharge: discharge
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
return explain(diff, options) if @verb == "explain"
|
|
105
|
+
|
|
106
|
+
EffectsDiffRenderer.new(out: @out, path: path).render(diff, format: options.fetch(:format))
|
|
107
|
+
@verb == "check" && diff.drift? ? 1 : 0
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def effect_discharge(configuration)
|
|
111
|
+
Effects::Discharge.new(configuration.effects_tolerated)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A missing snapshot is drift with a routed message, not an error: it is the state every project is
|
|
115
|
+
# in before its first `update`, and the fix is one command away.
|
|
116
|
+
def load_recorded(path)
|
|
117
|
+
return nil unless File.exist?(path)
|
|
118
|
+
|
|
119
|
+
Effects::Snapshot.load(path)
|
|
120
|
+
rescue Effects::Snapshot::ParseError => e
|
|
121
|
+
@err.puts("rigor: effect snapshot load failed: #{path}: #{e.message}")
|
|
122
|
+
:error
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# ---- explain ------------------------------------------------
|
|
126
|
+
|
|
127
|
+
def explain(diff, options)
|
|
128
|
+
rows = options.fetch(:symbol) ? rows_for_symbol(options.fetch(:symbol)) : rows_for_changes(diff)
|
|
129
|
+
EffectsExplainRenderer.new(out: @out).render(rows, format: options.fetch(:format))
|
|
130
|
+
0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def rows_for_symbol(symbol)
|
|
134
|
+
entry = @table[symbol]
|
|
135
|
+
if entry.nil?
|
|
136
|
+
@err.puts("rigor: no effect unit named #{symbol}")
|
|
137
|
+
return []
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
reach_rows(symbol, entry.proven.to_a) + method_rows(symbol, entry.direct.proven.to_a)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Every label a change introduced, explained once: a `reach:` change gets its shortest edge path, a
|
|
144
|
+
# `methods:` change gets the origin in the method's own body.
|
|
145
|
+
def rows_for_changes(diff)
|
|
146
|
+
diff.events.flat_map do |event|
|
|
147
|
+
labels = labels_of(event)
|
|
148
|
+
next [] if labels.empty?
|
|
149
|
+
|
|
150
|
+
event.table == "reach" ? reach_rows(event.symbol, labels) : method_rows(event.symbol, labels)
|
|
151
|
+
end.uniq
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def labels_of(event)
|
|
155
|
+
return [event.label] if event.label
|
|
156
|
+
return [] unless event.category == Effects::SnapshotDiff::SYMBOL_ADDED
|
|
157
|
+
|
|
158
|
+
entry = @table[event.symbol]
|
|
159
|
+
entry ? entry.proven.to_a : []
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def reach_rows(symbol, labels)
|
|
163
|
+
labels.filter_map do |label|
|
|
164
|
+
path = Effects::PathFinder.shortest(@table, symbol: symbol, label: label)
|
|
165
|
+
next if path.nil?
|
|
166
|
+
|
|
167
|
+
EffectsExplainRenderer::Row.new(table: "reach", symbol: symbol, label: label,
|
|
168
|
+
path: path.to_a, origin: path.origin)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def method_rows(symbol, labels)
|
|
173
|
+
entry = @table[symbol]
|
|
174
|
+
return [] if entry.nil?
|
|
175
|
+
|
|
176
|
+
labels.filter_map do |label|
|
|
177
|
+
origin = entry.direct.bundles.find { |_origin, set| set.include?(label) }&.first
|
|
178
|
+
next if origin.nil?
|
|
179
|
+
|
|
180
|
+
EffectsExplainRenderer::Row.new(table: "methods", symbol: symbol, label: label,
|
|
181
|
+
path: [].freeze, origin: origin.to_s)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# ---- the run ------------------------------------------------
|
|
186
|
+
|
|
187
|
+
# Same run the report takes: collection forced on for this invocation, sequential, and going through
|
|
188
|
+
# the ADR-45 whole-run result cache like `rigor check` — the diagnostics entry plus the #382 effects
|
|
189
|
+
# sidecar keyed beside it, so `rigor effects check` after `rigor check` under a configured `effects:`
|
|
190
|
+
# block is a warm hit plus the fixpoint.
|
|
191
|
+
def build_snapshot(configuration, full:)
|
|
192
|
+
runner = Analysis::Runner.new(
|
|
193
|
+
configuration: configuration,
|
|
194
|
+
cache_store: Cache::Store.new(root: configuration.cache_path),
|
|
195
|
+
collect_stats: false,
|
|
196
|
+
workers: 0
|
|
197
|
+
)
|
|
198
|
+
runner.run(configuration.paths)
|
|
199
|
+
@table = runner.effect_table
|
|
200
|
+
Effects::Snapshot.build(
|
|
201
|
+
table: @table, configuration: configuration, sources: runner.effect_sources, full: full,
|
|
202
|
+
registry: Effects::Registry.for_configuration(configuration,
|
|
203
|
+
plugin_facts: runner.effect_plugin_facts)
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# ---- options ------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def parse_options
|
|
210
|
+
options = { config: nil, format: "text", full: false, baseline: nil, symbol: nil,
|
|
211
|
+
strict_tolerated: false, no_tolerated: false }
|
|
212
|
+
OptionParser.new do |opts|
|
|
213
|
+
opts.banner = "Usage: rigor effects #{@verb} [options]"
|
|
214
|
+
Options.add_config(opts, options)
|
|
215
|
+
add_common_options(opts, options)
|
|
216
|
+
add_comparison_options(opts, options) if COMPARING_VERBS.include?(@verb)
|
|
217
|
+
if @verb == "explain"
|
|
218
|
+
opts.on("--symbol=KEY", "Explain this unit instead of the changed ones") do |value|
|
|
219
|
+
options[:symbol] = value
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end.parse!(@argv)
|
|
223
|
+
options
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# The policy switches are accepted by every verb, `update` included, and there they are deliberately
|
|
227
|
+
# inert: the record is **undischarged**, so `update --no-tolerated-effects` and `update` write
|
|
228
|
+
# byte-identical files. A flag that changed the record would make the file a function of policy and
|
|
229
|
+
# a policy change indistinguishable from a code change.
|
|
230
|
+
def add_common_options(opts, options)
|
|
231
|
+
opts.on("--format=FORMAT", "Output format: text (default) or json") { |value| options[:format] = value }
|
|
232
|
+
opts.on("--full", "Record every method, including the omitted trivial and synthesised ones") do
|
|
233
|
+
options[:full] = true
|
|
234
|
+
end
|
|
235
|
+
opts.on("--strict-tolerated", "Fail the gate on tolerated changes too (check / diff)") do
|
|
236
|
+
options[:strict_tolerated] = true
|
|
237
|
+
end
|
|
238
|
+
opts.on("--no-tolerated-effects", "Judge as if effects.tolerated: were empty (check / diff)") do
|
|
239
|
+
options[:no_tolerated] = true
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def add_comparison_options(opts, options)
|
|
244
|
+
opts.on("--baseline=PATH", "Compare against PATH instead of effects.snapshot.path") do |value|
|
|
245
|
+
options[:baseline] = value
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def usage_error(message)
|
|
250
|
+
@err.puts(message)
|
|
251
|
+
@err.puts("Usage: rigor effects #{@verb} [options]")
|
|
252
|
+
CLI::EXIT_USAGE
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optionparser"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "../configuration"
|
|
7
|
+
require_relative "../analysis/path_expansion"
|
|
8
|
+
require_relative "../analysis/reachability/scan"
|
|
9
|
+
require_relative "../analysis/reachability/graph"
|
|
10
|
+
require_relative "../analysis/reachability/plugin_roots"
|
|
11
|
+
require_relative "../analysis/reachability/signature_scan"
|
|
12
|
+
require_relative "../analysis/reachability/project_files"
|
|
13
|
+
require_relative "options"
|
|
14
|
+
require_relative "command"
|
|
15
|
+
require_relative "probe_environment"
|
|
16
|
+
|
|
17
|
+
module Rigor
|
|
18
|
+
class CLI
|
|
19
|
+
# ADR-102 — executes `rigor unused`.
|
|
20
|
+
#
|
|
21
|
+
# Reports project constants that nothing reachable references. It is a REPORT, never a diagnostic: the
|
|
22
|
+
# measured precision of this signal is 7.0% on an adjudicated corpus target
|
|
23
|
+
# (`docs/notes/20260813-unused-constant-fp-baseline.md`), so its output is a review queue, not a defect
|
|
24
|
+
# list, and it never enters `rigor check`'s stream at any severity (WD1). Exits 0 whatever it finds.
|
|
25
|
+
class UnusedCommand < Command
|
|
26
|
+
USAGE = "Usage: rigor unused [options] [paths]"
|
|
27
|
+
|
|
28
|
+
# WD7 — the REFERENCE corpus is wider than the ANALYSIS corpus. `.rake` files sit inside `paths:` and
|
|
29
|
+
# reference project constants, but `PathExpansion::RUBY_GLOB` never reads them, which made them pure
|
|
30
|
+
# artifacts on two of three corpus targets. Harvesting references from a file is far cheaper than
|
|
31
|
+
# type-checking it, so the report takes the wider set.
|
|
32
|
+
REFERENCE_GLOB = "**/*.{rb,rake}"
|
|
33
|
+
|
|
34
|
+
# Templates and data files that carry class names as strings. A name here is NOT counted as a reference —
|
|
35
|
+
# a YAML value is far weaker evidence than a constant node, and treating it as proof would silently hide
|
|
36
|
+
# real dead code. It demotes the declaration to `cannot-decide` instead, with the file named, so the
|
|
37
|
+
# reader judges it (ADR-102 WD4).
|
|
38
|
+
TEMPLATE_GLOB = "**/*.{erb,haml,slim,yml,yaml,json}"
|
|
39
|
+
|
|
40
|
+
def run
|
|
41
|
+
options = parse_options
|
|
42
|
+
return CLI::EXIT_USAGE if options == :usage_error
|
|
43
|
+
|
|
44
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
45
|
+
paths = @argv.empty? ? configuration.paths : @argv
|
|
46
|
+
declarations, references, dynamic_uses = scan(paths, configuration)
|
|
47
|
+
references.concat(signature_references(configuration))
|
|
48
|
+
dynamic_uses.concat(template_mentions(declarations))
|
|
49
|
+
|
|
50
|
+
contribution = Analysis::Reachability::PluginRoots.collect(configuration: configuration)
|
|
51
|
+
references.concat(plugin_references(contribution.references))
|
|
52
|
+
graph = Analysis::Reachability::Graph.new(
|
|
53
|
+
declarations: declarations, references: references, dynamic_uses: dynamic_uses,
|
|
54
|
+
root_fqns: root_fqns(declarations, options.fetch(:entry_points)) + contribution.roots,
|
|
55
|
+
foreign: foreign_predicate(configuration)
|
|
56
|
+
)
|
|
57
|
+
emit(graph.report, options, supply: root_supply(contribution.roots, declarations))
|
|
58
|
+
0
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def parse_options
|
|
64
|
+
options = { config: nil, format: "text", entry_points: [], limit: nil }
|
|
65
|
+
parser = OptionParser.new do |opts|
|
|
66
|
+
opts.banner = USAGE
|
|
67
|
+
Options.add_config(opts, options)
|
|
68
|
+
opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
|
|
69
|
+
opts.on("--entry-point=GLOB", "Treat declarations in files matching GLOB as roots (repeatable)") do |v|
|
|
70
|
+
options[:entry_points] << v
|
|
71
|
+
end
|
|
72
|
+
opts.on("--limit=N", Integer, "Print at most N candidates (default: all)") { |v| options[:limit] = v }
|
|
73
|
+
# WD5 — refuse rather than silently absorb. A reachability answer is sound only over a full run, and a
|
|
74
|
+
# user who asked for the fast path must not silently get the slow one.
|
|
75
|
+
opts.on("--incremental", "(unsupported)") { options[:incremental] = true }
|
|
76
|
+
end
|
|
77
|
+
parser.parse!(@argv)
|
|
78
|
+
return usage_error(parser) if options[:incremental]
|
|
79
|
+
|
|
80
|
+
options
|
|
81
|
+
rescue OptionParser::ParseError => e
|
|
82
|
+
@err.puts(e.message)
|
|
83
|
+
:usage_error
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def usage_error(_parser)
|
|
87
|
+
@err.puts("rigor unused does not support --incremental: reachability is only sound over a whole-project " \
|
|
88
|
+
"run, so an incremental pass would report constants as unused merely because the files that " \
|
|
89
|
+
"reference them were served from cache. Re-run without --incremental.")
|
|
90
|
+
:usage_error
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Declarations come from the analysed paths; references additionally from the wider corpus (WD7).
|
|
94
|
+
def scan(paths, configuration)
|
|
95
|
+
declaration_files = Analysis::PathExpansion.ruby_files(paths, configuration.exclude_patterns).to_set
|
|
96
|
+
declarations = []
|
|
97
|
+
references = []
|
|
98
|
+
dynamic_uses = []
|
|
99
|
+
(declaration_files + reference_files(paths, configuration)).sort.each do |file|
|
|
100
|
+
result = read_and_scan(file, configuration)
|
|
101
|
+
next if result.nil?
|
|
102
|
+
|
|
103
|
+
declarations.concat(result.declarations) if declaration_files.include?(file)
|
|
104
|
+
references.concat(result.references)
|
|
105
|
+
dynamic_uses.concat(result.dynamic_uses)
|
|
106
|
+
end
|
|
107
|
+
[declarations, references, dynamic_uses]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The reference corpus is the PROJECT, not the analysed paths. A constant declared in `lib/` is commonly
|
|
111
|
+
# referenced from `config/initializers`, a `.rake` task, or a spec — none of which are in `paths:` — and
|
|
112
|
+
# treating those as absent is what manufactured artifacts on two of three corpus targets in #345.
|
|
113
|
+
# Widening the *declaration* set the same way would cancel the gain (measured: +1 / −3 / +2 candidates,
|
|
114
|
+
# ADR-102 WD2), which is exactly why the two corpora are separated rather than both widened.
|
|
115
|
+
def reference_files(_paths, configuration)
|
|
116
|
+
relative = Analysis::Reachability::ProjectFiles.own(Dir.glob(REFERENCE_GLOB, base: Dir.pwd), Dir.pwd)
|
|
117
|
+
absolute = relative.map { |rel| File.expand_path(rel) }
|
|
118
|
+
Analysis::PathExpansion.reject_excluded(absolute, configuration.exclude_patterns).to_set
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def read_and_scan(file, configuration)
|
|
122
|
+
Analysis::Reachability::Scan.call(path: file, source: File.read(file),
|
|
123
|
+
target_ruby: configuration.target_ruby)
|
|
124
|
+
rescue SystemCallError
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# A glob is matched against the declaration's path AND its path relative to the working directory:
|
|
129
|
+
# `configuration.paths` are expanded to absolute, so a user-written `--entry-point=lib/entry.rb` would
|
|
130
|
+
# otherwise never match anything and silently produce a root set of zero.
|
|
131
|
+
# A constant named only from the project's own `sig/` is referenced — the #345 probe reported exactly this
|
|
132
|
+
# as a false candidate until an RBS-side hook was added, because `Reflection.resolve_constant_type` is
|
|
133
|
+
# source-side only. A signature also DECLARES, though, and {Analysis::Reachability::SignatureScan} is
|
|
134
|
+
# what keeps the two apart (issue #363).
|
|
135
|
+
def signature_references(configuration)
|
|
136
|
+
Array(configuration.signature_paths).flat_map do |dir|
|
|
137
|
+
next [] unless File.directory?(dir)
|
|
138
|
+
|
|
139
|
+
Dir.glob(File.join(dir, "**/*.rbs")).flat_map { |file| Analysis::Reachability::SignatureScan.call(file) }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# #350 — a plugin's `:reachability_references` fact enters the graph exactly where a scanned file-level
|
|
144
|
+
# reference does: `from: nil` (so it seeds) carrying the role the plugin claimed. That is what keeps a
|
|
145
|
+
# class named only by a FactoryBot `class:` string in the `reachable only from tests` section instead of
|
|
146
|
+
# promoting it to production-reachable, which rooting it would have done silently.
|
|
147
|
+
#
|
|
148
|
+
# `path` is a synthetic marker rather than a file: the graph never reads a reference's path (rows are
|
|
149
|
+
# located from the DECLARATION site), and inventing a plausible-looking file path for a fact would be a
|
|
150
|
+
# worse lie than an obviously synthetic one.
|
|
151
|
+
def plugin_references(rows)
|
|
152
|
+
rows.map do |reference|
|
|
153
|
+
Analysis::Reachability::Scan::Reference.new(as_written: reference.name, nesting: [].freeze, from: nil,
|
|
154
|
+
role: reference.role, path: "(plugin)", line: 1)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def template_mentions(declarations)
|
|
159
|
+
names = declarations.map(&:fqn)
|
|
160
|
+
return [] if names.empty?
|
|
161
|
+
|
|
162
|
+
Analysis::Reachability::ProjectFiles.own(Dir.glob(TEMPLATE_GLOB, base: Dir.pwd), Dir.pwd).flat_map do |rel|
|
|
163
|
+
text = File.read(File.expand_path(rel)).scrub
|
|
164
|
+
names.filter_map do |fqn|
|
|
165
|
+
next unless text.include?(fqn)
|
|
166
|
+
|
|
167
|
+
Analysis::Reachability::Scan::DynamicUse.new(name: nil, prefix: fqn, site: nil,
|
|
168
|
+
reason: "named as a string in #{rel}",
|
|
169
|
+
path: rel, line: 1)
|
|
170
|
+
end
|
|
171
|
+
rescue SystemCallError, ArgumentError
|
|
172
|
+
[]
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def root_fqns(declarations, globs)
|
|
177
|
+
return [] if globs.empty?
|
|
178
|
+
|
|
179
|
+
matcher = Analysis::Reachability::ProjectFiles.method(:entry_point_match?)
|
|
180
|
+
cwd = "#{File.expand_path(Dir.pwd)}/"
|
|
181
|
+
declarations.filter_map do |d|
|
|
182
|
+
relative = d.path.delete_prefix(cwd)
|
|
183
|
+
d.fqn if globs.any? { |g| matcher.call(g, d.path) || matcher.call(g, relative) }
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# WD6 — ownership means "declared FIRST here". Reopening a gem or stdlib class registers it as a project
|
|
188
|
+
# declaration, which produced three of redmine's artifacts from a single initializer. A name the bundled
|
|
189
|
+
# (non-project) environment already knows is not ours to call unused. The project's own `sig/` is
|
|
190
|
+
# deliberately excluded from this environment, so a project class that ships a signature stays owned.
|
|
191
|
+
def foreign_predicate(configuration)
|
|
192
|
+
env = Environment.for_project(libraries: configuration.libraries, signature_paths: [])
|
|
193
|
+
->(fqn) { !env.singleton_for_name(fqn).nil? }
|
|
194
|
+
rescue StandardError
|
|
195
|
+
->(_fqn) { false }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# ADR-102 § Consequences — "a root source that OVER-supplies silently hides real dead code, which is
|
|
199
|
+
# worse than one that under-supplies, so each plugin's contribution needs its own corpus check". A
|
|
200
|
+
# supplied root naming a constant the project does not declare is inert in the graph, but it is the
|
|
201
|
+
# observable symptom of a root source drifting away from the code — a renamed controller, an
|
|
202
|
+
# inflection the plugin gets wrong, a convention that stopped holding. Reporting the count makes that
|
|
203
|
+
# drift measurable on a real project instead of invisible.
|
|
204
|
+
Supply = Data.define(:supplied, :unmatched)
|
|
205
|
+
private_constant :Supply
|
|
206
|
+
|
|
207
|
+
def root_supply(plugin_roots, declarations)
|
|
208
|
+
declared = declarations.to_set(&:fqn)
|
|
209
|
+
Supply.new(supplied: plugin_roots.size, unmatched: plugin_roots.count { |fqn| !declared.include?(fqn) })
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def emit(report, options, supply:)
|
|
213
|
+
@out.puts(options.fetch(:format) == "json" ? json(report, options, supply) : text(report, options, supply))
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def json(report, options, supply)
|
|
217
|
+
JSON.pretty_generate(
|
|
218
|
+
declared: report.declared, reachable: report.reachable, roots: report.roots, edges: report.edges,
|
|
219
|
+
namespaces: report.namespaces,
|
|
220
|
+
plugin_roots: supply.supplied, plugin_roots_unmatched: supply.unmatched,
|
|
221
|
+
candidates: rows_json(report.candidates, options), test_only: rows_json(report.test_only, options),
|
|
222
|
+
undecidable: limited(report.undecidable, options).map do |u|
|
|
223
|
+
{ name: u.fqn, path: u.path, line: u.line, reason: u.reason }
|
|
224
|
+
end
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def rows_json(rows, options)
|
|
229
|
+
limited(rows, options).map { |c| { name: c.fqn, path: c.path, line: c.line } }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def text(report, options, supply)
|
|
233
|
+
lines = ["Reachability", " declared (project-owned): #{report.declared}",
|
|
234
|
+
" roots: #{report.roots}#{plugin_root_note(supply)}",
|
|
235
|
+
" reachable: #{report.reachable}",
|
|
236
|
+
" candidates: #{report.candidates.size}",
|
|
237
|
+
" reachable only from tests: #{report.test_only.size}",
|
|
238
|
+
" cannot decide: #{report.undecidable.size}",
|
|
239
|
+
" namespace-only (excluded): #{report.namespaces}"]
|
|
240
|
+
lines.concat(section("Candidates — nothing reachable references these", report.candidates, options))
|
|
241
|
+
lines.concat(section("Reachable only from test code — live test, dead production path",
|
|
242
|
+
report.test_only, options))
|
|
243
|
+
lines.concat(undecidable_section(report.undecidable, options))
|
|
244
|
+
lines << ""
|
|
245
|
+
lines << "These are CANDIDATES, not findings. The measured precision of this report on an adjudicated"
|
|
246
|
+
lines << "corpus target is 7% — every row needs a human to confirm it. Roots a framework supplies come"
|
|
247
|
+
lines << "from the project's plugins, so a framework Rigor has no plugin for still reads as an upper bound."
|
|
248
|
+
lines.join("\n")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# `(N from plugins, M matched nothing)` — omitted entirely when no plugin contributed, so a
|
|
252
|
+
# non-Rails project's report is unchanged. A non-zero `matched nothing` is the over-supply signal
|
|
253
|
+
# described on {#root_supply}: those roots claim constants this project does not declare.
|
|
254
|
+
def plugin_root_note(supply)
|
|
255
|
+
return "" if supply.supplied.zero?
|
|
256
|
+
|
|
257
|
+
" (#{supply.supplied} from plugins, #{supply.unmatched} matched no declaration)"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def undecidable_section(rows, options)
|
|
261
|
+
return [] if rows.empty?
|
|
262
|
+
|
|
263
|
+
out = ["", "Cannot decide — something can name these at runtime (#{rows.size})"]
|
|
264
|
+
limited(rows, options).each_with_index do |u, i|
|
|
265
|
+
out << format(" %<n>3d %<name>-52s %<path>s:%<line>d", n: i + 1, name: u.fqn, path: u.path,
|
|
266
|
+
line: u.line)
|
|
267
|
+
out << " #{u.reason}"
|
|
268
|
+
end
|
|
269
|
+
out
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def section(title, rows, options)
|
|
273
|
+
return [] if rows.empty?
|
|
274
|
+
|
|
275
|
+
out = ["", "#{title} (#{rows.size})"]
|
|
276
|
+
limited(rows, options).each_with_index do |c, i|
|
|
277
|
+
out << format(" %<n>3d %<name>-52s %<path>s:%<line>d", n: i + 1, name: c.fqn, path: c.path, line: c.line)
|
|
278
|
+
end
|
|
279
|
+
out
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def limited(candidates, options)
|
|
283
|
+
limit = options.fetch(:limit)
|
|
284
|
+
limit ? candidates.first(limit) : candidates
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -37,6 +37,7 @@ module Rigor
|
|
|
37
37
|
"type-of" => :run_type_of,
|
|
38
38
|
"trace" => :run_trace,
|
|
39
39
|
"type-scan" => :run_type_scan,
|
|
40
|
+
"effects" => :run_effects,
|
|
40
41
|
"explain" => :run_explain,
|
|
41
42
|
"diff" => :run_diff,
|
|
42
43
|
"sig-gen" => :run_sig_gen,
|
|
@@ -44,6 +45,7 @@ module Rigor
|
|
|
44
45
|
"mcp" => :run_mcp,
|
|
45
46
|
"baseline" => :run_baseline,
|
|
46
47
|
"triage" => :run_triage,
|
|
48
|
+
"unused" => :run_unused,
|
|
47
49
|
"coverage" => :run_coverage,
|
|
48
50
|
"plugins" => :run_plugins,
|
|
49
51
|
"plugin" => :run_plugin,
|
|
@@ -220,6 +222,12 @@ module Rigor
|
|
|
220
222
|
TypeScanCommand.new(argv: @argv, out: @out, err: @err).run
|
|
221
223
|
end
|
|
222
224
|
|
|
225
|
+
def run_effects
|
|
226
|
+
require_relative "cli/effects_command"
|
|
227
|
+
|
|
228
|
+
EffectsCommand.new(argv: @argv, out: @out, err: @err).run
|
|
229
|
+
end
|
|
230
|
+
|
|
223
231
|
def run_explain
|
|
224
232
|
require_relative "cli/explain_command"
|
|
225
233
|
|
|
@@ -262,6 +270,12 @@ module Rigor
|
|
|
262
270
|
CLI::TriageCommand.new(argv: @argv, out: @out, err: @err).run
|
|
263
271
|
end
|
|
264
272
|
|
|
273
|
+
def run_unused
|
|
274
|
+
require_relative "cli/unused_command"
|
|
275
|
+
|
|
276
|
+
CLI::UnusedCommand.new(argv: @argv, out: @out, err: @err).run
|
|
277
|
+
end
|
|
278
|
+
|
|
265
279
|
def run_coverage
|
|
266
280
|
require_relative "cli/coverage_command"
|
|
267
281
|
|
|
@@ -341,6 +355,8 @@ module Rigor
|
|
|
341
355
|
type-of Print the inferred type at FILE:LINE:COL
|
|
342
356
|
trace Replay how the engine typed FILE as a terminal animation
|
|
343
357
|
type-scan Report Scope#type_of coverage across PATHs
|
|
358
|
+
effects Report each method's effect labels, and the committed effect snapshot
|
|
359
|
+
(ADR-103, opt-in; effects update/check/diff/explain)
|
|
344
360
|
explain Print the description of one or all CheckRules
|
|
345
361
|
diff Compare current diagnostics to a saved baseline JSON
|
|
346
362
|
sig-gen Emit RBS skeletons inferred from .rb sources (ADR-14)
|
|
@@ -63,6 +63,19 @@ module Rigor
|
|
|
63
63
|
# ADR-100 — a new required diagnostic (ADR-50 WD1), so it is `:off` in every shipped profile and
|
|
64
64
|
# reaches a user only through the `use-of-void-value` bleeding-edge feature, which overrides this
|
|
65
65
|
# to `:warning`.
|
|
66
|
+
# ADR-103 WD8 / #383 — opt-in twice over (the `effects:` block, then the author's own
|
|
67
|
+
# envelope directive), so it is never unsolicited noise and needs no bleeding-edge gate:
|
|
68
|
+
# `:warning` even under lenient, `:error` under strict.
|
|
69
|
+
"effect.envelope-exceeded" => :warning,
|
|
70
|
+
# ADR-103 WD14 / #386 — the inherited-bound reading rides its sibling's severities exactly:
|
|
71
|
+
# both-sides-authored (someone wrote the ancestor's envelope), and as strict as proven.
|
|
72
|
+
"effect.liskov-widened" => :warning,
|
|
73
|
+
# ADR-103 WD14 — `unknown-label` info / info / warning. It reports that a bound stopped
|
|
74
|
+
# bounding, never that code is wrong, so even `strict` stops at `:warning`.
|
|
75
|
+
"effect.unknown-label" => :info,
|
|
76
|
+
# The residual is advisory in every profile: it says a declaration is inert, and the fix is
|
|
77
|
+
# a config edit the author may deliberately not want.
|
|
78
|
+
"effect.annotations-unchecked" => :info,
|
|
66
79
|
"static.value-use.void" => :off,
|
|
67
80
|
# Opt-in author assertion: you only see it if you wrote a
|
|
68
81
|
# `conforms-to` directive, so it stays a :warning even in
|
|
@@ -101,6 +114,10 @@ module Rigor
|
|
|
101
114
|
"suppression.empty" => :warning,
|
|
102
115
|
"suppression.unknown-marker" => :warning,
|
|
103
116
|
"static.value-use.void" => :off,
|
|
117
|
+
"effect.envelope-exceeded" => :warning,
|
|
118
|
+
"effect.liskov-widened" => :warning,
|
|
119
|
+
"effect.unknown-label" => :info,
|
|
120
|
+
"effect.annotations-unchecked" => :info,
|
|
104
121
|
"rbs_extended.unsatisfied-conformance" => :warning
|
|
105
122
|
}.freeze,
|
|
106
123
|
strict: {
|
|
@@ -135,6 +152,12 @@ module Rigor
|
|
|
135
152
|
"suppression.unknown-marker" => :warning,
|
|
136
153
|
# `:off` even under strict: the gate is `bleeding_edge:`, not the profile (ADR-50 WD1 / ADR-100).
|
|
137
154
|
"static.value-use.void" => :off,
|
|
155
|
+
"effect.envelope-exceeded" => :error,
|
|
156
|
+
"effect.liskov-widened" => :error,
|
|
157
|
+
"effect.unknown-label" => :warning,
|
|
158
|
+
# `:info` even under strict: a residual that failed a build would punish the project for
|
|
159
|
+
# carrying an annotation it has not opted into checking, which is the opposite of the point.
|
|
160
|
+
"effect.annotations-unchecked" => :info,
|
|
138
161
|
"rbs_extended.unsatisfied-conformance" => :error
|
|
139
162
|
}.freeze
|
|
140
163
|
}.freeze
|