view design_system/test/design_system_policy_test.js @ 255:5ec271d612ae

[dowa] Enforce arena ownership Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Tue, 04 Aug 2026 15:12:18 -0700
parents 2b6e732087ff
children 60a876c4587a
line wrap: on
line source

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');

const RUNFILES = process.env.JS_BINARY__RUNFILES;
const WORKSPACE = process.env.JS_BINARY__WORKSPACE;
assert.ok(RUNFILES);
assert.ok(WORKSPACE);

const sourceRoot = path.join(RUNFILES, WORKSPACE, 'design_system/src');
const files = [];

function walk(directory) {
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
    const entryPath = path.join(directory, entry.name);
    if (entry.isDirectory()) walk(entryPath);
    else if (/\.(?:css|html|js)$/.test(entry.name)) files.push(entryPath);
  }
}

walk(sourceRoot);

function validateCallableDocumentation(
  relative,
  source,
  index,
  name,
  parameterSource,
  failures,
) {
  const prefix = source.slice(0, index);
  const comment = prefix.match(/\/\*\*([\s\S]*?)\*\/\s*$/);
  const body = comment?.[1] || '';
  const parameters = parameterSource
    .split(',')
    .map(parameter => parameter.trim()
      .replace(/^\.\.\./, '')
      .split('=')[0].trim())
    .filter(Boolean);
  if (!/@return\s+\{[^}]+\}/.test(body)) {
    failures.push(`${relative} exported callable ${name} lacks @return`);
  }
  const documented = [...body.matchAll(
    /@param\s+\{[^}]+\}\s+([A-Za-z0-9_$]+)/g,
  )].map(parameter => parameter[1]);
  for (const parameter of parameters) {
    if (!documented.includes(parameter)) {
      failures.push(
        `${relative} exported callable ${name} lacks @param for ${parameter}`,
      );
    }
  }
}

const rawColor = new RegExp([
  '#[0-9a-fA-F]{3,8}\\b',
  '\\b(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\\s*\\(',
].join('|'), 'gi');
const forbiddenGlyph = /[×←→↑↓+◆◇✓✔✕✖★☆⚠]/gu;
const pictographic = /\p{Extended_Pictographic}/gu;
const failures = [];

