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.
Files changed (179) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/array.yml +11 -24
  4. data/data/builtins/ruby_core/enumerable.yml +4 -8
  5. data/data/builtins/ruby_core/exception.yml +6 -9
  6. data/data/builtins/ruby_core/hash.yml +13 -20
  7. data/data/builtins/ruby_core/io.yml +6 -9
  8. data/data/builtins/ruby_core/numeric.yml +2 -3
  9. data/data/builtins/ruby_core/proc.yml +4 -8
  10. data/data/builtins/ruby_core/re.yml +6 -9
  11. data/data/builtins/ruby_core/set.yml +2 -3
  12. data/data/builtins/ruby_core/string.yml +4 -7
  13. data/data/builtins/ruby_core/struct.yml +0 -6
  14. data/data/builtins/ruby_core/time.yml +3 -6
  15. data/data/effects/core.yml +843 -0
  16. data/data/effects/registry.yml +67 -0
  17. data/docs/handbook/07-rbs-and-extended.md +106 -5
  18. data/docs/manual/02-cli-reference.md +311 -1
  19. data/docs/manual/03-configuration.md +84 -1
  20. data/docs/manual/04-diagnostics.md +14 -2
  21. data/docs/manual/16-rbs-extended-annotations.md +77 -0
  22. data/docs/manual/18-removing-dead-code.md +326 -0
  23. data/docs/manual/README.md +5 -0
  24. data/docs/manual/plugins/rigor-actionmailer.md +5 -0
  25. data/docs/manual/plugins/rigor-activejob.md +47 -0
  26. data/docs/manual/plugins/rigor-factorybot.md +16 -0
  27. data/docs/manual/plugins/rigor-pundit.md +26 -0
  28. data/docs/manual/plugins/rigor-rails-routes.md +41 -2
  29. data/docs/manual/plugins/rigor-rspec-rails.md +5 -0
  30. data/docs/manual/plugins/rigor-rspec.md +8 -0
  31. data/docs/manual/plugins/rigor-sidekiq.md +54 -0
  32. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
  33. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
  34. data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
  35. data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
  36. data/lib/rigor/analysis/check_rules.rb +62 -22
  37. data/lib/rigor/analysis/incremental_session.rb +140 -6
  38. data/lib/rigor/analysis/reachability/graph.rb +244 -0
  39. data/lib/rigor/analysis/reachability/plugin_roots.rb +164 -0
  40. data/lib/rigor/analysis/reachability/project_files.rb +58 -0
  41. data/lib/rigor/analysis/reachability/scan.rb +241 -0
  42. data/lib/rigor/analysis/reachability/signature_scan.rb +134 -0
  43. data/lib/rigor/analysis/rule_catalog.rb +163 -1
  44. data/lib/rigor/analysis/run_cache_key.rb +16 -0
  45. data/lib/rigor/analysis/runner/declaration_position.rb +54 -0
  46. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
  47. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
  48. data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
  49. data/lib/rigor/analysis/runner/pool_coordinator.rb +68 -10
  50. data/lib/rigor/analysis/runner.rb +420 -10
  51. data/lib/rigor/analysis/severity_stamp.rb +2 -1
  52. data/lib/rigor/analysis/worker_session.rb +58 -2
  53. data/lib/rigor/bleeding_edge.rb +22 -0
  54. data/lib/rigor/cache/incremental_snapshot.rb +24 -5
  55. data/lib/rigor/cli/check_command.rb +13 -1
  56. data/lib/rigor/cli/check_runner_factory.rb +4 -1
  57. data/lib/rigor/cli/effects_command.rb +131 -0
  58. data/lib/rigor/cli/effects_diff_renderer.rb +123 -0
  59. data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
  60. data/lib/rigor/cli/effects_renderer.rb +54 -0
  61. data/lib/rigor/cli/effects_report.rb +53 -0
  62. data/lib/rigor/cli/effects_snapshot_command.rb +256 -0
  63. data/lib/rigor/cli/unused_command.rb +288 -0
  64. data/lib/rigor/cli.rb +16 -0
  65. data/lib/rigor/configuration/severity_profile.rb +23 -0
  66. data/lib/rigor/configuration.rb +296 -14
  67. data/lib/rigor/effects/attribution.rb +76 -0
  68. data/lib/rigor/effects/catalog.rb +275 -0
  69. data/lib/rigor/effects/collector.rb +182 -0
  70. data/lib/rigor/effects/config_envelopes.rb +185 -0
  71. data/lib/rigor/effects/discharge.rb +69 -0
  72. data/lib/rigor/effects/effect_table.rb +92 -0
  73. data/lib/rigor/effects/entry_points.rb +93 -0
  74. data/lib/rigor/effects/envelope.rb +86 -0
  75. data/lib/rigor/effects/envelope_check.rb +172 -0
  76. data/lib/rigor/effects/envelope_index.rb +157 -0
  77. data/lib/rigor/effects/file_collection.rb +160 -0
  78. data/lib/rigor/effects/framework_units.rb +167 -0
  79. data/lib/rigor/effects/identity.rb +104 -0
  80. data/lib/rigor/effects/label.rb +77 -0
  81. data/lib/rigor/effects/label_intent.rb +73 -0
  82. data/lib/rigor/effects/label_set.rb +136 -0
  83. data/lib/rigor/effects/liskov_check.rb +167 -0
  84. data/lib/rigor/effects/local_ownership.rb +132 -0
  85. data/lib/rigor/effects/method_key.rb +40 -0
  86. data/lib/rigor/effects/mutation_classifier.rb +92 -0
  87. data/lib/rigor/effects/narrowing.rb +202 -0
  88. data/lib/rigor/effects/origin.rb +66 -0
  89. data/lib/rigor/effects/path_finder.rb +89 -0
  90. data/lib/rigor/effects/plugin_facts.rb +335 -0
  91. data/lib/rigor/effects/propagator.rb +275 -0
  92. data/lib/rigor/effects/registry.rb +193 -0
  93. data/lib/rigor/effects/scanner.rb +290 -0
  94. data/lib/rigor/effects/signature_sources.rb +74 -0
  95. data/lib/rigor/effects/snapshot.rb +380 -0
  96. data/lib/rigor/effects/snapshot_diff.rb +265 -0
  97. data/lib/rigor/effects/summary.rb +154 -0
  98. data/lib/rigor/effects/taint_cause.rb +38 -0
  99. data/lib/rigor/effects/unit_scan.rb +572 -0
  100. data/lib/rigor/effects/unknown_label_check.rb +86 -0
  101. data/lib/rigor/effects/unknown_label_report.rb +59 -0
  102. data/lib/rigor/effects.rb +47 -0
  103. data/lib/rigor/environment/rbs_loader.rb +34 -0
  104. data/lib/rigor/environment.rb +8 -1
  105. data/lib/rigor/flow_contribution/element.rb +1 -0
  106. data/lib/rigor/flow_contribution/merge_result.rb +5 -3
  107. data/lib/rigor/flow_contribution/merger.rb +16 -1
  108. data/lib/rigor/flow_contribution.rb +20 -4
  109. data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
  110. data/lib/rigor/inference/expression_typer.rb +74 -17
  111. data/lib/rigor/inference/fork_map.rb +45 -11
  112. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +147 -17
  113. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +97 -2
  114. data/lib/rigor/inference/method_dispatcher.rb +41 -10
  115. data/lib/rigor/inference/optimistic_origin.rb +68 -3
  116. data/lib/rigor/inference/pre_eval_constants.rb +189 -0
  117. data/lib/rigor/inference/scope_indexer.rb +134 -31
  118. data/lib/rigor/inference/singleton_object_constant.rb +63 -0
  119. data/lib/rigor/inference/statement_evaluator.rb +25 -12
  120. data/lib/rigor/plugin/base.rb +24 -0
  121. data/lib/rigor/plugin/effect_attribution.rb +208 -0
  122. data/lib/rigor/plugin/effect_edge.rb +101 -0
  123. data/lib/rigor/plugin/effect_entry_points.rb +51 -0
  124. data/lib/rigor/plugin/first_party.rb +57 -0
  125. data/lib/rigor/plugin/isolation.rb +4 -1
  126. data/lib/rigor/plugin/manifest.rb +103 -3
  127. data/lib/rigor/plugin/node_rule_walk.rb +6 -0
  128. data/lib/rigor/plugin/registry.rb +48 -0
  129. data/lib/rigor/plugin.rb +1 -0
  130. data/lib/rigor/protection/closure_kill_oracle.rb +20 -1
  131. data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
  132. data/lib/rigor/rbs_extended.rb +168 -0
  133. data/lib/rigor/reflection.rb +131 -30
  134. data/lib/rigor/scope.rb +62 -2
  135. data/lib/rigor/source/node_walker.rb +12 -0
  136. data/lib/rigor/type/anonymous_class_name.rb +40 -0
  137. data/lib/rigor/type/nominal.rb +5 -2
  138. data/lib/rigor/type/singleton.rb +4 -1
  139. data/lib/rigor/version.rb +1 -1
  140. data/lib/rigor.rb +1 -0
  141. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
  142. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
  143. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
  144. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
  145. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
  146. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
  147. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
  148. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
  149. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
  150. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
  151. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
  152. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
  153. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
  154. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
  155. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
  156. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
  157. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +385 -28
  158. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
  159. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
  160. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
  161. data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
  162. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
  163. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
  164. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
  165. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
  166. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
  167. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
  168. data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
  169. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
  170. data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
  171. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
  172. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +56 -3
  173. data/sig/rigor/analysis/fact_store.rbs +1 -0
  174. data/sig/rigor/inference.rbs +2 -0
  175. data/sig/rigor/rbs_extended.rbs +6 -0
  176. data/sig/rigor/scope.rbs +4 -0
  177. data/sig/rigor.rbs +21 -1
  178. data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
  179. 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