Class: Rigor::Plugin::EffectAttribution

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/plugin/effect_attribution.rb

Overview

One row of a plugin's effect_attributions: — "a call to this receiver's method contributes these effect labels" (ADR-103 WD6 / WD10; design note § 6.6; the contract is docs/internal-spec/plugin.md § Effect contributions).

A framework method has no body Rigor reads, so someone must colour it. Three channels can, in descending order of preference:

  1. a %a{rigor:v1:effect …} annotation in the plugin's own signature_paths: RBS — tier 1, read through Effects::EnvelopeIndex's accepted stratum, and the right channel whenever the plugin already ships a signature for the method;
  2. this field, for the methods RBS cannot name per app — association readers, find_by_*, scopes — and for classes the plugin ships no RBS for at all;
  3. the project's own effects.attribution: table, which is the user's answer for everything left.

The receiver

receiver is spelled one of two ways, and the spelling picks the matching rule:

  • a class name ("ActiveRecord::Base", "I18n") matches the class the call's receiver projects to, through the project's inheritance chain: an ActiveRecord::Base row applies to User.find because the project declares User < ApplicationRecord < ActiveRecord::Base. This is the difference from a catalogue row, which matches its exact owner and nothing else — Ruby's core classes are leaves in practice and a framework's base class never is.
  • a receiver path ("Rails.cache", "Time.zone", "Rails.application.credentials") matches the receiver expression as the syntax spells it. Rails.cache.read has no receiver class the typer can name — Rails.cache is a call, and its return type is adapter-dependent by design — so the only honest handle on it is the path that was written.
  • a self path ("self.session", "self.flash.now", "self.cookies.encrypted") is the same thing rooted at implicit self, and is what a Rails controller's accessors actually look like: session[:user_id] = id is []= on the result of a receiver-less session. A self-path row MUST name a within: class, because a receiver-less session in some other project class is a different session — the row applies only inside a class whose project ancestry reaches within.

on_result: true shifts a class-name row one link outwards: it matches a call on what a call to that class returned. UserMailer.welcome(u).deliver_now and WelcomeJob.set(wait: 1.hour) .perform_later are the two idioms that need it — the object in the middle is a lazy MessageDelivery / ConfiguredJob whose type nothing in the project declares, while the class that produced it is written right there in the source. Without it the send and the enqueue, the two calls a reviewer most wants coloured, would go unattributed in the spelling Rails actually uses.

Discharge

discharge: true says the label is derived from the framework's own semantics rather than guessed, so the site is exhaustive rather than tainted. ADR-103 WD6 grants that only to a first-party bundled plugin (FirstParty), gated by make check-plugins; a third-party plugin's true is ignored with a load-time warning and the row behaves like the project's effects.attribution: table — declared, and carrying a plugin-attribution taint.

Either way the labels land in the declared lane, never the proven one. A discharging row is a trusted claim, exactly like an accepted signature's %a{…}: "this is what it does", not "the analyzer read the body and saw this".

Constant Summary collapse

CLASS_NAME =

A receiver spelled as a Constant::Path — the class-name form. Anything else with a . in it is read as a receiver path.

