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 | 5x 5x 5x 7x 5x 20x 20x 16x 4x 12x 12x 12x 12x 16x 11x 11x 12x 11x 12x 12x 5x 7x 12x 8x 4x | /**
* Rule 5 (writes): hook writes on bare modifier or element selectors
* leak via custom-property inheritance.
*
* Modifiers and BEM elements are system-owned painting vocabulary;
* customers don't override them directly (they compose against the
* component's base). What this check enforces is **cascade hygiene**:
* a bare modifier or element class can match any element carrying
* that class — including a wrapper that doesn't carry the component
* root. When that happens, the rule's `--slds-c-*` write sets a
* custom property on the wrapper, which inherits down to every real
* component instance rendered inside that wrapper, repainting them
* with values they never asked for.
*
* The fix is to anchor the selector to the component root so the
* rule only matches inside an actual component instance.
*
* Companion check: {@link noBareBemPaintRules} flags bare-selector
* rules that paint without writing hooks (the no-cascade-amplification
* variant of the same anchoring problem).
*
* 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 an unconditional override. `revert-layer` defers to
* the lower layer when the relay isn't set, so the rule does not
* unconditionally repaint unrelated descendants. See
* `helpers/relayRouting.ts`.
*/
import { classifySurface, isAnchoringIncorrect, type SelectorSurface } from '../helpers/selectorSurface.js';
import { resolveBemRoot, 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-bare-bem-hook-writes';
const LABEL = 'No hook writes on bare child-element selectors';
const CATEGORY = 'customer-reach' as const;
function fixForSurface(_surface: SelectorSurface, part: string, prop: string, prefix: string): string {
// Only `system-element-bare` reaches this function; the surface
// parameter is kept on the signature so future surface variants
// can branch here without an API change.
return (
`Anchor \`${part}\` as a root-anchored descendant (\`.slds-${prefix} .slds-${prefix}__element\`) ` +
`so the rule only matches inside an actual \`.slds-${prefix}\` instance. ` +
`Without the anchor, this rule's \`${prop}\` write inherits to every \`.slds-${prefix}\` descendant ` +
`whenever the element class is placed on an element outside a \`.slds-${prefix}\`. ` +
`(Bare modifier classes are exempt — they're component-prefixed and only ever land on \`.slds-${prefix}\` instances.)`
);
}
export const noBareBemHookWrites: 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 = resolveBemRoot(input.componentName);
const ownHookPrefix = resolveHookPrefix(input.componentName);
const offenders: Offender[] = [];
for (const file of files) {
visitHookWriteRules(file, ({ rule, hookDecls }) => {
// Relay-routing carveout: a rule whose hook writes all forward
// INTO an embedded component's hook (LHS foreign to this
// component) FROM this component's own hook namespace (RHS
// `var(--slds-c-{ownHookPrefix}-...)`) with a `revert-layer`
// fallback is an embedding-component relay, not an unanchored
// modifier override.
Iif (isRelayRoutingRule(hookDecls, ownHookPrefix)) return;
const parts = rule.selector
.split(',')
.map((p) => p.trim())
.filter(Boolean);
for (const part of parts) {
const surface = classifySurface(part, input.componentName);
if (!isAnchoringIncorrect(surface)) continue;
for (const decl of hookDecls) {
offenders.push({
selector: part,
hook: decl.prop,
location: declLocation(file, decl),
fix: fixForSurface(surface, part, decl.prop, ownPrefix),
});
}
}
});
}
if (offenders.length === 0) {
return passRow(
ID,
LABEL,
'Every hook write on a child-element selector is anchored to the component root, so the assignment cannot leak into unrelated cascade contexts.',
CATEGORY,
);
}
return failRow(
ID,
LABEL,
`${offenders.length} hook write${offenders.length === 1 ? '' : 's'} on bare child-element selectors. ` +
`BEM-element class shapes (\`.slds-${ownPrefix}__…\`) can land as descendants of any cascade context; ` +
`without an instance anchor (\`.slds-${ownPrefix} .slds-${ownPrefix}__…\`), the hook write inherits down ` +
`wherever the element class lands and repaints unrelated descendants. ` +
`Anchor each selector to the component root per RFC 1157 Rule 5. ` +
`Bare modifier selectors are exempt — they're component-prefixed and only ever land on real instances.`,
offenders,
);
};
|