Skip to content

Commit ffab432

Browse files
New: report-message-format rule
1 parent 63415f8 commit ffab432

File tree

7 files changed

+377
-0
lines changed

7 files changed

+377
-0
lines changed

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,4 @@ Additionally, you can enable all recommended rules from this plugin:
5757
🛠 indicates that a rule is fixable.
5858

5959
* ✔️ 🛠 [no-deprecated-report-api](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-report-api.md): Prohibits the deprecated `context.report(node, message)` API
60+
[report-message-format](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/report-message-format.md): Enforces a consistent format for report messages

Diff for: docs/rules/report-message-format.md

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# enforce a consistent format for rule report messages (report-message-format)
2+
3+
It is sometimes desirable to maintain consistent formatting for all report messages. For example, you might want to mandate that all report messages begin with a capital letter and end with a period.
4+
5+
## Rule Details
6+
7+
This rule aims to enforce a consistent format for rule report messages.
8+
9+
### Options
10+
11+
This rule has a string option. The string should be a regular expression that all report messages must match.
12+
13+
For example, in order to mandate that all report messages begin with a capital letter and end with a period, you can use the following configuration:
14+
15+
```json
16+
{
17+
"rules": {
18+
"eslint-plugin/report-message-format": ["error", "^[A-Z].*\\.$"]
19+
},
20+
"plugins": [
21+
"eslint-plugin"
22+
]
23+
}
24+
```
25+
26+
Note that since this rule uses static analysis and does not actually run your code, it will attempt to match report messages *before* placeholders are inserted.
27+
28+
The following patterns are considered warnings:
29+
30+
```js
31+
/* eslint eslint-plugin/report-message-format: ["error", "^[A-Z].*\\.$"] */
32+
33+
module.exports = {
34+
meta: {},
35+
create(context) {
36+
37+
context.report(node, 'this message does not match the regular expression.');
38+
39+
context.report(node, 'Neither does this one');
40+
41+
context.report(node, 'This will get reported, regardless of the value of the {{placeholder}}', { placeholder: foo })
42+
43+
}
44+
};
45+
46+
```
47+
48+
The following patterns are not warnings:
49+
50+
```js
51+
52+
module.exports = {
53+
meta: {},
54+
create(context) {
55+
56+
context.report(node, 'This message matches the regular expression.');
57+
58+
context.report(node, 'So does this one.');
59+
60+
}
61+
};
62+
63+
```
64+
65+
## When Not To Use It
66+
67+
If you don't want to enforce consistent formatting for your report messages, you can turn off this rule.

Diff for: lib/rules/report-message-format.js

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* @fileoverview enforce a consistent format for rule report messages
3+
* @author Teddy Katz
4+
*/
5+
6+
'use strict';
7+
8+
const utils = require('../utils');
9+
10+
// ------------------------------------------------------------------------------
11+
// Rule Definition
12+
// ------------------------------------------------------------------------------
13+
14+
module.exports = {
15+
meta: {
16+
docs: {
17+
description: 'enforce a consistent format for rule report messages',
18+
category: 'Rules',
19+
recommended: false,
20+
},
21+
fixable: null,
22+
schema: [
23+
{ type: 'string' },
24+
],
25+
},
26+
27+
create (context) {
28+
const pattern = new RegExp(context.options[0] || '');
29+
let contextIdentifiers;
30+
31+
// ----------------------------------------------------------------------
32+
// Public
33+
// ----------------------------------------------------------------------
34+
35+
return {
36+
Program (node) {
37+
contextIdentifiers = utils.getContextIdentifiers(context, node);
38+
},
39+
CallExpression (node) {
40+
if (
41+
node.callee.type === 'MemberExpression' &&
42+
contextIdentifiers.has(node.callee.object) &&
43+
node.callee.property.type === 'Identifier' && node.callee.property.name === 'report'
44+
) {
45+
if (node.arguments.length === 1 && node.arguments[0].type !== 'ObjectExpression') {
46+
return;
47+
}
48+
49+
const message = node.arguments.length === 1
50+
? node.arguments[0].properties.find(prop =>
51+
(prop.key.type === 'Literal' && prop.key.value === 'message') ||
52+
(prop.key.type === 'Identifier' && prop.key.name === 'message')
53+
).value
54+
: node.arguments[1];
55+
56+
if (
57+
(message.type === 'Literal' && typeof message.value === 'string' && !pattern.test(message.value)) ||
58+
(message.type === 'TemplateLiteral' && message.quasis.length === 1 && !pattern.test(message.quasis[0].value.cooked))
59+
) {
60+
context.report({ node, message: `Report message does not match the pattern '${context.options[0] || ''}'.` });
61+
}
62+
}
63+
},
64+
};
65+
},
66+
};

Diff for: lib/utils.js

