Class: Rigor::Effects::PluginFacts

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/effects/plugin_facts.rb

Overview

The loaded plugins' effect contributions, compiled once per process into the tables the scan and the snapshot read (ADR-103 WD2 / WD6 / WD10 / WD14; #387).

A plugin's manifest states its contribution declaratively — effect_labels:, effect_attributions:, effect_edges:, effect_entry_points:, all frozen value objects. Nothing here re-reads a manifest at scan time: the per-call lookup is two Hash reads and, for a class-name row, a walk up a superclass table that is a Hash read per level.

Nothing builds one unless collection is on. Plugin::Registry#effect_contributions is itself lazy — a plugin MAY compute its rows from project facts, and rigor-activejob does — and this class is what turns the result into indices. PluginFacts.build is called from exactly one place on each side of the fork boundary, behind configuration.effects_enabled?, so a rigor check with no effects: block neither allocates one nor lets a plugin read a file for it.

Two matching rules, because the framework has two shapes

A class-name row (ActiveRecord::Base#save) matches through the project's inheritance chain: user.save on a User < ApplicationRecord < ActiveRecord::Base is the whole point, and matching the exact owner (which is what Catalog does, correctly, for Ruby's core) would find nothing. The chain walked is the project's own superclass table — the cross-file discovery pre-pass's discovered_superclasses, the same table ExpressionTyper walks for an unresolved implicit-self call. It stops where the project stops, which is exactly right: ApplicationRecord < ActiveRecord::Base is a line in app/models/application_record.rb, and a class that never names a framework base in project source is not one.

A receiver-path row (Rails.cache#read) matches the receiver expression as written. Rails.cache returns an adapter-dependent object by design — memory, Redis, the filesystem — so there is no receiver class for a class-name row to key on, and the only stable handle is the path the programmer typed.

What is deliberately not consulted

The RBS ancestor chain. A gem's shipped signatures do carry class User < ApplicationRecord-shaped ancestry when the project generates RBS for its models, but reading it here would make a row's reach a function of whether the project happens to run rbs prototype — a plugin contribution that appears and disappears with an unrelated tool. The project's own class … < lines are the fact; a project whose models are declared only in RBS gets no plugin attribution and no taint, which is the fail-quiet direction.

Defined Under Namespace

Classes: Edge, Row

Constant Summary collapse

ANCESTRY_CAP =

How far the superclass walk climbs before giving up. A project hierarchy deeper than this is either pathological or cyclic, and a cycle is possible in a project's as-written table.

24

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(contributions:, superclasses:) ⇒ PluginFacts

Returns a new instance of PluginFacts.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/rigor/effects/plugin_facts.rb', line 97

def initialize(contributions:, superclasses:)
  @warnings = []
  @class_rows = {}
  @path_rows = {}
  @self_rows = {}
  @result_rows = {}
  @edges = []
  @labels_by_owner = {}
  @entry_points = []
  contributions.each { |contribution| absorb(contribution) }
  @superclasses = superclasses || NO_ROWS
  @ancestry = {}
  @digest = compute_digest
  finalize
end

Instance Attribute Details

#digestObject (readonly)

A content digest of every compiled plugin fact — labels, attributions, edges and presets, each with the plugin that contributed it. Identity folds it in, so upgrading a plugin whose rows moved invalidates the effects slot exactly as a re-audited data/effects/core.yml row does. Deliberately independent of the project's superclass table, which is a project input the diagnostics identity already covers.



118
119
120
# File 'lib/rigor/effects/plugin_facts.rb', line 118

def digest
  @digest
end

#entry_pointsObject (readonly)

Every effect_entry_points: preset across the loaded set, in registration order.



95
96
97
# File 'lib/rigor/effects/plugin_facts.rb', line 95

def entry_points
  @entry_points
end

#labels_by_ownerObject (readonly)

{owner => [labels]} — what #extend_registry folds into the run's vocabulary.



92
93
94
# File 'lib/rigor/effects/plugin_facts.rb', line 92

def labels_by_owner
  @labels_by_owner
end

#warningsObject (readonly)

Human-readable notes about contributions that were accepted only in part — a third-party plugin's discharge: true demoted, a label root refused. Surfaced by rigor effects; never a diagnostic, because a plugin the user chose is not the project's mistake to be flagged for.



89
90
91
# File 'lib/rigor/effects/plugin_facts.rb', line 89

def warnings
  @warnings
end

Class Method Details

.build(plugin_registry, superclasses: NO_ROWS) ⇒ Object

Parameters:

  • plugin_registry (Rigor::Plugin::Registry, nil)
  • superclasses (Hash{String=>String,Array<String>}) (defaults to: NO_ROWS)

    the project's as-written superclass table (Scope::DiscoveryIndex#discovered_superclasses). Empty is legal and simply means no row matches through inheritance.



77
78
79
80
81
82
83
84
# File 'lib/rigor/effects/plugin_facts.rb', line 77

def self.build(plugin_registry, superclasses: NO_ROWS)
  contributions = plugin_registry&.effect_contributions || []
  return empty if contributions.empty?

  new(contributions: contributions, superclasses: superclasses)
rescue StandardError
  empty
end

.emptyObject



69
70
71
# File 'lib/rigor/effects/plugin_facts.rb', line 69

def self.empty
  @empty ||= new(contributions: [], superclasses: NO_ROWS)
end

Instance Method Details

#attributions?Boolean

Whether any attribution row exists at all — the scan's fast path, asked once per call site.

Returns:

  • (Boolean)


125
126
127
# File 'lib/rigor/effects/plugin_facts.rb', line 125

def attributions?
  !@class_rows.empty? || !@path_rows.empty? || !@self_rows.empty? || !@result_rows.empty?
end

#class_row(owner, singleton, selector) ⇒ Row?

The row colouring owner's selector, found on owner itself or on a project ancestor of it.

Parameters:

  • owner (String, nil)

    the receiver's class name as the syntax or the typer named it

  • singleton (Boolean)

    whether the call is Owner.selector

Returns:



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/rigor/effects/plugin_facts.rb', line 138

def class_row(owner, singleton, selector)
  return nil if owner.nil? || @class_rows.empty?

  bucket = @class_rows[singleton]
  return nil if bucket.nil?

  ancestry(owner).each do |candidate|
    row = bucket[candidate]&.[](selector)
    return row if row
  end
  nil
end

#descends_from?(class_name, ancestor) ⇒ Boolean

Whether class_name is ancestor, or reaches it through the project's own class … < lines.

Returns:

  • (Boolean)


193
194
195
196
197
# File 'lib/rigor/effects/plugin_facts.rb', line 193

def descends_from?(class_name, ancestor)
  return false if class_name.nil?

  ancestry(class_name).include?(ancestor)
end

#edges?Boolean

Returns:

  • (Boolean)


129
130
131
# File 'lib/rigor/effects/plugin_facts.rb', line 129

def edges?
  !@edges.empty?
end

#edges_for(target) ⇒ Object

The framework-edge strategies of one kind, e.g. every :activerecord_callbacks base class the loaded plugins named.



188
189
190
# File 'lib/rigor/effects/plugin_facts.rb', line 188

def edges_for(target)
  @edges.select { |edge| edge.target == target }
end

#empty?Boolean

Returns:

  • (Boolean)


120
121
122
# File 'lib/rigor/effects/plugin_facts.rb', line 120

def empty?
  !attributions? && @edges.empty? && @labels_by_owner.empty? && @entry_points.empty?
end

#extend_registry(registry) ⇒ Object

registry extended with every plugin's effect_labels:, each under its own owner so Registry#with enforces root ownership per plugin rather than for the set as a whole. A refusal is recorded as a warning and that plugin's labels are dropped; the rest of the run keeps its vocabulary, because one plugin overreaching must not un-name another's labels.



203
204
205
206
207
208
209
210
# File 'lib/rigor/effects/plugin_facts.rb', line 203

def extend_registry(registry)
  @labels_by_owner.each do |owner, labels|
    registry = registry.with(labels: labels, owner: owner)
  rescue Registry::Error => e
    @warnings << "effect labels from #{owner.inspect} were not registered: #{e.message}"
  end
  registry
end

#path_row(path, selector) ⇒ Object

The row colouring path's selector, where path is a receiver expression ("Rails.cache"). Exact — a receiver path names one object and has no ancestry to walk.



153
154
155
156
157
# File 'lib/rigor/effects/plugin_facts.rb', line 153

def path_row(path, selector)
  return nil if path.nil? || @path_rows.empty?

  @path_rows[path]&.[](selector)
end

#result_row(producer, selector) ⇒ Object

The row colouring selector on the RESULT of a call to producer (or to a project ancestor of it): UserMailer.welcome(u).deliver_now, WelcomeJob.set(wait: 1.hour).perform_later. The lazy object in between has no declared type; the class that made it is written in the source.



176
177
178
179
180
181
182
183
184
# File 'lib/rigor/effects/plugin_facts.rb', line 176

def result_row(producer, selector)
  return nil if producer.nil? || @result_rows.empty?

  ancestry(producer).each do |candidate|
    row = @result_rows[candidate]&.[](selector)
    return row if row
  end
  nil
end

#self_path_row(path, selector, owner_class) ⇒ Object

The row colouring path's selector for a receiver rooted at implicit self ("self.flash.now"), inside a unit whose class is owner_class. Answers nil when the row's within: class is not on owner_class's project ancestry — a receiver-less session outside a controller is a different session.



163
164
165
166
167
168
169
170
171
# File 'lib/rigor/effects/plugin_facts.rb', line 163

def self_path_row(path, selector, owner_class)
  return nil if path.nil? || @self_rows.empty?

  row = @self_rows[path]&.[](selector)
  return nil if row.nil?
  return nil unless descends_from?(owner_class, row.within)

  row
end