Docs

proa fmt / lint / check

Format macro bodies, run lints, and gate CI.

Open Markdown

rustfmt formats Rust syntax, but it does not understand Proa template macro bodies. Use proa fmt for the inside of html!, html_sync!, xml!, xml_sync!, md!, and md_sync!.

Format

proa fmt src
proa fmt --check --verify src
FlagBehavior
--checkDo not write files. Print a unified diff and exit non-zero if formatting differs.
--verifyReparse formatted output and verify the template tree stayed structurally identical. Always on when writing; this flag adds the pass to --check runs.
--no-verifySkip the render-preserving verification before rewriting files.

Use --check --verify in CI and before large render-path changes.

For multiline raw Markdown templates, the formatter removes excess common source indentation and places the closing macro brace on its own line at the invocation's indentation:

md_sync! { r###"
    # {self.title}

    {self.body}
    "###
}

--verify compares the dedented Markdown semantics before writing, so changing source alignment cannot silently change the rendered document. proa lint reports noncanonical raw-template layout as format/markdown-layout.

Lint

proa lint src
proa lint --deny-warnings src
proa lint --json --min-severity info src
proa lint --github --changed --base origin/main

The linter checks template formatting, accessibility-sensitive markup, render-path performance, component composition, content coverage across render targets, and rsjs slot restrictions.

FlagBehavior
--deny-warningsExit non-zero when warnings are found.
--deny <LEVEL_OR_CODE>--deny warnings is the equivalent warning-deny mode; a diagnostic code such as --deny render/content-coverage fails the run when that code is found. Repeatable. Denied codes are printed even below --min-severity.
--jsonEmit stable JSON diagnostics.
--githubEmit GitHub Actions annotations.
--min-severity error|warning|infoControl printed diagnostics. Text defaults to warnings and errors. JSON and GitHub output default to all diagnostics.
--include-testsInclude Rust test files and #[cfg(test)] modules.
--content-summaryReport paired, unpaired, and whole-view-exempt content status as informational diagnostics. Use --min-severity info to show them in text output.
--root <PATH>Select the project root for content counterpart discovery and RSJS strategy sidecars. Defaults to the nearest Cargo workspace/package or proa.config.json.

Content coverage

A component can render a field or child in HTML and accidentally leave it out of Markdown. Both views still compile. Content coverage compares supported rendered contributions across the component's views and points to the missing content. Reading a field, constructing a child, or creating an unused template does not count as rendering it.

The default policy checks HTML → Markdown when both views exist for the same component. Sync and async implementations belong to the same logical target family. HTML-only and Markdown-only components are valid; neither needs an exception. Other targets can be registered through the content-analysis API; a trait name ending in Render does not register it automatically.

Start by showing informational diagnostics and discovery status:

proa lint src --min-severity info
proa lint src --content-summary --min-severity info

The summary distinguishes paired, unpaired, and exempt views. “Paired” means the views were discovered; it does not certify equivalent content. An opaque helper or different presence condition can leave a contribution unchecked even when its views are paired.

Context and loader usage

Content coverage follows values into the rendered body. It does not require matching cx accesses, loader queries, cache operations, or metadata setup. Known proa_core::WebContext preparation APIs and supported metadata aliases do not create content obligations or unchecked-body diagnostics on their own. An unused fetch does not cover missing content.

Loaded data that reaches output still needs correspondence. If the two views fetch or transform it differently, put the same content_part! name around the rendered contribution in each template. That name is an author assertion, not a proof that two queries or helpers are equivalent. Unmapped loaded output, direct writer calls, and opaque helpers receiving cx remain unchecked. Custom methods merely named load are not treated as known Proa preparation. Response headers, head assets, and telemetry are outside this body-content check.

Intentional omissions: put the marker inside the checked view

Import omit at file scope in the file containing the view that leaves content out. Put the invocation inside its render method, before the template. For HTML → Markdown, that is MdRenderSync::render_md or MdRender::render_md. If the views live in separate files, edit the file containing the Markdown impl.

This complete src/components/product.rs deliberately keeps the image in HTML:

use proa_core::{MdRenderSync, WebContext, WebRenderSync, WriteError};
use proa_macros::{html_sync, md_sync, omit};

