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,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "label_set"
4
+ require_relative "origin"
5
+ require_relative "taint_cause"
6
+
7
+ module Rigor
8
+ module Effects
9
+ # One method's effect summary — the value the collector produces and the propagator closes over
10
+ # (ADR-103 WD1 / WD12; the lanes and the exhaustiveness bit are normative in
11
+ # `docs/type-specification/effect-labels.md`).
12
+ #
13
+ # Three carriers and one projection:
14
+ #
15
+ # - {#bundles} — `{Origin => LabelSet}`, the labels kept **per origin** so the judgment can discharge
16
+ # policy per origin (tolerating what an origin was *for* frees its transport, WD14; {Discharge}).
17
+ # - {#declared_bundles} — the same shape for the `≤` lane, and {#declared} its flat projection. Its one
18
+ # producer today is the project's `effects.attribution:` table (#385), whose origins carry
19
+ # {Origin::ATTRIBUTION}; a *nominal supertype's* envelope joins it with #386. The lane is kept apart
20
+ # from {#bundles} because nothing declared is ever proven: diagnostics read {#proven} only, so an
21
+ # attributed label can never manufacture an `effect.envelope-exceeded`.
22
+ # - {#exhaustive} — false when some call this method makes could not be resolved.
23
+ # - {#causes} — why, from {TaintCause}'s closed enum, as `[cause, detail]` pairs. Empty when exhaustive.
24
+ #
25
+ # {#proven} is the flat projection of {#bundles}: the join of every bundle. It is computed once at
26
+ # construction rather than memoised, because the instance is frozen (and so crosses a fork-pool marshal
27
+ # and a Ractor boundary as it is).
28
+ #
29
+ # Summaries compose by {#join}, which is associative and commutative on every field, so merging a
30
+ # project's per-file collections in any order — pooled or sequential — yields the same table.
31
+ class Summary
32
+ NO_BUNDLES = {}.freeze
33
+ private_constant :NO_BUNDLES
34
+
35
+ NO_CAUSES = [].freeze
36
+ private_constant :NO_CAUSES
37
+
38
+ # The bound {#trivial?} measures against. Not a user-facing knob: it is `%a{pure}`'s carve-out,
39
+ # spelled once.
40
+ TRIVIAL_BOUND = LabelSet.new(["mutate.local"])
41
+
42
+ # The pure-modulo-frame-ownership summary: no origins, nothing declared, exhaustive, untainted.
43
+ def self.empty
44
+ @empty ||= new
45
+ end
46
+
47
+ # Builds a summary carrying a single taint and no labels — the fail-soft answer for a unit the
48
+ # collector could not finish (`collector-error`) and the shape a caller uses to taint a method it
49
+ # knows nothing else about.
50
+ def self.tainted(cause, detail = nil)
51
+ new(exhaustive: false, causes: [[cause, detail]])
52
+ end
53
+
54
+ attr_reader :bundles, :declared_bundles, :declared, :causes, :proven
55
+
56
+ def initialize(bundles: NO_BUNDLES, declared_bundles: NO_BUNDLES, exhaustive: true, causes: NO_CAUSES)
57
+ @bundles = normalize_bundles(bundles)
58
+ @declared_bundles = normalize_bundles(declared_bundles)
59
+ @exhaustive = exhaustive ? true : false
60
+ @causes = normalize_causes(causes)
61
+ @proven = flatten(@bundles)
62
+ @declared = flatten(@declared_bundles)
63
+ freeze
64
+ end
65
+
66
+ # Whether every call this method makes was resolved. A false bit reads "these effects, and possibly
67
+ # more" and MUST NOT on its own produce a finding.
68
+ def exhaustive?
69
+ @exhaustive
70
+ end
71
+
72
+ # Whether this summary is worth showing at all: an exhaustive method whose whole proven footprint is
73
+ # frame-local mutation and which claims nothing. `mutate.local` is tolerated by every envelope
74
+ # (`%a{pure}` included), so a method that only mutates what its own frame allocated reads as pure
75
+ # for reporting purposes. The report omits these unless `--full` is given; the snapshot of #381
76
+ # omits them from `methods:`.
77
+ #
78
+ # A surviving DECLARED label makes the row non-trivial (#386): `≤ io.db` is exactly the kind of
79
+ # thing a reviewer reads a report or a snapshot diff for, and dropping the row would say "clean"
80
+ # about a method that claims otherwise. "Surviving" is the rendering rule — a declared label the
81
+ # proven lane already admits says nothing new and cannot rescue a row on its own.
82
+ def trivial?
83
+ @exhaustive && @proven.subsumed_by?(TRIVIAL_BOUND) && @declared.excluding_subsumed_by(@proven).empty?
84
+ end
85
+
86
+ # Union in every lane: bundles join per origin, the declared lane joins, the exhaustiveness bit ANDs,
87
+ # causes union. Reopenings of one method and the several files that contribute to it fold through
88
+ # this.
89
+ def join(other)
90
+ return self if other.equal?(self)
91
+
92
+ self.class.new(
93
+ bundles: merge_bundles(@bundles, other.bundles),
94
+ declared_bundles: merge_bundles(@declared_bundles, other.declared_bundles),
95
+ exhaustive: @exhaustive && other.exhaustive?,
96
+ causes: @causes + other.causes
97
+ )
98
+ end
99
+
100
+ def ==(other)
101
+ other.is_a?(Summary) && other.bundles == @bundles && other.declared_bundles == @declared_bundles &&
102
+ other.exhaustive? == @exhaustive && other.causes == @causes
103
+ end
104
+ alias eql? ==
105
+
106
+ def hash
107
+ [self.class, @bundles, @declared_bundles, @exhaustive, @causes].hash
108
+ end
109
+
110
+ def inspect
111
+ "#<Rigor::Effects::Summary #{@proven.to_a.join(', ')}#{' ?' unless @exhaustive}>"
112
+ end
113
+
114
+ private
115
+
116
+ def merge_bundles(mine, theirs)
117
+ return mine if theirs.empty?
118
+ return theirs if mine.empty?
119
+
120
+ merged = mine.dup
121
+ theirs.each { |origin, set| merged[origin] = merged.key?(origin) ? merged[origin].join(set) : set }
122
+ merged
123
+ end
124
+
125
+ def flatten(bundles)
126
+ bundles.each_value.reduce(LabelSet::EMPTY) { |acc, set| acc.join(set) }
127
+ end
128
+
129
+ # Origins are sorted by their rendering so two summaries built from the same facts in different
130
+ # orders are `==` and render identically. A repeated origin joins rather than clobbers.
131
+ def normalize_bundles(bundles)
132
+ return NO_BUNDLES if bundles.empty?
133
+
134
+ merged = {}
135
+ bundles.each do |origin, set|
136
+ merged[origin] = merged.key?(origin) ? merged[origin].join(set) : set
137
+ end
138
+ merged.reject { |_, set| set.empty? }.sort_by { |origin, _| origin.to_s }.to_h.freeze
139
+ end
140
+
141
+ # `[cause, detail]` pairs, de-duplicated and sorted. A cause outside {TaintCause}'s enum is dropped
142
+ # rather than raised on: the collector is fail-soft, and a producer bug must not take a run down.
143
+ def normalize_causes(causes)
144
+ return NO_CAUSES if causes.empty?
145
+
146
+ causes
147
+ .filter_map { |cause, detail| [cause.to_s, detail&.to_s].freeze if TaintCause.valid?(cause) }
148
+ .uniq
149
+ .sort_by { |cause, detail| [cause, detail.to_s] }
150
+ .freeze
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Effects
5
+ # The closed enum of reasons a method's effect summary is not exhaustive (ADR-103 WD14;
6
+ # normative in `docs/type-specification/effect-labels.md`).
7
+ #
8
+ # A tainted summary reads "these effects, and possibly more": the cause says which knowledge the
9
+ # analyzer could not have. Taint never produces a finding — diagnostics read the proven lane
10
+ # only — so this enum is a reporting vocabulary, not a diagnostic one.
11
+ #
12
+ # Nothing produces causes yet; the collector of #379 is the first writer.
13
+ module TaintCause
14
+ # Closed and ordered by how it arises: dispatch the analyzer could not resolve first, then
15
+ # attribution it had to take on trust, then the collector's own limits.
16
+ ALL = %w[
17
+ dynamic-receiver
18
+ dynamic-send
19
+ method-missing
20
+ unresolved-self-call
21
+ opaque-callable
22
+ unknown-ownership
23
+ plugin-attribution
24
+ template-not-analysed
25
+ collector-error
26
+ budget
27
+ ].freeze
28
+
29
+ module_function
30
+
31
+ # Whether `cause` is one of the enum's members. The enum is closed: a spelling outside it is
32
+ # a bug in the producer, not a vocabulary extension point.
33
+ def valid?(cause)
34
+ ALL.include?(cause.to_s)
35
+ end
36
+ end
37
+ end
38
+ end