Open Design Tokens
Guides

Writing custom rules

Add project-specific checks with the same API the built-in rules use.

A rule is an object with an id, metadata, and a create function that returns visitors. defineRule infers the option type.

import { defineRule, lintDtcg } from 'open-design-tokens/linter';

interface Options {
  prefix: string;
}

export const requirePrefix = defineRule<Options>({
  id: 'require-prefix',
  meta: {
    description: 'Top-level group names start with a prefix.',
    defaultSeverity: 'warning',
    defaultOptions: { prefix: 'ds-' },
    validateOptions: (o) =>
      typeof o === 'object' && o !== null && typeof (o as Options).prefix === 'string'
        ? undefined
        : 'prefix must be a string',
  },
  create(ctx) {
    return {
      group(node) {
        if (node.path.length !== 1) return;
        const name = node.path[0];
        if (name && !name.startsWith(ctx.options.prefix)) {
          ctx.report({
            code: 'missing',
            message: `Group "${name}" should start with "${ctx.options.prefix}".`,
            path: node.path,
            meta: { name },
          });
        }
      },
    };
  },
});

const result = lintDtcg(tokens, { rules: { 'require-prefix': ['error', { prefix: 'brand-' }] } }, { rules: [requirePrefix] });

What a rule sees

ctx gives you the prepared document (ctx.doc, with an index of every token and group), the resolver (ctx.resolver, for alias and reference resolution), the validation result, a reference graph (which tokens reference which), the merged options, isInvalid(pointer), and report.

Visitors: token(node) for every token and reference node, group(node) for every group, and document() once after the walk for document-wide checks.

Conventions

  • Rule ids are flat kebab-case. Reported codes become rule-id.code.
  • Rules never restate spec requirements; those belong in the validator, and a test rejects any rule that emits a validator issue code.
  • Build messages with formatValue from the validator entry when they contain user values, so control characters are escaped.
  • Rules are not run on nodes with validation errors; you do not need to defend against malformed values the validator already rejected.

On this page