diff options
| author | Fuwn <[email protected]> | 2026-02-07 02:00:59 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-07 02:00:59 -0800 |
| commit | f93bad7da47093a12116ff0f390abb548289b600 (patch) | |
| tree | e2a9debcca3473af8f293c3215704549e5bde17f /apps/web/eslint-rules | |
| parent | style: format Go worker with iku (diff) | |
| download | asa.news-f93bad7da47093a12116ff0f390abb548289b600.tar.xz asa.news-f93bad7da47093a12116ff0f390abb548289b600.zip | |
style: lowercase all user-facing strings and add custom eslint rule
Comprehensive sweep of all user-facing text to enforce lowercase
convention, including acronyms (api, rest, http, opml, json, totp,
mfa, qr, hmac). Added asa-lowercase/lowercase-strings eslint rule
that reports uppercase in notify() calls, error messages, jsx text,
and checked attributes (placeholder, alt, title).
Diffstat (limited to 'apps/web/eslint-rules')
| -rw-r--r-- | apps/web/eslint-rules/lowercase-strings.mjs | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/apps/web/eslint-rules/lowercase-strings.mjs b/apps/web/eslint-rules/lowercase-strings.mjs new file mode 100644 index 0000000..096d236 --- /dev/null +++ b/apps/web/eslint-rules/lowercase-strings.mjs @@ -0,0 +1,262 @@ +const UPPERCASE_PATTERN = /\b[A-Z]/ + +const IGNORED_JSX_PARENTS = new Set(["code", "Code"]) + +const CHECKED_JSX_ATTRIBUTES = new Set(["placeholder", "alt", "title"]) + +const NOTIFY_NAMES = new Set(["notify"]) + +function isInsideCodeElement(node) { + let current = node.parent + while (current) { + if ( + current.type === "JSXElement" && + current.openingElement?.name?.name && + IGNORED_JSX_PARENTS.has(current.openingElement.name.name) + ) { + return true + } + current = current.parent + } + return false +} + +function isClassName(node) { + return ( + node.parent?.type === "JSXAttribute" && + node.parent.name?.name === "className" + ) +} + +function isHrefOrSrc(node) { + return ( + node.parent?.type === "JSXAttribute" && + (node.parent.name?.name === "href" || node.parent.name?.name === "src") + ) +} + +function isFetchMethod(node) { + if (node.parent?.type !== "Property") return false + const key = node.parent.key + return key?.type === "Identifier" && key.name === "method" +} + +function isObjectKey(node) { + return ( + node.parent?.type === "Property" && + node.parent.key === node + ) +} + +function isImportSource(node) { + return ( + node.parent?.type === "ImportDeclaration" && + node.parent.source === node + ) +} + +function isTypeContext(node) { + return ( + node.parent?.type === "TSTypeReference" || + node.parent?.type === "TSLiteralType" || + node.parent?.type === "TSPropertySignature" + ) +} + +function isQueryKey(node) { + return ( + node.parent?.type === "ArrayExpression" && + node.parent.parent?.type === "Property" && + node.parent.parent.key?.name === "queryKey" + ) +} + +function isContentTypeHeader(node) { + if (node.parent?.type !== "Property") return false + const key = node.parent.key + return key?.type === "Literal" && key.value === "Content-Type" +} + +function isHeaderValue(node) { + if (node.parent?.type !== "Property") return false + const key = node.parent.key + if (key?.type === "Literal") { + const keyValue = String(key.value).toLowerCase() + return keyValue === "content-type" || keyValue === "authorization" + } + return false +} + +function isComparisonTarget(node) { + return ( + node.parent?.type === "BinaryExpression" && + (node.parent.operator === "===" || node.parent.operator === "!==") && + node.parent.right === node + ) +} + +function looksLikeCode(value) { + if (value.startsWith("Bearer ")) return true + if (value.startsWith("Authorization:")) return true + if (/^https?:\/\//.test(value)) return true + if (value.includes("::")) return true + return false +} + +const rule = { + meta: { + type: "suggestion", + docs: { + description: "enforce lowercase user-facing strings", + }, + messages: { + uppercaseString: + "user-facing string contains uppercase: \"{{value}}\". all user-facing text must be lowercase.", + uppercaseJsxText: + "jsx text contains uppercase: \"{{value}}\". all user-facing text must be lowercase.", + }, + schema: [], + }, + create(context) { + function checkStringLiteral(node, value, messageId) { + if (!UPPERCASE_PATTERN.test(value)) return + if (looksLikeCode(value)) return + + context.report({ + node, + messageId, + data: { + value: value.length > 60 ? value.slice(0, 57) + "..." : value, + }, + }) + } + + return { + JSXText(node) { + const trimmed = node.value.trim() + if (!trimmed) return + if (!UPPERCASE_PATTERN.test(trimmed)) return + if (isInsideCodeElement(node)) return + + context.report({ + node, + messageId: "uppercaseJsxText", + data: { + value: + trimmed.length > 60 ? trimmed.slice(0, 57) + "..." : trimmed, + }, + }) + }, + + Literal(node) { + if (typeof node.value !== "string") return + if (!UPPERCASE_PATTERN.test(node.value)) return + + if (isInsideCodeElement(node)) return + if (isClassName(node)) return + if (isHrefOrSrc(node)) return + if (isFetchMethod(node)) return + if (isObjectKey(node)) return + if (isImportSource(node)) return + if (isTypeContext(node)) return + if (isQueryKey(node)) return + if (isContentTypeHeader(node)) return + if (isHeaderValue(node)) return + if (isComparisonTarget(node)) return + + if ( + node.parent?.type === "CallExpression" && + node.parent.callee?.type === "Identifier" && + NOTIFY_NAMES.has(node.parent.callee.name) + ) { + checkStringLiteral(node, node.value, "uppercaseString") + return + } + + if ( + node.parent?.type === "NewExpression" && + node.parent.callee?.type === "Identifier" && + node.parent.callee.name === "Error" + ) { + checkStringLiteral(node, node.value, "uppercaseString") + return + } + + if ( + node.parent?.type === "JSXAttribute" && + CHECKED_JSX_ATTRIBUTES.has(node.parent.name?.name) + ) { + checkStringLiteral(node, node.value, "uppercaseString") + return + } + + if (node.parent?.type === "JSXExpressionContainer") { + if (isInsideCodeElement(node.parent)) return + checkStringLiteral(node, node.value, "uppercaseString") + return + } + + if ( + node.parent?.type === "Property" && + node.parent.value === node && + node.parent.key?.type === "Identifier" + ) { + const keyName = node.parent.key.name + if ( + keyName === "label" || + keyName === "description" || + keyName === "title" || + keyName === "error" + ) { + checkStringLiteral(node, node.value, "uppercaseString") + return + } + } + }, + + TemplateLiteral(node) { + const isNotifyArg = + node.parent?.type === "CallExpression" && + node.parent.callee?.type === "Identifier" && + NOTIFY_NAMES.has(node.parent.callee.name) + + const isErrorArg = + node.parent?.type === "NewExpression" && + node.parent.callee?.type === "Identifier" && + node.parent.callee.name === "Error" + + const isJsxExpression = + node.parent?.type === "JSXExpressionContainer" + + if (!isNotifyArg && !isErrorArg && !isJsxExpression) return + if (isInsideCodeElement(node)) return + + for (const quasi of node.quasis) { + const raw = quasi.value.raw + if (UPPERCASE_PATTERN.test(raw) && !looksLikeCode(raw)) { + context.report({ + node: quasi, + messageId: "uppercaseString", + data: { + value: + raw.length > 60 ? raw.slice(0, 57) + "..." : raw, + }, + }) + } + } + }, + } + }, +} + +const plugin = { + meta: { + name: "asa-lowercase", + version: "1.0.0", + }, + rules: { + "lowercase-strings": rule, + }, +} + +export default plugin |