Make the a11y linter aware of child templates - #573
Conversation
🦋 Changeset detectedLatest commit: 159a225 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe HTML extractor now records conditional, dynamic, body-slot, root, uncertainty, and inline-region metadata while resolving child templates. The language server caches extraction results, maps accessibility violations to inline usage sites, and applies context-based rule exceptions. New fixtures cover child templates, conditional content, full documents, and parent contexts. Axe rule documentation is updated to version 4.12, a new ARIA rule is cataloged, and release, spellcheck, and bug-tracking files are updated. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@agent-feedback/bugs.md`:
- Around line 8-11: Record both suspected-bug findings from the diff in
agent-feedback/README.md, including their affected symbols, reproduction
examples, and proposed fix direction. Preserve the existing entries in
agent-feedback/bugs.md and do not implement the extractor or validation changes
in this task.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 423a3bad-589c-4343-8d90-5f183d425c87
⛔ Files ignored due to path filters (81)
packages/language-server/src/__tests__/fixtures/html/child-template-context/__snapshots__/child-template-context.expected/components/fancy-item.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-context/__snapshots__/child-template-context.expected/components/fancy-item.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-context/__snapshots__/child-template-context.expected/components/fancy-item.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-context/__snapshots__/child-template-context.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-context/__snapshots__/child-template-context.expected/index.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-context/__snapshots__/child-template-context.expected/index.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-link-name/__snapshots__/child-template-link-name.expected/components/icon-arrow.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-link-name/__snapshots__/child-template-link-name.expected/components/icon-arrow.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-link-name/__snapshots__/child-template-link-name.expected/components/icon-arrow.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-link-name/__snapshots__/child-template-link-name.expected/components/text-label.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-link-name/__snapshots__/child-template-link-name.expected/components/text-label.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-link-name/__snapshots__/child-template-link-name.expected/components/text-label.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-link-name/__snapshots__/child-template-link-name.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-link-name/__snapshots__/child-template-link-name.expected/index.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-link-name/__snapshots__/child-template-link-name.expected/index.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-recursive/__snapshots__/child-template-recursive.expected/components/tree-item.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-recursive/__snapshots__/child-template-recursive.expected/components/tree-item.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-recursive/__snapshots__/child-template-recursive.expected/components/tree-item.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-recursive/__snapshots__/child-template-recursive.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-recursive/__snapshots__/child-template-recursive.expected/index.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-recursive/__snapshots__/child-template-recursive.expected/index.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-slot/__snapshots__/child-template-slot.expected/components/my-card.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-slot/__snapshots__/child-template-slot.expected/components/my-card.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-slot/__snapshots__/child-template-slot.expected/components/my-card.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-slot/__snapshots__/child-template-slot.expected/components/my-list.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-slot/__snapshots__/child-template-slot.expected/components/my-list.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-slot/__snapshots__/child-template-slot.expected/components/my-list.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-slot/__snapshots__/child-template-slot.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-slot/__snapshots__/child-template-slot.expected/index.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-slot/__snapshots__/child-template-slot.expected/index.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-uncertain/__snapshots__/child-template-uncertain.expected/components/plain-box.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-uncertain/__snapshots__/child-template-uncertain.expected/components/plain-box.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-uncertain/__snapshots__/child-template-uncertain.expected/components/plain-box.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-uncertain/__snapshots__/child-template-uncertain.expected/components/raw-html.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-uncertain/__snapshots__/child-template-uncertain.expected/components/raw-html.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-uncertain/__snapshots__/child-template-uncertain.expected/components/raw-html.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-uncertain/__snapshots__/child-template-uncertain.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-uncertain/__snapshots__/child-template-uncertain.expected/index.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/child-template-uncertain/__snapshots__/child-template-uncertain.expected/index.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/control-flow-conditional/__snapshots__/control-flow-conditional.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/control-flow-conditional/__snapshots__/control-flow-conditional.expected/index.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/control-flow-conditional/__snapshots__/control-flow-conditional.expected/index.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/dlitem-context/__snapshots__/dlitem-context.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/dlitem-context/__snapshots__/dlitem-context.expected/index.mdis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/html/dlitem-context/__snapshots__/dlitem-context.expected/index.tsis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-class-id-shorthands/__snapshots__/attr-class-id-shorthands.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-class-shorthand-merge/__snapshots__/attr-class-shorthand-merge.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-tags-dynamic-for-typed/__snapshots__/attr-tags-dynamic-for-typed.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-tags-dynamic-for-typed/__snapshots__/attr-tags-dynamic-for-typed.expected/tags/my-menu/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-tags-dynamic-for/__snapshots__/attr-tags-dynamic-for.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-tags-dynamic-if/__snapshots__/attr-tags-dynamic-if.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-tags-dynamic-while/__snapshots__/attr-tags-dynamic-while.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-tags-dynamic-with-scriptlets/__snapshots__/attr-tags-dynamic-with-scriptlets.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-tags-for-narrowing/__snapshots__/attr-tags-for-narrowing.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-tags-generic/__snapshots__/attr-tags-generic.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-dynamic-for-typed/__snapshots__/attr-tags-nested-dynamic-for-typed.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-dynamic-for-typed/__snapshots__/attr-tags-nested-dynamic-for-typed.expected/tags/my-layout/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/__snapshots__/attr-tags-nested-type.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/attr-tags-static-dynamic-renderbody/__snapshots__/attr-tags-static-dynamic-renderbody.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/bound-attr-mut-ident/__snapshots__/bound-attr-mut-ident.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/class-api-basic/__snapshots__/class-api-basic.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/custom-tag-args/__snapshots__/custom-tag-args.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/el-ref-hoist/__snapshots__/el-ref-hoist.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/for-tag-attr-tags/__snapshots__/for-tag-attr-tags.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/for-tag-input/__snapshots__/for-tag-input.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/for-tag/__snapshots__/for-tag.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/if-tag/__snapshots__/if-tag.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/prefer-local-identifier-tag-name/__snapshots__/prefer-local-identifier-tag-name.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/recursive-define-tag/__snapshots__/recursive-define-tag.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/render-body-basic/__snapshots__/render-body-basic.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/return-tag-basic/__snapshots__/return-tag-basic.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/return-tag-nested/__snapshots__/return-tag-nested.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/simple-hoist/__snapshots__/simple-hoist.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/tag-param-mutation/__snapshots__/tag-param-mutation.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/tag-var-hoisting-from-control-flow/__snapshots__/tag-var-hoisting-from-control-flow.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/tag-var-mutation-patterns/__snapshots__/tag-var-mutation-patterns.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/tag-var-mutation-shadowing/__snapshots__/tag-var-mutation-shadowing.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/tag-var-mutations-nested/__snapshots__/tag-var-mutations-nested.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/tag-var-mutations/__snapshots__/tag-var-mutations.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/tags-api-basic/__snapshots__/tags-api-basic.expected/index.htmlis excluded by!**/__snapshots__/**and included by**packages/language-server/src/__tests__/fixtures/script/while-tag/__snapshots__/while-tag.expected/index.htmlis excluded by!**/__snapshots__/**and included by**
📒 Files selected for processing (22)
.changeset/spotty-moons-repeat.mdagent-feedback/bugs.mdcspell.jsonpackages/language-server/src/__tests__/fixtures/html/child-template-context/components/fancy-item.markopackages/language-server/src/__tests__/fixtures/html/child-template-context/index.markopackages/language-server/src/__tests__/fixtures/html/child-template-link-name/components/icon-arrow.markopackages/language-server/src/__tests__/fixtures/html/child-template-link-name/components/text-label.markopackages/language-server/src/__tests__/fixtures/html/child-template-link-name/index.markopackages/language-server/src/__tests__/fixtures/html/child-template-recursive/components/tree-item.markopackages/language-server/src/__tests__/fixtures/html/child-template-recursive/index.markopackages/language-server/src/__tests__/fixtures/html/child-template-slot/components/my-card.markopackages/language-server/src/__tests__/fixtures/html/child-template-slot/components/my-list.markopackages/language-server/src/__tests__/fixtures/html/child-template-slot/index.markopackages/language-server/src/__tests__/fixtures/html/child-template-uncertain/components/plain-box.markopackages/language-server/src/__tests__/fixtures/html/child-template-uncertain/components/raw-html.markopackages/language-server/src/__tests__/fixtures/html/child-template-uncertain/index.markopackages/language-server/src/__tests__/fixtures/html/control-flow-conditional/index.markopackages/language-server/src/__tests__/fixtures/html/dlitem-context/index.markopackages/language-server/src/service/html/axe-rules/rule-exceptions.tspackages/language-server/src/service/html/index.tspackages/language-tools/src/extractors/html/index.tspackages/language-tools/src/util/extractor.ts
|
|
||
| - `packages/language-tools/src/extractors/html/index.ts` › `HTMLExtractor.#visitNode` | 2026-07-22 | impact:med | effort:med — Dynamic _attribute_ values on descendants do not propagate into an ancestor's `hasDynamicBody`, so `unknownBody` rule exceptions can miss. Example: `<div role="list"><div role=input.x></div></div>` extracts the child as `role="dynamic"` (an invalid role) while the outer element's `hasDynamicBody` stays false, so axe's `aria-required-children` reports a violation even though the runtime role could be `listitem` — a false positive that predates child-template inlining. `#visitNode` returns `isDynamic || hasDynamicBody` and deliberately ignores `hasDynamicAttrs`; a fix could propagate a separate "semantics-affecting dynamic attr" flag (role/aria-\*/tabindex only, to avoid over-suppressing on eg dynamic `class`) into ancestors. Re-verify: `<div role="list"><div role=input.x></div></div>` in an html fixture produces an `aria-required-children` diagnostic. | ||
|
|
||
| - `packages/language-server/src/service/html/index.ts` › `doValidate` | 2026-07-22 | impact:low | effort:med — The `conditionalContent` exception only suppresses duplicate-detection rules (accesskeys, landmark-unique, identical-links-same-purpose, ...) when the _violating_ element is inside a control flow branch. When the violating element is unconditional but its duplicate counterpart is conditional (eg a static `<label>` plus a second `<label>` inside `<if>` for `form-field-multiple-labels`), the false positive remains. axe exposes the counterparts via each check's `relatedNodes`; suppressing when any related node maps to an `inConditional` element would close the gap. Re-verify: fixture with `<main>` plus `<if=x><main></if>` — the unconditional `<main>` may still be reported by `landmark-no-duplicate-main`. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Record these actionable findings in agent-feedback/README.md.
These suspected-bug records are currently added to agent-feedback/bugs.md, but the repository instruction requires actionable out-of-scope findings to be recorded in agent-feedback/README.md.
As per coding guidelines: “Record actionable out-of-scope findings in agent-feedback/README.md before finishing; do not silently omit them or fix them in an unrelated change.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@agent-feedback/bugs.md` around lines 8 - 11, Record both suspected-bug
findings from the diff in agent-feedback/README.md, including their affected
symbols, reproduction examples, and proposed fix direction. Preserve the
existing entries in agent-feedback/bugs.md and do not implement the extractor or
validation changes in this task.
Source: Coding guidelines
The axe-core HTML plugin previously replaced every custom tag with a
bare <div> wrapper, so it could never see what a child template renders
and any component usage suppressed most content rules on its ancestors
(via the unknownBody exception). Extraction now inlines the extracted
skeleton of resolvable child templates at the usage site, guarded so a
diagnostic is only ever produced when it is provably true for rendered
output:
- Child skeletons are resolved through the taglib lookup, cached per
template and project version, and bounded by cycle detection, a depth
limit, and a byte budget; anything unresolvable falls back to the old
<div> behavior.
- A template's default body slot (`<${input.renderBody}/>`) is detected
so usage-site body content is spliced into the child's true position;
templates without a single known slot keep the previous behavior.
- Any uncertainty inside a child (unescaped placeholders, unresolvable
tags, dynamic body content) propagates to the usage site so existing
unknownBody suppression still applies.
- Diagnostics on a child skeleton's top-level elements are re-anchored
to the tag name at the usage site; deeper elements are left to the
child template's own validation.
- Control flow tags (if/else/for/while) emit their body directly
instead of a fabricated <div>, so structural rules see real
parent/child relationships.
- listitem and dlitem are un-blacklisted, gated by a new
requiresKnownParent exception that mirrors exactly the ancestor chain
axe consults (including dlitem's presentational-div lookthrough), and
never fires for elements whose rendered parent is unknown.
- Duplicate-detection rules (accesskeys, landmark-unique, ...) are
suppressed for elements in control flow branches, removing a class of
pre-existing false positives from mutually exclusive branches.
No diagnostics changed on the existing fixture corpus; new fixtures
cover inlined context, slot splicing, uncertainty suppression,
recursion, control flow, and dlitem gating.
The child-template-uncertain fixture's only diagnostic was an accidental TypeScript error (raw-html required an input the fixture never passed), so nothing in its snapshots demonstrated accessibility behavior. The input is now optional, and the fixture gained a contrasting certain child (plain-box rendering a <div>) so the snapshot shows the `list` rule firing for the known child while the uncertain child stays suppressed.
A diagnostic re-anchored from an inlined child template's root element to the usage site read as if it were about the tag itself (eg "List item does not have a <ul>, <ol> parent element" pointing at <fancy-item/>). The message now leads with which element the tag renders, found during a self-audit of the child template inlining work.
Condense the JSDoc and inline commentary added with child template inlining, and drop the long-dead commented-out severity mapping.
Delete interface JSDoc and narration that restated the code; rename regionAt to innermostRegionAt so the selection rule is in the name.
The requiresKnownParent gates mirror axe-core 4.12's parent checks, so flag that for future axe upgrades, and strip attribute-unsafe characters from template basenames used in data-marko-node-id.
d38a10e to
14a0504
Compare
Regenerating axe-rules.ts against the installed axe-core 4.12.1 (the file was generated from 4.10) picks up the new aria-tab-name rule, whitelisted with the same exceptions as its sibling name rules. The extracted HTML is now exact enough for a stronger guarantee: when a file renders an authored <html> containing no dynamic attributes, placeholders, or unresolvable content, the extraction IS the rendered page. A new requiresFullDocument exception runs rules gated on that exactness, re-enabling all fifteen previously blacklisted page/ancestor-context rules (label, region, bypass, heading-order, nested-interactive, aria-required-parent, landmark placement, and friends) for fully static documents while suppressing them everywhere else. Extraction also now emits static shorthand ids/classes (dynamic ones become "dynamic" like other attribute values) and tracks whether any placeholders exist, both feeding the exactness check.
A single-quoted (or escape-containing) attribute value with an embedded double quote was copied verbatim between double quotes, garbling the generated attribute — which the new full-document exactness gate would then wrongly trust. Such values now extract as "dynamic", which both keeps the attribute well-formed and blocks the gate. Also drops two cspell words orphaned by earlier comment trims.
The language server was slicing skeleton strings at body-slot offsets and re-deriving extraction exactness by scanning generated HTML — both extraction-domain work. The extractor now assembles inline child templates itself (extractChildTemplate) and reports a single `exact` flag computed where dynamic values are written, which is also more precise than the string scan (an authored literal "dynamic" no longer blocks the full-document gate). The public extractHTML result shrinks to what hosts consume: extracted, nodeDetails, inlineRegions, exact.
- Collapse the uncertain/exact flag pair into a single three-state fidelity (exact | approximate | uncertain), removing a representable illegal state and the confusing dual-flag computation. - Group the body-slot metadata into an optional bodySlot object so the fields can't outlive the slot they describe. - Unify the server's "skeleton" vocabulary with language-tools' "child template", and rename requiresFullDocument to requiresExactDocument to match the gate it feeds. - Make isKnownElement a type guard (drops the parent! assertions), extract violation anchoring out of the doValidate callback, and invert the body-slot branch for a linear early return.
Name the extractHTML result (HTMLExtraction) instead of ReturnType indirection in the server, rename #writeDynamicTag to #visitNonHTMLTag to match its dispatch role, and restore the in-progress cache marker comment a fresh reader flagged as missing.
Child template resolution goes through the compiler's TaglibLookup, so directory conventions are the compiler's concern; converting one fixture from components/ to tags/ locks that in without growing the suite.
Validation results are now cached per document, keyed on the extraction's semantic content (generated HTML, node details, inline regions, fidelity); a cache hit re-maps stored generated-space anchors through the fresh extraction instead of re-running jsdom and axe. Edits inside scriptlets, event handlers, styles, or placeholder expressions hit this path, dropping revalidation from ~200ms to ~2ms on a 24-section benchmark page. Adds a BENCH-gated perf suite (pnpm --filter @marko/language-server run bench) that generates a synthetic component-heavy app, guards against benching a broken page, and measures cold/warm validation and extraction with and without inlining, plus a regression test proving cache hits re-map diagnostic ranges after extraction-neutral edits.
Profiling the bench page showed no pathological rule — cost is spread across axe's per-node work — but two lossless cuts: skip axe's CSSOM preload (no enabled rule reads CSS), and when the exact-document gate is closed, drop the fifteen requiresExactDocument rules from runOnly instead of running them and filtering every violation. Cold validation on the bench page drops ~10%; the remaining gap versus the legacy extraction is the added analysis surface itself (mostly inlined component roots, which are the point of the feature).
CPU profiling showed cold validation dominated not by axe's rule logic but by jsdom recomputing styles element-by-element for axe's visibility checks. Extracted documents carry no stylesheets, so an element's computed style is a pure function of the UA sheet and its ancestor chain of tags and attributes (jsdom implements inheritance and the UA sheet uses only tag/attr child combinators — verified against jsdom's computed-style helper and default stylesheet). getComputedStyle is now memoized per ancestor-chain key, excluding the style-inert data-marko-node-id so repeated component instances share entries. Also passes iframes:false since extracted frames never have content documents. Bench page cold validation drops ~200ms to ~156ms (legacy shape ~103ms to ~74ms); axe results verified identical on the corpus and on an inheritance/visibility torture page.
The accessibility linter now takes custom tags' child templates into account, catching issues that span a tag usage and the template it renders (and enabling the
listitem/dlitemrules) without reporting anything that isn't provably true for the rendered output.Generated by Claude Code