UNPKG

eslint-plugin-formatjs

Version:
288 lines (287 loc) 10.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getSettings = getSettings; exports.isIntlFormatMessageCall = isIntlFormatMessageCall; exports.extractMessageDescriptor = extractMessageDescriptor; exports.extractMessages = extractMessages; exports.patchMessage = patchMessage; const FORMAT_FUNCTION_NAMES = new Set(['$formatMessage', 'formatMessage', '$t']); const COMPONENT_NAMES = new Set(['FormattedMessage']); const DECLARATION_FUNCTION_NAMES = new Set(['defineMessage']); function getSettings({ settings }) { return settings.formatjs ?? settings; } function isStringLiteral(node) { return node.type === 'Literal' && typeof node.value === 'string'; } function isTemplateLiteralWithoutVar(node) { return node.type === 'TemplateLiteral' && node.quasis.length === 1; } function staticallyEvaluateStringConcat(node) { if (!isStringLiteral(node.right)) { return ['', false]; } if (isStringLiteral(node.left)) { return [String(node.left.value) + node.right.value, true]; } if (node.left.type === 'BinaryExpression') { const [result, isStaticallyEvaluatable] = staticallyEvaluateStringConcat(node.left); return [result + node.right.value, isStaticallyEvaluatable]; } return ['', false]; } function isIntlFormatMessageCall(node) { return (node.type === 'CallExpression' && node.callee.type === 'MemberExpression' && ((node.callee.object.type === 'Identifier' && node.callee.object.name === 'intl') || (node.callee.object.type === 'MemberExpression' && node.callee.object.property.type === 'Identifier' && node.callee.object.property.name === 'intl')) && node.callee.property.type === 'Identifier' && (node.callee.property.name === 'formatMessage' || node.callee.property.name === '$t') && node.arguments.length >= 1 && node.arguments[0].type === 'ObjectExpression'); } function isSingleMessageDescriptorDeclaration(node, functionNames) { return (node.type === 'CallExpression' && node.callee.type === 'Identifier' && functionNames.has(node.callee.name)); } function isMultipleMessageDescriptorDeclaration(node) { return (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'defineMessages'); } function extractMessageDescriptor(node) { if (!node || node.type !== 'ObjectExpression') { return; } const result = { message: {}, messageNode: undefined, messagePropNode: undefined, descriptionNode: undefined, idValueNode: undefined, }; for (const prop of node.properties) { if (prop.type !== 'Property' || prop.key.type !== 'Identifier') { continue; } const valueNode = prop.value; let value = undefined; if (isStringLiteral(valueNode)) { value = valueNode.value; } // like "`asd`" else if (isTemplateLiteralWithoutVar(valueNode)) { value = valueNode.quasis[0].value.cooked; } // like "dedent`asd`" else if (valueNode.type === 'TaggedTemplateExpression') { const { quasi } = valueNode; if (!isTemplateLiteralWithoutVar(quasi)) { throw new Error('Tagged template expression must be no substitution'); } value = quasi.quasis[0].value.cooked; } // like "`asd` + `asd`" else if (valueNode.type === 'BinaryExpression') { const [result, isStatic] = staticallyEvaluateStringConcat(valueNode); if (isStatic) { value = result; } } switch (prop.key.name) { case 'defaultMessage': result.messagePropNode = prop; result.messageNode = valueNode; result.message.defaultMessage = value; break; case 'description': result.descriptionNode = valueNode; result.message.description = value; break; case 'id': result.message.id = value; result.idValueNode = valueNode; result.idPropNode = prop; break; } } return result; } function extractMessageDescriptorFromJSXElement(node) { if (!node || !node.attributes) { return; } let values; const result = { message: {}, messageNode: undefined, messagePropNode: undefined, descriptionNode: undefined, idValueNode: undefined, idPropNode: undefined, }; let hasSpreadAttribute = false; for (const prop of node.attributes) { // We can't analyze spread attr if (prop.type === 'JSXSpreadAttribute') { hasSpreadAttribute = true; } if (prop.type !== 'JSXAttribute' || prop.name.type !== 'JSXIdentifier') { continue; } const key = prop.name; let valueNode = prop.value; let value = undefined; if (valueNode) { if (isStringLiteral(valueNode)) { value = valueNode.value; } else if (valueNode?.type === 'JSXExpressionContainer') { const { expression } = valueNode; if (expression.type === 'BinaryExpression') { const [result, isStatic] = staticallyEvaluateStringConcat(expression); if (isStatic) { value = result; } } // like "`asd`" else if (isTemplateLiteralWithoutVar(expression)) { value = expression.quasis[0].value.cooked; } // like "dedent`asd`" else if (expression.type === 'TaggedTemplateExpression') { const { quasi } = expression; if (!isTemplateLiteralWithoutVar(quasi)) { throw new Error('Tagged template expression must be no substitution'); } value = quasi.quasis[0].value.cooked; } } } switch (key.name) { case 'defaultMessage': result.messagePropNode = prop; result.messageNode = valueNode; if (value) { result.message.defaultMessage = value; } break; case 'description': result.descriptionNode = valueNode; if (value) { result.message.description = value; } break; case 'id': result.idValueNode = valueNode; result.idPropNode = prop; if (value) { result.message.id = value; } break; case 'values': if (valueNode?.type === 'JSXExpressionContainer' && valueNode.expression.type === 'ObjectExpression') { values = valueNode.expression; } break; } } if (!result.messagePropNode && !result.descriptionNode && !result.idPropNode && hasSpreadAttribute) { return; } return [result, values]; } function extractMessageDescriptors(node) { if (!node || node.type !== 'ObjectExpression' || !node.properties.length) { return []; } const msgs = []; for (const prop of node.properties) { if (prop.type !== 'Property') { continue; } const msg = prop.value; if (msg.type !== 'ObjectExpression') { continue; } const nodeInfo = extractMessageDescriptor(msg); if (nodeInfo) { msgs.push(nodeInfo); } } return msgs; } function extractMessages(node, { additionalComponentNames, additionalFunctionNames, excludeMessageDeclCalls, } = {}) { const allFormatFunctionNames = Array.isArray(additionalFunctionNames) ? new Set([ ...Array.from(FORMAT_FUNCTION_NAMES), ...additionalFunctionNames, ]) : FORMAT_FUNCTION_NAMES; const allComponentNames = Array.isArray(additionalComponentNames) ? new Set([...Array.from(COMPONENT_NAMES), ...additionalComponentNames]) : COMPONENT_NAMES; if (node.type === 'CallExpression') { const expr = node; const args0 = expr.arguments[0]; const args1 = expr.arguments[1]; // We can't really analyze spread element if (!args0 || args0.type === 'SpreadElement') { return []; } if ((!excludeMessageDeclCalls && isSingleMessageDescriptorDeclaration(node, DECLARATION_FUNCTION_NAMES)) || isIntlFormatMessageCall(node) || isSingleMessageDescriptorDeclaration(node, allFormatFunctionNames)) { const msgDescriptorNodeInfo = extractMessageDescriptor(args0); if (msgDescriptorNodeInfo && (!args1 || args1.type !== 'SpreadElement')) { return [[msgDescriptorNodeInfo, args1]]; } } else if (!excludeMessageDeclCalls && isMultipleMessageDescriptorDeclaration(node)) { return extractMessageDescriptors(args0).map(msg => [msg, undefined]); } } else if (node.type === 'JSXOpeningElement' && node.name && node.name.type === 'JSXIdentifier' && allComponentNames.has(node.name.name)) { const msgDescriptorNodeInfo = extractMessageDescriptorFromJSXElement(node); if (msgDescriptorNodeInfo) { return [msgDescriptorNodeInfo]; } } return []; } /** * Apply changes to the ICU message in code. The return value can be used in * `fixer.replaceText(messageNode, <return value>)`. If the return value is null, * it means that the patch cannot be applied. */ function patchMessage(messageNode, ast, patcher) { if (messageNode.type === 'Literal' && messageNode.value && typeof messageNode.value === 'string') { return ('"' + patcher(messageNode.value, ast).replace('"', '\\"') + '"'); } else if (messageNode.type === 'TemplateLiteral' && messageNode.quasis.length === 1 && messageNode.expressions.length === 0) { return ('`' + patcher(messageNode.quasis[0].value.cooked, ast) .replace(/\\/g, '\\\\') .replace(/`/g, '\\`') + '`'); } return null; }