From 4bfe5e9eba2f566abec18f657e9805e3d12fa387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jastrz=C4=99bski?= Date: Sun, 2 Feb 2025 21:53:44 +0100 Subject: [PATCH 1/2] feat(no-expression-in-message): add support for ph and explicit labels --- src/rules/no-expression-in-message.ts | 52 +++++++++++++------ .../rules/no-expression-in-message.test.ts | 28 ++++++++++ 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/rules/no-expression-in-message.ts b/src/rules/no-expression-in-message.ts index f8ca987..5ff7de2 100644 --- a/src/rules/no-expression-in-message.ts +++ b/src/rules/no-expression-in-message.ts @@ -29,27 +29,37 @@ export const rule = createRule({ defaultOptions: [], create: function (context) { - const linguiMacroFunctionNames = ['plural', 'select', 'selectOrdinal'] + const linguiMacroFunctionNames = ['plural', 'select', 'selectOrdinal', 'ph'] function checkExpressionsInTplLiteral(node: TSESTree.TemplateLiteral) { - node.expressions.forEach((expression) => { - if (expression.type === TSESTree.AST_NODE_TYPES.Identifier) { - return - } + node.expressions.forEach((expression) => checkExpression(expression)) + } - const isCallToLinguiMacro = - expression.type === TSESTree.AST_NODE_TYPES.CallExpression && - expression.callee.type === TSESTree.AST_NODE_TYPES.Identifier && - linguiMacroFunctionNames.includes(expression.callee.name) + function checkExpression(expression: TSESTree.Expression) { + if (expression.type === TSESTree.AST_NODE_TYPES.Identifier) { + return + } - if (isCallToLinguiMacro) { - return - } + const isCallToLinguiMacro = + expression.type === TSESTree.AST_NODE_TYPES.CallExpression && + expression.callee.type === TSESTree.AST_NODE_TYPES.Identifier && + linguiMacroFunctionNames.includes(expression.callee.name) + + if (isCallToLinguiMacro) { + return + } - context.report({ - node: expression, - messageId: 'default', - }) + const isExplicitLabel = + expression.type === TSESTree.AST_NODE_TYPES.ObjectExpression && + expression.properties.length === 1 + + if (isExplicitLabel) { + return + } + + context.report({ + node: expression, + messageId: 'default', }) } @@ -76,6 +86,16 @@ export const rule = createRule({ return checkExpressionsInTplLiteral(node) } + if (node.type === TSESTree.AST_NODE_TYPES.ObjectExpression) { + // Hello {{name: obj.prop}} + return checkExpression(node) + } + + if (node.type === TSESTree.AST_NODE_TYPES.CallExpression) { + // Hello {ph({name: obj.prop})} + return checkExpression(node) + } + if (node.type !== TSESTree.AST_NODE_TYPES.Identifier) { context.report({ node, diff --git a/tests/src/rules/no-expression-in-message.test.ts b/tests/src/rules/no-expression-in-message.test.ts index 0cbb875..d4acc2d 100644 --- a/tests/src/rules/no-expression-in-message.test.ts +++ b/tests/src/rules/no-expression-in-message.test.ts @@ -87,6 +87,18 @@ ruleTester.run(name, rule, { name: 'Strings as children are preserved', code: '{"hello {count, plural, one {world} other {worlds}}"}', }, + { + code: 't`hello ${{name: obj.prop}}`', + }, + { + code: 't`hello ${ph({name: obj.prop})}`', + }, + { + code: 'hello {{name: obj.prop}}', + }, + { + code: 'hello {ph({name: obj.prop})}', + }, ], invalid: [ { @@ -131,5 +143,21 @@ ruleTester.run(name, rule, { code: 't`hello ${func()}?`', errors: [{ messageId: 'default' }], }, + { + code: 't`hello ${{name: obj.foo, surname: obj.bar}}`', + errors: [{ messageId: 'default' }], + }, + { + code: 't`hello ${greeting({name: obj.prop})}`', + errors: [{ messageId: 'default' }], + }, + { + code: 'hello {{name: obj.foo, surname: obj.bar}}', + errors: [{ messageId: 'default' }], + }, + { + code: 'hello {greeting({name: obj.prop})}', + errors: [{ messageId: 'default' }], + }, ], }) From 3f41b1cd00b2024889aba461113cc7655aca1ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jastrz=C4=99bski?= Date: Mon, 3 Feb 2025 21:51:14 +0100 Subject: [PATCH 2/2] multiple placeholders warning --- src/rules/no-expression-in-message.ts | 14 +++++++++++--- tests/src/rules/no-expression-in-message.test.ts | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/rules/no-expression-in-message.ts b/src/rules/no-expression-in-message.ts index 5ff7de2..d2446b9 100644 --- a/src/rules/no-expression-in-message.ts +++ b/src/rules/no-expression-in-message.ts @@ -16,6 +16,8 @@ export const rule = createRule({ }, messages: { default: 'Should be ${variable}, not ${object.property} or ${myFunction()}', + multiplePlaceholders: + 'Invalid placeholder: Expected an object with a single key-value pair, but found multiple keys', }, schema: [ { @@ -49,11 +51,17 @@ export const rule = createRule({ return } - const isExplicitLabel = - expression.type === TSESTree.AST_NODE_TYPES.ObjectExpression && - expression.properties.length === 1 + const isExplicitLabel = expression.type === TSESTree.AST_NODE_TYPES.ObjectExpression if (isExplicitLabel) { + // there can be only one key in the object + if (expression.properties.length === 1) { + return + } + context.report({ + node: expression, + messageId: 'multiplePlaceholders', + }) return } diff --git a/tests/src/rules/no-expression-in-message.test.ts b/tests/src/rules/no-expression-in-message.test.ts index d4acc2d..85fe59d 100644 --- a/tests/src/rules/no-expression-in-message.test.ts +++ b/tests/src/rules/no-expression-in-message.test.ts @@ -145,7 +145,7 @@ ruleTester.run(name, rule, { }, { code: 't`hello ${{name: obj.foo, surname: obj.bar}}`', - errors: [{ messageId: 'default' }], + errors: [{ messageId: 'multiplePlaceholders' }], }, { code: 't`hello ${greeting({name: obj.prop})}`', @@ -153,7 +153,7 @@ ruleTester.run(name, rule, { }, { code: 'hello {{name: obj.foo, surname: obj.bar}}', - errors: [{ messageId: 'default' }], + errors: [{ messageId: 'multiplePlaceholders' }], }, { code: 'hello {greeting({name: obj.prop})}',