+22
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,26 @@ module.exports = {
8585
? Object.assign({ isNewStyle: !exportsIsFunction, meta: null }, exportNodes)
8686
: null;
8787
},
88+
89+
/**
90+
* Gets all the identifiers referring to the `context` variable in a rule source file. Note that this function will
91+
* only work correctly after traversing the AST has started (e.g. in the first `Program` node).
92+
* @param {RuleContext} context The `context` variable for the source file itself
93+
* @param {ASTNode} ast The `Program` node for the file
94+
* @returns {Set<ASTNode>} A Set of all `Identifier` nodes that are references to the `context` value for the file
95+
*/
96+
getContextIdentifiers (context, ast) {
97+
const ruleInfo = module.exports.getRuleInfo(ast);
98+
99+
if (!ruleInfo || !ruleInfo.create.params.length || ruleInfo.create.params[0].type !== 'Identifier') {
100+
return new Set();
101+
}
102+
103+
return new Set(
104+
context.getDeclaredVariables(ruleInfo.create)
105+
.find(variable => variable.name === ruleInfo.create.params[0].name)
106+
.references
107+
.map(ref => ref.identifier)
108+
);
109+
},
88110
};

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"devDependencies": {
3131
"chai": "^3.5.0",
3232
"dirty-chai": "^1.2.2",
33+
"escope": "^3.6.0",
3334
"eslint": "^3.12.1",
3435
"eslint-config-not-an-aardvark": "^2.0.0",
3536
"eslint-plugin-node": "^3.0.5",

Diff for: tests/lib/rules/report-message-format.js

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* @fileoverview enforce a consistent format for rule report messages
3+
* @author Teddy Katz
4+
*/
5+
6+
'use strict';
7+
8+
// ------------------------------------------------------------------------------
9+
// Requirements
10+
// ------------------------------------------------------------------------------
11+
12+
const rule = require('../../../lib/rules/report-message-format');
13+
const RuleTester = require('eslint').RuleTester;
14+
15+
16+
// ------------------------------------------------------------------------------
17+
// Tests
18+
// ------------------------------------------------------------------------------
19+
20+
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
21+
ruleTester.run('report-message-format', rule, {
22+
23+
valid: [
24+
// with no configuration, everything is allowed
25+
'module.exports = context => context.report(node, "foo");',
26+
{
27+
code: `
28+
module.exports = {
29+
create(context) {
30+
context.report(node, 'foo');
31+
}
32+
};
33+
`,
34+
options: ['foo'],
35+
},
36+
{
37+
code: `
38+
module.exports = {
39+
create(context) {
40+
context.report(node, 'foo');
41+
}
42+
};
43+
`,
44+
options: ['f'],
45+
},
46+
{
47+
code: `
48+
module.exports = {
49+
create(context) {
50+
context.report(node, message);
51+
}
52+
};
53+
`,
54+
options: ['foo'],
55+
},
56+
{
57+
code: `
58+
module.exports = {
59+
create(context) {
60+
context.report(node, 'not foo' + message);
61+
}
62+
};
63+
`,
64+
options: ['^foo$'],
65+
},
66+
{
67+
code: `
68+
module.exports = {
69+
create(context) {
70+
context.report(node, 'not foo' + message);
71+
}
72+
};
73+
`,
74+
options: ['^foo$'],
75+
},
76+
{
77+
code: `
78+
module.exports = {
79+
create(context) {
80+
context.report({node, message: 'foo'});
81+
}
82+
};
83+
`,
84+
options: ['^foo$'],
85+
},
86+
{
87+
code: `
88+
module.exports = {
89+
create(context) {
90+
context.report({node, message: 'foo'});
91+
}
92+
};
93+
`,
94+
options: ['^foo$'],
95+
},
96+
{
97+
code: `
98+
module.exports = {
99+
create(context) {
100+
context.report({node, message: 'foobarbaz'});
101+
}
102+
};
103+
`,
104+
options: ['bar'],
105+
},
106+
{
107+
code: `
108+
module.exports = {
109+
create(context) {
110+
context.report({node, message: \`foobarbaz\`});
111+
}
112+
};
113+
`,
114+
options: ['bar'],
115+
},
116+
],
117+
118+
invalid: [
119+
{
120+
code: `
121+
module.exports = {
122+
create(context) {
123+
context.report(node, 'bar');
124+
}
125+
};
126+
`,
127+
options: ['foo'],
128+
},
129+
{
130+
code: `
131+
module.exports = {
132+
create(context) {
133+
context.report(node, 'foobar');
134+
}
135+
};
136+
`,
137+
options: ['^foo$'],
138+
},
139+
{
140+
code: `
141+
module.exports = {
142+
create(context) {
143+
context.report(node, 'FOO');
144+
}
145+
};
146+
`,
147+
options: ['foo'],
148+
},
149+
{
150+
code: `
151+
module.exports = {
152+
create(context) {
153+
context.report(node, \`FOO\`);
154+
}
155+
};
156+
`,
157+
options: ['foo'],
158+
},
159+
{
160+
code: `
161+
module.exports = {
162+
create(context) {
163+
context.report({node, message: 'FOO'});
164+
}
165+
};
166+
`,
167+
options: ['foo'],
168+
},
169+
{
170+
code: `
171+
module.exports = {
172+
create(context) {
173+
context.report({node, message: \`FOO\`});
174+
}
175+
};
176+
`,
177+
options: ['foo'],
178+
},
179+
].map(invalidCase => {
180+
return Object.assign({
181+
errors: [{ message: `Report message does not match the pattern '${invalidCase.options[0]}'.` }],
182+
}, invalidCase);
183+
}),
184+
});

0 commit comments

Comments
 (0)