All files / packages/sds-customization-compliance/src/checks no-sizing-scale-modifier-hooks.ts

95.55% Statements 43/45
76.66% Branches 23/30
100% Functions 7/7
97.29% Lines 36/37

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                                                              4x   4x 4x         14x 87x   11x 38x   7x       7x       4x 14x 14x 5x                 9x 9x 9x 9x 16x     9x 9x 14x 14x 14x 7x     9x 7x                   7x 7x 2x 2x 1x       2x 1x         2x           7x   7x                      
/**
 * Compliance row: no per-size or per-feedback-semantic modifier hooks.
 *
 * RFC 1157 § "Which Modifiers Get Hooks" says SLDS1 modifier hooks for
 * sizing-scale modifiers (`--slds-c-{C}-{prop}-large`,
 * `--slds-c-{C}-{prop}-small`, etc.) and feedback-semantic modifiers
 * (`--slds-c-{C}-{prop}-error`, `-warning`, etc.) are **not carried
 * forward** into the SLDS2 Theme Layer API. These modifiers are owned
 * by the global token system; per-modifier hooks expose a customer
 * surface that lets consumers fork modifier definitions away from the
 * scale, creating drift the system cannot maintain.
 *
 * Customer migration: scope the canonical base hook to the parent or
 * container selector that renders the specific instance, instead of
 * setting the per-modifier hook.
 *
 * Companion check: {@link noInventedStateModifierHooks} catches
 * modifier-flavored *color* hook names. This check covers non-color
 * properties (`sizing`, `radius-border`, etc.) where the modifier
 * suffix lands on the trailing segment.
 */
 
import type { ComplianceCheck, ComplianceRow } from '../types.js';
import { resolveHookPrefix } from './helpers/resolveHookPrefix.js';
import {
  hookDefsFromThemeData,
  sampleHooks,
  NOT_MIGRATED_DETAIL,
  type HookDefRow,
} from './helpers/themeDataAccess.js';
 
const LABEL = 'No per-size or per-feedback-semantic modifier hooks';
 
const SIZING_SCALE_SUFFIXES = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
const FEEDBACK_SEMANTIC_SUFFIXES = ['error', 'warning', 'success', 'info'];
 
function endsInModifierSuffix(
  prop: string,
): { suffix: string; kind: 'sizing-scale' | 'feedback-semantic' } | null {
  for (const suffix of SIZING_SCALE_SUFFIXES) {
    if (prop.endsWith(`-${suffix}`)) return { suffix, kind: 'sizing-scale' };
  }
  for (const suffix of FEEDBACK_SEMANTIC_SUFFIXES) {
    if (prop.endsWith(`-${suffix}`)) return { suffix, kind: 'feedback-semantic' };
  }
  return null;
}
 
function locationOf(def: HookDefRow | undefined): string | undefined {
  Eif (!def?.source) return undefined;
  return def.line ? `${def.source}:${def.line}` : def.source;
}
 
export const noSizingScaleModifierHooks: ComplianceCheck = (input): ComplianceRow => {
  const { componentName: name, themeData } = input;
  if (!themeData) {
    return {
      id: 'no-sizing-scale-modifier-hooks',
      label: LABEL,
      status: 'info',
      count: 0,
      detail: NOT_MIGRATED_DETAIL,
    };
  }
 
  const prefix = `--slds-c-${resolveHookPrefix(name)}-`;
  const defs = hookDefsFromThemeData(themeData);
  const firstDefByHook = new Map<string, HookDefRow>();
  for (const def of defs) {
    if (!firstDefByHook.has(def.prop)) firstDefByHook.set(def.prop, def);
  }
 
  const offenders: { hook: string; suffix: string; kind: 'sizing-scale' | 'feedback-semantic' }[] = [];
  for (const hook of firstDefByHook.keys()) {
    Iif (!hook.startsWith(prefix)) continue;
    const matched = endsInModifierSuffix(hook);
    if (!matched) continue;
    offenders.push({ hook, ...matched });
  }
 
  if (offenders.length === 0) {
    return {
      id: 'no-sizing-scale-modifier-hooks',
      label: LABEL,
      status: 'pass',
      count: 0,
      detail:
        'No per-size or per-feedback-semantic modifier hooks defined. Modifier styling flows through the global token system, not through per-modifier component hooks.',
    };
  }
 
  const sizingCount = offenders.filter((o) => o.kind === 'sizing-scale').length;
  const semanticCount = offenders.filter((o) => o.kind === 'feedback-semantic').length;
  const detailParts: string[] = [];
  if (sizingCount > 0) {
    detailParts.push(
      `${sizingCount} sizing-scale hook${sizingCount === 1 ? '' : 's'} (e.g. \`-large\`, \`-small\`) — these are owned by the global size scale (\`--slds-g-sizing-*\`).`,
    );
  }
  if (semanticCount > 0) {
    detailParts.push(
      `${semanticCount} feedback-semantic hook${semanticCount === 1 ? '' : 's'} (e.g. \`-error\`, \`-warning\`) — these are owned by the global feedback tokens (\`--slds-g-color-{error,warning,success,info}-*\`).`,
    );
  }
 
  return {
    id: 'no-sizing-scale-modifier-hooks',
    label: LABEL,
    status: 'fail',
    count: offenders.length,
    detail:
      `${offenders.length} modifier hook${offenders.length === 1 ? '' : 's'} not carried forward by RFC 1157: ${sampleHooks(offenders.map((o) => o.hook))}. ${detailParts.join(' ')} ` +
      `Delete the hook and have customers scope the canonical base hook (e.g. \`--slds-c-${resolveHookPrefix(name)}-sizing\`) to the context where the size or semantic applies.`,
    offenders: offenders.map((o) => ({
      hook: o.hook,
      location: locationOf(firstDefByHook.get(o.hook)),
      fix:
        `Delete \`${o.hook}\`. ` +
        (o.kind === 'sizing-scale'
          ? `Sizing-scale modifiers are owned by the global size scale; customers should scope the canonical base hook to the parent context that renders this size, not override a per-size hook.`
          : `Feedback-semantic modifiers are owned by the global feedback tokens; customers should change the global token to retheme all error/warning/success/info states uniformly, not override a per-semantic hook.`),
    })),
  };
};