All files / packages/sds-customization-compliance/src/checks/structural no-private-hooks.ts

100% Statements 33/33
92.85% Branches 13/14
100% Functions 5/5
100% Lines 29/29

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                                                                    5x 5x 5x   5x 5x     14x       6x 6x       14x         14x 14x 3x             3x   11x 11x 4x 4x 3x 3x                       5x 20x 20x 15x 5x     10x 14x   10x 5x               5x                  
/**
 * Rule: no private hooks in the theme layer.
 *
 * Private hooks are any custom property whose name starts with `--_`.
 * They are an internal indirection mechanism a stylesheet can use to
 * pass a value from one declaration to another without exposing it as a
 * customer-facing API. The compliance package treats them as
 * out-of-bounds for the customization surface — the theme layer
 * (`role: 'theme' | 'theme-base'`) — for two reasons:
 *
 * 1. Customers cannot address a private hook. Any value routed through
 *    one is invisible to the customization engine, so the routing
 *    decision becomes a "wall" the customer cannot reach over.
 * 2. Private hooks sidestep the other structural rules. The four
 *    "no hook write on X" checks all match `--slds-c-*`, so a private
 *    hook used as a workaround silently passes those checks while
 *    reproducing the same bypass they were written to prevent.
 *
 * Both *writes* (`--_foo: value`) and *consumption* (`var(--_foo)`
 * inside any declaration's value) are flagged. A theme file may legitimately
 * route a single value through several declarations; do that with a public
 * `--slds-c-*` hook (if the routing decision deserves a customer API), a
 * preprocessor variable, or by inlining the value at each paint site.
 *
 * Legacy `<component>.css` files (`role: 'base'`) and auxiliary CSS
 * (`role: 'aux'`) are out of scope — same boundary the other authoring
 * rules apply. They are the existing paint surface SLDS already ships,
 * not the customer-facing theme layer the compliance package gates.
 */
import type { Declaration } from 'postcss';
 
import type { ComplianceCheck, ComplianceRow, ComponentSourceFile, Offender } from '../../types.js';
import { declLocation, failRow, notRunYetRow, passRow, themeLayerFilesFor } from './internals.js';
 
const ID = 'structural-no-private-hooks';
const LABEL = 'No private hooks';
const CATEGORY = 'customer-reach' as const;
 
const PRIVATE_HOOK_PREFIX = '--_';
const PRIVATE_HOOK_REF_RE = /var\(\s*(--_[A-Za-z0-9_-]+)/g;
 
function isPrivateHookDecl(decl: Declaration): boolean {
  return decl.prop.startsWith(PRIVATE_HOOK_PREFIX);
}
 
function selectorOf(decl: Declaration): string {
  const parent = decl.parent;
  return parent?.type === 'rule' ? (parent as { selector: string }).selector : '<root>';
}
 
function collectPrivateHookOffenders(file: ComponentSourceFile, into: Offender[]): void {
  file.root.walkDecls((decl) => {
    // Reset the stateful /g regex unconditionally before any branch
    // returns. The regex remembers `lastIndex` between `.exec()` calls,
    // so a previous decl's read scan would otherwise carry over and
    // skip matches in later decls.
    PRIVATE_HOOK_REF_RE.lastIndex = 0;
    if (isPrivateHookDecl(decl)) {
      into.push({
        selector: selectorOf(decl),
        hook: decl.prop,
        location: declLocation(file, decl),
        note: 'Private hook write',
        fix: `Convert \`${decl.prop}\` to a public \`--slds-c-*\` hook (if the routing decision deserves a customer API), or inline the value at each paint site.`,
      });
      return;
    }
    const seen = new Set<string>();
    for (let m = PRIVATE_HOOK_REF_RE.exec(decl.value); m; m = PRIVATE_HOOK_REF_RE.exec(decl.value)) {
      const ref = m[1];
      if (seen.has(ref)) continue;
      seen.add(ref);
      into.push({
        selector: selectorOf(decl),
        hook: ref,
        prop: decl.prop,
        location: declLocation(file, decl),
        note: `Private hook read in \`${decl.prop}\``,
        fix: `Replace \`var(${ref})\` with a public \`--slds-c-*\` hook reference or the literal value.`,
      });
    }
  });
}
 
export const noPrivateHooks: 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 offenders: Offender[] = [];
  for (const file of files) collectPrivateHookOffenders(file, offenders);
 
  if (offenders.length === 0) {
    return passRow(
      ID,
      LABEL,
      "No private hooks (`--_*`) are written or consumed in this component's theme layer.",
      CATEGORY,
    );
  }
 
  return failRow(
    ID,
    LABEL,
    `${offenders.length} private hook reference${offenders.length === 1 ? '' : 's'} in theme-layer CSS. ` +
      `Private hooks (\`--_*\`) bypass the customization surface and recreate the wall the structural rules ` +
      `are written to prevent. Replace each with a public \`--slds-c-*\` hook or inline the value at the paint site.`,
    offenders,
  );
};