UNPKG

eslint-plugin-formatjs

Version:
196 lines (195 loc) 8.38 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = exports.name = void 0; const tslib_1 = require("tslib"); const icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser"); const magic_string_1 = tslib_1.__importDefault(require("magic-string")); const context_compat_1 = require("../context-compat"); const util_1 = require("../util"); function verifyAst(context, messageNode, ast) { const patches = []; _verifyAst(ast); if (patches.length > 0) { const patchedMessage = (0, util_1.patchMessage)(messageNode, ast, content => { return patches .reduce((magicString, patch) => { switch (patch.type) { case 'prependLeft': return magicString.prependLeft(patch.index, patch.content); case 'remove': return magicString.remove(patch.start, patch.end); case 'update': return magicString.update(patch.start, patch.end, patch.content); } }, new magic_string_1.default(content)) .toString(); }); context.report({ node: messageNode, messageId: 'preferPoundInPlurals', fix: patchedMessage !== null ? fixer => fixer.replaceText(messageNode, patchedMessage) : null, }); } function _verifyAst(ast) { for (let i = 0; i < ast.length; i++) { const current = ast[i]; switch (current.type) { case icu_messageformat_parser_1.TYPE.argument: case icu_messageformat_parser_1.TYPE.number: { // Applicable to only plain argument or number argument without any style if (current.type === icu_messageformat_parser_1.TYPE.number && current.style) { break; } const next = ast[i + 1]; const nextNext = ast[i + 2]; if (next && nextNext && next.type === icu_messageformat_parser_1.TYPE.literal && next.value === ' ' && nextNext.type === icu_messageformat_parser_1.TYPE.plural && nextNext.value === current.value) { // `{A} {A, plural, one {B} other {Bs}}` => `{A, plural, one {# B} other {# Bs}}` _removeRangeAndPrependPluralClauses(current.location.start.offset, next.location.end.offset, nextNext, '# '); } else if (next && next.type === icu_messageformat_parser_1.TYPE.plural && next.value === current.value) { // `{A}{A, plural, one {B} other {Bs}}` => `{A, plural, one {#B} other {#Bs}}` _removeRangeAndPrependPluralClauses(current.location.start.offset, current.location.end.offset, next, '#'); } break; } case icu_messageformat_parser_1.TYPE.plural: { // `{A, plural, one {{A} B} other {{A} Bs}}` => `{A, plural, one {# B} other {# Bs}}` const name = current.value; for (const { value } of Object.values(current.options)) { _replacementArgumentWithPound(name, value); } break; } case icu_messageformat_parser_1.TYPE.select: { for (const { value } of Object.values(current.options)) { _verifyAst(value); } break; } case icu_messageformat_parser_1.TYPE.tag: _verifyAst(current.children); break; default: break; } } } // Replace plain argument of number argument w/o style option that matches // the name with a pound sign. function _replacementArgumentWithPound(name, ast) { for (const element of ast) { switch (element.type) { case icu_messageformat_parser_1.TYPE.argument: case icu_messageformat_parser_1.TYPE.number: { if (element.value === name && // Either plain argument or number argument without any style (element.type !== icu_messageformat_parser_1.TYPE.number || !element.style)) { patches.push({ type: 'update', start: element.location.start.offset, end: element.location.end.offset, content: '#', }); } break; } case icu_messageformat_parser_1.TYPE.tag: { _replacementArgumentWithPound(name, element.children); break; } case icu_messageformat_parser_1.TYPE.select: { for (const { value } of Object.values(element.options)) { _replacementArgumentWithPound(name, value); } break; } default: break; } } } // Helper to remove a certain text range and then prepend the specified text to // each plural clause. function _removeRangeAndPrependPluralClauses(rangeToRemoveStart, rangeToRemoveEnd, pluralElement, prependText) { // Delete both the `{A}` and ` ` patches.push({ type: 'remove', start: rangeToRemoveStart, end: rangeToRemoveEnd, }); // Insert `# ` to the beginning of every option clause for (const { location } of Object.values(pluralElement.options)) { // location marks the entire clause with the surrounding braces patches.push({ type: 'prependLeft', index: location.start.offset + 1, content: prependText, }); } } } function checkNode(context, node) { const msgs = (0, util_1.extractMessages)(node, (0, util_1.getSettings)(context)); for (const [{ message: { defaultMessage }, messageNode, },] of msgs) { if (!defaultMessage || !messageNode) { continue; } let ast; try { ast = (0, icu_messageformat_parser_1.parse)(defaultMessage, { captureLocation: true }); } catch (e) { context.report({ node: messageNode, messageId: 'parseError', data: { message: e instanceof Error ? e.message : String(e) }, }); return; } verifyAst(context, messageNode, ast); } } exports.name = 'prefer-pound-in-plural'; exports.rule = { meta: { type: 'suggestion', docs: { description: 'Prefer using # to reference the count in the plural argument.', url: 'https://formatjs.github.io/docs/tooling/linter#prefer-pound-in-plurals', }, messages: { preferPoundInPlurals: 'Prefer using # to reference the count in the plural argument instead of repeating the argument.', parseError: '{{message}}', }, fixable: 'code', schema: [], }, defaultOptions: [], // TODO: Vue support 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, }; }, };