Skip to content

Commit d030d8e

Browse files
Trevor Burnhamljharb
Trevor Burnham
authored andcommitted
[New] no-namespace: Make rule fixable
- Add guards to avoid crashing older versions of ESLint - Note that no-namespace's --fix requires ESLint 5+ - Prevent no-namespace --fix tests from running under ESLint < 5
1 parent 3704801 commit d030d8e

File tree

5 files changed

+228
-20
lines changed

5 files changed

+228
-20
lines changed

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
77

88
### Added
99
- [`group-exports`]: make aggregate module exports valid ([#1472], thanks [@atikenny])
10+
- [`no-namespace`]: Make rule fixable ([#1401], thanks [@TrevorBurnham])
1011

1112
### Added
1213
- support `parseForESLint` from custom parser ([#1435], thanks [@JounQin])
@@ -617,6 +618,7 @@ for info on changes for earlier releases.
617618
[#1412]: https://github.com/benmosher/eslint-plugin-import/pull/1412
618619
[#1409]: https://github.com/benmosher/eslint-plugin-import/pull/1409
619620
[#1404]: https://github.com/benmosher/eslint-plugin-import/pull/1404
621+
[#1401]: https://github.com/benmosher/eslint-plugin-import/pull/1401
620622
[#1393]: https://github.com/benmosher/eslint-plugin-import/pull/1393
621623
[#1389]: https://github.com/benmosher/eslint-plugin-import/pull/1389
622624
[#1377]: https://github.com/benmosher/eslint-plugin-import/pull/1377
@@ -989,3 +991,4 @@ for info on changes for earlier releases.
989991
[@JounQin]: https://github.com/JounQin
990992
[@atikenny]: https://github.com/atikenny
991993
[@schmidsi]: https://github.com/schmidsi
994+
[@TrevorBurnham]: https://github.com/TrevorBurnham

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
8181
* Ensure all imports appear before other statements ([`first`])
8282
* Ensure all exports appear after other statements ([`exports-last`])
8383
* Report repeated import of the same module in multiple places ([`no-duplicates`])
84-
* Report namespace imports ([`no-namespace`])
84+
* Forbid namespace (a.k.a. "wildcard" `*`) imports ([`no-namespace`])
8585
* Ensure consistent use of file extension within the import path ([`extensions`])
8686
* Enforce a convention in module import order ([`order`])
8787
* Enforce a newline after import statements ([`newline-after-import`])

Diff for: docs/rules/no-namespace.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# import/no-namespace
22

3-
Reports if namespace import is used.
3+
Enforce a convention of not using namespace (a.k.a. "wildcard" `*`) imports.
4+
5+
+(fixable) The `--fix` option on the [command line] automatically fixes problems reported by this rule, provided that the namespace object is only used for direct member access, e.g. `namespace.a`.
6+
The `--fix` functionality for this rule requires ESLint 5 or newer.
47

58
## Rule Details
69

@@ -12,10 +15,13 @@ import { a, b } from './bar'
1215
import defaultExport, { a, b } from './foobar'
1316
```
1417

15-
...whereas here imports will be reported:
18+
Invalid:
1619

1720
```js
1821
import * as foo from 'foo';
22+
```
23+
24+
```js
1925
import defaultExport, * as foo from 'foo';
2026
```
2127

Diff for: src/rules/no-namespace.js

+131-1
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,143 @@ module.exports = {
1616
docs: {
1717
url: docsUrl('no-namespace'),
1818
},
19+
fixable: 'code',
1920
},
2021

2122
create: function (context) {
2223
return {
2324
'ImportNamespaceSpecifier': function (node) {
24-
context.report(node, `Unexpected namespace import.`)
25+
const scopeVariables = context.getScope().variables
26+
const namespaceVariable = scopeVariables.find((variable) =>
27+
variable.defs[0].node === node
28+
)
29+
const namespaceReferences = namespaceVariable.references
30+
const namespaceIdentifiers = namespaceReferences.map(reference => reference.identifier)
31+
const canFix = namespaceIdentifiers.length > 0 && !usesNamespaceAsObject(namespaceIdentifiers)
32+
33+
context.report({
34+
node,
35+
message: `Unexpected namespace import.`,
36+
fix: canFix && (fixer => {
37+
const scopeManager = context.getSourceCode().scopeManager
38+
const fixes = []
39+
40+
// Pass 1: Collect variable names that are already in scope for each reference we want
41+
// to transform, so that we can be sure that we choose non-conflicting import names
42+
const importNameConflicts = {}
43+
namespaceIdentifiers.forEach((identifier) => {
44+
const parent = identifier.parent
45+
if (parent && parent.type === 'MemberExpression') {
46+
const importName = getMemberPropertyName(parent)
47+
const localConflicts = getVariableNamesInScope(scopeManager, parent)
48+
if (!importNameConflicts[importName]) {
49+
importNameConflicts[importName] = localConflicts
50+
} else {
51+
localConflicts.forEach((c) => importNameConflicts[importName].add(c))
52+
}
53+
}
54+
})
55+
56+
// Choose new names for each import
57+
const importNames = Object.keys(importNameConflicts)
58+
const importLocalNames = generateLocalNames(
59+
importNames,
60+
importNameConflicts,
61+
namespaceVariable.name
62+
)
63+
64+
// Replace the ImportNamespaceSpecifier with a list of ImportSpecifiers
65+
const namedImportSpecifiers = importNames.map((importName) =>
66+
importName === importLocalNames[importName]
67+
? importName
68+
: `${importName} as ${importLocalNames[importName]}`
69+
)
70+
fixes.push(fixer.replaceText(node, `{ ${namedImportSpecifiers.join(', ')} }`))
71+
72+
// Pass 2: Replace references to the namespace with references to the named imports
73+
namespaceIdentifiers.forEach((identifier) => {
74+
const parent = identifier.parent
75+
if (parent && parent.type === 'MemberExpression') {
76+
const importName = getMemberPropertyName(parent)
77+
fixes.push(fixer.replaceText(parent, importLocalNames[importName]))
78+
}
79+
})
80+
81+
return fixes
82+
}),
83+
})
2584
},
2685
}
2786
},
2887
}
88+
89+
/**
90+
* @param {Identifier[]} namespaceIdentifiers
91+
* @returns {boolean} `true` if the namespace variable is more than just a glorified constant
92+
*/
93+
function usesNamespaceAsObject(namespaceIdentifiers) {
94+
return !namespaceIdentifiers.every((identifier) => {
95+
const parent = identifier.parent
96+
97+
// `namespace.x` or `namespace['x']`
98+
return (
99+
parent && parent.type === 'MemberExpression' &&
100+
(parent.property.type === 'Identifier' || parent.property.type === 'Literal')
101+
)
102+
})
103+
}
104+
105+
/**
106+
* @param {MemberExpression} memberExpression
107+
* @returns {string} the name of the member in the object expression, e.g. the `x` in `namespace.x`
108+
*/
109+
function getMemberPropertyName(memberExpression) {
110+
return memberExpression.property.type === 'Identifier'
111+
? memberExpression.property.name
112+
: memberExpression.property.value
113+
}
114+
115+
/**
116+
* @param {ScopeManager} scopeManager
117+
* @param {ASTNode} node
118+
* @return {Set<string>}
119+
*/
120+
function getVariableNamesInScope(scopeManager, node) {
121+
let currentNode = node
122+
let scope = scopeManager.acquire(currentNode)
123+
while (scope == null) {
124+
currentNode = currentNode.parent
125+
scope = scopeManager.acquire(currentNode, true)
126+
}
127+
return new Set([
128+
...scope.variables.map(variable => variable.name),
129+
...scope.upper.variables.map(variable => variable.name),
130+
])
131+
}
132+
133+
/**
134+
*
135+
* @param {*} names
136+
* @param {*} nameConflicts
137+
* @param {*} namespaceName
138+
*/
139+
function generateLocalNames(names, nameConflicts, namespaceName) {
140+
const localNames = {}
141+
names.forEach((name) => {
142+
let localName
143+
if (!nameConflicts[name].has(name)) {
144+
localName = name
145+
} else if (!nameConflicts[name].has(`${namespaceName}_${name}`)) {
146+
localName = `${namespaceName}_${name}`
147+
} else {
148+
for (let i = 1; i < Infinity; i++) {
149+
if (!nameConflicts[name].has(`${namespaceName}_${name}_${i}`)) {
150+
localName = `${namespaceName}_${name}_${i}`
151+
break
152+
}
153+
}
154+
}
155+
localNames[name] = localName
156+
})
157+
return localNames
158+
}

Diff for: tests/src/rules/no-namespace.js

+85-16
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,113 @@
11
import { RuleTester } from 'eslint'
2+
import eslintPkg from 'eslint/package.json'
3+
import semver from 'semver'
4+
import { test } from '../utils'
25

36
const ERROR_MESSAGE = 'Unexpected namespace import.'
47

58
const ruleTester = new RuleTester()
69

10+
// --fix functionality requires ESLint 5+
11+
const FIX_TESTS = semver.satisfies(eslintPkg.version, '>5.0.0') ? [
12+
test({
13+
code: `
14+
import * as foo from './foo';
15+
florp(foo.bar);
16+
florp(foo['baz']);
17+
`.trim(),
18+
output: `
19+
import { bar, baz } from './foo';
20+
florp(bar);
21+
florp(baz);
22+
`.trim(),
23+
errors: [ {
24+
line: 1,
25+
column: 8,
26+
message: ERROR_MESSAGE,
27+
}],
28+
}),
29+
test({
30+
code: `
31+
import * as foo from './foo';
32+
const bar = 'name conflict';
33+
const baz = 'name conflict';
34+
const foo_baz = 'name conflict';
35+
florp(foo.bar);
36+
florp(foo['baz']);
37+
`.trim(),
38+
output: `
39+
import { bar as foo_bar, baz as foo_baz_1 } from './foo';
40+
const bar = 'name conflict';
41+
const baz = 'name conflict';
42+
const foo_baz = 'name conflict';
43+
florp(foo_bar);
44+
florp(foo_baz_1);
45+
`.trim(),
46+
errors: [ {
47+
line: 1,
48+
column: 8,
49+
message: ERROR_MESSAGE,
50+
}],
51+
}),
52+
test({
53+
code: `
54+
import * as foo from './foo';
55+
function func(arg) {
56+
florp(foo.func);
57+
florp(foo['arg']);
58+
}
59+
`.trim(),
60+
output: `
61+
import { func as foo_func, arg as foo_arg } from './foo';
62+
function func(arg) {
63+
florp(foo_func);
64+
florp(foo_arg);
65+
}
66+
`.trim(),
67+
errors: [ {
68+
line: 1,
69+
column: 8,
70+
message: ERROR_MESSAGE,
71+
}],
72+
}),
73+
] : []
74+
775
ruleTester.run('no-namespace', require('rules/no-namespace'), {
876
valid: [
9-
{ code: "import { a, b } from 'foo';", parserOptions: { ecmaVersion: 2015, sourceType: 'module' } },
10-
{ code: "import { a, b } from './foo';", parserOptions: { ecmaVersion: 2015, sourceType: 'module' } },
11-
{ code: "import bar from 'bar';", parserOptions: { ecmaVersion: 2015, sourceType: 'module' } },
12-
{ code: "import bar from './bar';", parserOptions: { ecmaVersion: 2015, sourceType: 'module' } },
77+
{ code: 'import { a, b } from \'foo\';', parserOptions: { ecmaVersion: 2015, sourceType: 'module' } },
78+
{ code: 'import { a, b } from \'./foo\';', parserOptions: { ecmaVersion: 2015, sourceType: 'module' } },
79+
{ code: 'import bar from \'bar\';', parserOptions: { ecmaVersion: 2015, sourceType: 'module' } },
80+
{ code: 'import bar from \'./bar\';', parserOptions: { ecmaVersion: 2015, sourceType: 'module' } },
1381
],
1482

1583
invalid: [
16-
{
17-
code: "import * as foo from 'foo';",
84+
test({
85+
code: 'import * as foo from \'foo\';',
86+
output: 'import * as foo from \'foo\';',
1887
errors: [ {
1988
line: 1,
2089
column: 8,
2190
message: ERROR_MESSAGE,
2291
} ],
23-
parserOptions: { ecmaVersion: 2015, sourceType: 'module' },
24-
},
25-
{
26-
code: "import defaultExport, * as foo from 'foo';",
92+
}),
93+
test({
94+
code: 'import defaultExport, * as foo from \'foo\';',
95+
output: 'import defaultExport, * as foo from \'foo\';',
2796
errors: [ {
2897
line: 1,
2998
column: 23,
3099
message: ERROR_MESSAGE,
31100
} ],
32-
parserOptions: { ecmaVersion: 2015, sourceType: 'module' },
33-
},
34-
{
35-
code: "import * as foo from './foo';",
101+
}),
102+
test({
103+
code: 'import * as foo from \'./foo\';',
104+
output: 'import * as foo from \'./foo\';',
36105
errors: [ {
37106
line: 1,
38107
column: 8,
39108
message: ERROR_MESSAGE,
40109
} ],
41-
parserOptions: { ecmaVersion: 2015, sourceType: 'module' },
42-
},
110+
}),
111+
...FIX_TESTS,
43112
],
44113
})

0 commit comments

Comments
 (0)