# `Adze.Definition`
[🔗](https://github.com/matthewlehner/adze/blob/v0.1.0/lib/adze/definition.ex#L1)

Logical-definition primitive.

A definition is a `def` (or `defp` / `defmacro` / `defmacrop` /
`defguard` / `defguardp` / `defdelegate`) bundled with its leading
allowlisted attributes (`@doc`, `@spec`, `@typedoc`, `@impl`,
`@deprecated`, `@since`) and any leading comments those nodes carry.

Used by write ops (`mv`, `extract`, `topo`) — the unit of mechanical
edit is always the whole group, never a bare `def`.

## Grouping rules

  * Group key is `{kind, name, arity}`. Multi-clause defs that share
    the same key and are AST-adjacent collapse into one definition.
  * Blank lines do not break adjacency — only intervening AST nodes do.
  * Allowlisted attributes directly preceding the def attach to it.
    `@moduledoc` is excluded (module-level).
  * `@type`, `@typep`, `@opaque`, `@callback`, and `@macrocallback` are
    *also* valid attachment targets for `@doc`/`@spec`/`@typedoc`/etc.
    Treat them as **consumers** — when one appears, it absorbs any
    pending allowlisted attrs and resets state. No definition is
    emitted for them (definitions are def-family only); they just
    prevent attrs intended for a type or callback from poisoning the
    next def.
  * A non-allowlisted attribute (e.g. `@some_const 5`, `@dialyzer ...`,
    `@job opts`) interleaved between an allowlisted attribute and the
    def is genuinely ambiguous and raises
    `{:error, {:ambiguous_attribute, info}}` — the caller decides
    whether to skip the attr or attach it.
  * Leading comments hang off the *next* AST node (Sourceror behavior)
    so the walk collects them from each attribute it pulls in.

## Ambiguous attribute error shape

    {:error,
     {:ambiguous_attribute,
      %{
        def: %{kind: :def, name: :foo, arity: 0, line: 12},
        attributes: [%{name: :spec, line: 10}, %{name: :doc, line: 9}],
        intervening: [%{name: :some_const, line: 11}]
      }}}

  * `def` — the def whose attachment is ambiguous.
  * `attributes` — the allowlisted attrs that were pending.
  * `intervening` — the non-allowlisted attrs that interrupted the
    run. Always at least one.

## Widening the allowlist

Some Elixir libraries introduce attributes that semantically belong to
the next def — Oban Pro's `@job`, the Decorator library's `@decorate`,
occasionally `@dialyzer`. There are two ways to treat them as
attachable:

**Per call** — pass `include_attrs:` in opts:

    Adze.Definition.find(source, "foo/1", include_attrs: [:job, :decorate])

**Project-wide** — set the application config in `config/config.exs`:

    config :adze, include_attrs: [:job, :decorate]

This is the recommended path when consuming adze as a project dep
(`mix adze ...`): set the allowlist once based on what your project
uses, and every `Adze.Definition` call — including those made by
future `mv` / `extract` / `topo` write ops — picks it up.

Both sources are merged with the built-in allowlist (`@doc`, `@spec`,
`@typedoc`, `@impl`, `@deprecated`, `@since`). Per-call `include_attrs`
adds to the application config; it does not override.

User-provided names take precedence over the built-in consumer list
(`@type`, `@typep`, `@opaque`, `@callback`, `@macrocallback`) — if you
pass `[:type]` (you probably shouldn't), `@type` will attach to a def
instead of acting as a consumer.

# `definition_spec`

```elixir
@type definition_spec() :: String.t() | {atom(), non_neg_integer()}
```

# `opts`

```elixir
@type opts() :: [{:include_attrs, [atom()]}]
```

# `t`

```elixir
@type t() :: %Adze.Definition{
  arity: non_neg_integer(),
  kind: atom(),
  module: String.t(),
  name: atom(),
  nodes: [Macro.t()],
  parts: %{
    leading_comments: [map()],
    attributes: [{atom(), Macro.t()}],
    clauses: [Macro.t()]
  },
  range: Sourceror.Range.t() | nil,
  visibility: :public | :private
}
```

# `find`

```elixir
@spec find(String.t(), definition_spec(), opts()) ::
  {:ok, t()} | {:error, :not_found} | {:error, term()}
```

# `find_file`

```elixir
@spec find_file(Path.t(), definition_spec(), opts()) ::
  {:ok, t()} | {:error, :not_found} | {:error, term()}
```

# `list`

```elixir
@spec list(String.t(), opts()) :: {:ok, [t()]} | {:error, term()}
```

# `list_file`

```elixir
@spec list_file(Path.t(), opts()) :: {:ok, [t()]} | {:error, term()}
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
