eslint-plugin-formatjs
Version:
ESLint plugin for formatjs
184 lines (183 loc) • 6.91 kB
JavaScript
;
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);
}
},
};
},
};