struct Product<'a> {
    title: &'a str,
    image: &'a str,
}

impl WebRenderSync for Product<'_> {
    fn render(self, cx: &mut WebContext) -> Result<(), WriteError> {
        html_sync! {
            <article data-product>
                <h1>{self.title}</h1>
                <img src={self.image} alt="" />
            </article>
        }
        .render(cx)
    }
}

impl MdRenderSync for Product<'_> {
    fn render_md(self, cx: &mut WebContext) -> Result<(), WriteError> {
        omit!(self.image, "Image is only shown in HTML");
        md_sync! { r###"# {self.title}"### }.render_md(cx)
    }
}

The reason is optional. The value must type-check, but the marker does not evaluate or move it. Future HTML additions still get checked. Bare paths select child types; use a Rust borrow (&image) to select a local value. Put a local's marker after its declaration and before it is moved. For example, replace the Markdown impl above with:

impl MdRenderSync for Product<'_> {
    fn render_md(self, cx: &mut WebContext) -> Result<(), WriteError> {
        let image = self.image;
        omit!(&image, "Image is only shown in HTML");
        md_sync! { r###"# {self.title}"### }.render_md(cx)
    }
}

An existing child local works the same way. When the HTML view renders a child that has no local in Markdown, select its type directly. For a product whose HTML view renders RestockNotice, this could live in src/views/product_markdown.rs, with the types imported from crate::product. That module must expose the product's title and the notice type:

use proa_core::{MdRenderSync, WebContext, WriteError};
use proa_macros::{md_sync, omit};
use crate::product::{Product, RestockNotice};

impl MdRenderSync for Product<'_> {
    fn render_md(self, cx: &mut WebContext) -> Result<(), WriteError> {
        omit!(RestockNotice<'_>, "Browser-only notice");
        md_sync! { r###"# {self.title}"### }.render_md(cx)
    }
}

This is an alternative example for that product, rather than another impl to add alongside the earlier one. The macro checks the type without constructing a child. Use omit!(RestockNotice) for a type without generic arguments. Lifetime-only child types can use '_. For type/const-generic children, or several occurrences with different inputs or conditions, use an existing child local or a named part to narrow the exception. A field used by several distinct children is ambiguous and waives none of their contributions.

When HTML names a rendered contribution with content_part!("stock-status", ...), use that name as a string selector. In the same file as the Markdown impl, with omit imported as above:

impl MdRenderSync for Product<'_> {
    fn render_md(self, cx: &mut WebContext) -> Result<(), WriteError> {
        omit!("stock-status", "Shown in the overview instead");
        proa_macros::md_sync! { r###"# {self.title}"### }.render_md(cx)
    }
}

Use matching content_part! names in rendered slots when the views present the same content through different helpers. A name is an author assertion about correspondence; the linter does not prove the helpers equivalent.

Exempt a whole view

To intentionally exclude a whole view, including future additions, put the attribute above its impl:

#[proa_macros::content(skip, reason = "This view is a brief summary")]
impl MdRenderSync for Product<'_> {
    fn render_md(self, cx: &mut WebContext) -> Result<(), WriteError> {
        proa_macros::md_sync! { r###"# {self.title}"### }.render_md(cx)
    }
}

Alternatively, put the attribute above the render method inside the impl:

impl MdRenderSync for Product<'_> {
    #[proa_macros::content(skip)]
    fn render_md(self, cx: &mut WebContext) -> Result<(), WriteError> {
        proa_macros::md_sync! { r###"# {self.title}"### }.render_md(cx)
    }
}

Both placements exempt the view's pair checks, including omission-marker checks. The reason is optional. A parent's exemption does not exempt its child's own paired views.

HTML-only and Markdown-only components need no exceptions. Content is compared when both views exist; the linter does not require another render implementation. Separate representation types can pair explicitly with #[proa_macros::content(source = HtmlProduct, from = "html")] above the Markdown impl; use shared content_part! names to establish correspondence between them.

Diagnostics and CI enforcement

Diagnostics name the component and missing contribution, show source frames for both views, and suggest a repair at the correct location. Omissions are explicit author decisions; the linter never inserts them automatically.

For example, removing the omit! line from the complete product above reports:

error[render/content-coverage]: `Product<'_>`'s Markdown view does not render `self.image` from its HTML view
  --> src/components/product.rs:23:9
    |
 22 |     fn render_md(self, cx: &mut WebContext) -> Result<(), WriteError> {
 23 |         md_sync! { r###"# {self.title}"### }.render_md(cx)
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view does not render the contribution
 24 |     }
 25 | }
    |
  ::: src/components/product.rs:14:27
    |
 13 |                 <h1>{self.title}</h1>
 14 |                 <img src={self.image} alt="" />
    |                           ---------- `self.image` rendered in HTML
 15 |             </article>
 16 |         }
    |
   = help: render `self.image` in the Markdown view
   = help: to omit it intentionally, add this inside the render method before its template
           proa_macros::omit!(self.image);
