Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | 5x 5x 5x 5x 11x 11x 11x 11x 16x 16x 11x 5x 18x 18x 14x 3x 11x 11x 11x 11x 15x 11x 11x 7x 7x 7x 4x 4x 4x 11x 4x 11x 7x 4x | /**
* Rule 4b: no cross-component hook writes on BEM `__element` selectors.
*
* A component's theme file may write hooks on its OWN element selectors
* (`button/themes/cosmos.css` writing `.slds-button .slds-button__icon`):
* that's the normal theme-layer shape — the theme decorates canonical
* children of the component it owns. Customer overrides on the
* component root flow into descendants through custom-property
* inheritance.
*
* What this check flags is *cross-component* element writes:
* `icon/themes/cosmos.css` writing `.slds-input__icon { ... }` reaches
* into another component's inner DOM that the customer cannot place
* the class on. Customer overrides on `.slds-input` cannot beat the
* descendant write. Same wall problem as Rule 4a but specific to BEM-
* element shape.
*
* Fix: move the write into the owning component's theme file, or
* define a new hook on the owning component's root that the descendant
* rule reads via var().
*
* Carveout — relay routing: a rule whose hook writes all use the
* `var(<relay>, revert-layer)` shape is a Path 4 recipe-level relay
* route, not a cross-component override. `revert-layer` defers to
* the composed component's own layered paint when the relay isn't
* set. See `helpers/relayRouting.ts`.
*/
import selectorParser from 'postcss-selector-parser';
import { resolveBemRoots, resolveHookPrefix } from '../helpers/resolveHookPrefix.js';
import { isRelayRoutingRule } from '../helpers/relayRouting.js';
import type { ComplianceCheck, ComplianceRow, Offender } from '../../types.js';
import {
declLocation,
failRow,
notRunYetRow,
passRow,
themeLayerFilesFor,
visitHookWriteRules,
} from './internals.js';
const ID = 'structural-no-bem-element-hook-writes';
const LABEL = 'No cross-component hook writes on BEM `__element` selectors';
const CATEGORY = 'customer-reach' as const;
const BEM_ELEMENT_CLASS_RE = /^slds-([a-z][a-z0-9]*)__[a-z]/;
/**
* For each selector in the comma list, return the set of component
* names whose `__element` classes appear in that selector. A selector
* with no BEM-element class returns an empty set.
*/
function elementOwners(selector: string): Set<string> {
const owners = new Set<string>();
let ast;
try {
ast = selectorParser().astSync(selector);
} catch {
// Modern selector forms (e.g. comma-separated `:not(...)` from
// Selectors Level 4) may not parse with the bundled
// postcss-selector-parser. Treat unparseable selectors as having
// no element owners — the rule won't flag them, which is the
// conservative choice for a check that's specifically scoped to
// `__element` classes anyway.
return owners;
}
ast.walkClasses((node) => {
const m = BEM_ELEMENT_CLASS_RE.exec(node.value ?? '');
if (m) owners.add(m[1]);
});
return owners;
}
export const noBemElementHookWrites: ComplianceCheck = (input): ComplianceRow => {
const files = themeLayerFilesFor(input);
if (!files) return notRunYetRow(ID, LABEL, CATEGORY);
if (files.length === 0) {
return passRow(ID, LABEL, 'Component has no theme or theme-base files; nothing to check.', CATEGORY);
}
const ownPrefix = resolveHookPrefix(input.componentName);
// Multi-root awareness: card ships `.slds-card` AND `.slds-card-wrapper`.
// Element classes anchored to either root (e.g. `.slds-card-wrapper__foo`)
// are still own-component, not foreign. Compare element owners against
// every root the directory owns.
const ownRoots = new Set(resolveBemRoots(input.componentName));
const offenders: Offender[] = [];
for (const file of files) {
visitHookWriteRules(file, ({ rule, hookDecls }) => {
const owners = elementOwners(rule.selector);
if (owners.size === 0) return;
// Same-component element writes (`button/themes/cosmos.css`
// writing `.slds-button .slds-button__icon`) are normal theme-
// layer shape — the theme decorates canonical children of the
// component it owns. Only flag when an element class belongs to
// a *different* component (not own hook prefix, not any sibling
// BEM root the directory owns).
const foreignOwners = [...owners].filter(
(owner) => resolveHookPrefix(owner) !== ownPrefix && !ownRoots.has(owner),
);
if (foreignOwners.length === 0) return;
// Relay-routing carveout: a rule whose hook writes all use the
// `var(<relay>, revert-layer)` shape is a recipe-level relay
// routing the composed component's hook through the recipe's
// own surface. The composed component's customer overrides
// remain reachable through `revert-layer` when the relay
// isn't set.
Iif (isRelayRoutingRule(hookDecls, ownPrefix)) return;
const ownerList = foreignOwners.map((o) => '`.slds-' + o + '__…`').join(', ');
const ownerPossessive = foreignOwners.length === 1 ? "that component's" : "those components'";
for (const decl of hookDecls) {
offenders.push({
selector: rule.selector,
hook: decl.prop,
location: declLocation(file, decl),
fix:
`\`${rule.selector}\` reaches into ${ownerList} ` +
`(owned by another component). Move \`${decl.prop}\` into ${ownerPossessive} theme file, ` +
`or define a hook on its root that the descendant rule reads via \`var()\`.`,
});
}
});
}
if (offenders.length === 0) {
return passRow(
ID,
LABEL,
'No cross-component BEM `__element` hook writes. Every element-targeted hook write decorates a canonical child of the component that owns it.',
CATEGORY,
);
}
return failRow(
ID,
LABEL,
`${offenders.length} hook write${offenders.length === 1 ? '' : 's'} target another component's BEM \`__element\` selectors. ` +
`Customer overrides on the owning component cannot reach descendant writes authored from outside that component; relocate each write to the owning component's theme file.`,
offenders,
);
};
|