Packages

spark

2.7.5
2.7.8 2.7.7 2.7.6 2.7.5 2.7.4 2.7.3 2.7.2 2.7.1 2.7.0 2.6.1 2.6.0 2.5.0 2.4.1 2.4.0 2.3.14 2.3.13 2.3.12 2.3.11 2.3.10 2.3.9 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.69 2.2.68 2.2.67 2.2.66 2.2.65 2.2.64 2.2.63 2.2.62 2.2.61 2.2.60 2.2.59 2.2.58 2.2.57 2.2.56 2.2.55 2.2.54 2.2.53 2.2.52 2.2.51 2.2.50 2.2.49 2.2.48 2.2.47 2.2.46 2.2.45 2.2.44 2.2.43 2.2.42 2.2.41 2.2.40 2.2.39 2.2.38 2.2.37 2.2.36 2.2.35 2.2.34 2.2.33 2.2.32 2.2.31 2.2.30 2.2.29 2.2.28 2.2.27 2.2.26 2.2.25 2.2.24 2.2.23 2.2.22 2.2.21 2.2.20 2.2.19 2.2.18 2.2.17 2.2.16 2.2.15 2.2.14 2.2.13 2.2.12 2.2.11 2.2.10 2.2.9 2.2.8 2.2.7 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 2.2.1 2.2.0 2.1.24 2.1.23 2.1.22 2.1.21 2.1.20 2.1.19 2.1.18 2.1.17 2.1.16 2.1.15 2.1.14 2.1.13 2.1.12 2.1.11 2.1.10 2.1.9 2.1.8 2.1.7 2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.1 2.0.0 1.1.55 1.1.54 1.1.53 1.1.52 1.1.51 1.1.50 1.1.49 1.1.48 1.1.47 1.1.46 1.1.45 1.1.44 1.1.43 1.1.42 1.1.41 1.1.40 1.1.39 1.1.38 1.1.37 1.1.36 1.1.35 1.1.34 1.1.32 1.1.31 1.1.30 1.1.29 1.1.28 1.1.27 1.1.26 1.1.25 1.1.24 1.1.22 1.1.21 1.1.20 1.1.19 1.1.18 1.1.17 1.1.16 1.1.15 1.1.14 retired 1.1.13 1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.4.12 0.4.11 0.4.10 0.4.9 0.4.8 0.4.7 0.4.6 0.4.5 0.4.4 0.4.3 0.4.2 0.4.1 0.3.12 0.3.11 0.3.10 0.3.9 0.3.8 0.3.7 0.3.6 0.3.5 0.3.4 0.3.3 0.3.2 0.3.1 0.3.0 0.2.18 0.2.17 0.2.16 0.2.15 0.2.14 0.2.13 0.2.12 0.2.11 0.2.10 0.2.9 0.2.8 0.2.7 0.2.6 0.2.5 0.2.4 0.2.3 0.2.2 0.2.1 0.2.0 0.1.29 0.1.28 0.1.27 0.1.26 0.1.25 0.1.24 0.1.23 0.1.22 0.1.21 0.1.20 0.1.19 0.1.18 0.1.17 0.1.16 0.1.15 0.1.14 0.1.13 0.1.12 0.1.11 0.1.10 0.1.9 0.1.8 0.1.7 0.1.6 0.1.5 0.1.4 0.1.3 0.1.2 0.1.1 0.1.0

Generic tooling for building DSLs

Current section

Files

Jump to
spark documentation how_to use-source-annotations.md
Raw

documentation/how_to/use-source-annotations.md

<!--
SPDX-FileCopyrightText: 2020 Zach Daniel
SPDX-FileCopyrightText: 2022 spark contributors <https://github.com/ash-project/spark/graphs.contributors>

SPDX-License-Identifier: MIT
-->

# Using Source Annotations

Spark automatically tracks source location information for all DSL elements
using Erlang's `:erl_anno` module. This provides comprehensive location tracking
for sections, options, and entities, enabling better error messages, IDE
integration, and debugging capabilities.

Source annotations are only enabled when the Elixir compile option `debug_info`
is enabled (`Code.get_compiler_option(:debug_info)` returns true). By default,
debug info is disabled in production and in `.exs` script files, which means
source annotations won't be available in those contexts.

> #### ExUnit Test Cases {: .warning}
>
> If you're defining modules inside ExUnit test cases (which use `.exs` files),
> source annotations won't be available unless you explicitly enable
> `debug_info` in your tests.
>
> ```elixir
> setup do
>   debug_info? = Code.get_compiler_option(:debug_info)
>   Code.put_compiler_option(:debug_info, true)
>   on_exit(fn -> Code.put_compiler_option(:debug_info, debug_info?) end)
>   :ok
> end
> ```

