UNPKG

eslint-plugin-formatjs

Version:
184 lines (183 loc) 6.91 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = exports.name = void 0; const tslib_1 = require("tslib"); const utils_1 = require("@typescript-eslint/utils"); const picomatch_1 = tslib_1.__importDefault(require("picomatch")); const propMatcherSchema = { type: 'array', items: { type: 'array', items: [{ type: 'string' }, { type: 'string' }], }, }; const defaultPropIncludePattern = [ ['*', 'aria-{label,description,details,errormessage}'], ['[a-z]*([a-z0-9])', '(placeholder|title)'], ['img', 'alt'], ]; const defaultPropExcludePattern = []; function stringifyJsxTagName(tagName) { switch (tagName.type) { case utils_1.TSESTree.AST_NODE_TYPES.JSXIdentifier: return tagName.name; case utils_1.TSESTree.AST_NODE_TYPES.JSXMemberExpression: return `${stringifyJsxTagName(tagName.object)}.${tagName.property.name}`; case utils_1.TSESTree.AST_NODE_TYPES.JSXNamespacedName: return `${tagName.namespace.name}:${tagName.name.name}`; } } function compilePropMatcher(propMatcher) { return propMatcher.map(([tagNamePattern, propNamePattern]) => { return [ picomatch_1.default.makeRe(tagNamePattern, { contains: false }), picomatch_1.default.makeRe(propNamePattern, { contains: false }), ]; }); } exports.name = 'no-literal-string-in-jsx'; exports.rule = { meta: { type: 'problem', docs: { description: 'Disallow untranslated literal strings without translation.', url: 'https://formatjs.github.io/docs/tooling/linter#no-literal-string-in-jsx', }, schema: [ { type: 'object', properties: { props: { type: 'object', properties: { include: { ...propMatcherSchema, }, exclude: { ...propMatcherSchema, }, }, }, }, }, ], messages: { noLiteralStringInJsx: 'Cannot have untranslated text in JSX', }, }, defaultOptions: [], // TODO: Vue support create(context) { const userConfig = context.options[0] || {}; const propIncludePattern = compilePropMatcher([ ...defaultPropIncludePattern, ...(userConfig.props?.include ?? []), ]); const propExcludePattern = compilePropMatcher([ ...defaultPropExcludePattern, ...(userConfig.props?.exclude ?? []), ]); const lexicalJsxStack = []; const shouldSkipCurrentJsxAttribute = (node) => { const currentJsxNode = lexicalJsxStack[lexicalJsxStack.length - 1]; if (currentJsxNode.type === 'JSXFragment') { return false; } const nameString = stringifyJsxTagName(currentJsxNode.openingElement.name); const attributeName = typeof node.name.name === 'string' ? node.name.name : node.name.name.name; // match exclude for (const [tagNamePattern, propNamePattern] of propExcludePattern) { if (tagNamePattern.test(nameString) && propNamePattern.test(attributeName)) { return true; } } // match include for (const [tagNamePattern, propNamePattern] of propIncludePattern) { if (tagNamePattern.test(nameString) && propNamePattern.test(attributeName)) { return false; } } return true; }; const checkJSXExpression = (node) => { // Check if this is either a string literal / template literal, or the concat of them. // It also ignores the empty string. if ((node.type === 'Literal' && typeof node.value === 'string' && node.value.length > 0) || (node.type === 'TemplateLiteral' && (node.quasis.length > 1 || node.quasis[0].value.raw.length > 0))) { context.report({ node: node, messageId: 'noLiteralStringInJsx', }); } else if (node.type === 'BinaryExpression' && node.operator === '+') { checkJSXExpression(node.left); checkJSXExpression(node.right); } else if (node.type === 'ConditionalExpression') { checkJSXExpression(node.consequent); checkJSXExpression(node.alternate); } else if (node.type === 'LogicalExpression') { checkJSXExpression(node.left); checkJSXExpression(node.right); } }; return { JSXElement: (node) => { lexicalJsxStack.push(node); }, 'JSXElement:exit': () => { lexicalJsxStack.pop(); }, JSXFragment: (node) => { lexicalJsxStack.push(node); }, 'JSXFragment:exit': () => { lexicalJsxStack.pop(); }, JSXAttribute: (node) => { if (shouldSkipCurrentJsxAttribute(node)) { return; } if (!node.value) { return; } if (node.value.type === 'Literal' && typeof node.value.value === 'string' && node.value.value.length > 0) { context.report({ node: node, messageId: 'noLiteralStringInJsx', }); } else if (node.value.type === 'JSXExpressionContainer' && node.value.expression.type !== 'JSXEmptyExpression') { checkJSXExpression(node.value.expression); } }, JSXText: (node) => { // Ignore purely spacing fragments if (!node.value.replace(/\s*/gm, '')) { return; } context.report({ node: node, messageId: 'noLiteralStringInJsx', }); }, // Children expression container 'JSXElement > JSXExpressionContainer': (node) => { if (node.expression.type !== 'JSXEmptyExpression') { checkJSXExpression(node.expression); } }, }; }, };