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.
- 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 +860 -0
- data/data/effects/registry.yml +95 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +359 -2
- data/docs/manual/03-configuration.md +101 -1
- data/docs/manual/04-diagnostics.md +8 -3
- data/docs/manual/11-ci.md +37 -0
- data/docs/manual/12-caching.md +39 -0
- data/docs/manual/16-rbs-extended-annotations.md +90 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/19-effect-labels.md +671 -0
- data/docs/manual/README.md +10 -0
- data/docs/manual/ci-templates/README.md +9 -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/rule_ids.rb +28 -2
- 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 +168 -3
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/run_cache_probe.rb +69 -1
- data/lib/rigor/analysis/runner/declaration_position.rb +38 -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 +93 -10
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
- data/lib/rigor/analysis/runner.rb +429 -10
- data/lib/rigor/analysis/worker_session.rb +60 -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 +245 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +127 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +92 -0
- data/lib/rigor/cli/effects_report.rb +163 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +275 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +28 -1
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +322 -28
- data/lib/rigor/configuration_error.rb +20 -0
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +219 -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 +139 -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 +174 -0
- data/lib/rigor/effects/framework_units.rb +222 -0
- data/lib/rigor/effects/identity.rb +104 -0
- data/lib/rigor/effects/inline_anchor.rb +134 -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 +384 -0
- data/lib/rigor/effects/propagator.rb +335 -0
- data/lib/rigor/effects/registry.rb +200 -0
- data/lib/rigor/effects/scanner.rb +302 -0
- data/lib/rigor/effects/signature_sources.rb +90 -0
- data/lib/rigor/effects/snapshot.rb +396 -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 +39 -0
- data/lib/rigor/effects/unit_scan.rb +647 -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/expression_typer.rb +18 -4
- data/lib/rigor/inference/pre_eval_constants.rb +189 -0
- data/lib/rigor/inference/scope_indexer.rb +15 -3
- data/lib/rigor/plugin/base.rb +28 -0
- data/lib/rigor/plugin/box.rb +18 -2
- data/lib/rigor/plugin/effect_ancestry.rb +80 -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 +127 -3
- data/lib/rigor/plugin/registry.rb +55 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +188 -0
- data/lib/rigor/reflection.rb +131 -30
- 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 +401 -29
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -0
- 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-rbs-inline/lib/rigor/plugin/rbs_inline.rb +56 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +75 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +68 -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.rbs +17 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +84 -1
data/lib/rigor/rbs_extended.rb
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
require_relative "type"
|
|
4
4
|
require_relative "builtins/imported_refinements"
|
|
5
5
|
require_relative "flow_contribution"
|
|
6
|
+
require_relative "effects/envelope"
|
|
7
|
+
require_relative "effects/inline_anchor"
|
|
8
|
+
require_relative "effects/label"
|
|
9
|
+
require_relative "effects/label_set"
|
|
10
|
+
require_relative "effects/signature_sources"
|
|
6
11
|
require_relative "rbs_extended/reporter"
|
|
7
12
|
require_relative "rbs_extended/hkt_directives"
|
|
8
13
|
|
|
@@ -621,6 +626,189 @@ module Rigor
|
|
|
621
626
|
facts.empty? ? nil : facts
|
|
622
627
|
end
|
|
623
628
|
|
|
629
|
+
# ── Effect envelopes (ADR-103 WD1 / WD5 / WD14; #383) ──────────────────────────────────────────
|
|
630
|
+
#
|
|
631
|
+
# Two spellings reach the same value. `%a{pure}` is the ecosystem's existing purity annotation
|
|
632
|
+
# (rbs core and Steep both carry it) and reads as the EMPTY envelope; `%a{rigor:v1:effect …}` is
|
|
633
|
+
# Rigor's labelled bound. `rigor:v1:pure` is deliberately NOT implemented — WD14 fixed `%a{pure}`
|
|
634
|
+
# as the only purity spelling.
|
|
635
|
+
|
|
636
|
+
# The bare rbs-native purity annotation. Matched whole, with surrounding whitespace tolerated, so
|
|
637
|
+
# `%a{purely}` and `%a{pure io}` are not it.
|
|
638
|
+
PURE_ANNOTATION_PATTERN = /\A\s*pure\s*\z/
|
|
639
|
+
private_constant :PURE_ANNOTATION_PATTERN
|
|
640
|
+
|
|
641
|
+
# `%a{rigor:v1:effect io.db, nondet.time}` — a space-separated head (the `assert` / `conforms-to`
|
|
642
|
+
# family), then a comma-separated list of BARE label tokens. There is no parenthesised comment
|
|
643
|
+
# form: RBS has real comments. The payload group is optional so that `%a{rigor:v1:effect}` still
|
|
644
|
+
# MATCHES the directive and is reported as malformed rather than read as "not a directive".
|
|
645
|
+
EFFECT_DIRECTIVE_PATTERN = /\Arigor:v1:effect(?:\s+(?<labels>.*?))?\s*\z/
|
|
646
|
+
private_constant :EFFECT_DIRECTIVE_PATTERN
|
|
647
|
+
|
|
648
|
+
# The outcome of reading one `%a{rigor:v1:effect …}` payload.
|
|
649
|
+
#
|
|
650
|
+
# `bound` is the declared {Rigor::Effects::LabelSet}, or {Effects::LabelSet::TOP} when the tag
|
|
651
|
+
# could not be given a meaning. `malformed` says the grammar was violated (an empty list, a token
|
|
652
|
+
# that is not a label); `unknown_labels` says the grammar held but the registry does not
|
|
653
|
+
# recognise a spelling — a different condition with different handling, and what #384's
|
|
654
|
+
# `effect.unknown-label` reads. `labels` is the list exactly as written, recognised or not, which
|
|
655
|
+
# is what lets the diagnostic ask whether some OTHER member of the list was known.
|
|
656
|
+
EffectAnnotation = Data.define(:bound, :labels, :unknown_labels, :malformed) do
|
|
657
|
+
def malformed? = malformed
|
|
658
|
+
def top? = bound.top?
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
NO_EFFECT_LABELS = [].freeze
|
|
662
|
+
private_constant :NO_EFFECT_LABELS
|
|
663
|
+
|
|
664
|
+
# Whether `string` is the bare `%a{pure}` annotation.
|
|
665
|
+
def pure_annotation?(string)
|
|
666
|
+
!string.nil? && PURE_ANNOTATION_PATTERN.match?(string)
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# Reads one annotation string as an effect-envelope payload.
|
|
670
|
+
#
|
|
671
|
+
# Returns `nil` when the string is not a `rigor:v1:effect` directive at all (so a caller can walk
|
|
672
|
+
# an annotation list without pre-filtering). Otherwise an {EffectAnnotation}:
|
|
673
|
+
#
|
|
674
|
+
# - well-formed and fully recognised → the declared bound;
|
|
675
|
+
# - malformed (empty list, a token outside the label grammar) → {Effects::LabelSet::TOP}, and a
|
|
676
|
+
# `record_unresolved` event on `reporter`;
|
|
677
|
+
# - well-formed but carrying a spelling `registry` does not know → {Effects::LabelSet::TOP} and
|
|
678
|
+
# the unrecognised spellings, because an unknown label makes the WHOLE tag ⊤ rather than the
|
|
679
|
+
# subset that happened to parse. Narrowing to the recognised subset would turn a typo into
|
|
680
|
+
# findings on correct code; widening suppresses them, which is the direction the false-positive
|
|
681
|
+
# budget runs (ADR-5).
|
|
682
|
+
def parse_effect_annotation(string, registry: nil, reporter: nil, source_location: nil)
|
|
683
|
+
match = EFFECT_DIRECTIVE_PATTERN.match(string.to_s)
|
|
684
|
+
return nil if match.nil?
|
|
685
|
+
|
|
686
|
+
tokens = match[:labels].to_s.split(",", -1).map(&:strip)
|
|
687
|
+
if tokens.empty? || tokens.any? { |token| !Effects::Label.valid?(token) }
|
|
688
|
+
record_unresolved(reporter, string, source_location)
|
|
689
|
+
return EffectAnnotation.new(
|
|
690
|
+
bound: Effects::LabelSet::TOP, labels: NO_EFFECT_LABELS, unknown_labels: NO_EFFECT_LABELS, malformed: true
|
|
691
|
+
)
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
tokens = tokens.freeze
|
|
695
|
+
unknown = registry.nil? ? [] : tokens.reject { |token| registry.known?(token) }
|
|
696
|
+
EffectAnnotation.new(
|
|
697
|
+
bound: unknown.empty? ? Effects::LabelSet.new(tokens) : Effects::LabelSet::TOP,
|
|
698
|
+
labels: tokens, unknown_labels: unknown.uniq.sort.freeze, malformed: false
|
|
699
|
+
)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Reads the effect envelope off an annotation list — `RBS::Definition::Method#annotations`, or the
|
|
703
|
+
# `#annotations` of an `RBS::AST::Members::MethodDefinition` / class declaration, which carry the
|
|
704
|
+
# same `(string, location)` shape.
|
|
705
|
+
#
|
|
706
|
+
# Precedence, per the spec: `%a{pure}` and `%a{rigor:v1:effect …}` on ONE declaration are
|
|
707
|
+
# contradictory and `pure` wins; the contradiction is recorded on `reporter` (the existing
|
|
708
|
+
# `RBS::Extended` conflict channel) rather than silently resolved. Returns `nil` when the list
|
|
709
|
+
# carries neither spelling.
|
|
710
|
+
#
|
|
711
|
+
# @param annotations [Array<#string>] the node's annotations, in source order.
|
|
712
|
+
# @param owner_key [String] the method key (`Class#m` / `Class.m`) or class name the bound binds.
|
|
713
|
+
# @param source [Symbol] {Effects::Envelope::SOURCES} member to stamp when the labelled spelling
|
|
714
|
+
# matched; `%a{pure}` always stamps `:pure_annotation`, and a class-level read overrides both.
|
|
715
|
+
def read_effect_envelope(annotations, owner_key:, source: :effect_annotation, registry: nil, reporter: nil)
|
|
716
|
+
return nil if annotations.nil? || annotations.empty?
|
|
717
|
+
|
|
718
|
+
pure = annotations.find { |annotation| pure_annotation?(annotation.string) }
|
|
719
|
+
labelled = nil
|
|
720
|
+
parsed = nil
|
|
721
|
+
annotations.each do |annotation|
|
|
722
|
+
result = parse_effect_annotation(
|
|
723
|
+
annotation.string, registry: registry, reporter: reporter, source_location: annotation_location(annotation)
|
|
724
|
+
)
|
|
725
|
+
next if result.nil?
|
|
726
|
+
|
|
727
|
+
labelled = annotation
|
|
728
|
+
parsed = result
|
|
729
|
+
break
|
|
730
|
+
end
|
|
731
|
+
return nil if pure.nil? && labelled.nil?
|
|
732
|
+
|
|
733
|
+
if pure && labelled
|
|
734
|
+
record_unresolved(
|
|
735
|
+
reporter,
|
|
736
|
+
"`%a{pure}` and `%a{#{labelled.string}}` on one declaration are contradictory; `pure` wins",
|
|
737
|
+
annotation_location(pure)
|
|
738
|
+
)
|
|
739
|
+
end
|
|
740
|
+
return build_pure_envelope(pure, owner_key: owner_key, source: source) if pure
|
|
741
|
+
|
|
742
|
+
build_effect_envelope(labelled, parsed, owner_key: owner_key, source: source)
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def build_pure_envelope(annotation, owner_key:, source:)
|
|
746
|
+
Effects::Envelope.build(
|
|
747
|
+
owner_key: owner_key,
|
|
748
|
+
bound: Effects::LabelSet::EMPTY,
|
|
749
|
+
source: source == :class_annotation ? :class_annotation : :pure_annotation,
|
|
750
|
+
location: render_annotation_location(annotation),
|
|
751
|
+
spelling: "%a{#{annotation.string}}"
|
|
752
|
+
)
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
def build_effect_envelope(annotation, parsed, owner_key:, source:)
|
|
756
|
+
Effects::Envelope.build(
|
|
757
|
+
owner_key: owner_key,
|
|
758
|
+
bound: parsed.bound,
|
|
759
|
+
source: source,
|
|
760
|
+
location: render_annotation_location(annotation),
|
|
761
|
+
spelling: "%a{#{annotation.string}}",
|
|
762
|
+
unknown_labels: parsed.unknown_labels,
|
|
763
|
+
declared_labels: parsed.labels
|
|
764
|
+
)
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def annotation_location(annotation)
|
|
768
|
+
annotation.respond_to?(:location) ? annotation.location : nil
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
# `path:line` for the annotation, project-relative when it sits under the working directory, so a
|
|
772
|
+
# diagnostic message names `sig/foo.rbs:12` rather than an absolute path.
|
|
773
|
+
#
|
|
774
|
+
# A synthesized buffer is re-anchored here rather than at any of the surfaces that render it
|
|
775
|
+
# ({Effects::InlineAnchor}; #432). The buffer's own line numbers describe a document the author
|
|
776
|
+
# never saw, so an envelope carrying one is wrong for every consumer at once — the diagnostic, the
|
|
777
|
+
# `effect.unknown-label` position, `rigor explain`, the JSON formatter, LSP hover. Correcting it
|
|
778
|
+
# where the value is *built* is what makes them agree without each learning the mapping.
|
|
779
|
+
def render_annotation_location(annotation)
|
|
780
|
+
location = annotation_location(annotation)
|
|
781
|
+
return nil if location.nil?
|
|
782
|
+
|
|
783
|
+
buffer = location.respond_to?(:buffer) ? location.buffer : nil
|
|
784
|
+
name = buffer.respond_to?(:name) ? buffer.name.to_s : nil
|
|
785
|
+
return nil if name.nil? || name.empty?
|
|
786
|
+
|
|
787
|
+
path = relative_annotation_path(name)
|
|
788
|
+
"#{path}:#{annotation_line(annotation, location, buffer, path)}"
|
|
789
|
+
rescue StandardError
|
|
790
|
+
nil
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# The line a reader can open. For a real `.rbs` that is the parser's own answer; for the
|
|
794
|
+
# `virtual:rbs-inline:…rb` buffer the writer produced, the annotation is found again in the Ruby
|
|
795
|
+
# source by its own spelling.
|
|
796
|
+
def annotation_line(annotation, location, buffer, path)
|
|
797
|
+
line = location.respond_to?(:start_line) ? location.start_line : 1
|
|
798
|
+
content = buffer.respond_to?(:content) ? buffer.content : nil
|
|
799
|
+
return line if content.nil?
|
|
800
|
+
|
|
801
|
+
Effects::InlineAnchor.ruby_line(
|
|
802
|
+
path: path, buffer: content, buffer_line: line, spelling: "%a{#{annotation.string}}"
|
|
803
|
+
)
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
# The buffer-name → readable-path rule lives with the walk that produces the buffers
|
|
807
|
+
# ({Effects::SignatureSources}), so a `virtual:` name is stripped identically wherever it surfaces.
|
|
808
|
+
def relative_annotation_path(name)
|
|
809
|
+
Effects::SignatureSources.source_path(name)
|
|
810
|
+
end
|
|
811
|
+
|
|
624
812
|
# ADR-13 slice 3b — guards every reporter call so the in-RbsExtended-module call sites can record events
|
|
625
813
|
# uniformly without nil-checking each time. When the reporter is nil (the v0.1.0 → v0.1.3 default for call
|
|
626
814
|
# sites that do not yet thread `environment:`), the call is a no-op and the parser stays fail-soft.
|
data/lib/rigor/reflection.rb
CHANGED
|
@@ -44,6 +44,10 @@ module Rigor
|
|
|
44
44
|
# out of scope for the v0.0.7 first pass; v0.1.0's plugin API added it as a separate
|
|
45
45
|
# concern.
|
|
46
46
|
module Reflection
|
|
47
|
+
# #354 — thread-local slot for the per-run ancestor-scope memo. See {.ancestor_constant_scopes}.
|
|
48
|
+
ANCESTOR_SCOPES_KEY = :__rigor_ancestor_constant_scopes__
|
|
49
|
+
private_constant :ANCESTOR_SCOPES_KEY
|
|
50
|
+
|
|
47
51
|
module_function
|
|
48
52
|
|
|
49
53
|
# @param class_name [String, Symbol]
|
|
@@ -118,42 +122,139 @@ module Rigor
|
|
|
118
122
|
# reads it to type a constant read, and `Inference::Narrowing` reads it to recognise a
|
|
119
123
|
# value-pinned `Constant[Regexp]` match-predicate operand.
|
|
120
124
|
def resolve_constant_type(name, scope: Scope.empty)
|
|
125
|
+
prefix = enclosing_class_path(scope)
|
|
126
|
+
|
|
127
|
+
# Step 1 — `Module.nesting`, innermost first. Each entry contributes only its OWN constants.
|
|
128
|
+
walker = prefix
|
|
129
|
+
while walker && !walker.empty?
|
|
130
|
+
hit = constant_type_at("#{walker}::#{name}", scope)
|
|
131
|
+
return hit if hit
|
|
132
|
+
|
|
133
|
+
idx = walker.rindex("::")
|
|
134
|
+
walker = idx ? walker[0, idx] : nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Step 2 (#354) — the ancestors of the innermost cresting scope, which Ruby consults BEFORE
|
|
138
|
+
# falling back to the top level. Skipping this step did not merely lose a resolution: when the
|
|
139
|
+
# same name also exists at top level, step 3 answered a lookup Ruby gives to the ancestor, so
|
|
140
|
+
# `KEY` inside `class Sub < Base` typed as the top-level constant rather than `Base::KEY` — a
|
|
141
|
+
# wrong type on correct code, which outranks any worst-case reading (AGENTS.md
|
|
142
|
+
# § "Implementation Guidelines"). Only project ancestors are walked; an RBS-known superclass
|
|
143
|
+
# contributes no name here (see {.ancestor_constant_scopes}).
|
|
144
|
+
if prefix
|
|
145
|
+
ancestor_constant_scopes(prefix, scope).each do |ancestor|
|
|
146
|
+
hit = constant_type_at("#{ancestor}::#{name}", scope)
|
|
147
|
+
return hit if hit
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Step 3 — the bare name (top level).
|
|
152
|
+
constant_type_at(name, scope)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# One candidate name, consulted in source-precedence order: the class registry (yielding a
|
|
156
|
+
# `Singleton[C]`), source-discovered classes, in-source value constants, then RBS-side
|
|
157
|
+
# constants. In-source values win over RBS constant decls because the user's source is
|
|
158
|
+
# authoritative for its own constants. Returns nil when no source knows `candidate`.
|
|
159
|
+
def constant_type_at(candidate, scope)
|
|
121
160
|
env = scope.environment
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
161
|
+
|
|
162
|
+
singleton = env.singleton_for_name(candidate)
|
|
163
|
+
return singleton if singleton
|
|
164
|
+
|
|
165
|
+
in_source_class = scope.discovered_classes[candidate]
|
|
166
|
+
return in_source_class if in_source_class
|
|
167
|
+
|
|
168
|
+
in_source_value = scope.in_source_constants[candidate]
|
|
169
|
+
return in_source_value if in_source_value
|
|
170
|
+
|
|
171
|
+
env.constant_for_name(candidate)
|
|
172
|
+
end
|
|
173
|
+
private_class_method :constant_type_at
|
|
174
|
+
|
|
175
|
+
# #354 — the project classes and modules whose own constants `class_name` inherits, in Ruby's
|
|
176
|
+
# ancestor order: included / prepended modules before the superclass (Ruby places mixins nearer),
|
|
177
|
+
# transitively, breadth-first. `class_name` itself is excluded — step 1 already covered it.
|
|
178
|
+
#
|
|
179
|
+
# Only PROJECT ancestors appear. `Scope#superclass_of` / `#includes_of` carry as-written names
|
|
180
|
+
# from the discovery pre-pass, and an as-written name that resolves to no discovered class or
|
|
181
|
+
# module is dropped — so a `class Foo < ActiveRecord::Base` contributes nothing and a constant
|
|
182
|
+
# owned by an RBS-known ancestor still resolves only if the bare name reaches it at step 3. That
|
|
183
|
+
# gap is deliberate for this slice: widening to the RBS ancestor graph is a separate question
|
|
184
|
+
# with its own FP surface.
|
|
185
|
+
#
|
|
186
|
+
# Memoised per run because step 2 runs on every constant reference whose lexical candidates all
|
|
187
|
+
# miss — which is the common case for a core-class reference (`String` inside `class Foo`). The
|
|
188
|
+
# bucket keys on the identity of the runner-seeded run-generation token (ADR-84 WD2), falling
|
|
189
|
+
# back to the per-file discovery table for runner-less scopes, so a re-run in one process (LSP,
|
|
190
|
+
# ADR-62 warm loop) cannot hit stale entries.
|
|
191
|
+
def ancestor_constant_scopes(class_name, scope)
|
|
192
|
+
# ADR-46: `superclass_of` / `includes_of` record a cross-file class dependency per consumer
|
|
193
|
+
# file, and the memo is run-scoped rather than file-scoped — a hit would skip the recording and
|
|
194
|
+
# under-record the edge for every later file. Recording runs are rare (incremental only), so
|
|
195
|
+
# they simply bypass the memo rather than complicate its key.
|
|
196
|
+
return compute_ancestor_constant_scopes(class_name, scope) if Analysis::DependencyRecorder.active?
|
|
197
|
+
|
|
198
|
+
generation = scope.run_generation || scope.discovered_superclasses
|
|
199
|
+
slot = Thread.current[ANCESTOR_SCOPES_KEY]
|
|
200
|
+
unless slot && slot[0].equal?(generation)
|
|
201
|
+
slot = [generation, {}]
|
|
202
|
+
Thread.current[ANCESTOR_SCOPES_KEY] = slot
|
|
138
203
|
end
|
|
139
|
-
|
|
204
|
+
bucket = slot[1]
|
|
205
|
+
bucket.fetch(class_name) { bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) }
|
|
140
206
|
end
|
|
207
|
+
private_class_method :ancestor_constant_scopes
|
|
141
208
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
209
|
+
def compute_ancestor_constant_scopes(class_name, scope)
|
|
210
|
+
queue = [class_name]
|
|
211
|
+
seen = { class_name => true }
|
|
212
|
+
out = []
|
|
213
|
+
until queue.empty?
|
|
214
|
+
current = queue.shift
|
|
215
|
+
# Mixins first, then the superclass — Ruby's ancestor order.
|
|
216
|
+
scope.includes_of(current).each do |raw|
|
|
217
|
+
resolved = resolve_ancestor_name(current, raw, scope)
|
|
218
|
+
next if resolved.nil? || seen[resolved]
|
|
219
|
+
|
|
220
|
+
seen[resolved] = true
|
|
221
|
+
out << resolved
|
|
222
|
+
queue << resolved
|
|
223
|
+
end
|
|
224
|
+
raw_super = scope.superclass_of(current)
|
|
225
|
+
next if raw_super.nil?
|
|
226
|
+
|
|
227
|
+
resolved_super = resolve_ancestor_name(current, raw_super, scope)
|
|
228
|
+
next if resolved_super.nil? || seen[resolved_super]
|
|
229
|
+
|
|
230
|
+
seen[resolved_super] = true
|
|
231
|
+
out << resolved_super
|
|
232
|
+
queue << resolved_super
|
|
233
|
+
end
|
|
234
|
+
out.freeze
|
|
235
|
+
end
|
|
236
|
+
private_class_method :compute_ancestor_constant_scopes
|
|
237
|
+
|
|
238
|
+
# Resolves an ancestor name AS WRITTEN (`"Base"`, or a qualified `"A::B"`) against the
|
|
239
|
+
# subclass's lexical nesting, innermost first — the same walk
|
|
240
|
+
# `ExpressionTyper#compute_ancestor_class_name` performs for method lookup. Returns nil when no
|
|
241
|
+
# candidate names a discovered project class or module.
|
|
242
|
+
def resolve_ancestor_name(subclass_qualified, raw, scope)
|
|
243
|
+
segments = subclass_qualified.split("::")
|
|
244
|
+
(segments.length - 1).downto(0) do |i|
|
|
245
|
+
candidate = (segments[0, i] + [raw]).join("::")
|
|
246
|
+
return candidate if known_project_namespace?(candidate, scope)
|
|
152
247
|
end
|
|
153
|
-
|
|
154
|
-
|
|
248
|
+
nil
|
|
249
|
+
end
|
|
250
|
+
private_class_method :resolve_ancestor_name
|
|
251
|
+
|
|
252
|
+
def known_project_namespace?(name, scope)
|
|
253
|
+
scope.discovered_superclasses.key?(name) ||
|
|
254
|
+
scope.discovered_includes.key?(name) ||
|
|
255
|
+
scope.discovered_classes.key?(name)
|
|
155
256
|
end
|
|
156
|
-
private_class_method :
|
|
257
|
+
private_class_method :known_project_namespace?
|
|
157
258
|
|
|
158
259
|
# Pulls the enclosing qualified class name out of `scope.self_type` when one is set.
|
|
159
260
|
# `Nominal[T]` and `Singleton[T]` both expose `class_name`. Returns nil at the top level.
|
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
|