Build-Validation Analyzers

XMLUI's static analyzer catches whole categories of bugs before your app runs. A typo'd component name (<Buttn>), an attribute the component doesn't declare (labl="…"), an event that doesn't exist (onClik), a conditional that's always true (1 ? a : b), or an event handler that forgot its parentheses (onClick="myAction") — all of these reach you as a precise file:line:column diagnostic with a suggested fix, not a silent no-op at runtime.

The same analyzer pipeline drives three surfaces: the VS Code language server (live red squiggles while you type), the Vite plugin (fail the build on a deliberately broken markup), and a standalone xmlui check CLI (so buildless apps can gate CI the same way). All three share one rule registry — a diagnostic on a missing prop reads identically in the editor, in the build log, and in your GitHub Actions output.

What problems this prevents

  • <Buttn label="Save" /> — unknown component name now fails the build (or the LSP highlights it) with the suggestion Did you mean "Button"?.
  • <Button labl="Save" /> — unknown prop reaches you as a warning with suggestion "label", instead of being silently dropped at render time.
  • <Button onClik="…" /> — unknown event handler reaches you as a warning with suggestion "onClick", instead of never firing.
  • <Button when="1 ? true : false"> — dead conditional gets flagged before it ships, catching debug code that nobody cleaned up.
  • <Button onClick="myAction" /> — the handler that forgot its parens (and therefore does nothing) is caught with suggestion "myAction()".

How it works

Every .xmlui file the framework parses also passes through the analyzer. Each registered rule walks the markup AST (or the expression AST) and emits structured BuildDiagnostic entries with a stable code (id-unknown-component, expr-handler-no-value, …), a severity (info | warn | error), a precise location, and an optional suggested replacement. The walker is the same in all three surfaces — the only thing that changes is how diagnostics are presented.

Rules shipped today

CodeDefault severityWhat it catches
id-unknown-componenterrorComponent tag not in the registry.
id-unknown-propwarnProp name not declared by the component.
id-unknown-eventwarnEvent handler name not declared by the component.
expr-dead-conditionalinfoConditional whose condition is a constant.
expr-handler-no-valueinfoEvent-handler body that returns nothing useful (e.g., a bare identifier).
determinism-floating-point-tokeninfoFloating-point literals in expressions evaluated under deterministic schedulers.
determinism-iteration-order-symbolinfoIteration helpers that read Object.getOwnPropertySymbols.

Additional rule families — id-unknown-method, id-unknown-slot, expr-unused-var, expr-unbound-identifier, id-undefined-component-ref, id-undefined-form-ref, theming-missing-prefix — are registered as no-op stubs today and will activate once their supporting infrastructure (metadata-driven slot declarations and cross-expression scope tracking from the verified-type-contracts plan) ships.

The three surfaces

VS Code (live diagnostics)

Install the XMLUI VS Code extension. Every .xmlui file you open is analyzed on save (and on edit, debounced); findings appear in the Problems panel and as inline squiggles. Rules that emit a suggestions payload also surface as quick-fix code actions — clicking "Replace ButtnButton" applies the rename.

Vite plugin (analyze option)

In a Vite-built XMLUI app, the vite-xmlui-plugin accepts an analyze option:

// vite.config.ts
import { defineConfig } from "vite";
import { xmluiPlugin } from "xmlui/vite";

export default defineConfig({
  plugins: [
    xmluiPlugin({ analyze: "warn" }), // "off" | "warn" (default) | "strict"
  ],
});
  • "off" — analyzer disabled entirely.
  • "warn" (default) — analyzer runs; info and warn print to the console, error fails the build.
  • "strict" — analyzer runs with severity escalation: every rule's strictSeverity overrides its default (info → warn → error).

xmlui check CLI

Buildless (standalone-mode) apps gate CI through the CLI:

npx xmlui check ./my-app                 # default: gnu format, warn mode
npx xmlui check ./my-app --strict        # escalate severities
npx xmlui check ./my-app --format json   # machine-readable output
npx xmlui check ./my-app --rule id-unknown-component
npx xmlui check ./my-app --no-rule expr-dead-conditional

Exit code is non-zero on any error-severity diagnostic, so the CLI plugs straight into a GitHub Actions workflow.

CI integration in new apps

Every template generated by create-xmlui-app ships a ready-to-use GitHub Actions workflow at .github/workflows/check.yml and a default xmlui.config.json that pins each rule's severity. The workflow runs xmlui check on every PR; failing diagnostics block merge per your repo's branch-protection rules.

The xmlui.config.json accepts per-rule overrides:

{
  "analyzer": {
    "rules": {
      "id-unknown-prop": "error",
      "expr-dead-conditional": "off"
    }
  }
}

Suppression directives

Single-rule, comment-based, modelled on the standard LSP norm:

<!-- xmlui-disable-next-line id-unknown-component -->
<MyExperimentalThing />

<!-- xmlui-disable id-unknown-prop -->
<Button experimentalProp="…" />
<!-- xmlui-enable id-unknown-prop -->

There is no blanket disable (<!-- xmlui-disable --> with no code is intentionally not supported). Suppression must always name the rule it silences — the analyzer exists precisely to make rule-by-rule decisions visible.

Strict mode

Strict mode is opt-in today: pass --strict to the CLI or analyze: "strict" to the Vite plugin. When strict is on, every rule's strictSeverity (always one step higher than the default, capped at error) replaces its default severity — so warn findings become build-failing errors. The global appGlobals.strictBuildValidation setting that flips strict on for every surface will become the default in the next major release; for now, choose strict at each call site.

Related