Skip to content
Source
Sign inSign up

no-sparkles-icon.ts

50 lines2.0 KB

1import { defineRule } from "@oxlint/plugins";
2​
3const SPARKLE_NAME = /sparkle/iu;
4// SF Symbol (`sparkles`, `sparkles.square.filled.on.square`, `wand.and.sparkles`) and
5// Material (`auto_awesome`) names reach native icons as plain strings.
6const SPARKLE_SYMBOL_NAME = /^(?:(?:[\w]+\.)*sparkles?(?:\.[\w.]+)?|auto_awesome(?:_\w+)?)$/u;
7const TABLER_SOURCE = /^@tabler\/icons(?:-react|-react-native)?(?:\/|$)/u;
8​
9const MESSAGE =
10 "Sparkles says 'AI', not what the control does. Pick an icon for the action itself, such as Bot for agent work or ShieldCheck for Auto mode.";
11​
12export default defineRule({
13 meta: {
14 type: "problem",
15 docs: {
16 description:
17 "Disallow sparkles icons; they label a control as AI instead of saying what it does.",
18 },
19 },
20 create(context) {
21 return {
22 ImportDeclaration(node) {
23 const source = node.source.value;
24 if (source === "lucide" || source === "lucide-react" || TABLER_SOURCE.test(source)) {
25 if (TABLER_SOURCE.test(source) && SPARKLE_NAME.test(source.split("/").at(-1) ?? "")) {
26 context.report({ node: node.source, message: MESSAGE });
27 return;
28 }
29 for (const specifier of node.specifiers) {
30 if (specifier.type !== "ImportSpecifier") continue;
31 const imported =
32 specifier.imported.type === "Identifier"
33 ? specifier.imported.name
34 : specifier.imported.value;
35 if (SPARKLE_NAME.test(imported)) context.report({ node: specifier, message: MESSAGE });
36 }
37 }
38 },
39 Literal(node) {
40 if (typeof node.value !== "string" || !SPARKLE_SYMBOL_NAME.test(node.value)) return;
41 if (node.parent.type === "ImportDeclaration") return;
42 context.report({ node, message: MESSAGE });
43 },
44 TemplateElement(node) {
45 if (!SPARKLE_SYMBOL_NAME.test(node.value.cooked ?? "")) return;
46 context.report({ node, message: MESSAGE });
47 },
48 };
49 },
50});