## What are Source Annotations?

Source annotations capture metadata about where DSL elements are defined in your
source code, including:

- **File path**: The source file where the DSL element is declared
- **Line number**: The exact line where the element starts
- **End location**: The line where DSL blocks end (available on OTP 28+,
  requires Elixir Parser Configuration)

```elixir
defmodule Acme.MixProject do
  use Mix.Project

  def project do
    [
      app: :acme,
      elixirc_options: [
        parser_options: [
          token_metadata: true,
          parser_columns: true
        ]
      ],
      # ...
    ]
  end
end
```

Spark tracks annotations for:
- **Sections**: Location where section blocks are defined (`section do ... end`)
- **Options**: Location where individual options are set (`option_name "value"`)
- **Entities**: Location where entities are declared (`entity :name do ... end`)

## Annotation Introspection

### Universal Access via Introspection Functions

Spark provides introspection functions that work regardless of whether entities
define an `anno_field`. These functions access annotation data stored in the DSL
state:

```elixir
# Get DSL state
dsl_state = MyModule.spark_dsl_config()

# Section annotations
section_anno = Spark.Dsl.Extension.get_section_anno(dsl_state, [:my_section])
if section_anno do
  # Extract line number (Spark currently provides line numbers only)
  line = case :erl_anno.location(section_anno) do
    {line_num, _col} -> line_num
    line_num -> line_num
  end
  file = :erl_anno.file(section_anno) |> to_string()
  IO.puts("Section defined at #{file}:#{line}")
end

# Option annotations
option_anno = Spark.Dsl.Extension.get_opt_anno(dsl_state, [:my_section], :option_name)
if option_anno do
  line = :erl_anno.location(option_anno)
  file = :erl_anno.file(option_anno) |> to_string()
  IO.puts("Option defined at #{file}:#{line}")
end

# Entity annotations
entities = Spark.Dsl.Extension.get_entities(dsl_state, [:my_section])
Enum.each(entities, fn entity ->
  case Spark.Dsl.Entity.anno(entity) do
    nil -> :ok
    anno ->
      line = :erl_anno.location(anno)
      file = :erl_anno.file(anno) |> to_string()
      IO.puts("Entity defined at #{file}:#{line}")
  end
end)
```

### Entity Annotations

For direct access to annotations, entities should include the
`__spark_metadata__` field in their struct definition:

```elixir
defmodule MyEntity do
  defstruct [
    :name,
    :__spark_metadata__ # Required for annotation access
  ]
end

@my_entity %Spark.Dsl.Entity{
  name: :my_entity,
  target: MyEntity,
  schema: [
    name: [type: :atom, required: true]
  ]
}

# Access annotations
entities = Spark.Dsl.Extension.get_entities(dsl_state, [:my_section])
Enum.each(entities, fn entity ->
  if entity_anno = Spark.Dsl.Entity.anno(entity) do
    line = :erl_anno.location(entity_anno)
    file = :erl_anno.file(entity_anno) |> to_string()
    IO.puts("Entity defined at #{file}:#{line}")
  end

  if name_anno = Spark.Dsl.Entity.property_anno(entity, :name) do
    line = :erl_anno.location(name_anno)
    file = :erl_anno.file(name_anno) |> to_string()
    IO.puts("Entity name property defined at #{file}:#{line}")
  end
end)
```

## Working with Annotations

Annotations use Erlang's `:erl_anno` module, which provides several utilities:

```elixir
# Check if something is an annotation
:erl_anno.is_anno(anno)

# Get the location (line number or {line, column})
# Note: Spark currently only provides line numbers, not column information
location = :erl_anno.location(anno)

# Helper function to extract line number from location
get_line = fn location ->
  case location do
    {line_num, _column} -> line_num  # Future column support
    line_num when is_integer(line_num) -> line_num  # Current Spark behavior
  end
end

line = get_line.(location)

# Get the file (returns :undefined or a charlist)
file = :erl_anno.file(anno)

# Convert charlist to string safely
file_string = case file do
  :undefined -> "unknown"
  charlist -> to_string(charlist)
end

# Get the end location (OTP 28+, returns :undefined if not available)
if function_exported?(:erl_anno, :end_location, 1) do
  end_location = :erl_anno.end_location(anno)
end
```

## Use Cases

### Enhanced Error Messages in Verifiers

Create precise error messages that point to the exact source location:

