EUIX Doctor — Static Analysis

EUIX Doctor (@euix/doctor) statically analyzes .xml, .html, and JavaScript/TypeScript EUIX templates without mounting the app. Use it to catch broken bindings, missing handlers, composition errors, and prop contract violations before browser or E2E tests.

Doctor is a linter + semantic analyzer + lightweight dry-run simulator. It complements Vitest and Playwright; it does not replace them.


🚀 Quick Start #

BASH
# Monorepo root
npm run doctor

# Scan a directory or single file
npx euix doctor apps/playground/components
npx euix doctor path/to/Component.xml

# Fail CI on errors
npx euix doctor . --json > doctor-report.json

# Inspect one file (entity breakdown + diagnostics)
npx euix doctor inspect path/to/Component.xml

# Run safe dry-run flow scenarios
npx euix doctor path/to/components --test

Standalone package (after publish):

BASH
npx @euix/doctor
npx euix-doctor inspect path/to/Component.xml

✅ What Doctor Validates #

AreaRulesExamples
State modelEUIX1001, EUIX1002Action writes unknown state; unused state
ComputedEUIX1101, EUIX1102Computed dependency cycles; unknown deps
WatchersEUIX1201Watcher writes the path it watches
Events & actionsEUIX1301Event handler action not found
CompositionEUIX1401, EUIX1402, EUIX1403Unresolved component; missing required prop; prop type mismatch
API & streamsEUIX1501Missing error path on fetch/stream

Doctor also builds a behavior graph (event → action → state → computed → binding) and generates safe test scenarios with --test.


🧩 Composition & Multi-File Analysis #

Doctor resolves component composition without requiring a full app mount:

FeatureSupport
<component name="X" src="./X.xml">Resolves path; auto-imports missing files
Custom tags (<Header />, <Dashboard />)Matched to component_def or sibling .xml files
<import src="...">, <route component="...">Recorded and linked in the dependency graph
Single-file scanScanning App.xml auto-discovers sibling Header.xml, Dashboard.xml, etc.

Composition edges appear in the dependency graph as composes (parent → child).

Example: multi-file app #

App.xml

XML
<uid_spec>
  <data_model>
    <state id="user" type="object">{"name": "Guest"}</state>
  </data_model>

  <Header user="{data.user}" />
</uid_spec>

Header.xml

XML
<component_def name="Header">
  <param name="user" type="object" required="true" />
  <span>{props.user.name}</span>
</component_def>

Running npx euix doctor App.xml loads both files, links App → Header, and validates that user is passed and matches type="object".


🏷️ Prop Contracts (<param>) #

Declare prop types inside <component_def>:

XML
<component_def name="CounterBadge">
  <param name="count" type="number" required="true" />
  <param name="label" type="string" default="Count" />
  <param name="priority" type="string" enum="Low,Normal,High" />
  <span>{props.label}: {props.count}</span>
</component_def>

Supported type values (aligned with EUIX runtime coercion): string, number, boolean, object, array.

Doctor checks:

RuleWhen it fires
EUIX1402Required prop omitted by parent
EUIX1403Passed value type does not match declared type, or literal outside enum

Type inference (static):

  • Literal attributes: count="42" → number, active="true" → boolean
  • State bindings: user="{data.user}" → uses parent <state type="...">
  • Complex expressions like {user.name} are skipped (confidence: inferred) to avoid false positives

Example mismatch — Doctor reports EUIX1403:

XML
<!-- Parent: user state is string -->
<state id="user" type="string">Guest</state>
<Header user="{data.user}" />

<!-- Child expects object -->
<param name="user" type="object" required="true" />

🩺 Diagnostic Reference #

RuleSeverityMeaning
EUIX1001errorAction writes to unknown state
EUIX1002warningState never read or written
EUIX1101errorComputed dependency cycle
EUIX1102errorComputed depends on unknown symbol
EUIX1201warningWatcher reactive loop
EUIX1301errorCustom action not defined (import shared <action_def> modules when needed)
EUIX1302errorPlugin action used without plugin markup/import (e.g. STREAM_SEND without <api_stream>)
EUIX1401errorComponent reference could not be resolved
EUIX1402errorMissing required prop on child component
EUIX1403errorProp type or enum mismatch (inferred)
EUIX1501warningAPI call may leave loading state stuck

Treat all error diagnostics as blocking before merge or release.

Engine & plugin actions vs <action_def> #

Doctor recognizes core engine actions, plugin actions, and attribute shorthands — no empty <action_def> stubs required.

StyleExample
Core declarative<on_click action="SET_STATE">, <watch action="RUN_SCRIPT">
Plugin declarative<on_click action="VALIDATE_FORM">, STREAM_SEND, NAVIGATE, SET_DATE_LOCALE, CHART_UPDATE, XHR
Attribute shorthandon_click:set, :toggle, :mutate, :revalidate, :run, :emit, :focus, :undo, :redo, :snapshot, :retry
Custom shorthandon_click:call="MyWorkflow", on_submit:call="SubmitForm" → needs <action_def name="MyWorkflow">
Callback attrs<on_click action="VALIDATE_FORM" on_success="AfterValidate"> → AfterValidate must exist
Inline aliason_error="SET_STATE:hasError=true" on <error_boundary>

Do not add fake stubs like <action_def name="SET_STATE" /> — only define real custom workflows.

Plugin-scoped actions: Doctor detects active plugins from markup tags (<api_stream>, <date_config>, <chart>, …) and JS imports (euixjs/api, …). Using STREAM_SEND without stream/api markup → EUIX1302.

Cross-file actions: <import src="./SharedActions.xml" /> loads shared <action_def> modules; custom actions resolve project-wide after import expansion.

Dynamic handlers: on_click:action="{data.handlerName}" → info + confidence: inferred (not a blocking error).

Still not statically resolved: runtime engine.registerAction(...) and handlers that depend on plugins not signaled in the scanned files.


🖥️ VS Code / Cursor Extension #

The EUIX Doctor extension (euix-doctor) surfaces the same rules in the Problems panel:

  • Analyze on save (debounced)
  • Status bar error/warning count
  • Commands: Analyze Workspace, Analyze Current File, Run Safe Test Scenarios
BASH
npm run vscode-doctor:build
npm run vscode-doctor:package   # produces .vsix

Open packages/vscode-euix-doctor and press F5 for Extension Development Host debugging.


  1. Edit EUIX XML/HTML/JS templates.
  2. Run Doctor on changed paths: npx euix doctor <path>.
  3. Fix errors — especially EUIX1001, EUIX1101, EUIX1301, EUIX1401–EUIX1403.
  4. Review warnings — watcher loops, API error paths, unused state.
  5. Optional: npx euix doctor <path> --test for dry-run flow scenarios.
  6. Runtime tests — Vitest unit tests and Playwright E2E after Doctor passes.

⚠️ Limitations #

Doctor performs static analysis only:

  • Does not execute real plugin runtime hooks or network calls
  • Complex JS in action bodies may be partially inferred
  • Dynamic component URLs in template literals may be unresolved
  • Prop type checks on complex expressions are intentionally conservative

These limits keep Doctor fast and CI-friendly as a first line of defense.


🧭 Next Step #

Continue with runtime debugging in Debugging & DevTools, or add automated tests in Testing & Playwright.

5 min read · Created · Updated