# `MishkaCmsCore.Runtime.PageBuilder.CodeCheck`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/page_builder/code_check.ex#L5)

Compiles a page's code without loading it, so an author is told about a mistake instead of a
visitor being shown one.

## Why this has to exist

A page's `template`, `events`, `infos` and `helpers` are all Elixir by the time
`MishkaCmsCore.Runtime.Compilers.LiveViewCompiler` is done with them, and nothing between the form
and the BEAM reads them first. The failure that follows is quiet in three different ways:

  * a page whose `precompile` is false — the default — is not compiled when it is saved, so the
    save reports success and `MishkaCmsCore.Runtime.RuntimeStatus` reports `Ready`;
  * a page whose module IS loaded and then fails to recompile keeps serving the previous version,
    because the old code is only purged on the success branch;
  * the reason is recorded in `MishkaCmsCore.Runtime.States.CompileFailureState` and read by
    nothing that a human can see.

So the author saves, is told nothing, and the page either does not change or 500s for the first
visitor. This module is the missing answer to "did that work?".

## Three classes of wrong, and which of them a check can catch

Measured against the real compiler:

    {:noreply, socket}     parses, compiles, works
    {:noreply, socket      raises TokenMissingError — caught here, with a line
    def def def            parses fine, fails when the module is compiled — caught here
    ""                     parses to nothing, compiles to nil, crashes on the first click —
                           caught here by name, because the message a reader needs is not the
                           one the runtime would eventually give them

## The budget

Compiling a page means expanding `use MishkaCmsWeb, :live_view`, which is about a second of work,
so the budget is generous: it is here to catch a macro that never returns, not to police a busy
machine. Checks do not wait for each other — each compiles under a name of its own — so the
deadline only ever covers one compile.

## Fidelity

The stages are `LiveViewCompiler`'s own — `build_assigns/1`, `build_page_forms/1`,
`build_event_handlers/1`, `build_info_handlers/1`, `build_renders/1`, `build_helpers/1`,
`create_module_ast/1` — called directly rather than through `build_module/1`, which differs only
in firing the `runtime:compile:before_page` hook and a telemetry span. A check must not fire either, and a check
that reimplemented the stages would eventually pass code the real compile rejects.

Running them one at a time is also what makes the error attributable: each stage owns one group of
fragments, so a raise identifies the tab without parsing the message.

# `fault`

```elixir
@type fault() :: %{
  tab: :template | :events | :infos | :helpers | :page,
  index: non_neg_integer() | nil,
  name: String.t() | nil,
  line: pos_integer() | nil,
  message: String.t()
}
```

One thing wrong. `tab` is the group it belongs to, `index` and `name` locate it within that group,
and `line` is a line in the author's own box — each fragment is authored standalone, so there is no
offset to apply.

# `check`

```elixir
@spec check(map()) :: :ok | {:error, [fault()]}
```

Checks one page's code.

Takes the same shape `LiveViewCompiler` takes — `:template`, `:events`, `:infos`, `:state`,
`:helpers` — and fills in the rest, because none of it affects whether the code compiles.

Returns `:ok`, or every fault it can attribute. Fragment faults are reported together so an author
fixing three typos is told about three typos; a module-level fault is reported alone, because
everything after the first one is noise.

# `message`

```elixir
@spec message(fault()) :: String.t()
```

One line of prose for a fault, the way it should read above the box it belongs to.

# `ok?`

```elixir
@spec ok?(map()) :: boolean()
```

Whether the page's code compiles. For a caller that does not want to render the reasons.

# `template`

```elixir
@spec template(String.t() | nil) :: :ok | {:error, fault()}
```

Whether a TEMPLATE is HEEx the compiler will take.

THE TOKENIZER IS NOT THE COMPILER, and until this existed the tokenizer was the only thing the
editor asked. `MishkaCmsCore.Runtime.PageBuilder.Tree.parse/1` accepts `{@a + }` — the braces
delimit an expression and the expression is a token to it — and HEEx refuses it. So hand-written
markup went into the document, into a draft and into the live record, and the failure surfaced
later as a site that would not compile, in a log nobody was looking at.

This is `check/1`'s syntax half on its own: `Phoenix.LiveView.TagEngine.compile/2`, which is the
call every one of this runtime's compilers makes, with no module built around it. It costs
milliseconds rather than the second a full page compile does, which is what lets it sit in front of
every Apply in the code drawer rather than only in front of a publish.

What it CANNOT catch is anything semantic — an undefined function, a component called with the
wrong attrs — because none of that is decided until the module is compiled. `check/1` is where
those are caught, for a page; the class this catches is the class the tokenizer lets through.

---

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