Mutare.CallRouting.Call (mutare v0.5.1)

Copy Markdown View Source

Stable, resolved view of a known-macro call passed to routing and hosting callbacks.

arguments are the call's arguments, all of them, however the call was written: Mutare treats a piped routed call as the direct call Kernel.|>/2 would build, so (p in Post) |> from(order_by: …) is shown as from(p in Post, order_by: …). A classifier routes the piped operand as the first argument it is, and a host or mutator can read and rewrite it like any other. rebuild.(name, arguments) preserves the source's bare, qualified, or aliased call form; reports still show the pipe the user wrote.

At classification, arguments retain their written syntax. After routing, hosts see Elixir arguments resolved and :raw/:hosted fragments preserved, including their pipe operators.

A module named in an argument is written as the call site's aliases make it (Post for MyApp.Post under alias MyApp.Post), or through __MODULE__ (__MODULE__, __MODULE__.Comment). Arguments are not resolved at classification, so resolved_module/2 reads such a name through the aliases and the module in force at the call site. It answers for the call handed to Mutare.CallRouting.route_arguments/1; a call obtained any other way carries neither, and resolves only an Elixir.-qualified name.

The struct is produced by Mutare. Extension callbacks should match only the fields they need so additional fields can be added compatibly. alias_env and enclosing_module are internal, read only through resolved_module/2. To build one in a test, use new/4 or new/5.

Summary

Functions

Build a call value from the written call node, the resolved module and name, and the rebuild function. arguments are read off node, so the two always agree.

new/4, with what is in force at the call site, for resolved_module/2 to read

The module a node in the call's arguments names, read through the aliases and the module in force at the call site: {:ok, module}, or :error for a node that is not a module name, or that needs something the call does not carry (see the moduledoc). An Elixir.-qualified name ignores aliases, as the compiler does, and so resolves either way. __MODULE__ is the module the call is written in, and __MODULE__.Comment a name beneath it, as the compiler reads them: at a file's top level __MODULE__ is :error and __MODULE__.Comment is Comment, and in a module whose name is computed (defmodule Module.concat(…)) both are :error. Whether the module exists is not checked.

Types

t()

@type t() :: %Mutare.CallRouting.Call{
  alias_env: map() | nil,
  arguments: [Macro.t()],
  enclosing_module: module() | nil,
  module: module() | atom() | nil,
  name: atom(),
  node: Macro.t(),
  rebuild: (atom(), [Macro.t()] -> Macro.t())
}

Functions

new(node, module, name, rebuild)

@spec new(Macro.t(), module() | atom() | nil, atom(), (atom(), [Macro.t()] ->
                                                   Macro.t())) :: t()

Build a call value from the written call node, the resolved module and name, and the rebuild function. arguments are read off node, so the two always agree.

iex> node = quote(do: where(Post, x > 1))
iex> call = Mutare.CallRouting.Call.new(node, Ecto.Query, :where,
...>   fn name, args -> {name, [], args} end)
iex> length(call.arguments)
2

new(node, module, name, rebuild, site)

@spec new(
  Macro.t(),
  module() | atom() | nil,
  atom(),
  (atom(), [Macro.t()] -> Macro.t()),
  aliases: %{required(atom()) => module()},
  enclosing_module: module() | nil
) :: t()

new/4, with what is in force at the call site, for resolved_module/2 to read:

  • aliases: %{Short => Module} — the aliases, the way an alias Module, as: Short would bind them;
  • enclosing_module: Module — the module the call is written in, which __MODULE__ names; nil for a file's top level.

Either may be left out; a call without it resolves no name that needs it.

iex> node = quote(do: where(Post, x > 1))
iex> call = Mutare.CallRouting.Call.new(node, Ecto.Query, :where,
...>   fn name, args -> {name, [], args} end, aliases: %{Post: MyApp.Post})
iex> Mutare.CallRouting.Call.resolved_module(call, hd(call.arguments))
{:ok, MyApp.Post}

resolved_module(call, node)

@spec resolved_module(t(), Macro.t()) :: {:ok, module()} | :error

The module a node in the call's arguments names, read through the aliases and the module in force at the call site: {:ok, module}, or :error for a node that is not a module name, or that needs something the call does not carry (see the moduledoc). An Elixir.-qualified name ignores aliases, as the compiler does, and so resolves either way. __MODULE__ is the module the call is written in, and __MODULE__.Comment a name beneath it, as the compiler reads them: at a file's top level __MODULE__ is :error and __MODULE__.Comment is Comment, and in a module whose name is computed (defmodule Module.concat(…)) both are :error. Whether the module exists is not checked.

iex> node = quote(do: from(p in Post))
iex> call = Mutare.CallRouting.Call.new(node, Ecto.Query, :from,
...>   fn name, args -> {name, [], args} end, aliases: %{Post: MyApp.Post})
iex> {:in, _, [_binding, source]} = hd(call.arguments)
iex> Mutare.CallRouting.Call.resolved_module(call, source)
{:ok, MyApp.Post}
iex> Mutare.CallRouting.Call.resolved_module(call, quote(do: Post.Comment))
{:ok, MyApp.Post.Comment}
iex> Mutare.CallRouting.Call.resolved_module(call, quote(do: p))
:error

__MODULE__ reads the module the call is written in:

iex> node = quote(do: from(p in __MODULE__))
iex> call = Mutare.CallRouting.Call.new(node, Ecto.Query, :from,
...>   fn name, args -> {name, [], args} end, enclosing_module: MyApp.Post)
iex> {:in, _, [_binding, source]} = hd(call.arguments)
iex> Mutare.CallRouting.Call.resolved_module(call, source)
{:ok, MyApp.Post}
iex> Mutare.CallRouting.Call.resolved_module(call, quote(do: __MODULE__.Comment))
{:ok, MyApp.Post.Comment}
iex> unplaced = Mutare.CallRouting.Call.new(node, Ecto.Query, :from,
...>   fn name, args -> {name, [], args} end)
iex> Mutare.CallRouting.Call.resolved_module(unplaced, source)
:error