eslint-plugin-lingui
Version:
ESLint plugin for Lingui
588 lines • 25.9 kB
JavaScript
;
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 () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__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, // ignore non word messages
...((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 = ['←', ' ', '·'];
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;
}
case utils_1.TSESTree.AST_NODE_TYPES.TSModuleDeclaration: {
if (parent.declare === true && parent.id === node) {
return true;
}
break;
}
}
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);
},
'ImportExpression > Literal'(node) {
// allow import('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