Product design skills

scripts/validate_tokens.py

A supporting file of the token-build skill, shown as source.

#!/usr/bin/env python3
"""Validate DTCG token files.

Checks:
  1. Every token file parses as valid JSON.
  2. Every {alias.reference} resolves to a token defined in the same set.

Usage:
  python3 scripts/validate_tokens.py                    # the engine's tokens/ directory
  python3 scripts/validate_tokens.py design-tokens.json # any file or directory

With explicit paths the set is treated as self-contained, so an unresolved alias
FAILS. The default tokens/ run only warns, because a reference there may point at
a token that is about to be added.
Exit code 0 = all good, 1 = problems found.
"""
import json
import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
TOKENS = ROOT / "tokens"
ALIAS = re.compile(r"\{([^}]+)\}")


def flatten(obj, prefix=""):
    """Yield dotted token paths that have a $value (DTCG leaf tokens)."""
    out = {}
    if isinstance(obj, dict):
        if "$value" in obj:
            out[prefix] = obj["$value"]
        for k, v in obj.items():
            if k.startswith("$"):
                continue
            child = f"{prefix}.{k}" if prefix else k
            out.update(flatten(v, child))
    return out


def collect_aliases(value):
    """Find all {ref} strings inside a value (which may be nested)."""
    found = []
    if isinstance(value, str):
        found += ALIAS.findall(value)
    elif isinstance(value, dict):
        for v in value.values():
            found += collect_aliases(v)
    elif isinstance(value, list):
        for v in value:
            found += collect_aliases(v)
    return found


def main(argv=()):
    explicit = [Path(a) for a in argv if not a.startswith("-")]
    if explicit:
        files = []
        for p in explicit:
            if p.is_dir():
                files += sorted(p.glob("*.json"))
            elif p.is_file():
                files.append(p)
            else:
                print(f"ERROR: {p} not found")
                return 1
    else:
        if not TOKENS.is_dir():
            print(f"ERROR: {TOKENS} not found")
            return 1
        files = sorted(TOKENS.glob("*.json"))
    if not files:
        print("ERROR: no token files found")
        return 1

    all_tokens = {}
    errors = []

    # Pass 1: parse + collect every defined token path (per file namespace + global)
    parsed = {}
    for f in files:
        try:
            data = json.loads(f.read_text())
        except json.JSONDecodeError as e:
            errors.append(f"{f.name}: invalid JSON — {e}")
            continue
        parsed[f] = data
        flat = flatten(data)
        for path, val in flat.items():
            all_tokens[path] = val           # e.g. "duration.fast"
            all_tokens[f"{f.stem}.{path}"] = val  # e.g. "motion.duration.fast"

    # Pass 2: resolve aliases
    unresolved = []
    for f, data in parsed.items():
        for path, val in flatten(data).items():
            for ref in collect_aliases(val):
                ref = ref.strip()
                # normalize cross-file refs like {../colors.semantic.border.default}
                norm = ref
                while norm.startswith("../") or norm.startswith("./"):
                    norm = norm[3:] if norm.startswith("../") else norm[2:]
                if ref in all_tokens or norm in all_tokens:
                    continue
                # tolerate cross-file refs that omit the file prefix
                tail = norm.split(".", 1)[-1]
                if tail in all_tokens:
                    continue
                if any(k.endswith(norm) for k in all_tokens):
                    continue
                unresolved.append(f"{f.name}: {path}{{{ref}}} (unresolved)")

    print(f"Parsed {len(parsed)}/{len(files)} token files, {len(all_tokens)//2} tokens defined.")
    for e in errors:
        print("  x " + e)
    for u in unresolved:
        print("  ! " + u)

    if errors:
        print("\nFAIL: JSON errors above.")
        return 1
    if unresolved:
        if explicit:
            print(f"\nFAIL: {len(unresolved)} unresolved alias(es) in a self-contained token set.")
            return 1
        print(f"\nWARN: {len(unresolved)} unresolved alias(es) — may reference tokens to be added.")
        # warnings don't fail the build
    print("\nOK: all token files valid JSON.")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))