summaryrefslogtreecommitdiff
path: root/apps/web/eslint-rules
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-07 02:00:59 -0800
committerFuwn <[email protected]>2026-02-07 02:00:59 -0800
commitf93bad7da47093a12116ff0f390abb548289b600 (patch)
treee2a9debcca3473af8f293c3215704549e5bde17f /apps/web/eslint-rules
parentstyle: format Go worker with iku (diff)
downloadasa.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.mjs262
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