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
formatValuefrom 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.