Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: add no-import-assign (fixes #12237) #12252

Merged
merged 6 commits into from Sep 14, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
mysticatea marked this conversation as resolved.
Show resolved Hide resolved
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