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
|
@@ -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`
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
class ActivesupportCoreExt < Rigor::Plugin::Base
|
|
8
|
+
# rigor-activesupport-core-ext's effect contract (ADR-103 WD10; design note § 11.2; issue #387).
|
|
9
|
+
#
|
|
10
|
+
# **Scope note.** This is the *impure* half of ActiveSupport only: the clock, the notification bus and
|
|
11
|
+
# `CurrentAttributes`. The `%a{pure}` sweep over `blank?` / `present?` / `deep_dup` and the rest of
|
|
12
|
+
# the core_ext predicate surface — the single cheapest purity win in a Rails app, per WD10 — is issue
|
|
13
|
+
# #388 and lands in this plugin's shipped RBS, not here (`try` was audited and skipped: it dispatches
|
|
14
|
+
# to whatever method the caller names, so its purity is a fact about the call site, not about `try`).
|
|
15
|
+
# One row below (`in_time_zone_row`) is #388's own addition: `DateTime#in_time_zone` reads `Time.zone`
|
|
16
|
+
# through a default argument, a gap the RBS sweep found but can only fix here.
|
|
17
|
+
#
|
|
18
|
+
# ## The clock
|
|
19
|
+
#
|
|
20
|
+
# `Time.current` and friends are `nondet.time`, plus `global.read` for the zone: `Time.zone` is
|
|
21
|
+
# process state that `Time.use_zone` and the per-request `around_action` both change. The distinction
|
|
22
|
+
# matters for the purity policy of ADR-103 WD9 — a method that calls `Time.current` may never have its
|
|
23
|
+
# result remembered, and that is the whole reason the label exists.
|
|
24
|
+
#
|
|
25
|
+
# ## `CurrentAttributes`
|
|
26
|
+
#
|
|
27
|
+
# `Current.user` is fiber-local mutable state: `global.read` on the way in, `global.write` on the way
|
|
28
|
+
# out, with `rails.current.read` / `.write` as the meaning. Rigor cannot name the attributes per app
|
|
29
|
+
# — they come from an `attribute :user` macro in the project's own `Current` class — so the rows are
|
|
30
|
+
# keyed on the framework base class and reach every project subclass through inheritance. `reset` and
|
|
31
|
+
# `set` are named explicitly because they are the ones a reviewer looks for.
|
|
32
|
+
module Effects
|
|
33
|
+
TIME = "Time"
|
|
34
|
+
DATE = "Date"
|
|
35
|
+
DATETIME = "DateTime"
|
|
36
|
+
ZONE = "Time.zone"
|
|
37
|
+
CURRENT = "ActiveSupport::CurrentAttributes"
|
|
38
|
+
NOTIFICATIONS = "ActiveSupport::Notifications"
|
|
39
|
+
|
|
40
|
+
CLOCK = ["nondet.time", "global.read"].freeze
|
|
41
|
+
CLOCK_NO_ZONE = ["nondet.time"].freeze
|
|
42
|
+
ZONE_READ = ["global.read"].freeze
|
|
43
|
+
CURRENT_READ = ["global.read", "rails.current.read"].freeze
|
|
44
|
+
CURRENT_WRITE = ["global.write", "rails.current.write"].freeze
|
|
45
|
+
TELEMETRY = %w[io telemetry].freeze
|
|
46
|
+
|
|
47
|
+
# Zone-aware clock readers: they consult `Time.zone`, which is mutable process state.
|
|
48
|
+
ZONED_NOW = %w[current].freeze
|
|
49
|
+
# Zone-blind ones. `Time.now` / `Date.today` are already catalogue rows for core Ruby; these are the
|
|
50
|
+
# ActiveSupport additions.
|
|
51
|
+
ZONE_BLIND = %w[].freeze
|
|
52
|
+
|
|
53
|
+
module_function
|
|
54
|
+
|
|
55
|
+
def attributions
|
|
56
|
+
clock_rows + zone_rows + current_rows + notification_rows + [in_time_zone_row]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def clock_rows
|
|
60
|
+
[
|
|
61
|
+
row(TIME, :current, CLOCK, singleton: true,
|
|
62
|
+
why: "reads the clock AND `Time.zone`, which `Time.use_zone` and a per-request " \
|
|
63
|
+
"`around_action` both rewrite"),
|
|
64
|
+
row(DATE, :current, CLOCK, singleton: true, why: "the same, to day precision"),
|
|
65
|
+
row(DATE, :yesterday, CLOCK, singleton: true, why: "`Date.current - 1`"),
|
|
66
|
+
row(DATE, :tomorrow, CLOCK, singleton: true, why: "`Date.current + 1`"),
|
|
67
|
+
row(DATETIME, :current, CLOCK, singleton: true, why: "the DateTime spelling of `Time.current`"),
|
|
68
|
+
row("ActiveSupport::TimeZone", :now, CLOCK_NO_ZONE, singleton: true,
|
|
69
|
+
why: "the zone is the receiver here, so only the clock read is left"),
|
|
70
|
+
row(ZONE, :now, CLOCK_NO_ZONE, why: "`Time.zone.now` — the zone is named, the clock is read"),
|
|
71
|
+
row(ZONE, :today, CLOCK_NO_ZONE, why: "`Time.zone.today`")
|
|
72
|
+
] + duration_rows
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# `2.days.ago` / `3.hours.from_now`. The receiver is an `ActiveSupport::Duration`; the call reads
|
|
76
|
+
# the clock, which is what makes `created_at > 7.days.ago` a `nondet.time` expression and not a
|
|
77
|
+
# constant.
|
|
78
|
+
def duration_rows
|
|
79
|
+
%w[ago until since from_now after before].map do |selector|
|
|
80
|
+
row("ActiveSupport::Duration", selector, CLOCK,
|
|
81
|
+
why: "`n.days.ago` and its family read the clock (and the zone) at the call — this is what " \
|
|
82
|
+
"stops a duration comparison folding to a constant")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def zone_rows
|
|
87
|
+
[
|
|
88
|
+
row(TIME, :zone, ZONE_READ, singleton: true,
|
|
89
|
+
why: "reads the process/fiber's current time zone, which is mutable"),
|
|
90
|
+
row(TIME, :zone=, ["global.write"], singleton: true,
|
|
91
|
+
why: "rewrites the process/fiber's time zone for everything downstream"),
|
|
92
|
+
row(TIME, :use_zone, ["global.write"], singleton: true,
|
|
93
|
+
why: "swaps the zone around a block; the block's own origins join by containment")
|
|
94
|
+
]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# `DateTime#in_time_zone` — a gap the #388 `%a{pure}` sweep over `sig/active_support/core_ext.rbs`
|
|
98
|
+
# surfaced: `in_time_zone(zone = ::Time.zone)` reads `Time.zone` through its own default argument
|
|
99
|
+
# whenever the caller doesn't pass one explicitly, so it cannot be annotated `%a{pure}` there. It
|
|
100
|
+
# belongs here rather than in the RBS because the label is `global.read` alone (not the
|
|
101
|
+
# `nondet.time` + `global.read` pair `CLOCK` carries) — `in_time_zone` converts an already-fixed
|
|
102
|
+
# instant into a zone, it does not read the clock itself.
|
|
103
|
+
def in_time_zone_row
|
|
104
|
+
row("DateTime", :in_time_zone, ZONE_READ,
|
|
105
|
+
why: "the default argument reads `Time.zone` when the caller doesn't name one explicitly")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def current_rows
|
|
109
|
+
[
|
|
110
|
+
row(CURRENT, :set, CURRENT_WRITE, singleton: true,
|
|
111
|
+
why: "writes fiber-local request state that every later read in the request sees"),
|
|
112
|
+
row(CURRENT, :reset, CURRENT_WRITE, singleton: true, why: "clears the same fiber-local state"),
|
|
113
|
+
row(CURRENT, :attributes, CURRENT_READ, singleton: true, why: "reads the whole bag"),
|
|
114
|
+
row(CURRENT, :attributes=, CURRENT_WRITE, singleton: true, why: "replaces the whole bag"),
|
|
115
|
+
row(CURRENT, :instance, CURRENT_READ, singleton: true,
|
|
116
|
+
why: "materialises the per-fiber instance the accessors read through")
|
|
117
|
+
]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def notification_rows
|
|
121
|
+
[
|
|
122
|
+
EffectAttribution.new(
|
|
123
|
+
receiver: NOTIFICATIONS, method: :instrument, labels: TELEMETRY, singleton: true,
|
|
124
|
+
discharge: true, taint: "opaque-callable",
|
|
125
|
+
why: "publishes to the notification bus — `io` because a subscriber may do anything and " \
|
|
126
|
+
"`telemetry` because that is the meaning. The taint is the honest half: the SUBSCRIBERS " \
|
|
127
|
+
"are registered at runtime and the analyzer cannot see them, so the summary reads " \
|
|
128
|
+
"'this much, and possibly more'"
|
|
129
|
+
),
|
|
130
|
+
row(NOTIFICATIONS, :publish, TELEMETRY, singleton: true, why: "the bare publish"),
|
|
131
|
+
row(NOTIFICATIONS, :subscribe, ["mutate.static"], singleton: true,
|
|
132
|
+
why: "registers a subscriber on process-global state")
|
|
133
|
+
]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def row(receiver, selector, labels, why:, singleton: false)
|
|
137
|
+
EffectAttribution.new(receiver: receiver, method: selector, labels: labels,
|
|
138
|
+
singleton: singleton, discharge: true, why: why)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "rigor/plugin"
|
|
4
4
|
|
|
5
|
+
require_relative "activesupport_core_ext/effects"
|
|
6
|
+
|
|
5
7
|
module Rigor
|
|
6
8
|
module Plugin
|
|
7
9
|
# ADR-25 — a pure RBS-bundle plugin. It ships NO analyzer code: no `diagnostics_for_file`, no
|
|
@@ -24,7 +26,13 @@ module Rigor
|
|
|
24
26
|
# aliases that return Time, not Date).
|
|
25
27
|
version: "0.2.0",
|
|
26
28
|
description: "RBS bundle for the most-frequently-flagged ActiveSupport core_ext extensions.",
|
|
27
|
-
signature_paths: ["sig"]
|
|
29
|
+
signature_paths: ["sig"],
|
|
30
|
+
# ADR-103 WD10 (#387) — the IMPURE half of ActiveSupport: the clock, the notification bus and
|
|
31
|
+
# `CurrentAttributes`. The `%a{pure}` sweep over the predicate surface is issue #388 and lands in
|
|
32
|
+
# `sig/`, not here. See {Effects}.
|
|
33
|
+
effect_root: "rails",
|
|
34
|
+
effect_labels: %w[rails.current.read rails.current.write],
|
|
35
|
+
effect_attributions: Effects.attributions
|
|
28
36
|
)
|
|
29
37
|
end
|
|
30
38
|
|