UNPKG

eslint-plugin-formatjs

Version:
151 lines (150 loc) 5.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = exports.name = void 0; const icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser"); const context_compat_1 = require("../context-compat"); const util_1 = require("../util"); function collectPlaceholderNames(ast) { const placeholderNames = new Set(); _traverse(ast); return placeholderNames; function _traverse(ast) { for (const element of ast) { switch (element.type) { case icu_messageformat_parser_1.TYPE.literal: case icu_messageformat_parser_1.TYPE.pound: break; case icu_messageformat_parser_1.TYPE.tag: placeholderNames.add(element.value); _traverse(element.children); break; case icu_messageformat_parser_1.TYPE.plural: case icu_messageformat_parser_1.TYPE.select: placeholderNames.add(element.value); for (const { value } of Object.values(element.options)) { _traverse(value); } break; default: placeholderNames.add(element.value); break; } } } } function checkNode(context, node) { const settings = (0, util_1.getSettings)(context); const msgs = (0, util_1.extractMessages)(node, { excludeMessageDeclCalls: true, ...settings, }); const { options: [opt], } = context; const ignoreList = new Set(opt?.ignoreList || []); for (const [{ message: { defaultMessage }, messageNode, }, values,] of msgs) { if (!defaultMessage || !messageNode) { continue; } if (values && values.type !== 'ObjectExpression') { // cannot evaluate this continue; } if (values?.properties.find(prop => prop.type === 'SpreadElement')) { // cannot evaluate the spread element continue; } const literalElementByLiteralKey = new Map(); if (values) { for (const prop of values.properties) { if (prop.type === 'Property' && !prop.computed) { const name = prop.key.type === 'Identifier' ? prop.key.name : String(prop.key.value); literalElementByLiteralKey.set(name, prop); } } } let ast; try { ast = (0, icu_messageformat_parser_1.parse)(defaultMessage, { ignoreTag: settings.ignoreTag }); } catch (e) { context.report({ node: messageNode, messageId: 'parserError', data: { message: e instanceof Error ? e.message : String(e) }, }); continue; } const placeholderNames = collectPlaceholderNames(ast); const missingPlaceholders = []; placeholderNames.forEach(name => { if (!ignoreList.has(name) && !literalElementByLiteralKey.has(name)) { missingPlaceholders.push(name); } }); if (missingPlaceholders.length > 0) { context.report({ node: messageNode, messageId: 'missingValue', data: { list: missingPlaceholders.join(', '), }, }); } literalElementByLiteralKey.forEach((element, key) => { if (!ignoreList.has(key) && !placeholderNames.has(key)) { context.report({ node: element, messageId: 'unusedValue', }); } }); } } exports.name = 'enforce-placeholders'; exports.rule = { meta: { type: 'problem', docs: { description: 'Enforce that all messages with placeholders have enough passed-in values', url: 'https://formatjs.github.io/docs/tooling/linter#enforce-placeholders', }, schema: [ { type: 'object', properties: { ignoreList: { type: 'array', items: { type: 'string', }, }, }, additionalProperties: false, }, ], messages: { parserError: '{{message}}', missingValue: 'Missing value(s) for the following placeholder(s): {{list}}.', unusedValue: 'Value not used by the message.', }, }, defaultOptions: [], create(context) { const callExpressionVisitor = (node) => checkNode(context, node); const parserServices = (0, context_compat_1.getParserServices)(context); //@ts-expect-error defineTemplateBodyVisitor exists in Vue parser if (parserServices?.defineTemplateBodyVisitor) { //@ts-expect-error return parserServices.defineTemplateBodyVisitor({ CallExpression: callExpressionVisitor, }, { CallExpression: callExpressionVisitor, }); } return { JSXOpeningElement: (node) => checkNode(context, node), CallExpression: callExpressionVisitor, }; }, };