Skip to content

Commit 067e8aa

Browse files
authored
feat: Add new rules no-missing-message-ids and no-unused-message-ids (#254)
* feat: add new rules `no-missing-message-ids` and `no-unused-message-ids` * handle variable messages object keys in no-{missing,unused}-message-ids and add messageId to messages
1 parent 34bcb74 commit 067e8aa

10 files changed

+1392
-17
lines changed

Diff for: README.md

+2
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ Name | ✔️ | 🛠 | 💡 | Description
6767
[no-deprecated-context-methods](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-context-methods.md) | ✔️ | 🛠 | | disallow usage of deprecated methods on rule context objects
6868
[no-deprecated-report-api](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-report-api.md) | ✔️ | 🛠 | | disallow the version of `context.report()` with multiple arguments
6969
[no-identical-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-identical-tests.md) | ✔️ | 🛠 | | disallow identical tests
70+
[no-missing-message-ids](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-missing-message-ids.md) | | | | disallow `messageId`s that are missing from `meta.messages`
7071
[no-missing-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-missing-placeholders.md) | ✔️ | | | disallow missing placeholders in rule report messages
7172
[no-only-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-only-tests.md) | ✔️ | | 💡 | disallow the test case property `only`
73+
[no-unused-message-ids](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-unused-message-ids.md) | | | | disallow unused `messageId`s in `meta.messages`
7274
[no-unused-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-unused-placeholders.md) | ✔️ | | | disallow unused placeholders in rule report messages
7375
[no-useless-token-range](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-useless-token-range.md) | ✔️ | 🛠 | | disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()`
7476
[prefer-message-ids](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-message-ids.md) | | | | require using `messageId` instead of `message` to report rule violations

Diff for: docs/rules/no-missing-message-ids.md

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Disallow `messageId`s that are missing from `meta.messages` (no-missing-message-ids)
2+
3+
When using `meta.messages` and `messageId` to report rule violations, it's possible to mistakenly use a `messageId` that doesn't exist in `meta.messages`.
4+
5+
## Rule Details
6+
7+
Examples of **incorrect** code for this rule:
8+
9+
```js
10+
/* eslint eslint-plugin/no-missing-message-ids: error */
11+
12+
module.exports = {
13+
meta: {
14+
messages: {
15+
foo: 'hello world',
16+
},
17+
},
18+
create(context) {
19+
return {
20+
CallExpression(node) {
21+
context.report({
22+
node,
23+
messageId: 'abc',
24+
});
25+
},
26+
};
27+
},
28+
};
29+
```
30+
31+
Examples of **correct** code for this rule:
32+
33+
```js
34+
/* eslint eslint-plugin/no-missing-message-ids: error */
35+
36+
module.exports = {
37+
meta: {
38+
messages: {
39+
foo: 'hello world',
40+
},
41+
},
42+
create(context) {
43+
return {
44+
CallExpression(node) {
45+
context.report({
46+
node,
47+
messageId: 'foo',
48+
});
49+
},
50+
};
51+
},
52+
};
53+
```
54+
55+
## Further Reading
56+
57+
* [messageIds API](https://eslint.org/docs/developer-guide/working-with-rules#messageids)
58+
* [no-unused-message-ids](./no-unused-message-ids.md) rule
59+
* [prefer-message-ids](./prefer-message-ids.md) rule

Diff for: docs/rules/no-unused-message-ids.md

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Disallow unused `messageId`s in `meta.messages` (no-unused-message-ids)
2+
3+
When using `meta.messages` and `messageId` to report rule violations, it's possible to mistakenly leave a message in `meta.messages` that is never used.
4+
5+
## Rule Details
6+
7+
Examples of **incorrect** code for this rule:
8+
9+
```js
10+
/* eslint eslint-plugin/no-unused-message-ids: error */
11+
12+
module.exports = {
13+
meta: {
14+
messages: {
15+
foo: 'hello world',
16+
bar: 'lorem ipsum', // this message is never used
17+
},
18+
},
19+
create(context) {
20+
return {
21+
CallExpression(node) {
22+
context.report({
23+
node,
24+
messageId: 'foo',
25+
});
26+
},
27+
};
28+
},
29+
};
30+
```
31+
32+
Examples of **correct** code for this rule:
33+
34+
```js
35+
/* eslint eslint-plugin/no-unused-message-ids: error */
36+
37+
module.exports = {
38+
meta: {
39+
messages: {
40+
foo: 'hello world',
41+
},
42+
},
43+
create(context) {
44+
return {
45+
CallExpression(node) {
46+
context.report({
47+
node,
48+
messageId: 'foo',
49+
});
50+
},
51+
};
52+
},
53+
};
54+
```
55+
56+
## Further Reading
57+
58+
* [messageIds API](https://eslint.org/docs/developer-guide/working-with-rules#messageids)
59+
* [no-missing-message-ids](./no-missing-message-ids.md) rule
60+
* [prefer-message-ids](./prefer-message-ids.md) rule

Diff for: docs/rules/prefer-message-ids.md

+2
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,5 @@ module.exports = {
5555
## Further Reading
5656

5757
* [messageIds API](https://eslint.org/docs/developer-guide/working-with-rules#messageids)
58+
* [no-invalid-message-ids](./no-invalid-message-ids.md) rule
59+
* [no-missing-message-ids](./no-missing-message-ids.md) rule

Diff for: lib/rules/no-missing-message-ids.js

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use strict';
2+
3+
const utils = require('../utils');
4+
5+
// ------------------------------------------------------------------------------
6+
// Rule Definition
7+
// ------------------------------------------------------------------------------
8+
9+
/** @type {import('eslint').Rule.RuleModule} */
10+
module.exports = {
11+
meta: {
12+
type: 'problem',
13+
docs: {
14+
description:
15+
'disallow `messageId`s that are missing from `meta.messages`',
16+
category: 'Rules',
17+
recommended: false,
18+
url: 'https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-missing-message-ids.md',
19+
},
20+
fixable: null,
21+
schema: [],
22+
messages: {
23+
missingMessage:
24+
'`meta.messages` is missing the messageId "{{messageId}}".',
25+
},
26+
},
27+
28+
create(context) {
29+
const sourceCode = context.getSourceCode();
30+
const { scopeManager } = sourceCode;
31+
const ruleInfo = utils.getRuleInfo(sourceCode);
32+
33+
const messagesNode = utils.getMessagesNode(ruleInfo, scopeManager);
34+
35+
let contextIdentifiers;
36+
37+
if (!messagesNode || messagesNode.type !== 'ObjectExpression') {
38+
// If we can't find `meta.messages`, disable the rule.
39+
return {};
40+
}
41+
42+
return {
43+
Program(ast) {
44+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
45+
},
46+
47+
CallExpression(node) {
48+
// Check for messageId properties used in known calls to context.report();
49+
if (
50+
node.callee.type === 'MemberExpression' &&
51+
contextIdentifiers.has(node.callee.object) &&
52+
node.callee.property.type === 'Identifier' &&
53+
node.callee.property.name === 'report'
54+
) {
55+
const reportInfo = utils.getReportInfo(node.arguments, context);
56+
if (!reportInfo) {
57+
return;
58+
}
59+
60+
const reportMessagesAndDataArray =
61+
utils.collectReportViolationAndSuggestionData(reportInfo);
62+
for (const { messageId } of reportMessagesAndDataArray.filter(
63+
(obj) => obj.messageId
64+
)) {
65+
const values =
66+
messageId.type === 'Literal'
67+
? [messageId]
68+
: utils.findPossibleVariableValues(messageId, scopeManager);
69+
70+
// Look for any possible string values we found for this messageId.
71+
values.forEach((val) => {
72+
if (
73+
val.type === 'Literal' &&
74+
val.value !== null &&
75+
val.value !== '' &&
76+
!utils.getMessageIdNodeById(
77+
val.value,
78+
ruleInfo,
79+
scopeManager,
80+
context.getScope()
81+
)
82+
)
83+
// Couldn't find this messageId in `meta.messages`.
84+
context.report({
85+
node: val,
86+
messageId: 'missingMessage',
87+
data: {
88+
messageId: val.value,
89+
},
90+
});
91+
});
92+
}
93+
}
94+
},
95+
};
96+
},
97+
};

Diff for: lib/rules/no-unused-message-ids.js

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
'use strict';
2+
3+
const utils = require('../utils');
4+
5+
// ------------------------------------------------------------------------------
6+
// Rule Definition
7+
// ------------------------------------------------------------------------------
8+
9+
/** @type {import('eslint').Rule.RuleModule} */
10+
module.exports = {
11+
meta: {
12+
type: 'problem',
13+
docs: {
14+
description: 'disallow unused `messageId`s in `meta.messages`',
15+
category: 'Rules',
16+
recommended: false,
17+
url: 'https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-unused-message-ids.md',
18+
},
19+
fixable: null,
20+
schema: [],
21+
messages: {
22+
unusedMessage: 'The messageId "{{messageId}}" is never used.',
23+
},
24+
},
25+
26+
create(context) {
27+
const sourceCode = context.getSourceCode();
28+
const { scopeManager } = sourceCode;
29+
const info = utils.getRuleInfo(sourceCode);
30+
31+
const messageIdsUsed = new Set();
32+
let contextIdentifiers;
33+
let shouldPerformUnusedCheck = true;
34+
35+
const messageIdNodes = utils.getMessageIdNodes(info, scopeManager);
36+
if (!messageIdNodes) {
37+
// If we can't find `meta.messages`, disable the rule.
38+
return {};
39+
}
40+
41+
return {
42+
Program(ast) {
43+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
44+
},
45+
46+
'Program:exit'() {
47+
if (shouldPerformUnusedCheck) {
48+
const messageIdNodesUnused = messageIdNodes.filter(
49+
(node) =>
50+
!messageIdsUsed.has(utils.getKeyName(node, context.getScope()))
51+
);
52+
53+
// Report any messageIds that were never used.
54+
for (const messageIdNode of messageIdNodesUnused) {
55+
context.report({
56+
node: messageIdNode,
57+
messageId: 'unusedMessage',
58+
data: {
59+
messageId: utils.getKeyName(messageIdNode, context.getScope()),
60+
},
61+
});
62+
}
63+
}
64+
},
65+
66+
CallExpression(node) {
67+
// Check for messageId properties used in known calls to context.report();
68+
if (
69+
node.callee.type === 'MemberExpression' &&
70+
contextIdentifiers.has(node.callee.object) &&
71+
node.callee.property.type === 'Identifier' &&
72+
node.callee.property.name === 'report'
73+
) {
74+
const reportInfo = utils.getReportInfo(node.arguments, context);
75+
if (!reportInfo) {
76+
return;
77+
}
78+
79+
const reportMessagesAndDataArray =
80+
utils.collectReportViolationAndSuggestionData(reportInfo);
81+
for (const { messageId } of reportMessagesAndDataArray.filter(
82+
(obj) => obj.messageId
83+
)) {
84+
const values =
85+
messageId.type === 'Literal'
86+
? [messageId]
87+
: utils.findPossibleVariableValues(messageId, scopeManager);
88+
if (
89+
values.length === 0 ||
90+
values.some((val) => val.type !== 'Literal')
91+
) {
92+
// When a dynamic messageId is used and we can't detect its value, disable the rule to avoid false positives.
93+
shouldPerformUnusedCheck = false;
94+
}
95+
values.forEach((val) => messageIdsUsed.add(val.value));
96+
}
97+
}
98+
},
99+
100+
Property(node) {
101+
// In order to reduce false positives, we will also check for messageId properties anywhere in the file.
102+
// This is helpful especially in the event that helper functions are used for reporting violations.
103+
if (node.key.type === 'Identifier' && node.key.name === 'messageId') {
104+
const values =
105+
node.value.type === 'Literal'
106+
? [node.value]
107+
: utils.findPossibleVariableValues(node.value, scopeManager);
108+
if (
109+
values.length === 0 ||
110+
values.some((val) => val.type !== 'Literal')
111+
) {
112+
// When a dynamic messageId is used and we can't detect its value, disable the rule to avoid false positives.
113+
shouldPerformUnusedCheck = false;
114+
}
115+
values.forEach((val) => messageIdsUsed.add(val.value));
116+
}
117+
},
118+
};
119+
},
120+
};

0 commit comments

Comments
 (0)