Adze.Types (Adze v0.1.0)

Copy Markdown View Source

Module-local typespec primitives.

Two responsibilities:

  • index/1 — build a {name, arity} => :public | :private map of the @type / @typep / @opaque declarations in a module's body, used to recognize local type references when rewriting extracted specs.

  • qualify/3 — walk an @spec / @callback / @macrocallback attribute AST and rewrite every bare reference to a local type into a fully-qualified call against the source module. So @spec foo() :: t() inside MyApp.Source becomes @spec foo() :: MyApp.Source.t() when extracted.

Built specifically for Adze.Extract: when a public def whose @spec references a module-local type gets pulled into a new module, the qualified form keeps the type defined in exactly one place (the original source) and the new module simply references it.

What's rewritten

  • Bare t/0Source.t/0.
  • result(a, b)Source.result(a, b), recursively walking a and b too.
  • id() inside @spec fetch(id())Source.id(). The function name (fetch) is preserved because the rewriter is structurally aware of @spec/@callback shape.

What's left alone

  • String.t() and any other already-qualified call.
  • Built-in types (integer, term, pos_integer, etc.) — they're not in the index so the qualification check misses cleanly.
  • @spec head name (function being specced).
  • Anything outside @spec / @callback / @macrocallback (e.g. @doc, @typedoc, function bodies — see qualify/3 clauses).

@typep references

An extracted @spec cannot reference a @typep from another module — the type is module-private. We surface this by throwing {:typep_referenced, %{type: {name, arity}, source: module}}, which Adze.Extract catches and returns as {:error, ...}. The user's remedy: widen the @typep to @type in source first, then re-extract.

Summary

Functions

Build a type index from a list of top-level module body nodes.

Qualify a typespec attribute against source_module.

Does this typespec AST reference any local type in type_index?

Types

type_index()

@type type_index() :: %{required({atom(), non_neg_integer()}) => :public | :private}

Functions

index(body_nodes)

@spec index([Macro.t()]) :: type_index()

Build a type index from a list of top-level module body nodes.

Walks the body for @type / @typep / @opaque declarations. Returns %{} if there are none.

qualify(other, type_index, source_module)

@spec qualify(Macro.t(), type_index(), String.t()) :: Macro.t()

Qualify a typespec attribute against source_module.

Rewrites @spec / @callback / @macrocallback ASTs so every local type reference is replaced with a fully-qualified call against source_module. Other attributes (@doc, @typedoc, etc.) pass through unchanged.

May throw {:typep_referenced, info} if the typespec references a @typep from the source module — callers should catch and convert to an error.

references_local?(ast, type_index)

@spec references_local?(Macro.t(), type_index()) :: boolean()

Does this typespec AST reference any local type in type_index?

Cheap pre-check used by Adze.Extract to decide whether a closure definition needs the AST-rewrite path or can stay on the slice-from-source path.