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:
@@ -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'}],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user