import type { FactIndex } from "../facts/index.js"; import { makeFinding } from "./finding.js"; import type { Finding } from "./types.js"; import { cssVariableNames } from "./utils.js"; export const RULE_ID = "tokens/require-dual-fallback"; export const RULE_VERSION = "1"; const CSS_VAR_WITH_OPTIONAL_FALLBACK = /var\(\s*(--[A-Za-z0-9_-]+)(\s*,\s*[^)]*)?\)/g; /** * The dual-fallback convention — `var(--token, $token)` — is only meaningful in * a Sass pipeline, where the `$token` half resolves at build time. In plain CSS * (`.css`/`.module.css`), inline styles, or CSS-in-JS, a `$token` literal is * invalid and would silently break the declaration, so the rule (and its * autofix) must never touch those files. */ const SASS_FILE = /\.(scss|sass)$/i; export function ruleTokensRequireDualFallback(ix: FactIndex): Finding[] { const policy = ix.policy.ruleConfig(RULE_ID); if (!policy?.enabled) return []; const knownTokenVars = cssVariableNames(ix.tokens.list()); const findings: Finding[] = []; for (const decl of ix.byKind("style_declaration")) { if (!SASS_FILE.test(decl.file)) continue; CSS_VAR_WITH_OPTIONAL_FALLBACK.lastIndex = 0; let match: RegExpExecArray | null; while ((match = CSS_VAR_WITH_OPTIONAL_FALLBACK.exec(decl.value)) !== null) { const tokenName = match[1]; const hasFallback = match[2] !== undefined; if (hasFallback) continue; if (!knownTokenVars.has(tokenName) && !tokenName.startsWith("--fui-")) continue; const rawValue = match[0]; const scssVar = tokenName.replace(/^--/, "$"); const replacement = `var(${tokenName}, ${scssVar})`; const nextValue = decl.value.replace(rawValue, replacement); findings.push( makeFinding({ ruleId: RULE_ID, ruleVersion: RULE_VERSION, severity: policy.severity ?? "warn", message: `var(${tokenName}) is missing an SCSS fallback. Use var(${tokenName}, ${scssVar}).`, location: decl.location, evidence: ix.evidence([decl.id, policy.id]), fingerprintIdentity: { file: decl.file, selector: decl.selector, declarationPath: decl.declarationPath, property: decl.property, tokenName, }, fix: { kind: "replaceStyleValue", title: `Add fallback for ${tokenName}`, property: decl.property, value: nextValue, deterministic: true, }, attributes: { property: decl.property, rawValue: decl.value, tokenName, suggestedValue: nextValue, source: "css", }, }), ); } } return findings; }