Bloccs.Node.EffectLint (bloccs v0.12.0)

Copy Markdown View Source

Compile-time capability linter for node bodies.

A node's pure_core and effect_shell are ordinary Elixir, which means nothing in the language stops them from calling File.write!/2, System.cmd/2, :httpc.request/4, or Req.get/1 directly — sidestepping the declared-capability facade (ctx.effects.*) entirely. This pass closes that gap: it walks the clause bodies and rejects, at compile time, any call that reaches the world outside the facade, so the facade is the only legal path to IO.

What is allowed

A remote call M.f(...) in a node body is permitted iff M is:

  1. the effect facade (Bloccs.Effects.HTTP/DB/Time/Random) — effect_shell only; pure_core must be side-effect-free, so the facade is rejected there;
  2. a curated, known-pure stdlib module (Enum, Map, String, Date, …);
  3. Application for read-only config (its mutators are denied);
  4. a module in this node's own OTP application (resolves local helpers), or sharing the node module's top-level namespace;
  5. a module explicitly listed in the manifest's [lint] allow.

Everything else world-touching is a CompileError. Dynamic dispatch (apply/3, a call on a variable module, captures of denied modules) is rejected because it is not statically analyzable. Process/messaging escapes (spawn, send, receive, Process.*, Task.*) are rejected — concurrency is the generated supervision tree's job, not a node body's.

Honest scope (the TCB)

The trusted computing base is this pass plus the effect adapters. Within it the capability declaration is unbypassable for the restricted module surface. It is not a soundness proof: a too-permissive allow entry, a node calling its own app's Repo directly (allowed by rule 4), or a compromised dependency at runtime are out of scope. Runtime isolation remains OTP's job.

Opting out

[lint] effects = "off" downgrades violations to a single loud warning so a node with a vetted exception still compiles; the opt-out is recorded on the manifest and surfaced by tooling so the residual review stays visible.

Summary

Functions

Classify a single call target module.fun under ctx (%{kind, app, ns, allow}). Shared by the intraprocedural walker (this module) and the transitive call-graph pass (Bloccs.Lint.Transitive).

Build the classification context the transitive pass passes to classify/3: %{kind, app, ns, allow}. (The intraprocedural walker builds its own, which additionally carries :env for alias expansion.)

The set of effect-facade modules (permitted only in effect_shell).

Lint the clause bodies of one node function. kind is :pure_core or :effect_shell. lint is the manifest [lint] config (or nil → enforced with no extra allows). Raises CompileError on a violation when enforcing; emits a warning when opted out; returns :ok otherwise.

Functions

classify(module, fun, ctx)

@spec classify(module(), atom(), map()) ::
  :ok | {:follow, module()} | {:deny, String.t()}

Classify a single call target module.fun under ctx (%{kind, app, ns, allow}). Shared by the intraprocedural walker (this module) and the transitive call-graph pass (Bloccs.Lint.Transitive).

  • :ok — permitted and terminal (facade, pure stdlib, config, explicit allow, exception callback).
  • {:follow, module} — a same-application / same-namespace module: permitted for a directly-written call, but a target the transitive pass must traverse.
  • {:deny, reason} — a capability violation.

context(kind, module, app, lint)

@spec context(
  :pure_core | :effect_shell,
  module(),
  atom(),
  Bloccs.Manifest.Lint.t() | nil
) :: map()

Build the classification context the transitive pass passes to classify/3: %{kind, app, ns, allow}. (The intraprocedural walker builds its own, which additionally carries :env for alias expansion.)

facade()

The set of effect-facade modules (permitted only in effect_shell).

lint(env, kind, bodies, lint)

@spec lint(
  Macro.Env.t(),
  :pure_core | :effect_shell,
  [Macro.t()],
  Bloccs.Manifest.Lint.t() | nil
) ::
  :ok

Lint the clause bodies of one node function. kind is :pure_core or :effect_shell. lint is the manifest [lint] config (or nil → enforced with no extra allows). Raises CompileError on a violation when enforcing; emits a warning when opted out; returns :ok otherwise.