All files / packages/sds-customization-compliance/src/checks/helpers resolveHookPrefix.ts

100% Statements 12/12
100% Branches 6/6
100% Functions 3/3
100% Lines 10/10

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 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407                      25x                                                                     359x 339x                           25x                                                                                                                                                                                                     154x 142x                                     25x                                                                                                                                                                                                                                                                                                                                                                                                                                                           114x 114x 114x    
/**
 * Resolve the `--slds-c-{prefix}-*` namespace a component uses for
 * its hooks.
 *
 * Most components use their directory name lowercased (`alert` →
 * `alert`, `card` → `card`), but some flatten camelCase
 * (`avatarGroup` → `avatargroup`, `progressBar` → `progressbar`) and
 * a few share another component's namespace entirely (`buttonIcon`
 * → `button`).
 */
 
const HOOK_PREFIX_OVERRIDES = new Map<string, string>([
  ['buttonIcon', 'button'],
  // buttonStateful and buttonDualStateful relay onto button's hook
  // surface (same pattern as buttonIcon): both ship as button-shaped
  // surfaces and write `--slds-c-button-*` hooks rather than minting
  // their own namespace.
  ['buttonStateful', 'button'],
  ['buttonDualStateful', 'button'],
  ['menu', 'dropdown'],
  // progressIndicator's hook namespace is the default squash
  // (`progressindicator`), matching sibling progress components
  // (`progressbar`, `progressring`). Its BEM class root still diverges
  // (`.slds-progress`), so it stays in BEM_ROOT_OVERRIDES below — but
  // no hook-prefix override is needed.
  ['trees', 'tree'],
  // dynamicIcons authors hooks under `--slds-c-dynamicicon-{ellie,eq,…}-*`.
  // Singular prefix is intentional: each `.slds-icon-{name}` BEM root
  // is one icon, so the namespace reads "this dynamic icon, ellie
  // variant" rather than "the dynamic-icons collection".
  ['dynamicIcons', 'dynamicicon'],
  // dockedUtilityBar ships as `.slds-utility-bar*` / `.slds-utility-panel*`
  // and authors hooks under `--slds-c-utilitybar-*` (promoting the legacy
  // private `--_slds-c-utilitybar-*` set), so both the class root and the
  // hook namespace diverge from the `dockedutilitybar` default squash.
  ['dockedUtilityBar', 'utilitybar'],
]);
 
/**
 * Default resolver: honor the override map, otherwise lowercase-and-strip-
 * hyphens. The generator's validated variant consulted a "context" of
 * already-seen hooks; that heuristic is not necessary at check time since
 * every caller passes a themeData shape that is already scoped to one
 * component.
 */
export function resolveHookPrefix(name: string): string {
  if (HOOK_PREFIX_OVERRIDES.has(name)) return HOOK_PREFIX_OVERRIDES.get(name)!;
  return name.toLowerCase().replaceAll('-', '').replaceAll('_', '');
}
 
/**
 * Class-root override map: components whose authored CSS class root
 * doesn't match the kebab-case form of their directory name. Most
 * multi-word components map cleanly (`avatarGroup` → `avatar-group`,
 * `buttonGroup` → `button-group`) — only list exceptions here.
 *
 * Examples that need overrides: `dataTable` ships as `slds-table`,
 * `primitive-icon` ships as `slds-icon`. These match what the
 * component's `<name>.system.slds.uif.json` declares at
 * `structure.attributes.static.class`.
 */
