Pentiment.Elixir (pentiment v0.1.5)

Helpers for extracting spans from Elixir AST metadata.

This module provides convenience functions for working with Elixir's compile-time metadata, making it easy to integrate Pentiment with macros and DSLs.

Usage in Macros

defmacro my_macro(expr) do
  span = Pentiment.Elixir.span_from_ast(expr)
  source = Pentiment.Elixir.source_from_env(__CALLER__)

  if invalid?(expr) do
    report = Pentiment.Report.error("Invalid expression")
      |> Pentiment.Report.with_source(__CALLER__.file)
      |> Pentiment.Report.with_label(Pentiment.Label.primary(span, "here"))

    raise CompileError, description: Pentiment.format(report, source)
  end

  # ... normal macro expansion
end

Metadata Keys

Elixir AST metadata typically includes:

  • :line - Line number (1-indexed, always present)
  • :column - Column number (1-indexed, often present)
  • :end_line - End line for multi-line nodes (optional)
  • :end_column - End column (optional)
  • :file - File path (sometimes present)

Summary

Functions

Extracts file path from a Macro.Env struct.

Extracts the leftmost span from two AST nodes.

Extracts line number from a Macro.Env struct.

Creates a Source from a Macro.Env struct.

Creates a span for a literal value at a known position.

Extracts a span from an Elixir AST node.

Extracts a span from a Macro.Env struct.

Extracts a span from Elixir AST metadata.

Returns the display length of a value as it would appear in source code.

Functions

file_from_env(arg1)

@spec file_from_env(Macro.Env.t()) :: String.t() | nil

Extracts file path from a Macro.Env struct.

Examples

iex> Pentiment.Elixir.file_from_env(__CALLER__)
"lib/my_app.ex"

leftmost_span(node1, node2)

@spec leftmost_span(term(), term()) :: Pentiment.Span.Position.t() | nil

Extracts the leftmost span from two AST nodes.

Useful for binary operators where the first operand typically has better position information.

Examples

iex> left = quote do: x
iex> right = quote do: y
iex> Pentiment.Elixir.leftmost_span(left, right)
# Returns span from `left` if available, otherwise from `right`

line_from_env(arg1)

@spec line_from_env(Macro.Env.t()) :: pos_integer() | nil

Extracts line number from a Macro.Env struct.

Examples

iex> Pentiment.Elixir.line_from_env(__CALLER__)
42

source_from_env(arg1)

@spec source_from_env(Macro.Env.t()) :: Pentiment.Source.t() | nil

Creates a Source from a Macro.Env struct.

Reads the source file from the environment's file path.

Examples

defmacro my_macro(expr) do
  source = Pentiment.Elixir.source_from_env(__CALLER__)
  # ...
end

span_for_value(value, line, column)

@spec span_for_value(term(), pos_integer(), pos_integer()) ::
  Pentiment.Span.Position.t()

Creates a span for a literal value at a known position.

Computes the display length for common Elixir literals:

  • Atoms: includes the leading colon (e.g., :foo = 4 chars)
  • Integers: digit count
  • Strings: includes quotes (e.g., "hi" = 4 chars)
  • Variables/identifiers: string length

Examples

iex> Pentiment.Elixir.span_for_value(:foo, 5, 10)
%Pentiment.Span.Position{start_line: 5, start_column: 10, end_line: 5, end_column: 14}

iex> Pentiment.Elixir.span_for_value(12345, 1, 1)
%Pentiment.Span.Position{start_line: 1, start_column: 1, end_line: 1, end_column: 6}

span_from_ast(arg1)

@spec span_from_ast(Macro.t() | term()) :: Pentiment.Span.Position.t() | nil

Extracts a span from an Elixir AST node.

Works with:

  • Raw Elixir AST tuples: {name, meta, args}
  • Structs with a :meta field

When possible, computes the end column from the AST structure:

  • Variables: uses the variable name length
  • Function calls with :closing metadata: uses the closing paren/bracket position
  • Maps, binaries, anonymous functions: uses closing metadata
  • Aliases: uses :last metadata for multi-part aliases
  • Block expressions (case, cond, etc.): uses :end metadata
  • Otherwise: falls back to metadata only

Examples

iex> ast = quote do: x + y
iex> Pentiment.Elixir.span_from_ast(ast)
%Pentiment.Span.Position{start_line: ..., start_column: ...}

iex> Pentiment.Elixir.span_from_ast(:not_ast)
nil

span_from_env(arg1)

@spec span_from_env(Macro.Env.t()) :: Pentiment.Span.Position.t() | nil

Extracts a span from a Macro.Env struct.

Note: Macro.Env only provides line information, not column.

Examples

iex> Pentiment.Elixir.span_from_env(__CALLER__)
%Pentiment.Span.Position{start_line: 42, start_column: 1}

span_from_meta(meta)

@spec span_from_meta(keyword()) :: Pentiment.Span.Position.t() | nil

Extracts a span from Elixir AST metadata.

Accepts a keyword list of metadata (as found in AST nodes).

Examples

iex> Pentiment.Elixir.span_from_meta([line: 10, column: 5])
%Pentiment.Span.Position{start_line: 10, start_column: 5}

iex> Pentiment.Elixir.span_from_meta([line: 10, column: 5, end_line: 10, end_column: 15])
%Pentiment.Span.Position{start_line: 10, start_column: 5, end_line: 10, end_column: 15}

iex> Pentiment.Elixir.span_from_meta([])
nil

value_display_length(atom)

@spec value_display_length(term()) :: pos_integer()

Returns the display length of a value as it would appear in source code.

Examples

iex> Pentiment.Elixir.value_display_length(:foo)
4  # `:foo`

iex> Pentiment.Elixir.value_display_length(12345)
5

iex> Pentiment.Elixir.value_display_length("hello")
7  # `"hello"`