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/ecosystem/baselines/hds-design-system.json b/ecosystem/baselines/hds-design-system.json index 4f40965..be20b15 100644 --- a/ecosystem/baselines/hds-design-system.json +++ b/ecosystem/baselines/hds-design-system.json @@ -162,22 +162,6 @@ "ruleId": "attribute-allowed-values", "message": "Attribute \"selected\" has invalid value \"true\"" }, - { - "file": "showcase/app/components/mock/app/main/table-complex.gts", - "line": 98, - "column": 10, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/accordion/sub-sections/content.gts", - "line": 124, - "column": 18, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, { "file": "showcase/app/components/page-components/advanced-table/sub-sections/base-elements.gts", "line": 520, @@ -194,14 +178,6 @@ "ruleId": "prefer-native-element", "message": "Prefer to use the native elements in " - }, { "file": "showcase/app/components/page-components/badge-count/sub-sections/content.gts", "line": 29, @@ -290,14 +266,6 @@ "ruleId": "wcag/h32", "message": " element must have a submit button" }, - { - "file": "showcase/app/components/page-components/copy/snippet/sub-sections/containers.gts", - "line": 48, - "column": 12, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, { "file": "showcase/app/components/page-components/dropdown/sub-sections/list-items/checkmark.gts", "line": 28, @@ -386,22 +354,6 @@ "ruleId": "prefer-native-element", "message": "Prefer to use the native elements in " - }, - { - "file": "showcase/app/components/page-components/filter-bar/code-fragments/with-table.gts", - "line": 172, - "column": 10, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, { "file": "showcase/app/components/page-components/form/base-elements/sub-sections/label.gts", "line": 37, @@ -1226,14 +1178,6 @@ "ruleId": "unique-landmark", "message": "Landmarks must have a non-empty and unique accessible name (aria-label or aria-labelledby)" }, - { - "file": "showcase/app/components/page-components/pagination/code-fragments/with-user-table.gts", - "line": 48, - "column": 10, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, { "file": "showcase/app/components/page-components/rich-tooltip/sub-sections/demo.gts", "line": 114, @@ -1338,14 +1282,6 @@ "ruleId": "close-order", "message": "Stray end tag '

'" }, - { - "file": "showcase/app/components/page-components/rich-tooltip/sub-sections/demo.gts", - "line": 343, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, { "file": "showcase/app/components/page-components/rich-tooltip/sub-sections/demo.gts", "line": 376, @@ -1482,286 +1418,6 @@ "ruleId": "element-permitted-content", "message": "
element is not permitted as content under
elements in " - }, - { - "file": "showcase/app/components/page-components/table/code-fragments/with-multi-select/filter.gts", - "line": 192, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/code-fragments/with-multi-select/sorting.gts", - "line": 145, - "column": 10, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/code-fragments/with-music-data.gts", - "line": 26, - "column": 10, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/code-fragments/with-selectable-data.gts", - "line": 42, - "column": 10, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/code-fragments/with-users-data.gts", - "line": 60, - "column": 12, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/code-fragments/with-users-data.gts", - "line": 86, - "column": 12, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 57, - "column": 16, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 95, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 125, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 151, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 176, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 204, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 224, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 253, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 287, - "column": 20, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 302, - "column": 20, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 323, - "column": 18, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 338, - "column": 18, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 363, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 498, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 543, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/base-elements.gts", - "line": 588, - "column": 14, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/customization.gts", - "line": 46, - "column": 22, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/customization.gts", - "line": 93, - "column": 16, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/customization.gts", - "line": 148, - "column": 8, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/customization.gts", - "line": 186, - "column": 8, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/customization.gts", - "line": 207, - "column": 8, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/customization.gts", - "line": 266, - "column": 8, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/data-model.gts", - "line": 27, - "column": 8, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/layout.gts", - "line": 72, - "column": 8, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/multi-select.gts", - "line": 62, - "column": 10, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/sorting.gts", - "line": 217, - "column": 10, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/sorting.gts", - "line": 300, - "column": 10, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, - { - "file": "showcase/app/components/page-components/table/sub-sections/tooltip.gts", - "line": 33, - "column": 8, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, { "file": "showcase/app/components/page-components/tooltip/sub-sections/button.gts", "line": 59, @@ -1946,22 +1602,6 @@ "ruleId": "element-permitted-content", "message": "
element is not permitted as content under
elements in " - }, - { - "file": "showcase/app/components/page-foundations/focus-ring/sub-sections/components.gts", - "line": 913, - "column": 16, - "severity": "error", - "ruleId": "prefer-tbody", - "message": "Prefer to wrap elements in " - }, { "file": "showcase/app/components/page-foundations/focus-ring/sub-sections/components.gts", "line": 1041, 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 `