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 144 145 146 147 148 149 150 151 152 153 154 155 156 | 13x 13x 13x 37x 37x 37x 34x 31x 31x 31x 20x 12x 12x 9x 7x 2x 11x 12x 12x 12x 12x 12x 12x 12x 12x 12x | /**
* Selector-role classifier. Tells the compliance logic whether a
* declaration or hook definition lives on the component's root selector,
* a pseudo-class state, a modifier (BEM `_mod`) class, or a descendant
* element.
*
* Under RFC 1157 only the `component-base` role defines the base hook
* API. State / modifier / descendant roles should *reassign* the base
* hook, not mint a new one or consume a raw value at the paint property.
*
* RFC 1157 Rule 5 (Root-Anchored Modifier and Element Selectors) requires
* modifier selectors to be written as the compound form
* `.slds-{C}.slds-{C}_modifier` and element selectors as the descendant
* form `.slds-{C} .slds-{C}__element`. The role classifier accepts both
* the legacy bare form and the new root-anchored form for backward
* compatibility during migration. Use {@link isRule5Canonical} to ask
* whether a selector follows the Rule 5 authoring shape.
*/
import { resolveBemRoots } from './resolveHookPrefix.js';
const STATE_RE = /:(hover|focus(-visible)?|active|disabled|visited|selected)\b/;
/* Multi-word BEM roots (`.slds-avatar-group_large`,
`.slds-button-group__item`) need hyphens in the root segment.
The trailing modifier/element value still uses kebab-case. */
const MODIFIER_RE = /\.slds-[a-z][a-z0-9-]*_[a-z][a-z0-9-]*/;
const BEM_ELEMENT_RE = /\.slds-[a-z][a-z0-9-]*__[a-z][a-z0-9-]*/;
export type SelectorRole = 'component-base' | 'state' | 'modifier' | 'descendant';
export function selectorRole(selector: string, name: string): SelectorRole {
const raw = String(selector ?? '').trim();
Iif (!raw) return 'component-base';
if (STATE_RE.test(raw)) return 'state';
if (MODIFIER_RE.test(raw)) return 'modifier';
// Walk every BEM root the component owns (primary + siblings). The
// override map handles components whose class root diverges from
// their directory name (`trialBar` → `.slds-trial-header`, etc.);
// the sibling-root list handles components whose theme files own
// multiple top-level roots (`card` ships both `.slds-card` and
// `.slds-card-wrapper`). The first root that matches the selector
// determines the role; without the iteration, a sibling root
// misclassifies as a descendant of the primary, which inflates
// `no-descendant-only-color-hooks` false positives.
for (const root of resolveBemRoots(name)) {
const componentBase = `.slds-${root}`;
if (raw === componentBase) return 'component-base';
if (raw.startsWith(componentBase)) {
const tail = raw.slice(componentBase.length);
// The BEM root must end at a class-name boundary. `-` and `_`
// continue the class identifier — keep walking other roots in
// case one is a longer match (e.g. `.slds-card-wrapper` is its
// own root, not `.slds-card` + `-wrapper` tail).
if (/^[-_]/.test(tail)) continue;
if (/^(\[|:|\.)/.test(tail)) return 'component-base';
if (/^[\s>+~]/.test(tail)) return 'descendant';
// Any other CSS-identifier continuation (alphanumeric) is a
// suffix that extends the class name into a sibling class. The
// canonical case is `.slds-avatar-grouped` against the BEM root
// `.slds-avatar-group` — `grouped` is not a child of `group`,
// it's a related-but-distinct class. Treat as a sibling root we
// don't own and route through the descendant path.
Eif (/^[A-Za-z0-9]/.test(tail)) return 'descendant';
return 'component-base';
}
}
return 'descendant';
}
/**
* Returns true when a selector follows the RFC 1157 Rule 5 authoring shape
* for component {@link name}:
*
* - The bare component root: `.slds-{C}` (with optional pseudo-state or
* attribute tail).
* - A root-anchored modifier compound: `.slds-{C}.slds-{C}_mod` with the
* two classes chained on the same compound (no whitespace).
* - A root-anchored element descendant: `.slds-{C} .slds-{C}__element`
* (with optional further descendants).
*
* Returns false for bare modifier or element classes whose leftmost
* compound is not anchored to the component root, e.g.
* `.slds-{C}_modifier` or `.slds-{C}__element`.
*/
export function isRule5Canonical(selector: string, name: string): boolean {
const raw = String(selector ?? '').trim();
if (!raw) return true;
const root = `.slds-${name}`;
const modifierPrefix = `${root}_`;
const elementPrefix = `${root}__`;
// Inspect only the leftmost compound. Whitespace, combinators, and
// commas mark the boundary; everything before is the leftmost compound.
const boundary = raw.search(/[\s,>+~]/);
const leftmost = boundary === -1 ? raw : raw.slice(0, boundary);
// Bare modifier or element class as the leftmost compound is the Rule 5
// violation we want to flag.
if (leftmost.startsWith(modifierPrefix)) return false;
if (leftmost.startsWith(elementPrefix)) return false;
// Any other modifier/element class appearing in the leftmost compound
// must be paired with the component root on the same compound.
const compoundClasses: string[] = leftmost.match(/\.[A-Za-z_][\w-]*/g) ?? [];
const hasModifierOrElement = compoundClasses.some(
(cls) => cls.startsWith(modifierPrefix) || cls.startsWith(elementPrefix),
);
if (hasModifierOrElement) {
// Compound contains a modifier/element class for this component; root
// must also be present in the same compound.
return compoundClasses.includes(root);
}
// Foreign modifier/element classes (different component) get a pass at
// this level; they belong to a different component's authoring rules.
return true;
}
/**
* Canonicalizes a selector for Rule 5 paired-form recognition during the
* migration from bare modifier/element selectors to root-anchored forms.
* Used when comparing legacy and new theme outputs for the same hook
* target so a shape-only migration does not register as a regression.
* Maps both the legacy bare form and the new root-anchored form to the
* bare canonical key, so equal hook targets compare equal.
*
* Transforms:
*
* - `.slds-{C}.slds-{C}_mod[trailing]` becomes `.slds-{C}_mod[trailing]`.
* - `.slds-{C} .slds-{C}__el[trailing]` becomes `.slds-{C}__el[trailing]`.
*
* Returns the input unchanged when neither pattern applies. The result is
* suitable for equality comparison only; it is not a generally valid CSS
* selector and must not be emitted into output stylesheets.
*/
export function canonicalizeRule5Selector(selector: string): string {
const raw = String(selector ?? '').trim();
Iif (!raw) return raw;
// Modifier compound: .slds-X.slds-X_mod[tail] -> .slds-X_mod[tail]
const modifierCompound = /^\.slds-([a-z][a-z0-9]*)\.slds-\1(_[a-z][a-z0-9-]*)/;
const modifierMatch = raw.match(modifierCompound);
Iif (modifierMatch) {
const [whole, name, suffix] = modifierMatch;
return `.slds-${name}${suffix}${raw.slice(whole.length)}`;
}
// Element descendant: .slds-X .slds-X__el[tail] -> .slds-X__el[tail]
const elementDescendant = /^\.slds-([a-z][a-z0-9]*) \.slds-\1(__[a-z][a-z0-9-]*)/;
const elementMatch = raw.match(elementDescendant);
Iif (elementMatch) {
const [whole, name, suffix] = elementMatch;
return `.slds-${name}${suffix}${raw.slice(whole.length)}`;
}
return raw;
}
/** Returns true if {@link selector} appears to use SLDS BEM element notation (`__`). */
export function hasBemElementClass(selector: string): boolean {
return BEM_ELEMENT_RE.test(String(selector ?? ''));
}
|