diff --git a/eslint/__tests__/lingui-msg-rule.test.js b/eslint/__tests__/lingui-msg-rule.test.js index be0d801d99..74c94f4142 100644 --- a/eslint/__tests__/lingui-msg-rule.test.js +++ b/eslint/__tests__/lingui-msg-rule.test.js @@ -57,79 +57,128 @@ const position = 1 const x = _(selectOrdinal(position, {one: '#st', two: '#nd', few: '#rd', other: '#th'})) `, }, - // Different function named _ (not from useLingui context, but rule doesn't track that) - // This is fine - the rule just checks the pattern + // msg function call with object (descriptor form) { code: ` -const _ = (x) => x -const x = _(someValue) +const {_} = useLingui() +const x = _(msg({message: 'Hello'})) + `, + }, + // msg function call with object and context + { + code: ` +const {_} = useLingui() +const x = _(msg({message: 'Hello', context: 'greeting'})) `, }, ], invalid: [ - // Plain string literal (single quotes) + // Plain string literal (single quotes) - with auto-fix { code: ` const {_} = useLingui() const x = _('Bad') + `, + output: ` +const {_} = useLingui() +const x = _(msg\`Bad\`) `, errors: [{messageId: 'missingMsg'}], }, - // Plain string literal (double quotes) + // Plain string literal (double quotes) - with auto-fix { code: ` const {_} = useLingui() const x = _("Bad") + `, + output: ` +const {_} = useLingui() +const x = _(msg\`Bad\`) `, errors: [{messageId: 'missingMsg'}], }, - // Template literal without msg tag + // Template literal without msg tag - with auto-fix { code: ` const {_} = useLingui() const x = _(\`Bad\`) + `, + output: ` +const {_} = useLingui() +const x = _(msg\`Bad\`) `, errors: [{messageId: 'missingMsg'}], }, - // Variable/identifier + // Template literal with interpolation - with auto-fix + { + code: ` +const {_} = useLingui() +const name = 'World' +const x = _(\`Hello \${name}\`) + `, + output: ` +const {_} = useLingui() +const name = 'World' +const x = _(msg\`Hello \${name}\`) + `, + errors: [{messageId: 'missingMsg'}], + }, + // String with backticks that need escaping + { + code: ` +const {_} = useLingui() +const x = _('Use \\\`code\\\` here') + `, + output: ` +const {_} = useLingui() +const x = _(msg\`Use \\\`code\\\` here\`) + `, + errors: [{messageId: 'missingMsg'}], + }, + // Variable/identifier - no auto-fix possible { code: ` const {_} = useLingui() const message = 'Hello' const x = _(message) `, + output: null, errors: [{messageId: 'missingMsg'}], }, - // Arbitrary function call + // Arbitrary function call - no auto-fix possible { code: ` const {_} = useLingui() const x = _(getMessage()) `, + output: null, errors: [{messageId: 'missingMsg'}], }, - // Empty call + // Empty call - no auto-fix possible { code: ` const {_} = useLingui() const x = _() `, + output: null, errors: [{messageId: 'missingMsg'}], }, - // Tagged template with wrong tag + // Tagged template with wrong tag - no auto-fix (would need to replace tag) { code: ` const {_} = useLingui() const x = _(html\`Hello\`) `, + output: null, errors: [{messageId: 'missingMsg'}], }, - // Number literal + // Number literal - no auto-fix possible { code: ` const {_} = useLingui() const x = _(123) `, + output: null, errors: [{messageId: 'missingMsg'}], }, ], diff --git a/eslint/lingui-msg-rule.js b/eslint/lingui-msg-rule.js index 990b627144..27bd7a115d 100644 --- a/eslint/lingui-msg-rule.js +++ b/eslint/lingui-msg-rule.js @@ -11,9 +11,10 @@ module.exports = { 'Enforce that Lingui _() function is called with msg`` template literal or plural/select macros', recommended: true, }, + fixable: 'code', messages: { missingMsg: - 'Lingui _() must be called with msg`...` template literal or plural/select/selectOrdinal. Example: _(msg`Hello`)', + 'Lingui _() must be called with msg`...` or msg({...}) or plural/select/selectOrdinal. Example: _(msg`Hello`)', }, schema: [], }, @@ -21,11 +22,45 @@ module.exports = { create(context) { // Valid Lingui macro functions that can be passed to _() const VALID_MACRO_FUNCTIONS = new Set([ + 'msg', 'plural', 'select', 'selectOrdinal', ]) + /** + * Escape backticks and backslashes for template literal + */ + function escapeForTemplateLiteral(str) { + return str.replace(/\\`/g, '`').replace(/`/g, '\\`') + } + + /** + * Try to get a fixer for the given argument + * Returns null if we can't safely fix it + */ + function getFixer(firstArg) { + const sourceCode = context.sourceCode ?? context.getSourceCode() + + // Fix string literals: _('foo') -> _(msg`foo`) + if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') { + const escaped = escapeForTemplateLiteral(firstArg.value) + return function (fixer) { + return fixer.replaceText(firstArg, 'msg`' + escaped + '`') + } + } + + // Fix untagged template literals: _(`foo`) -> _(msg`foo`) + if (firstArg.type === 'TemplateLiteral') { + const text = sourceCode.getText(firstArg) + return function (fixer) { + return fixer.replaceText(firstArg, 'msg' + text) + } + } + + return null + } + return { CallExpression(node) { // Check if this is a call to _() @@ -53,7 +88,7 @@ module.exports = { return } - // Valid: _(plural(...)), _(select(...)), _(selectOrdinal(...)) + // Valid: _(msg(...)), _(plural(...)), _(select(...)), _(selectOrdinal(...)) if ( firstArg.type === 'CallExpression' && firstArg.callee.type === 'Identifier' && @@ -63,9 +98,11 @@ module.exports = { } // Everything else is invalid + const fix = getFixer(firstArg) context.report({ node, messageId: 'missingMsg', + fix, }) }, }