This roadmap tracks Vuer from its current working scaffold toward a first
public, production-grade release (v1.0.0) and beyond. It follows the
architecture, goals, and constraints set out in
README.md, AGENT.md, and the docs/ reference.
Vuer is a security-focused, AST-based static analyser for Vue.js Single
File Components (.vue), written in Rust. It is not an ESLint plugin: it
parses each .vue file with its own template parser and oxc_parser for the
script block, then runs every enabled rule against the resulting AST.
Nothing here is claimed to ship until it is verified by tests and, where
relevant, by CI on every tier (see Phase 9 and below). The bar is borrowed
from mature SAST tools (zizmor, Ruff, Semgrep, CodeQL): low false
positives, low false negatives, actionable remediation, and stable machine
output (SARIF) for CI.
Vuer's terminal state is a self-contained, dependency-light, cross-platform static analysis engine and editor-tooling layer for Vue projects, trusted in CI and in-editor, with these properties:
- Accurate by construction. A real Vue template/style parser (not a
hand-rolled scanner for block boundaries only), plus a taint/control-flow
aware script analyser on top of
oxc, so findings are structural, not textual. Zerounwrap()/panic!()in production code (already true). - Rule catalogue that covers the declared categories. Today only
securityandbest-practicehave rules;performance,accessibility, andarchitectureare declared inCategoryand the CLI filter but empty. These must be populated, or the empty categories removed with a documented rationale (Phase 4). - Automation that earns trust. Autofix for safe, unambiguous findings
(
v-html→v-text, missing:key, etc.), always behind an explicit--fix, never silent. - First-class CI and editor integrations. SARIF (done) plus a published GitHub Action, pre-commit hook docs, and an LSP server driving inline diagnostics in VS Code / Neovim / JetBrains.
- Reproducible, audited, well-governed. Reproducible builds, a clean
dependency tree (
cargo-audit/cargo-denygreen), fuzz targets for the parser and the rule engine, and a release pipeline that publishes binaries- checksums for Linux/macOS/Windows.
- Performance budgeted, not just fast. A criterion benchmark harness and a CI gate enforcing startup, per-file, and binary-size budgets.
The phases below are ordered so that each one leaves the tree in a buildable, tested state. "Done" means the items are implemented and covered by the CI gates in Phase 9.
- Cargo package
vuer(edition 2024, MSRV pinned to 1.97.0 to matchoxc0.136's requirement; seeCargo.tomlheader comment) - CLI via
clap(derive): paths,--rules,--format,--list,--deny-warnings,--no-ignores,--no-config,--category,--min-severity - Diagnostic stack:
miette(fancy) +thiserror, rustc-styleerror[rule-id]output viaannotate-snippets - SFC extraction: native block-boundary scanner splitting
<template>/<script>/<style>with byte-accurate offsets (src/parser/mod.rs) - Native recursive-descent template parser producing
TemplateRoot(src/parser/template/) - Script parsing via
oxc_parser+ arena (src/parser/script.rs) - Rule trait +
RuleRegistry(src/rules/mod.rs); 15 rules acrosssecurityandbest-practice(26 since Phase 3) - Output formats: pretty, JSON, minimal, SARIF 2.1.0
(
src/report/) - Inline suppression:
vuer-ignore[...]/vuer: ignore[...]with--no-ignoresoverride (src/suppression.rs) - Config discovery:
.vuerc.yml/vuer.yml, strict unknown-key handling, CLI layers on top (src/config.rs) - Parallel scan:
ignorewalker +rayonper-file fan-out (src/scanner.rs) - CI:
fmt --check,clippy -D warnings,cargo test --all-featureson ubuntu + macOS (.github/workflows/ci.yml) - Docs: README, AGENT.md,
docs/installation.md,docs/usage.md,docs/audits.md(per-rule reference)
The template parser is currently a hand-rolled recursive-descent parser. It must be proven correct against real-world Vue before we trust it for security findings, because a parser that silently mis-reads a node hides findings (false negatives) or fabricates them (false positives).
- Template parser conformance suite. A fixture corpus of real Vue
components (Vue 3 docs examples, Nuxt UI, Element Plus, PrimeVue
snippets — vendored under
tests/fixtures/templates/, MIT/Apache compatible) that must parse without panic and produce aTemplateRootwhose element/attribute tree matches an expected structural snapshot. Implemented with an original corpus modelled on Vue 3 patterns (login form, product card, data table, SVG, modal, nav menu, settings form); each fixture must parse with zero errors and matches a committed insta snapshot (tests/conformance.rs). - Edge cases enumerated and tested:
<template>with multiple root nodes (Vue 3 fragments),<slot>/v-slot,v-binddynamic argument (v-bind:[key]),v-onmodifiers, self-closing custom elements,<component :is>and<Teleport>/<Transition>/<Suspense>, interpolation with filters removed in Vue 3, whitespace control (v-pre,v-once,v-cloak), HTML entities, comments, and CDATA in<svg>/<math>foreign content. Implemented as a dedicated suite (tests/edge_cases.rs) plus an adversarial corpus that must terminate without panicking. The hardening exposed and fixed real bugs: infinite loops on stray closing tags, CDATA hanging the text lexer,v-presubtrees parsed as interpolation, mismatched closing tags silently accepted, spans that included}}/], and a block extractor that truncated the template at a nested<template v-if>element. -
TemplateErrorsurfaced, not swallowed. Non-fatal parse errors are already collected inScanContext::template_errors; rules and the CLI summary must report them (count malformed files, warn the user) so a parse failure degrades to "this file needs review" instead of "this file is clean." Implemented:Scannerreturns aScanReportwithParseIssues; the CLI prints per-error warnings (file, byte offset, message) on stderr and a summary line;--deny-warningsfails on malformed files. - No
unwrap()/panic!()in the parser outside#[cfg(test)]. Malformed input is a typedTemplateError, never a crash. Verify with agrep/lint gate and an explicit fuzz seed corpus (Phase 8). Implemented: CI step grepssrc/parser/and fails on any production match; the adversarial corpus intests/edge_cases.rsis the seed list for the Phase 8 fuzz targets. - Offset integrity test. For every parsed node, the reported span
resolves to the exact source bytes in the original
.vuefile, not the trimmed block. Add a property test that re-slices the source by the reported span and asserts it equals the node's text. Implemented:tests/offset_integrity.rswalks every node over the conformance corpus, a canonical corpus, and a generated corpus at two base offsets, asserting slice == node text; also a rule-level spot check that diagnostics land on the AST spans. -
oxcupgrade discipline. Bumpingoxcre-checks the MSRV pin inCargo.tomland reviews theoxc_*breaking changes. Documented as part of the release checklist (Phase 10/11). Implemented:docs/upgrading.mdis the bump checklist (MSRV re-pin, version-cohort coherence, full gate + snapshot review, changelog note). - Style block handling.
BlockKind::Styleis currently extracted but unused. Decide scope: at minimum, emit a structural warning for risky patterns (e.g.expression(...)in scoped styles is a non-issue, butv-html-like CSS injection via:deep()dynamic values deserves a documented "out of scope" note rather than silent ignoring). Make the extractor'sStylearm either used or explicitlyallow(dead_code)with a rationale comment (already half-done). Implemented: the extractor collects every<style>block intoScanContext::style_blocks(the arm is used); CSS analysis including:deep()injection is documented out of scope for v1 in the README's "Scope:<style>blocks" section.
All current rules are syntactic: they match a directive, a call name, or an attribute and flag it. A real SAST tool reasons about data flow.
-
Taint tracking for the script block (Phase 2 core). Build a lightweight taint analysis on top of the
oxcAST: - Sources: route params (useRoute().params,$route.query), props (defineProps),ref()/reactive()seeded from external input,fetch/axiosresponses,localStorage.getItem,window.location,eventpayloads. - Sinks:v-html-bound expressions,innerHTML,document.write,eval/dynamicFunction,locationwrites,postMessage,window.open, dynamic:src/:hrefbindings,dangerouslySetInnerHTML(when Vue is used with React-style renderers). - Propagators: string concat, template literals,.map/.filterover tainted arrays, Vuecomputed/refassignment. - Sanitizers: calls matchingDOMPurify.sanitize,escapeHtml, framework-safe interpolation. A tainted value that passes through a recognized sanitizer is downgraded (and the sanitizer call is reported as the "why" so the user can verify it). - This upgradesno-v-html,no-inner-html,no-dangerous-url,no-dynamic-bind-src,no-open-redirectfrom "this pattern exists" to "this pattern carries untrusted data," dramatically cutting false positives while keeping zero false negatives on the unsafe path. -
Inter-procedural awareness (bounded). Within a single
<script>block, follow taint through local function calls and componentemit/expose. Cross-file analysis (imports, mixins, composables) is explicitly deferred to Phase 6 with a documented scope boundary. -
Re-classify existing rules under taint. Each script rule gains a
TaintKind(source/sink/flow) and the rule engine reports flow paths in the diagnostichelp, e.g. "taint fromuseRoute().query.idreachesv-htmlat line 12." This is what makes Vuer's output actionable rather than alarming. -
Determinism guarantee. Taint results are order-independent and stable across runs (the engine already forbids global mutable state). Property test: scanning the same file N times yields byte-identical JSON.
-
Ruletrait extension without breaking callers. Add an optionalfn kind(&self) -> RuleKindand aflow_pathsaccessor on the diagnostic; old rules default toSyntactic.scanner.rsand the report layer consume the new fields only when present.Implemented as `src/taint/` (see its module docs for the full model and documented boundaries). Sources, propagators, and sanitizers are implemented per the list; `no-dangerous-url` is intentionally kept syntactic because the dangerous pattern there *is* the literal scheme (documented in `docs/audits.md`). Sinks are detected by the (now taint-gated) rules; the engine exposes `status_at`/`flow_at` per expression span. Implemented: local function calls propagate taint when a tainted argument reaches a parameter the function's return depends on, or when the body returns a tainted closure value (recursion guarded). `emit`/`expose` payloads and cross-file imports are documented out of scope (Phase 6). Implemented: taint-gated rules report `= note: taint from <source> reaches <sink> via <ids>` in pretty output and structured `flow` arrays in JSON; `RuleKind::Taint` marks the re-classified rules. Implemented: `tests/determinism.rs` (byte-identical JSON/SARIF across repeated binary runs over the fixture corpus) plus a per-run unit test asserting identical span facts. Implemented: `Rule::kind()` defaults to `Syntactic`; `Rule::check` returns `Vec<Finding>` (diagnostic + optional `Vec<FlowPath>`); non-taint rules return `flow: None` and the report layers skip it.
Category already declares Performance, Accessibility, and
Architecture, and the CLI --category filter already accepts them — but no
rule implements them. Either populate them or remove them with a rationale.
-
performancerules:no-v-if-with-v-for— Vue 3 forbids usingv-ifandv-foron the same element; flag and suggest computed filtering.no-deep-watch-without-handler—watch(src, cb, { deep: true })without an explicit handler object / without{ once }where applicable.no-reactive-in-v-for— reactive object creation insidev-forbodies (loop statements and array-iteration callbacks).no-large-list-without-virtualization— heuristic:v-forover a variable whose name implies a large/remote collection without a known virtual scroll wrapper (low-severity, best-effort, documented as heuristic). Implemented asvue/performance/*(4 rules) with unit + integration coverage; the large-list name list is curated (generic names likeitemsare not flagged) and the heuristic is documented indocs/audits.md.
-
accessibilityrules:no-img-without-alt—<img>withoutalt(template walk).no-click-without-role-keyboard—@clickon a non-interactive element withoutrole+@keydown/keyboard handler.no-form-without-label— input/select/textarea without an associated<label>oraria-label.no-button-without-type—<button>without explicittype(defaults tosubmit). Implemented asvue/accessibility/*(4 rules);no-form-without-labelresolves<label for>associations and wrapping<label>s within the template, and bound/unprovable attribute forms are accepted to keep the false-positive rate low.
-
architecturerules (conservative):no-side-effect-in-computed— assignments / async /watch-like side effects insidecomputed(() => ...).no-mutation-of-props— writing to adefinePropsdestructured value orprops.x = ....no-async-setup-without-error-boundary—async setup()without a sibling<Suspense>(heuristic, low-severity). Implemented asvue/architecture/*(3 rules) with documented scope boundaries per rule (Options APIcomputed:/this.xforms deferred; nested function bodies in getters not descended into).
- Decision gate:
Met with all three categories populated at a meaningful rule count
(4 + 4 + 3) and per-rule low-false-positive boundaries documented in
docs/audits.md; the CLI--categoryfilter is covered by integration tests for each new category.
Findings that have one unambiguous safe rewrite should be fixable, never
silently. Everything is behind --fix; --dry-run prints the diff and exits
0.
- Safe, single-rewrite fixes:
no-v-html→v-text(only when the binding is plain text; refuse if it contains HTML tags, and say so).v-for-missing-key→ insert:key="item.id"using the item identifier heuristically (refuse if no obvious key; report instead).no-button-without-type→type="button".no-inline-style→ move the style to aclassstub (best-effort, opt-in only).
- Fix application model. Fixes are computed against absolute byte
spans and applied with non-overlapping interval merging; a fix that
would overlap another finding is skipped (never silently truncates the
file).
--fixwrites only when every fix is conflict-free. -
--fixrespects suppression and config. Ignored findings are not auto-fixed. A dry run shows what would change. - Tests. Snapshot tests per fixable rule: input → fixed output → re-scan of the fixed file yields zero findings for that rule.
CI output is necessary but not sufficient; developers want findings in-editor.
- LSP server (
vuer lsp).tower-lspbased:textDocument/diagnostic(pull model),hovershowing the rule's help text,codeActionfor the autofixes from Phase 4. One binary, subcommand-gated, no extra deps in the default build unless feature-flagged. - VS Code extension (separate repo, thin). Talks to the
vuerLSP binary; ships the binary path resolution and avuer.pathsetting. Keep the extension minimal; all logic stays in the Rust binary. - Neovim / JetBrains docs. Document wiring
vuer lspintonvim-lspconfigand the JetBrains LSP-over-stdio path, plus a null-ls/diagnostic-langsrv pattern for the interim. - Pre-commit hook. A
hooks:snippet for.pre-commit-config.yamlinvokingvuer --format minimal --deny-warnings(fails the commit on high/critical by default, configurable). - Published GitHub Action.
vuer/action(or auses:shim in this repo) that installs the release binary and runsvueragainst a path, uploading SARIF to Code Scanning. Reuses the release artifacts from Phase 11.
Phase 2 was intra-file. Real Vue apps spread risk across files.
- Composable/import resolution. Follow
import/exportto resolve taint through local composables and mixins within the scanned root. Bounded to the files under the scan path (never reads outside it). -
defineProps/defineEmitsschema. Propagate prop types so a tainted prop at a call site is traced to itsdefinePropsorigin. - Multi-file cache. Reuse parsed
oxcASTs across files in one scan so large monorepos do not re-parse shared modules (rayon already fans out per file; add an arena/parse cache keyed by canonical path). - Scope boundary (documented). Cross-repo, npm-dependency internals, and Vue compiler transform output are out of scope; findings stop at the project boundary. State this in README's "Accuracy" section.
- Rule severity override in config.
.vuerc.ymlgainsseverity: { "no-v-html": critical }andinherit: pathfor cascading config through a monorepo (walk-up discovery already exists; extend it to merge rather than first-match). - Baseline / triage mode.
vuer baseline --write baseline.jsonrecords current findings; subsequent runs can--diff-against baseline.jsonto only report new findings (supports "fail CI on new issues, not the whole backlog"). - Ignore paths in config.
ignore: [ "**/*.stories.vue", "node_modules" ]layered with.gitignore(theignorecrate already handles gitignore; add explicit user excludes). - Exit-code contract (documented & tested). 0 = clean, 1 = findings
under
--deny-warnings/ scan error, 2 = usage/input error, 3 = internal/engine error. Currentlymain.rsuses 0/1 inconsistently for input vs internal; clarify and cover withassert_cmdtests. - Stable JSON schema. Lock the
JsonViolationshape behind aschema_versionfield so downstream consumers can detect drift; add a JSON Schema file underdocs/.
-
unsafeaudit. Inventory everyunsafe(currently none in production paths;regex/oxcmay pull some transitively — document each, justify, isolate). Zero unjustifiedunsafe. - Fuzz targets (
cargo-fuzz):- template parser — feed arbitrary bytes; must never panic, only produce
TemplateError. - script/oxc wrapper — malformed/weird JS/TS; must not crash the engine.
- rule engine — synthetic
ScanContexts with adversarial ASTs. - config parser — malformed YAML; must error, never panic.
- template parser — feed arbitrary bytes; must never panic, only produce
- Property-based tests (
proptest/rstest): offset integrity (Phase 1), determinism (Phase 2), suppression idempotence, config merge laws. -
cargo-audit+cargo-denyin CI. License allowlist (MIT/Apache/ compatible), advisory gate, bans on known-bad crates. Add as a CI job next toclippy. - Reproducible build check. Same input tree → byte-identical release
binary (verify
SOURCE_DATE_EPOCH/ stripped paths;oxcmay embed paths — audit and neutralize). - Least-privilege CI. The existing
ci.ymlalready pinspermissions: contents: readand uses pinned action SHAs — keep this discipline and addzizmorself-scanning of the workflow as a job.
- Criterion harness. Benchmarks for: cold startup, per-file parse + rule time on a fixture corpus, and full-repo scan time vs. LOC.
- Budgets enforced in CI:
- startup
<50ms(cold, no file scanned), - per-file
<2msmedian on a representative corpus (excluding first-run parse warmup), - memory
<100MBfor a 10k-file monorepo scan, - binary
<15MBrelease (stripped, single static-ish artifact).
- startup
- Profiling passes.
cargo flamegraph/perfon a large Vue monorepo fixture; eliminate per-file allocations in the hot path (the engine already prefers borrowed&strand arena allocation — verify and extend). - CI gate. A
benchjob runs the criterion suite and fails if any budget regresses beyond a small tolerance (e.g. 10%), catching performance cliffs before release.
- Release workflow (
release.yml). Tag-triggered: build release binaries forx86_64-unknown-linux-gnu(musl too),x86_64-apple-darwin,aarch64-apple-darwin(Apple Silicon),x86_64-pc-windows-msvc, plusaarch64-unknown-linux-gnufor ARM servers. Upload to GitHub Releases withSHA256SUMS.txtand a signed checksum where the runner allows. - Windows CI. Add
windows-latestto theci.ymlmatrix (currently ubuntu + macOS only) — confirmsignore/rayon/path handling are platform-clean, since Windows path separators and.gitignoresemantics differ. -
cargo install+ crates.io. Verified publish path;Cargo.tomlmetadata (repository,homepage,keywords,categories) completed. Version follows SemVer; MSRV bump is a minor-or-major event per Phase 1'soxcdiscipline. - Homebrew / Scoop / arch AUR shims (optional). Community-maintained; the release artifacts are the source of truth.
- Changelog & versioning policy.
CHANGELOG.md(Keep a Changelog), and a documented rule-id stability promise: avue/...rule id is never reused or silently re-severed. Removing a rule is a major-version event announced in the changelog. Implemented at the v0.2.0 release:CHANGELOG.mdcovers 0.2.0 (taint engine, taint-gated rules, flow paths, determinism) with the 0.1.0 baseline, and records the rule-id stability promise in its header.
- Rule catalogue frozen & documented. Every shipped rule has a section
in
docs/audits.mdwith vulnerable/safe examples and remediation, plus a stability marker (stablefor v1 rules). - Golden corpus CI gate. A fixture set of known-good and known-bad components; the suite fails if a rule's behavior changes (catches accidental false-negative regressions — the SAST equivalent of MenSung's "zero false negative" gate).
- Docs complete. Installation (crates.io, binaries, editor, CI, pre-commit), Usage (every flag, format, suppression), Audits (per rule), and an Architecture page describing the parser → AST → rule → report pipeline and the taint model from Phase 2.
-
v1.0.0tag +release.ymlrun publishes Linux/macOS/Windows binaries,SHA256SUMS.txt, and the GitHub Action reference. - Governance docs.
CODE_OF_CONDUCT.md,SECURITY.md(how to report a false positive / missed finding),CONTRIBUTING.md(rule authoring guide referencing AGENT.md),LICENSE(MIT, already set). - False-positive triage channel. A documented issue template ("false positive" / "missed vulnerability") so the accuracy floor (Phase 1/2) is community-maintained post-release.
- Cross-file taint across npm dependencies (after Phase 6) via an optional, offline type-stub index — explicitly opt-in, never default.
- TypeScript-aware narrowing. Use
oxc's TS type info to reduce false positives (e.g. "this prop isstringfrom a trusted config, not user input"). - SARIF 2.1.0 advanced features.
codeFlowsfor taint paths,relatedLocationsfor sanitizer calls,baselineintegration. - Watch mode (
vuer watch). Re-scan changed files vianotify, emitting incremental diagnostics for editor/live use. - HTML report. A standalone
vuer report --htmlfor PR comments and dashboards (distinct from SARIF, which stays machine-only). - Plugin rule API. A stable
#[vuer_rule]macro + dynamic loading story (carefully scoped — security rules must remain auditable; external plugins are a major-version consideration, not v1). - Additional frameworks' template dialects (e.g. Vue 2 legacy, or Petite-Vue) behind feature flags, only if the accuracy bar from Phase 1 can be met for each.
- Localization of diagnostics for non-English-speaking teams, kept behind config so the default stays English and machine output (JSON/ SARIF) is locale-invariant.
| Current file | Roadmap phase |
|---|---|
src/parser/template/ |
Phase 1 (conformance), Phase 2 (taint sources in template) |
src/parser/script.rs (oxc) |
Phase 2 (taint on script AST), Phase 6 (imports) |
src/rules/ (26 rules) |
Phase 2 (taint upgrade), Phase 3 (new categories, done), Phase 4 (fixes) |
src/report/sarif.rs |
Phase 5 (LSP hover), Future (codeFlows) |
src/config.rs |
Phase 7 (overrides, baseline, ignore) |
src/scanner.rs |
Phase 6 (parse cache), Phase 7 (exit codes), Phase 9 (budgets) |
.github/workflows/ci.yml |
Phase 8 (audit/deny/fuzz), Phase 9 (bench), Phase 10 (Windows) |
docs/audits.md |
Phase 3/11 (per-category docs) |
AGENT.md |
Phase 11 (CONTRIBUTING rule-authoring reference) |