/\A[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*\z/
RECEIVER_PATH =

A receiver path: a constant head followed by one or more sends (Rails.cache, Rails.application.credentials).

/\A[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*(\.[a-z_][A-Za-z0-9_]*)+\z/
SELF_PATH =

A self path: implicit self followed by one or more receiver-less sends (self.flash.now).

/\Aself(\.[a-z_][A-Za-z0-9_]*)+\z/
SELF_HEAD =

What a self path is rooted at, once the self. head is stripped.

"self"
TAINT_CAUSES =

The taint causes a plugin row may name. A closed subset of Effects::TaintCause::ALL: a plugin may say "and there is more here I cannot see", but only for the two reasons a framework model can honestly have — a template it does not read, and a callable whose body is supplied by the application.

%w[template-not-analysed opaque-callable].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(receiver:, method:, labels:, why:, singleton: false, narrow: nil, discharge: false, within: nil, on_result: false, taint: nil) ⇒ EffectAttribution

Returns a new instance of EffectAttribution.

Parameters:

  • receiver (String)

    a class name or a receiver path (see above)

  • method (Symbol, String)

    the selector this row colours

  • singleton (Boolean) (defaults to: false)

    whether the row is Receiver.method rather than Receiver#method. Meaningless — and ignored — for a receiver path, whose head already fixes the receiver object.

  • labels (Array<String>)

    the effect labels the call contributes

  • narrow (String, nil) (defaults to: nil)

    a Effects::Narrowing handler name, when the call's own argument literals settle a question the row cannot (connection.execute("SELECT …"))

  • discharge (Boolean) (defaults to: false)

    see above; honoured only for a first-party bundled plugin

  • why (String)

    the audit justification, required exactly as data/effects/core.yml requires one of every row: a label with no stated reason is a claim nobody can review.



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/rigor/plugin/effect_attribution.rb', line 90

def initialize(receiver:, method:, labels:, why:, singleton: false, narrow: nil, discharge: false, # rubocop:disable Metrics/ParameterLists
               within: nil, on_result: false, taint: nil)
  @receiver = validate_receiver!(receiver)
  @method = method.to_sym
  @singleton = singleton ? true : false
  @labels = normalize_labels(labels)
  @narrow = narrow.nil? ? nil : narrow.to_s.dup.freeze
  @discharge = discharge ? true : false
  @within = validate_within!(within)
  @on_result = on_result ? true : false
  validate_on_result!
  @taint = validate_taint!(taint)
  @why = validate_why!(why)
  freeze
end

Instance Attribute Details

#dischargeObject (readonly)

Returns the value of attribute discharge.



71
72
73
# File 'lib/rigor/plugin/effect_attribution.rb', line 71

def discharge
  @discharge
end

#labelsObject (readonly)

Returns the value of attribute labels.



71
72
73
# File 'lib/rigor/plugin/effect_attribution.rb', line 71

def labels
  @labels
end

#methodObject (readonly)

Returns the value of attribute method.



71
72
73
# File 'lib/rigor/plugin/effect_attribution.rb', line 71

def method
  @method
end

#narrowObject (readonly)

Returns the value of attribute narrow.



71
72
73
# File 'lib/rigor/plugin/effect_attribution.rb', line 71

def narrow
  @narrow
end

#on_resultObject (readonly)

Returns the value of attribute on_result.



71
72
73
# File 'lib/rigor/plugin/effect_attribution.rb', line 71

def on_result
  @on_result
end

#receiverObject (readonly)

Returns the value of attribute receiver.



71
72
73
# File 'lib/rigor/plugin/effect_attribution.rb', line 71

def receiver
  @receiver
end

#singletonObject (readonly)

Returns the value of attribute singleton.



71
72
73
# File 'lib/rigor/plugin/effect_attribution.rb', line 71

def singleton
  @singleton
end

#taintObject (readonly)

Returns the value of attribute taint.



71
72
73
# File 'lib/rigor/plugin/effect_attribution.rb', line 71

def taint
  @taint
end

#whyObject (readonly)

Returns the value of attribute why.



71
72
73
# File 'lib/rigor/plugin/effect_attribution.rb', line 71

def why
  @why
end

#withinObject (readonly)

Returns the value of attribute within.



71
72
73
# File 'lib/rigor/plugin/effect_attribution.rb', line 71

def within
  @within
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?



132
133
134
# File 'lib/rigor/plugin/effect_attribution.rb', line 132

def ==(other)
  other.is_a?(EffectAttribution) && to_h == other.to_h
end

#hashObject



137
138
139
# File 'lib/rigor/plugin/effect_attribution.rb', line 137

def hash
  to_h.hash
end

#keyObject

The key an origin and a report spell this row as.



117
118
119
120
121
122
# File 'lib/rigor/plugin/effect_attribution.rb', line 117

def key
  return "#{@receiver}.#{@method}" if receiver_path? || self_path?
  return "#{@receiver}()##{@method}" if @on_result

  "#{@receiver}#{@singleton ? '.' : '#'}#{@method}"
end

#receiver_path?Boolean

Whether #receiver is a receiver path (Rails.cache) rather than a class name or a self path.

Returns:

  • (Boolean)


107
108
109
# File 'lib/rigor/plugin/effect_attribution.rb', line 107

def receiver_path?
  @receiver.include?(".") && !self_path?
end

#self_path?Boolean

Whether #receiver is a self path (self.flash.now).

Returns:

  • (Boolean)


112
113
114
# File 'lib/rigor/plugin/effect_attribution.rb', line 112

def self_path?
  @receiver.start_with?("#{SELF_HEAD}.")
end

#to_hObject



124
125
126
127
128
129
130
# File 'lib/rigor/plugin/effect_attribution.rb', line 124

def to_h
  {
    "receiver" => @receiver, "method" => @method.to_s, "singleton" => @singleton,
    "labels" => @labels, "narrow" => @narrow, "discharge" => @discharge, "within" => @within,
    "on_result" => @on_result, "taint" => @taint
  }
end