```elixir
defmodule MyLibrary.Verifiers.UniqueNames do
  use Spark.Dsl.Verifier

  def verify(dsl_state) do
    entities = Spark.Dsl.Extension.get_entities(dsl_state, [:my_section])

    case find_duplicate(entities) do
      nil -> :ok
      {duplicate_name, duplicate_entity} ->
        location = Spark.Dsl.Entity.anno(duplicate_entity)

        {:error,
         Spark.Error.DslError.exception(
           message: "Duplicate entity name: #{duplicate_name}",
           path: [:my_section, duplicate_name],
           module: Spark.Dsl.Verifier.get_persisted(dsl_state, :module),
           location: location
         )}
    end
  end
end
```

### Enhanced Error Messages in Transformers

```elixir
defmodule MyLibrary.Transformers.ValidateEntity do
  use Spark.Dsl.Transformer

  def transform(dsl_state) do
    entities = Spark.Dsl.Extension.get_entities(dsl_state, [:my_section])

    entities
    |> Enum.each(fn entity ->
      if invalid?(entity) do
        location = Spark.Dsl.Entity.anno(entity)

        raise Spark.Error.DslError,
          message: "Invalid configuration for #{entity.name}",
          path: [:my_section, entity.name],
          location: location
      end
    end)

    {:ok, dsl_state}
  end
end
```

### IDE Integration and Language Servers

Language servers can provide enhanced features using annotation data:

```elixir
defmodule MyLanguageServer do
  def find_definition(file, line, column) do
    # Find modules that might contain DSL at this location
    modules = find_modules_in_file(file)

    Enum.find_value(modules, fn module ->
      dsl_state = module.spark_dsl_config()

      # Check section annotations
      Enum.find_value(dsl_state, fn {path, config} ->
        if match_location?(config.section_anno, line) do
          {:section, path, config.section_anno}
        end
      end) ||

      # Check entity annotations
      find_entity_at_location(dsl_state, line)
    end)
  end
end
```

### Debugging and Development Tools

Create debugging utilities that show DSL source locations:

```elixir
defmodule MyLibrary.Debug do
  def inspect_dsl_sources(module) do
    dsl_state = module.spark_dsl_config()

    # Show all DSL elements with their locations
    Enum.each(dsl_state, fn {path, config} ->
      IO.puts("Section #{inspect(path)}:")

      if config.section_anno do
        print_location("  Section", config.section_anno)
      end

      # Show options
      Enum.each(config.opts_anno, fn {opt_name, anno} ->
        print_location("  Option #{opt_name}", anno)
      end)

      # Show entities
      Enum.each(config.entities, fn entity ->
        anno = Spark.Dsl.Entity.anno(entity)
        print_location("  Entity #{entity.name}", anno)
      end)
    end)
  end

  defp print_location(label, anno)
  defp print_location(label, nil), do: nil
  defp print_location(label, anno) do
    line = :erl_anno.location(anno)
    file = :erl_anno.file(anno) |> to_string() |> Path.relative_to_cwd()
    IO.puts("    #{label}: #{file}:#{line}")
  end
end
```

## Best Practices

### 1. Always Include Location in DslErrors

When creating DslErrors, include location information whenever available:

```elixir
# Get the appropriate annotation for your error context
location = case error_type do
  :section_error ->
    Spark.Dsl.Transformer.get_section_anno(dsl_state, path)
  :option_error ->
    Spark.Dsl.Transformer.get_opt_anno(dsl_state, path, option_name)
  :entity_error ->
    entity = Enum.at(entities, entity_index)
    Spark.Dsl.Entity.anno(entity)
end

{:error,
 Spark.Error.DslError.exception(
   message: "Clear error description",
   path: path,
   module: module,
   location: location
 )}
```

### 2. Handle Missing Annotations Gracefully

Not all annotations may be available (e.g., programmatically generated DSL
elements):

```elixir
location_info = if anno do
  line = :erl_anno.location(anno)
  file = :erl_anno.file(anno) |> to_string()
  " at #{file}:#{line}"
else
  ""
end

IO.puts("Error in entity#{location_info}")
```

### 3. Use Both Introspection and Anno Fields

- **Use introspection functions** for universal access in verifiers and
  transformers
- **Use anno fields** in entity structs for convenient access in application
  code

### 4. Check OTP Version for End Location

End location tracking requires OTP 28+:

```elixir
if function_exported?(:erl_anno, :end_location, 1) do
  end_location = :erl_anno.end_location(anno)
  # Use end location for precise span information
end
```

## Current Limitations

- Column information is not currently tracked (only line numbers)
- End Location is only tracked for OTP28+
- End Location is not available for multiline options