Skip to content

Commit da64dc0

Browse files
committed
feat: add new rules no-missing-message-ids and no-unused-message-ids
1 parent 34bcb74 commit da64dc0

10 files changed

+1192
-1
lines changed

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

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

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

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

lib/rules/no-missing-message-ids.js

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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: '`meta.messages` is missing this `messageId`.',
24+
},
25+
},
26+
27+
create(context) {
28+
const sourceCode = context.getSourceCode();
29+
const { scopeManager } = sourceCode;
30+
const ruleInfo = utils.getRuleInfo(sourceCode);
31+
32+
const messagesNode = utils.getMessagesNode(ruleInfo, scopeManager);
33+
34+
let contextIdentifiers;
35+
36+
if (!messagesNode || messagesNode.type !== 'ObjectExpression') {
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+
CallExpression(node) {
47+
// Check for messageId properties used in known calls to context.report();
48+
if (
49+
node.callee.type === 'MemberExpression' &&
50+
contextIdentifiers.has(node.callee.object) &&
51+
node.callee.property.type === 'Identifier' &&
52+
node.callee.property.name === 'report'
53+
) {
54+
const reportInfo = utils.getReportInfo(node.arguments, context);
55+
if (!reportInfo) {
56+
return;
57+
}
58+
59+
const reportMessagesAndDataArray =
60+
utils.collectReportViolationAndSuggestionData(reportInfo);
61+
for (const { messageId } of reportMessagesAndDataArray.filter(
62+
(obj) => obj.messageId
63+
)) {
64+
const values =
65+
messageId.type === 'Literal'
66+
? [messageId]
67+
: utils.findPossibleVariableValues(messageId, scopeManager);
68+
69+
// Look for any possible string values we found for this messageId.
70+
values.forEach((val) => {
71+
if (
72+
val.type === 'Literal' &&
73+
val.value !== null &&
74+
val.value !== '' &&
75+
!utils.getMessageIdNodeById(val.value, ruleInfo, scopeManager)
76+
)
77+
// Couldn't find this messageId in `meta.messages`.
78+
context.report({
79+
node: val,
80+
messageId: 'missingMessage',
81+
});
82+
});
83+
}
84+
}
85+
},
86+
};
87+
},
88+
};

lib/rules/no-unused-message-ids.js

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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: 'This message 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+
for (const messageIdNode of messageIdNodes.filter(
49+
(node) => !messageIdsUsed.has(node.key.name)
50+
)) {
51+
context.report({
52+
node: messageIdNode,
53+
messageId: 'unusedMessage',
54+
});
55+
}
56+
}
57+
},
58+
59+
CallExpression(node) {
60+
// Check for messageId properties used in known calls to context.report();
61+
if (
62+
node.callee.type === 'MemberExpression' &&
63+
contextIdentifiers.has(node.callee.object) &&
64+
node.callee.property.type === 'Identifier' &&
65+
node.callee.property.name === 'report'
66+
) {
67+
const reportInfo = utils.getReportInfo(node.arguments, context);
68+
if (!reportInfo) {
69+
return;
70+
}
71+
72+
const reportMessagesAndDataArray =
73+
utils.collectReportViolationAndSuggestionData(reportInfo);
74+
for (const { messageId } of reportMessagesAndDataArray.filter(
75+
(obj) => obj.messageId
76+
)) {
77+
const values =
78+
messageId.type === 'Literal'
79+
? [messageId]
80+
: utils.findPossibleVariableValues(messageId, scopeManager);
81+
if (
82+
values.length === 0 ||
83+
values.some((val) => val.type !== 'Literal')
84+
) {
85+
// When a dynamic messageId is used and we can't detect its value, disable the rule to avoid false positives.
86+
shouldPerformUnusedCheck = false;
87+
}
88+
values.forEach((val) => messageIdsUsed.add(val.value));
89+
}
90+
}
91+
},
92+
93+
Property(node) {
94+
// In order to reduce false positives, we will also check for messageId properties anywhere in the file.
95+
// This is helpful especially in the event that helper functions are used for reporting violations.
96+
if (node.key.type === 'Identifier' && node.key.name === 'messageId') {
97+
const values =
98+
node.value.type === 'Literal'
99+
? [node.value]
100+
: utils.findPossibleVariableValues(node.value, scopeManager);
101+
if (
102+
values.length === 0 ||
103+
values.some((val) => val.type !== 'Literal')
104+
) {
105+
// When a dynamic messageId is used and we can't detect its value, disable the rule to avoid false positives.
106+
shouldPerformUnusedCheck = false;
107+
}
108+
values.forEach((val) => messageIdsUsed.add(val.value));
109+
}
110+
},
111+
};
112+
},
113+
};

0 commit comments

Comments
 (0)