From ef20d99efee67521d0431ccba9d11905e482c1ac Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Fri, 12 Aug 2022 11:31:33 -0400 Subject: [PATCH] fix: false positives with violation reporting helper function in `no-unused-message-ids` rule --- lib/rules/no-unused-message-ids.js | 3 +- lib/utils.js | 15 ++++++ tests/lib/rules/no-missing-message-ids.js | 66 +++++++++++++++++++++++ tests/lib/rules/no-unused-message-ids.js | 46 +++++++++++++++- tests/lib/utils.js | 35 ++++++++++++ 5 files changed, 163 insertions(+), 2 deletions(-) diff --git a/lib/rules/no-unused-message-ids.js b/lib/rules/no-unused-message-ids.js index 8aa4bfb0..01fc262c 100644 --- a/lib/rules/no-unused-message-ids.js +++ b/lib/rules/no-unused-message-ids.js @@ -120,7 +120,8 @@ module.exports = { if ( values.length === 0 || - values.some((val) => val.type !== 'Literal') + values.some((val) => val.type !== 'Literal') || + utils.isVariableFromParameter(node.value, scopeManager) ) { // When a dynamic messageId is used and we can't detect its value, disable the rule to avoid false positives. hasSeenUnknownMessageId = true; diff --git a/lib/utils.js b/lib/utils.js index d2bc6505..6dbd6b0c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -888,4 +888,19 @@ module.exports = { return []; }); }, + + /** + * Check whether a variable's definition is from a function parameter. + * @param {Node} node - the Identifier node for the variable. + * @param {ScopeManager} scopeManager + * @returns {boolean} whether the variable comes from a function parameter + */ + isVariableFromParameter(node, scopeManager) { + const variable = findVariable( + scopeManager.acquire(node) || scopeManager.globalScope, + node + ); + + return variable?.defs[0]?.type === 'Parameter'; + }, }; diff --git a/tests/lib/rules/no-missing-message-ids.js b/tests/lib/rules/no-missing-message-ids.js index 22a465dd..02cd8a05 100644 --- a/tests/lib/rules/no-missing-message-ids.js +++ b/tests/lib/rules/no-missing-message-ids.js @@ -196,6 +196,48 @@ ruleTester.run('no-missing-message-ids', rule, { } }; `, + // Helper function with messageId parameter, outside rule. + ` + function report(node, messageId) { + context.report({node, messageId}); + } + module.exports = { + meta: { messages: { foo: 'hello' } }, + create(context) { + report(node, 'foo'); + } + }; + `, + // Helper function with messageId parameter, inside rule, with parameter reassignment. + ` + module.exports = { + meta: { messages: { foo: 'hello', bar: 'world' } }, + create(context) { + function report(node, messageId) { + if (foo) { + messageId = 'bar'; + } + context.report({node, messageId}); + } + report(node, 'foo'); + } + }; + `, + // Helper function with messageId parameter, inside rule, with missing messageId. + // TODO: this should be an invalid test case because a non-existent `messageId` is used. + // Eventually, we should be able to detect what values are passed to this function for its `messageId` parameter. + ` + module.exports = { + meta: { messages: { foo: 'hello' } }, + create(context) { + function report(node, messageId) { + context.report({node, messageId}); + } + report(node, 'foo'); + report(node, 'bar'); + } + }; + `, ], invalid: [ @@ -287,5 +329,29 @@ ruleTester.run('no-missing-message-ids', rule, { }, ], }, + { + // Helper function with messageId parameter, inside rule, with missing messageId due to parameter reassignment. + code: ` + module.exports = { + meta: { messages: { foo: 'hello' } }, + create(context) { + function report(node, messageId) { + if (foo) { + messageId = 'bar'; + } + context.report({node, messageId}); + } + report(node, 'foo'); + } + }; + `, + errors: [ + { + messageId: 'missingMessage', + data: { messageId: 'bar' }, + type: 'Literal', + }, + ], + }, ], }); diff --git a/tests/lib/rules/no-unused-message-ids.js b/tests/lib/rules/no-unused-message-ids.js index d35cadfe..52e3f80c 100644 --- a/tests/lib/rules/no-unused-message-ids.js +++ b/tests/lib/rules/no-unused-message-ids.js @@ -227,6 +227,50 @@ ruleTester.run('no-unused-message-ids', rule, { create(context) {} }; `, + // Helper function messageId parameter, outside rule. + ` + function reportFoo(node, messageId) { + context.report({ node, messageId }); + } + module.exports = { + meta: { messages: { foo: 'hello', bar: 'world', baz: 'planet' } }, + create(context) { + reportFoo(node, 'foo'); + reportFoo(node, 'bar'); + reportFoo(node, 'baz'); + } + }; + `, + // Helper function with messageId parameter, inside rule, parameter reassignment. + ` + module.exports = { + meta: { messages: { foo: 'hello', bar: 'world', baz: 'planet' } }, + create(context) { + function reportFoo(node, messageId) { + if (foo) { + messageId = 'baz'; + } + context.report({ node, messageId }); + } + reportFoo(node, 'foo'); + reportFoo(node, 'bar'); + } + }; + `, + // Helper function with messageId parameter, outside rule, with an unused messageId. + // TODO: this should be an invalid test case because a messageId is unused. + // Eventually, we should be able to detect what values are passed to this function for its messageId parameter. + ` + function reportFoo(node, messageId) { + context.report({ node, messageId }); + } + module.exports = { + meta: { messages: { foo: 'hello', bar: 'world' } }, + create(context) { + reportFoo(node, 'foo'); + } + }; + `, ], invalid: [ @@ -363,7 +407,7 @@ ruleTester.run('no-unused-message-ids', rule, { context.report({ node, messageId }); } module.exports = { - meta: { messages: { foo: 'hello world' } }, + meta: { messages: { foo: 'hello world', bar: 'baz' } }, create(context) { reportFoo(node); } diff --git a/tests/lib/utils.js b/tests/lib/utils.js index 66e1b02d..ebebacca 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -1646,4 +1646,39 @@ describe('utils', () => { ); }); }); + + describe('isVariableFromParameter', function () { + it('returns true for function parameter', () => { + const code = + 'function myFunc(x) { if (foo) { x = "abc"; } console.log(x) }; myFunc("def");'; + const ast = espree.parse(code, { + ecmaVersion: 9, + range: true, + }); + + const scopeManager = eslintScope.analyze(ast); + assert.ok( + utils.isVariableFromParameter( + ast.body[0].body.body[1].expression.arguments[0], + scopeManager + ) + ); + }); + + it('returns false for const variable', () => { + const code = 'const x = "abc"; console.log(x);'; + const ast = espree.parse(code, { + ecmaVersion: 9, + range: true, + }); + + const scopeManager = eslintScope.analyze(ast); + assert.notOk( + utils.isVariableFromParameter( + ast.body[1].expression.arguments[0], + scopeManager + ) + ); + }); + }); });