rigortype 0.3.3 → 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 +6 -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/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 +158 -0
- 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/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/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 +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/registry.rb +48 -0
- data/lib/rigor/plugin.rb +1 -0
- 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/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.rbs +17 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +79 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
class Activerecord < Rigor::Plugin::Base
|
|
8
|
+
# rigor-activerecord's effect contract (ADR-103 WD10; design note § 11.2; issue #387).
|
|
9
|
+
#
|
|
10
|
+
# ## The two channels, and which row goes where
|
|
11
|
+
#
|
|
12
|
+
# The plugin already ships `sig/active_record/relation.rbs`, so **every `ActiveRecord::Relation`
|
|
13
|
+
# method it declares carries its own `%a{…}` annotation there** — the tier-1 channel, read as an
|
|
14
|
+
# accepted signature and therefore discharging (ADR-103 WD6). That file is also where the builder /
|
|
15
|
+
# materializer split lives, because it is the file that already draws it.
|
|
16
|
+
#
|
|
17
|
+
# What is left over lands here, and each item is left over for a reason RBS cannot fix:
|
|
18
|
+
#
|
|
19
|
+
# - **`ActiveRecord::Base`'s own surface.** The plugin ships no `ActiveRecord::Base` signature and
|
|
20
|
+
# should not: an app's models are typed from `db/schema.rb`, per project, and a generic base-class
|
|
21
|
+
# RBS would fight whatever the project's own `rbs collection` supplies.
|
|
22
|
+
# - **The `Enumerable` delegations on a Relation.** `map`, `filter_map`, `each_with_object` and the
|
|
23
|
+
# rest materialise by delegating to `each`, and declaring them in the bundled RBS would change how
|
|
24
|
+
# they *type* (`Enumerable#map`'s block-return element type is the whole point of the include).
|
|
25
|
+
# - **The connection adapter and the migration DSL**, neither of which the plugin types at all.
|
|
26
|
+
#
|
|
27
|
+
# ## Why the reads and writes are worth spelling out at all
|
|
28
|
+
#
|
|
29
|
+
# An `ActiveRecord::Base` row reaches `User.find` through the project's own
|
|
30
|
+
# `User < ApplicationRecord < ActiveRecord::Base` lines, so one row covers every model in the app.
|
|
31
|
+
# That is the whole economy of the framework layer: thirty-odd rows here colour the several thousand
|
|
32
|
+
# database touches a Rails app performs, and none of them needed a line of application code.
|
|
33
|
+
module Effects
|
|
34
|
+
BASE = "ActiveRecord::Base"
|
|
35
|
+
RELATION = "ActiveRecord::Relation"
|
|
36
|
+
ADAPTER = "ActiveRecord::ConnectionAdapters::AbstractAdapter"
|
|
37
|
+
MIGRATION = "ActiveRecord::Migration"
|
|
38
|
+
|
|
39
|
+
READ = ["io.db.read"].freeze
|
|
40
|
+
WRITE = ["io.db.write"].freeze
|
|
41
|
+
TRANSACTION = ["io.db.transaction"].freeze
|
|
42
|
+
SCHEMA_WRITE = ["io.db.write", "rails.schema.write"].freeze
|
|
43
|
+
|
|
44
|
+
# Class-side finders. Every one issues a `SELECT` the moment it is called — that is what separates
|
|
45
|
+
# them from `where`, which returns a relation and issues nothing.
|
|
46
|
+
SINGLETON_READS = %w[
|
|
47
|
+
find find_by find_by! first first! last last! take take! sole find_sole_by
|
|
48
|
+
second third fourth fifth forty_two second_to_last third_to_last
|
|
49
|
+
exists? any? none? one? many? empty? count sum average minimum maximum calculate
|
|
50
|
+
pluck pick ids find_each find_in_batches in_batches find_by_sql count_by_sql
|
|
51
|
+
find_or_initialize_by
|
|
52
|
+
].freeze
|
|
53
|
+
|
|
54
|
+
# Class-side writers.
|
|
55
|
+
SINGLETON_WRITES = %w[
|
|
56
|
+
create create! insert insert! insert_all insert_all! upsert upsert_all
|
|
57
|
+
update update! update_all delete delete_all delete_by destroy destroy_all destroy_by
|
|
58
|
+
find_or_create_by find_or_create_by! create_or_find_by create_or_find_by! touch_all
|
|
59
|
+
].freeze
|
|
60
|
+
|
|
61
|
+
# Instance-side readers. `reload` re-issues the `SELECT` and replaces the record's attributes,
|
|
62
|
+
# which is a receiver mutation as well as a read.
|
|
63
|
+
INSTANCE_READS = %w[valid? invalid? reload].freeze
|
|
64
|
+
|
|
65
|
+
# Instance-side writers. Each is a statement issued now; `save`'s callbacks and validators are the
|
|
66
|
+
# `effect_edges:` half, and arrive as edges rather than labels.
|
|
67
|
+
INSTANCE_WRITES = %w[
|
|
68
|
+
save save! update update! update_attribute update_attributes update_attributes!
|
|
69
|
+
update_column update_columns destroy destroy! delete touch increment! decrement!
|
|
70
|
+
toggle! becomes! insert
|
|
71
|
+
].freeze
|
|
72
|
+
|
|
73
|
+
# The `Enumerable` surface a Relation inherits. Every one of these runs the query, because every
|
|
74
|
+
# one of them calls `each`.
|
|
75
|
+
RELATION_MATERIALIZERS = %w[
|
|
76
|
+
map flat_map filter_map collect collect_concat select filter reject find detect find_all
|
|
77
|
+
each_with_object each_with_index each_entry each_slice each_cons reduce inject
|
|
78
|
+
group_by partition sort sort_by min min_by max max_by minmax tally zip
|
|
79
|
+
take_while drop_while chunk_while slice_when lazy to_set to_h
|
|
80
|
+
all? any? none? one? include? member? first count sum
|
|
81
|
+
].freeze
|
|
82
|
+
|
|
83
|
+
# Raw SQL, narrowed by the statement's own leading verb ({Rigor::Effects::Narrowing} `sql_verb`).
|
|
84
|
+
ADAPTER_SQL = %w[execute exec_query exec_insert exec_update exec_delete select_all select_one
|
|
85
|
+
select_value select_values select_rows query query_value query_values].freeze
|
|
86
|
+
|
|
87
|
+
# The migration DSL. `rails.schema.write` is the meaning a reviewer names ("this changes the
|
|
88
|
+
# schema"); `io.db.write` is the transport that makes the summary honest.
|
|
89
|
+
MIGRATION_DSL = %w[
|
|
90
|
+
create_table drop_table rename_table change_table create_join_table drop_join_table
|
|
91
|
+
add_column remove_column rename_column change_column change_column_null
|
|
92
|
+
change_column_default change_column_comment
|
|
93
|
+
add_index remove_index rename_index add_reference remove_reference
|
|
94
|
+
add_foreign_key remove_foreign_key add_check_constraint remove_check_constraint
|
|
95
|
+
add_timestamps remove_timestamps enable_extension disable_extension
|
|
96
|
+
execute add_belongs_to remove_belongs_to
|
|
97
|
+
].freeze
|
|
98
|
+
|
|
99
|
+
ADAPTER_WHY = "raw SQL; the statement's own leading verb narrows the direction, so a literal " \
|
|
100
|
+
"`execute(\"UPDATE …\")` reads as a write and a computed string keeps the honest " \
|
|
101
|
+
"`io.db`."
|
|
102
|
+
|
|
103
|
+
# Ambient AR calls that are neither a read nor a write of rows.
|
|
104
|
+
TRANSACTIONAL = %w[transaction with_lock lock!].freeze
|
|
105
|
+
|
|
106
|
+
module_function
|
|
107
|
+
|
|
108
|
+
def attributions
|
|
109
|
+
singleton_rows + instance_rows + relation_rows + adapter_rows + migration_rows
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def singleton_rows
|
|
113
|
+
rows(BASE, SINGLETON_READS, READ, singleton: true,
|
|
114
|
+
why: "issues the SELECT at the call — this is the materializing " \
|
|
115
|
+
"half of the builder/materializer split") +
|
|
116
|
+
rows(BASE, SINGLETON_WRITES, WRITE, singleton: true,
|
|
117
|
+
why: "issues an INSERT / UPDATE / DELETE at the call") +
|
|
118
|
+
rows(BASE, TRANSACTIONAL, TRANSACTION, singleton: true,
|
|
119
|
+
why: "opens a transaction; the block's own origins join " \
|
|
120
|
+
"by containment, so the row states only the BEGIN")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def instance_rows
|
|
124
|
+
rows(BASE, INSTANCE_READS, READ,
|
|
125
|
+
why: "re-reads the row (`reload`) or runs the validators, whose uniqueness checks query") +
|
|
126
|
+
rows(BASE, INSTANCE_WRITES, WRITE,
|
|
127
|
+
why: "persists the record — the write a `db: none` envelope is written to catch") +
|
|
128
|
+
rows(BASE, TRANSACTIONAL, TRANSACTION,
|
|
129
|
+
why: "opens a transaction / takes a row lock around the block")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def relation_rows
|
|
133
|
+
rows(RELATION, RELATION_MATERIALIZERS, READ,
|
|
134
|
+
why: "an Enumerable delegation on a Relation: it calls `each`, which runs the query. Not " \
|
|
135
|
+
"declared in the bundled RBS because declaring it would change how it types")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Two rows per selector, because raw SQL is written two ways and only one of them names a type.
|
|
139
|
+
# `adapter.execute(sql)` on a receiver the typer managed to name is the first; the second matches
|
|
140
|
+
# `ActiveRecord::Base.connection.execute(sql)` and `User.connection.exec_query(sql)`, where the
|
|
141
|
+
# connection object has no declared type but the class that handed it over is written in the source.
|
|
142
|
+
def adapter_rows
|
|
143
|
+
ADAPTER_SQL.flat_map do |selector|
|
|
144
|
+
[
|
|
145
|
+
EffectAttribution.new(
|
|
146
|
+
receiver: ADAPTER, method: selector, labels: ["io.db"], narrow: "sql_verb", discharge: true,
|
|
147
|
+
why: ADAPTER_WHY
|
|
148
|
+
),
|
|
149
|
+
EffectAttribution.new(
|
|
150
|
+
receiver: BASE, method: selector, labels: ["io.db"], narrow: "sql_verb", on_result: true,
|
|
151
|
+
discharge: true,
|
|
152
|
+
why: "#{ADAPTER_WHY} Matched on the result of `Model.connection`, which is how raw SQL is " \
|
|
153
|
+
"actually spelled in a Rails app."
|
|
154
|
+
)
|
|
155
|
+
]
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def migration_rows
|
|
160
|
+
rows(MIGRATION, MIGRATION_DSL, SCHEMA_WRITE,
|
|
161
|
+
why: "the migration DSL issues DDL: `io.db.write` is the transport, `rails.schema.write` " \
|
|
162
|
+
"the meaning a `db/migrate/**` envelope names")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def rows(receiver, selectors, labels, why:, singleton: false)
|
|
166
|
+
selectors.map do |selector|
|
|
167
|
+
EffectAttribution.new(receiver: receiver, method: selector, labels: labels,
|
|
168
|
+
singleton: singleton, discharge: true, why: why)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# ADR-103 WD10 — the one edge ActiveRecord contributes: `save` runs the class body's callbacks and
|
|
173
|
+
# validators. The engine owns the walk; this names the base class whose descendants it applies to.
|
|
174
|
+
def edges
|
|
175
|
+
[
|
|
176
|
+
EffectEdge.new(
|
|
177
|
+
receiver: BASE, target: :activerecord_callbacks,
|
|
178
|
+
why: "`before_save :normalize` / `validate :check` / `after_commit :notify` are real, " \
|
|
179
|
+
"synchronous, in-process calls that no syntax at the call site contains"
|
|
180
|
+
)
|
|
181
|
+
]
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -8,6 +8,7 @@ require_relative "activerecord/structure_sql_parser"
|
|
|
8
8
|
require_relative "activerecord/model_index"
|
|
9
9
|
require_relative "activerecord/model_discoverer"
|
|
10
10
|
require_relative "activerecord/analyzer"
|
|
11
|
+
require_relative "activerecord/effects"
|
|
11
12
|
|
|
12
13
|
module Rigor
|
|
13
14
|
module Plugin
|
|
@@ -72,7 +73,17 @@ module Rigor
|
|
|
72
73
|
# ADR-26 — `ActiveRecord::Relation` is an "open" receiver: it delegates an unbounded set of
|
|
73
74
|
# user-defined scopes / class methods to its model, so `call.undefined-method` must not fire for
|
|
74
75
|
# it. `CheckRules` reads this manifest field and skips the rule for the class.
|
|
75
|
-
open_receivers: ["ActiveRecord::Relation"]
|
|
76
|
+
open_receivers: ["ActiveRecord::Relation"],
|
|
77
|
+
# ADR-103 WD2 / WD10 (#387) — the effect layer. rigor-activerecord models ActiveRecord, which is
|
|
78
|
+
# part of Rails, so it opens the framework's own `rails.*` root rather than one named after the
|
|
79
|
+
# plugin; `effect_root:` is granted only because the engine bundles this plugin
|
|
80
|
+
# ({Rigor::Plugin::FirstParty}). Every row's justification is in {Effects}, and the
|
|
81
|
+
# `ActiveRecord::Relation` half is in the bundled `sig/active_record/relation.rbs` instead —
|
|
82
|
+
# tier 1, because the plugin already ships that signature.
|
|
83
|
+
effect_root: "rails",
|
|
84
|
+
effect_labels: ["rails.schema.write"],
|
|
85
|
+
effect_attributions: Effects.attributions,
|
|
86
|
+
effect_edges: Effects.edges
|
|
76
87
|
)
|
|
77
88
|
|
|
78
89
|
# The class the bundled `sig/active_record/relation.rbs` describes; `dynamic_return` contributes
|
|
@@ -26,6 +26,26 @@
|
|
|
26
26
|
# `rbs collection`), that definition conflicts with this one and
|
|
27
27
|
# the conflict degrades through Rigor's plugin-RBS failure memo
|
|
28
28
|
# (ADR-25 WD4) — the richer upstream definition simply wins.
|
|
29
|
+
#
|
|
30
|
+
# ## Effect annotations (ADR-103 WD10, issue #387)
|
|
31
|
+
#
|
|
32
|
+
# Every method carries an effect annotation, and the split down
|
|
33
|
+
# the middle of this file is the whole point of the Rails effect
|
|
34
|
+
# layer: a **builder** is `%a{pure}` because it issues nothing —
|
|
35
|
+
# `where` composes an Arel tree and returns a relation — while a
|
|
36
|
+
# **materializer** is `%a{rigor:v1:effect io.db.read}` because it
|
|
37
|
+
# is the call that finally runs the query. A presenter that builds
|
|
38
|
+
# and returns a scope has pure code; the caller that materialises
|
|
39
|
+
# it gets the read. That is the truthful reading of what the code
|
|
40
|
+
# does, not of how Rails developers narrate it.
|
|
41
|
+
#
|
|
42
|
+
# This is the plugin's tier-1 channel: these are *accepted
|
|
43
|
+
# signatures*, so a call site importing one of these bounds is
|
|
44
|
+
# exhaustive rather than tainted (ADR-103 WD6). The methods this
|
|
45
|
+
# file does not declare — `map`, `filter_map` and the rest of the
|
|
46
|
+
# `Enumerable` surface, which materialise by delegating to `each`
|
|
47
|
+
# — are covered by the plugin's `effect_attributions:` instead,
|
|
48
|
+
# because declaring them here would change how they TYPE.
|
|
29
49
|
|
|
30
50
|
module ActiveRecord
|
|
31
51
|
class Relation[Elem]
|
|
@@ -34,149 +54,274 @@ module ActiveRecord
|
|
|
34
54
|
# `Enumerable` only *requires* `each` (the includer supplies
|
|
35
55
|
# it); it is declared here so `relation.each { |m| ... }`
|
|
36
56
|
# resolves and the block parameter carries the element type.
|
|
57
|
+
%a{rigor:v1:effect io.db.read}
|
|
37
58
|
def each: () { (Elem) -> void } -> self
|
|
38
59
|
| () -> Enumerator[Elem, self]
|
|
39
60
|
|
|
40
61
|
# --- query builders (return another relation) ---
|
|
62
|
+
%a{pure}
|
|
41
63
|
def where: (*untyped) ?{ () -> untyped } -> self
|
|
64
|
+
%a{pure}
|
|
42
65
|
def rewhere: (*untyped) -> self
|
|
66
|
+
%a{pure}
|
|
43
67
|
def not: (*untyped) -> self
|
|
68
|
+
%a{pure}
|
|
44
69
|
def or: (untyped) -> self
|
|
70
|
+
%a{pure}
|
|
45
71
|
def and: (untyped) -> self
|
|
72
|
+
%a{pure}
|
|
46
73
|
def order: (*untyped) -> self
|
|
74
|
+
%a{pure}
|
|
47
75
|
def reorder: (*untyped) -> self
|
|
76
|
+
%a{pure}
|
|
48
77
|
def in_order_of: (*untyped) -> self
|
|
78
|
+
%a{pure}
|
|
49
79
|
def limit: (untyped) -> self
|
|
80
|
+
%a{pure}
|
|
50
81
|
def offset: (untyped) -> self
|
|
82
|
+
%a{pure}
|
|
51
83
|
def distinct: (?untyped) -> self
|
|
84
|
+
%a{pure}
|
|
52
85
|
def group: (*untyped) -> self
|
|
86
|
+
%a{pure}
|
|
53
87
|
def regroup: (*untyped) -> self
|
|
88
|
+
%a{pure}
|
|
54
89
|
def having: (*untyped) -> self
|
|
90
|
+
%a{pure}
|
|
55
91
|
def includes: (*untyped) -> self
|
|
92
|
+
%a{pure}
|
|
56
93
|
def preload: (*untyped) -> self
|
|
94
|
+
%a{pure}
|
|
57
95
|
def eager_load: (*untyped) -> self
|
|
96
|
+
%a{pure}
|
|
58
97
|
def joins: (*untyped) -> self
|
|
98
|
+
%a{pure}
|
|
59
99
|
def left_joins: (*untyped) -> self
|
|
100
|
+
%a{pure}
|
|
60
101
|
def left_outer_joins: (*untyped) -> self
|
|
102
|
+
%a{pure}
|
|
61
103
|
def references: (*untyped) -> self
|
|
104
|
+
%a{pure}
|
|
62
105
|
def select: (*untyped) -> self
|
|
106
|
+
%a{pure}
|
|
63
107
|
def reselect: (*untyped) -> self
|
|
108
|
+
%a{pure}
|
|
64
109
|
def unscope: (*untyped) -> self
|
|
110
|
+
%a{pure}
|
|
65
111
|
def merge: (untyped) -> self
|
|
112
|
+
%a{pure}
|
|
66
113
|
def except: (*untyped) -> self
|
|
114
|
+
%a{pure}
|
|
67
115
|
def only: (*untyped) -> self
|
|
116
|
+
%a{pure}
|
|
68
117
|
def extending: (*untyped) -> self
|
|
118
|
+
%a{pure}
|
|
69
119
|
def from: (untyped, ?untyped) -> self
|
|
120
|
+
%a{pure}
|
|
70
121
|
def lock: (?untyped) -> self
|
|
122
|
+
%a{pure}
|
|
71
123
|
def readonly: (?untyped) -> self
|
|
124
|
+
%a{pure}
|
|
72
125
|
def strict_loading: (?untyped) -> self
|
|
126
|
+
%a{pure}
|
|
73
127
|
def create_with: (untyped) -> self
|
|
128
|
+
%a{pure}
|
|
74
129
|
def none: () -> self
|
|
130
|
+
%a{pure}
|
|
75
131
|
def all: () -> self
|
|
132
|
+
%a{pure}
|
|
76
133
|
def unscoped: () -> self
|
|
134
|
+
%a{pure}
|
|
77
135
|
def default_scoped: () -> self
|
|
136
|
+
%a{pure}
|
|
78
137
|
def annotate: (*untyped) -> self
|
|
138
|
+
%a{pure}
|
|
79
139
|
def optimizer_hints: (*untyped) -> self
|
|
140
|
+
%a{pure}
|
|
80
141
|
def excluding: (*untyped) -> self
|
|
142
|
+
%a{pure}
|
|
81
143
|
def without: (*untyped) -> self
|
|
144
|
+
%a{pure}
|
|
82
145
|
def with: (*untyped) -> self
|
|
83
146
|
def scoping: () { () -> untyped } -> untyped
|
|
84
147
|
|
|
85
148
|
# --- finders (return an element) ---
|
|
149
|
+
%a{rigor:v1:effect io.db.read}
|
|
86
150
|
def find: (*untyped) -> Elem
|
|
151
|
+
%a{rigor:v1:effect io.db.read}
|
|
87
152
|
def find_by: (*untyped) -> Elem?
|
|
153
|
+
%a{rigor:v1:effect io.db.read}
|
|
88
154
|
def find_by!: (*untyped) -> Elem
|
|
155
|
+
%a{rigor:v1:effect io.db.read}
|
|
89
156
|
def find_sole_by: (*untyped) -> Elem
|
|
157
|
+
%a{rigor:v1:effect io.db.read}
|
|
90
158
|
def sole: () -> Elem
|
|
159
|
+
%a{rigor:v1:effect io.db.read}
|
|
91
160
|
def first: () -> Elem?
|
|
92
161
|
| (Integer limit) -> Array[Elem]
|
|
162
|
+
%a{rigor:v1:effect io.db.read}
|
|
93
163
|
def first!: () -> Elem
|
|
164
|
+
%a{rigor:v1:effect io.db.read}
|
|
94
165
|
def last: () -> Elem?
|
|
95
166
|
| (Integer limit) -> Array[Elem]
|
|
167
|
+
%a{rigor:v1:effect io.db.read}
|
|
96
168
|
def last!: () -> Elem
|
|
169
|
+
%a{rigor:v1:effect io.db.read}
|
|
97
170
|
def take: () -> Elem?
|
|
98
171
|
| (Integer limit) -> Array[Elem]
|
|
172
|
+
%a{rigor:v1:effect io.db.read}
|
|
99
173
|
def take!: () -> Elem
|
|
174
|
+
%a{rigor:v1:effect io.db.read}
|
|
100
175
|
def second: () -> Elem?
|
|
176
|
+
%a{rigor:v1:effect io.db.read}
|
|
101
177
|
def third: () -> Elem?
|
|
178
|
+
%a{rigor:v1:effect io.db.read}
|
|
102
179
|
def fourth: () -> Elem?
|
|
180
|
+
%a{rigor:v1:effect io.db.read}
|
|
103
181
|
def fifth: () -> Elem?
|
|
182
|
+
%a{rigor:v1:effect io.db.read}
|
|
104
183
|
def forty_two: () -> Elem?
|
|
184
|
+
%a{rigor:v1:effect io.db.read}
|
|
105
185
|
def second_to_last: () -> Elem?
|
|
186
|
+
%a{rigor:v1:effect io.db.read}
|
|
106
187
|
def third_to_last: () -> Elem?
|
|
107
188
|
|
|
108
189
|
# --- batched enumeration ---
|
|
190
|
+
%a{rigor:v1:effect io.db.read}
|
|
109
191
|
def find_each: (**untyped) ?{ (Elem) -> void } -> untyped
|
|
192
|
+
%a{rigor:v1:effect io.db.read}
|
|
110
193
|
def find_in_batches: (**untyped) ?{ (Array[Elem]) -> void } -> untyped
|
|
194
|
+
%a{rigor:v1:effect io.db.read}
|
|
111
195
|
def in_batches: (**untyped) ?{ (ActiveRecord::Relation[Elem]) -> void } -> untyped
|
|
112
196
|
|
|
113
197
|
# --- scalars / aggregates ---
|
|
198
|
+
%a{rigor:v1:effect io.db.read}
|
|
114
199
|
def count: (*untyped) ?{ (Elem) -> untyped } -> untyped
|
|
200
|
+
%a{rigor:v1:effect io.db.read}
|
|
115
201
|
def sum: (*untyped) ?{ (Elem) -> untyped } -> untyped
|
|
202
|
+
%a{rigor:v1:effect io.db.read}
|
|
116
203
|
def average: (untyped) -> untyped
|
|
204
|
+
%a{rigor:v1:effect io.db.read}
|
|
117
205
|
def minimum: (untyped) -> untyped
|
|
206
|
+
%a{rigor:v1:effect io.db.read}
|
|
118
207
|
def maximum: (untyped) -> untyped
|
|
208
|
+
%a{rigor:v1:effect io.db.read}
|
|
119
209
|
def calculate: (*untyped) -> untyped
|
|
210
|
+
%a{rigor:v1:effect io.db.read}
|
|
120
211
|
def pluck: (*untyped) -> Array[untyped]
|
|
212
|
+
%a{rigor:v1:effect io.db.read}
|
|
121
213
|
def pick: (*untyped) -> untyped
|
|
214
|
+
%a{rigor:v1:effect io.db.read}
|
|
122
215
|
def ids: () -> Array[untyped]
|
|
216
|
+
%a{rigor:v1:effect io.db.read}
|
|
123
217
|
def size: () -> Integer
|
|
218
|
+
%a{rigor:v1:effect io.db.read}
|
|
124
219
|
def length: () -> Integer
|
|
220
|
+
%a{rigor:v1:effect io.db.read}
|
|
125
221
|
def empty?: () -> bool
|
|
222
|
+
%a{rigor:v1:effect io.db.read}
|
|
126
223
|
def any?: (*untyped) ?{ (Elem) -> boolish } -> bool
|
|
224
|
+
%a{rigor:v1:effect io.db.read}
|
|
127
225
|
def none?: (*untyped) ?{ (Elem) -> boolish } -> bool
|
|
226
|
+
%a{rigor:v1:effect io.db.read}
|
|
128
227
|
def one?: (*untyped) ?{ (Elem) -> boolish } -> bool
|
|
228
|
+
%a{rigor:v1:effect io.db.read}
|
|
129
229
|
def many?: () ?{ (Elem) -> boolish } -> bool
|
|
230
|
+
%a{rigor:v1:effect io.db.read}
|
|
130
231
|
def exists?: (*untyped) -> bool
|
|
232
|
+
%a{rigor:v1:effect io.db.read}
|
|
131
233
|
def include?: (untyped) -> bool
|
|
234
|
+
%a{rigor:v1:effect io.db.read}
|
|
132
235
|
def member?: (untyped) -> bool
|
|
133
236
|
|
|
134
237
|
# --- conversion / state ---
|
|
238
|
+
%a{rigor:v1:effect io.db.read}
|
|
135
239
|
def to_a: () -> Array[Elem]
|
|
240
|
+
%a{rigor:v1:effect io.db.read}
|
|
136
241
|
def to_ary: () -> Array[Elem]
|
|
242
|
+
%a{rigor:v1:effect io.db.read}
|
|
137
243
|
def entries: () -> Array[Elem]
|
|
244
|
+
%a{rigor:v1:effect io.db.read}
|
|
138
245
|
def load: (?untyped) -> self
|
|
246
|
+
%a{rigor:v1:effect io.db.read}
|
|
139
247
|
def reload: () -> self
|
|
248
|
+
%a{pure}
|
|
140
249
|
def reset: () -> self
|
|
250
|
+
%a{pure}
|
|
141
251
|
def loaded?: () -> bool
|
|
252
|
+
%a{pure}
|
|
142
253
|
def to_sql: () -> String
|
|
254
|
+
%a{rigor:v1:effect io.db.read}
|
|
143
255
|
def explain: (*untyped) -> String
|
|
256
|
+
%a{rigor:v1:effect io.db.read}
|
|
144
257
|
def cache_key: (?untyped) -> String
|
|
258
|
+
%a{rigor:v1:effect io.db.read}
|
|
145
259
|
def cache_version: (?untyped) -> untyped
|
|
260
|
+
%a{rigor:v1:effect io.db.read}
|
|
146
261
|
def to_json: (*untyped) -> String
|
|
262
|
+
%a{rigor:v1:effect io.db.read}
|
|
147
263
|
def as_json: (?untyped) -> untyped
|
|
264
|
+
%a{rigor:v1:effect io.db.read}
|
|
148
265
|
def blank?: () -> bool
|
|
266
|
+
%a{rigor:v1:effect io.db.read}
|
|
149
267
|
def present?: () -> bool
|
|
268
|
+
%a{pure}
|
|
150
269
|
def klass: () -> untyped
|
|
270
|
+
%a{pure}
|
|
151
271
|
def model: () -> untyped
|
|
272
|
+
%a{pure}
|
|
152
273
|
def table: () -> untyped
|
|
274
|
+
%a{pure}
|
|
153
275
|
def arel: () -> untyped
|
|
154
276
|
|
|
155
277
|
# --- whole-relation persistence ---
|
|
278
|
+
%a{rigor:v1:effect io.db.write}
|
|
156
279
|
def update_all: (untyped) -> Integer
|
|
280
|
+
%a{rigor:v1:effect io.db.write}
|
|
157
281
|
def delete_all: () -> Integer
|
|
282
|
+
%a{rigor:v1:effect io.db.write}
|
|
158
283
|
def destroy_all: () -> Array[Elem]
|
|
284
|
+
%a{rigor:v1:effect io.db.write}
|
|
159
285
|
def update: (*untyped) -> untyped
|
|
286
|
+
%a{rigor:v1:effect io.db.write}
|
|
160
287
|
def update!: (*untyped) -> untyped
|
|
288
|
+
%a{rigor:v1:effect io.db.write}
|
|
161
289
|
def delete_by: (*untyped) -> Integer
|
|
290
|
+
%a{rigor:v1:effect io.db.write}
|
|
162
291
|
def destroy_by: (*untyped) -> Array[Elem]
|
|
292
|
+
%a{rigor:v1:effect io.db.write}
|
|
163
293
|
def touch_all: (*untyped) -> Integer
|
|
294
|
+
%a{rigor:v1:effect io.db.write}
|
|
164
295
|
def insert_all: (untyped, **untyped) -> untyped
|
|
296
|
+
%a{rigor:v1:effect io.db.write}
|
|
165
297
|
def insert_all!: (untyped, **untyped) -> untyped
|
|
298
|
+
%a{rigor:v1:effect io.db.write}
|
|
166
299
|
def upsert_all: (untyped, **untyped) -> untyped
|
|
167
300
|
|
|
168
301
|
# --- instantiating builders (return an element) ---
|
|
302
|
+
%a{pure}
|
|
169
303
|
def new: (*untyped) ?{ (Elem) -> void } -> Elem
|
|
304
|
+
%a{pure}
|
|
170
305
|
def build: (*untyped) ?{ (Elem) -> void } -> Elem
|
|
306
|
+
%a{rigor:v1:effect io.db.write}
|
|
171
307
|
def create: (*untyped) ?{ (Elem) -> void } -> Elem
|
|
308
|
+
%a{rigor:v1:effect io.db.write}
|
|
172
309
|
def create!: (*untyped) ?{ (Elem) -> void } -> Elem
|
|
310
|
+
%a{rigor:v1:effect io.db.write}
|
|
173
311
|
def find_or_create_by: (untyped) ?{ (Elem) -> void } -> Elem
|
|
312
|
+
%a{rigor:v1:effect io.db.write}
|
|
174
313
|
def find_or_create_by!: (untyped) ?{ (Elem) -> void } -> Elem
|
|
314
|
+
%a{rigor:v1:effect io.db.read}
|
|
175
315
|
def find_or_initialize_by: (untyped) ?{ (Elem) -> void } -> Elem
|
|
316
|
+
%a{rigor:v1:effect io.db.write}
|
|
176
317
|
def create_or_find_by: (untyped) ?{ (Elem) -> void } -> Elem
|
|
318
|
+
%a{rigor:v1:effect io.db.write}
|
|
177
319
|
def create_or_find_by!: (untyped) ?{ (Elem) -> void } -> Elem
|
|
320
|
+
%a{rigor:v1:effect io.db.write}
|
|
178
321
|
def first_or_create: (?untyped) ?{ (Elem) -> void } -> Elem
|
|
322
|
+
%a{rigor:v1:effect io.db.write}
|
|
179
323
|
def first_or_create!: (?untyped) ?{ (Elem) -> void } -> Elem
|
|
324
|
+
%a{rigor:v1:effect io.db.read}
|
|
180
325
|
def first_or_initialize: (?untyped) ?{ (Elem) -> void } -> Elem
|
|
181
326
|
end
|
|
182
327
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
class Activestorage < Rigor::Plugin::Base
|
|
8
|
+
# rigor-activestorage's effect contract (ADR-103 WD10; design note § 11.2; issue #387).
|
|
9
|
+
#
|
|
10
|
+
# ActiveStorage is the design note's argument for framework labels in miniature: the *transport* is
|
|
11
|
+
# whatever `config/storage.yml` names — the local disk, S3, GCS — and is therefore statically
|
|
12
|
+
# unknowable, while the *operation* is fixed. So every row carries bare `io` for the transport and
|
|
13
|
+
# `rails.activestorage.read` / `.write` for the meaning, which is the half a policy can grip.
|
|
14
|
+
#
|
|
15
|
+
# Almost every write is also an `io.db.write`: an attachment is a row in `active_storage_attachments`
|
|
16
|
+
# and a blob is a row in `active_storage_blobs`, so `user.avatar.attach(io)` really does touch the
|
|
17
|
+
# database whether or not the bytes go to S3. A "no database writes on this path" envelope is right
|
|
18
|
+
# to object, and would be wrong not to.
|
|
19
|
+
module Effects
|
|
20
|
+
ATTACHED_ONE = "ActiveStorage::Attached::One"
|
|
21
|
+
ATTACHED_MANY = "ActiveStorage::Attached::Many"
|
|
22
|
+
BLOB = "ActiveStorage::Blob"
|
|
23
|
+
ATTACHMENT = "ActiveStorage::Attachment"
|
|
24
|
+
|
|
25
|
+
READ = ["io", "rails.activestorage.read"].freeze
|
|
26
|
+
WRITE = ["io", "rails.activestorage.write", "io.db.write"].freeze
|
|
27
|
+
# `download` / `open` pull bytes out of the service and touch no row.
|
|
28
|
+
PURE_READ = ["io", "rails.activestorage.read"].freeze
|
|
29
|
+
|
|
30
|
+
ATTACH_TARGETS = [ATTACHED_ONE, ATTACHED_MANY].freeze
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
def attributions
|
|
35
|
+
attach_rows + blob_rows
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def attach_rows
|
|
39
|
+
ATTACH_TARGETS.flat_map do |receiver|
|
|
40
|
+
[
|
|
41
|
+
row(receiver, :attach, WRITE,
|
|
42
|
+
"uploads the bytes to the configured service AND inserts the attachment + blob rows"),
|
|
43
|
+
row(receiver, :purge, WRITE, "deletes the stored file and the attachment + blob rows"),
|
|
44
|
+
row(receiver, :purge_later, WRITE,
|
|
45
|
+
"enqueues the purge; the attachment row is detached now, so the database write is here"),
|
|
46
|
+
row(receiver, :detach, ["io.db.write"], "removes the attachment row and touches no service"),
|
|
47
|
+
row(receiver, :download, PURE_READ, "pulls the bytes back out of the service"),
|
|
48
|
+
row(receiver, :open, PURE_READ, "streams the bytes into a tempfile")
|
|
49
|
+
]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def blob_rows
|
|
54
|
+
[
|
|
55
|
+
row(BLOB, :download, PURE_READ, "reads the object out of the storage service"),
|
|
56
|
+
row(BLOB, :open, PURE_READ, "streams the object into a tempfile"),
|
|
57
|
+
row(BLOB, :upload, WRITE, "writes the object to the service and records its checksum"),
|
|
58
|
+
row(BLOB, :purge, WRITE, "deletes the object and its row"),
|
|
59
|
+
row(BLOB, :purge_later, WRITE, "enqueues the object's deletion; the row goes now"),
|
|
60
|
+
row(BLOB, :create_and_upload!, WRITE, "inserts the blob row and uploads in one call",
|
|
61
|
+
singleton: true),
|
|
62
|
+
row(BLOB, :url, READ, "signs a URL against the service, which for some services is a request"),
|
|
63
|
+
row(ATTACHMENT, :purge, WRITE, "deletes the stored file and the attachment row"),
|
|
64
|
+
row(ATTACHMENT, :download, PURE_READ, "reads the attached object out of the service")
|
|
65
|
+
]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def row(receiver, selector, labels, why, singleton: false)
|
|
69
|
+
EffectAttribution.new(receiver: receiver, method: selector, labels: labels,
|
|
70
|
+
singleton: singleton, discharge: true,
|
|
71
|
+
why: "#{why}; the transport is bare `io` because `config/storage.yml` " \
|
|
72
|
+
"decides it per environment and no static reading can")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -5,6 +5,7 @@ require "rigor/plugin"
|
|
|
5
5
|
require_relative "activestorage/attachment_discoverer"
|
|
6
6
|
require_relative "activestorage/attachment_index"
|
|
7
7
|
require_relative "activestorage/analyzer"
|
|
8
|
+
require_relative "activestorage/effects"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Plugin
|
|
@@ -41,7 +42,11 @@ module Rigor
|
|
|
41
42
|
config_schema: {
|
|
42
43
|
"model_search_paths" => { kind: :array, default: ["app/models"] }
|
|
43
44
|
},
|
|
44
|
-
consumes: [{ plugin_id: "activerecord", name: :model_index, optional: true }]
|
|
45
|
+
consumes: [{ plugin_id: "activerecord", name: :model_index, optional: true }],
|
|
46
|
+
# ADR-103 WD10 (#387) — see {Effects} for what each row is and why.
|
|
47
|
+
effect_root: "rails",
|
|
48
|
+
effect_labels: %w[rails.activestorage.read rails.activestorage.write],
|
|
49
|
+
effect_attributions: Effects.attributions
|
|
45
50
|
)
|
|
46
51
|
|
|
47
52
|
# Cached: attachment index. Walks every `.rb` file under `model_search_paths` for `has_*_attached`
|