/
githubmirror
/
eslint-plugin
Обзор
Документация
Войти
/
githubmirror
/
eslint-plugin
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
lib/rules/ui/sentenceCaseLocaleModule.ts
185 строк
8 KB
saberzero1
refactor: replace all hardcoded URLs with docsUrl()
10 июн 2026, 16:14
10 июн 2026, 16:14
396eae1
Код
Авторство
О чём код?
import { TSESTree, TSESLint } from "@typescript-eslint/utils"; import { docsUrl, ruleCreator } from "../../ruleCreator.js"; import { createSentenceCaseReporter, getContextFilename, isEnglishLocalePath, resolveSentenceCaseConfig, SentenceCaseRuleOptions, unwrapExpression, } from "./sentenceCaseUtil.js"; type MessageId = "useSentenceCase"; // Default configuration with no custom options specified const defaultOptions: SentenceCaseRuleOptions = [{}] as const; // Checks if the current file is an English locale TypeScript or JavaScript module function isEnglishLocaleModule( context: TSESLint.RuleContext<MessageId, SentenceCaseRuleOptions>, ): boolean { const file = getContextFilename(context); return file != null && isEnglishLocalePath(file, ["ts", "js", "cjs", "mjs"]); } // Recursively collects all string literals from nested objects and arrays function collectStringLiterals( root: TSESTree.Expression, cb: (node: TSESTree.Node, value: string) => void, ) { const stack: TSESTree.Node[] = [root]; while (stack.length > 0) { const popped = stack.pop(); if (!popped) continue; const current = unwrapExpression(popped); if (!current) continue; if (current.type === TSESTree.AST_NODE_TYPES.Literal) { if (typeof current.value === "string") cb(current, current.value); continue; } if (current.type === TSESTree.AST_NODE_TYPES.TemplateLiteral) { if (current.expressions.length === 0) { const raw = current.quasis.map((q) => q.value.raw).join(""); cb(current, raw); } continue; } if (current.type === TSESTree.AST_NODE_TYPES.ObjectExpression) { for (const property of current.properties) { if (property.type === TSESTree.AST_NODE_TYPES.Property) { const value = property.value; stack.push(value); } else if (property.type === TSESTree.AST_NODE_TYPES.SpreadElement) { stack.push(property.argument); } } continue; } if (current.type === TSESTree.AST_NODE_TYPES.ArrayExpression) { for (const element of current.elements) { if (!element || element.type === TSESTree.AST_NODE_TYPES.SpreadElement) continue; stack.push(element); } } } } export default ruleCreator({ meta: { type: "suggestion" as const, docs: { description: "Enforce sentence case for English TS/JS locale module strings", url: docsUrl("sentence-case-locale-module", "ui"), }, fixable: "code", hasSuggestions: false, schema: [ { type: "object", properties: { mode: { type: "string", enum: ["loose", "strict"], description: "Controls sentence case strictness; loose preserves CamelCase tokens while strict lowercases them.", }, brands: { type: "array", items: { type: "string" }, description: "Brand names to preserve with their canonical casing.", }, acronyms: { type: "array", items: { type: "string" }, description: "Acronyms that should remain uppercase.", }, ignoreWords: { type: "array", items: { type: "string" }, description: "Words to skip when applying sentence case.", }, ignoreRegex: { type: "array", items: { type: "string" }, description: "Regex patterns for strings to ignore entirely.", }, allowAutoFix: { type: "boolean", description: "Whether to enable auto-fixes for sentence case violations.", }, enforceCamelCaseLower: { type: "boolean", description: "Whether to force CamelCase/PascalCase tokens to lowercase.", }, }, additionalProperties: false, }, ], messages: { useSentenceCase: "Use sentence case for UI text.", }, }, defaultOptions, create(context: TSESLint.RuleContext<MessageId, SentenceCaseRuleOptions>) { if (!isEnglishLocaleModule(context)) return {}; const { evaluatorOptions, allowAutoFix } = resolveSentenceCaseConfig(context.options); const varObjects = new Map<string, TSESTree.ObjectExpression>(); const reportIfNeeded = createSentenceCaseReporter({ context, evaluatorOptions, allowAutoFix, messageId: "useSentenceCase" }); return { Program(program: TSESTree.Program) { // Track top-level variable declarations that are objects for (const stmt of program.body) { if (stmt.type === TSESTree.AST_NODE_TYPES.VariableDeclaration) { for (const d of stmt.declarations) { if ( d.id.type === TSESTree.AST_NODE_TYPES.Identifier && d.init ) { const unwrapped = unwrapExpression(d.init); if (unwrapped && unwrapped.type === TSESTree.AST_NODE_TYPES.ObjectExpression) { varObjects.set(d.id.name, unwrapped); } } } } } }, ExportDefaultDeclaration(node: TSESTree.ExportDefaultDeclaration) { if (!node.declaration) return; if (node.declaration.type === TSESTree.AST_NODE_TYPES.ObjectExpression) { collectStringLiterals(node.declaration, (n, v) => reportIfNeeded(n, v)); } else if (node.declaration.type === TSESTree.AST_NODE_TYPES.Identifier) { const obj = varObjects.get(node.declaration.name); if (obj) collectStringLiterals(obj, (n, v) => reportIfNeeded(n, v)); } }, ExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration) { if (node.declaration && node.declaration.type === TSESTree.AST_NODE_TYPES.VariableDeclaration) { for (const decl of node.declaration.declarations) { if ( decl.init ) { const unwrapped = unwrapExpression(decl.init); if (unwrapped && unwrapped.type === TSESTree.AST_NODE_TYPES.ObjectExpression) { collectStringLiterals(unwrapped, (n, v) => reportIfNeeded(n, v)); } } } } else if (!node.source && node.specifiers && node.specifiers.length > 0) { for (const spec of node.specifiers) { if (spec.type === TSESTree.AST_NODE_TYPES.ExportSpecifier && spec.local.type === TSESTree.AST_NODE_TYPES.Identifier) { const obj = varObjects.get(spec.local.name); if (obj) collectStringLiterals(obj, (n, v) => reportIfNeeded(n, v)); } } } }, }; }, });