eslint-plugin-formatjs
Version:
ESLint plugin for formatjs
137 lines (136 loc) • 4.72 kB
JavaScript
import { TYPE, parse, } from '@formatjs/icu-messageformat-parser';
import { getParserServices } from '../context-compat';
import { extractMessages, getSettings } from '../util';
function calculateComplexity(ast) {
// Dynamic programming: define a complexity function f, where:
// f(plural | select) = sum(f(option) for each option) * f(next element),
// f(tag) = f(first element of children) * f(next element),
// f(other) = f(next element),
// f(out of bound) = 1.
const complexityByNode = new Map();
return _calculate(ast, 0);
function _calculate(ast, index) {
const element = ast[index];
if (!element) {
return 1;
}
const cachedComplexity = complexityByNode.get(element);
if (cachedComplexity !== undefined) {
return cachedComplexity;
}
let complexity;
switch (element.type) {
case TYPE.plural:
case TYPE.select: {
let sumOfOptions = 0;
for (const { value } of Object.values(element.options)) {
sumOfOptions += _calculate(value, 0);
}
complexity = sumOfOptions * _calculate(ast, index + 1);
break;
}
case TYPE.tag:
complexity =
_calculate(element.children, 0) * _calculate(ast, index + 1);
break;
default:
complexity = _calculate(ast, index + 1);
break;
}
complexityByNode.set(element, complexity);
return complexity;
}
}
function checkNode(context, node) {
const settings = getSettings(context);
const msgs = extractMessages(node, settings);
if (!msgs.length) {
return;
}
const config = {
limit: 20,
...(context.options[0] || {}),
};
for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
if (!defaultMessage || !messageNode) {
continue;
}
let ast;
try {
ast = parse(defaultMessage, {
ignoreTag: settings.ignoreTag,
});
}
catch (e) {
context.report({
node: messageNode,
messageId: 'parserError',
data: { message: e instanceof Error ? e.message : String(e) },
});
return;
}
const complexity = calculateComplexity(ast);
if (complexity > config.limit) {
context.report({
node: messageNode,
messageId: 'tooComplex',
data: {
complexity,
limit: config.limit,
},
});
}
}
}
export const name = 'no-complex-selectors';
export const rule = {
meta: {
type: 'problem',
docs: {
description: `Make sure a sentence is not too complex.
Complexity is determined by how many strings are produced when we try to
flatten the sentence given its selectors. For example:
"I have {count, plural, one{a dog} other{many dogs}}"
has the complexity of 2 because flattening the plural selector
results in 2 sentences: "I have a dog" & "I have many dogs".
Default complexity limit is 20
(using Smartling as a reference: https://help.smartling.com/hc/en-us/articles/360008030994-ICU-MessageFormat)
`,
url: 'https://formatjs.github.io/docs/tooling/linter#no-complex-selectors',
},
schema: [
{
type: 'object',
properties: {
limit: {
type: 'number',
},
},
additionalProperties: false,
},
],
fixable: 'code',
messages: {
tooComplex: `Message complexity is too high ({{complexity}} vs limit at {{limit}})`,
parserError: '{{message}}',
},
},
defaultOptions: [{ limit: 20 }],
create(context) {
const callExpressionVisitor = (node) => checkNode(context, node);
const parserServices = 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,
};
},
};