diff --git a/README.md b/README.md index d2bc3812..b68094bf 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ $ npm install --save-dev eslint eslint-plugin-node | Rule ID | Description | | |:--------|:------------|:--:| +| [node/no-callback-literal](./docs/rules/no-callback-literal.md) | ensure Node.js-style error-first callback pattern is followed | | +| [node/no-exports-assign](./docs/rules/no-exports-assign.md) | disallow the assignment to `exports` | | | [node/no-extraneous-import](./docs/rules/no-extraneous-import.md) | disallow `import` declarations which import extraneous modules | ⭐️ | | [node/no-extraneous-require](./docs/rules/no-extraneous-require.md) | disallow `require()` expressions which import extraneous modules | ⭐️ | | [node/no-missing-import](./docs/rules/no-missing-import.md) | disallow `import` declarations which import non-existence modules | ⭐️ | diff --git a/docs/rules/no-callback-literal.md b/docs/rules/no-callback-literal.md index 6281c313..979498f2 100644 --- a/docs/rules/no-callback-literal.md +++ b/docs/rules/no-callback-literal.md @@ -1,6 +1,5 @@ # node/no-callback-literal - -> Ensures the Node.js error-first callback pattern is followed +> ensure Node.js-style error-first callback pattern is followed When invoking a callback function which uses the Node.js error-first callback pattern, all of your errors should either use the `Error` class or a subclass of it. It is also acceptable to use `undefined` or `null` if there is no error. diff --git a/docs/rules/no-exports-assign.md b/docs/rules/no-exports-assign.md new file mode 100644 index 00000000..98546320 --- /dev/null +++ b/docs/rules/no-exports-assign.md @@ -0,0 +1,45 @@ +# node/no-exports-assign +> disallow the assignment to `exports` + +To assign to `exports` variable would not work as expected. + +```js +// This assigned object is not exported. +// You need to use `module.exports = { ... }`. +exports = { + foo: 1 +} +``` + +## 📖 Rule Details + +This rule is aimed at disallowing `exports = {}`, but allows `module.exports = exports = {}` to avoid conflict with [node/exports-style](./exports-style.md) rule's `allowBatchAssign` option. + +👍 Examples of **correct** code for this rule: + +```js +/*eslint node/no-exports-assign: error */ + +module.exports.foo = 1 +exports.bar = 2 + +module.exports = {} + +// allows `exports = {}` if along with `module.exports =` +module.exports = exports = {} +exports = module.exports = {} +``` + +👎 Examples of **incorrect** code for this rule: + +```js +/*eslint node/no-exports-assign: error */ + +exports = {} +``` + + +## 🔎 Implementation + +- [Rule source](../../lib/rules/no-exports-assign.js) +- [Test source](../../tests/lib/rules/no-exports-assign.js) diff --git a/lib/index.js b/lib/index.js index a45d5a2f..2d782b91 100644 --- a/lib/index.js +++ b/lib/index.js @@ -12,7 +12,9 @@ module.exports = { rules: { "exports-style": require("./rules/exports-style"), "file-extension-in-import": require("./rules/file-extension-in-import"), + "no-callback-literal": require("./rules/no-callback-literal"), "no-deprecated-api": require("./rules/no-deprecated-api"), + "no-exports-assign": require("./rules/no-exports-assign"), "no-extraneous-import": require("./rules/no-extraneous-import"), "no-extraneous-require": require("./rules/no-extraneous-require"), "no-missing-import": require("./rules/no-missing-import"), diff --git a/lib/rules/no-exports-assign.js b/lib/rules/no-exports-assign.js new file mode 100644 index 00000000..04e0539f --- /dev/null +++ b/lib/rules/no-exports-assign.js @@ -0,0 +1,75 @@ +/** + * @author Toru Nagashima + * See LICENSE file in root directory for full license. + */ +"use strict" + +const { findVariable } = require("eslint-utils") + +function isExports(node, scope) { + let variable = null + + return ( + node != null && + node.type === "Identifier" && + node.name === "exports" && + (variable = findVariable(scope, node)) != null && + variable.scope.type === "global" + ) +} + +function isModuleExports(node, scope) { + let variable = null + + return ( + node != null && + node.type === "MemberExpression" && + !node.computed && + node.object.type === "Identifier" && + node.object.name === "module" && + node.property.type === "Identifier" && + node.property.name === "exports" && + (variable = findVariable(scope, node.object)) != null && + variable.scope.type === "global" + ) +} + +module.exports = { + meta: { + docs: { + description: "disallow the assignment to `exports`", + category: "Possible Errors", + recommended: false, + url: + "https://github.com/mysticatea/eslint-plugin-node/blob/v9.2.0/docs/rules/no-exports-assign.md", + }, + fixable: null, + messages: { + forbidden: + "Unexpected assignment to 'exports' variable. Use 'module.exports' instead.", + }, + schema: [], + type: "problem", + }, + create(context) { + return { + AssignmentExpression(node) { + const scope = context.getScope() + if ( + !isExports(node.left, scope) || + // module.exports = exports = {} + (node.parent.type === "AssignmentExpression" && + node.parent.right === node && + isModuleExports(node.parent.left, scope)) || + // exports = module.exports = {} + (node.right.type === "AssignmentExpression" && + isModuleExports(node.right.left, scope)) + ) { + return + } + + context.report({ node, messageId: "forbidden" }) + }, + } + }, +} diff --git a/tests/lib/rules/no-exports-assign.js b/tests/lib/rules/no-exports-assign.js new file mode 100644 index 00000000..34ce6657 --- /dev/null +++ b/tests/lib/rules/no-exports-assign.js @@ -0,0 +1,31 @@ +/** + * @author Toru Nagashima + * See LICENSE file in root directory for full license. + */ +"use strict" + +const { RuleTester } = require("eslint") +const rule = require("../../../lib/rules/no-exports-assign.js") + +new RuleTester({ + globals: { + exports: "writable", + module: "readonly", + }, +}).run("no-exports-assign", rule, { + valid: [ + "module.exports.foo = 1", + "exports.bar = 1", + "module.exports = exports = {}", + "exports = module.exports = {}", + "function f(exports) { exports = {} }", + ], + invalid: [ + { + code: "exports = {}", + errors: [ + "Unexpected assignment to 'exports' variable. Use 'module.exports' instead.", + ], + }, + ], +})