Skip to content
Source
Sign inSign up

max-settings-description-length.ts

63 lines2.3 KB

1import { defineRule, type ESTree } from "@oxlint/plugins";
2​
3const MAX_LENGTH = 200;
4​
5const MESSAGE = (length: number) =>
6 `SettingsRow description is ${length} characters; keep it at ${MAX_LENGTH} or fewer so it stays two lines next to a control. Shorten the copy: keep each fact a user needs and drop restatements.`;
7​
8type Node = ESTree.Node | ESTree.JSXElement["children"][number] | null | undefined;
9​
10const staticText = (node: Node): string => {
11 if (!node) return "";
12 switch (node.type) {
13 case "Literal":
14 return typeof node.value === "string" ? node.value : "";
15 case "TemplateLiteral":
16 return node.quasis.map((quasi) => quasi.value.cooked ?? "").join("");
17 case "JSXText":
18 return node.value
19 .split(/\r\n|\n|\r/u)
20 .map((line, index, lines) => {
21 const start = index > 0 ? line.trimStart() : line;
22 return index < lines.length - 1 ? start.trimEnd() : start;
23 })
24 .filter(Boolean)
25 .join(" ");
26 case "JSXExpressionContainer":
27 return node.expression.type === "JSXEmptyExpression" ? "" : staticText(node.expression);
28 case "JSXElement":
29 case "JSXFragment":
30 return node.children.map(staticText).join("");
31 case "ConditionalExpression": {
32 const consequent = staticText(node.consequent);
33 const alternate = staticText(node.alternate);
34 return consequent.length >= alternate.length ? consequent : alternate;
35 }
36 default:
37 return "";
38 }
39};
40​
41export default defineRule({
42 meta: {
43 type: "suggestion",
44 docs: {
45 description: `Limit SettingsRow descriptions to ${MAX_LENGTH} characters of static text. The column is at most 672px, about 110 characters per line next to a dropdown, so ${MAX_LENGTH} stays two lines.`,
46 },
47 },
48 create(context) {
49 return {
50 JSXOpeningElement(node) {
51 if (node.name.type !== "JSXIdentifier" || node.name.name !== "SettingsRow") return;
52 for (const attribute of node.attributes) {
53 if (attribute.type !== "JSXAttribute") continue;
54 if (attribute.name.type !== "JSXIdentifier" || attribute.name.name !== "description") {
55 continue;
56 }
57 const length = staticText(attribute.value).replace(/\s+/gu, " ").trim().length;
58 if (length > MAX_LENGTH) context.report({ node: attribute, message: MESSAGE(length) });
59 }
60 },
61 };
62 },
63});