const BEM_ROOT_OVERRIDES = new Map<string, string>([
  // alert ships as `.slds-notify_alert` (a notify-family variant compound),
  // with severity siblings (`alert_warning`, `alert_error`, `alert_offline`)
  // registered below.
  ['alert', 'notify_alert'],
  // breadcrumbs (plural directory) ships under `.slds-breadcrumb` (singular).
  ['breadcrumbs', 'breadcrumb'],
  // notifications (plural directory) ships under `.slds-notification` (singular)
  ['notifications', 'notification'],
  ['buttonIcon', 'button'],
  // buttonDualStateful and buttonStateful ship as button-shaped variant
  // compounds: their primary class root uses an underscore-modifier form
  // that the default kebab conversion would produce as a hyphenated form.
  ['buttonDualStateful', 'button_dual-stateful'],
  ['buttonStateful', 'button_stateful'],
  // checkboxToggle ships under `.slds-checkbox_toggle`, an underscore-modifier
  // root the default kebab conversion would mangle to `checkbox-toggle`.
  ['checkboxToggle', 'checkbox_toggle'],
  ['dataTable', 'table'],
  // dockedUtilityBar (directory) ships under `.slds-utility-bar` — the
  // directory name and class root drop "docked" and reorder. The pop-out
  // container and the separate `.slds-utility-panel` block are siblings
  // (see BEM_SIBLING_ROOTS).
  ['dockedUtilityBar', 'utility-bar'],
  // duelingPicklist ships under `.slds-dueling-list` (the directory name
  // "dueling picklist" and the class root "dueling list" diverge).
  ['duelingPicklist', 'dueling-list'],
  // expandableSection ships the BEM root `.slds-section`, not
  // `.slds-expandable-section`. The hook namespace stays the default squash
  // (`--slds-c-expandablesection-*`); only the class root diverges, same shape
  // as the tooltip / prompt entries.
  ['expandableSection', 'section'],
  ['globalNavigation', 'context-bar'],
  // activityTimeline ships under `.slds-timeline` classes, not
  // `.slds-activity-timeline`.
  ['activityTimeline', 'timeline'],
  ['activitytimeline', 'timeline'],
  ['activity-timeline', 'timeline'],
  ['menu', 'dropdown'],
  ['primitive-icon', 'icon'],
  ['progressIndicator', 'progress'],
  // prompt ships as `.slds-modal_prompt` — a modal variant compound,
  // not its own root. Storing the full variant class as the BEM root
  // lets ownership checks match the leftmost compound correctly.
  ['prompt', 'modal_prompt'],
  // radioButtonGroup ships under `.slds-radio_button-group` (wrapper)
  // and `.slds-radio_button` (per-item) — the class roots use an
  // underscore where the default kebab conversion would produce a
  // hyphen (`radio-button-group`). Override the primary root here and
  // register `radio_button` as a sibling below.
  ['radioButtonGroup', 'radio_button-group'],
  // radioGroup (a checkbox/radio form-element) ships under `.slds-radio`.
  ['radioGroup', 'radio'],
  // regions (plural directory) ships under `.slds-region` (singular).
  ['regions', 'region'],
  // scopedNotifications (plural directory) ships under
  // `.slds-scoped-notification` (singular), with `_dark` / `_light`
  // variant compounds registered as siblings below.
  ['scopedNotifications', 'scoped-notification'],
  // toast ships as `.slds-notify_toast` (a notify-family variant compound),
  // with `.slds-notify_container` registered as a sibling below.
  ['toast', 'notify_toast'],
  // Tooltip ships as `.slds-popover_tooltip` — a popover variant compound,
  // not its own root. Storing the full variant class as the BEM root lets
  // ownership checks match the leftmost compound exactly while keeping the
  // hook namespace (`--slds-c-tooltip-*`) intact.
  ['tooltip', 'popover_tooltip'],
  // treeGrid ships as `.slds-table_tree` (a table variant compound).
  ['treeGrid', 'table_tree'],
  ['trees', 'tree'],
  ['trialBar', 'trial-header'],
  // verticalNavigation ships under `.slds-nav-vertical` (the directory
  // name and class root invert their word order).
  ['verticalNavigation', 'nav-vertical'],
  // dynamicIcons ships under eight `.slds-icon-{name}` roots; the
  // primary is ellie, the rest are siblings (see BEM_SIBLING_ROOTS).
  ['dynamicIcons', 'icon-ellie'],
  // tabs ships no bare `.slds-tabs` class — the LBC stamps the
  // variant directly. `.slds-tabs_default` is the primary root,
  // `.slds-tabs_scoped` and `.slds-tabs_card` are siblings.
  ['tabs', 'tabs_default'],
]);
 
