Support msg({...}) descriptor form and add auto-fix

- Allow msg() function call form: _(msg({message: 'Hello'}))
- Add auto-fix for string literals: _('Bad') -> _(msg`Bad`)
- Add auto-fix for untagged templates: _(`Bad`) -> _(msg`Bad`)
- No auto-fix for variables/function calls (not safely fixable)

https://claude.ai/code/session_01JMXXPUgAHiSBGmfGwUojKy
This commit is contained in:
Claude
2026-01-29 09:49:03 +00:00
committed by Samuel Newman
parent 6817b990a8
commit 0c4dd977d6
2 changed files with 100 additions and 14 deletions
+61 -12
View File
@@ -57,79 +57,128 @@ const position = 1
const x = _(selectOrdinal(position, {one: '#st', two: '#nd', few: '#rd', other: '#th'})) 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) // msg function call with object (descriptor form)
// This is fine - the rule just checks the pattern
{ {
code: ` code: `
const _ = (x) => x const {_} = useLingui()
const x = _(someValue) const x = _(msg({message: 'Hello'}))
`,
},
// msg function call with object and context
{
code: `
const {_} = useLingui()
const x = _(msg({message: 'Hello', context: 'greeting'}))
`, `,
}, },
], ],
invalid: [ invalid: [
// Plain string literal (single quotes) // Plain string literal (single quotes) - with auto-fix
{ {
code: ` code: `
const {_} = useLingui() const {_} = useLingui()
const x = _('Bad') const x = _('Bad')
`,
output: `
const {_} = useLingui()
const x = _(msg\`Bad\`)
`, `,
errors: [{messageId: 'missingMsg'}], errors: [{messageId: 'missingMsg'}],
}, },
// Plain string literal (double quotes) // Plain string literal (double quotes) - with auto-fix
{ {
code: ` code: `
const {_} = useLingui() const {_} = useLingui()
const x = _("Bad") const x = _("Bad")
`,
output: `
const {_} = useLingui()
const x = _(msg\`Bad\`)
`, `,
errors: [{messageId: 'missingMsg'}], errors: [{messageId: 'missingMsg'}],
}, },
// Template literal without msg tag // Template literal without msg tag - with auto-fix
{ {
code: ` code: `
const {_} = useLingui() const {_} = useLingui()
const x = _(\`Bad\`) const x = _(\`Bad\`)
`,
output: `
const {_} = useLingui()
const x = _(msg\`Bad\`)
`, `,
errors: [{messageId: 'missingMsg'}], 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: ` code: `
const {_} = useLingui() const {_} = useLingui()
const message = 'Hello' const message = 'Hello'
const x = _(message) const x = _(message)
`, `,
output: null,
errors: [{messageId: 'missingMsg'}], errors: [{messageId: 'missingMsg'}],
}, },
// Arbitrary function call // Arbitrary function call - no auto-fix possible
{ {
code: ` code: `
const {_} = useLingui() const {_} = useLingui()
const x = _(getMessage()) const x = _(getMessage())
`, `,
output: null,
errors: [{messageId: 'missingMsg'}], errors: [{messageId: 'missingMsg'}],
}, },
// Empty call // Empty call - no auto-fix possible
{ {
code: ` code: `
const {_} = useLingui() const {_} = useLingui()
const x = _() const x = _()
`, `,
output: null,
errors: [{messageId: 'missingMsg'}], errors: [{messageId: 'missingMsg'}],
}, },
// Tagged template with wrong tag // Tagged template with wrong tag - no auto-fix (would need to replace tag)
{ {
code: ` code: `
const {_} = useLingui() const {_} = useLingui()
const x = _(html\`Hello\`) const x = _(html\`Hello\`)
`, `,
output: null,
errors: [{messageId: 'missingMsg'}], errors: [{messageId: 'missingMsg'}],
}, },
// Number literal // Number literal - no auto-fix possible
{ {
code: ` code: `
const {_} = useLingui() const {_} = useLingui()
const x = _(123) const x = _(123)
`, `,
output: null,
errors: [{messageId: 'missingMsg'}], errors: [{messageId: 'missingMsg'}],
}, },
], ],
+39 -2
View File
@@ -11,9 +11,10 @@ module.exports = {
'Enforce that Lingui _() function is called with msg`` template literal or plural/select macros', 'Enforce that Lingui _() function is called with msg`` template literal or plural/select macros',
recommended: true, recommended: true,
}, },
fixable: 'code',
messages: { messages: {
missingMsg: 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: [], schema: [],
}, },
@@ -21,11 +22,45 @@ module.exports = {
create(context) { create(context) {
// Valid Lingui macro functions that can be passed to _() // Valid Lingui macro functions that can be passed to _()
const VALID_MACRO_FUNCTIONS = new Set([ const VALID_MACRO_FUNCTIONS = new Set([
'msg',
'plural', 'plural',
'select', 'select',
'selectOrdinal', '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 { return {
CallExpression(node) { CallExpression(node) {
// Check if this is a call to _() // Check if this is a call to _()
@@ -53,7 +88,7 @@ module.exports = {
return return
} }
// Valid: _(plural(...)), _(select(...)), _(selectOrdinal(...)) // Valid: _(msg(...)), _(plural(...)), _(select(...)), _(selectOrdinal(...))
if ( if (
firstArg.type === 'CallExpression' && firstArg.type === 'CallExpression' &&
firstArg.callee.type === 'Identifier' && firstArg.callee.type === 'Identifier' &&
@@ -63,9 +98,11 @@ module.exports = {
} }
// Everything else is invalid // Everything else is invalid
const fix = getFixer(firstArg)
context.report({ context.report({
node, node,
messageId: 'missingMsg', messageId: 'missingMsg',
fix,
}) })
}, },
} }