All files / packages/sds-customization-compliance/src/checks/structural no-layout-in-cosmetic-scope.ts

100% Statements 21/21
100% Branches 10/10
100% Functions 3/3
100% Lines 17/17

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                                          6x 6x             6x                                                                                       6x 15x 15x   10x   10x 21x 12x 12x   11x 4x 3x                     10x 8x     2x                
/**
 * Structural rule: no layout properties in rules that also write color hooks.
 *
 * A CSS rule that simultaneously writes cosmetic (color) hooks and controls
 * layout couples theme changes to layout side-effects. When a customer or
 * a future theme overrides the color hook, the layout declaration is not
 * in scope for that override, creating unexpected interactions. Color-hook
 * writes must live in their own rules, isolated from structural declarations.
 */
 
import type { ComplianceCheck, ComplianceRow, Offender } from '../../types.js';
import { classifyHook } from '../helpers/classifyHook.js';
import {
  declLocation,
  notRunYetRow,
  passRow,
  reviewRow,
  sourceFilesFor,
  visitHookWriteRules,
} from './internals.js';
 
const ID = 'structural-no-layout-properties-in-cosmetic-hook-scope';
const LABEL = 'No layout properties mixed with cosmetic color-hook writes';
 
/**
 * CSS properties that control box-model placement, sizing, or flow.
 * When authored in the same rule as a color hook, they couple visual
 * theming to layout behaviour.
 */
const LAYOUT_PROPERTIES = new Set([
  'display',
  'position',
  'top',
  'right',
  'bottom',
  'left',
  'z-index',
  'float',
  'clear',
  'width',
  'min-width',
  'max-width',
  'height',
  'min-height',
  'max-height',
  'margin',
  'margin-top',
  'margin-right',
  'margin-bottom',
  'margin-left',
  'padding',
  'padding-top',
  'padding-right',
  'padding-bottom',
  'padding-left',
  'grid',
  'grid-template',
  'grid-template-columns',
  'grid-template-rows',
  'grid-column',
  'grid-row',
  'flex',
  'flex-basis',
  'flex-grow',
  'flex-shrink',
  'order',
  'align-items',
  'justify-content',
  'gap',
  'place-items',
  'overflow',
]);
 
export const noLayoutInCosmeticScope: ComplianceCheck = (input): ComplianceRow => {
  const files = sourceFilesFor(input);
  if (!files) return notRunYetRow(ID, LABEL);
 
  const offenders: Offender[] = [];
 
  for (const file of files) {
    visitHookWriteRules(file, ({ rule, hookDecls, nonHookDecls }) => {
      const hasColorHook = hookDecls.some((decl) => classifyHook(decl.prop, input.componentName).isColor);
      if (!hasColorHook) return;
 
      for (const decl of nonHookDecls) {
        if (!LAYOUT_PROPERTIES.has(decl.prop.toLowerCase())) continue;
        offenders.push({
          selector: rule.selector,
          prop: decl.prop,
          location: declLocation(file, decl),
          note: `\`${decl.prop}\` appears in a rule that also writes a color hook; layout and cosmetic paint are coupled.`,
          fix: `Move \`${decl.prop}\` into a separate rule so color-hook writes are isolated from layout declarations.`,
        });
      }
    });
  }
 
  if (offenders.length === 0) {
    return passRow(ID, LABEL, 'No layout properties appear in rules that also write cosmetic color hooks.');
  }
 
  return reviewRow(
    ID,
    LABEL,
    `${offenders.length} layout declaration${offenders.length === 1 ? '' : 's'} appear alongside color-hook writes. ` +
      `Mixing layout and cosmetic paint in the same rule couples theme overrides to layout side-effects; split them into separate rules.`,
    offenders,
  );
};