rigortype 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) 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 +860 -0
  16. data/data/effects/registry.yml +95 -0
  17. data/docs/handbook/07-rbs-and-extended.md +106 -5
  18. data/docs/manual/02-cli-reference.md +359 -2
  19. data/docs/manual/03-configuration.md +101 -1
  20. data/docs/manual/04-diagnostics.md +8 -3
  21. data/docs/manual/11-ci.md +37 -0
  22. data/docs/manual/12-caching.md +39 -0
  23. data/docs/manual/16-rbs-extended-annotations.md +90 -0
  24. data/docs/manual/18-removing-dead-code.md +326 -0
  25. data/docs/manual/19-effect-labels.md +671 -0
  26. data/docs/manual/README.md +10 -0
  27. data/docs/manual/ci-templates/README.md +9 -0
  28. data/docs/manual/plugins/rigor-actionmailer.md +5 -0
  29. data/docs/manual/plugins/rigor-activejob.md +47 -0
  30. data/docs/manual/plugins/rigor-factorybot.md +16 -0
  31. data/docs/manual/plugins/rigor-pundit.md +26 -0
  32. data/docs/manual/plugins/rigor-rails-routes.md +41 -2
  33. data/docs/manual/plugins/rigor-rspec-rails.md +5 -0
  34. data/docs/manual/plugins/rigor-rspec.md +8 -0
  35. data/docs/manual/plugins/rigor-sidekiq.md +54 -0
  36. data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
  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 +168 -3
  44. data/lib/rigor/analysis/run_cache_key.rb +16 -0
  45. data/lib/rigor/analysis/run_cache_probe.rb +69 -1
  46. data/lib/rigor/analysis/runner/declaration_position.rb +38 -0
  47. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
  48. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
  49. data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
  50. data/lib/rigor/analysis/runner/pool_coordinator.rb +93 -10
  51. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
  52. data/lib/rigor/analysis/runner.rb +429 -10
  53. data/lib/rigor/analysis/worker_session.rb +60 -2
  54. data/lib/rigor/bleeding_edge.rb +22 -0
  55. data/lib/rigor/cache/incremental_snapshot.rb +24 -5
  56. data/lib/rigor/cli/check_command.rb +13 -1
  57. data/lib/rigor/cli/check_runner_factory.rb +4 -1
  58. data/lib/rigor/cli/effects_command.rb +245 -0
  59. data/lib/rigor/cli/effects_diff_renderer.rb +127 -0
  60. data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
  61. data/lib/rigor/cli/effects_renderer.rb +92 -0
  62. data/lib/rigor/cli/effects_report.rb +163 -0
  63. data/lib/rigor/cli/effects_snapshot_command.rb +275 -0
  64. data/lib/rigor/cli/unused_command.rb +288 -0
  65. data/lib/rigor/cli.rb +28 -1
  66. data/lib/rigor/configuration/severity_profile.rb +23 -0
  67. data/lib/rigor/configuration.rb +322 -28
  68. data/lib/rigor/configuration_error.rb +20 -0
  69. data/lib/rigor/effects/attribution.rb +76 -0
  70. data/lib/rigor/effects/catalog.rb +275 -0
  71. data/lib/rigor/effects/collector.rb +219 -0
  72. data/lib/rigor/effects/config_envelopes.rb +185 -0
  73. data/lib/rigor/effects/discharge.rb +69 -0
  74. data/lib/rigor/effects/effect_table.rb +92 -0
  75. data/lib/rigor/effects/entry_points.rb +139 -0
  76. data/lib/rigor/effects/envelope.rb +86 -0
  77. data/lib/rigor/effects/envelope_check.rb +172 -0
  78. data/lib/rigor/effects/envelope_index.rb +157 -0
  79. data/lib/rigor/effects/file_collection.rb +174 -0
  80. data/lib/rigor/effects/framework_units.rb +222 -0
  81. data/lib/rigor/effects/identity.rb +104 -0
  82. data/lib/rigor/effects/inline_anchor.rb +134 -0
  83. data/lib/rigor/effects/label.rb +77 -0
  84. data/lib/rigor/effects/label_intent.rb +73 -0
  85. data/lib/rigor/effects/label_set.rb +136 -0
  86. data/lib/rigor/effects/liskov_check.rb +167 -0
  87. data/lib/rigor/effects/local_ownership.rb +132 -0
  88. data/lib/rigor/effects/method_key.rb +40 -0
  89. data/lib/rigor/effects/mutation_classifier.rb +92 -0
  90. data/lib/rigor/effects/narrowing.rb +202 -0
  91. data/lib/rigor/effects/origin.rb +66 -0
  92. data/lib/rigor/effects/path_finder.rb +89 -0
  93. data/lib/rigor/effects/plugin_facts.rb +384 -0
  94. data/lib/rigor/effects/propagator.rb +335 -0
  95. data/lib/rigor/effects/registry.rb +200 -0
  96. data/lib/rigor/effects/scanner.rb +302 -0
  97. data/lib/rigor/effects/signature_sources.rb +90 -0
  98. data/lib/rigor/effects/snapshot.rb +396 -0
  99. data/lib/rigor/effects/snapshot_diff.rb +265 -0
  100. data/lib/rigor/effects/summary.rb +154 -0
  101. data/lib/rigor/effects/taint_cause.rb +39 -0
  102. data/lib/rigor/effects/unit_scan.rb +647 -0
  103. data/lib/rigor/effects/unknown_label_check.rb +86 -0
  104. data/lib/rigor/effects/unknown_label_report.rb +59 -0
  105. data/lib/rigor/effects.rb +47 -0
  106. data/lib/rigor/environment/rbs_loader.rb +34 -0
  107. data/lib/rigor/environment.rb +8 -1
  108. data/lib/rigor/flow_contribution/element.rb +1 -0
  109. data/lib/rigor/flow_contribution/merge_result.rb +5 -3
  110. data/lib/rigor/flow_contribution/merger.rb +16 -1
  111. data/lib/rigor/flow_contribution.rb +20 -4
  112. data/lib/rigor/inference/expression_typer.rb +18 -4
  113. data/lib/rigor/inference/pre_eval_constants.rb +189 -0
  114. data/lib/rigor/inference/scope_indexer.rb +15 -3
  115. data/lib/rigor/plugin/base.rb +28 -0
  116. data/lib/rigor/plugin/box.rb +18 -2
  117. data/lib/rigor/plugin/effect_ancestry.rb +80 -0
  118. data/lib/rigor/plugin/effect_attribution.rb +208 -0
  119. data/lib/rigor/plugin/effect_edge.rb +101 -0
  120. data/lib/rigor/plugin/effect_entry_points.rb +51 -0
  121. data/lib/rigor/plugin/first_party.rb +57 -0
  122. data/lib/rigor/plugin/isolation.rb +4 -1
  123. data/lib/rigor/plugin/manifest.rb +127 -3
  124. data/lib/rigor/plugin/registry.rb +55 -0
  125. data/lib/rigor/plugin.rb +1 -0
  126. data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
  127. data/lib/rigor/rbs_extended.rb +188 -0
  128. data/lib/rigor/reflection.rb +131 -30
  129. data/lib/rigor/version.rb +1 -1
  130. data/lib/rigor.rb +1 -0
  131. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
  132. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
  133. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
  134. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
  135. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
  136. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
  137. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
  138. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
  139. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
  140. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
  141. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
  142. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
  143. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
  144. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
  145. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
  146. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
  147. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +401 -29
  148. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -0
  149. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
  150. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
  151. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
  152. data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
  153. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
  154. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
  155. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
  156. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
  157. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
  158. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
  159. data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
  160. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
  161. data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
  162. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +56 -1
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +75 -0
  164. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
  165. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +68 -3
  166. data/sig/rigor/analysis/fact_store.rbs +1 -0
  167. data/sig/rigor/inference.rbs +2 -0
  168. data/sig/rigor/rbs_extended.rbs +6 -0
  169. data/sig/rigor.rbs +17 -1
  170. data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
  171. metadata +84 -1
