rigortype 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/builtins/ruby_core/array.yml +11 -24
- data/data/builtins/ruby_core/enumerable.yml +4 -8
- data/data/builtins/ruby_core/exception.yml +6 -9
- data/data/builtins/ruby_core/hash.yml +13 -20
- data/data/builtins/ruby_core/io.yml +6 -9
- data/data/builtins/ruby_core/numeric.yml +2 -3
- data/data/builtins/ruby_core/proc.yml +4 -8
- data/data/builtins/ruby_core/re.yml +6 -9
- data/data/builtins/ruby_core/set.yml +2 -3
- data/data/builtins/ruby_core/string.yml +4 -7
- data/data/builtins/ruby_core/struct.yml +0 -6
- data/data/builtins/ruby_core/time.yml +3 -6
- data/data/effects/core.yml +843 -0
- data/data/effects/registry.yml +67 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +311 -1
- data/docs/manual/03-configuration.md +84 -1
- data/docs/manual/04-diagnostics.md +14 -2
- data/docs/manual/16-rbs-extended-annotations.md +77 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/README.md +5 -0
- data/docs/manual/plugins/rigor-actionmailer.md +5 -0
- data/docs/manual/plugins/rigor-activejob.md +47 -0
- data/docs/manual/plugins/rigor-factorybot.md +16 -0
- data/docs/manual/plugins/rigor-pundit.md +26 -0
- data/docs/manual/plugins/rigor-rails-routes.md +41 -2
- data/docs/manual/plugins/rigor-rspec-rails.md +5 -0
- data/docs/manual/plugins/rigor-rspec.md +8 -0
- data/docs/manual/plugins/rigor-sidekiq.md +54 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
- data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +62 -22
- data/lib/rigor/analysis/incremental_session.rb +140 -6
- data/lib/rigor/analysis/reachability/graph.rb +244 -0
- data/lib/rigor/analysis/reachability/plugin_roots.rb +164 -0
- data/lib/rigor/analysis/reachability/project_files.rb +58 -0
- data/lib/rigor/analysis/reachability/scan.rb +241 -0
- data/lib/rigor/analysis/reachability/signature_scan.rb +134 -0
- data/lib/rigor/analysis/rule_catalog.rb +163 -1
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +54 -0
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
- data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +68 -10
- data/lib/rigor/analysis/runner.rb +420 -10
- data/lib/rigor/analysis/severity_stamp.rb +2 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/bleeding_edge.rb +22 -0
- data/lib/rigor/cache/incremental_snapshot.rb +24 -5
- data/lib/rigor/cli/check_command.rb +13 -1
- data/lib/rigor/cli/check_runner_factory.rb +4 -1
- data/lib/rigor/cli/effects_command.rb +131 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +123 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +54 -0
- data/lib/rigor/cli/effects_report.rb +53 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +256 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +16 -0
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +296 -14
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +182 -0
- data/lib/rigor/effects/config_envelopes.rb +185 -0
- data/lib/rigor/effects/discharge.rb +69 -0
- data/lib/rigor/effects/effect_table.rb +92 -0
- data/lib/rigor/effects/entry_points.rb +93 -0
- data/lib/rigor/effects/envelope.rb +86 -0
- data/lib/rigor/effects/envelope_check.rb +172 -0
- data/lib/rigor/effects/envelope_index.rb +157 -0
- data/lib/rigor/effects/file_collection.rb +160 -0
- data/lib/rigor/effects/framework_units.rb +167 -0
- data/lib/rigor/effects/identity.rb +104 -0
- data/lib/rigor/effects/label.rb +77 -0
- data/lib/rigor/effects/label_intent.rb +73 -0
- data/lib/rigor/effects/label_set.rb +136 -0
- data/lib/rigor/effects/liskov_check.rb +167 -0
- data/lib/rigor/effects/local_ownership.rb +132 -0
- data/lib/rigor/effects/method_key.rb +40 -0
- data/lib/rigor/effects/mutation_classifier.rb +92 -0
- data/lib/rigor/effects/narrowing.rb +202 -0
- data/lib/rigor/effects/origin.rb +66 -0
- data/lib/rigor/effects/path_finder.rb +89 -0
- data/lib/rigor/effects/plugin_facts.rb +335 -0
- data/lib/rigor/effects/propagator.rb +275 -0
- data/lib/rigor/effects/registry.rb +193 -0
- data/lib/rigor/effects/scanner.rb +290 -0
- data/lib/rigor/effects/signature_sources.rb +74 -0
- data/lib/rigor/effects/snapshot.rb +380 -0
- data/lib/rigor/effects/snapshot_diff.rb +265 -0
- data/lib/rigor/effects/summary.rb +154 -0
- data/lib/rigor/effects/taint_cause.rb +38 -0
- data/lib/rigor/effects/unit_scan.rb +572 -0
- data/lib/rigor/effects/unknown_label_check.rb +86 -0
- data/lib/rigor/effects/unknown_label_report.rb +59 -0
- data/lib/rigor/effects.rb +47 -0
- data/lib/rigor/environment/rbs_loader.rb +34 -0
- data/lib/rigor/environment.rb +8 -1
- data/lib/rigor/flow_contribution/element.rb +1 -0
- data/lib/rigor/flow_contribution/merge_result.rb +5 -3
- data/lib/rigor/flow_contribution/merger.rb +16 -1
- data/lib/rigor/flow_contribution.rb +20 -4
- data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +74 -17
- data/lib/rigor/inference/fork_map.rb +45 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +147 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +97 -2
- data/lib/rigor/inference/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/optimistic_origin.rb +68 -3
- data/lib/rigor/inference/pre_eval_constants.rb +189 -0
- data/lib/rigor/inference/scope_indexer.rb +134 -31
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +25 -12
- data/lib/rigor/plugin/base.rb +24 -0
- data/lib/rigor/plugin/effect_attribution.rb +208 -0
- data/lib/rigor/plugin/effect_edge.rb +101 -0
- data/lib/rigor/plugin/effect_entry_points.rb +51 -0
- data/lib/rigor/plugin/first_party.rb +57 -0
- data/lib/rigor/plugin/isolation.rb +4 -1
- data/lib/rigor/plugin/manifest.rb +103 -3
- data/lib/rigor/plugin/node_rule_walk.rb +6 -0
- data/lib/rigor/plugin/registry.rb +48 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +20 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +168 -0
- data/lib/rigor/reflection.rb +131 -30
- data/lib/rigor/scope.rb +62 -2
- data/lib/rigor/source/node_walker.rb +12 -0
- data/lib/rigor/type/anonymous_class_name.rb +40 -0
- data/lib/rigor/type/nominal.rb +5 -2
- data/lib/rigor/type/singleton.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +385 -28
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
- data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
- data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
- data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +56 -3
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor/rbs_extended.rbs +6 -0
- data/sig/rigor/scope.rbs +4 -0
- data/sig/rigor.rbs +21 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +83 -1
data/lib/rigor/scope.rb
CHANGED
|
@@ -23,6 +23,7 @@ module Rigor
|
|
|
23
23
|
:indexed_narrowings, :method_chain_narrowings,
|
|
24
24
|
:declaration_sourced,
|
|
25
25
|
:source_path, :discovery, :struct_fold_safe_locals,
|
|
26
|
+
:opaque_block_self,
|
|
26
27
|
:dynamic_origins, :local_origins, :ivar_origins,
|
|
27
28
|
:void_origins,
|
|
28
29
|
:optimistic_origins, :optimistic_locals, :optimistic_ivars
|
|
@@ -147,6 +148,7 @@ module Rigor
|
|
|
147
148
|
declaration_sourced: EMPTY_DECLARATION_SOURCED,
|
|
148
149
|
source_path: nil,
|
|
149
150
|
struct_fold_safe_locals: EMPTY_FOLD_SAFE,
|
|
151
|
+
opaque_block_self: false,
|
|
150
152
|
dynamic_origins: {}.compare_by_identity,
|
|
151
153
|
local_origins: EMPTY_ORIGINS,
|
|
152
154
|
ivar_origins: EMPTY_ORIGINS,
|
|
@@ -168,6 +170,7 @@ module Rigor
|
|
|
168
170
|
@declaration_sourced = declaration_sourced
|
|
169
171
|
@source_path = source_path
|
|
170
172
|
@struct_fold_safe_locals = struct_fold_safe_locals
|
|
173
|
+
@opaque_block_self = opaque_block_self
|
|
171
174
|
@dynamic_origins = dynamic_origins
|
|
172
175
|
@local_origins = local_origins
|
|
173
176
|
@ivar_origins = ivar_origins
|
|
@@ -274,6 +277,21 @@ module Rigor
|
|
|
274
277
|
rebuild(struct_fold_safe_locals: locals)
|
|
275
278
|
end
|
|
276
279
|
|
|
280
|
+
# Issue #316 — marks a block body whose `self` Rigor does not model. Ruby gives a block no `self` of its
|
|
281
|
+
# own: the yielding method decides, and `instance_eval` / `instance_exec` (the mechanism behind every
|
|
282
|
+
# `self`-rebinding DSL — RSpec example groups, `Class.new { … }`, Rake, Sinatra) is indistinguishable from
|
|
283
|
+
# `Array#each` without knowing the callee. The flag is set at every block entry that leaves `self_type`
|
|
284
|
+
# unnarrowed and is inherited by every scope derived inside the block; it never leaks past the block,
|
|
285
|
+
# because `eval_call` returns the caller's scope unchanged.
|
|
286
|
+
def entering_opaque_block
|
|
287
|
+
return self if @opaque_block_self
|
|
288
|
+
|
|
289
|
+
rebuild(opaque_block_self: true)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
|
|
293
|
+
def opaque_block_self? = @opaque_block_self
|
|
294
|
+
|
|
277
295
|
# True when `name`'s `Struct` member reads are fold-safe in this body (the local is provably never mutated /
|
|
278
296
|
# aliased / escaped).
|
|
279
297
|
def struct_fold_safe?(name)
|
|
@@ -352,8 +370,11 @@ module Rigor
|
|
|
352
370
|
# whose type is an open-call-site *lower bound* — firing against a lower bound is a false positive by
|
|
353
371
|
# construction (the ADR-67 WD1 reasoning at the parameter boundary, carried one hop into the body). The
|
|
354
372
|
# distinct kind keeps the inferred-param sites separable from ADR-58's ivar-copy `:local` mark, which a
|
|
355
|
-
# later un-guarding slice (WD6b) needs
|
|
356
|
-
#
|
|
373
|
+
# later un-guarding slice (WD6b) needs — and the two kinds behave OPPOSITELY on both axes: `:local` is
|
|
374
|
+
# dropped by `with_local` and intersected by `join`, while `:inferred_param` is sticky across `with_local`
|
|
375
|
+
# and unioned by `join`. See {#without_inferred_param_mark} below for the clearing contract, and
|
|
376
|
+
# `docs/internal-spec/inference-engine.md` § "Declaration-sourced provenance mark (ADR-58)" for the
|
|
377
|
+
# normative statement of both.
|
|
357
378
|
def with_inferred_param_mark(name)
|
|
358
379
|
rebuild(declaration_sourced: add_declaration_sourced(:inferred_param, name))
|
|
359
380
|
end
|
|
@@ -505,6 +526,43 @@ module Rigor
|
|
|
505
526
|
Inference::DefNodeResolver.resolve(entry)
|
|
506
527
|
end
|
|
507
528
|
|
|
529
|
+
# Issue #316 — the CONFIDENCE-GATED companion of {#top_level_def_for}, and the only accessor the type
|
|
530
|
+
# inference may bind through. {#top_level_def_for} stays unrestricted because it also serves the
|
|
531
|
+
# *suppression* side (`call.unresolved-toplevel`, `call.undefined-method`): a name the project defines at
|
|
532
|
+
# the top level must never be reported as unresolved, whatever this gate decides.
|
|
533
|
+
#
|
|
534
|
+
# Returns nil — decline to bind, stay silent — when BOTH hold:
|
|
535
|
+
#
|
|
536
|
+
# 1. The call site sits inside a block whose `self` is unmodelled ({#opaque_block_self?}) and no narrowed
|
|
537
|
+
# `self_type` says otherwise. A top-level `def` is a private method on `Object`, so it is *callable*
|
|
538
|
+
# from any `self`; what the analyzer cannot see is whether the block's real `self` gained a PUBLIC
|
|
539
|
+
# same-named method by `include` / `extend`, which wins the MRO over the private `Object` def. RSpec's
|
|
540
|
+
# `output` / `include` / `match` matchers against a project's own `def output` are exactly this.
|
|
541
|
+
# 2. The `def` lives in a DIFFERENT file from the call site. Collocation is the evidence that the two
|
|
542
|
+
# belong to one lexical structure — the `RSpec.describe do; def helper; end; it { helper } end` case
|
|
543
|
+
# v0.0.3 A and #319 deliberately serve. Cross-file, the two share only a name.
|
|
544
|
+
#
|
|
545
|
+
# Both conditions are required, so a top-level helper called from genuine top-level code keeps resolving
|
|
546
|
+
# (cross-file included), and a helper defined beside its DSL-block call site keeps resolving too. When the
|
|
547
|
+
# project pre-pass recorded no source for the name, the file test cannot be answered and the historical
|
|
548
|
+
# bind is kept.
|
|
549
|
+
def bindable_top_level_def_for(method_name)
|
|
550
|
+
node = top_level_def_for(method_name)
|
|
551
|
+
return node if node.nil?
|
|
552
|
+
return node unless @opaque_block_self && @self_type.nil?
|
|
553
|
+
|
|
554
|
+
same_file_top_level_def?(method_name) ? node : nil
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def same_file_top_level_def?(method_name)
|
|
558
|
+
key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
559
|
+
site = discovered_def_sources.dig(key, method_name.to_sym)
|
|
560
|
+
return true if site.nil? || @source_path.nil?
|
|
561
|
+
|
|
562
|
+
File.expand_path(site.sub(/:\d+\z/, "")) == File.expand_path(@source_path)
|
|
563
|
+
end
|
|
564
|
+
private :same_file_top_level_def?
|
|
565
|
+
|
|
508
566
|
# ADR-46 slice 3 — a top-level (`def helper` outside any class) call has NO class ancestry to walk, so unlike
|
|
509
567
|
# {#user_def_for} a miss here records no positive ancestry edge that would re-check the consumer when the
|
|
510
568
|
# method later appears. Record the cross-file edge explicitly: the file defining the top-level method
|
|
@@ -756,6 +814,7 @@ module Rigor
|
|
|
756
814
|
declaration_sourced: @declaration_sourced,
|
|
757
815
|
source_path: @source_path,
|
|
758
816
|
struct_fold_safe_locals: @struct_fold_safe_locals,
|
|
817
|
+
opaque_block_self: @opaque_block_self,
|
|
759
818
|
dynamic_origins: @dynamic_origins,
|
|
760
819
|
local_origins: @local_origins,
|
|
761
820
|
ivar_origins: @ivar_origins,
|
|
@@ -774,6 +833,7 @@ module Rigor
|
|
|
774
833
|
declaration_sourced: declaration_sourced,
|
|
775
834
|
source_path: source_path,
|
|
776
835
|
struct_fold_safe_locals: struct_fold_safe_locals,
|
|
836
|
+
opaque_block_self: opaque_block_self,
|
|
777
837
|
dynamic_origins: dynamic_origins,
|
|
778
838
|
local_origins: local_origins,
|
|
779
839
|
ivar_origins: ivar_origins,
|
|
@@ -14,6 +14,14 @@ module Rigor
|
|
|
14
14
|
#
|
|
15
15
|
# Non-Prism children (literals embedded in node attributes, virtual nodes, or `nil` slots) are silently
|
|
16
16
|
# skipped so callers can rely on every yielded value responding to the `Prism::Node` API.
|
|
17
|
+
#
|
|
18
|
+
# Issue #318 — a `Prism::DefinedNode`'s operand is never evaluated at runtime (`defined?` inspects the
|
|
19
|
+
# expression statically; it does not run it), so the walk yields the `DefinedNode` itself but does NOT
|
|
20
|
+
# descend into its `value` subtree. Every consumer of this walker treats a yielded node as "reachable,
|
|
21
|
+
# evaluated code" (mutation/break/return scans, the check-rules main-pass oracle, coverage and precision
|
|
22
|
+
# probes); walking into the operand would make them reason about code that can never run, which is
|
|
23
|
+
# exactly the false-positive class the issue reports (`defined?(@x) && ...` flagging a call that is
|
|
24
|
+
# actually inert).
|
|
17
25
|
module NodeWalker
|
|
18
26
|
module_function
|
|
19
27
|
|
|
@@ -30,6 +38,8 @@ module Rigor
|
|
|
30
38
|
return unless node.is_a?(Prism::Node)
|
|
31
39
|
|
|
32
40
|
yield node
|
|
41
|
+
return if node.is_a?(Prism::DefinedNode)
|
|
42
|
+
|
|
33
43
|
node.rigor_each_child { |child| walk(child, &) }
|
|
34
44
|
end
|
|
35
45
|
|
|
@@ -52,6 +62,8 @@ module Rigor
|
|
|
52
62
|
return unless node.is_a?(Prism::Node)
|
|
53
63
|
|
|
54
64
|
block.call(node, ancestors)
|
|
65
|
+
return if node.is_a?(Prism::DefinedNode)
|
|
66
|
+
|
|
55
67
|
ancestors.push(node)
|
|
56
68
|
node.rigor_each_child { |child| walk_with_ancestors(child, ancestors, &block) }
|
|
57
69
|
ancestors.pop
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Type
|
|
5
|
+
# #319 — the spelling of a class that Ruby created without a name: the `Class.new do ... end` /
|
|
6
|
+
# `Module.new do ... end` form away from constant-write position, which owns whatever its block body defines
|
|
7
|
+
# but has no constant to be keyed by.
|
|
8
|
+
#
|
|
9
|
+
# `Inference::AnonymousMetaClass` decides WHICH call sites get one and what goes in the key; this module owns
|
|
10
|
+
# the spelling, because the two places a name reaches a human live here. `#<Label:key>` is deliberately
|
|
11
|
+
# unspellable as a Ruby constant path, so a synthetic name can never collide with a real class in the
|
|
12
|
+
# discovery tables.
|
|
13
|
+
#
|
|
14
|
+
# Both renderings drop the key. `describe` keeps only the label (`#<Class>`) because the key is a file
|
|
15
|
+
# position — reproducing it would pin every `assert_type` fixture and precision snapshot to a line number.
|
|
16
|
+
# `erase_to_rbs` answers `untyped`: the name is not valid RBS, and emitting it from `rigor sig-gen` would
|
|
17
|
+
# produce a signature file that does not parse.
|
|
18
|
+
module AnonymousClassName
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
PREFIX = "#<"
|
|
22
|
+
|
|
23
|
+
# `build("Class", "lib/a.rb:3:17") #=> "#<Class:lib/a.rb:3:17>"`
|
|
24
|
+
def build(label, key)
|
|
25
|
+
"#{PREFIX}#{label}:#{key}>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def match?(class_name)
|
|
29
|
+
class_name.is_a?(String) && class_name.start_with?(PREFIX)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The display form: the label alone, key dropped.
|
|
33
|
+
def display(class_name)
|
|
34
|
+
return class_name unless match?(class_name)
|
|
35
|
+
|
|
36
|
+
"#{PREFIX}#{class_name.delete_prefix(PREFIX).split(':', 2).first}>"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/rigor/type/nominal.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../trinary"
|
|
4
4
|
require_relative "../value_semantics"
|
|
5
5
|
require_relative "acceptance_router"
|
|
6
|
+
require_relative "anonymous_class_name"
|
|
6
7
|
require_relative "plain_lattice"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
@@ -35,13 +36,15 @@ module Rigor
|
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def describe(verbosity = :short)
|
|
38
|
-
|
|
39
|
+
rendered_name = AnonymousClassName.display(class_name)
|
|
40
|
+
return rendered_name if type_args.empty?
|
|
39
41
|
|
|
40
42
|
rendered = type_args.map { |t| t.describe(verbosity) }.join(", ")
|
|
41
|
-
"#{
|
|
43
|
+
"#{rendered_name}[#{rendered}]"
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def erase_to_rbs
|
|
47
|
+
return "untyped" if AnonymousClassName.match?(class_name)
|
|
45
48
|
return class_name if type_args.empty?
|
|
46
49
|
|
|
47
50
|
rendered = type_args.map(&:erase_to_rbs).join(", ")
|
data/lib/rigor/type/singleton.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../trinary"
|
|
4
4
|
require_relative "../value_semantics"
|
|
5
5
|
require_relative "acceptance_router"
|
|
6
|
+
require_relative "anonymous_class_name"
|
|
6
7
|
require_relative "plain_lattice"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
@@ -26,10 +27,12 @@ module Rigor
|
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def describe(_verbosity = :short)
|
|
29
|
-
"singleton(#{class_name})"
|
|
30
|
+
"singleton(#{AnonymousClassName.display(class_name)})"
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def erase_to_rbs
|
|
34
|
+
return "untyped" if AnonymousClassName.match?(class_name)
|
|
35
|
+
|
|
33
36
|
"singleton(#{class_name})"
|
|
34
37
|
end
|
|
35
38
|
|
data/lib/rigor/version.rb
CHANGED
data/lib/rigor.rb
CHANGED
|
@@ -32,6 +32,7 @@ require_relative "rigor/cache/rbs_class_ancestor_table"
|
|
|
32
32
|
require_relative "rigor/cache/rbs_class_type_param_names"
|
|
33
33
|
require_relative "rigor/cache/rbs_environment"
|
|
34
34
|
require_relative "rigor/type_node"
|
|
35
|
+
require_relative "rigor/effects"
|
|
35
36
|
require_relative "rigor/flow_contribution"
|
|
36
37
|
require_relative "rigor/flow_contribution/fact"
|
|
37
38
|
require_relative "rigor/flow_contribution/conflict"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
class Actioncable < Rigor::Plugin::Base
|
|
8
|
+
# rigor-actioncable's effect contract (ADR-103 WD10; design note § 11.2; issue #387).
|
|
9
|
+
#
|
|
10
|
+
# A broadcast leaves the process — through Redis, through the async in-memory adapter, or through
|
|
11
|
+
# Solid Cable's database table, depending on `config/cable.yml`. Bare `io` is the only sound
|
|
12
|
+
# transport, and `rails.actioncable.broadcast` is what a reviewer means when they say "this model
|
|
13
|
+
# callback pushes to every connected browser".
|
|
14
|
+
#
|
|
15
|
+
# Turbo Streams' `broadcast_*` family rides the same rows: it is ActionCable underneath, and a
|
|
16
|
+
# `broadcast_replace_later_to` in a model callback is exactly the effect a policy on `app/models/**`
|
|
17
|
+
# exists to surface.
|
|
18
|
+
module Effects
|
|
19
|
+
CHANNEL = "ActionCable::Channel::Base"
|
|
20
|
+
SERVER = "ActionCable.server"
|
|
21
|
+
BROADCASTING = ["io", "rails.actioncable.broadcast"].freeze
|
|
22
|
+
|
|
23
|
+
# `Turbo::Broadcastable`'s class-body-declared broadcasts, called on a model instance.
|
|
24
|
+
TURBO_SELECTORS = %w[
|
|
25
|
+
broadcast_append_to broadcast_prepend_to broadcast_replace_to broadcast_update_to
|
|
26
|
+
broadcast_remove_to broadcast_before_to broadcast_after_to broadcast_action_to
|
|
27
|
+
broadcast_render_to broadcast_refresh_to
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
# The `_later` twins enqueue an ActiveJob that broadcasts, so they carry the enqueue meaning too.
|
|
31
|
+
TURBO_LATER_SELECTORS = TURBO_SELECTORS.map { |name| "#{name.delete_suffix('_to')}_later_to" }.freeze
|
|
32
|
+
|
|
33
|
+
LATER = (BROADCASTING + ["rails.activejob.enqueue", "job.enqueue"]).freeze
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def attributions
|
|
38
|
+
channel_rows + server_rows + turbo_rows
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def channel_rows
|
|
42
|
+
[
|
|
43
|
+
row(CHANNEL, :broadcast_to, BROADCASTING, singleton: true,
|
|
44
|
+
why: "publishes to the channel's stream — out of this process, through whatever " \
|
|
45
|
+
"`config/cable.yml` names"),
|
|
46
|
+
row(CHANNEL, :transmit, BROADCASTING,
|
|
47
|
+
why: "sends a message down this subscriber's own connection"),
|
|
48
|
+
row(CHANNEL, :stream_from, BROADCASTING,
|
|
49
|
+
why: "subscribes the connection to a stream, which registers with the pubsub adapter"),
|
|
50
|
+
row(CHANNEL, :stream_for, BROADCASTING, why: "the model-keyed `stream_from`")
|
|
51
|
+
]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def server_rows
|
|
55
|
+
[
|
|
56
|
+
row(SERVER, :broadcast, BROADCASTING, why: "the bare pubsub publish"),
|
|
57
|
+
row("ActionCable.server.pubsub", :broadcast, BROADCASTING, why: "the adapter-level publish")
|
|
58
|
+
]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def turbo_rows
|
|
62
|
+
TURBO_SELECTORS.map do |selector|
|
|
63
|
+
row("ActiveRecord::Base", selector, BROADCASTING,
|
|
64
|
+
why: "Turbo Streams broadcasts over ActionCable — a model callback that pushes HTML to " \
|
|
65
|
+
"every connected browser is exactly the effect an `app/models/**` policy names")
|
|
66
|
+
end +
|
|
67
|
+
TURBO_LATER_SELECTORS.map do |selector|
|
|
68
|
+
row("ActiveRecord::Base", selector, LATER,
|
|
69
|
+
why: "the `_later` twin enqueues a job that broadcasts, so it carries the enqueue " \
|
|
70
|
+
"meaning as well as the broadcast one")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def row(receiver, selector, labels, why:, singleton: false)
|
|
75
|
+
EffectAttribution.new(receiver: receiver, method: selector, labels: labels,
|
|
76
|
+
singleton: singleton, discharge: true, why: why)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def entry_points
|
|
80
|
+
[
|
|
81
|
+
EffectEntryPoints.new(
|
|
82
|
+
name: "rails-channels", globs: ["app/channels/**/*.rb"],
|
|
83
|
+
why: "channel callbacks — `subscribed`, `receive` and friends are invoked by the framework"
|
|
84
|
+
)
|
|
85
|
+
]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -5,6 +5,7 @@ require "rigor/plugin"
|
|
|
5
5
|
require_relative "actioncable/channel_index"
|
|
6
6
|
require_relative "actioncable/channel_discoverer"
|
|
7
7
|
require_relative "actioncable/analyzer"
|
|
8
|
+
require_relative "actioncable/effects"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Plugin
|
|
@@ -80,7 +81,12 @@ module Rigor
|
|
|
80
81
|
param_types: [{ index: 0, type_name: "Hash" }]
|
|
81
82
|
# return_type_name: nil — receive's return value is discarded by the framework dispatcher.
|
|
82
83
|
)
|
|
83
|
-
]
|
|
84
|
+
],
|
|
85
|
+
# ADR-103 WD10 (#387) — see {Effects} for what each row is and why.
|
|
86
|
+
effect_root: "rails",
|
|
87
|
+
effect_labels: ["rails.actioncable.broadcast"],
|
|
88
|
+
effect_attributions: Effects.attributions,
|
|
89
|
+
effect_entry_points: Effects.entry_points
|
|
84
90
|
)
|
|
85
91
|
|
|
86
92
|
# `watch:` covers every `.rb` file under the channel search paths so the cache invalidates when
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
class Actionmailer < Rigor::Plugin::Base
|
|
8
|
+
# rigor-actionmailer's effect contract (ADR-103 WD10; design note § 11.2; issue #387).
|
|
9
|
+
#
|
|
10
|
+
# ActionMailer is the clearest case in Rails of a lazy builder followed by a transport, and the
|
|
11
|
+
# design note's deferred-execution rule reads it straight off the syntax:
|
|
12
|
+
#
|
|
13
|
+
# UserMailer.welcome(user).deliver_now
|
|
14
|
+
# └──────── builder ─────┘└─ the effect ┘
|
|
15
|
+
#
|
|
16
|
+
# `UserMailer.welcome(u)` runs the mailer method and returns a `MessageDelivery` that has sent
|
|
17
|
+
# nothing. So it is an **edge** into `UserMailer#welcome` (whose own body may well read the
|
|
18
|
+
# database) and contributes no transport of its own. `deliver_now` is the send. `deliver_later`
|
|
19
|
+
# enqueues, which is ActiveJob's row plus the mail meaning — `email.send` still belongs there,
|
|
20
|
+
# because the mail WILL go out and a policy that forbids sending mail from a presenter means to
|
|
21
|
+
# forbid both spellings.
|
|
22
|
+
#
|
|
23
|
+
# The `on_result:` rows are what make this work on the idiom people actually write: the
|
|
24
|
+
# `MessageDelivery` in the middle has no type the project declares, but the class that produced it
|
|
25
|
+
# is written right there.
|
|
26
|
+
module Effects
|
|
27
|
+
BASE = "ActionMailer::Base"
|
|
28
|
+
DELIVERY = "ActionMailer::MessageDelivery"
|
|
29
|
+
|
|
30
|
+
NOW = ["io", "email.send", "rails.actionmailer.deliver"].freeze
|
|
31
|
+
LATER = ["io", "email.send", "rails.actionmailer.deliver", "rails.activejob.enqueue",
|
|
32
|
+
"job.enqueue"].freeze
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
def attributions
|
|
37
|
+
rows(:deliver_now, NOW,
|
|
38
|
+
why: "the SMTP / API round trip itself — `io` because the delivery method is configured " \
|
|
39
|
+
"per environment and is statically unknowable, `email.send` because that is what a " \
|
|
40
|
+
"policy names") +
|
|
41
|
+
rows(:deliver_now!, NOW, why: "`deliver_now!` is `deliver_now` ignoring the perform-deliveries " \
|
|
42
|
+
"setting; the same send") +
|
|
43
|
+
rows(:deliver_later, LATER,
|
|
44
|
+
why: "enqueues a delivery job: the enqueue rows, plus `email.send` — the mail goes out, " \
|
|
45
|
+
"and a policy forbidding mail from this layer means both spellings") +
|
|
46
|
+
rows(:deliver_later!, LATER, why: "`deliver_later!` raises rather than serialising a missing " \
|
|
47
|
+
"record; the same enqueue")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Each selector twice: once on a `MessageDelivery` receiver the typer managed to name, and once on
|
|
51
|
+
# the RESULT of a call to the mailer class, which is the spelling in every Rails codebase.
|
|
52
|
+
def rows(selector, labels, why:)
|
|
53
|
+
[
|
|
54
|
+
EffectAttribution.new(receiver: DELIVERY, method: selector, labels: labels, discharge: true,
|
|
55
|
+
why: why),
|
|
56
|
+
EffectAttribution.new(receiver: BASE, method: selector, labels: labels, on_result: true,
|
|
57
|
+
discharge: true,
|
|
58
|
+
why: "#{why} Matched on the result of `UserMailer.welcome(u)`, whose " \
|
|
59
|
+
"MessageDelivery has no type the project declares.")
|
|
60
|
+
]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def edges
|
|
64
|
+
[
|
|
65
|
+
EffectEdge.new(
|
|
66
|
+
receiver: BASE, target: :mailer_body,
|
|
67
|
+
why: "`UserMailer.welcome(u)` instantiates the mailer and runs `#welcome` — synchronously, " \
|
|
68
|
+
"in this process, before any delivery is attempted. The mailer body's own effects " \
|
|
69
|
+
"(the records it reads to build the mail) belong to the caller"
|
|
70
|
+
)
|
|
71
|
+
]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def entry_points
|
|
75
|
+
[
|
|
76
|
+
EffectEntryPoints.new(
|
|
77
|
+
name: "rails-mailers", globs: ["app/mailers/**/*.rb"],
|
|
78
|
+
why: "mailer actions — invoked by the framework through the class-method mapping"
|
|
79
|
+
)
|
|
80
|
+
]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -5,6 +5,7 @@ require "rigor/plugin"
|
|
|
5
5
|
require_relative "actionmailer/mailer_index"
|
|
6
6
|
require_relative "actionmailer/mailer_discoverer"
|
|
7
7
|
require_relative "actionmailer/analyzer"
|
|
8
|
+
require_relative "actionmailer/effects"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Plugin
|
|
@@ -52,7 +53,13 @@ module Rigor
|
|
|
52
53
|
"mailer_search_paths" => { kind: :array, default: ["app/mailers"] },
|
|
53
54
|
"mailer_base_classes" => { kind: :array, default: %w[ApplicationMailer ActionMailer::Base] },
|
|
54
55
|
"views_root" => { kind: :string, default: "app/views" }
|
|
55
|
-
}
|
|
56
|
+
},
|
|
57
|
+
# ADR-103 WD10 (#387) — see {Effects} for what each row is and why.
|
|
58
|
+
effect_root: "rails",
|
|
59
|
+
effect_labels: ["rails.actionmailer.deliver"],
|
|
60
|
+
effect_attributions: Effects.attributions,
|
|
61
|
+
effect_edges: Effects.edges,
|
|
62
|
+
effect_entry_points: Effects.entry_points
|
|
56
63
|
)
|
|
57
64
|
|
|
58
65
|
# `watch:` covers every mailer class under `mailer_search_paths` AND every view template under
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
class Actionpack < Rigor::Plugin::Base
|
|
8
|
+
# rigor-actionpack's effect contract (ADR-103 WD10 / WD14; design note § 11.2; issue #387).
|
|
9
|
+
#
|
|
10
|
+
# A controller action's effects are mostly writes to the response and to per-request state, and the
|
|
11
|
+
# vocabulary distinguishes them because a reviewer does: `rails.response.write` is "this action
|
|
12
|
+
# answers the request", `rails.session.write` is "this action changes who the user is logged in as".
|
|
13
|
+
#
|
|
14
|
+
# ## `mutate.self`, not `io`
|
|
15
|
+
#
|
|
16
|
+
# `render` and `redirect_to` do not write to a socket. They set `@_response_body` and the status on
|
|
17
|
+
# the controller instance; Rack writes it later, outside any project method. So the honest label is
|
|
18
|
+
# `mutate.self` plus the framework meaning — and an envelope forbidding `io` in a service object is
|
|
19
|
+
# not violated by a helper that happens to call `render_to_string`.
|
|
20
|
+
#
|
|
21
|
+
# `render` additionally keeps a **taint**: the template is not an effect unit yet (ADR-103 WD11 /
|
|
22
|
+
# issue #392), so what the view does is genuinely unknown and the summary says so rather than
|
|
23
|
+
# pretending the action stops at the `render` line.
|
|
24
|
+
#
|
|
25
|
+
# ## The self-path rows
|
|
26
|
+
#
|
|
27
|
+
# `session[:user_id] = id` is `[]=` on the result of a receiver-less `session`, and nothing types
|
|
28
|
+
# that result. The `self.session` spelling matches the receiver expression as written, scoped by
|
|
29
|
+
# `within:` to classes whose project ancestry reaches `ActionController::Base` — so a `session`
|
|
30
|
+
# method on some unrelated project class is not mistaken for this one.
|
|
31
|
+
module Effects
|
|
32
|
+
CONTROLLER = "ActionController::Base"
|
|
33
|
+
|
|
34
|
+
RESPONSE = ["mutate.self", "rails.response.write"].freeze
|
|
35
|
+
SESSION_WRITE = ["mutate", "rails.session.write"].freeze
|
|
36
|
+
SESSION_READ = ["io", "rails.session.read"].freeze
|
|
37
|
+
COOKIE_WRITE = ["mutate", "rails.cookie.write"].freeze
|
|
38
|
+
FLASH_WRITE = ["mutate", "rails.flash.write"].freeze
|
|
39
|
+
|
|
40
|
+
# Response writers that say everything about themselves.
|
|
41
|
+
RESPONSE_WRITERS = %w[redirect_to redirect_back redirect_back_or_to head].freeze
|
|
42
|
+
|
|
43
|
+
# The render family. Same labels, plus the `template-not-analysed` taint: what the controller does
|
|
44
|
+
# is fully stated, and what the TEMPLATE does is unknown until views are effect units (ADR-103
|
|
45
|
+
# WD11 / issue #392). A summary that stopped at the `render` line and read exhaustive would be
|
|
46
|
+
# the one genuinely misleading row in the whole Rails layer.
|
|
47
|
+
RENDERERS = %w[render render_to_string render_to_body].freeze
|
|
48
|
+
|
|
49
|
+
# The cookie jars a Rails app writes through.
|
|
50
|
+
COOKIE_JARS = ["self.cookies", "self.cookies.signed", "self.cookies.encrypted",
|
|
51
|
+
"self.cookies.permanent"].freeze
|
|
52
|
+
|
|
53
|
+
module_function
|
|
54
|
+
|
|
55
|
+
def attributions
|
|
56
|
+
response_rows + file_rows + session_rows + cookie_rows + flash_rows
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def response_rows
|
|
60
|
+
RESPONSE_WRITERS.map do |selector|
|
|
61
|
+
EffectAttribution.new(
|
|
62
|
+
receiver: CONTROLLER, method: selector, labels: RESPONSE, discharge: true,
|
|
63
|
+
why: "sets the response on the controller instance — Rack writes the socket later, outside " \
|
|
64
|
+
"any project method, so this is `mutate.self` and deliberately not `io`"
|
|
65
|
+
)
|
|
66
|
+
end + render_rows
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def render_rows
|
|
70
|
+
RENDERERS.map do |selector|
|
|
71
|
+
EffectAttribution.new(
|
|
72
|
+
receiver: CONTROLLER, method: selector, labels: RESPONSE, discharge: true,
|
|
73
|
+
taint: "template-not-analysed",
|
|
74
|
+
why: "sets the response body from a template. The controller half is fully stated; the " \
|
|
75
|
+
"template's own effects are unknown until views become effect units, and the taint " \
|
|
76
|
+
"is how the summary says so rather than reading exhaustive"
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# `send_file` streams from disk; `send_data` does not.
|
|
82
|
+
def file_rows
|
|
83
|
+
[
|
|
84
|
+
EffectAttribution.new(
|
|
85
|
+
receiver: CONTROLLER, method: :send_data, labels: RESPONSE, discharge: true,
|
|
86
|
+
why: "sets the response body from an in-memory string"
|
|
87
|
+
),
|
|
88
|
+
EffectAttribution.new(
|
|
89
|
+
receiver: CONTROLLER, method: :send_file, labels: RESPONSE + ["io.fs.read"], discharge: true,
|
|
90
|
+
why: "sets the response AND reads the named file off disk"
|
|
91
|
+
)
|
|
92
|
+
]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def session_rows
|
|
96
|
+
[
|
|
97
|
+
EffectAttribution.new(
|
|
98
|
+
receiver: "self.session", method: :[]=, labels: SESSION_WRITE, within: CONTROLLER,
|
|
99
|
+
discharge: true,
|
|
100
|
+
why: "writes per-request state whose store may be a cookie, a cache or the database"
|
|
101
|
+
),
|
|
102
|
+
EffectAttribution.new(
|
|
103
|
+
receiver: "self.session", method: :delete, labels: SESSION_WRITE, within: CONTROLLER,
|
|
104
|
+
discharge: true, why: "same store, same write"
|
|
105
|
+
),
|
|
106
|
+
EffectAttribution.new(
|
|
107
|
+
receiver: "self.session", method: :[], labels: SESSION_READ, within: CONTROLLER,
|
|
108
|
+
discharge: true,
|
|
109
|
+
why: "reads the session store — `io` because a cache- or database-backed store really does " \
|
|
110
|
+
"go out to fetch it"
|
|
111
|
+
),
|
|
112
|
+
EffectAttribution.new(
|
|
113
|
+
receiver: CONTROLLER, method: :reset_session, labels: SESSION_WRITE, discharge: true,
|
|
114
|
+
why: "discards the whole session — the logout write"
|
|
115
|
+
)
|
|
116
|
+
]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def cookie_rows
|
|
120
|
+
COOKIE_JARS.flat_map do |jar|
|
|
121
|
+
%i[[]= delete].map do |selector|
|
|
122
|
+
EffectAttribution.new(
|
|
123
|
+
receiver: jar, method: selector, labels: COOKIE_WRITE, within: CONTROLLER, discharge: true,
|
|
124
|
+
why: "queues a Set-Cookie header on the response — state that outlives the request"
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def flash_rows
|
|
131
|
+
["self.flash", "self.flash.now"].flat_map do |jar|
|
|
132
|
+
%i[[]= alert= notice=].map do |selector|
|
|
133
|
+
EffectAttribution.new(
|
|
134
|
+
receiver: jar, method: selector, labels: FLASH_WRITE, within: CONTROLLER, discharge: true,
|
|
135
|
+
why: "writes the flash, which rides the session into the next request"
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def entry_points
|
|
142
|
+
[
|
|
143
|
+
EffectEntryPoints.new(
|
|
144
|
+
name: "rails-controllers", globs: ["app/controllers/**/*.rb"],
|
|
145
|
+
why: "controller actions — the request entry points; nothing in the project calls them"
|
|
146
|
+
)
|
|
147
|
+
]
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "rigor/plugin"
|
|
4
4
|
|
|
5
5
|
require_relative "actionpack/analyzer"
|
|
6
|
+
require_relative "actionpack/effects"
|
|
6
7
|
require_relative "actionpack/controller_discoverer"
|
|
7
8
|
require_relative "actionpack/controller_index"
|
|
8
9
|
|
|
@@ -69,7 +70,14 @@ module Rigor
|
|
|
69
70
|
consumes: [
|
|
70
71
|
{ plugin_id: "rails-routes", name: :helper_table, optional: true },
|
|
71
72
|
{ plugin_id: "activerecord", name: :model_index, optional: true }
|
|
72
|
-
]
|
|
73
|
+
],
|
|
74
|
+
# ADR-103 WD10 / WD14 (#387) — see {Effects} for what each row is and why.
|
|
75
|
+
effect_root: "rails",
|
|
76
|
+
effect_labels: %w[
|
|
77
|
+
rails.response.write rails.session.read rails.session.write rails.cookie.write rails.flash.write
|
|
78
|
+
],
|
|
79
|
+
effect_attributions: Effects.attributions,
|
|
80
|
+
effect_entry_points: Effects.entry_points
|
|
73
81
|
)
|
|
74
82
|
|
|
75
83
|
# Phase 2 cached producer — the controller index built from `controller_search_paths`. `watch:`
|