cloudflare/kumo

Public

mirrored fromhttps://github.com/cloudflare/kumoAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
9fbf3a830bde68cc1da790676dc967d1f731fe04

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

lint/no-tailwind-dark-variant.js

151lines · modecode

1import { defineRule } from "oxlint";
2
3const RULE_NAME = "no-tailwind-dark-variant";
4
5function extractStrings(node) {
6 if (!node) return [];
7 const out = [];
8
9 switch (node.type) {
10 case "Literal": {
11 if (typeof node.value === "string") out.push(node.value);
12 break;
13 }
14 case "TemplateLiteral": {
15 for (const q of node.quasis) {
16 if (typeof q.value.cooked === "string") out.push(q.value.cooked);
17 }
18 break;
19 }
20 case "BinaryExpression": {
21 if (node.operator === "+") {
22 out.push(...extractStrings(node.left));
23 out.push(...extractStrings(node.right));
24 }
25 break;
26 }
27 case "ArrayExpression": {
28 for (const el of node.elements) {
29 if (el) {
30 out.push(...extractStrings(el));
31 }
32 }
33 break;
34 }
35 case "ObjectExpression": {
36 for (const prop of node.properties) {
37 if (prop.type === "Property") {
38 out.push(...extractStrings(prop.key));
39 out.push(...extractStrings(prop.value));
40 }
41 }
42 break;
43 }
44 case "CallExpression": {
45 for (const arg of node.arguments) {
46 if (arg.type === "SpreadElement") continue;
47 out.push(...extractStrings(arg));
48 }
49 break;
50 }
51 case "ConditionalExpression": {
52 out.push(...extractStrings(node.consequent));
53 out.push(...extractStrings(node.alternate));
54 out.push(...extractStrings(node.test));
55 break;
56 }
57 case "UnaryExpression": {
58 out.push(...extractStrings(node.argument));
59 break;
60 }
61 case "LogicalExpression": {
62 out.push(...extractStrings(node.left));
63 out.push(...extractStrings(node.right));
64 break;
65 }
66 case "JSXText": {
67 out.push(node.value);
68 break;
69 }
70 case "JSXExpressionContainer": {
71 out.push(...extractStrings(node.expression));
72 break;
73 }
74 }
75
76 return out;
77}
78
79function hasDarkVariant(str) {
80 // Match dark: followed by a Tailwind utility pattern (e.g., dark:bg-blue-500, dark:text-white)
81 // This avoids false positives for object keys like { dark: "vesper" } or { light: ..., dark: ... }
82 return /\bdark:[a-z]+[-\w]*/.test(str);
83}
84
85function isInsideJsxAttribute(node) {
86 let current = node.parent;
87 while (current) {
88 if (current.type === "JSXAttribute") return true;
89 current = current.parent;
90 }
91 return false;
92}
93
94export const noTailwindDarkVariantRule = defineRule({
95 meta: {
96 type: "problem",
97 docs: {
98 description: "Disallow Tailwind dark: variant usage in class names",
99 },
100 messages: {
101 [RULE_NAME]:
102 "Avoid using Tailwind's dark: variant. Use the design system token or component API for dark mode handling.",
103 },
104 schema: [],
105 },
106 defaultOptions: [],
107 createOnce(context) {
108 function reportIfDark(node, collected) {
109 for (const s of collected) {
110 if (hasDarkVariant(s)) {
111 context.report({ node, messageId: RULE_NAME });
112 return;
113 }
114 }
115 }
116
117 return {
118 JSXAttribute(node) {
119 const name =
120 node.name.type === "JSXIdentifier" ? node.name.name : undefined;
121 if (name !== "className" && name !== "class") return;
122
123 if (node.value) {
124 const strings = extractStrings(node.value);
125 reportIfDark(node, strings);
126 }
127 },
128 Literal(node) {
129 if (
130 typeof node.value !== "string" ||
131 !hasDarkVariant(node.value) ||
132 isInsideJsxAttribute(node)
133 ) {
134 return;
135 }
136
137 context.report({ node, messageId: RULE_NAME });
138 },
139 TemplateLiteral(node) {
140 if (isInsideJsxAttribute(node)) {
141 return;
142 }
143
144 const strings = extractStrings(node);
145 if (strings.some(hasDarkVariant)) {
146 context.report({ node, messageId: RULE_NAME });
147 }
148 },
149 };
150 },
151});
152