Pentiment (pentiment v0.2.0)

View Source

Beautiful, informative compiler-style error messages for Elixir.

Pentiment provides rich diagnostic formatting with highlighted source spans, helpful suggestions, and clear error context. It's designed for:

  • Compile-time macro errors
  • DSL validation
  • Parser error reporting
  • Configuration file validation

Quick Start

# Create a diagnostic
report = Pentiment.Report.error("Type mismatch")
  |> Pentiment.Report.with_code("E0001")
  |> Pentiment.Report.with_source("lib/my_app.ex")
  |> Pentiment.Report.with_label(
    Pentiment.Label.primary(Pentiment.Span.position(15, 10), "expected `integer`, found `float`")
  )
  |> Pentiment.Report.with_help("Use `trunc/1` to convert")

# Format and display
IO.puts(Pentiment.format(report, source))

Output Example

error[E0001]: Type mismatch
   ╭─[lib/my_app.ex:15:10]
   │
14 │   add = fn x :: integer, y :: integer ->
15 │     x + y + 1.5
   •             ────┬────
   •                 ╰── expected `integer`, found `float`
   │
   ╰─────
      help: Use `trunc/1` to convert

Core Concepts

  • Span - A region in source code (byte offset or line/column)
  • Label - An annotated span with a message and priority
  • Source - The source text to display context from
  • Diagnostic - The complete error/warning with all metadata
  • Report - A ready-to-use diagnostic struct with builder API

Modules

Summary

Functions

Formats a diagnostic for display.

Formats multiple diagnostics for display.

Formats a diagnostic as a single line (compact format).

Types

format_options()

@type format_options() :: [
  colors: boolean(),
  context_lines: non_neg_integer(),
  formatter: module(),
  syntax: :auto | boolean(),
  highlighter: module()
]

Functions

format(diagnostic, sources, opts \\ [])

Formats a diagnostic for display.

Arguments

For diagnostics whose labels carry their own :source (multi-file diagnostics), pass a map covering every labeled file — labels render as continuation frames against their own source. The single-Source and file-path forms cover only the report's own file; groups for any other file render header-only (├─[file:line:col], no excerpt). Files are never read from disk implicitly for labels.

Options

  • :colors - Whether to use ANSI colors (default: true)
  • :context_lines - Lines of context around labels (default: 2)
  • :formatter - Formatter module (default: Pentiment.Formatter.Renderer)
  • :syntax - Whether to syntax-highlight source context lines (default: :auto: highlight when colors are active, a highlighter is available, and the source's language is known; false disables). Strictly subordinate to :colors — colors: false always yields plain text. Highlighting requires the optional makeup lexers; see Pentiment.Highlighter.Makeup.
  • :highlighter - Module implementing Pentiment.Highlighter (default: Pentiment.Highlighter.Makeup)

Examples

# With a Source struct
source = Pentiment.Source.from_file("lib/app.ex")
Pentiment.format(report, source)

# With a map of sources
Pentiment.format(report, %{"lib/app.ex" => File.read!("lib/app.ex")})

# With a file path (reads from disk)
Pentiment.format(report, "lib/app.ex")

# Without colors
Pentiment.format(report, source, colors: false)

# Multi-file diagnostic: cover every labeled file
Pentiment.format(report, %{
  "lib/app.ex" => Pentiment.Source.from_file("lib/app.ex"),
  "lib/other.ex" => Pentiment.Source.from_file("lib/other.ex")
})

format_all(diagnostics, sources, opts \\ [])

@spec format_all(
  [Pentiment.Diagnostic.t()],
  Pentiment.Source.t() | map() | String.t(),
  format_options()
) :: String.t()

Formats multiple diagnostics for display.

Arguments

Examples

errors = [error1, error2, error3]
IO.puts(Pentiment.format_all(errors, sources))

format_compact(diagnostic)

@spec format_compact(Pentiment.Diagnostic.t()) :: String.t()

Formats a diagnostic as a single line (compact format).

Useful for log output or machine-parseable formats.

Examples

report = Pentiment.Report.error("Type mismatch")
  |> Pentiment.Report.with_code("E0001")
  |> Pentiment.Report.with_source("lib/app.ex")
  |> Pentiment.Report.with_label(Pentiment.Label.primary(Pentiment.Span.position(15, 10), "here"))

Pentiment.format_compact(report)
# => "[E0001] Type mismatch (lib/app.ex:15:10)"