Skip to content

Commit 06a6e5a

Browse files
New: no-deprecated-report-api rule
1 parent 8b0ae4f commit 06a6e5a

9 files changed

+566
-2
lines changed

README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ Then configure the rules you want to use under the rules section.
3636
```json
3737
{
3838
"rules": {
39-
"eslint-plugin/new-report-api": "error"
39+
"eslint-plugin/no-deprecated-report-api": "error"
4040
}
4141
}
4242
```
4343

4444
## Supported Rules
4545

46-
* None yet
46+
🛠 indicates that a rule is fixable.
47+
48+
* 🛠 [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
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# disallow use of the deprecated context.report() API (no-deprecated-report-api)
2+
3+
(fixable) The `--fix` option on the [command line](../user-guide/command-line-interface#fix) automatically fixes problems reported by this rule.
4+
5+
ESLint has two APIs that rules can use to report problems. The [deprecated API](http://eslint.org/docs/developer-guide/working-with-rules-deprecated) accepts multiple arguments: `context.report(node, [loc], message)`. The ["new API"](http://eslint.org/docs/developer-guide/working-with-rules#contextreport) accepts a single argument: an object containing information about the reported problem. It is recommended that all rules use the new API.
6+
7+
## Rule Details
8+
9+
This rule aims to disallow use of the deprecated `context.report(node, [loc], message)` API.
10+
11+
The following patterns are considered warnings:
12+
13+
```js
14+
module.exports = {
15+
create(context) {
16+
17+
context.report(node, 'This node is bad.');
18+
19+
context.report(node, loc, 'This node is bad.');
20+
21+
}
22+
};
23+
24+
```
25+
26+
The following patterns are not warnings:
27+
28+
```js
29+
module.exports = {
30+
create(context) {
31+
32+
context.report({ node, message: 'This node is bad.' });
33+
34+
context.report({ node, loc, message: 'This node is bad.' });
35+
36+
}
37+
};
38+
```
39+
40+
41+
## Further Reading
42+
43+
* [Deprecated rule API](http://eslint.org/docs/developer-guide/working-with-rules-deprecated)
44+
* [New rule API](http://eslint.org/docs/developer-guide/working-with-rules)

lib/rules/no-deprecated-report-api.js

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @fileoverview disallow use of the deprecated context.report() API
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: 'disallow use of the deprecated context.report() API',
18+
category: 'Rules',
19+
recommended: false,
20+
},
21+
fixable: 'code', // or "code" or "whitespace"
22+
schema: [],
23+
},
24+
25+
create (context) {
26+
const sourceCode = context.getSourceCode();
27+
const ruleInfo = utils.getRuleInfo(sourceCode.ast);
28+
29+
if (!ruleInfo || !ruleInfo.create.params.length) {
30+
return {};
31+
}
32+
33+
if (ruleInfo.create.params[0].type !== 'Identifier') {
34+
// TODO: Make the rule detect `module.exports = { create({report}) { report(foo, bar); } };`
35+
return {};
36+
}
37+
38+
const contextIdentifiers = new WeakSet();
39+
40+
// ----------------------------------------------------------------------
41+
// Public
42+
// ----------------------------------------------------------------------
43+
44+
return {
45+
[ruleInfo.create.type] (node) {
46+
if (node === ruleInfo.create) {
47+
context.getDeclaredVariables(node)
48+
.find(variable => variable.name === node.params[0].name)
49+
.references
50+
.map(ref => ref.identifier)
51+
.forEach(identifier => contextIdentifiers.add(identifier));
52+
}
53+
},
54+
CallExpression (node) {
55+
if (
56+
node.callee.type === 'MemberExpression' &&
57+
contextIdentifiers.has(node.callee.object) &&
58+
node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' &&
59+
node.arguments.length > 1
60+
) {
61+
context.report({
62+
node,
63+
message: 'Use the new-style context.report() API.',
64+
fix (fixer) {
65+
const openingParen = sourceCode.getTokenBefore(node.arguments[0]);
66+
const closingParen = sourceCode.getLastToken(node);
67+
const keyNames = node.arguments.length === 5
68+
? ['node', 'loc', 'message', 'data', 'fix']
69+
: ['node', 'message', 'data', 'fix'];
70+
71+
return fixer.replaceTextRange(
72+
[openingParen.range[1], closingParen.range[0]],
73+
'{' + node.arguments.map((arg, index) => `${keyNames[index]}: ${sourceCode.getText(arg)}`).join(', ') + '}'
74+
);
75+
},
76+
});
77+
}
78+
},
79+
};
80+
},
81+
};

lib/utils.js

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
'use strict';
2+
3+
/**
4+
* Determines whether a node is a 'normal' (i.e. non-async, non-generator) function expression.
5+
* @param {ASTNode} node The node in question
6+
* @returns {boolean} `true` if the node is a normal function expression
7+
*/
8+
function isNormalFunctionExpression (node) {
9+
return (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && !node.generator && !node.async;
10+
}
11+
12+
module.exports = {
13+
14+
/**
15+
* Performs static analysis on an AST to try to determine the final value of `module.exports`.
16+
* @param {ASTNode} ast The `Program` AST node
17+
* @returns {Object} An object with keys `meta`, `create`, and `isNewStyle`. `meta` and `create` correspond to the AST nodes
18+
for the final values of `module.exports.meta` and `module.exports.create`. `isNewStyle` will be `true` if `module.exports`
19+
is an object, and `false` if module.exports is just the `create` function. If no valid ESLint rule info can be extracted
20+
from the file, the return value will be `null`.
21+
*/
22+
getRuleInfo (ast) {
23+
const INTERESTING_KEYS = ['create', 'meta'];
24+
let exportsVarOverridden = false;
25+
let exportsIsFunction = false;
26+
27+
const exportNodes = ast.body
28+
.filter(statement => statement.type === 'ExpressionStatement')
29+
.map(statement => statement.expression)
30+
.filter(expression => expression.type === 'AssignmentExpression')
31+
.filter(expression => expression.left.type === 'MemberExpression')
32+
.reduce((currentExports, node) => {
33+
if (
34+
node.left.object.type === 'Identifier' && node.left.object.name === 'module' &&
35+
node.left.property.type === 'Identifier' && node.left.property.name === 'exports'
36+
) {
37+
exportsVarOverridden = true;
38+
39+
if (isNormalFunctionExpression(node.right)) {
40+
// Check `module.exports = function () {}`
41+
42+
exportsIsFunction = true;
43+
return { create: node.right, meta: null };
44+
} else if (node.right.type === 'ObjectExpression') {
45+
// Check `module.exports = { create: function () {}, meta: {} }`
46+
47+
exportsIsFunction = false;
48+
return node.right.properties.reduce((parsedProps, prop) => {
49+
const keyValue = prop.key.type === 'Literal'
50+
? prop.key.value
51+
: prop.key.type === 'Identifier'
52+
? prop.key.name
53+
: null;
54+
55+
if (INTERESTING_KEYS.indexOf(keyValue) !== -1) {
56+
parsedProps[keyValue] = prop.value;
57+
}
58+
return parsedProps;
59+
}, {});
60+
}
61+
return {};
62+
} else if (
63+
!exportsIsFunction &&
64+
node.left.object.type === 'MemberExpression' &&
65+
node.left.object.object.type === 'Identifier' && node.left.object.object.name === 'module' &&
66+
node.left.object.property.type === 'Identifier' && node.left.object.property.name === 'exports' &&
67+
node.left.property.type === 'Identifier' && INTERESTING_KEYS.indexOf(node.left.property.name) !== -1
68+
) {
69+
// Check `module.exports.create = () => {}`
70+
71+
currentExports[node.left.property.name] = node.right;
72+
} else if (
73+
!exportsVarOverridden &&
74+
node.left.object.type === 'Identifier' && node.left.object.name === 'exports' &&
75+
node.left.property.type === 'Identifier' && INTERESTING_KEYS.indexOf(node.left.property.name) !== -1
76+
) {
77+
// Check `exports.create = () => {}`
78+
79+
currentExports[node.left.property.name] = node.right;
80+
}
81+
return currentExports;
82+
}, {});
83+
84+
return Object.prototype.hasOwnProperty.call(exportNodes, 'create') && isNormalFunctionExpression(exportNodes.create)
85+
? Object.assign({ isNewStyle: !exportsIsFunction, meta: null }, exportNodes)
86+
: null;
87+
},
88+
};

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@
2828
"homepage": "https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin#readme",
2929
"dependencies": {},
3030
"devDependencies": {
31+
"chai": "^3.5.0",
32+
"dirty-chai": "^1.2.2",
3133
"eslint": "^3.12.1",
3234
"eslint-config-not-an-aardvark": "^2.0.0",
3335
"eslint-plugin-node": "^3.0.5",
36+
"espree": "^3.3.2",
37+
"lodash": "^4.17.2",
3438
"mocha": "^2.4.5"
3539
},
3640
"peerDependencies": {

tests/.eslintrc.yml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
env:
2+
mocha: true

tests/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)