All files / packages/sds-customization-compliance/src/checks/structural no-attribute-substring-hook-writes.ts

95.65% Statements 22/23
83.33% Branches 10/12
100% Functions 2/2
100% Lines 20/20

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                                                                      4x 4x 4x   4x 13x 13x 9x 3x     6x 6x   6x 10x 6x 6x             2x 2x 2x                   6x 4x               2x                  
/**
 * Rule 4c: no customer hook writes on attribute-substring selectors.
 *
 * Attribute-substring selectors (`[icon-name^='action']`,
 * `[class*='_inverse']`) are routing tools for the design system, not
 * a surface the customer can address. Writing a hook on one of them
 * (e.g. inside `:host([icon-name*='action'])`) makes the value
 * dependent on a routing decision the customer cannot influence
 * through any documented pattern.
 *
 * Fix: route modifiers through customer-addressable classes
 * (`.slds-icon_action`) and write the hook there. If the routing must
 * stay on the attribute, define a hook the routing rule reads via
 * var() so the customer can override the hook on the component root.
 *
 * Carveout — relay routing: a rule whose hook writes all use the
 * `var(<relay>, revert-layer)` shape is a Path 4 recipe-level relay
 * route. `revert-layer` defers to the lower layer when the relay
 * isn't set, so the rule doesn't wall the composed component's
 * customer surface. See `helpers/relayRouting.ts`.
 */
 
import { classifySelector } from '../helpers/selectorIssues.js';
import { resolveHookPrefix } from '../helpers/resolveHookPrefix.js';
import { isRelayRoutingRule } from '../helpers/relayRouting.js';
import type { ComplianceCheck, ComplianceRow, Offender } from '../../types.js';
import {
  declLocation,
  failRow,
  notRunYetRow,
  passRow,
  themeLayerFilesFor,
  visitHookWriteRules,
} from './internals.js';
 
const ID = 'structural-no-attribute-substring-hook-writes';
const LABEL = 'No hook writes on attribute-substring selectors';
const CATEGORY = 'customer-reach' as const;
 
export const noAttributeSubstringHookWrites: 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 ownHookPrefix = resolveHookPrefix(input.componentName);
  const offenders: Offender[] = [];
 
  for (const file of files) {
    visitHookWriteRules(file, ({ rule, hookDecls }) => {
      const cls = classifySelector(rule.selector, input.componentName);
      if (!cls.issues.includes('attribute-substring')) return;
      // Relay-routing carveout: a rule whose hook writes all forward
      // INTO an embedded component's hook FROM this component's own
      // `--slds-c-{ownHookPrefix}-*` namespace with a `revert-layer`
      // fallback is an embedding-component relay; the embedded
      // component's customer surface remains reachable through
      // `revert-layer` when the relay isn't set.
      Iif (isRelayRoutingRule(hookDecls, ownHookPrefix)) return;
      for (const decl of hookDecls) {
        offenders.push({
          selector: rule.selector,
          hook: decl.prop,
          location: declLocation(file, decl),
          fix: `Move \`${decl.prop}\` off \`${rule.selector}\` and onto a customer-addressable class (e.g. a modifier class on the component root), or write it on the component root and have the attribute-substring rule read it via \`var(${decl.prop})\`.`,
        });
      }
    });
  }
 
  if (offenders.length === 0) {
    return passRow(
      ID,
      LABEL,
      'No customer hooks are written on attribute-substring selectors. Modifier routing exposes a class the customer can address.',
      CATEGORY,
    );
  }
 
  return failRow(
    ID,
    LABEL,
    `${offenders.length} hook write${offenders.length === 1 ? '' : 's'} on attribute-substring selectors. ` +
      `Customers cannot address attribute-routing positions; route modifiers through addressable classes ` +
      `or define a root-level hook the routing rule reads via var().`,
    offenders,
  );
};