/** * `tailwind/forbidden-palette` — enforces Tailwind palette allow/deny policy * against resolved token-form color utilities. */ import type { FactIndex, TailwindClassFact } from "../facts/index.js"; import { makeFinding } from "./finding.js"; import type { Finding, FindingReplaceClassTokenFix } from "./types.js"; import { indexResolvedTailwindTokens, matchPattern, resolvedForClass, TAILWIND_COLOR_UTILITIES, UNIVERSAL_TAILWIND_COLOR_TOKENS, } from "./tailwind-rule-utils.js"; import { readUsageNode } from "./utils.js"; export const RULE_ID = "tailwind/forbidden-palette"; export const RULE_VERSION = "1"; export function ruleTailwindForbiddenPalette(ix: FactIndex): Finding[] { const deny = ix.policy.tailwindPaletteDeny(); const allow = ix.policy.tailwindPaletteAllow(); if (!deny && !allow) return []; const resolvedByKey = indexResolvedTailwindTokens(ix); const findings: Finding[] = []; for (const klass of ix.byKind("tailwind_class")) { if (klass.value.kind !== "token") continue; const token = klass.value.token; if (!TAILWIND_COLOR_UTILITIES.has(klass.utility)) continue; if (UNIVERSAL_TAILWIND_COLOR_TOKENS.has(token)) continue; const resolved = resolvedForClass(resolvedByKey, klass); if (!resolved || resolved.resolved.kind !== "color") continue; if (resolved.resolved.source === "theme-css") continue; const deniedPattern = deny ? matchPattern(token, deny.patterns) : null; const allowedPattern = allow ? matchPattern(token, allow.patterns) : null; const allowViolation = allow && !allowedPattern; if (!deniedPattern && !allowViolation) continue; const policy = deniedPattern ? deny! : allow!; const matchedPattern = deniedPattern ?? allowedPattern ?? null; const node = readUsageNode(ix, klass.nodeId); const evidenceIds = node ? [node.id, klass.id, resolved.id, policy.id] : [klass.id, resolved.id, policy.id]; findings.push( makeFinding({ ruleId: RULE_ID, ruleVersion: RULE_VERSION, severity: policy.severity, message: messageFor(klass, token, deniedPattern, allow?.patterns ?? []), 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, matchedPattern, }, fix: buildFix(klass), attributes: { rawClass: klass.raw, utility: klass.utility, token, matchedPattern, allowedPatterns: allow?.patterns, source: resolved.resolved.source, }, }) ); } return findings; } function messageFor( klass: TailwindClassFact, token: string, deniedPattern: string | null, allowedPatterns: readonly string[] ): string { if (deniedPattern) { return `\`${klass.raw}\` uses the \`${token}\` palette token, which is forbidden by policy \`deny: ${deniedPattern}\`.`; } return `\`${klass.raw}\` uses the \`${token}\` palette token, which is outside the allowed Tailwind palette patterns [${allowedPatterns.join(", ")}].`; } function buildFix(klass: TailwindClassFact): FindingReplaceClassTokenFix { return { kind: "replaceClassToken", title: `Review ${klass.raw}`, from: klass.raw, to: null, utility: klass.utility, deterministic: false, }; }