All files / packages/sds-stylelint-config/src/plugins design-token-pattern.ts

94% Statements 47/50
82.85% Branches 29/35
100% Functions 8/8
100% Lines 44/44

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 126 127 128 129 130 131                                  1x 1x     25x 5x   20x 25x 20x 20x                     27x 27x 2x               2x     25x 25x 25x 27x   27x 5x     25x 27x 2x                                 64x 32x     32x 3x 3x 3x 1x             1x   3x       29x   26x 26x   26x         1x   1x 29x 29x 29x   29x 29x   29x 30x 64x                
import path from 'node:path';
import stylelint from 'stylelint';
import valueParser from 'postcss-value-parser';
import type { Declaration } from 'postcss';
import metadata, { getMergedMetadata } from '../metadata/metadata.js';
import {
  validateNamespace,
  validateComponentTokens,
  tokenize,
  shouldValidate,
  formatErrors,
  formatLegacyWarning,
  formatPrivateHookWarning,
} from '../utils/index.js';
import { isPrivateHook, stripPrivatePrefix, isPrivateComponentHook } from '../deprecated/private-hooks.js';
import type { RuleOptions } from '../types.js';
 
const { report, validateOptions } = stylelint.utils;
const ruleName = 'sds-stylelint-plugin/design-token-pattern';
 
function deriveContext(decl: Declaration): string | null {
  if (decl.source?.input?.file) {
    return path.parse(decl.source.input.file).name.split('.')[0];
  }
  const sel = (decl.parent as any)?.selector as string | undefined;
  Iif (!sel) return null;
  const match = /\.([a-z][a-z0-9]*)/.exec(sel);
  return match ? match[1] : null;
}
 
function runValidation(
  displayValue: string,
  tokens: string[],
  decl: Declaration,
  result: stylelint.PostcssResult,
  meta: ReturnType<typeof getMergedMetadata>,
  privatePrefix: string,
): void {
  const nsResult = validateNamespace(tokens[0], privatePrefix);
  if (!nsResult.valid) {
    report({
      ruleName,
      result,
      message: formatErrors(displayValue, [
        { segment: 'namespace', expected: nsResult.names, received: tokens[0] },
      ]),
      node: decl,
    });
    return;
  }
 
  const scope = tokens[1];
  const expectedComponent = deriveContext(decl);
  const aliases = meta.legacyPropertyAliases || {};
  const { errors, warnings } = validateComponentTokens(tokens, scope, expectedComponent, meta, aliases);
 
  if (errors.length > 0) {
    report({ ruleName, result, message: formatErrors(displayValue, errors), node: decl });
  }
 
  const legacyWarnings = warnings?.filter((w) => w.type === 'legacy-syntax') ?? [];
  if (legacyWarnings.length > 0) {
    report({
      ruleName,
      result,
      message: formatLegacyWarning(displayValue, legacyWarnings),
      node: decl,
      severity: 'warning',
    });
  }
}
 
function processNode(
  node: any,
  decl: Declaration,
  result: stylelint.PostcssResult,
  meta: ReturnType<typeof getMergedMetadata>,
  privatePrefix: string,
): void {
  if (node.type !== 'word') return;
  const rawValue: string = node.value;
 
  // --- Deprecated: private hooks (--_slds-c-*) ---
  if (isPrivateHook(rawValue)) {
    const strippedValue = stripPrivatePrefix(rawValue);
    const strippedTokens = tokenize(strippedValue);
    if (isPrivateComponentHook(strippedTokens)) {
      report({
        ruleName,
        result,
        message: formatPrivateHookWarning(rawValue),
        node: decl,
        severity: 'warning',
      });
      runValidation(strippedValue, strippedTokens, decl, result, meta, privatePrefix);
    }
    return;
  }
  // --- End deprecated block ---
 
  if (!shouldValidate(rawValue, privatePrefix)) return;
 
  const tokens = tokenize(rawValue);
  Iif (!tokens[1] || (tokens[1] !== 'c' && tokens[1] !== 's')) return;
 
  runValidation(rawValue, tokens, decl, result, meta, privatePrefix);
}
 
type Rule = Parameters<typeof stylelint.createPlugin>[1];
 
const messages = stylelint.utils.ruleMessages(ruleName, {});
 
const componentPlugin = Object.assign(
  (primary: boolean, options?: RuleOptions) => (root: any, result: stylelint.PostcssResult) => {
    const valid = validateOptions(result, ruleName, { actual: primary });
    Iif (!valid) return;
 
    const meta = getMergedMetadata(metadata, options ?? {});
    const privatePrefix = options?.privateSyntax || metadata.privateSyntax[0];
 
    root.walkDecls((decl: Declaration) => {
      valueParser(decl.value).walk((node: any) => {
        processNode(node, decl, result, meta, privatePrefix);
      });
    });
  },
  { ruleName, messages },
) satisfies Rule;
 
export default stylelint.createPlugin(ruleName, componentPlugin);