DiagnosticMeaning and repair
render/content-coverageA supported rendered contribution is missing. Render it in the checked view or acknowledge the intentional omission with omit!.
render/content-coverage-uncheckedA helper, presence condition, or discovery boundary prevents a conclusion. Align the supported conditions or declare correspondence with named parts where appropriate.
render/content-omit-stale, render/content-omit-redundantThe source no longer contributes the selected content, or the checked view now renders it. Remove the marker.
render/content-omit-ambiguous, render/content-omit-unresolved, render/content-policy-invalidNarrow the selector, fix its resolution, or correct the policy.

Missing content (render/content-coverage) is an error and fails proa lint by default. Unchecked coverage (render/content-coverage-unchecked) is a warning: it is visible by default and fails with --deny-warnings. proa check and lifecycle source checks reject both errors and warnings. proa check --allow-warnings permits unchecked coverage while still rejecting missing content. Use omit! to acknowledge an intentional difference.

proa lint src/components/product --deny-warnings

Paired, unpaired, and exempt discovery status stays informational. The four omission-marker hygiene codes also remain informational; deny them individually when enforcing exception hygiene. Denied codes are printed even at the default warning threshold; --deny changes exit status without changing a diagnostic's printed severity. --json includes related ranges and structured suggestions. --github includes counterpart locations and repair guidance in Actions annotations.

The checker follows supported output contributions, rather than proving equivalent meaning, order, cardinality, or accessibility across formats. See the content coverage authoring reference for supported analysis, explicit source pairs, scoped source selectors, and future target registration.

Changed Files

lint supports Git-based file selection:

proa lint --changed
proa lint --base origin/main
proa lint --staged

--base <REF> implies changed-file selection against that ref. --staged cannot be combined with --changed or --base.

For content coverage, selected files determine which pairs are reported; analysis also loads unchanged counterparts from their enclosing Cargo packages. A pair's diagnostic is reported when either view is selected. --staged reads the selected paths' working-tree contents, rather than Git index snapshots. Deleted paths are excluded from Git selection; use a full source scan when the only change deletes a view.

Suppressions

For intentional content differences, use the targeted omission or whole-view exemption above.

Performance suppressions must include a reason:

// proa-perf-ok: static SVG is trusted and emitted as raw markup
StaticRaw(ICON_HTML).render(cx)

For an item-level suppression, put the scoped marker directly above the item:

// proa-perf-ok(scope): integration shim allocates outside request render paths
fn render_legacy_fragment(...) { ... }

Use suppressions for intentional low-level cases only. Prefer restructuring the component when the lint points to render-path allocation or avoidable copying.

Check

proa check combines fmt --check --verify and lint, then fails on formatting differences, errors, or warnings (pass --allow-warnings to report warnings without failing):

proa check src
proa check --changed --base origin/main
proa check --staged

With explicit paths or Git selection, check runs only those source checks. Run with no file selection, it scans the project's src and then also runs the linked RSJS Cargo check of the whole project. The Cargo flags (-p, --bin, --profile, --target, --features, --all-features, --no-default-features, --locked, --offline) select the one exact target the linked check compiles; in a multi-package workspace pass -p or --bin explicitly. Pass --source-only to run only the formatter and lints; Cargo flags cannot be combined with file selection or --source-only.

Use check for local pre-push validation and CI. Use fmt or lint directly when you need write mode, JSON diagnostics, GitHub annotations, or severity filtering.

Search

Type at least 2 characters