Skip to content

Commit

Permalink
New: add no-import-assign (fixes #12237)
Browse files Browse the repository at this point in the history
  • Loading branch information
mysticatea committed Sep 10, 2019
1 parent 460c5ad commit d9c7cfa
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 0 deletions.
44 changes: 44 additions & 0 deletions 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.
1 change: 1 addition & 0 deletions lib/rules/index.js
Expand Up @@ -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"),
Expand Down
95 changes: 95 additions & 0 deletions lib/rules/no-import-assign.js
@@ -0,0 +1,95 @@
/**
* @fileoverview Rule to flag updates of imported bindings.
* @author Toru Nagashima <https://github.com/mysticatea>
*/

"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 }
});
}
}
}
}
};

}
};
104 changes: 104 additions & 0 deletions tests/lib/rules/no-import-assign.js
@@ -0,0 +1,104 @@
/**
* @fileoverview Tests for no-import-assign rule.
* @author Toru Nagashima <https://github.com/mysticatea>
*/

"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 }]
}
]
});
1 change: 1 addition & 0 deletions tools/rule-types.json
Expand Up @@ -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",
Expand Down

0 comments on commit d9c7cfa

Please sign in to comment.