@@ -0,0 +1,860 @@
1
+ # Core effect catalogue — which core / stdlib methods colour a summary, and with what
2
+ # (ADR-103 WD3 / WD14; the loader is `Rigor::Effects::Catalog`, the contract is
3
+ # `docs/internal-spec/effect-summaries.md` § The catalogue).
4
+ #
5
+ # HAND-AUDITED, NOT GENERATED. The generated `data/builtins/ruby_core/*.yml` `purity:` facet is
6
+ # **never** read here: it answers fold-safety in the C-dispatch sense — `Random#rand` is `leaf`,
7
+ # `Array#push` is `leaf` — and reading it as effect freedom would be wrong in both directions.
8
+ # What did seed this file, cited per row where it decided one: the `c_effects: mutate` / `block`
9
+ # markers, the hand-audited `mutating_selectors:` blocklists in
10
+ # `lib/rigor/inference/builtins/*_catalog.rb` (`random_catalog.rb` and `time_catalog.rb` in
11
+ # particular), `MethodCatalog::NON_REPRODUCIBLE_SELECTORS`, `MutationWidening`'s mutator sets and
12
+ # `ClosureEscapeAnalyzer`'s escape tables.
13
+ #
14
+ # Every row is an UPPER BOUND. An argument-blind row on a world-facing class is the parent label
15
+ # (`IO#write` is `io`, not `io.fs.write` — the channel is a socket as readily as a file), and
16
+ # precision returns only where the call's own literal arguments prove it (`narrow:`).
17
+ #
18
+ # Row grammar:
19
+ #
20
+ # ClassName:
21
+ # posture: <key of `defaults:`> # what an UNCATALOGUED method of this class contributes
22
+ # kind: object # the constant names an object, not a class (ENV, STDOUT, …)
23
+ # mutators: array | hash | string # the receiver-mutator set, BY REFERENCE — never re-spelt here
24
+ # why: "one line"
25
+ # methods: { name: { effects: [...], narrow: handler, mutates: receiver, why: "…" } }
26
+ # singleton_methods: { … }
27
+ #
28
+ # The `<<` selector is spelt `!!str "<<"`: a bare (even quoted) `<<` key is YAML's MERGE key, and
29
+ # Psych would splice the row's own mapping into the enclosing `methods:` map instead of filing it
30
+ # under the selector. The explicit string tag is the only spelling that survives `YAML.safe_load`.
31
+ #
32
+ # `effects:` is the row's labels, `[]` being an explicit ∅ — which is NOT the same as having no
33
+ # row: an explicit ∅ says the catalogue knows the call and knows it contributes nothing, which is
34
+ # what stops `Thread.new` from reading as an unresolved call while its block joins the enclosing
35
+ # method by containment. `mutates: receiver` defers to `MutationClassifier`'s ownership judgment,
36
+ # which taints `unknown-ownership` when it cannot classify.
37
+ schema: 1
38
+ vocabulary: 1
39
+
40
+ # Per-class default postures. A class listed below contributes its posture's labels for any method
41
+ # it does not row; a class absent from `classes:` contributes NOTHING and does not taint, which is
42
+ # the tracer's behaviour and stays the reading for project and gem classes.
43
+ #
44
+ # Ruby's core surface is far larger than the frequency-seeded PHP set Steins catalogues, so a
45
+ # per-class default is what keeps a bound honest without a row per method. `world` is the blunt
46
+ # instrument; the narrower families exist because `io` is not an upper bound of `nondet.random`,
47
+ # and a WRONG label is worse than a missing one (ADR-5).
48
+ defaults:
49
+ value: []
50
+ world: [io]
51
+ fs: [io.fs]
52
+ net: [io.net]
53
+ ipc: [io.ipc]
54
+ http: [io.net.http]
55
+ process: [io.process]
56
+ signal: [io.signal]
57
+ global: [global]
58
+ nondet: [nondet]
59
+ ffi: [ffi]
60
+ stdout: [io.output.stdout]
61
+ stderr: [io.output.stderr]
62
+ stdin: [io.input]
63
+
64
+ # Selectors present on EVERY receiver — `Object` / `BasicObject` / `Kernel`'s instance side — that
65
+ # touch nothing. Consulted after a class's own rows and BEFORE its posture, so a world-facing
66
+ # default does not colour `socket.class`, `io.respond_to?` or `handle.frozen?`. Without this the
67
+ # posture would put a wrong label on the most-called methods in Ruby, and a wrong label is worse
68
+ # than a missing one (ADR-5). A class's own row still wins over this list.
69
+ universal:
70
+ - class
71
+ - singleton_class
72
+ - is_a?
73
+ - kind_of?
74
+ - instance_of?
75
+ - nil?
76
+ - respond_to?
77
+ - respond_to_missing?
78
+ - frozen?
79
+ - freeze
80
+ - dup
81
+ - clone
82
+ - itself
83
+ - tap
84
+ - then
85
+ - yield_self
86
+ - inspect
87
+ - to_s
88
+ - hash
89
+ - object_id
90
+ - __id__
91
+ - "=="
92
+ - "!="
93
+ - "==="
94
+ - "=~"
95
+ - "!"
96
+ - equal?
97
+ - eql?
98
+ - instance_variable_get
99
+ - instance_variable_defined?
100
+ - instance_variables
101
+ - methods
102
+ - public_methods
103
+ - method
104
+
105
+ classes:
106
+ # ---------------------------------------------------------------------------------------------
107
+ # Kernel — the implicit-self surface.
108
+ #
109
+ # NOTE the posture rule's one carve-out, enforced in the collector rather than here: the `world`
110
+ # default is NOT applied to an implicit-self (or `self.`) call. Every unqualified call in a
111
+ # project body spells `Kernel#name`, and defaulting those to `io` would colour the whole world.
112
+ # An implicit-self call therefore matches a ROW or nothing.
113
+ # ---------------------------------------------------------------------------------------------
114
+ Kernel:
115
+ posture: world
116
+ # `Kernel.x` is the `module_function` copy, and what actually gets called that way is the
117
+ # conversion family (`Kernel.Float`, `Kernel.Integer`, `Kernel.Array`), which is pure. Measured:
118
+ # the `world` default on this side put `io` on `Kernel.Float` in Redmine. The instance-side rows
119
+ # are where Kernel's world-facing surface is spelt out, and they are not reached through `Kernel.`.
120
+ singleton_posture: value
121
+ why: "Kernel's own surface is the process boundary; a method of it that is not rowed touches the world"
122
+ methods:
123
+ puts: { effects: [io.output.stdout], why: "writes $stdout" }
124
+ print: { effects: [io.output.stdout], why: "writes $stdout" }
125
+ p: { effects: [io.output.stdout], why: "writes $stdout" }
126
+ pp: { effects: [io.output.stdout], why: "writes $stdout" }
127
+ printf: { effects: [io.output.stdout], why: "writes $stdout (the no-IO form)" }
128
+ putc: { effects: [io.output.stdout], why: "writes $stdout" }
129
+ display: { effects: [io.output.stdout], why: "Object#display writes $stdout" }
130
+ warn: { effects: [io.output.stderr], why: "writes $stderr" }
131
+ gets: { effects: [io.input], why: "reads ARGF or $stdin" }
132
+ readline: { effects: [io.input], why: "reads ARGF or $stdin" }
133
+ readlines: { effects: [io.input], why: "reads ARGF or $stdin to EOF" }
134
+ exit: { effects: [exit], why: "terminates the process (through an exception, but the process ends)" }
135
+ exit!: { effects: [exit], why: "terminates the process without running at_exit handlers" }
136
+ abort: { effects: [exit, io.output.stderr], why: "prints to $stderr, then terminates" }
137
+ at_exit: { effects: [], why: "registers a handler; the block joins this method by containment (WD4)" }
138
+ system: { effects: [io.process], why: "runs a subprocess" }
139
+ spawn: { effects: [io.process], why: "runs a subprocess" }
140
+ exec: { effects: [io.process], why: "replaces the process image" }
141
+ fork: { effects: [io.process], why: "forks the process; the block joins by containment" }
142
+ "`": { effects: [io.process], why: "backtick capture — the same origin the XStringNode construct records" }
143
+ trap: { effects: [io.signal], why: "installs a process-wide signal handler" }
144
+ require: { effects: [io.fs.read, mutate.static], why: "reads the load path AND installs constants and methods (WD14)" }
145
+ require_relative: { effects: [io.fs.read, mutate.static], why: "reads a file AND installs constants and methods (WD14)" }
146
+ load: { effects: [io.fs.read, mutate.static], why: "reads a file AND installs constants and methods (WD14)" }
147
+ autoload: { effects: [io.fs.read, mutate.static], why: "registers a deferred require; the registration itself is a static mutation" }
148
+ sleep: { effects: [io], why: "a syscall whose duration the world decides; no narrower label in the shared vocabulary fits (WD14)" }
149
+ rand: { effects: [nondet.random], why: "consumes the process-wide generator; `random_catalog.rb` blocklists :rand for the same reason" }
150
+ srand: { effects: [global.write, nondet.random], why: "re-seeds the process-wide generator AND consumes entropy for the old seed" }
151
+ open: { narrow: kernel_open, effects: [io], why: "a path, a `|command` pipe or a URI depending on the argument — the classic pipe-injection footgun, made visible" }
152
+ caller: { effects: [], why: "reads the frame stack, which is neither global state nor the world" }
153
+ raise: { effects: [], why: "control flow, not a label; the throw set lives in the flow-effect bundle (design note § 2)" }
154
+ catch: { effects: [], why: "control flow; the block joins by containment" }
155
+ throw: { effects: [], why: "control flow, not a label" }
156
+ loop: { effects: [], why: "control flow; the block joins by containment" }
157
+ lambda: { effects: [], why: "allocates a Proc; the literal's body joins by containment" }
158
+ proc: { effects: [], why: "allocates a Proc; the literal's body joins by containment" }
159
+ block_given?: { effects: [], why: "reads the frame's block slot" }
160
+ freeze: { effects: [], why: "Object#freeze — deliberately not a mutation; `MutationWidening::PURE_SELF_RETURNERS` lists it" }
161
+ dup: { effects: [], why: "allocates a copy; ADR-76's allocation witness, never a receiver mutation" }
162
+ clone: { effects: [], why: "allocates a copy; ADR-76's allocation witness, never a receiver mutation" }
163
+ format: { effects: [], why: "pure string formatting; only `printf` writes" }
164
+ sprintf: { effects: [], why: "pure string formatting" }
165
+
166
+ # ---------------------------------------------------------------------------------------------
167
+ # Object model. Every one of these installs or removes a method / constant at run time.
168
+ # ---------------------------------------------------------------------------------------------
169
+ Object:
170
+ posture: value
171
+ why: "the root of ordinary values; an uncatalogued Object method is not a world call"
172
+ methods:
173
+ instance_variable_set:
174
+ effects: []
175
+ mutates: receiver
176
+ why: "writes another object's state; the label follows the receiver's ownership (mutate.self on self, mutate.instance on a parameter, taint otherwise)"
177
+ instance_variable_get: { effects: [], why: "reads an ivar — object state, not global state" }
178
+ hash: { effects: [], why: "per-process SipHash salt makes it irreproducible (`NON_REPRODUCIBLE_SELECTORS`), but irreproducibility is a fold hazard, not an observable effect" }
179
+ object_id: { effects: [], why: "per-process identity — a fold hazard (`NON_REPRODUCIBLE_SELECTORS`), not an effect" }
180
+ __id__: { effects: [], why: "alias of object_id; same reading" }
181
+ extend: { effects: [mutate.static], why: "inserts a module into the receiver's singleton ancestry" }
182
+ define_singleton_method: { effects: [mutate.static], why: "installs a method on the receiver's singleton class" }
183
+ instance_variable_defined?: { effects: [], why: "reads object state" }
184
+
185
+ Module:
186
+ posture: value
187
+ why: "a Module's read surface (name, ancestors, instance_methods) is pure; only the object-model writers are rowed"
188
+ methods:
189
+ define_method: { effects: [mutate.static], why: "installs a method on the class object" }
190
+ alias_method: { effects: [mutate.static], why: "installs a method on the class object" }
191
+ remove_method: { effects: [mutate.static], why: "removes a method from the class object" }
192
+ undef_method: { effects: [mutate.static], why: "installs an undef entry on the class object" }
193
+ include: { effects: [mutate.static], why: "rewrites the class's ancestry" }
194
+ prepend: { effects: [mutate.static], why: "rewrites the class's ancestry" }
195
+ extend: { effects: [mutate.static], why: "rewrites the class object's singleton ancestry" }
196
+ attr_reader: { effects: [mutate.static], why: "installs a method on the class object" }
197
+ attr_writer: { effects: [mutate.static], why: "installs a method on the class object" }
198
+ attr_accessor: { effects: [mutate.static], why: "installs two methods on the class object" }
199
+ attr: { effects: [mutate.static], why: "installs a method on the class object" }
200
+ const_set: { effects: [mutate.static], why: "installs a constant" }
201
+ remove_const: { effects: [mutate.static], why: "removes a constant" }
202
+ const_get: { effects: [], why: "reads the constant table" }
203
+ private: { effects: [mutate.static], why: "changes a method's visibility on the class object" }
204
+ public: { effects: [mutate.static], why: "changes a method's visibility on the class object" }
205
+ protected: { effects: [mutate.static], why: "changes a method's visibility on the class object" }
206
+ private_constant: { effects: [mutate.static], why: "changes a constant's visibility on the class object" }
207
+ public_constant: { effects: [mutate.static], why: "changes a constant's visibility on the class object" }
208
+ module_function: { effects: [mutate.static], why: "installs singleton copies of the module's methods" }
209
+ private_class_method: { effects: [mutate.static], why: "changes a singleton method's visibility" }
210
+ public_class_method: { effects: [mutate.static], why: "changes a singleton method's visibility" }
211
+ refine: { effects: [mutate.static], why: "creates a refinement module and attaches it to the receiver" }
212
+ using: { effects: [mutate.static], why: "activates a refinement for the lexical scope" }
213
+ autoload: { effects: [io.fs.read, mutate.static], why: "Module#autoload — the same reading as Kernel#autoload (WD14)" }
214
+ module_eval: { effects: [mutate.static], why: "the block form is containment; the string form additionally taints (opaque-callable)" }
215
+ class_eval: { effects: [mutate.static], why: "the block form is containment; the string form additionally taints (opaque-callable)" }
216
+
217
+ Class:
218
+ posture: value
219
+ why: "Class inherits Module's surface; the rowed writers are Module's and resolve through it"
220
+
221
+ # ---------------------------------------------------------------------------------------------
222
+ # Value classes. Uncatalogued methods are ∅; the mutators come from the sets the widening rules
223
+ # and the mutation classifier already maintain, BY REFERENCE — this file never re-spells them,
224
+ # so the two cannot drift (a spec pins the agreement).
225
+ # ---------------------------------------------------------------------------------------------
226
+ Array:
227
+ posture: value
228
+ mutators: array
229
+ why: "a value class; mutation is ownership-classified from `MutationWidening::ARRAY_MUTATORS`"
230
+ methods:
231
+ shuffle: { effects: [nondet.random], why: "draws from the process-wide generator unless `random:` is given" }
232
+ shuffle!: { effects: [nondet.random], mutates: receiver, why: "draws from the process-wide generator AND reorders the receiver" }
233
+ sample: { effects: [nondet.random], why: "draws from the process-wide generator unless `random:` is given" }
234
+
235
+ Hash:
236
+ posture: value
237
+ mutators: hash
238
+ why: "a value class; mutation is ownership-classified from `MutationWidening::HASH_MUTATORS`"
239
+
240
+ String:
241
+ posture: value
242
+ mutators: string
243
+ why: "a value class; mutation is ownership-classified from `MutationClassifier::STRING_MUTATORS`"
244
+
245
+ Symbol: { posture: value, why: "immutable value class" }
246
+ Integer: { posture: value, why: "immutable value class" }
247
+ Float: { posture: value, why: "immutable value class" }
248
+ Rational: { posture: value, why: "immutable value class" }
249
+ Complex: { posture: value, why: "immutable value class" }
250
+ Numeric: { posture: value, why: "immutable value class" }
251
+ Range: { posture: value, why: "immutable value class" }
252
+ Regexp: { posture: value, why: "immutable value class; `$~` and friends are frame-local, not global state" }
253
+ MatchData: { posture: value, why: "immutable value class" }
254
+ NilClass: { posture: value, why: "immutable value class" }
255
+ TrueClass: { posture: value, why: "immutable value class" }
256
+ FalseClass: { posture: value, why: "immutable value class" }
257
+ Struct: { posture: value, why: "a value class; its writers are attribute writers, which the universal writer rule already classifies" }
258
+ Data: { posture: value, why: "an immutable value class" }
259
+ Comparable: { posture: value, why: "a mixin over value receivers" }
260
+ Enumerable: { posture: value, why: "a mixin over value receivers; a block argument joins by containment" }
261
+ Set:
262
+ posture: value
263
+ why: "a value class; its mutators are not in a shared set yet, so only the universal writer rule claims them"
264
+ Proc:
265
+ posture: value
266
+ why: "a value class — but `Proc#call` is deliberately NOT rowed: the collector defers `call` to the opaque-callable taint, which is the more specific reading of an uninspectable body"
267
+ Method:
268
+ posture: value
269
+ why: "same as Proc: `call` defers to the opaque-callable taint"
270
+ Enumerator: { posture: value, why: "a value class; the block a lazy enumerator carries joins by containment at its literal" }
271
+
272
+ # ---------------------------------------------------------------------------------------------
273
+ # Filesystem and streams. `io.fs.read` / `io.fs.write` where the direction is proven; `io.fs`
274
+ # where only the subsystem is; bare `io` where even the channel is open.
275
+ # ---------------------------------------------------------------------------------------------
276
+ IO:
277
+ posture: world
278
+ why: "an IO's channel is a file, a socket, a pipe or a terminal — the parent `io` is the only sound default"
279
+ methods:
280
+ write: { effects: [io], why: "the channel is unknown; a narrower label would be a guess" }
281
+ !!str "<<": { effects: [io], why: "stream write on an unknown channel" }
282
+ puts: { effects: [io], why: "stream write on an unknown channel" }
283
+ print: { effects: [io], why: "stream write on an unknown channel" }
284
+ printf: { effects: [io], why: "stream write on an unknown channel" }
285
+ read: { effects: [io], why: "stream read on an unknown channel" }
286
+ gets: { effects: [io], why: "stream read on an unknown channel" }
287
+ readlines: { effects: [io], why: "stream read on an unknown channel" }
288
+ each_line: { effects: [io], why: "stream read on an unknown channel; the block joins by containment" }
289
+ close: { effects: [io], why: "releases a descriptor the world owns" }
290
+ flush: { effects: [io], why: "forces the buffered write out" }
291
+ fileno: { effects: [], why: "reads a field of the IO object" }
292
+ path: { effects: [], why: "reads a field of the IO object" }
293
+ closed?: { effects: [], why: "reads a field of the IO object" }
294
+ singleton_methods:
295
+ read: { effects: [io.fs.read], why: "IO.read takes a path" }
296
+ binread: { effects: [io.fs.read], why: "IO.binread takes a path" }
297
+ readlines: { effects: [io.fs.read], why: "IO.readlines takes a path" }
298
+ foreach: { effects: [io.fs.read], why: "IO.foreach takes a path; the block joins by containment" }
299
+ write: { effects: [io.fs.write], why: "IO.write takes a path" }
300
+ binwrite: { effects: [io.fs.write], why: "IO.binwrite takes a path" }
301
+ popen: { effects: [io.process], why: "runs a subprocess and attaches to its stream" }
302
+ pipe: { effects: [io.ipc], why: "creates an anonymous in-process pipe pair" }
303
+ select: { effects: [io], why: "blocks on a set of descriptors of unknown kind" }
304
+ sysopen: { effects: [io.fs], why: "opens a path; the direction is the mode argument, which is not read here" }
305
+ copy_stream: { effects: [io], why: "both ends are of unknown kind" }
306
+
307
+ File:
308
+ posture: fs
309
+ why: "every File method that is not path algebra touches the filesystem; the direction is rowed where it is known"
310
+ methods:
311
+ path: { effects: [], why: "reads a field of the File object" }
312
+ to_path: { effects: [], why: "reads a field of the File object" }
313
+ write: { effects: [io.fs.write], why: "writes the open file" }
314
+ !!str "<<": { effects: [io.fs.write], why: "writes the open file" }
315
+ puts: { effects: [io.fs.write], why: "writes the open file" }
316
+ print: { effects: [io.fs.write], why: "writes the open file" }
317
+ read: { effects: [io.fs.read], why: "reads the open file" }
318
+ gets: { effects: [io.fs.read], why: "reads the open file" }
319
+ readlines: { effects: [io.fs.read], why: "reads the open file" }
320
+ each_line: { effects: [io.fs.read], why: "reads the open file; the block joins by containment" }
321
+ flock: { effects: [io.fs.write], why: "takes a lock other processes observe" }
322
+ singleton_methods:
323
+ open: { narrow: file_open, effects: [io.fs], why: "the mode literal decides the direction; a non-literal mode reads as the subsystem parent" }
324
+ read: { effects: [io.fs.read], why: "reads a path whole" }
325
+ binread: { effects: [io.fs.read], why: "reads a path whole" }
326
+ readlines: { effects: [io.fs.read], why: "reads a path whole" }
327
+ foreach: { effects: [io.fs.read], why: "reads a path; the block joins by containment" }
328
+ exist?: { effects: [io.fs.read], why: "stats a path" }
329
+ exists?: { effects: [io.fs.read], why: "deprecated alias of exist?" }
330
+ file?: { effects: [io.fs.read], why: "stats a path" }
331
+ directory?: { effects: [io.fs.read], why: "stats a path" }
332
+ readable?: { effects: [io.fs.read], why: "stats a path" }
333
+ writable?: { effects: [io.fs.read], why: "stats a path" }
334
+ executable?: { effects: [io.fs.read], why: "stats a path" }
335
+ symlink?: { effects: [io.fs.read], why: "lstats a path" }
336
+ size: { effects: [io.fs.read], why: "stats a path" }
337
+ size?: { effects: [io.fs.read], why: "stats a path" }
338
+ zero?: { effects: [io.fs.read], why: "stats a path" }
339
+ empty?: { effects: [io.fs.read], why: "stats a path" }
340
+ stat: { effects: [io.fs.read], why: "stats a path" }
341
+ lstat: { effects: [io.fs.read], why: "lstats a path" }
342
+ mtime: { effects: [io.fs.read], why: "stats a path" }
343
+ ctime: { effects: [io.fs.read], why: "stats a path" }
344
+ atime: { effects: [io.fs.read], why: "stats a path" }
345
+ birthtime: { effects: [io.fs.read], why: "stats a path" }
346
+ ftype: { effects: [io.fs.read], why: "stats a path" }
347
+ realpath: { effects: [io.fs.read], why: "resolves symlinks, which reads the filesystem" }
348
+ realdirpath: { effects: [io.fs.read], why: "resolves symlinks, which reads the filesystem" }
349
+ identical?: { effects: [io.fs.read], why: "stats both paths" }
350
+ write: { effects: [io.fs.write], why: "writes a path whole" }
351
+ binwrite: { effects: [io.fs.write], why: "writes a path whole" }
352
+ delete: { effects: [io.fs.write], why: "unlinks a path" }
353
+ unlink: { effects: [io.fs.write], why: "unlinks a path" }
354
+ rename: { effects: [io.fs.write], why: "renames a path" }
355
+ chmod: { effects: [io.fs.write], why: "changes a path's mode" }
356
+ chown: { effects: [io.fs.write], why: "changes a path's owner" }
357
+ utime: { effects: [io.fs.write], why: "changes a path's timestamps" }
358
+ truncate: { effects: [io.fs.write], why: "truncates a path" }
359
+ symlink: { effects: [io.fs.write], why: "creates a symlink" }
360
+ link: { effects: [io.fs.write], why: "creates a hard link" }
361
+ readlink: { effects: [io.fs.read], why: "reads a symlink's target" }
362
+ # Pure path algebra. These are the reason File is not blanket-`io.fs`: a `File.join` in a
363
+ # method body must not colour it.
364
+ join: { effects: [], why: "string algebra over separators; touches no filesystem" }
365
+ basename: { effects: [], why: "string algebra; touches no filesystem" }
366
+ dirname: { effects: [], why: "string algebra; touches no filesystem" }
367
+ extname: { effects: [], why: "string algebra; touches no filesystem" }
368
+ split: { effects: [], why: "string algebra; touches no filesystem" }
369
+ absolute_path?: { effects: [], why: "string algebra; touches no filesystem" }
370
+ fnmatch: { effects: [], why: "glob matching over a string; touches no filesystem" }
371
+ fnmatch?: { effects: [], why: "glob matching over a string; touches no filesystem" }
372
+ # `expand_path` consults the CWD (and `~`) when the path is relative, which is process-global
373
+ # state rather than a filesystem read.
374
+ expand_path: { effects: [global.read], why: "resolves against the process CWD and $HOME when the argument is relative" }
375
+
376
+ Dir:
377
+ posture: fs
378
+ why: "every Dir method touches the filesystem or the process CWD"
379
+ singleton_methods:
380
+ glob: { effects: [io.fs.read], why: "walks the filesystem" }
381
+ "[]": { effects: [io.fs.read], why: "Dir[] is glob" }
382
+ entries: { effects: [io.fs.read], why: "reads a directory" }
383
+ children: { effects: [io.fs.read], why: "reads a directory" }
384
+ each_child: { effects: [io.fs.read], why: "reads a directory; the block joins by containment" }
385
+ foreach: { effects: [io.fs.read], why: "reads a directory; the block joins by containment" }
386
+ exist?: { effects: [io.fs.read], why: "stats a path" }
387
+ empty?: { effects: [io.fs.read], why: "reads a directory" }
388
+ home: { effects: [global.read], why: "reads $HOME / the passwd entry — process-global state, not the filesystem tree" }
389
+ tmpdir: { effects: [global.read], why: "reads $TMPDIR and friends" }
390
+ mkdir: { effects: [io.fs.write], why: "creates a directory" }
391
+ rmdir: { effects: [io.fs.write], why: "removes a directory" }
392
+ delete: { effects: [io.fs.write], why: "removes a directory" }
393
+ unlink: { effects: [io.fs.write], why: "removes a directory" }
394
+ mktmpdir: { effects: [io.fs.write], why: "tmpdir's extension: creates a directory; the block joins by containment" }
395
+ pwd: { effects: [global.read], why: "reads the process CWD — global state, not a filesystem read" }
396
+ getwd: { effects: [global.read], why: "alias of pwd" }
397
+ chdir: { effects: [global.write], why: "writes the process CWD, which every later relative path observes" }
398
+
399
+ FileUtils:
400
+ posture: fs
401
+ why: "a filesystem toolbox; the writers dominate and are rowed, the few readers likewise"
402
+ singleton_methods:
403
+ cp: { effects: [io.fs.read, io.fs.write], why: "reads the source and writes the destination" }
404
+ cp_r: { effects: [io.fs.read, io.fs.write], why: "reads the source tree and writes the destination" }
405
+ copy: { effects: [io.fs.read, io.fs.write], why: "alias of cp" }
406
+ mv: { effects: [io.fs.write], why: "renames or copies-and-unlinks" }
407
+ move: { effects: [io.fs.write], why: "alias of mv" }
408
+ rm: { effects: [io.fs.write], why: "unlinks" }
409
+ rm_f: { effects: [io.fs.write], why: "unlinks, ignoring errors" }
410
+ rm_r: { effects: [io.fs.write], why: "unlinks a tree" }
411
+ rm_rf: { effects: [io.fs.write], why: "unlinks a tree, ignoring errors" }
412
+ rmtree: { effects: [io.fs.write], why: "unlinks a tree" }
413
+ remove_entry: { effects: [io.fs.write], why: "unlinks a tree" }
414
+ mkdir: { effects: [io.fs.write], why: "creates a directory" }
415
+ mkdir_p: { effects: [io.fs.write], why: "creates a directory chain" }
416
+ mkpath: { effects: [io.fs.write], why: "alias of mkdir_p" }
417
+ makedirs: { effects: [io.fs.write], why: "alias of mkdir_p" }
418
+ touch: { effects: [io.fs.write], why: "creates or restamps a path" }
419
+ ln: { effects: [io.fs.write], why: "creates a hard link" }
420
+ ln_s: { effects: [io.fs.write], why: "creates a symlink" }
421
+ ln_sf: { effects: [io.fs.write], why: "creates a symlink, replacing" }
422
+ install: { effects: [io.fs.read, io.fs.write], why: "copies and sets a mode" }
423
+ chmod: { effects: [io.fs.write], why: "changes a mode" }
424
+ chmod_R: { effects: [io.fs.write], why: "changes a tree's modes" }
425
+ chown: { effects: [io.fs.write], why: "changes an owner" }
426
+ chown_R: { effects: [io.fs.write], why: "changes a tree's owners" }
427
+ compare_file: { effects: [io.fs.read], why: "reads both paths" }
428
+ identical?: { effects: [io.fs.read], why: "reads both paths" }
429
+ pwd: { effects: [global.read], why: "reads the process CWD" }
430
+ cd: { effects: [global.write], why: "writes the process CWD" }
431
+
432
+ Tempfile:
433
+ posture: fs
434
+ why: "a Tempfile is a File over a path the library creates"
435
+ singleton_methods:
436
+ new: { effects: [io.fs.write], why: "creates a file under the temporary directory" }
437
+ create: { effects: [io.fs.write], why: "creates a file under the temporary directory; the block joins by containment" }
438
+ open: { effects: [io.fs.write], why: "creates a file under the temporary directory; the block joins by containment" }
439
+
440
+ Pathname:
441
+ posture: value
442
+ why: "Pathname's surface is mostly path algebra (`+`, `join`, `basename`, `to_s`); a blanket io.fs would colour string manipulation, so only the filesystem calls are rowed"
443
+ methods:
444
+ read: { effects: [io.fs.read], why: "reads the path whole" }
445
+ binread: { effects: [io.fs.read], why: "reads the path whole" }
446
+ readlines: { effects: [io.fs.read], why: "reads the path whole" }
447
+ each_line: { effects: [io.fs.read], why: "reads the path; the block joins by containment" }
448
+ exist?: { effects: [io.fs.read], why: "stats the path" }
449
+ file?: { effects: [io.fs.read], why: "stats the path" }
450
+ directory?: { effects: [io.fs.read], why: "stats the path" }
451
+ symlink?: { effects: [io.fs.read], why: "lstats the path" }
452
+ readable?: { effects: [io.fs.read], why: "stats the path" }
453
+ writable?: { effects: [io.fs.read], why: "stats the path" }
454
+ size: { effects: [io.fs.read], why: "stats the path" }
455
+ stat: { effects: [io.fs.read], why: "stats the path" }
456
+ lstat: { effects: [io.fs.read], why: "lstats the path" }
457
+ mtime: { effects: [io.fs.read], why: "stats the path" }
458
+ children: { effects: [io.fs.read], why: "reads the directory" }
459
+ entries: { effects: [io.fs.read], why: "reads the directory" }
460
+ glob: { effects: [io.fs.read], why: "walks the filesystem" }
461
+ find: { effects: [io.fs.read], why: "walks the filesystem; the block joins by containment" }
462
+ realpath: { effects: [io.fs.read], why: "resolves symlinks" }
463
+ realdirpath: { effects: [io.fs.read], why: "resolves symlinks" }
464
+ open: { narrow: pathname_open, effects: [io.fs], why: "Pathname#open forwards to File.open, with the path as the receiver — so the mode is the FIRST argument" }
465
+ write: { effects: [io.fs.write], why: "writes the path whole" }
466
+ binwrite: { effects: [io.fs.write], why: "writes the path whole" }
467
+ mkdir: { effects: [io.fs.write], why: "creates the directory" }
468
+ mkpath: { effects: [io.fs.write], why: "creates the directory chain" }
469
+ rmdir: { effects: [io.fs.write], why: "removes the directory" }
470
+ rmtree: { effects: [io.fs.write], why: "removes the tree" }
471
+ delete: { effects: [io.fs.write], why: "unlinks the path" }
472
+ unlink: { effects: [io.fs.write], why: "unlinks the path" }
473
+ rename: { effects: [io.fs.write], why: "renames the path" }
474
+ chmod: { effects: [io.fs.write], why: "changes the path's mode" }
475
+ touch: { effects: [io.fs.write], why: "creates or restamps the path" }
476
+ expand_path: { effects: [global.read], why: "resolves against the process CWD when the path is relative" }
477
+ singleton_methods:
478
+ pwd: { effects: [global.read], why: "reads the process CWD" }
479
+ getwd: { effects: [global.read], why: "reads the process CWD" }
480
+ glob: { effects: [io.fs.read], why: "walks the filesystem" }
481
+
482
+ StringIO:
483
+ posture: value
484
+ why: "an in-memory stream; nothing leaves the process, so its writes are receiver mutation rather than io"
485
+
486
+ # ---------------------------------------------------------------------------------------------
487
+ # Processes and signals.
488
+ # ---------------------------------------------------------------------------------------------
489
+ Process:
490
+ posture: process
491
+ why: "the process-control surface; an uncatalogued Process method acts on a process"
492
+ singleton_methods:
493
+ pid: { effects: [global.read], why: "reads process-global identity, spawns nothing" }
494
+ ppid: { effects: [global.read], why: "reads process-global identity" }
495
+ uid: { effects: [global.read], why: "reads the process credentials" }
496
+ gid: { effects: [global.read], why: "reads the process credentials" }
497
+ euid: { effects: [global.read], why: "reads the process credentials" }
498
+ egid: { effects: [global.read], why: "reads the process credentials" }
499
+ clock_gettime: { effects: [nondet.time], why: "reads a clock — the same axis Time.now is on" }
500
+ exit: { effects: [exit], why: "terminates the process" }
501
+ exit!: { effects: [exit], why: "terminates the process without handlers" }
502
+ abort: { effects: [exit, io.output.stderr], why: "prints to $stderr, then terminates" }
503
+ kill: { effects: [io.signal], why: "sends a signal to another process" }
504
+ fork: { effects: [io.process], why: "forks the process; the block joins by containment" }
505
+ spawn: { effects: [io.process], why: "runs a subprocess" }
506
+ wait: { effects: [io.process], why: "blocks on a child" }
507
+ wait2: { effects: [io.process], why: "blocks on a child" }
508
+ waitpid: { effects: [io.process], why: "blocks on a child" }
509
+ waitpid2: { effects: [io.process], why: "blocks on a child" }
510
+ daemon: { effects: [io.process, global.write], why: "forks, detaches and rewrites the CWD" }
511
+ setsid: { effects: [io.process], why: "changes the process's session" }
512
+ setproctitle: { effects: [global.write], why: "rewrites process-global state the OS shows" }
513
+ argv0: { effects: [global.read], why: "reads process-global state" }
514
+
515
+ Signal:
516
+ posture: signal
517
+ why: "the signal surface"
518
+ singleton_methods:
519
+ trap: { effects: [io.signal], why: "installs a process-wide handler; the block joins by containment" }
520
+ list: { effects: [], why: "returns a constant table" }
521
+ signame: { effects: [], why: "returns a name from a constant table" }
522
+
523
+ Open3:
524
+ posture: process
525
+ why: "every Open3 entry point runs a subprocess"
526
+
527
+ PTY:
528
+ posture: process
529
+ why: "allocates a pseudo-terminal and runs a subprocess on it"
530
+
531
+ # ---------------------------------------------------------------------------------------------
532
+ # Network.
533
+ # ---------------------------------------------------------------------------------------------
534
+ Socket:
535
+ posture: net
536
+ why: "the BSD socket surface; the rows below read the machine's own identity and send nothing"
537
+ singleton_methods:
538
+ # `io.net` on these was a wrong label, not a conservative one, and it travelled: Redmine builds
539
+ # its Message-IDs from `Socket.gethostname`, so 215 of 4,234 rows — every model `save`, every
540
+ # `Mailer#*`, and `Redmine::IMAP.check`, whose actual IMAP connection is invisible — read as
541
+ # network traffic on the strength of a hostname lookup (#458). `io` is the honest bound: the
542
+ # call leaves the process to ask the kernel, and nothing goes over a wire.
543
+ gethostname: { effects: [io], why: "reads this machine's own name; no packet leaves the process" }
544
+ gethostbyname: { effects: [io.net], why: "resolves a name — the resolver, and possibly DNS" }
545
+ getifaddrs: { effects: [io], why: "reads this machine's own interfaces; local configuration, not resolution" }
546
+ ip_address_list: { effects: [io], why: "reads this machine's own addresses; local configuration, not resolution" }
547
+ BasicSocket: { posture: net, why: "the shared socket surface" }
548
+ TCPSocket: { posture: net, why: "a TCP stream" }
549
+ TCPServer: { posture: net, why: "a listening TCP socket" }
550
+ UDPSocket: { posture: net, why: "a UDP socket" }
551
+ SOCKSSocket: { posture: net, why: "a proxied TCP stream" }
552
+ Addrinfo:
553
+ posture: net
554
+ why: "resolution touches the resolver; the pure accessors are rowed"
555
+ methods:
556
+ ip_address: { effects: [], why: "reads a field of the Addrinfo object" }
557
+ ip_port: { effects: [], why: "reads a field of the Addrinfo object" }
558
+ afamily: { effects: [], why: "reads a field of the Addrinfo object" }
559
+ to_s: { effects: [], why: "reads a field of the Addrinfo object" }
560
+ Resolv: { posture: net, why: "the DNS resolver surface" }
561
+ "Resolv::DNS": { posture: net, why: "the DNS resolver surface" }
562
+ "OpenSSL::SSL::SSLSocket": { posture: net, why: "a TLS stream over a socket" }
563
+ UNIXSocket: { posture: ipc, why: "a UNIX-domain stream — inter-process, never off the machine" }
564
+ UNIXServer: { posture: ipc, why: "a listening UNIX-domain socket" }
565
+
566
+ "Net::HTTP":
567
+ posture: http
568
+ why: "every Net::HTTP entry point speaks HTTP"
569
+ methods:
570
+ request: { effects: [io.net.http], why: "issues the request" }
571
+ start: { effects: [io.net.http], why: "opens the connection; the block joins by containment" }
572
+ get: { effects: [io.net.http], why: "issues a GET" }
573
+ post: { effects: [io.net.http], why: "issues a POST" }
574
+ put: { effects: [io.net.http], why: "issues a PUT" }
575
+ delete: { effects: [io.net.http], why: "issues a DELETE" }
576
+ head: { effects: [io.net.http], why: "issues a HEAD" }
577
+ patch: { effects: [io.net.http], why: "issues a PATCH" }
578
+ "use_ssl=": { effects: [], mutates: receiver, why: "sets a field on the client before the connection opens; nothing is sent, so the only effect is the receiver mutation" }
579
+ singleton_methods:
580
+ get: { effects: [io.net.http], why: "issues a GET" }
581
+ get_response: { effects: [io.net.http], why: "issues a GET" }
582
+ post: { effects: [io.net.http], why: "issues a POST" }
583
+ post_form: { effects: [io.net.http], why: "issues a POST" }
584
+ start: { effects: [io.net.http], why: "opens the connection; the block joins by containment" }
585
+ new: { effects: [], why: "allocates the client; nothing is sent until start / request" }
586
+ "Net::SMTP": { posture: net, why: "an SMTP session; `email.send` is the plugin's semantic layer, not the transport's" }
587
+ # rbs ships `net-http` / `net-smtp` and not these, so a project's `Net::IMAP` types as `Dynamic` and
588
+ # only the constant the author wrote names the class. #463 is what lets a posture answer there; before
589
+ # it, `Net::FTP` had a row that could never fire and these two were not worth writing.
590
+ "Net::IMAP": { posture: net, why: "an IMAP session" }
591
+ "Net::POP3": { posture: net, why: "a POP3 session" }
592
+ "Net::FTP": { posture: net, why: "an FTP session" }
593
+
594
+ URI:
595
+ posture: value
596
+ why: "URI is parsing — string algebra — except for the open-uri extension"
597
+ singleton_methods:
598
+ open: { narrow: uri_open, effects: [io], why: "open-uri: an http(s) literal is HTTP, a file:// or bare path is a filesystem read, anything unproven is the parent" }
599
+ parse: { effects: [], why: "string parsing" }
600
+ join: { effects: [], why: "string algebra" }
601
+ encode_www_form: { effects: [], why: "string algebra" }
602
+ OpenURI:
603
+ posture: value
604
+ why: "only the entry point is world-facing"
605
+ singleton_methods:
606
+ open_uri: { narrow: uri_open, effects: [io], why: "the same narrowing as URI.open, which forwards here" }
607
+
608
+ # ---------------------------------------------------------------------------------------------
609
+ # Non-determinism: clocks and generators.
610
+ # ---------------------------------------------------------------------------------------------
611
+ Time:
612
+ posture: value
613
+ why: "a Time is a value; only the constructors that consult a clock and the receiver mutators are world-facing"
614
+ methods:
615
+ # `time_catalog.rb` blocklists these as receiver mutators, quoting the C bodies:
616
+ # `time_localtime` / `time_gmtime` call `time_modify(time)` and rewrite the `vtm` cache.
617
+ localtime: { effects: [global.read], mutates: receiver, why: "rewrites the receiver's vtm cache in place AND reads the process TZ (time_catalog.rb blocklists it for the same C body)" }
618
+ gmtime: { effects: [], mutates: receiver, why: "rewrites the receiver's vtm cache in place (time_catalog.rb: time_modify then TZMODE_SET_UTC)" }
619
+ utc: { effects: [], mutates: receiver, why: "the same cfunc as gmtime, registered under a second name" }
620
+ getlocal: { effects: [global.read], why: "returns a fresh Time pinned to the process TZ; time_catalog.rb blocks the fold for exactly this host-dependence" }
621
+ getutc: { effects: [], why: "UTC, so machine-independent — time_catalog.rb leaves it foldable" }
622
+ getgm: { effects: [], why: "UTC, so machine-independent" }
623
+ strftime: { effects: [], why: "formatting; the TZ is already fixed in the receiver" }
624
+ singleton_methods:
625
+ now: { effects: [nondet.time], why: "reads the wall clock" }
626
+ new: { narrow: time_new, effects: [nondet.time], why: "no arguments is Time.now; with arguments it constructs from them and consults nothing" }
627
+ at: { effects: [], why: "constructs from its argument" }
628
+ utc: { effects: [], why: "constructs from its arguments" }
629
+ gm: { effects: [], why: "constructs from its arguments" }
630
+ local: { effects: [global.read], why: "constructs from its arguments in the process TZ" }
631
+ mktime: { effects: [global.read], why: "alias of local" }
632
+ parse: { effects: [global.read], why: "an unqualified parse fills missing fields from the process TZ and today's date" }
633
+
634
+ Date:
635
+ posture: value
636
+ why: "a Date is a value; only `today` consults a clock"
637
+ singleton_methods:
638
+ today: { effects: [nondet.time], why: "reads the wall clock (and the process TZ)" }
639
+ parse: { effects: [], why: "string parsing" }
640
+ new: { effects: [], why: "constructs from its arguments" }
641
+ DateTime:
642
+ posture: value
643
+ why: "a DateTime is a value; only `now` consults a clock"
644
+ singleton_methods:
645
+ now: { effects: [nondet.time], why: "reads the wall clock" }
646
+ parse: { effects: [], why: "string parsing" }
647
+ new: { effects: [], why: "constructs from its arguments" }
648
+
649
+ Random:
650
+ posture: value
651
+ why: "a seeded Random is a value object; the rows are the generator-consuming methods `random_catalog.rb` already blocklists"
652
+ methods:
653
+ rand: { effects: [nondet.random], mutates: receiver, why: "advances the receiver's MT state (random_catalog.rb blocklists :rand for the indirect mutation)" }
654
+ bytes: { effects: [nondet.random], mutates: receiver, why: "consumes MT output the same way #rand does (random_catalog.rb)" }
655
+ seed: { effects: [], why: "reads the seed the receiver was constructed with" }
656
+ singleton_methods:
657
+ new: { narrow: random_new, effects: [nondet.random], why: "a seed argument makes the generator reproducible; without one it draws from platform entropy" }
658
+ rand: { effects: [nondet.random], why: "advances the process-wide generator (random_catalog.rb)" }
659
+ bytes: { effects: [nondet.random], why: "advances the process-wide generator" }
660
+ new_seed: { effects: [nondet.random], why: "reads platform entropy (random_catalog.rb: with_random_seed)" }
661
+ urandom: { effects: [nondet.random], why: "reads the platform CSPRNG (random_catalog.rb)" }
662
+
663
+ SecureRandom:
664
+ posture: nondet
665
+ why: "every SecureRandom entry point draws from the platform CSPRNG"
666
+ singleton_methods:
667
+ hex: { effects: [nondet.random], why: "draws from the platform CSPRNG" }
668
+ uuid: { effects: [nondet.random], why: "draws from the platform CSPRNG" }
669
+ uuid_v4: { effects: [nondet.random], why: "draws from the platform CSPRNG" }
670
+ uuid_v7: { effects: [nondet.random, nondet.time], why: "draws from the platform CSPRNG AND stamps the wall clock into the prefix" }
671
+ random_bytes: { effects: [nondet.random], why: "draws from the platform CSPRNG" }
672
+ bytes: { effects: [nondet.random], why: "draws from the platform CSPRNG" }
673
+ base64: { effects: [nondet.random], why: "draws from the platform CSPRNG" }
674
+ urlsafe_base64: { effects: [nondet.random], why: "draws from the platform CSPRNG" }
675
+ alphanumeric: { effects: [nondet.random], why: "draws from the platform CSPRNG" }
676
+ random_number: { effects: [nondet.random], why: "draws from the platform CSPRNG" }
677
+
678
+ # ---------------------------------------------------------------------------------------------
679
+ # Process-global state.
680
+ # ---------------------------------------------------------------------------------------------
681
+ ENV:
682
+ kind: object
683
+ posture: global
684
+ why: "ENV is the process environment; every access is global state in one direction or the other"
685
+ methods:
686
+ "[]": { effects: [global.read], why: "reads the process environment" }
687
+ fetch: { effects: [global.read], why: "reads the process environment" }
688
+ key?: { effects: [global.read], why: "reads the process environment" }
689
+ has_key?: { effects: [global.read], why: "reads the process environment" }
690
+ include?: { effects: [global.read], why: "reads the process environment" }
691
+ member?: { effects: [global.read], why: "reads the process environment" }
692
+ keys: { effects: [global.read], why: "reads the process environment" }
693
+ values: { effects: [global.read], why: "reads the process environment" }
694
+ values_at: { effects: [global.read], why: "reads the process environment" }
695
+ to_h: { effects: [global.read], why: "reads the process environment" }
696
+ to_hash: { effects: [global.read], why: "reads the process environment" }
697
+ each: { effects: [global.read], why: "reads the process environment; the block joins by containment" }
698
+ each_pair: { effects: [global.read], why: "reads the process environment; the block joins by containment" }
699
+ dig: { effects: [global.read], why: "reads the process environment" }
700
+ "[]=": { effects: [global.write], why: "writes the process environment, which every later read and every subprocess observes" }
701
+ store: { effects: [global.write], why: "writes the process environment" }
702
+ delete: { effects: [global.write], why: "writes the process environment" }
703
+ update: { effects: [global.write], why: "writes the process environment" }
704
+ merge!: { effects: [global.write], why: "writes the process environment" }
705
+ replace: { effects: [global.write], why: "writes the process environment" }
706
+ clear: { effects: [global.write], why: "writes the process environment" }
707
+
708
+ ARGF:
709
+ kind: object
710
+ posture: stdin
711
+ why: "ARGF reads the files named on the command line, or $stdin"
712
+ STDIN:
713
+ kind: object
714
+ posture: stdin
715
+ why: "the process's standard input"
716
+ STDOUT:
717
+ kind: object
718
+ posture: stdout
719
+ why: "the process's standard output"
720
+ STDERR:
721
+ kind: object
722
+ posture: stderr
723
+ why: "the process's standard error"
724
+
725
+ Thread:
726
+ posture: value
727
+ why: "a Thread object's accessors are ordinary reads; the world-facing rows are the blocking ones"
728
+ methods:
729
+ join: { effects: [io], why: "blocks until another thread finishes — a wait the world's scheduling decides (the same reading WD14 gives sleep)" }
730
+ value: { effects: [io], why: "joins, then reads the result" }
731
+ "[]": { effects: [global.read], why: "fiber-local storage is state shared beyond this frame" }
732
+ "[]=": { effects: [global.write], why: "fiber-local storage is state shared beyond this frame" }
733
+ thread_variable_get: { effects: [global.read], why: "thread-local storage is state shared beyond this frame" }
734
+ thread_variable_set: { effects: [global.write], why: "thread-local storage is state shared beyond this frame" }
735
+ kill: { effects: [global.write], why: "terminates another thread, which every observer of its work sees" }
736
+ singleton_methods:
737
+ new: { effects: [], why: "the primitive carries no label; the block joins the enclosing method by containment (WD14)" }
738
+ start: { effects: [], why: "the primitive carries no label; the block joins by containment (WD14)" }
739
+ fork: { effects: [], why: "Thread.fork is Thread.start; no label, containment (WD14)" }
740
+ current: { effects: [], why: "returns the running thread object; the storage accessors on it are what carry global" }
741
+ pass: { effects: [], why: "a scheduling hint" }
742
+
743
+ Fiber:
744
+ posture: value
745
+ why: "a coroutine primitive; nothing leaves the process"
746
+ singleton_methods:
747
+ new: { effects: [], why: "the primitive carries no label; the block joins by containment (WD14)" }
748
+ yield: { effects: [], why: "transfers control within the process" }
749
+
750
+ Ractor:
751
+ posture: value
752
+ why: "a concurrency primitive; nothing leaves the process"
753
+ singleton_methods:
754
+ new: { effects: [], why: "the primitive carries no label; the block joins by containment (WD14)" }
755
+
756
+ Mutex:
757
+ posture: value
758
+ why: "a lock primitive; WD14 fixes it at ∅ + containment"
759
+ methods:
760
+ synchronize: { effects: [], why: "no label; the block joins the enclosing method by containment (WD14)" }
761
+ lock: { effects: [], why: "no label (WD14); a contended lock waits, but the waiting is not an observable effect of this code" }
762
+ unlock: { effects: [], why: "no label (WD14)" }
763
+ try_lock: { effects: [], why: "no label (WD14)" }
764
+ Monitor:
765
+ posture: value
766
+ why: "a reentrant Mutex; the same reading"
767
+ methods:
768
+ synchronize: { effects: [], why: "no label; the block joins by containment (WD14)" }
769
+
770
+ Queue:
771
+ posture: value
772
+ why: "an in-process channel; only the blocking ends are world-facing"
773
+ methods:
774
+ pop: { effects: [io], why: "blocks until another thread pushes — a wait the world decides (WD14)" }
775
+ shift: { effects: [io], why: "alias of pop (WD14)" }
776
+ deq: { effects: [io], why: "alias of pop (WD14)" }
777
+ push: { effects: [], why: "never blocks on a plain Queue" }
778
+ SizedQueue:
779
+ posture: value
780
+ why: "a Queue with a bound, so the push end blocks too"
781
+ methods:
782
+ pop: { effects: [io], why: "blocks until another thread pushes (WD14)" }
783
+ push: { effects: [io], why: "blocks when the queue is full (WD14)" }
784
+ !!str "<<": { effects: [io], why: "alias of push (WD14)" }
785
+ enq: { effects: [io], why: "alias of push (WD14)" }
786
+ ConditionVariable:
787
+ posture: value
788
+ why: "a wait primitive"
789
+ methods:
790
+ wait: { effects: [io], why: "blocks until another thread signals (WD14)" }
791
+ signal: { effects: [], why: "a wake-up, not a wait" }
792
+ broadcast: { effects: [], why: "a wake-up, not a wait" }
793
+
794
+ GC:
795
+ posture: global
796
+ why: "the collector is process-global machinery"
797
+ singleton_methods:
798
+ stat: { effects: [global.read], why: "reads collector counters" }
799
+ count: { effects: [global.read], why: "reads a collector counter" }
800
+ stress: { effects: [global.read], why: "reads the collector's stress flag" }
801
+ disable: { effects: [global.write], why: "changes collector behaviour for the whole process" }
802
+ enable: { effects: [global.write], why: "changes collector behaviour for the whole process" }
803
+ start: { effects: [global.write], why: "runs a collection, which every allocation timing observes" }
804
+ compact: { effects: [global.write], why: "moves objects for the whole process" }
805
+
806
+ ObjectSpace:
807
+ posture: global
808
+ why: "reads and writes the process's whole object graph"
809
+ singleton_methods:
810
+ each_object: { effects: [global.read], why: "walks every live object; the block joins by containment" }
811
+ count_objects: { effects: [global.read], why: "reads allocator counters" }
812
+ define_finalizer: { effects: [global.write], why: "installs a process-global finalizer" }
813
+ undefine_finalizer: { effects: [global.write], why: "removes a process-global finalizer" }
814
+ garbage_collect: { effects: [global.write], why: "runs a collection" }
815
+
816
+ Warning:
817
+ posture: value
818
+ why: "only the category switch is global"
819
+ singleton_methods:
820
+ "[]": { effects: [global.read], why: "reads a process-global warning category" }
821
+ "[]=": { effects: [global.write], why: "writes a process-global warning category" }
822
+ warn: { effects: [io.output.stderr], why: "writes $stderr" }
823
+
824
+ Encoding:
825
+ posture: value
826
+ why: "an Encoding is a value; only the process-wide default switches are global"
827
+ singleton_methods:
828
+ default_external: { effects: [global.read], why: "reads a process-global default" }
829
+ "default_external=": { effects: [global.write], why: "writes a process-global default every later IO observes" }
830
+ default_internal: { effects: [global.read], why: "reads a process-global default" }
831
+ "default_internal=": { effects: [global.write], why: "writes a process-global default every later IO observes" }
832
+ find: { effects: [], why: "looks a name up in a constant table" }
833
+ list: { effects: [], why: "reads a constant table" }
834
+
835
+ # ---------------------------------------------------------------------------------------------
836
+ # Logging and FFI.
837
+ # ---------------------------------------------------------------------------------------------
838
+ Logger:
839
+ posture: value
840
+ why: "a Logger's configuration accessors are ordinary; the emitters are the tolerated-policy poster child"
841
+ methods:
842
+ debug: { effects: [io, telemetry], why: "writes an unknown destination (file, socket, stdout) and carries telemetry meaning" }
843
+ info: { effects: [io, telemetry], why: "writes an unknown destination and carries telemetry meaning" }
844
+ warn: { effects: [io, telemetry], why: "writes an unknown destination and carries telemetry meaning" }
845
+ error: { effects: [io, telemetry], why: "writes an unknown destination and carries telemetry meaning" }
846
+ fatal: { effects: [io, telemetry], why: "writes an unknown destination and carries telemetry meaning" }
847
+ unknown: { effects: [io, telemetry], why: "writes an unknown destination and carries telemetry meaning" }
848
+ add: { effects: [io, telemetry], why: "the primitive every level method forwards to" }
849
+ log: { effects: [io, telemetry], why: "alias of add" }
850
+ !!str "<<": { effects: [io, telemetry], why: "writes the destination raw" }
851
+ level: { effects: [], why: "reads a field of the Logger object" }
852
+
853
+ "Fiddle::Function":
854
+ posture: value
855
+ why: "the object is a value; calling it leaves Ruby"
856
+ methods:
857
+ call: { effects: [ffi], why: "transfers control to a native function, whose effects Ruby cannot see" }
858
+ "Fiddle::Handle":
859
+ posture: ffi
860
+ why: "opens and reads a shared object, then hands out native function pointers"