for (const file of files) {
  const relative = path.relative(sourceRoot, file);
  const source = fs.readFileSync(file, 'utf8');

  if (relative !== 'styles/tokens.css') {
    for (const match of source.matchAll(rawColor)) {
      failures.push(
        `${relative}:${source.slice(0, match.index).split('\n').length} ` +
        `raw color ${JSON.stringify(match[0])}`,
      );
    }
    if (relative.endsWith('.css')) {
      const css = source.replace(/\/\*[\s\S]*?\*\//g, '');
      const allowedWords = new Set([
        'none',
        'transparent',
        'currentcolor',
        'inherit',
        'initial',
        'unset',
        'revert',
        'solid',
        'dashed',
        'dotted',
        'double',
        'groove',
        'ridge',
        'inset',
        'outset',
        'canvas',
        'canvastext',
        'linktext',
        'visitedtext',
        'activetext',
        'buttonface',
        'buttontext',
        'field',
        'fieldtext',
        'highlight',
        'highlighttext',
        'graytext',
        'mark',
        'marktext',
        'selecteditem',
        'selecteditemtext',
        'accentcolor',
        'accentcolortext',
        'color-mix',
        'in',
        'srgb',
        'linear-gradient',
        'radial-gradient',
        'conic-gradient',
        'to',
        'top',
        'right',
        'bottom',
        'left',
        'at',
        'circle',
        'ellipse',
        'px',
        'rem',
        'em',
        'vh',
        'vw',
        'vi',
        'vb',
        'vmin',
        'vmax',
        'deg',
        'rad',
        'turn',
      ]);
      for (const declaration of css.matchAll(
        /(^|[;{])\s*([-\w]+)\s*:\s*([^;{}]+)/gm,
      )) {
        const property = declaration[2].toLowerCase();
        const value = declaration[3];
        const colorProperty = [
          'accent-color',
          'background',
          'background-color',
          'box-shadow',
          'caret-color',
          'color',
          'fill',
          'outline',
          'outline-color',
          'scrollbar-color',
          'stroke',
          'text-shadow',
        ].includes(property) ||
          (property.startsWith('border') &&
           !/(?:collapse|image|radius|spacing|style|width)/.test(property));
        if (!colorProperty) continue;
        const withoutTokens = value.replace(
          /var\(\s*--zen-[\w-]+\s*\)/g,
          '',
        );
        const words = withoutTokens.toLowerCase()
          .match(/[a-z][a-z-]*/g) || [];
        if (words.some(word => !allowedWords.has(word)) &&
            !/^\s*(?:0|none|transparent|currentColor|inherit)\s*$/i.test(
              value,
            )) {
          failures.push(
            `${relative}:${css.slice(0, declaration.index).split('\n').length} ` +
            `${property} must use a --zen-* token`,
          );
        }
      }
    }
  }

  for (const pattern of [forbiddenGlyph, pictographic]) {
    for (const match of source.matchAll(pattern)) {
      failures.push(
        `${relative}:${source.slice(0, match.index).split('\n').length} ` +
        `icon glyph ${JSON.stringify(match[0])}; use <zen-icon>`,
      );
    }
  }
  if (relative !== 'components/icon.js' &&
      /<svg\b|createElementNS\([^)]*["']svg["']/i.test(source)) {
    failures.push(`${relative} defines SVG outside components/icon.js`);
  }
  if (relative.endsWith('.js')) {
    if (/\bimport\s*\/[/*]/.test(source)) {
      failures.push(`${relative} places comments inside import syntax`);
    }
    const importPattern =
      /(?:import\s+(?:[^'"]+\s+from\s+)?|export\s+[^'"]+\s+from\s+)["']([^"']+)["']/g;
    for (const match of source.matchAll(importPattern)) {
      if (!match[1].startsWith('.')) {
        failures.push(
          `${relative} imports external runtime ${JSON.stringify(match[1])}`,
        );
      }
    }
    if (/\bimport\s*\(/.test(source)) {
      failures.push(`${relative} uses forbidden dynamic import()`);
    }
  }
  if (relative.endsWith('.css') &&
      /@font-face|(?:font|font-family)\s*:[^;]*(?:icon|awesome|material)/i.test(
        source,
      )) {
    failures.push(`${relative} declares an icon font`);
  }
  if (relative.endsWith('.css')) {
    for (const match of source.matchAll(
      /@import\s+(?:url\(\s*)?["']?([^"')\s;]+)["']?\s*\)?/g,
    )) {
      if (!match[1].startsWith('.')) {
        failures.push(
          `${relative} imports external stylesheet ${JSON.stringify(match[1])}`,
        );
      }
    }
  }
}

for (const file of files.filter(file =>
  file.endsWith('.js')
)) {
  const relative = path.relative(sourceRoot, file);
  const source = fs.readFileSync(file, 'utf8');
  const declaration =
    /^export\s+(?:default\s+)?(?:async\s+)?(class|const|let|var|function)\s+([A-Za-z0-9_]+)/gm;
  for (const match of source.matchAll(declaration)) {
    const prefix = source.slice(0, match.index);
    const comment = prefix.match(/\/\*\*([\s\S]*?)\*\/\s*$/);
    const line = prefix.split('\n').length;
    if (!comment) {
      failures.push(`${relative}:${line} ${match[2]} lacks Google-style JSDoc`);
      continue;
    }
    const body = comment[1];
    if (match[1] === 'class' && !/@extends\s+\{[^}]+\}/.test(body)) {
      failures.push(`${relative}:${line} ${match[2]} lacks @extends`);
    } else if (['const', 'let', 'var'].includes(match[1]) &&
               !/@(?:const|type)\b/.test(body)) {
      failures.push(`${relative}:${line} ${match[2]} lacks @const/@type`);
    } else if (match[1] === 'function' &&
               (!/@param\s+\{[^}]+\}/.test(body) ||
                !/@return\s+\{[^}]+\}/.test(body))) {
      failures.push(`${relative}:${line} ${match[2]} lacks @param/@return`);
    } else if (match[1] === 'function') {
      const signature = source.slice(match.index).match(
        /^export\s+(?:default\s+)?(?:async\s+)?function\s+[A-Za-z0-9_]+\s*\(([^)]*)\)/,
      );
      const parameters = (signature?.[1] || '')
        .split(',')
        .map(parameter => parameter.trim()
          .replace(/^\.\.\./, '')
          .split('=')[0].trim())
        .filter(Boolean);
      const documented = [...body.matchAll(
        /@param\s+\{[^}]+\}\s+([A-Za-z0-9_$]+)/g,
      )].map(parameter => parameter[1]);
      for (const parameter of parameters) {
        if (!documented.includes(parameter)) {
          failures.push(
            `${relative}:${line} ${match[2]} lacks @param for ${parameter}`,
          );
        }
      }
    }
  }
  for (const match of source.matchAll(
    /^export\s+const\s+([A-Za-z0-9_]+)\s*=\s*(?:async\s*)?(?:\(([^)]*)\)|([A-Za-z0-9_$]+))\s*=>/gm,
  )) {
    validateCallableDocumentation(
      relative,
      source,
      match.index,
      match[1],
      match[2] || match[3] || '',
      failures,
    );
  }
  for (const match of source.matchAll(
    /^export\s+const\s+([A-Za-z0-9_]+)\s*=\s*(?:async\s*)?function(?:\s+[A-Za-z0-9_]+)?\s*\(([^)]*)\)/gm,
  )) {
    validateCallableDocumentation(
      relative,
      source,
      match.index,
      match[1],
      match[2],
      failures,
    );
  }
  for (const match of source.matchAll(
    /export\s*\{[\s\S]*?\}\s*(?!from\b)(?:;|$)/g,
  )) {
    failures.push(
      `${relative}:${source.slice(0, match.index).split('\n').length} ` +
      "uses an undocumented local export list",
    );
  }
}

assert.deepEqual(
  failures,
  [],
  `Design-system policy violations:\n${failures.join('\n')}`,
);