Skip to content

Commit 43a3550

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

10 files changed

+961
-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

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
'use strict';
2+
3+
const utils = require('../utils');
4+
const { findVariable } = require('eslint-utils');
5+
6+
// ------------------------------------------------------------------------------
7+
// Rule Definition
8+
// ------------------------------------------------------------------------------
9+
10+
/** @type {import('eslint').Rule.RuleModule} */
11+
module.exports = {
12+
meta: {
13+
type: 'problem',
14+
docs: {
15+
description:
16+
'disallow `messageId`s that are missing from `meta.messages`',
17+
category: 'Rules',
18+
recommended: false,
19+
url: 'https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-missing-message-ids.md',
20+
},
21+
fixable: null,
22+
schema: [],
23+
messages: {
24+
missingMessage: '`meta.messages` is missing this `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+
// ----------------------------------------------------------------------
38+
// Public
39+
// ----------------------------------------------------------------------
40+
41+
if (!messagesNode || messagesNode.type !== 'ObjectExpression') {
42+
return {};
43+
}
44+
45+
return {
46+
Program(ast) {
47+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
48+
},
49+
50+
CallExpression(node) {
51+
if (
52+
node.callee.type === 'MemberExpression' &&
53+
contextIdentifiers.has(node.callee.object) &&
54+
node.callee.property.type === 'Identifier' &&
55+
node.callee.property.name === 'report'
56+
) {
57+
const reportInfo = utils.getReportInfo(node.arguments, context);
58+
if (!reportInfo) {
59+
return;
60+
}
61+
62+
const reportMessagesAndDataArray =
63+
utils.collectReportViolationAndSuggestionData(reportInfo);
64+
65+
for (const { messageId } of reportMessagesAndDataArray.filter(
66+
(obj) => obj.messageId
67+
)) {
68+
const values =
69+
messageId.type === 'Literal'
70+
? [messageId]
71+
: utils.findPossibleVariableValues(messageId, scopeManager);
72+
73+
values.forEach((val) => {
74+
if (
75+
val.type === 'Literal' &&
76+
!utils.getMessageIdNodeById(val.value, ruleInfo, scopeManager)
77+
)
78+
context.report({
79+
node: val,
80+
messageId: 'missingMessage',
81+
});
82+
});
83+
}
84+
}
85+
},
86+
};
87+
},
88+
};

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

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
'use strict';
2+
3+
const utils = require('../utils');
4+
const { findVariable } = require('eslint-utils');
5+
6+
// ------------------------------------------------------------------------------
7+
// Rule Definition
8+
// ------------------------------------------------------------------------------
9+
10+
/** @type {import('eslint').Rule.RuleModule} */
11+
module.exports = {
12+
meta: {
13+
type: 'problem',
14+
docs: {
15+
description: 'disallow unused `messageId`s in `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-unused-message-ids.md',
19+
},
20+
fixable: null,
21+
schema: [],
22+
messages: {
23+
unusedMessage: 'This message is never used.',
24+
},
25+
},
26+
27+
create(context) {
28+
const sourceCode = context.getSourceCode();
29+
const { scopeManager } = sourceCode;
30+
const info = utils.getRuleInfo(sourceCode);
31+
32+
const messageIdsUsed = new Set();
33+
let contextIdentifiers;
34+
let shouldPerformUnusedCheck = true;
35+
36+
const messageIdNodes = utils.getMessageIdNodes(info, scopeManager);
37+
if (!messageIdNodes) {
38+
return {};
39+
}
40+
41+
// ----------------------------------------------------------------------
42+
// Public
43+
// ----------------------------------------------------------------------
44+
45+
return {
46+
Program(ast) {
47+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
48+
},
49+
50+
'Program:exit'() {
51+
if (shouldPerformUnusedCheck) {
52+
for (const messageIdNode of messageIdNodes.filter(
53+
(node) => !messageIdsUsed.has(node.key.name)
54+
)) {
55+
context.report({
56+
node: messageIdNode,
57+
messageId: 'unusedMessage',
58+
});
59+
}
60+
}
61+
},
62+
63+
CallExpression(node) {
64+
if (
65+
node.callee.type === 'MemberExpression' &&
66+
contextIdentifiers.has(node.callee.object) &&
67+
node.callee.property.type === 'Identifier' &&
68+
node.callee.property.name === 'report'
69+
) {
70+
const reportInfo = utils.getReportInfo(node.arguments, context);
71+
if (!reportInfo) {
72+
return;
73+
}
74+
75+
const reportMessagesAndDataArray =
76+
utils.collectReportViolationAndSuggestionData(reportInfo);
77+
78+
for (const { messageId } of reportMessagesAndDataArray.filter(
79+
(obj) => obj.messageId
80+
)) {
81+
const values =
82+
messageId.type === 'Literal'
83+
? [messageId]
84+
: utils.findPossibleVariableValues(messageId, scopeManager);
85+
if (values.some((val) => val.type !== 'Literal')) {
86+
shouldPerformUnusedCheck = false;
87+
}
88+
values.forEach((val) => {
89+
messageIdsUsed.add(val.value);
90+
});
91+
}
92+
}
93+
},
94+
};
95+
},
96+
};

lib/utils.js

+61
Original file line numberDiff line numberDiff line change
@@ -762,4 +762,65 @@ 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 (ref.writeExpr) {
820+
// Given node `x`, get `123` from `x = 123;`.
821+
return [ref.writeExpr];
822+
}
823+
return [];
824+
});
825+
},
765826
};

0 commit comments

Comments
 (0)