/** * `tailwind/raw-color-via-token` — flags Tailwind color tokens that resolve * to literal colors when policy requires named palette families. */ import type { FactIndex } from "../facts/index.js"; import { makeFinding } from "./finding.js"; import type { Finding } from "./types.js"; import { indexResolvedTailwindTokens, matchPattern, resolvedForClass, TAILWIND_COLOR_UTILITIES, UNIVERSAL_TAILWIND_COLOR_TOKENS, } from "./tailwind-rule-utils.js"; import { detectRawColor, readUsageNode } from "./utils.js"; export const RULE_ID = "tailwind/raw-color-via-token"; export const RULE_VERSION = "1"; export function ruleTailwindRawColorViaToken(ix: FactIndex): Finding[] { const allow = ix.policy.tailwindPaletteAllow(); if (!allow || allow.patterns.length === 0) return []; const resolvedByKey = indexResolvedTailwindTokens(ix); const findings: Finding[] = []; for (const klass of ix.byKind("tailwind_class")) { if (klass.value.kind !== "token") continue; if (!TAILWIND_COLOR_UTILITIES.has(klass.utility)) continue; if (UNIVERSAL_TAILWIND_COLOR_TOKENS.has(klass.value.token)) continue; if (matchPattern(klass.value.token, allow.patterns)) continue; const resolved = resolvedForClass(resolvedByKey, klass); if (!resolved || resolved.resolved.kind !== "color") continue; if (resolved.resolved.source === "theme-css") continue; const color = detectRawColor(resolved.resolved.value); if (!color) continue; const node = readUsageNode(ix, klass.nodeId); const evidenceIds = node ? [node.id, klass.id, resolved.id, allow.id] : [klass.id, resolved.id, allow.id]; findings.push( makeFinding({ ruleId: RULE_ID, ruleVersion: RULE_VERSION, severity: allow.severity, message: `\`${klass.raw}\` resolves to raw color \`${color}\` (${sourceLabel(resolved.resolved.source)}). Project policy requires \`${allow.patterns.join("`, `")}\` palette tokens.`, location: klass.location, evidence: ix.evidence(evidenceIds), fingerprintIdentity: { source: "tailwind_class", file: klass.file, nodeId: klass.nodeId, originPath: klass.originPath, raw: klass.raw, utility: klass.utility, token: klass.value.token, resolvedValue: color, }, attributes: { rawClass: klass.raw, utility: klass.utility, token: klass.value.token, resolvedValue: color, source: resolved.resolved.source, allowedPatterns: allow.patterns, }, }) ); } return findings; } function sourceLabel(source: "theme-css" | "default"): string { return source === "default" ? "Tailwind default" : source; }