All files / packages/sds-customization-compliance/src/checks/structural theme-base-reads-c-hooks-only.ts

100% Statements 35/35
95% Branches 19/20
100% Functions 7/7
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 114 115 116 117 118 119 120 121 122 123 124 125                                                                                          4x 4x 4x   4x     6x 6x       6x       10x           12x   11x 11x 11x 7x 7x 6x 6x 6x                               2x     4x 18x 18x 21x 13x 3x     10x 10x   10x 6x               4x                  
/**
 * Rule: `themes/base.css` only reads `--slds-c-*` component hooks.
 *
 * The RFC 1157 implementation guide is explicit: component base files
 * are "structural CSS with `var(--slds-c-*)` declarations." A
 * `var(--slds-s-*)` or `var(--slds-g-*)` reference inside `themes/base.css`
 * (`role: 'theme-base'`) skips the component-hook layer and breaks the
 * customization promise — the customer can override `--slds-c-foo` and
 * see no effect because the property is reading the shared/global tier
 * directly. Themes (`themes/cosmos.css`, `themes/lightning-blue.css`)
 * are where shared/global tokens get consumed, by *writing* them to
 * `--slds-c-*` hooks. The base file only reads those hooks.
 *
 * This rule is broader than `no-shared-to-global-fallback`: that one
 * catches the specific `var(--slds-s-*, var(--slds-g-*))` antipattern
 * anywhere; this one catches the full class of "base file references a
 * non-component token" violations, which only applies inside
 * `themes/base.css`.
 *
 * Carveouts:
 *   - Hook *writes* on `--slds-c-*` may take a `--slds-g-*` literal as a
 *     value (`--slds-c-icon-color-foreground: var(--slds-g-color-warning-1)`).
 *     The text-color modifiers in `icon/themes/base.css` are the
 *     canonical case: a class-to-global mapping that's theme-agnostic.
 *     Restrictions on those writes belong to the hook-shape rules; this
 *     check does not flag them.
 *
 * Legacy `<component>.css` (role: `'base'`) and aux files are out of
 * scope. They are the existing, pre-theme-layer paint surface; the
 * RFC's "base file" terminology refers specifically to the new
 * `themes/base.css` family.
 */
 
import type { Declaration } from 'postcss';
 
import type { ComplianceCheck, ComplianceRow, ComponentSourceFile, Offender } from '../../types.js';
import {
  declLocation,
  failRow,
  isHookWriteDecl,
  notRunYetRow,
  passRow,
  sourceFilesFor,
} from './internals.js';
 
const ID = 'structural-theme-base-reads-c-hooks-only';
const LABEL = '`themes/base.css` only reads `--slds-c-*` hooks';
const CATEGORY = 'customer-reach' as const;
 
const NON_COMPONENT_TOKEN_RE = /var\(\s*(--slds-(?:s|g)-[A-Za-z0-9_-]+)/g;
 
function selectorOf(decl: Declaration): string {
  const parent = decl.parent;
  return parent?.type === 'rule' ? (parent as { selector: string }).selector : '<root>';
}
 
function tierOf(token: string): 'shared' | 'global' {
  return token.startsWith('--slds-s-') ? 'shared' : 'global';
}
 
function collectOffenders(file: ComponentSourceFile, into: Offender[]): void {
  file.root.walkDecls((decl) => {
    // Carveout: hook-write declarations are allowed to take a literal
    // `var(--slds-g-*)` or `var(--slds-s-*)` as the right-hand side.
    // Theme-agnostic class-to-global mappings (e.g., `.slds-icon-text-warning`
    // setting `--slds-c-icon-color-foreground: var(--slds-g-color-warning-1)`)
    // legitimately live in `themes/base.css`.
    if (isHookWriteDecl(decl)) return;
 
    NON_COMPONENT_TOKEN_RE.lastIndex = 0;
    const seen = new Set<string>();
    for (let m = NON_COMPONENT_TOKEN_RE.exec(decl.value); m; m = NON_COMPONENT_TOKEN_RE.exec(decl.value)) {
      const ref = m[1];
      if (seen.has(ref)) continue;
      seen.add(ref);
      const tier = tierOf(ref);
      into.push({
        selector: selectorOf(decl),
        hook: ref,
        prop: decl.prop,
        location: declLocation(file, decl),
        note: `${tier === 'shared' ? 'Shared-tier' : 'Global-tier'} token read in \`${decl.prop}\``,
        fix:
          tier === 'shared'
            ? `Read this through a \`--slds-c-*\` component hook. Themes assign \`var(${ref})\` to the component hook; the base file reads \`var(--slds-c-${stripPrefix(ref)})\`.`
            : `Read this through a \`--slds-c-*\` component hook. Themes assign \`var(${ref})\` to the component hook; the base file reads the hook.`,
      });
    }
  });
}
 
function stripPrefix(token: string): string {
  return token.replace(/^--slds-(?:s|g)-/, '');
}
 
export const themeBaseReadsCHooksOnly: ComplianceCheck = (input): ComplianceRow => {
  const files = sourceFilesFor(input);
  if (!files) return notRunYetRow(ID, LABEL, CATEGORY);
  const themeBaseFiles = files.filter((f) => f.role === 'theme-base');
  if (themeBaseFiles.length === 0) {
    return passRow(ID, LABEL, 'Component has no `themes/base.css`; nothing to check.', CATEGORY);
  }
 
  const offenders: Offender[] = [];
  for (const file of themeBaseFiles) collectOffenders(file, offenders);
 
  if (offenders.length === 0) {
    return passRow(
      ID,
      LABEL,
      '`themes/base.css` reads only `--slds-c-*` component hooks. No shared- or global-tier token is consumed directly.',
      CATEGORY,
    );
  }
 
  return failRow(
    ID,
    LABEL,
    `${offenders.length} non-\`--slds-c-*\` token reference${offenders.length === 1 ? '' : 's'} in \`themes/base.css\`. ` +
      `Per RFC 1157, base files are structural CSS that reads only component hooks; shared- (\`--slds-s-*\`) and global-tier (\`--slds-g-*\`) tokens get consumed by themes that assign them to \`--slds-c-*\` hooks. ` +
      `Reading a non-component token directly here bypasses the customization layer.`,
    offenders,
  );
};