cloudflare/kumo

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
27f04eb919d6e913dbf53f15c37a4251611a568c

Branches

Tags

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

Clone

HTTPS

Download ZIP

lint/no-tailwind-dark-variant.js

149lines · 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 return str.includes("dark:");
81}
82
83function isInsideJsxAttribute(node) {
84 let current = node.parent;
85 while (current) {
86 if (current.type === "JSXAttribute") return true;
87 current = current.parent;
88 }
89 return false;
90}
91
92export const noTailwindDarkVariantRule = defineRule({
93 meta: {
94 type: "problem",
95 docs: {
96 description: "Disallow Tailwind dark: variant usage in class names",
97 },
98 messages: {
99 [RULE_NAME]:
100 "Avoid using Tailwind's dark: variant. Use the design system token or component API for dark mode handling.",
101 },
102 schema: [],
103 },
104 defaultOptions: [],
105 createOnce(context) {
106 function reportIfDark(node, collected) {
107 for (const s of collected) {
108 if (hasDarkVariant(s)) {
109 context.report({ node, messageId: RULE_NAME });
110 return;
111 }
112 }
113 }
114
115 return {
116 JSXAttribute(node) {
117 const name =
118 node.name.type === "JSXIdentifier" ? node.name.name : undefined;
119 if (name !== "className" && name !== "class") return;
120
121 if (node.value) {
122 const strings = extractStrings(node.value);
123 reportIfDark(node, strings);
124 }
125 },
126 Literal(node) {
127 if (
128 typeof node.value !== "string" ||
129 !hasDarkVariant(node.value) ||
130 isInsideJsxAttribute(node)
131 ) {
132 return;
133 }
134
135 context.report({ node, messageId: RULE_NAME });
136 },
137 TemplateLiteral(node) {
138 if (isInsideJsxAttribute(node)) {
139 return;
140 }
141
142 const strings = extractStrings(node);
143 if (strings.some(hasDarkVariant)) {
144 context.report({ node, messageId: RULE_NAME });
145 }
146 },
147 };
148 },
149});
150