Skip to content

Commit d91d1a0

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

10 files changed

+1065
-0
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

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
// ----------------------------------------------------------------------
37+
// Public
38+
// ----------------------------------------------------------------------
39+
40+
if (!messagesNode || messagesNode.type !== 'ObjectExpression') {
41+
return {};
42+
}
43+
44+
return {
45+
Program(ast) {
46+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
47+
},
48+
49+
CallExpression(node) {
50+
if (
51+
node.callee.type === 'MemberExpression' &&
52+
contextIdentifiers.has(node.callee.object) &&
53+
node.callee.property.type === 'Identifier' &&
54+
node.callee.property.name === 'report'
55+
) {
56+
const reportInfo = utils.getReportInfo(node.arguments, context);
57+
if (!reportInfo) {
58+
return;
59+
}
60+
61+
const reportMessagesAndDataArray =
62+
utils.collectReportViolationAndSuggestionData(reportInfo);
63+
64+
for (const { messageId } of reportMessagesAndDataArray.filter(
65+
(obj) => obj.messageId
66+
)) {
67+
const values =
68+
messageId.type === 'Literal'
69+
? [messageId]
70+
: utils.findPossibleVariableValues(messageId, scopeManager);
71+
72+
values.forEach((val) => {
73+
if (
74+
val.type === 'Literal' &&
75+
val.value !== null &&
76+
val.value !== '' &&
77+
!utils.getMessageIdNodeById(val.value, ruleInfo, scopeManager)
78+
)
79+
context.report({
80+
node: val,
81+
messageId: 'missingMessage',
82+
});
83+
});
84+
}
85+
}
86+
},
87+
};
88+
},
89+
};

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

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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+
return {};
38+
}
39+
40+
// ----------------------------------------------------------------------
41+
// Public
42+
// ----------------------------------------------------------------------
43+
44+
return {
45+
Program(ast) {
46+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
47+
},
48+
49+
'Program:exit'() {
50+
if (shouldPerformUnusedCheck) {
51+
for (const messageIdNode of messageIdNodes.filter(
52+
(node) => !messageIdsUsed.has(node.key.name)
53+
)) {
54+
context.report({
55+
node: messageIdNode,
56+
messageId: 'unusedMessage',
57+
});
58+
}
59+
}
60+
},
61+
62+
CallExpression(node) {
63+
if (
64+
node.callee.type === 'MemberExpression' &&
65+
contextIdentifiers.has(node.callee.object) &&
66+
node.callee.property.type === 'Identifier' &&
67+
node.callee.property.name === 'report'
68+
) {
69+
const reportInfo = utils.getReportInfo(node.arguments, context);
70+
if (!reportInfo) {
71+
return;
72+
}
73+
74+
const reportMessagesAndDataArray =
75+
utils.collectReportViolationAndSuggestionData(reportInfo);
76+
77+
for (const { messageId } of reportMessagesAndDataArray.filter(
78+
(obj) => obj.messageId
79+
)) {
80+
const values =
81+
messageId.type === 'Literal'
82+
? [messageId]
83+
: utils.findPossibleVariableValues(messageId, scopeManager);
84+
if (
85+
values.length === 0 ||
86+
values.some((val) => val.type !== 'Literal')
87+
) {
88+
shouldPerformUnusedCheck = false;
89+
}
90+
values.forEach((val) => {
91+
messageIdsUsed.add(val.value);
92+
});
93+
}
94+
}
95+
},
96+
};
97+
},
98+
};

lib/utils.js

+65
Original file line numberDiff line numberDiff line change
@@ -762,4 +762,69 @@ module.exports = {
762762
return [property];
763763
});
764764
},
765+
766+
getMessagesNode(ruleInfo, scopeManager) {
767+
if (!ruleInfo) {
768+
return;
769+
}
770+
771+
const metaNode = ruleInfo.meta;
772+
const messagesNode = module.exports
773+
.evaluateObjectProperties(metaNode, scopeManager)
774+
.find(
775+
(p) =>
776+
p.type === 'Property' && module.exports.getKeyName(p) === 'messages'
777+
);
778+
779+
if (messagesNode) {
780+
if (messagesNode.value.type === 'ObjectExpression') {
781+
return messagesNode.value;
782+
}
783+
const value = findVariableValue(messagesNode.value, scopeManager);
784+
if (value && value.type === 'ObjectExpression') {
785+
return value;
786+
}
787+
}
788+
},
789+
790+
getMessageIdNodes(ruleInfo, scopeManager) {
791+
const messagesNode = module.exports.getMessagesNode(ruleInfo, scopeManager);
792+
793+
return messagesNode && messagesNode.type === 'ObjectExpression'
794+
? module.exports.evaluateObjectProperties(messagesNode, scopeManager)
795+
: undefined;
796+
},
797+
798+
getMessageIdNodeById(messageId, ruleInfo, scopeManager) {
799+
return module.exports
800+
.getMessageIdNodes(ruleInfo, scopeManager)
801+
.find(
802+
(p) =>
803+
p.type === 'Property' && module.exports.getKeyName(p) === messageId
804+
);
805+
},
806+
807+
/**
808+
* Get the values (or functions) that a variable is initialized to.
809+
* @param {Node} node - the Identifier node for the variable.
810+
* @param {ScopeManager} scopeManager
811+
* @returns the values (or functions) that the given variable is initialized to.
812+
*/
813+
findPossibleVariableValues(node, scopeManager) {
814+
const variable = findVariable(
815+
scopeManager.acquire(node) || scopeManager.globalScope,
816+
node
817+
);
818+
return ((variable && variable.references) || []).flatMap((ref) => {
819+
if (
820+
ref.writeExpr &&
821+
(ref.writeExpr.parent.type !== 'AssignmentExpression' ||
822+
ref.writeExpr.parent.operator === '=')
823+
) {
824+
// Given node `x`, get `123` from `x = 123;`.
825+
return [ref.writeExpr];
826+
}
827+
return [];
828+
});
829+
},
765830
};

0 commit comments

Comments
 (0)