/**
 * Resolve the `.slds-{root}` class a component uses for its BEM
 * surface. Diverges from the hook prefix because hook names squash
 * camelCase (`--slds-c-avatargroup-*`) while CSS class names are
 * kebab-case (`.slds-avatar-group`). Both paths must agree to pass
 * the same string into `leftmostAnchoredToOwnRoot`-style checks
 * that match against the actual class on the leftmost compound.
 *
 * Default conversion:
 *   - directory names with `-` are kept as-is (`primitive-icon`
 *     overrides aside; `form-element` stays `form-element`).
 *   - camelCase directories convert to kebab-case
 *     (`avatarGroup` → `avatar-group`, `progressBar` → `progress-bar`).
 *   - all-lowercase directories pass through (`alert` → `alert`).
 */
export function resolveBemRoot(name: string): string {
  if (BEM_ROOT_OVERRIDES.has(name)) return BEM_ROOT_OVERRIDES.get(name)!;
  return name.replaceAll(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
}
 
/**
 * Sibling-root map: components whose theme files own MORE than one
 * top-level BEM root. Each entry lists the additional roots that
 * share the directory's theme files alongside the primary root from
 * {@link resolveBemRoot}. Each sibling is its own component-base for
 * compliance purposes — its hooks are NOT descendants of the primary.
 *
 * Hook namespaces remain distinct (e.g. `--slds-c-card-wrapper-*`
 * vs `--slds-c-card-*`); only the directory ownership is shared.
 *
 * Example: card ships `.slds-card` (primary) plus `.slds-card-wrapper`
 * (sibling). Without this map, `selectorRole('.slds-card-wrapper')`
 * walks the `-wrapper` tail and falsely classifies it as a descendant
 * of `.slds-card`, which trips `no-descendant-only-color-hooks` on
 * any wrapper-namespaced color hook.
 */
const BEM_SIBLING_ROOTS = new Map<string, ReadonlyArray<string>>([
  // dataTable directory ships the primary `.slds-table` plus several
  // composition-wrapper roots authored alongside it in the legacy
  // CSS. All share the `--slds-c-datatable-*` hook namespace; without
  // this entry the wrapper selectors classify as foreign.
  //   - `.slds-no-cell-focus`: inline-edit table mode that suppresses
  //     cell focus; owns its own edited/error/sorted-icon reassignments.
  //   - `.slds-popover_edit`: the inline-edit popover dialog, portaled
  //     outside the table subtree but authored and themed by dataTable.
  [
    'dataTable',
    [
      'table_header-fixed_container',
      'table_edit_container',
      'table_joined-wrapper',
      'no-cell-focus',
      'popover_edit',
    ],
  ],
  // buttonGroup directory ships THREE related layout roots:
  //   - `.slds-button-group` (primary; flex row of flush-bordered buttons)
  //   - `.slds-button-group-list` (alternate <ul>-based markup; same paint)
  //   - `.slds-button-group-row` (separated buttons with consistent gap;
  //     uses `.slds-button-group-item` as its row-item descendant)
  // All three share the `--slds-c-buttongroup-*` hook namespace.
  ['buttonGroup', ['button-group-list', 'button-group-row']],
  ['card', ['card-wrapper']],
  // dropZone ships the insertion slot as `.slds-drop-zone_drag__slot`, a
  // top-level class that stories/LBC can render outside `.slds-drop-zone`.
  // Slot hooks (`--slds-c-dropzone-slot-*`) live on that class, so it must
  // be a sibling root or its selectors classify as foreign.
  ['dropZone', ['drop-zone_drag__slot']],
  // expandableSection ships the standalone `.slds-section-title_divider` root alongside its primary `.slds-section`; register it so its `--slds-c-expandablesection-divider-*` hooks are audited instead of classified foreign.
  ['expandableSection', ['section-title_divider']],
  // popover owns the nubbin family (.slds-nubbin_*), painted in its base.css
  // and shared with tooltip. Nubbin hooks stay in the popover namespace
  // (--slds-c-popover-nubbin-*), so the bare block name is enough.
  ['popover', ['nubbin']],
  // dockedUtilityBar directory ships three top-level roots under one set
  // of theme files, all sharing the `--slds-c-utilitybar-*` namespace:
  //   - `.slds-utility-bar` (primary; the fixed bottom bar + its items,
  //     actions, text, and unread indicator)
  //   - `.slds-utility-bar_container` (the pop-out positioning wrapper)
  //   - `.slds-utility-panel` (the separate panel block — its own BEM
  //     block, not a `utility-bar` descendant — plus `__header` / `__body`)
  ['dockedUtilityBar', ['utility-bar_container', 'utility-panel']],
  // dockedComposer ships the primary `.slds-docked-composer` plus:
  //   - `.slds-docked_container` (fixed bottom tray; underscore root)
  //   - `.slds-docked-composer-modal` (popped-out modal host)
  //   - `.slds-email-composer` (email composition surface nested inside)
  // All share the directory's theme files; container / email use their
  // own hook namespaces (`--slds-c-dockedcontainer-*`,
  // `--slds-c-emailcomposer-*`).
  ['dockedComposer', ['docked_container', 'docked-composer-modal', 'email-composer']],
  // splitView ships the container wrapper and the bare global unread dot beside `.slds-split-view`.
  ['splitView', ['split-view_container', 'indicator_unread']],
  // chat directory ships a family of sibling BEM roots under one
  // directory, all sharing the `--slds-c-chat-*` hook namespace:
  //   - `.slds-chat` (primary; the log container)
  //   - `.slds-chat-list` / `.slds-chat-listitem` (message list + items,
  //     with `_inbound` / `_outbound` / `_bookend` / `_event` modifiers)
  //   - `.slds-chat-message` (message row + its `__text` / `__file` /
  //     `__image` bubbles and `_inbound` / `_outbound` / `_outbound-agent`
  //     / `_unsupported-type` / `_delivery-failure` variants)
  //   - `.slds-chat-icon` / `.slds-chat-avatar` (inline slots)
  //   - `.slds-chat-event` / `.slds-chat-bookend` (session chrome)
  // Without this entry every selector that paints these roots classifies
  // as foreign and the structural / hook-surface checks silently skip
  // chat's paint.
  [
    'chat',
    ['chat-list', 'chat-listitem', 'chat-icon', 'chat-message', 'chat-avatar', 'chat-event', 'chat-bookend'],
  ],
  // tabs directory: no bare `.slds-tabs` class is shipped. The
  // customer-facing root classes are the variant compounds the LBC
  // stamps directly: `.slds-tabs_default`, `.slds-tabs_scoped`,
  // `.slds-tabs_card`. Each is a self-contained tabs surface. Plus
  // three independent layout roots authored in the same theme files,
  // each a distinct customer-facing identity with its own hook
  // namespace:
  //   - `.slds-vertical-tabs` → `--slds-c-verticaltabs-*`
  //   - `.slds-sub-tabs`      → `--slds-c-subtabs-*`
  //   - `.slds-tabs-mobile`   → `--slds-c-tabsmobile-*`
  // The `--slds-c-tabs-*` namespace belongs to the tabs identity
  // (default / scoped / card). Sibling-identity hooks classify as
  // foreign to `tabs`, which is intentional: it signals to the
  // compliance classifier that these are distinct customer surfaces.
  ['tabs', ['tabs_scoped', 'tabs_card', 'vertical-tabs', 'sub-tabs', 'tabs-mobile', 'tab']],
  // checkboxButton directory ships TWO independent surfaces:
  //   - `.slds-checkbox-button` (icon-button shape, primary BEM root)
  //   - `.slds-checkbox_add-button` (checkbox-shaped add/remove button;
  //     spelled as a `slds-checkbox` modifier for HTML-wiring reasons,
  //     but its theme files own all paint and hooks for it)
  // Without this entry the `_add-button` selectors classify as
  // `slds-checkbox` modifiers and every hook write under them looks
  // like a cross-component descendant write.
  ['checkboxButton', ['checkbox_add-button']],
  // dynamicIcons ships eight decorative icon roots under one
  // directory; each icon has its own paint surface and customization
  // hooks. Primary root (`icon-ellie`) plus seven siblings.
  [
    'dynamicIcons',
    ['icon-eq', 'icon-score', 'icon-strength', 'icon-trend', 'icon-waffle', 'icon-help', 'icon-typing'],
  ],
  // radioButtonGroup directory ships TWO surfaces:
  //   - `.slds-radio_button-group` (wrapper, primary BEM root)
  //   - `.slds-radio_button` (per-item; the button-shaped radio)
  // The per-item is the dominant paint surface; the wrapper only
  // anchors group-level radius. Both share the directory's theme
  // files; without this entry every selector that paints
  // `.slds-radio_button` classifies as foreign.
  ['radioButtonGroup', ['radio_button']],
  // map directory ships THREE BEM roots:
  //   - `.slds-map` (primary) plus `.slds-map_container`: layout for
  //     the embedded map iframe (no themed paint).
  //   - `.slds-has-coordinates`: the parent grid wrapper that toggles
  //     the side coordinates panel; owns the panel-surface background.
  //   - `.slds-coordinates`: the side panel itself; owns
  //     header/title/item-action paint.
  // All three roots share the `--slds-c-map-*` hook namespace; the
  // directory owns every theme write under any of them.
  ['map', ['coordinates', 'has-coordinates']],
  // combobox directory ships multiple BEM roots:
  //   - `.slds-combobox` (primary)
  //   - `.slds-combobox_container` (the LBC-stamped wrapper that
  //     carries the dropdown-toggle class and selection-ring shadow)
  //   - `.slds-listbox` (the dropdown listbox family — options, icon,
  //     meta, selection-group, object-switcher). Combobox is the
  //     de-facto theme owner of `slds-listbox*` paint: the LBC renders
  //     this markup and no other component's theme files paint hooks
  //     onto these classes.
  //   - `.slds-combobox-button` (button-style variant wrapper)
  //   - `.slds-combobox-group` (grouped combobox layout wrapper)
  //   - `.slds-has-inline-listbox` (inline-listbox compound layout)
  //   - `.slds-has-object-switcher` (object-switcher compound layout)
  //   - `.slds-combobox_object-switcher` (the object-switcher itself)
  [
    'combobox',
    [
      'combobox_container',
      'listbox',
      'combobox-button',
      'combobox-group',
      'has-inline-listbox',
      'has-object-switcher',
      'combobox_object-switcher',
      'input-has-icon_left',
      'input-has-icon_left-right',
    ],
  ],
  // alert directory ships the primary `.slds-notify_alert` plus three
  // severity-variant compounds (`alert_warning`, `alert_error`,
  // `alert_offline`) and the `.slds-notify__close` button wrapper (the
  // light-DOM parent of the LBC close button, which the alert theme
  // paints via `--slds-c-alert-close-*`). All share the `--slds-c-alert-*`
  // hook namespace.
  ['alert', ['alert_warning', 'alert_error', 'alert_offline', 'notify__close']],
  // toast directory ships the primary `.slds-notify_toast` (the toast
  // body) plus `.slds-notify_container` (the positioning wrapper that
  // groups multiple toasts). Both share the `--slds-c-toast-*`
  // namespace; without this entry the container selectors classify as
  // foreign.
  ['toast', ['notify_container']],
  // notifications ships `.slds-notification-container` as the fixed positioning wrapper, sharing `--slds-c-notifications-*`
  ['notifications', ['notification-container']],
  // scopedNotifications ships `.slds-scoped-notification` (primary)
  // plus light/dark variant compounds. All share
  // `--slds-c-scopednotifications-*`.
  ['scopedNotifications', ['scoped-notification_light', 'scoped-notification_dark']],
  // trees directory ships `.slds-tree` (primary, registered via
  // BEM_ROOT_OVERRIDES) plus `.slds-tree-container` for the outer
  // scroll wrapper.
  ['trees', ['tree-container']],
  // colorPicker directory ships `.slds-color-picker` (primary) plus
  // `.slds-swatch` (the inline color swatch element used both inside
  // the picker and as a standalone preview). Both share the
  // `--slds-c-colorpicker-*` hook namespace.
  ['colorPicker', ['swatch']],
  // expression directory ships `.slds-expression` (primary) plus
  // `.slds-filters` (the compact filter-chip variant). Both share
  // the `--slds-c-expression-*` hook namespace.
  ['expression', ['filters']],
  // form-element ships `.slds-form-element` plus the record-detail grid roots
  // (`.slds-form__row` / `.slds-form__item`, PARENTS of form-element). Register
  // the full roots as bases, plus the bare `form` owner so
  // `no-bem-element-hook-writes` allows the hook writes.
  ['form-element', ['form__row', 'form__item', 'form']],
  // feed directory ships THREE related discussion roots:
  //   - `.slds-feed` (wrapper / list / item chrome; primary)
  //   - `.slds-post` (feed post body; dominant paint surface)
  //   - `.slds-comment` (comment content padding; painted as
  //     `.slds-comment__content` and nested under item-comments)
  // All three share the `--slds-c-feed-*` hook namespace.
  ['feed', ['post', 'comment']],
  // duelingPicklist owns the standalone `.slds-picklist__options` box that
  // core's commerce reorderableList renders with no `.slds-dueling-list`
  // ancestor (the `picklist` component is deprecated and has no `__options`).
  // Register the full `picklist__options` so the selector-surface classifier
  // treats it as a base, and the bare `picklist` owner so
  // `no-bem-element-hook-writes` (which matches the pre-`__` owner) allows
  // the hook writes.
  ['duelingPicklist', ['picklist__options', 'picklist']],
  // file directory ships the legacy figure alias `.slds-image*` alongside
  // the primary `.slds-file*` root, plus the hyphenated `.slds-file-has-actions`
  // / `.slds-image-has-actions` truncation-hack roots (top-level classes that
  // don't match the `.slds-file` / `.slds-image` prefixes). All share the
  // `--slds-c-file-*` hook namespace (the LBC and stories only stamp
  // `.slds-file`). Without these entries the alias and has-actions selectors
  // classify as foreign.
  ['file', ['image', 'file-has-actions', 'image-has-actions']],
]);
 
/**
 * Resolve every `.slds-{root}` class the component's theme files own:
 * the primary root from {@link resolveBemRoot} followed by any sibling
 * roots from {@link BEM_SIBLING_ROOTS}. Order matters — the primary
 * is always first, so callers that want "the canonical root" can keep
 * using {@link resolveBemRoot}. Callers that need to recognize ANY
 * owned root (most ownership / classification checks) iterate this
 * list and accept a match against any entry.
 */
export function resolveBemRoots(name: string): ReadonlyArray<string> {
  const primary = resolveBemRoot(name);
  const siblings = BEM_SIBLING_ROOTS.get(name) ?? [];
  return [primary, ...siblings];
}