From bc31a35885da8275bcc274c085d941516f93a57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Tue, 15 Sep 2026 16:59:33 +0200 Subject: [PATCH 1/2] Check named-block content where its yield sits A component call becomes its root element in the blanked output, so named-block content went directly under that root. When the block yields in a deeper element (a or under a root), parent rules gave false errors on that content. --- blank.ts | 47 +++++++++++++++++ examples/cell-yield-table-consumer.gts | 27 ++++++++++ lib/builtin-components.ts | 4 ++ lib/glint.ts | 3 +- lib/resolver/build-maps.ts | 1 + lib/resolver/walk.ts | 52 ++++++++++++++----- test/glint-fixtures/cell-yield-table-leaf.gts | 36 +++++++++++++ test/integration.test.ts | 16 ++++++ 8 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 examples/cell-yield-table-consumer.gts create mode 100644 test/glint-fixtures/cell-yield-table-leaf.gts diff --git a/blank.ts b/blank.ts index aea9091..5eb7ef4 100644 --- a/blank.ts +++ b/blank.ts @@ -1843,6 +1843,33 @@ function detectSuppressions( } walk(node.children); } + // Top-level elements the consumer passes into the listed blocks. + // Children outside any `<:named>` block belong to `default`. + function collectNestedBlockContentOffsets( + node: AST.ElementNode, + blocks: ReadonlyArray, + into: number[], + ): void { + function collect(stmts: ReadonlyArray): void { + for (const stmt of stmts) { + if (stmt.type === 'BlockStatement') { + const arm = selectBranch(stmt, branchSelections); + if (arm) collect(arm.body); + } else if (stmt.type === 'ElementNode') { + into.push(startOffset(stmt)); + } + } + } + const defaultContent: AST.Statement[] = []; + for (const child of node.children) { + if (child.type === 'ElementNode' && child.tag.startsWith(':')) { + if (blocks.includes(child.tag.slice(1))) collect(child.children); + } else { + defaultContent.push(child); + } + } + if (blocks.includes('default')) collect(defaultContent); + } // Custom walk — the off-the-shelf `traverse` would visit forms / // fieldsets that live entirely in a blanked-out branch for the // current pass, leaking their suppression rules into @@ -1968,6 +1995,17 @@ function detectSuppressions( collectThOffsets(stmt.children, thOffsets); for (const off of thOffsets) addPer(off, 'wcag/h63'); } + const nestedYieldBlocks = stmtKey ? glintComponentAttrMap?.get(stmtKey)?.nestedYieldBlocks : undefined; + if (nestedYieldBlocks) { + // Content of a block whose `{{yield}}` sits below the component's + // root lands under the root in the blanked output, but under a + // deeper element at runtime (`
` vs its ``). + const offsets: number[] = []; + collectNestedBlockContentOffsets(stmt, nestedYieldBlocks, offsets); + for (const off of offsets) { + for (const rule of PARENT_DEPENDENT_RULES) addPer(off, rule); + } + } walk(stmt.children); } } @@ -2036,6 +2074,15 @@ function selectBranch( // anything at runtime, so we don't trust them as a suppression signal. const INPUT_DRIVEN_FORM_EVENTS: ReadonlySet = new Set(['input', 'change']); +// Rules that judge an element by its parent or ancestors. +const PARENT_DEPENDENT_RULES: ReadonlyArray = [ + 'element-permitted-content', + 'element-permitted-parent', + 'element-permitted-order', + 'element-required-ancestor', + 'prefer-tbody', +]; + const STRUCTURAL_CONTENT_PARENTS: ReadonlySet = new Set([ 'ol', 'ul', 'menu', 'select', 'optgroup', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'colgroup', 'fieldset', 'details', 'picture', 'ruby', 'dl', diff --git a/examples/cell-yield-table-consumer.gts b/examples/cell-yield-table-consumer.gts new file mode 100644 index 0000000..7465de1 --- /dev/null +++ b/examples/cell-yield-table-consumer.gts @@ -0,0 +1,27 @@ +// `<:actions>` lands in a ``, so their +// content is valid. `<:caption>` lands directly in the `
`/`
` and `<:rows>` in a `
`, so the +// `
` there is a real element-permitted-content error. +import CellYieldTable from '../test/glint-fixtures/cell-yield-table-leaf.gts'; + +const rows = [{ id: '1', name: 'Excavator' }]; + + diff --git a/lib/builtin-components.ts b/lib/builtin-components.ts index 622bc36..13fa197 100644 --- a/lib/builtin-components.ts +++ b/lib/builtin-components.ts @@ -32,6 +32,10 @@ export interface ComponentAttrs { // from the substituted open tag to avoid html-validate firing // aria-label-misuse / aria-labelledby-misuse on the wrong element. fromYieldAncestor?: boolean; + // Blocks whose `{{yield}}` sits below the resolved element rather than + // directly in it, so their consumer content has a different parent at + // runtime than it has in the blanked output. + nestedYieldBlocks?: string[]; // Source byte offsets of the `