UNPKG

eslint-plugin-lingui

Version:
567 lines 25.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = exports.name = void 0; const utils_1 = require("@typescript-eslint/utils"); const helpers_1 = require("../helpers"); const create_rule_1 = require("../create-rule"); const micromatch = __importStar(require("micromatch")); const typescript_1 = require("typescript"); const MatcherSchema = { oneOf: [ { type: 'string', }, { type: 'object', properties: { regex: { type: 'object', properties: { pattern: { type: 'string', }, flags: { type: 'string', }, }, required: ['pattern'], additionalProperties: false, }, }, required: ['regex'], additionalProperties: false, }, ], }; function createMatcher(patterns) { const _patterns = patterns.map((item) => typeof item === 'string' ? item : new RegExp(item.regex.pattern, item.regex.flags)); return (str) => { return _patterns.some((pattern) => { if (typeof pattern === 'string') { return pattern === str; } return pattern.test(str); }); }; } function isAcceptableExpression(node) { switch (node.type) { case utils_1.TSESTree.AST_NODE_TYPES.LogicalExpression: case utils_1.TSESTree.AST_NODE_TYPES.BinaryExpression: case utils_1.TSESTree.AST_NODE_TYPES.ConditionalExpression: case utils_1.TSESTree.AST_NODE_TYPES.UnaryExpression: case utils_1.TSESTree.AST_NODE_TYPES.TSAsExpression: return true; default: return false; } } function isAssignedToIgnoredVariable(node, isIgnoredName) { let current = node; let parent = current.parent; while (parent && isAcceptableExpression(parent)) { current = parent; parent = parent.parent; } if (!parent) return false; if (parent.type === utils_1.TSESTree.AST_NODE_TYPES.VariableDeclarator && parent.init === current) { const variableDeclarator = parent; if ((0, helpers_1.isIdentifier)(variableDeclarator.id) && isIgnoredName(variableDeclarator.id.name)) { return true; } } else if (parent.type === utils_1.TSESTree.AST_NODE_TYPES.AssignmentExpression && parent.right === current) { const assignmentExpression = parent; if ((0, helpers_1.isIdentifier)(assignmentExpression.left) && isIgnoredName(assignmentExpression.left.name)) { return true; } } return false; } function isAsConstAssertion(node) { const parent = node.parent; if ((parent === null || parent === void 0 ? void 0 : parent.type) === utils_1.TSESTree.AST_NODE_TYPES.TSAsExpression) { const typeAnnotation = parent.typeAnnotation; return (typeAnnotation.type === utils_1.TSESTree.AST_NODE_TYPES.TSTypeReference && (0, helpers_1.isIdentifier)(typeAnnotation.typeName) && typeAnnotation.typeName.name === 'const'); } return false; } function isStringLiteralFromUnionType(node, tsService) { var _a; try { const checker = tsService.program.getTypeChecker(); const nodeTsNode = tsService.esTreeNodeToTSNodeMap.get(node); const isStringLiteralType = (type) => { if (type.flags & typescript_1.TypeFlags.Union) { const unionType = type; return unionType.types.every((t) => t.flags & typescript_1.TypeFlags.StringLiteral); } return !!(type.flags & typescript_1.TypeFlags.StringLiteral); }; // For arguments, check parameter type first if (((_a = node.parent) === null || _a === void 0 ? void 0 : _a.type) === utils_1.TSESTree.AST_NODE_TYPES.CallExpression) { const callNode = node.parent; const tsCallNode = tsService.esTreeNodeToTSNodeMap.get(callNode); const args = callNode.arguments; const argIndex = args.findIndex((arg) => arg === node); const signature = checker.getResolvedSignature(tsCallNode); // Only proceed if we have a valid signature and the argument index is valid if ((signature === null || signature === void 0 ? void 0 : signature.parameters) && argIndex >= 0 && argIndex < signature.parameters.length) { const param = signature.parameters[argIndex]; const paramType = checker.getTypeAtLocation(param.valueDeclaration); // For function parameters, we ONLY accept union types of string literals if (paramType.flags & typescript_1.TypeFlags.Union) { const unionType = paramType; return unionType.types.every((t) => t.flags & typescript_1.TypeFlags.StringLiteral); } } // If we're here, it's a function call argument that didn't match our criteria return false; } // Try to get the contextual type first const contextualType = checker.getContextualType(nodeTsNode); if (contextualType && isStringLiteralType(contextualType)) { return true; } } catch (error) { } /* istanbul ignore next */ return false; } exports.name = 'no-unlocalized-strings'; exports.rule = (0, create_rule_1.createRule)({ name: exports.name, meta: { docs: { description: 'Ensures all strings, templates, and JSX text are properly wrapped with `<Trans>`, `t`, or `msg` for translation.', recommended: 'error', }, messages: { default: 'String not marked for translation. Wrap it with t``, <Trans>, or msg``.', forJsxText: 'String not marked for translation. Wrap it with <Trans>.', forAttribute: 'Attribute not marked for translation. \n Wrap it with t`` from useLingui() macro hook.', }, schema: [ { type: 'object', properties: { ignore: { type: 'array', items: { type: 'string', }, }, ignoreNames: { type: 'array', items: MatcherSchema, }, ignoreFunctions: { type: 'array', items: { type: 'string', }, }, ignoreMethodsOnTypes: { type: 'array', items: { type: 'string', }, }, useTsTypes: { type: 'boolean', }, }, additionalProperties: false, }, ], type: 'problem', }, defaultOptions: [], create: function (context) { const { options: [option], } = context; let tsService; if (option === null || option === void 0 ? void 0 : option.useTsTypes) { tsService = utils_1.ESLintUtils.getParserServices(context, false); } const whitelists = [ // /^[^\p{L}]+$/u, ...((option === null || option === void 0 ? void 0 : option.ignore) || []).map((item) => new RegExp(item)), ]; const calleeWhitelists = [ // lingui callee 'i18n._', 't', 'plural', 'select', 'selectOrdinal', 'msg', ...((option === null || option === void 0 ? void 0 : option.ignoreFunctions) || []), ].map((pattern) => micromatch.matcher(pattern)); const isCalleeWhitelisted = (callee) => calleeWhitelists.some((matcher) => matcher(callee)); //---------------------------------------------------------------------- // Helpers //---------------------------------------------------------------------- function isTextWhiteListed(str) { return whitelists.some((item) => item.test(str)); } function isValidFunctionCall({ callee, }) { var _a; switch (callee.type) { case utils_1.TSESTree.AST_NODE_TYPES.MemberExpression: { if (isCalleeWhitelisted((0, helpers_1.buildCalleePath)(callee))) { return true; } // use power of TS compiler to exclude call on specific types, such Map.get, Set.get and so on if (tsService && (0, helpers_1.isIdentifier)(callee.property)) { for (const ignore of ignoredMethodsOnTypes) { const [type, method] = ignore.split('.'); if (method === callee.property.name) { const typeObj = tsService.getTypeAtLocation(callee.object); if (type === ((_a = typeObj === null || typeObj === void 0 ? void 0 : typeObj.getSymbol()) === null || _a === void 0 ? void 0 : _a.name)) { return true; } } } } return false; } case utils_1.TSESTree.AST_NODE_TYPES.Identifier: { return isCalleeWhitelisted(callee.name); } case utils_1.TSESTree.AST_NODE_TYPES.CallExpression: { return (((0, helpers_1.isMemberExpression)(callee.callee) || (0, helpers_1.isIdentifier)(callee.callee)) && isValidFunctionCall(callee)); } /* istanbul ignore next */ default: return false; } } /** * Helper function to determine if a node is inside an ignored property. */ function isInsideIgnoredProperty(node) { let parent = node.parent; while (parent) { if (parent.type === utils_1.TSESTree.AST_NODE_TYPES.Property) { const key = parent.key; if (((0, helpers_1.isIdentifier)(key) && isIgnoredName(key.name)) || (((0, helpers_1.isLiteral)(key) || (0, helpers_1.isTemplateLiteral)(key)) && isIgnoredName((0, helpers_1.getText)(key)))) { return true; } } parent = parent.parent; } return false; } const ignoredJSXSymbols = ['&larr;', '&nbsp;', '&middot;']; const ignoredMethodsOnTypes = (option === null || option === void 0 ? void 0 : option.ignoreMethodsOnTypes) || []; //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- const visited = new WeakSet(); function isIgnoredSymbol(str) { return ignoredJSXSymbols.some((name) => name === str); } const isIgnoredName = createMatcher((option === null || option === void 0 ? void 0 : option.ignoreNames) || []); function isStringLiteral(node) { if (!node) return false; switch (node.type) { case utils_1.TSESTree.AST_NODE_TYPES.Literal: return typeof node.value === 'string'; case utils_1.TSESTree.AST_NODE_TYPES.TemplateLiteral: return Boolean(node.quasis); case utils_1.TSESTree.AST_NODE_TYPES.JSXText: return true; /* istanbul ignore next */ default: return false; } } const getAttrName = (node) => { if (typeof node === 'string') { return node; } /* istanbul ignore next */ return node === null || node === void 0 ? void 0 : node.name; }; function isLiteralInsideJSX(node) { let parent = node.parent; let insideJSXExpression = false; while (parent) { if (parent.type === utils_1.TSESTree.AST_NODE_TYPES.JSXExpressionContainer) { insideJSXExpression = true; } if (parent.type === utils_1.TSESTree.AST_NODE_TYPES.JSXElement && insideJSXExpression) { return true; } parent = parent.parent; } /* istanbul ignore next */ return false; } function isInsideTypeContext(node) { let parent = node.parent; while (parent) { switch (parent.type) { case utils_1.TSESTree.AST_NODE_TYPES.TSPropertySignature: case utils_1.TSESTree.AST_NODE_TYPES.TSIndexSignature: case utils_1.TSESTree.AST_NODE_TYPES.TSTypeAnnotation: case utils_1.TSESTree.AST_NODE_TYPES.TSTypeLiteral: case utils_1.TSESTree.AST_NODE_TYPES.TSLiteralType: return true; } parent = parent.parent; } return false; } const processTextNode = (node) => { visited.add(node); const text = (0, helpers_1.getText)(node); if (!text || isIgnoredSymbol(text) || isTextWhiteListed(text)) { /* istanbul ignore next */ return; } // First, handle the JSXText case directly if (node.type === utils_1.TSESTree.AST_NODE_TYPES.JSXText) { context.report({ node, messageId: 'forJsxText' }); return; } // If it's not JSXText, it might be a Literal or TemplateLiteral. // Check if it's inside JSX. if (isLiteralInsideJSX(node)) { // If it's a Literal/TemplateLiteral inside a JSXExpressionContainer within JSXElement, // treat it like JSX text and report with `forJsxText`. context.report({ node, messageId: 'forJsxText' }); return; } /* istanbul ignore next */ // If neither JSXText nor a Literal inside JSX, fall back to default messageId. context.report({ node, messageId: 'default' }); }; const visitor = { 'ImportDeclaration Literal'(node) { // allow (import abc form 'abc') visited.add(node); }, 'ExportAllDeclaration Literal'(node) { // allow export * from 'mod' visited.add(node); }, 'ExportNamedDeclaration > Literal'(node) { // allow export { named } from 'mod' visited.add(node); }, [`:matches(${['Trans', 'Plural', 'Select', 'SelectOrdinal'].map((name) => `JSXElement[openingElement.name.name=${name}]`)}) :matches(TemplateLiteral, Literal, JSXText)`](node) { visited.add(node); }, 'JSXElement > JSXExpressionContainer > Literal'(node) { processTextNode(node); }, 'JSXElement > JSXExpressionContainer > TemplateLiteral'(node) { processTextNode(node); }, 'JSXAttribute :matches(Literal,TemplateLiteral)'(node) { var _a; const parent = (0, helpers_1.getNearestAncestor)(node, utils_1.TSESTree.AST_NODE_TYPES.JSXAttribute); const attrName = getAttrName((_a = parent === null || parent === void 0 ? void 0 : parent.name) === null || _a === void 0 ? void 0 : _a.name); // allow <MyComponent className="active" /> if (isIgnoredName(attrName)) { visited.add(node); return; } const jsxElement = (0, helpers_1.getNearestAncestor)(node, utils_1.TSESTree.AST_NODE_TYPES.JSXOpeningElement); const tagName = (0, helpers_1.getIdentifierName)(jsxElement === null || jsxElement === void 0 ? void 0 : jsxElement.name); const attributeNames = jsxElement === null || jsxElement === void 0 ? void 0 : jsxElement.attributes.map((attr) => (0, helpers_1.isJSXAttribute)(attr) && getAttrName(attr.name.name)); if ((0, helpers_1.isAllowedDOMAttr)(tagName, attrName, attributeNames)) { visited.add(node); return; } }, 'TSLiteralType Literal'(node) { // allow var a: Type['member']; visited.add(node); }, // ───────────────────────────────────────────────────────────────── 'ClassProperty > :matches(Literal,TemplateLiteral), PropertyDefinition > :matches(Literal,TemplateLiteral)'(node) { const { parent } = node; if ((parent.type === utils_1.TSESTree.AST_NODE_TYPES.Property || parent.type === utils_1.TSESTree.AST_NODE_TYPES.PropertyDefinition || //@ts-ignore parent.type === 'ClassProperty') && (0, helpers_1.isIdentifier)(parent.key) && isIgnoredName(parent.key.name)) { visited.add(node); } }, 'TSEnumMember > :matches(Literal,TemplateLiteral)'(node) { visited.add(node); }, 'VariableDeclarator > :matches(Literal,TemplateLiteral)'(node) { const parent = node.parent; // allow statements like const A_B = "test" if ((0, helpers_1.isIdentifier)(parent.id) && isIgnoredName(parent.id.name)) { visited.add(node); } }, 'MemberExpression[computed=true] > :matches(Literal,TemplateLiteral)'(node) { // obj["key with space"] visited.add(node); }, "AssignmentExpression[left.type='MemberExpression'] > Literal"(node) { // options: { ignoreProperties: ['myProperty'] } // MyComponent.myProperty = "Hello" const assignmentExp = node.parent; const memberExp = assignmentExp.left; if (!memberExp.computed && (0, helpers_1.isIdentifier)(memberExp.property) && isIgnoredName(memberExp.property.name)) { visited.add(node); } }, 'BinaryExpression > :matches(Literal,TemplateLiteral)'(node) { if (node.parent.type === utils_1.TSESTree.AST_NODE_TYPES.BinaryExpression) { const { parent: { operator }, } = node; // allow name === 'String' if (operator !== '+') { visited.add(node); } } }, 'CallExpression :matches(Literal,TemplateLiteral)'(node) { const parent = (0, helpers_1.getNearestAncestor)(node, utils_1.TSESTree.AST_NODE_TYPES.CallExpression); if (isValidFunctionCall(parent)) { visited.add(node); return; } }, 'NewExpression :matches(Literal,TemplateLiteral)'(node) { const parent = (0, helpers_1.getNearestAncestor)(node, utils_1.TSESTree.AST_NODE_TYPES.NewExpression); if (isValidFunctionCall(parent)) { visited.add(node); return; } }, 'SwitchCase > :matches(Literal,TemplateLiteral)'(node) { visited.add(node); }, 'TaggedTemplateExpression > TemplateLiteral'(node) { visited.add(node); }, 'TaggedTemplateExpression > TemplateLiteral :matches(Literal,TemplateLiteral)'(node) { visited.add(node); }, 'JSXText:exit'(node) { if (visited.has(node)) return; processTextNode(node); }, 'Literal:exit'(node) { if (visited.has(node)) return; const trimmed = `${node.value}`.trim(); if (!trimmed) return; if (isTextWhiteListed(trimmed)) { return; } if (isAsConstAssertion(node)) { return; } // Add check for object property key const parent = node.parent; if ((parent === null || parent === void 0 ? void 0 : parent.type) === utils_1.TSESTree.AST_NODE_TYPES.Property && parent.key === node) { return; } // More thorough type checking when enabled if ((option === null || option === void 0 ? void 0 : option.useTsTypes) && tsService) { try { if (isStringLiteralFromUnionType(node, tsService)) { return; } } catch (error) { // Ignore type checking errors } } if (isAssignedToIgnoredVariable(node, isIgnoredName)) { return; } if (isInsideIgnoredProperty(node)) { return; } if (isInsideTypeContext(node)) { return; } context.report({ node, messageId: 'default' }); }, 'TemplateLiteral:exit'(node) { if (visited.has(node)) return; const text = (0, helpers_1.getText)(node); if (!text || isTextWhiteListed(text)) return; if (isAsConstAssertion(node)) { return; } if (isAssignedToIgnoredVariable(node, isIgnoredName)) { return; // Do not report this template literal } if (isInsideIgnoredProperty(node)) { return; } context.report({ node, messageId: 'default' }); }, 'AssignmentPattern > :matches(Literal,TemplateLiteral)'(node) { const parent = node.parent; if ((0, helpers_1.isIdentifier)(parent.left) && isIgnoredName(parent.left.name)) { visited.add(node); } }, }; function wrapVisitor(visitor) { const newVisitor = {}; Object.keys(visitor).forEach((key) => { const old = visitor[key]; newVisitor[key] = (node) => { // make sure node is string literal if (!isStringLiteral(node)) return; old(node); }; }); return newVisitor; } return wrapVisitor(visitor); }, }); //# sourceMappingURL=no-unlocalized-strings.js.map