Class: Rigor::Plugin::Base

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

Overview

Base class every Rigor plugin subclasses. The plugin gem subclasses Base, declares its identity through Base.manifest, registers the subclass with register, and overrides #init to wire up any state it needs from the injected service container.

Slice 1 ships only the registration / loading plumbing. The protocol hooks (dynamic-return contributions, type-specifying contributions, dynamic reflection) land in subsequent v0.1.0 slices and arrive as additional methods on this class.

Example plugin:

class MyRailsPlugin < Rigor::Plugin::Base
  manifest(
    id: "rails",
    version: "0.1.0",
    description: "Rails framework support for Rigor"
  )

  def init(services)
    @reflection = services.reflection
    @type = services.type
  end
end

Rigor::Plugin.register(MyRailsPlugin)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(services:, config: {}) ⇒ Base

Returns a new instance of Base.



101
102
103
104
# File 'lib/rigor/plugin/base.rb', line 101

def initialize(services:, config: {})
  @services = services
  @config = config.freeze
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



99
100
101
# File 'lib/rigor/plugin/base.rb', line 99

def config
  @config
end

#servicesObject (readonly)

Returns the value of attribute services.



99
100
101
# File 'lib/rigor/plugin/base.rb', line 99

def services
  @services
end

Class Method Details

.manifest(**fields) ⇒ Object

Declares the plugin’s manifest. Called once at class definition time — the resulting Manifest is cached on the class so Loader reads it without constructing the plugin.



43
44
45
46
47
48
49
50
51
# File 'lib/rigor/plugin/base.rb', line 43

def manifest(**fields)
  if fields.empty?
    raise ArgumentError, "plugin #{self} did not declare a manifest" unless defined?(@manifest) && @manifest

    return @manifest
  end

  @manifest = Manifest.new(**fields)
end

.producer(id, serialize: nil, deserialize: nil, &block) ⇒ Object

ADR-7 § “Slice 6-A” — DSL declaration of a cached producer. Plugin authors write

class MyPlugin < Rigor::Plugin::Base
  manifest(id: "rails", version: "0.1.0")

  producer :schema_table do |params|
    schema = io_boundary.read_file("db/schema.rb")
    parse(schema, params)
  end
end

The block runs through ‘instance_exec` so `self` inside the body is the plugin instance — `io_boundary`, `services`, `manifest`, `config` are all in scope. The block receives the call-site `params` Hash as its sole argument; the same params Hash mixes into the cache key per `Cache::Descriptor#cache_key_for`.

‘serialize:` / `deserialize:` are forwarded verbatim to `Cache::Store#fetch_or_compute`. Default round-trip is `Marshal.dump` / `Marshal.load` per the v0.0.9 callable surface; producers whose return values are not Marshal- clean must supply their own pair.

Producer ids are auto-prefixed ‘plugin.<manifest.id>.` at the cache layer (slice 6-C) so plugin-side ids cannot collide with built-in producers.

Raises:

  • (ArgumentError)


81
82
83
84
85
86
87
# File 'lib/rigor/plugin/base.rb', line 81

def producer(id, serialize: nil, deserialize: nil, &block)
  raise ArgumentError, "Plugin::Base.producer requires a block body" if block.nil?

  @producers ||= {}
  @producers[id.to_sym] = { block: block, serialize: serialize, deserialize: deserialize }.freeze
  id.to_sym
end

.producersObject

Frozen snapshot of the producer table. Inherited producers from a superclass are intentionally NOT surfaced — Plugin::Base subclasses do not chain producers, and the loader instantiates one subclass per registration.



94
95
96
# File 'lib/rigor/plugin/base.rb', line 94

def producers
  (@producers || {}).dup.freeze
end

Instance Method Details

#cache_for(producer_id, params: {}, descriptor: nil) ⇒ Object

ADR-7 § “Slice 6-A” — returns a callable that performs a ‘Cache::Store#fetch_or_compute` round-trip for the named producer. The descriptor (per ADR-7 § “Slice 6-B”) is auto-assembled from the plugin’s ‘PluginEntry` template (id, version, config_hash) and the IoBoundary read history. The producer id is auto-prefixed `plugin.<manifest.id>.` per ADR-7 §“Slice 6-C” so plugin caches stay sandboxed from built-in producers.

When ‘services.cache_store` is `nil` (e.g. CLI `–no-cache`), the callable bypasses the cache and runs the producer block every time — same semantics as the v0.0.9 cache surface for built-in producers.

