From d9c7cfaf54b7fb55022cce6d79b355b052a93267 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Tue, 10 Sep 2019 10:39:14 +0900 Subject: [PATCH] New: add no-import-assign (fixes #12237) --- docs/rules/no-import-assign.md | 44 ++++++++++++ lib/rules/index.js | 1 + lib/rules/no-import-assign.js | 95 +++++++++++++++++++++++++ tests/lib/rules/no-import-assign.js | 104 ++++++++++++++++++++++++++++ tools/rule-types.json | 1 + 5 files changed, 245 insertions(+) create mode 100644 docs/rules/no-import-assign.md create mode 100644 lib/rules/no-import-assign.js create mode 100644 tests/lib/rules/no-import-assign.js diff --git a/docs/rules/no-import-assign.md b/docs/rules/no-import-assign.md new file mode 100644 index 00000000000..78b228dd828 --- /dev/null +++ b/docs/rules/no-import-assign.md @@ -0,0 +1,44 @@ +# disallow assigning to imported bindings (no-import-assign) + +The updates of imported bindings by ES Modules cause runtime errors. + +## Rule Details + +This rule warns the assignments, increments, and decrements of imported bindings. + +Examples of **incorrect** code for this rule: + +```js +/*eslint no-import-assign: "error"*/ + +import mod, { named } from "./mod.mjs" +import * as mod_ns from "./mod.mjs" + +mod = 1 // ERROR: 'mod' is readonly. +named = 2 // ERROR: 'named' is readonly. +mod_ns.named = 3 // ERROR: the members of 'mod_ns' is readonly. +mod_ns = {} // ERROR: 'mod_ns' is readonly. +``` + +Examples of **correct** code for this rule: + +```js +/*eslint no-import-assign: "error"*/ + +import mod, { named } from "./mod.mjs" +import * as mod_ns from "./mod.mjs" + +mod.prop = 1 +named.prop = 2 +mod_ns.named.prop = 3 + +// Known Limitation +function test(obj) { + obj.named = 4 // Not errored because 'obj' is not namespace objects. +} +test(mod_ns) // Not errored because it doesn't know that 'test' updates the member of the argument. +``` + +## When Not To Use It + +If you don't want to be notified about modifying imported bindings, you can disable this rule. diff --git a/lib/rules/index.js b/lib/rules/index.js index 51d224d219f..efa3d42aafd 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -132,6 +132,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({ "no-implicit-coercion": () => require("./no-implicit-coercion"), "no-implicit-globals": () => require("./no-implicit-globals"), "no-implied-eval": () => require("./no-implied-eval"), + "no-import-assign": () => require("./no-import-assign"), "no-inline-comments": () => require("./no-inline-comments"), "no-inner-declarations": () => require("./no-inner-declarations"), "no-invalid-regexp": () => require("./no-invalid-regexp"), diff --git a/lib/rules/no-import-assign.js b/lib/rules/no-import-assign.js new file mode 100644 index 00000000000..d7e1bbac7fe --- /dev/null +++ b/lib/rules/no-import-assign.js @@ -0,0 +1,95 @@ +/** + * @fileoverview Rule to flag updates of imported bindings. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Check if the identifier node is placed at to update members. + * @param {ASTNode} id The Identifier node to check. + * @returns {boolean} `true` if the member of `id` was updated. + */ +function isMemberUpdate(id) { + const parent = id.parent; + const grandparent = parent.parent; + + return ( + parent.type === "MemberExpression" && + parent.object === id && + ( + ( + grandparent.type === "AssignmentExpression" && + grandparent.left === parent + ) || + ( + grandparent.type === "UpdateExpression" && + grandparent.argument === parent + ) || + ( + grandparent.type === "UnaryExpression" && + grandparent.operator === "delete" && + grandparent.argument === parent + ) + ) + ); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: "problem", + + docs: { + description: "disallow assigning to imported bindings", + category: "Possible Errors", + recommended: false, + url: "https://eslint.org/docs/rules/no-import-assign" + }, + + schema: [], + + messages: { + readonly: "'{{name}}' is read-only.", + readonlyMember: "The members of '{{name}}' are read-only." + } + }, + + create(context) { + return { + ImportDeclaration(node) { + for (const variable of context.getDeclaredVariables(node)) { + const shouldCheckMembers = variable.defs.some( + d => d.node.type === "ImportNamespaceSpecifier" + ); + + for (const reference of variable.references) { + const idNode = reference.identifier; + + if (reference.isWrite()) { + context.report({ + node: idNode.parent, + messageId: "readonly", + data: { name: idNode.name } + }); + } else if (shouldCheckMembers && isMemberUpdate(idNode)) { + context.report({ + node: idNode.parent.parent, + messageId: "readonlyMember", + data: { name: idNode.name } + }); + } + } + } + } + }; + + } +}; diff --git a/tests/lib/rules/no-import-assign.js b/tests/lib/rules/no-import-assign.js new file mode 100644 index 00000000000..e7571fe105a --- /dev/null +++ b/tests/lib/rules/no-import-assign.js @@ -0,0 +1,104 @@ +/** + * @fileoverview Tests for no-import-assign rule. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/no-import-assign"), + { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2015, + sourceType: "module" + } +}); + +ruleTester.run("no-import-assign", rule, { + valid: [ + "import mod from 'mod'; mod.prop = 0", + "import mod from 'mod'; mod.prop += 0", + "import mod from 'mod'; mod.prop++", + "import mod from 'mod'; delete mod.prop", + "import {named} from 'mod'; named.prop = 0", + "import {named} from 'mod'; named.prop += 0", + "import {named} from 'mod'; named.prop++", + "import {named} from 'mod'; delete named.prop", + "import * as mod from 'mod'; mod.named.prop = 0", + "import * as mod from 'mod'; mod.named.prop += 0", + "import * as mod from 'mod'; mod.named.prop++", + "import * as mod from 'mod'; delete mod.named.prop", + "import * as mod from 'mod'; obj[mod] = 0", + "import * as mod from 'mod'; obj[mod.named] = 0", + "import mod from 'mod'; { let mod = 0; mod = 1 }", + "import * as mod from 'mod'; { let mod = 0; mod = 1 }", + "import {} from 'mod'" + ], + invalid: [ + { + code: "import mod1 from 'mod'; mod1 = 0", + errors: [{ messageId: "readonly", data: { name: "mod1" }, column: 25 }] + }, + { + code: "import mod2 from 'mod'; mod2 += 0", + errors: [{ messageId: "readonly", data: { name: "mod2" }, column: 25 }] + }, + { + code: "import mod3 from 'mod'; mod3++", + errors: [{ messageId: "readonly", data: { name: "mod3" }, column: 25 }] + }, + { + code: "import {named1} from 'mod'; named1 = 0", + errors: [{ messageId: "readonly", data: { name: "named1" }, column: 29 }] + }, + { + code: "import {named2} from 'mod'; named2 += 0", + errors: [{ messageId: "readonly", data: { name: "named2" }, column: 29 }] + }, + { + code: "import {named3} from 'mod'; named3++", + errors: [{ messageId: "readonly", data: { name: "named3" }, column: 29 }] + }, + { + code: "import {named4 as foo} from 'mod'; foo = 0; named4 = 0", + errors: [{ messageId: "readonly", data: { name: "foo" }, column: 36 }] + }, + { + code: "import * as mod1 from 'mod'; mod1 = 0", + errors: [{ messageId: "readonly", data: { name: "mod1" }, column: 30 }] + }, + { + code: "import * as mod2 from 'mod'; mod2 += 0", + errors: [{ messageId: "readonly", data: { name: "mod2" }, column: 30 }] + }, + { + code: "import * as mod3 from 'mod'; mod3++", + errors: [{ messageId: "readonly", data: { name: "mod3" }, column: 30 }] + }, + { + code: "import * as mod4 from 'mod'; mod4.named = 0", + errors: [{ messageId: "readonlyMember", data: { name: "mod4" }, column: 30 }] + }, + { + code: "import * as mod5 from 'mod'; mod5.named += 0", + errors: [{ messageId: "readonlyMember", data: { name: "mod5" }, column: 30 }] + }, + { + code: "import * as mod6 from 'mod'; mod6.named++", + errors: [{ messageId: "readonlyMember", data: { name: "mod6" }, column: 30 }] + }, + { + code: "import * as mod7 from 'mod'; delete mod7.named", + errors: [{ messageId: "readonlyMember", data: { name: "mod7" }, column: 30 }] + } + ] +}); diff --git a/tools/rule-types.json b/tools/rule-types.json index 74cae9391cb..5e5514f5506 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -119,6 +119,7 @@ "no-implicit-coercion": "suggestion", "no-implicit-globals": "suggestion", "no-implied-eval": "suggestion", + "no-import-assign": "problem", "no-inline-comments": "suggestion", "no-inner-declarations": "problem", "no-invalid-regexp": "problem",