rigortype 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) 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 +6 -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/rule_ids.rb +28 -2
  33. data/lib/rigor/analysis/incremental_session.rb +140 -6
  34. data/lib/rigor/analysis/reachability/graph.rb +244 -0
  35. data/lib/rigor/analysis/reachability/plugin_roots.rb +164 -0
  36. data/lib/rigor/analysis/reachability/project_files.rb +58 -0
  37. data/lib/rigor/analysis/reachability/scan.rb +241 -0
  38. data/lib/rigor/analysis/reachability/signature_scan.rb +134 -0
  39. data/lib/rigor/analysis/rule_catalog.rb +158 -0
  40. data/lib/rigor/analysis/run_cache_key.rb +16 -0
  41. data/lib/rigor/analysis/runner/declaration_position.rb +54 -0
  42. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
  43. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
  44. data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
  45. data/lib/rigor/analysis/runner/pool_coordinator.rb +68 -10
  46. data/lib/rigor/analysis/runner.rb +420 -10
  47. data/lib/rigor/analysis/worker_session.rb +58 -2
  48. data/lib/rigor/bleeding_edge.rb +22 -0
  49. data/lib/rigor/cache/incremental_snapshot.rb +24 -5
  50. data/lib/rigor/cli/check_command.rb +13 -1
  51. data/lib/rigor/cli/check_runner_factory.rb +4 -1
  52. data/lib/rigor/cli/effects_command.rb +131 -0
  53. data/lib/rigor/cli/effects_diff_renderer.rb +123 -0
  54. data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
  55. data/lib/rigor/cli/effects_renderer.rb +54 -0
  56. data/lib/rigor/cli/effects_report.rb +53 -0
  57. data/lib/rigor/cli/effects_snapshot_command.rb +256 -0
  58. data/lib/rigor/cli/unused_command.rb +288 -0
  59. data/lib/rigor/cli.rb +16 -0
  60. data/lib/rigor/configuration/severity_profile.rb +23 -0
  61. data/lib/rigor/configuration.rb +296 -14
  62. data/lib/rigor/effects/attribution.rb +76 -0
  63. data/lib/rigor/effects/catalog.rb +275 -0
  64. data/lib/rigor/effects/collector.rb +182 -0
  65. data/lib/rigor/effects/config_envelopes.rb +185 -0
  66. data/lib/rigor/effects/discharge.rb +69 -0
  67. data/lib/rigor/effects/effect_table.rb +92 -0
  68. data/lib/rigor/effects/entry_points.rb +93 -0
  69. data/lib/rigor/effects/envelope.rb +86 -0
  70. data/lib/rigor/effects/envelope_check.rb +172 -0
  71. data/lib/rigor/effects/envelope_index.rb +157 -0
  72. data/lib/rigor/effects/file_collection.rb +160 -0
  73. data/lib/rigor/effects/framework_units.rb +167 -0
  74. data/lib/rigor/effects/identity.rb +104 -0
  75. data/lib/rigor/effects/label.rb +77 -0
  76. data/lib/rigor/effects/label_intent.rb +73 -0
  77. data/lib/rigor/effects/label_set.rb +136 -0
  78. data/lib/rigor/effects/liskov_check.rb +167 -0
  79. data/lib/rigor/effects/local_ownership.rb +132 -0
  80. data/lib/rigor/effects/method_key.rb +40 -0
  81. data/lib/rigor/effects/mutation_classifier.rb +92 -0
  82. data/lib/rigor/effects/narrowing.rb +202 -0
  83. data/lib/rigor/effects/origin.rb +66 -0
  84. data/lib/rigor/effects/path_finder.rb +89 -0
  85. data/lib/rigor/effects/plugin_facts.rb +335 -0
  86. data/lib/rigor/effects/propagator.rb +275 -0
  87. data/lib/rigor/effects/registry.rb +193 -0
  88. data/lib/rigor/effects/scanner.rb +290 -0
  89. data/lib/rigor/effects/signature_sources.rb +74 -0
  90. data/lib/rigor/effects/snapshot.rb +380 -0
  91. data/lib/rigor/effects/snapshot_diff.rb +265 -0
  92. data/lib/rigor/effects/summary.rb +154 -0
  93. data/lib/rigor/effects/taint_cause.rb +38 -0
  94. data/lib/rigor/effects/unit_scan.rb +572 -0
  95. data/lib/rigor/effects/unknown_label_check.rb +86 -0
  96. data/lib/rigor/effects/unknown_label_report.rb +59 -0
  97. data/lib/rigor/effects.rb +47 -0
  98. data/lib/rigor/environment/rbs_loader.rb +34 -0
  99. data/lib/rigor/environment.rb +8 -1
  100. data/lib/rigor/flow_contribution/element.rb +1 -0
  101. data/lib/rigor/flow_contribution/merge_result.rb +5 -3
  102. data/lib/rigor/flow_contribution/merger.rb +16 -1
  103. data/lib/rigor/flow_contribution.rb +20 -4
  104. data/lib/rigor/inference/expression_typer.rb +18 -4
  105. data/lib/rigor/inference/pre_eval_constants.rb +189 -0
  106. data/lib/rigor/inference/scope_indexer.rb +15 -3
  107. data/lib/rigor/plugin/base.rb +24 -0
  108. data/lib/rigor/plugin/effect_attribution.rb +208 -0
  109. data/lib/rigor/plugin/effect_edge.rb +101 -0
  110. data/lib/rigor/plugin/effect_entry_points.rb +51 -0
  111. data/lib/rigor/plugin/first_party.rb +57 -0
  112. data/lib/rigor/plugin/isolation.rb +4 -1
  113. data/lib/rigor/plugin/manifest.rb +103 -3
  114. data/lib/rigor/plugin/registry.rb +48 -0
  115. data/lib/rigor/plugin.rb +1 -0
  116. data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
  117. data/lib/rigor/rbs_extended.rb +168 -0
  118. data/lib/rigor/reflection.rb +131 -30
  119. data/lib/rigor/version.rb +1 -1
  120. data/lib/rigor.rb +1 -0
  121. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
  122. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
  123. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
  124. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
  125. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
  126. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
  127. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
  128. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
  129. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
  130. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
  131. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
  132. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
  133. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
  134. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
  135. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
  136. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
  137. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +385 -28
  138. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
  139. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
  140. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
  141. data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
  142. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
  143. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
  144. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
  145. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
  146. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
  147. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
  148. data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
  149. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
  150. data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
  151. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
  152. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +56 -3
  153. data/sig/rigor/analysis/fact_store.rbs +1 -0
  154. data/sig/rigor/inference.rbs +2 -0
  155. data/sig/rigor/rbs_extended.rbs +6 -0
  156. data/sig/rigor.rbs +17 -1
  157. data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
  158. metadata +79 -1
@@ -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