docs: add rule for when @impl functions earn their own @doc
Pattern 10 (Callback Documentation Convention) now owns the full rule for callback documentation — both the behaviour side (@callback docs) and the implementation side (when to override @doc false on @impl functions). Patterns 2 and 5 cross-reference Pattern 10 instead of making their own partial statements. The test: "Would this statement be true of any implementation?" If yes, it belongs on the @callback. If no, the implementation earns its own @doc.
This commit is contained in:
@@ -215,7 +215,7 @@ def chunk(list, size), do: ...
|
||||
**Don't use this when:**
|
||||
- The function is private (use `# comments` for private function notes)
|
||||
- The function name + typespec are completely self-explanatory (e.g., `@spec pid() :: pid()`)
|
||||
- You're implementing a behaviour callback and want it hidden (`@impl true` sets `@doc false` automatically)
|
||||
- You're implementing a behaviour callback — `@impl true` sets `@doc false` automatically. Override only when the implementation has semantics the behaviour can't speak to (see [Pattern 10](#when-to-override-doc-false-on-impl-functions))
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
@@ -456,6 +456,7 @@ def __struct__(fields), do: ...
|
||||
- The function IS part of the public API (even if you think it's "obvious")
|
||||
- You want to discourage use but still document it (use `@doc deprecated:` instead)
|
||||
- You're hiding functions because you're too lazy to document them
|
||||
- The `@impl` function has implementation-specific semantics that callers need to know (see [Pattern 10 — implementation-side docs](#when-to-override-doc-false-on-impl-functions))
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
@@ -481,6 +482,8 @@ end
|
||||
|
||||
**Why:** `@doc false` means "this function is not part of the public API." If users are expected to call it, it needs documentation. Hiding public API behind `@doc false` is a maintenance hazard — users will call undocumented functions and break on upgrades.
|
||||
|
||||
> **Note:** `@impl true` auto-sets `@doc false`, but some implementations earn their own `@doc`. See [Pattern 10 — implementation-side docs](#when-to-override-doc-false-on-impl-functions) for the rule.
|
||||
|
||||
---
|
||||
|
||||
## 6. @moduledoc false — Hiding Modules
|
||||
@@ -941,6 +944,67 @@ Called to format the value.
|
||||
|
||||
**Why:** A callback with one parameter and one return type doesn't need a full reference manual. Match documentation depth to complexity — a one-liner with good naming is better than padded sections that add no information.
|
||||
|
||||
### When to override `@doc false` on `@impl` functions
|
||||
|
||||
`@impl true` sets `@doc false` by default — the assumption is that the behaviour's `@callback` doc covers it. This is correct when the implementation is unremarkable. Override it with an explicit `@doc` when the implementation has semantics that would not be true of every conforming implementation.
|
||||
|
||||
**The test:** "Would this statement be true of *any* implementation of this callback?" If yes → the doc belongs on the `@callback` in the behaviour, not here. If no → the implementation earns its own `@doc`.
|
||||
|
||||
**Override when:**
|
||||
- The implementation makes a guarantee the behaviour doesn't promise (e.g., "always returns immediately", "never buffers", "fires at most once")
|
||||
- There's a race condition, ordering constraint, or subtle failure mode specific to this implementation
|
||||
- The implementation's behavior under edge cases differs from what the behaviour's generic contract implies
|
||||
|
||||
**Don't override when:**
|
||||
- The doc would just restate the `@callback` doc in different words
|
||||
- The doc describes what the function does rather than what's surprising about this implementation
|
||||
- The function name + behaviour doc are sufficient for an implementor or caller to understand it
|
||||
|
||||
**Example — unnecessary override (just restates the contract):**
|
||||
```elixir
|
||||
defmodule JsonSerializer do
|
||||
@behaviour Serializer
|
||||
|
||||
@doc """
|
||||
Encodes the given term to a binary.
|
||||
"""
|
||||
@impl true
|
||||
def encode(term), do: Jason.encode!(term)
|
||||
end
|
||||
```
|
||||
|
||||
**Example — justified override (implementation-specific guarantee):**
|
||||
```elixir
|
||||
defmodule Immediate do
|
||||
@behaviour Aggregation
|
||||
|
||||
@doc """
|
||||
Always returns `{:ready, [signal]}` — immediate mode fires on first signal
|
||||
without buffering or waiting for a timer.
|
||||
"""
|
||||
@impl true
|
||||
def check(%Signal{} = signal), do: {:ready, [signal]}
|
||||
end
|
||||
```
|
||||
|
||||
**Example — justified override (race condition warning):**
|
||||
```elixir
|
||||
defmodule AlpacaAdapter do
|
||||
@behaviour BrokerAdapter
|
||||
|
||||
@doc """
|
||||
Cancels an open order by broker ID. Returns `:ok` on success.
|
||||
|
||||
The order may still receive a final fill between the cancel request
|
||||
and confirmation — callers must handle the `partially_filled` → `cancelled` race.
|
||||
"""
|
||||
@impl true
|
||||
def cancel(credential, broker_order_id), do: ...
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The behaviour's `@callback` doc is the generic contract — "what any implementation must do." An implementation's `@doc` is the specific contract — "what callers of *this* implementation can rely on." When those differ meaningfully, silence (`@doc false`) hides information that would prevent bugs.
|
||||
|
||||
---
|
||||
|
||||
## 11. Documentation with Link References (c: and t: prefixes)
|
||||
|
||||
Reference in New Issue
Block a user