rigortype 0.3.3 → 0.3.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 +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 +860 -0
- data/data/effects/registry.yml +95 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +359 -2
- data/docs/manual/03-configuration.md +101 -1
- data/docs/manual/04-diagnostics.md +8 -3
- data/docs/manual/11-ci.md +37 -0
- data/docs/manual/12-caching.md +39 -0
- data/docs/manual/16-rbs-extended-annotations.md +90 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/19-effect-labels.md +671 -0
- data/docs/manual/README.md +10 -0
- data/docs/manual/ci-templates/README.md +9 -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/rule_ids.rb +28 -2
- 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 +168 -3
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/run_cache_probe.rb +69 -1
- data/lib/rigor/analysis/runner/declaration_position.rb +38 -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 +93 -10
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
- data/lib/rigor/analysis/runner.rb +429 -10
- data/lib/rigor/analysis/worker_session.rb +60 -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 +245 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +127 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +92 -0
- data/lib/rigor/cli/effects_report.rb +163 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +275 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +28 -1
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +322 -28
- data/lib/rigor/configuration_error.rb +20 -0
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +219 -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 +139 -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 +174 -0
- data/lib/rigor/effects/framework_units.rb +222 -0
- data/lib/rigor/effects/identity.rb +104 -0
- data/lib/rigor/effects/inline_anchor.rb +134 -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 +384 -0
- data/lib/rigor/effects/propagator.rb +335 -0
- data/lib/rigor/effects/registry.rb +200 -0
- data/lib/rigor/effects/scanner.rb +302 -0
- data/lib/rigor/effects/signature_sources.rb +90 -0
- data/lib/rigor/effects/snapshot.rb +396 -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 +39 -0
- data/lib/rigor/effects/unit_scan.rb +647 -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/expression_typer.rb +18 -4
- data/lib/rigor/inference/pre_eval_constants.rb +189 -0
- data/lib/rigor/inference/scope_indexer.rb +15 -3
- data/lib/rigor/plugin/base.rb +28 -0
- data/lib/rigor/plugin/box.rb +18 -2
- data/lib/rigor/plugin/effect_ancestry.rb +80 -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 +127 -3
- data/lib/rigor/plugin/registry.rb +55 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +188 -0
- data/lib/rigor/reflection.rb +131 -30
- 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 +401 -29
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -0
- 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-rbs-inline/lib/rigor/plugin/rbs_inline.rb +56 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +75 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +68 -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.rbs +17 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +84 -1
|
@@ -10,6 +10,9 @@ require_relative "../rbs_extended/reporter"
|
|
|
10
10
|
require_relative "../reflection"
|
|
11
11
|
require_relative "../type/combinator"
|
|
12
12
|
require_relative "../inference/coverage_scanner"
|
|
13
|
+
require_relative "../effects/attribution"
|
|
14
|
+
require_relative "../effects/collector"
|
|
15
|
+
require_relative "../effects/envelope_index"
|
|
13
16
|
require_relative "../inference/scope_indexer"
|
|
14
17
|
require_relative "../inference/method_dispatcher/file_folding"
|
|
15
18
|
require_relative "check_rules"
|
|
@@ -93,6 +96,14 @@ module Rigor
|
|
|
93
96
|
# normal `--workers N` run leaves it false and pays nothing (the recorder's disabled fast path).
|
|
94
97
|
@record_dependencies = record_dependencies
|
|
95
98
|
@file_dependencies = {}
|
|
99
|
+
# ADR-103 WD13 — effect collection, derived from the configuration exactly as `Runner` derives it,
|
|
100
|
+
# so a worker and the parent agree without a flag to keep in sync. Records marshal back through
|
|
101
|
+
# {#drain_effects}.
|
|
102
|
+
@record_effects = configuration.effects_enabled?
|
|
103
|
+
# ADR-103 WD6 / #385 — same table, derived the same way, for the same reason as `@record_effects`
|
|
104
|
+
# above: a worker and the parent agree without a flag to keep in sync.
|
|
105
|
+
@effect_attribution = Effects::Attribution.build(configuration.effects_attribution)
|
|
106
|
+
@file_effects = {}
|
|
96
107
|
# ADR-32 WD4 — full project file list (frozen Array<String>) for env-build-time invocation of any
|
|
97
108
|
# loaded plugin's `source_rbs_synthesizer` callable.
|
|
98
109
|
@source_files = source_files
|
|
@@ -144,14 +155,52 @@ module Rigor
|
|
|
144
155
|
# worker captures the file's cross-file reads for {#drain_dependencies}; the recorder's own disabled
|
|
145
156
|
# fast path makes the unwrapped case free.
|
|
146
157
|
def analyze(path)
|
|
147
|
-
return
|
|
158
|
+
return analyze_with_effects(path) unless @record_dependencies
|
|
148
159
|
|
|
149
160
|
diagnostics = nil
|
|
150
|
-
record = DependencyRecorder.record_for(path) { diagnostics =
|
|
161
|
+
record = DependencyRecorder.record_for(path) { diagnostics = analyze_with_effects(path) }
|
|
151
162
|
@file_dependencies[path] = record
|
|
152
163
|
diagnostics
|
|
153
164
|
end
|
|
154
165
|
|
|
166
|
+
# ADR-103 WD13 — the worker-side mirror of `Runner#analyze_with_effects`.
|
|
167
|
+
def analyze_with_effects(path)
|
|
168
|
+
return analyze_body(path) unless @record_effects
|
|
169
|
+
|
|
170
|
+
diagnostics = nil
|
|
171
|
+
collection = Effects::Collector.collect_for(
|
|
172
|
+
path, attribution: @effect_attribution, envelopes: effect_envelope_index,
|
|
173
|
+
plugin_facts: effect_plugin_facts
|
|
174
|
+
) do
|
|
175
|
+
diagnostics = analyze_body(path)
|
|
176
|
+
end
|
|
177
|
+
@file_effects[path] = collection
|
|
178
|
+
diagnostics
|
|
179
|
+
end
|
|
180
|
+
private :analyze_with_effects
|
|
181
|
+
|
|
182
|
+
# ADR-103 WD6 / #386 — the worker-side mirror of `Runner#effect_envelope_index`. Derived from the
|
|
183
|
+
# same configuration and the session's own environment, so a worker's `≤` lane is the one the
|
|
184
|
+
# parent would have computed for the same file.
|
|
185
|
+
# ADR-103 WD10 / #387 — the worker-side mirror of `Runner#effect_plugin_facts`. The superclass table
|
|
186
|
+
# arrives with the coordinator's scope seed, so a worker resolves an `ActiveRecord::Base` row through
|
|
187
|
+
# the same project ancestry the parent would have walked.
|
|
188
|
+
def effect_plugin_facts
|
|
189
|
+
@effect_plugin_facts ||= Effects::PluginFacts.build(
|
|
190
|
+
@plugin_registry,
|
|
191
|
+
superclasses: @project_scope_seed[:discovered_superclasses] || {},
|
|
192
|
+
includes: @project_scope_seed[:discovered_includes] || {}
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
private :effect_plugin_facts
|
|
196
|
+
|
|
197
|
+
def effect_envelope_index
|
|
198
|
+
@effect_envelope_index ||= Effects::EnvelopeIndex.build(
|
|
199
|
+
configuration: @configuration, environment: @environment, plugin_facts: effect_plugin_facts
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
private :effect_envelope_index
|
|
203
|
+
|
|
155
204
|
def analyze_body(path)
|
|
156
205
|
parse_result = parse_source(path)
|
|
157
206
|
unless parse_result.errors.empty?
|
|
@@ -160,6 +209,7 @@ module Rigor
|
|
|
160
209
|
return parse_diagnostics(path, parse_result)
|
|
161
210
|
end
|
|
162
211
|
|
|
212
|
+
Effects::Collector.record_root(parse_result.value)
|
|
163
213
|
scope = seed_project_scope(Scope.empty(environment: @environment, source_path: path))
|
|
164
214
|
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
165
215
|
# ADR-53 B4 — built-in collectors + plugin node rules share one walk.
|
|
@@ -204,6 +254,14 @@ module Rigor
|
|
|
204
254
|
@file_dependencies
|
|
205
255
|
end
|
|
206
256
|
|
|
257
|
+
# ADR-103 WD12 — the per-file {Effects::FileCollection}s this session collected (empty unless the
|
|
258
|
+
# configuration carries an `effects:` block). Marshal-clean by construction — frozen Hashes of
|
|
259
|
+
# Strings, `LabelSet`s of frozen Arrays, and `Data` edges — so the fork pool ships them back to the
|
|
260
|
+
# coordinator, which folds them into the runner's collection exactly as the sequential path would.
|
|
261
|
+
def drain_effects
|
|
262
|
+
@file_effects
|
|
263
|
+
end
|
|
264
|
+
|
|
207
265
|
private
|
|
208
266
|
|
|
209
267
|
# Mirrors {Runner#seed_project_scope}: applies the cross-file pre-pass discovery tables the
|
data/lib/rigor/bleeding_edge.rb
CHANGED
|
@@ -159,6 +159,28 @@ module Rigor
|
|
|
159
159
|
"the two corpora measured so far (Rigor's own `lib`, redmine `app/models`) it added no " \
|
|
160
160
|
"kills — every surviving breakage there is one the analyzer reports nowhere at all, not one " \
|
|
161
161
|
"it reports in a caller."
|
|
162
|
+
),
|
|
163
|
+
# ADR-103 WD15 — the owner-ruled preview of the v0.4.0 default. `:behaviour` because it moves no rule's
|
|
164
|
+
# severity; safe under the header comment's cache-identity rule for a reason specific to this feature
|
|
165
|
+
# rather than the generic WD13 argument, so it is spelled out here: `effects:` collection is
|
|
166
|
+
# OBSERVATIONAL (ADR-103 WD13) and the `effects:` block itself is deliberately absent from the
|
|
167
|
+
# diagnostics cache-key identity, so whether this feature is adopted never changes what a `rigor check`
|
|
168
|
+
# diagnostics entry holds — only whether the (separately identity-keyed) effects sidecar slot
|
|
169
|
+
# (`Effects::Identity.descriptor`, `lib/rigor/analysis/runner.rb`) gets populated. Envelope findings
|
|
170
|
+
# (`effect.envelope-exceeded`) are recomputed every run from that sidecar and are never written into
|
|
171
|
+
# the diagnostics entry (ADR-103 WD12), so a warm diagnostics HIT is correct whichever way this feature
|
|
172
|
+
# is set, exactly like every other `:behaviour` feature. Verified against `Runner#compute_run_diagnostics`
|
|
173
|
+
# / `#serve_effect_collections` on 2026-08-17.
|
|
174
|
+
Feature.new(
|
|
175
|
+
id: "effects-on-by-default",
|
|
176
|
+
kind: :behaviour,
|
|
177
|
+
summary: "With it, a project whose `.rigor.yml` carries no `effects:` key at all behaves as though " \
|
|
178
|
+
"it had written `effects: {}` — effect collection, the `rigor effects` verbs' cache sharing, " \
|
|
179
|
+
"and `effects.check` all turn on with every sub-key at its default. Writing `effects: false` " \
|
|
180
|
+
"explicitly still opts out, and a project that writes its own `effects:` block keeps that " \
|
|
181
|
+
"block's shape untouched either way. Graduates to default-on at v0.4.0 (owner ruling, " \
|
|
182
|
+
"2026-08-17): ADR-50 § WD7 reads \"at a major\" as \"at v0.4.0\" for this feature on the 0.x " \
|
|
183
|
+
"evaluation line. See ADR-103 § WD15 for the preconditions that gate the flip."
|
|
162
184
|
)
|
|
163
185
|
].freeze
|
|
164
186
|
|
|
@@ -51,8 +51,11 @@ module Rigor
|
|
|
51
51
|
# nil (a clean cold rebuild — no migration). 11: ADR-67 WD6c lift adds `param_table` (the inferred-param
|
|
52
52
|
# seed table the run's diagnostics were computed under, diffed on the next recheck to invalidate a
|
|
53
53
|
# callee whose seeds moved because a caller changed); a pre-11 blob mismatches the SCHEMA gate and
|
|
54
|
-
# loads as nil (a clean cold rebuild — no migration).
|
|
55
|
-
|
|
54
|
+
# loads as nil (a clean cold rebuild — no migration). 12: ADR-103 WD13 / issue #382 adds the effects
|
|
55
|
+
# sidecar — `effect_collections` (the per-file {Rigor::Effects::FileCollection}s a collecting run
|
|
56
|
+
# produced) and the `effects_identity` they were produced under; a pre-12 blob mismatches the SCHEMA
|
|
57
|
+
# gate and loads as nil (a clean cold rebuild — no migration).
|
|
58
|
+
SCHEMA = 12
|
|
56
59
|
|
|
57
60
|
# The persisted per-file state.
|
|
58
61
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -87,10 +90,22 @@ module Rigor
|
|
|
87
90
|
# recomputes the table fresh (the pre-pass is whole-project by design) and diffs it against this copy;
|
|
88
91
|
# a changed entry invalidates the callee's file and its symbol dependents. The types are Marshal-clean
|
|
89
92
|
# by the session's per-entry filter; a dropped entry re-checks its callee, the conservative direction.
|
|
93
|
+
# ADR-103 WD13 / issue #382 — the effects sidecar, ADR-46's half of "one cache, two identities, one
|
|
94
|
+
# extra slot":
|
|
95
|
+
# `effect_collections` maps an analyzed path to the {Rigor::Effects::FileCollection} that file
|
|
96
|
+
# contributed (`{}` when collection is off), so a recheck re-collects only the changed closure and
|
|
97
|
+
# serves the rest from here. The propagated table is NEVER stored — it is recomputed from the merged
|
|
98
|
+
# whole every run, because a leaf's summary reaches every caller and a stored table would have to be
|
|
99
|
+
# invalidated by all of them.
|
|
100
|
+
# `effects_identity` is {Rigor::Effects::Identity.digest} at the run that wrote them (nil when
|
|
101
|
+
# collection was off). It is a SEPARATE gate from the global `fingerprint`: the `effects:` block is
|
|
102
|
+
# deliberately absent from `Configuration#to_h`, so turning collection on invalidates no diagnostics,
|
|
103
|
+
# and a vocabulary / catalogue / `effects:` change must invalidate the summaries alone.
|
|
90
104
|
Payload = Data.define(:cache, :sources, :digests, :analyzed,
|
|
91
105
|
:symbol_sources, :ancestry_sources, :symbol_fingerprints,
|
|
92
106
|
:missing, :class_decls, :seed_bundles, :plugin_fact_digest,
|
|
93
|
-
:return_summaries, :param_table
|
|
107
|
+
:return_summaries, :param_table,
|
|
108
|
+
:effect_collections, :effects_identity)
|
|
94
109
|
|
|
95
110
|
# The global fingerprint that gates a snapshot load: a digest of the inputs whose change requires a full
|
|
96
111
|
# rebuild — the engine version + schema, the engine's own SOURCE when the version does not pin it, the
|
|
@@ -206,7 +221,9 @@ module Rigor
|
|
|
206
221
|
seed_bundles: data[:seed_bundles] || {},
|
|
207
222
|
plugin_fact_digest: data[:plugin_fact_digest],
|
|
208
223
|
return_summaries: data[:return_summaries] || {},
|
|
209
|
-
param_table: data[:param_table] || {}
|
|
224
|
+
param_table: data[:param_table] || {},
|
|
225
|
+
effect_collections: data[:effect_collections] || {},
|
|
226
|
+
effects_identity: data[:effects_identity]
|
|
210
227
|
)
|
|
211
228
|
end
|
|
212
229
|
private :payload_from
|
|
@@ -227,7 +244,9 @@ module Rigor
|
|
|
227
244
|
seed_bundles: payload.seed_bundles,
|
|
228
245
|
plugin_fact_digest: payload.plugin_fact_digest,
|
|
229
246
|
return_summaries: payload.return_summaries,
|
|
230
|
-
param_table: payload.param_table
|
|
247
|
+
param_table: payload.param_table,
|
|
248
|
+
effect_collections: payload.effect_collections,
|
|
249
|
+
effects_identity: payload.effects_identity
|
|
231
250
|
)
|
|
232
251
|
blob = Zlib::Deflate.deflate(raw)
|
|
233
252
|
tmp = "#{@path}.#{Process.pid}.tmp"
|
|
@@ -459,6 +459,11 @@ module Rigor
|
|
|
459
459
|
# key. `:unset` means "no flag — use the configured selection"; `true` adopts the whole overlay, `false`
|
|
460
460
|
# adopts none, and an Array of ids adopts only those (see `apply_bleeding_edge_override`).
|
|
461
461
|
bleeding_edge: :unset,
|
|
462
|
+
# ADR-103 WD1 / #385 — the effect-policy audit switch. Judges `effect.envelope-exceeded` as if
|
|
463
|
+
# `effects.tolerated:` were empty, so a project can see what its discharge policy is hiding
|
|
464
|
+
# without editing the policy. Judgment-time only: the run, its collection and its cache identity
|
|
465
|
+
# are identical either way.
|
|
466
|
+
no_tolerated_effects: false,
|
|
462
467
|
# Type-precision coverage block. Off by default — it is a second precision pass over the analyzed files (the
|
|
463
468
|
# same scan `rigor coverage` runs), so it is opt-in to keep the default check path's cost unchanged. When set,
|
|
464
469
|
# `--format json` gains a `coverage` object (scan_files + precision tiers) and the text output prints a
|
|
@@ -528,6 +533,10 @@ module Rigor
|
|
|
528
533
|
"ADR-50: ignore any configured bleeding_edge: selection for this run") do
|
|
529
534
|
options[:bleeding_edge] = false
|
|
530
535
|
end
|
|
536
|
+
opts.on("--no-tolerated-effects",
|
|
537
|
+
"ADR-103: check effect envelopes as if effects.tolerated: were empty") do
|
|
538
|
+
options[:no_tolerated_effects] = true
|
|
539
|
+
end
|
|
531
540
|
end
|
|
532
541
|
parser.parse!(@argv)
|
|
533
542
|
options
|
|
@@ -558,9 +567,12 @@ module Rigor
|
|
|
558
567
|
|
|
559
568
|
path = options.fetch(:config) || Configuration.discover
|
|
560
569
|
data = path && File.exist?(path) ? Configuration.load_with_includes(path) : {}
|
|
570
|
+
# ADR-103 WD15 — captured before `DEFAULTS.merge` below folds "no `effects:` key" and "`effects:
|
|
571
|
+
# false`" into the same value; see `Configuration.load`'s identical capture.
|
|
572
|
+
effects_key_present = data.key?("effects")
|
|
561
573
|
data = data.dup
|
|
562
574
|
data["plugins"] = inject_treat_all_as_inline_rbs(Array(data["plugins"]))
|
|
563
|
-
Configuration.new(Configuration::DEFAULTS.merge(data))
|
|
575
|
+
Configuration.new(Configuration::DEFAULTS.merge(data), effects_key_present)
|
|
564
576
|
end
|
|
565
577
|
|
|
566
578
|
# ADR-50 § WD2 — applies the `--bleeding-edge[=ids]` / `--no-bleeding-edge` CLI selection over the configured
|
|
@@ -35,7 +35,10 @@ module Rigor
|
|
|
35
35
|
cache_store: cache_store,
|
|
36
36
|
collect_stats: options.fetch(:stats),
|
|
37
37
|
workers: resolve_workers(options, configuration),
|
|
38
|
-
buffer: buffer
|
|
38
|
+
buffer: buffer,
|
|
39
|
+
# ADR-103 #385 — absent from `doctor`'s option hash and every other reuser's, so it defaults off
|
|
40
|
+
# for them without each having to declare a key about a feature they do not surface.
|
|
41
|
+
no_tolerated_effects: options.fetch(:no_tolerated_effects, false)
|
|
39
42
|
)
|
|
40
43
|
end
|
|
41
44
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optionparser"
|
|
4
|
+
|
|
5
|
+
require_relative "../configuration"
|
|
6
|
+
require_relative "../analysis/runner"
|
|
7
|
+
require_relative "../effects/plugin_facts"
|
|
8
|
+
require_relative "../effects/registry"
|
|
9
|
+
require_relative "../plugin/loader"
|
|
10
|
+
require_relative "../cache/store"
|
|
11
|
+
require_relative "command"
|
|
12
|
+
require_relative "effects_renderer"
|
|
13
|
+
require_relative "effects_report"
|
|
14
|
+
require_relative "effects_snapshot_command"
|
|
15
|
+
require_relative "options"
|
|
16
|
+
|
|
17
|
+
module Rigor
|
|
18
|
+
class CLI
|
|
19
|
+
# ADR-103 — executes `rigor effects`.
|
|
20
|
+
#
|
|
21
|
+
# Two surfaces behind one command, the shape `rigor baseline` already has:
|
|
22
|
+
#
|
|
23
|
+
# - `rigor effects [PATH…]` — the **report**. Runs the same analysis `rigor check` runs, with effect
|
|
24
|
+
# collection on, and prints the resulting summaries instead of the diagnostic stream.
|
|
25
|
+
# - `rigor effects {update,check,diff,explain}` — the committed **effect snapshot** and its drift
|
|
26
|
+
# gate, in {EffectsSnapshotCommand}.
|
|
27
|
+
#
|
|
28
|
+
# Neither emits a diagnostic nor enters `rigor check`'s stream; the report always exits 0, and only
|
|
29
|
+
# `check` ever exits non-zero (1 on drift, 64 on a usage error).
|
|
30
|
+
#
|
|
31
|
+
# The command turns collection on for its own run by loading the project's configuration and enabling
|
|
32
|
+
# an implicit `effects: {}` when the file carries no `effects:` block — the ad-hoc mode ADR-103 WD14
|
|
33
|
+
# describes. A project that *does* configure `effects:` gets its own settings instead, and then this
|
|
34
|
+
# run shares the whole cache with `rigor check`: the diagnostics come from the ADR-45 run entry and the
|
|
35
|
+
# summaries from the effects sidecar keyed beside it (#382), so `rigor effects` after `rigor check` in
|
|
36
|
+
# the same job is a warm hit plus the fixpoint. The ad-hoc mode digests a different `effects:` block
|
|
37
|
+
# than the project's, so it shares the diagnostics entry and keys its own effects slot.
|
|
38
|
+
class EffectsCommand < Command
|
|
39
|
+
USAGE = "Usage: rigor effects [options] [paths]"
|
|
40
|
+
|
|
41
|
+
# @return [Integer] CLI exit status: 0 on success, 1 on `check` drift, 64 on a usage error.
|
|
42
|
+
def run
|
|
43
|
+
verb = @argv.first
|
|
44
|
+
return run_verb(verb) if EffectsSnapshotCommand::VERBS.include?(verb)
|
|
45
|
+
return print_help if %w[help --help -h].include?(verb)
|
|
46
|
+
|
|
47
|
+
run_report
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def run_verb(verb)
|
|
53
|
+
@argv.shift
|
|
54
|
+
EffectsSnapshotCommand.new(argv: @argv, verb: verb, out: @out, err: @err).run
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def print_help
|
|
58
|
+
@out.puts(help)
|
|
59
|
+
0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def help
|
|
63
|
+
<<~HELP
|
|
64
|
+
#{USAGE}
|
|
65
|
+
|
|
66
|
+
With no subcommand, prints one line per method: its proven effect labels and whether that
|
|
67
|
+
list is exhaustive. A PATH selects which methods are printed, never which are analysed.
|
|
68
|
+
|
|
69
|
+
Options:
|
|
70
|
+
--config=PATH Path to the Rigor configuration file
|
|
71
|
+
--format=FORMAT Output format: text (default) or json
|
|
72
|
+
--full List every method, including the ones with nothing to say
|
|
73
|
+
--label=LABEL Only methods carrying LABEL (or a label under it), in either lane
|
|
74
|
+
--pure Only methods proven to do nothing beyond mutate.local
|
|
75
|
+
--limit=N Print at most N methods
|
|
76
|
+
--why Expand each method's unresolved reasons and declared-lane sources
|
|
77
|
+
--list-labels Print the effect vocabulary this project can name, and exit
|
|
78
|
+
|
|
79
|
+
Subcommands (the committed effect snapshot, ADR-103 WD7):
|
|
80
|
+
update Write the snapshot to effects.snapshot.path. Commit it; review its diff.
|
|
81
|
+
check Recompute and compare; exits 1 on drift, 0 when fresh.
|
|
82
|
+
diff The same comparison, never gating.
|
|
83
|
+
explain The shortest edge path behind a reach change (--symbol KEY for one unit).
|
|
84
|
+
|
|
85
|
+
Run `rigor effects <subcommand> --help` for subcommand options.
|
|
86
|
+
HELP
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def run_report
|
|
90
|
+
options = parse_options
|
|
91
|
+
return usage_error("unsupported format: #{options.fetch(:format)}") unless FORMATS.include?(options[:format])
|
|
92
|
+
|
|
93
|
+
configuration = Configuration.load(options.fetch(:config)).with_effects_enabled
|
|
94
|
+
return list_labels(configuration) if options.fetch(:list_labels)
|
|
95
|
+
|
|
96
|
+
scope = @argv.dup
|
|
97
|
+
table, sources = analyze(configuration, scope)
|
|
98
|
+
report = EffectsReport.build(
|
|
99
|
+
table, full: options.fetch(:full), sources: sources, scope: scope,
|
|
100
|
+
label: options.fetch(:label), pure: options.fetch(:pure), limit: options.fetch(:limit)
|
|
101
|
+
)
|
|
102
|
+
note_scope(scope, report, table)
|
|
103
|
+
EffectsRenderer.new(out: @out, why: options.fetch(:why)).render(report, format: options.fetch(:format))
|
|
104
|
+
0
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A path argument is a **view**, and the note says so (#439).
|
|
108
|
+
#
|
|
109
|
+
# It used to narrow the analysed set, and effect labels are transitive over whatever was analysed —
|
|
110
|
+
# so `rigor effects app/controllers/issues_controller.rb` reported `IssuesController#create: [] …?`
|
|
111
|
+
# where the whole-project run reported four labels and a declared lane. The weakened answer was
|
|
112
|
+
# indistinguishable from a genuinely effect-free method, and a path argument is the only tractability
|
|
113
|
+
# lever the report has, so it was the thing an adopter reached for first.
|
|
114
|
+
#
|
|
115
|
+
# The note goes to stderr rather than into the report: it is about the invocation, not about the
|
|
116
|
+
# code, and `--format json` and `rigor effects … > report.txt` both stay exactly what they were.
|
|
117
|
+
def note_scope(scope, report, table)
|
|
118
|
+
return if scope.empty?
|
|
119
|
+
|
|
120
|
+
if report.empty?
|
|
121
|
+
@err.puts("rigor: no effect unit is defined in #{scope.join(', ')} " \
|
|
122
|
+
"(a path selects what is printed, not what is analysed)")
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@err.puts("rigor: showing #{report.rows.length} of #{table.size} units, selected by " \
|
|
127
|
+
"#{scope.join(', ')}; a path narrows the printing and not the analysis, so every " \
|
|
128
|
+
"label is the one the whole-project run reports")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
FORMATS = %w[text json].freeze
|
|
132
|
+
private_constant :FORMATS
|
|
133
|
+
|
|
134
|
+
def parse_options
|
|
135
|
+
options = { config: nil, format: "text", full: false, no_tolerated: false, label: [], pure: false,
|
|
136
|
+
limit: nil, why: false, list_labels: false }
|
|
137
|
+
OptionParser.new do |opts|
|
|
138
|
+
opts.banner = USAGE
|
|
139
|
+
Options.add_config(opts, options)
|
|
140
|
+
opts.on("--format=FORMAT", "Output format: text (default) or json") { |value| options[:format] = value }
|
|
141
|
+
opts.on("--full", "List every method, including the ones with nothing to say") do
|
|
142
|
+
options[:full] = true
|
|
143
|
+
end
|
|
144
|
+
opts.on("--label=LABEL", "Only methods carrying LABEL (or a label under it), in either lane") do |value|
|
|
145
|
+
options[:label].concat(value.split(",").map(&:strip).reject(&:empty?))
|
|
146
|
+
end
|
|
147
|
+
opts.on("--pure", "Only methods proven to do nothing beyond mutate.local — the %a{pure} set") do
|
|
148
|
+
options[:pure] = true
|
|
149
|
+
end
|
|
150
|
+
opts.on("--limit=N", Integer, "Print at most N methods") { |value| options[:limit] = value }
|
|
151
|
+
opts.on("--why", "Expand each method's unresolved reasons and declared-lane sources") do
|
|
152
|
+
options[:why] = true
|
|
153
|
+
end
|
|
154
|
+
opts.on("--list-labels", "Print the effect vocabulary this project can name, and exit") do
|
|
155
|
+
options[:list_labels] = true
|
|
156
|
+
end
|
|
157
|
+
# Accepted here and deliberately inert, exactly as it is on `update`: the report is an
|
|
158
|
+
# observation, and observations are undischarged. Only a JUDGMENT reads `effects.tolerated:` —
|
|
159
|
+
# `rigor effects check` / `diff`, and `rigor check`'s envelope contract.
|
|
160
|
+
opts.on("--no-tolerated-effects", "Judge as if effects.tolerated: were empty (inert on the report)") do
|
|
161
|
+
options[:no_tolerated] = true
|
|
162
|
+
end
|
|
163
|
+
end.parse!(@argv)
|
|
164
|
+
options
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def usage_error(message)
|
|
168
|
+
@err.puts(message)
|
|
169
|
+
@err.puts(USAGE)
|
|
170
|
+
CLI::EXIT_USAGE
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Sequential and cache-backed, through the same ADR-45 whole-run result cache `rigor check` uses:
|
|
174
|
+
# the diagnostics entry serves the run and the #382 effects sidecar serves the collections, leaving
|
|
175
|
+
# only the fixpoint. Sequential is not a cache decision — the run-result cache declines pool mode —
|
|
176
|
+
# but a collecting run is pinned to the fork backend anyway, so `workers: 0` costs nothing here.
|
|
177
|
+
# #429 — the vocabulary, from the product rather than from a document the gem does not ship.
|
|
178
|
+
#
|
|
179
|
+
# Four configuration keys and two annotation forms all require typing an effect label, and until
|
|
180
|
+
# this there was no way to find out what the labels are: the manual's pointers resolve into
|
|
181
|
+
# `docs/type-specification/`, which `rigortype.gemspec` does not package. The registry is shipped
|
|
182
|
+
# data and already knows the answer; nothing surfaced it.
|
|
183
|
+
#
|
|
184
|
+
# It loads plugins but never analyses, because a plugin is where `rails.flash.write` comes from and
|
|
185
|
+
# a reader deciding what to write in `effects.envelopes:` needs to see it. A plugin that fails to
|
|
186
|
+
# load costs its labels and nothing else — `rigor plugins` is where that failure is reported.
|
|
187
|
+
def list_labels(configuration)
|
|
188
|
+
registry = project_registry(configuration)
|
|
189
|
+
@out.puts("Effect vocabulary — #{registry.labels.length} labels (vocabulary #{registry.vocabulary_version})")
|
|
190
|
+
registry.roots.each do |root|
|
|
191
|
+
@out.puts
|
|
192
|
+
@out.puts(root_heading(root, registry))
|
|
193
|
+
in_root = registry.labels.select { |label| Effects::Label.root(label) == root }
|
|
194
|
+
in_root.each_slice(4) { |slice| @out.puts(" #{slice.join(', ')}") }
|
|
195
|
+
end
|
|
196
|
+
@out.puts
|
|
197
|
+
@out.puts("A bound naming a label admits everything under it and nothing above it: `io` covers " \
|
|
198
|
+
"`io.db.read`, `io.db.read` does not cover `io`.")
|
|
199
|
+
0
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# A shipped root carries the registry's own one-liner; a root a plugin or this project opened
|
|
203
|
+
# carries where it came from instead, which is the more useful thing to say about it.
|
|
204
|
+
def root_heading(root, registry)
|
|
205
|
+
description = registry.descriptions[root]
|
|
206
|
+
return "#{root} — #{description}" if description
|
|
207
|
+
|
|
208
|
+
"#{root} — opened by a plugin or by this project's `effects.labels:`"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def project_registry(configuration)
|
|
212
|
+
services = Plugin::Services.new(reflection: Reflection, type: Type::Combinator,
|
|
213
|
+
configuration: configuration, cache_store: nil)
|
|
214
|
+
plugins = Plugin::Loader.load(configuration: configuration, services: services)
|
|
215
|
+
Effects::Registry.for_configuration(configuration, plugin_facts: Effects::PluginFacts.build(plugins))
|
|
216
|
+
rescue StandardError
|
|
217
|
+
Effects::Registry.for_configuration(configuration)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# The analysed set is the configured `paths:` **plus** whatever the arguments name — never the
|
|
221
|
+
# arguments alone (#439). An effect summary is transitive over whatever was analysed, so analysing
|
|
222
|
+
# less does not filter the report, it lowers every answer in it: `rigor effects
|
|
223
|
+
# app/controllers/issues_controller.rb` used to report `IssuesController#create: [] …?` where the
|
|
224
|
+
# whole-project run reported four labels and a declared lane, with nothing marking the difference.
|
|
225
|
+
#
|
|
226
|
+
# The union rather than the configured paths alone, so that pointing the command at a tree the
|
|
227
|
+
# configuration does not cover — which is what every `rigor effects PATH` invocation from outside a
|
|
228
|
+
# project does — still analyses it. Inside a project the argument is already under `paths:` and the
|
|
229
|
+
# union is the configured set unchanged.
|
|
230
|
+
#
|
|
231
|
+
# @return [Array(Rigor::Effects::EffectTable, Hash{String=>Array<String>})] the table, and which
|
|
232
|
+
# file each unit was defined in — the map {EffectsReport} needs to answer a path argument.
|
|
233
|
+
def analyze(configuration, scope)
|
|
234
|
+
runner = Analysis::Runner.new(
|
|
235
|
+
configuration: configuration,
|
|
236
|
+
cache_store: Cache::Store.new(root: configuration.cache_path),
|
|
237
|
+
collect_stats: false,
|
|
238
|
+
workers: 0
|
|
239
|
+
)
|
|
240
|
+
runner.run((configuration.paths + scope).uniq)
|
|
241
|
+
[runner.effect_table, runner.effect_sources]
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "../effects/snapshot_diff"
|
|
6
|
+
require_relative "renderable"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
class CLI
|
|
10
|
+
# Prints a {Rigor::Effects::SnapshotDiff} — what `rigor effects check` and `rigor effects diff` show a
|
|
11
|
+
# reviewer (ADR-103 WD7).
|
|
12
|
+
#
|
|
13
|
+
# The text form is grouped by table and then by policy: real drift under `methods:` / `reach:`,
|
|
14
|
+
# policy-discharged drift under `tolerated:`, and a regeneration event — a record written by a
|
|
15
|
+
# different Rigor, vocabulary or `effects:` block — under `regeneration:` above both. It closes with
|
|
16
|
+
# the one line the whole workflow turns on: intent is expressed by regenerating and committing the
|
|
17
|
+
# file, not by annotating the code.
|
|
18
|
+
#
|
|
19
|
+
# It is deliberately not a diagnostic format: no severities, no positions, no exit-code weight of its
|
|
20
|
+
# own. The exit code comes from the gate.
|
|
21
|
+
class EffectsDiffRenderer
|
|
22
|
+
include Renderable
|
|
23
|
+
|
|
24
|
+
# How each category renders as the marker after the symbol. Steins' event vocabulary, made
|
|
25
|
+
# symmetric: `≤` is the declared lane, and a hedged removal says why it is hedged.
|
|
26
|
+
MARKERS = {
|
|
27
|
+
Effects::SnapshotDiff::LABEL_ADDED => "+ %<label>s",
|
|
28
|
+
Effects::SnapshotDiff::LABEL_REMOVED => "- %<label>s",
|
|
29
|
+
Effects::SnapshotDiff::DECLARED_ADDED => "≤+ %<label>s",
|
|
30
|
+
Effects::SnapshotDiff::DECLARED_REMOVED => "≤- %<label>s",
|
|
31
|
+
Effects::SnapshotDiff::MATERIALISED => "materialised %<label>s (declared → proven)",
|
|
32
|
+
Effects::SnapshotDiff::SYMBOL_ADDED => "+symbol %<detail>s",
|
|
33
|
+
Effects::SnapshotDiff::SYMBOL_REMOVED => "-symbol %<detail>s"
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
HEDGED_REMOVAL = "-? %<label>s (current summary is not exhaustive)"
|
|
37
|
+
|
|
38
|
+
# Both commands, because they answer the two questions a drift report raises and a reader almost
|
|
39
|
+
# always asks them in this order (#435). `explain` is the one the manual's own narrative reaches for
|
|
40
|
+
# first, and the footer used to name only the one that makes the report go away.
|
|
41
|
+
CLOSING_LINE = "Run `rigor effects explain` to see what caused this, and `rigor effects update` to " \
|
|
42
|
+
"accept it."
|
|
43
|
+
|
|
44
|
+
def initialize(out:, path:)
|
|
45
|
+
@out = out
|
|
46
|
+
@path = path
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def render_text(diff)
|
|
52
|
+
if diff.fresh?
|
|
53
|
+
@out.puts("No effect drift against #{@path}.")
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@out.puts("Effect drift against #{@path}:")
|
|
58
|
+
%w[snapshot header methods reach].each { |table| render_section(table, diff.events_for(table)) }
|
|
59
|
+
render_section("tolerated", diff.tolerated_events, qualified: true)
|
|
60
|
+
render_footer(diff)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# `qualified:` names each event's table inline, which the `tolerated:` heading needs: it pools events
|
|
64
|
+
# from both tables, and one symbol commonly appears in each.
|
|
65
|
+
def render_section(table, events, qualified: false)
|
|
66
|
+
return if events.empty?
|
|
67
|
+
|
|
68
|
+
@out.puts("")
|
|
69
|
+
@out.puts("#{section_name(table)}:")
|
|
70
|
+
events.each do |event|
|
|
71
|
+
suffix = qualified ? " (#{event.table})" : ""
|
|
72
|
+
@out.puts(" #{render_event(event)}#{suffix}")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def section_name(table)
|
|
77
|
+
case table
|
|
78
|
+
when "header" then "regeneration"
|
|
79
|
+
when "snapshot" then "snapshot"
|
|
80
|
+
else table
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_event(event)
|
|
85
|
+
marker = marker_for(event)
|
|
86
|
+
event.symbol.nil? ? marker : "#{event.symbol} #{marker}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def marker_for(event)
|
|
90
|
+
return format(HEDGED_REMOVAL, label: event.label) if
|
|
91
|
+
event.category == Effects::SnapshotDiff::LABEL_REMOVED && event.hedged?
|
|
92
|
+
|
|
93
|
+
template = MARKERS[event.category]
|
|
94
|
+
return event.detail.to_s unless template
|
|
95
|
+
|
|
96
|
+
format(template, label: event.label.to_s, detail: event.detail.to_s)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def render_footer(diff)
|
|
100
|
+
footer = diff.footer
|
|
101
|
+
@out.puts("")
|
|
102
|
+
unless footer[:added_symbols].zero? && footer[:removed_symbols].zero?
|
|
103
|
+
@out.puts("symbols: +#{footer[:added_symbols]} / -#{footer[:removed_symbols]} " \
|
|
104
|
+
"(a rename is one of each)")
|
|
105
|
+
end
|
|
106
|
+
@out.puts(CLOSING_LINE)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def render_json(diff)
|
|
110
|
+
@out.puts(JSON.pretty_generate(
|
|
111
|
+
"fresh" => diff.fresh?,
|
|
112
|
+
"events" => diff.events.map(&:to_h),
|
|
113
|
+
"footer" => {
|
|
114
|
+
"added_symbols" => diff.footer[:added_symbols],
|
|
115
|
+
"removed_symbols" => diff.footer[:removed_symbols]
|
|
116
|
+
},
|
|
117
|
+
"header" => {
|
|
118
|
+
"snapshot" => @path,
|
|
119
|
+
"gate" => diff.gate.to_s,
|
|
120
|
+
"recorded" => diff.recorded&.header,
|
|
121
|
+
"current" => diff.current.header
|
|
122
|
+
}
|
|
123
|
+
))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|