Skip to content

Commit 6d5be9f

Browse files
authored
Fix: improve detection of static arguments of context.report() in several rules (#129)
Several of our rules check the arguments of `context.report()`. This PR makes some improvements: 1. When inferring the arguments passed to `context.report()` in `utils.getReportInfo()`, take into account the static value of the second argument when available. 2. Update the rules using `utils.getReportInfo()` to also consider the static value or variable declaration of the message argument when necessary. I noticed these issues because in a number of my plugins, we have stored rule error messages in variables like `const MESSAGE = 'do A instead of B';`, and this prevented many of our rules from operating correctly. Example of how this improvement enables the `no-deprecated-report-api` rule to correctly autofix this code: ```js const MESSAGE = 'do A instead of B'; module.exports = { create(context) { // Before autofix context.report(theNode, MESSAGE); // After autofix context.report({node: theNode, message: MESSAGE}); } }; ```
1 parent b4320c6 commit 6d5be9f

13 files changed

+298
-31
lines changed

lib/rules/no-deprecated-report-api.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ module.exports = {
4848
fix (fixer) {
4949
const openingParen = sourceCode.getTokenBefore(node.arguments[0]);
5050
const closingParen = sourceCode.getLastToken(node);
51-
const reportInfo = utils.getReportInfo(node.arguments);
51+
const reportInfo = utils.getReportInfo(node.arguments, context);
5252

5353
if (!reportInfo) {
5454
return null;

lib/rules/no-missing-placeholders.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
'use strict';
77

88
const utils = require('../utils');
9+
const { getStaticValue } = require('eslint-utils');
910

1011
// ------------------------------------------------------------------------------
1112
// Rule Definition
@@ -40,21 +41,25 @@ module.exports = {
4041
contextIdentifiers.has(node.callee.object) &&
4142
node.callee.property.type === 'Identifier' && node.callee.property.name === 'report'
4243
) {
43-
const reportInfo = utils.getReportInfo(node.arguments);
44+
const reportInfo = utils.getReportInfo(node.arguments, context);
45+
if (!reportInfo || !reportInfo.message) {
46+
return;
47+
}
4448

49+
const messageStaticValue = getStaticValue(reportInfo.message, context.getScope());
4550
if (
46-
reportInfo &&
47-
reportInfo.message &&
48-
reportInfo.message.type === 'Literal' &&
49-
typeof reportInfo.message.value === 'string' &&
51+
(
52+
(reportInfo.message.type === 'Literal' && typeof reportInfo.message.value === 'string') ||
53+
(messageStaticValue && typeof messageStaticValue.value === 'string')
54+
) &&
5055
(!reportInfo.data || reportInfo.data.type === 'ObjectExpression')
5156
) {
5257
// Same regex as the one ESLint uses
5358
// https://github.com/eslint/eslint/blob/e5446449d93668ccbdb79d78cc69f165ce4fde07/lib/eslint.js#L990
5459
const PLACEHOLDER_MATCHER = /\{\{\s*([^{}]+?)\s*\}\}/g;
5560
let match;
5661

57-
while ((match = PLACEHOLDER_MATCHER.exec(reportInfo.message.value))) { // eslint-disable-line no-extra-parens
62+
while ((match = PLACEHOLDER_MATCHER.exec(reportInfo.message.value || messageStaticValue.value))) { // eslint-disable-line no-extra-parens
5863
const matchingProperty = reportInfo.data &&
5964
reportInfo.data.properties.find(prop => utils.getKeyName(prop) === match[1]);
6065

lib/rules/no-unused-placeholders.js

+13-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
'use strict';
77

88
const utils = require('../utils');
9+
const { getStaticValue } = require('eslint-utils');
910

1011
// ------------------------------------------------------------------------------
1112
// Rule Definition
@@ -40,16 +41,21 @@ module.exports = {
4041
contextIdentifiers.has(node.callee.object) &&
4142
node.callee.property.type === 'Identifier' && node.callee.property.name === 'report'
4243
) {
43-
const reportInfo = utils.getReportInfo(node.arguments);
44+
const reportInfo = utils.getReportInfo(node.arguments, context);
45+
if (!reportInfo || !reportInfo.message) {
46+
return;
47+
}
4448

49+
const messageStaticValue = getStaticValue(reportInfo.message, context.getScope());
4550
if (
46-
reportInfo &&
47-
reportInfo.message &&
48-
reportInfo.message.type === 'Literal' &&
49-
typeof reportInfo.message.value === 'string' &&
50-
reportInfo.data && reportInfo.data.type === 'ObjectExpression'
51+
(
52+
(reportInfo.message.type === 'Literal' && typeof reportInfo.message.value === 'string') ||
53+
(messageStaticValue && typeof messageStaticValue.value === 'string')
54+
) &&
55+
reportInfo.data &&
56+
reportInfo.data.type === 'ObjectExpression'
5157
) {
52-
const message = reportInfo.message.value;
58+
const message = reportInfo.message.value || messageStaticValue.value;
5359
// https://github.com/eslint/eslint/blob/2874d75ed8decf363006db25aac2d5f8991bd969/lib/linter.js#L986
5460
const PLACEHOLDER_MATCHER = /\{\{\s*([^{}]+?)\s*\}\}/g;
5561
const placeholdersInMessage = new Set();

lib/rules/prefer-placeholders.js

+36-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
'use strict';
77

88
const utils = require('../utils');
9+
const { findVariable } = require('eslint-utils');
910

1011
// ------------------------------------------------------------------------------
1112
// Rule Definition
@@ -26,6 +27,9 @@ module.exports = {
2627
create (context) {
2728
let contextIdentifiers;
2829

30+
const sourceCode = context.getSourceCode();
31+
const { scopeManager } = sourceCode;
32+
2933
// ----------------------------------------------------------------------
3034
// Public
3135
// ----------------------------------------------------------------------
@@ -40,16 +44,42 @@ module.exports = {
4044
contextIdentifiers.has(node.callee.object) &&
4145
node.callee.property.type === 'Identifier' && node.callee.property.name === 'report'
4246
) {
43-
const reportInfo = utils.getReportInfo(node.arguments);
47+
const reportInfo = utils.getReportInfo(node.arguments, context);
48+
49+
if (!reportInfo || !reportInfo.message) {
50+
return;
51+
}
52+
53+
let messageNode = reportInfo.message;
54+
55+
if (messageNode.type === 'Identifier') {
56+
// See if we can find the variable declaration.
57+
58+
const variable = findVariable(
59+
scopeManager.acquire(messageNode) || scopeManager.globalScope,
60+
messageNode
61+
);
62+
63+
if (
64+
!variable ||
65+
!variable.defs ||
66+
!variable.defs[0] ||
67+
!variable.defs[0].node ||
68+
variable.defs[0].node.type !== 'VariableDeclarator' ||
69+
!variable.defs[0].node.init
70+
) {
71+
return;
72+
}
73+
74+
messageNode = variable.defs[0].node.init;
75+
}
4476

4577
if (
46-
reportInfo && reportInfo.message && (
47-
(reportInfo.message.type === 'TemplateLiteral' && reportInfo.message.expressions.length) ||
48-
(reportInfo.message.type === 'BinaryExpression' && reportInfo.message.operator === '+')
49-
)
78+
(messageNode.type === 'TemplateLiteral' && messageNode.expressions.length) ||
79+
(messageNode.type === 'BinaryExpression' && messageNode.operator === '+')
5080
) {
5181
context.report({
52-
node: reportInfo.message,
82+
node: messageNode,
5383
message: 'Use report message placeholders instead of string concatenation.',
5484
});
5585
}

lib/rules/prefer-replace-text.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ module.exports = {
4747
parent.parent.parent.type === 'CallExpression' &&
4848
contextIdentifiers.has(parent.parent.parent.callee.object) &&
4949
parent.parent.parent.callee.property.name === 'report' &&
50-
utils.getReportInfo(parent.parent.parent.arguments).fix === node;
50+
utils.getReportInfo(parent.parent.parent.arguments, context).fix === node;
5151

5252
funcInfo = {
5353
upper: funcInfo,

lib/rules/report-message-format.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
'use strict';
77

8+
const { getStaticValue } = require('eslint-utils');
89
const utils = require('../utils');
910

1011
// ------------------------------------------------------------------------------
@@ -35,9 +36,11 @@ module.exports = {
3536
* @returns {void}
3637
*/
3738
function processMessageNode (message) {
39+
const staticValue = getStaticValue(message, context.getScope());
3840
if (
3941
(message.type === 'Literal' && typeof message.value === 'string' && !pattern.test(message.value)) ||
40-
(message.type === 'TemplateLiteral' && message.quasis.length === 1 && !pattern.test(message.quasis[0].value.cooked))
42+
(message.type === 'TemplateLiteral' && message.quasis.length === 1 && !pattern.test(message.quasis[0].value.cooked)) ||
43+
(staticValue && !pattern.test(staticValue.value))
4144
) {
4245
context.report({
4346
node: message,
@@ -75,7 +78,7 @@ module.exports = {
7578
contextIdentifiers.has(node.callee.object) &&
7679
node.callee.property.type === 'Identifier' && node.callee.property.name === 'report'
7780
) {
78-
const reportInfo = utils.getReportInfo(node.arguments);
81+
const reportInfo = utils.getReportInfo(node.arguments, context);
7982
const message = reportInfo && reportInfo.message;
8083

8184
if (!message) {

lib/utils.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
const { getStaticValue } = require('eslint-utils');
4+
35
/**
46
* Determines whether a node is a 'normal' (i.e. non-async, non-generator) function expression.
57
* @param {ASTNode} node The node in question
@@ -260,8 +262,9 @@ module.exports = {
260262
/**
261263
* Gets information on a report, given the arguments passed to context.report().
262264
* @param {ASTNode[]} reportArgs The arguments passed to context.report()
265+
* @param {Context} context
263266
*/
264-
getReportInfo (reportArgs) {
267+
getReportInfo (reportArgs, context) {
265268
// If there is exactly one argument, the API expects an object.
266269
// Otherwise, if the second argument is a string, the arguments are interpreted as
267270
// ['node', 'message', 'data', 'fix'].
@@ -287,15 +290,17 @@ module.exports = {
287290

288291
let keys;
289292

293+
const secondArgStaticValue = getStaticValue(reportArgs[1], context.getScope());
290294
if (
291-
(reportArgs[1].type === 'Literal' && typeof reportArgs[1].value === 'string') ||
295+
(secondArgStaticValue && typeof secondArgStaticValue.value === 'string') ||
292296
reportArgs[1].type === 'TemplateLiteral'
293297
) {
294298
keys = ['node', 'message', 'data', 'fix'];
295299
} else if (
296300
reportArgs[1].type === 'ObjectExpression' ||
297301
reportArgs[1].type === 'ArrayExpression' ||
298-
(reportArgs[1].type === 'Literal' && typeof reportArgs[1].value !== 'string')
302+
(reportArgs[1].type === 'Literal' && typeof reportArgs[1].value !== 'string') ||
303+
(secondArgStaticValue && ['object', 'number'].includes(typeof secondArgStaticValue.value))
299304
) {
300305
keys = ['node', 'loc', 'message', 'data', 'fix'];
301306
} else {

tests/lib/rules/no-deprecated-report-api.js

+94
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,24 @@ ruleTester.run('no-deprecated-report-api', rule, {
6262
}
6363
};
6464
`,
65+
// With object as variable.
66+
`
67+
const OBJ = {node, message};
68+
module.exports = {
69+
create(context) {
70+
context.report(OBJ);
71+
}
72+
};
73+
`,
74+
// With object as variable but cannot determine its value statically.
75+
`
76+
const OBJ = getObj();
77+
module.exports = {
78+
create(context) {
79+
context.report(OBJ);
80+
}
81+
};
82+
`,
6583
],
6684

6785
invalid: [
@@ -161,6 +179,39 @@ ruleTester.run('no-deprecated-report-api', rule, {
161179
`,
162180
errors: [ERROR],
163181
},
182+
{
183+
// With message string in variable.
184+
code: `
185+
const MESSAGE = 'foo';
186+
module.exports = {
187+
create(context) {
188+
context.report(theNode, MESSAGE);
189+
}
190+
};
191+
`,
192+
output: `
193+
const MESSAGE = 'foo';
194+
module.exports = {
195+
create(context) {
196+
context.report({node: theNode, message: MESSAGE});
197+
}
198+
};
199+
`,
200+
errors: [ERROR],
201+
},
202+
{
203+
// With message in variable but no autofix since we can't statically determine its type.
204+
code: `
205+
const MESSAGE = getMessage();
206+
module.exports = {
207+
create(context) {
208+
context.report(theNode, MESSAGE);
209+
}
210+
};
211+
`,
212+
output: null,
213+
errors: [ERROR],
214+
},
164215
{
165216
code: `
166217
module.exports = {
@@ -198,6 +249,49 @@ ruleTester.run('no-deprecated-report-api', rule, {
198249
`,
199250
errors: [ERROR],
200251
},
252+
{
253+
// Location in variable as number.
254+
code: `
255+
const LOC = 5;
256+
module.exports.create = context => {
257+
context.report(theNode, LOC, foo, bar);
258+
};
259+
`,
260+
output: `
261+
const LOC = 5;
262+
module.exports.create = context => {
263+
context.report({node: theNode, loc: LOC, message: foo, data: bar});
264+
};
265+
`,
266+
errors: [ERROR],
267+
},
268+
{
269+
// Location in variable as object.
270+
code: `
271+
const LOC = { line: 1, column: 2 };
272+
module.exports.create = context => {
273+
context.report(theNode, LOC, foo, bar);
274+
};
275+
`,
276+
output: `
277+
const LOC = { line: 1, column: 2 };
278+
module.exports.create = context => {
279+
context.report({node: theNode, loc: LOC, message: foo, data: bar});
280+
};
281+
`,
282+
errors: [ERROR],
283+
},
284+
{
285+
// Location in variable but no autofix since we can't statically determine its type.
286+
code: `
287+
const LOC = getLoc();
288+
module.exports.create = context => {
289+
context.report(theNode, LOC, foo, bar);
290+
};
291+
`,
292+
output: null,
293+
errors: [ERROR],
294+
},
201295
{
202296
code: `
203297
module.exports = {

tests/lib/rules/no-missing-placeholders.js

+26-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ const RuleTester = require('eslint').RuleTester;
1717
* @param {string} missingKey The placeholder that is missing
1818
* @returns {object} An expected error
1919
*/
20-
function error (missingKey) {
21-
return { type: 'Literal', message: `The placeholder {{${missingKey}}} does not exist.` };
20+
function error (missingKey, type = 'Literal') {
21+
return { type, message: `The placeholder {{${missingKey}}} does not exist.` };
2222
}
2323

2424
// ------------------------------------------------------------------------------
@@ -114,6 +114,20 @@ ruleTester.run('no-missing-placeholders', rule, {
114114
}
115115
};
116116
`,
117+
// Message in variable.
118+
`
119+
const MESSAGE = 'foo {{bar}}';
120+
module.exports = context => {
121+
context.report(node, MESSAGE, { bar: 'baz' });
122+
};
123+
`,
124+
// Message in variable but cannot statically determine its type.
125+
`
126+
const MESSAGE = getMessage();
127+
module.exports = context => {
128+
context.report(node, MESSAGE, { baz: 'qux' });
129+
};
130+
`,
117131
],
118132

119133
invalid: [
@@ -166,6 +180,16 @@ ruleTester.run('no-missing-placeholders', rule, {
166180
`,
167181
errors: [error('bar')],
168182
},
183+
{
184+
// Message in variable.
185+
code: `
186+
const MESSAGE = 'foo {{bar}}';
187+
module.exports = context => {
188+
context.report(node, MESSAGE, { baz: 'qux' });
189+
};
190+
`,
191+
errors: [error('bar', 'Identifier')],
192+
},
169193
{
170194
code: `
171195
module.exports = context => {

0 commit comments

Comments
 (0)