Add ESLint rule to enforce Lingui msg usage (#9789)
* Add ESLint rule to enforce Lingui msg usage
Adds a custom ESLint rule 'lingui-msg-rule' that ensures the Lingui _()
function is called with msg`` template literals or plural/select macros,
preventing accidental misuse like _('string') which bypasses i18n.
https://claude.ai/code/session_01JMXXPUgAHiSBGmfGwUojKy
* 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
* fix complex cases
* run autofix HELL YEAH
---------
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @type {import('eslint').Rule.RuleModule}
|
||||
*/
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'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`...` or msg({...}) or plural/select/selectOrdinal. Example: _(msg`Hello`)',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
|
||||
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 _()
|
||||
if (node.callee.type !== 'Identifier' || node.callee.name !== '_') {
|
||||
return
|
||||
}
|
||||
|
||||
// Must have at least one argument
|
||||
if (node.arguments.length === 0) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'missingMsg',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const firstArg = node.arguments[0]
|
||||
|
||||
// Valid: _(msg`...`)
|
||||
if (
|
||||
firstArg.type === 'TaggedTemplateExpression' &&
|
||||
firstArg.tag.type === 'Identifier' &&
|
||||
firstArg.tag.name === 'msg'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Valid: _(msg(...)), _(plural(...)), _(select(...)), _(selectOrdinal(...))
|
||||
if (
|
||||
firstArg.type === 'CallExpression' &&
|
||||
firstArg.callee.type === 'Identifier' &&
|
||||
VALID_MACRO_FUNCTIONS.has(firstArg.callee.name)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Everything else is invalid
|
||||
const fix = getFixer(firstArg)
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'missingMsg',
|
||||
fix,
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user