‘descriptor:` (optional, ADR-7 § “Slice 6” follow-up) supplies extra `Cache::Descriptor` rows the plugin author wants to compose into the auto-built descriptor — typically gem-version `GemEntry`, configuration-file `FileEntry` digests, or `ConfigEntry` rows for external state the IoBoundary cannot capture itself. The passed descriptor composes via `Cache::Descriptor.compose` with the auto-built one (PluginEntry template + boundary reads); per-slot conflicts raise `Cache::Descriptor::Conflict` to make divergent inputs visible rather than silently shadowing.



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/rigor/plugin/base.rb', line 221

def cache_for(producer_id, params: {}, descriptor: nil)
  producer = self.class.producers[producer_id.to_sym]
  unless producer
    raise ArgumentError,
          "plugin #{manifest.id.inspect} did not declare producer #{producer_id.inspect}"
  end

  compute = -> { instance_exec(params, &producer[:block]) }
  store = services.cache_store
  return compute unless store

  prefixed_id = "plugin.#{manifest.id}.#{producer_id}"
  composed_descriptor = compose_cache_descriptor(descriptor)
  lambda do
    store.fetch_or_compute(
      producer_id: prefixed_id,
      params: params,
      descriptor: composed_descriptor,
      serialize: producer[:serialize],
      deserialize: producer[:deserialize],
      &compute
    )
  end
end

#diagnostics_for_file(path:, scope:, root:) ⇒ Object

ADR-7 § “Slice 5-A” — per-file diagnostic emission hook. Override in plugin subclasses to return an array of ‘Rigor::Analysis::Diagnostic` rows for the analysed file. The runner stamps each returned diagnostic with `source_family: “plugin.<manifest.id>”` automatically per ADR-7 § “Slice 5-B”; plugin authors should construct diagnostics without setting `source_family` (any value they pass is overwritten).

‘path` is the analysed file path; `scope` is the entry `Rigor::Scope` after `ScopeIndexer` ran; `root` is the parsed `Prism::Node` root. Plugin authors traverse `root` themselves if they need node-scoped rules — the `Rule<TNode>` API ADR-2 § “Custom rules” mentions stays deferred to v0.1.x.

Default returns ‘[]` so plugins that contribute through other channels (e.g. slice-4 narrowing contributions, slice-6 cache producers) do not have to override.



176
177
178
# File 'lib/rigor/plugin/base.rb', line 176

def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
  []
end

#flow_contribution_for(call_node:, scope:) ⇒ Object

ADR-2 § “Flow Contribution Bundle” / v0.1.1 Track 2 slice 7 — per-call return-type contribution hook. When the inference engine dispatches a ‘Prism::CallNode` and neither the precision tiers nor RBS resolve a result, `MethodDispatcher` consults each loaded plugin via this hook ahead of `RbsDispatch`. Plugins that override the default return a FlowContribution bundle whose `return_type` slot pins the call site’s result type.

Default returns nil — plugins that don’t refine return types skip the override. Failures are isolated: a hook that raises gets its contribution dropped silently for this call so the rest of the dispatch chain continues.



127
128
129
# File 'lib/rigor/plugin/base.rb', line 127

def flow_contribution_for(call_node:, scope:) # rubocop:disable Lint/UnusedMethodArgument
  nil
end

#init(services) ⇒ Object

Override in subclasses to wire any state the plugin needs from the injected service container. Default is a no-op so plugins that only contribute through later-slice protocol hooks do not have to define an explicit body.



110
111
112
# File 'lib/rigor/plugin/base.rb', line 110

def init(services) # rubocop:disable Lint/UnusedMethodArgument
  nil
end

#io_boundaryObject

ADR-7 § “Slice 6-A/6-B” — per-plugin IoBoundary. Memoised so the boundary’s accumulated ‘FileEntry` rows persist across producer invocations within the same plugin instance and feed cache invalidation via `cache_for`.



191
192
193
# File 'lib/rigor/plugin/base.rb', line 191

def io_boundary
  @io_boundary ||= services.io_boundary_for(manifest.id)
end

#manifestObject

Convenience accessor — ‘manifest` on the instance returns the class-level manifest declaration.



182
183
184
# File 'lib/rigor/plugin/base.rb', line 182

def manifest
  self.class.manifest
end

#prepare(services) ⇒ Object

ADR-9 slice 3 — per-run preparation hook. The runner invokes ‘#prepare(services)` on every loaded plugin once per `Analysis::Runner.run`, after `#init` has run on every plugin and before any `#diagnostics_for_file` call. Plugins use this hook to compute and publish facts other plugins consume:

def prepare(services)
  services.fact_store.publish(
    plugin_id: manifest.id, name: :model_index, value: model_index
  )
end

Default no-op so plugins without facts to publish leave ‘#prepare` unimplemented. Failures isolate as `:plugin_loader runtime-error` diagnostics; a plugin that raises in `#prepare` has its facts considered un-published and downstream consumers see `nil` from `fact_store.read`.

Slice 3 calls plugins in registration order. ADR-9 slice 5 introduces topological ordering by ‘consumes:` so producers always run before consumers.



153
154
155
# File 'lib/rigor/plugin/base.rb', line 153

def prepare(services) # rubocop:disable Lint/UnusedMethodArgument
  nil
end