All files / packages/sds-customization-compliance/src/checks no-descendant-owned-color-hooks.ts

92.85% Statements 26/28
78.57% Branches 11/14
100% Functions 3/3
100% Lines 25/25

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                                        4x   4x 10x 10x 4x                 6x 6x 6x 6x 7x 7x 7x 4x 4x   7x     6x 6x 4x 4x 6x 3x       6x 3x                   3x           3x            
/**
 * Compliance row: no descendant-only color hooks.
 *
 * A color hook that only ever lives under a descendant selector (e.g.
 * `.slds-button a { --slds-c-button-color-foreground: ... }`) should
 * either disappear in favor of `currentColor` or be promoted to an
 * element-segment hook on the base selector.
 */
 
import type { ComplianceCheck, ComplianceRow } from '../types.js';
import { classifyHook } from './helpers/classifyHook.js';
import { resolveHookPrefix } from './helpers/resolveHookPrefix.js';
import { selectorRole } from './helpers/selectorRole.js';
import {
  hookDefsFromThemeData,
  sampleHooks,
  NOT_MIGRATED_DETAIL,
  type HookDefRow,
} from './helpers/themeDataAccess.js';
 
const LABEL = 'No descendant-only color hooks';
 
export const noDescendantOwnedColorHooks: ComplianceCheck = (input): ComplianceRow => {
  const { componentName: name, themeData } = input;
  if (!themeData) {
    return {
      id: 'no-descendant-owned-color-hooks',
      label: LABEL,
      status: 'info',
      count: 0,
      detail: NOT_MIGRATED_DETAIL,
    };
  }
 
  const defs = hookDefsFromThemeData(themeData);
  const hookPrefix = `--slds-c-${resolveHookPrefix(name)}-`;
  const defsByHook = new Map<string, HookDefRow[]>();
  for (const d of defs) {
    Iif (!d.prop?.startsWith(hookPrefix)) continue;
    let bucket = defsByHook.get(d.prop);
    if (!bucket) {
      bucket = [];
      defsByHook.set(d.prop, bucket);
    }
    bucket.push(d);
  }
 
  const descendantOwned: string[] = [];
  for (const [hook, rows] of defsByHook) {
    const cls = classifyHook(hook, name);
    Iif (!cls.isColor) continue;
    if (rows.every((d) => selectorRole(d.selector, name) === 'descendant')) {
      descendantOwned.push(hook);
    }
  }
 
  if (descendantOwned.length === 0) {
    return {
      id: 'no-descendant-owned-color-hooks',
      label: LABEL,
      status: 'pass',
      count: 0,
      detail:
        'Every color hook is defined on the component root or an element segment — none exist only under descendant selectors.',
    };
  }
 
  return {
    id: 'no-descendant-owned-color-hooks',
    label: LABEL,
    status: 'fail',
    count: descendantOwned.length,
    detail: `${descendantOwned.length} color hook${descendantOwned.length === 1 ? '' : 's'} only ever defined under a descendant selector (e.g. \`.slds-button a { --slds-c-…-color-foreground: … }\`): ${sampleHooks(descendantOwned)}. Either drop the hook and inherit via \`currentColor\`, or promote it to an element-segment hook on the component root so customers can reach it.`,
    offenders: descendantOwned.map((hook) => ({
      hook,
      fix: `Either drop \`${hook}\` and let the descendant inherit via \`currentColor\`, or define it on the component root selector so customers can override it.`,
    })),
  };
};