Skip to content

Commit 7c382f0

Browse files
maxkomarychevHypnosphialaddin-add
authored andcommitted
[New] no-unused-modules: support dynamic imports
All occurences of `import('...')` are treated as namespace imports (`import * as X from '...'`) See #1660, #2212. Co-authored-by: Max Komarychev <[email protected]> Co-authored-by: Filipp Riabchun <[email protected]> Co-authored-by: 薛定谔的猫 <[email protected]>
1 parent 7579748 commit 7c382f0

11 files changed

+198
-7
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
1010
- [`no-unresolved`]: add `caseSensitiveStrict` option ([#1262], thanks [@sergei-startsev])
1111
- [`no-unused-modules`]: add eslint v8 support ([#2194], thanks [@coderaiser])
1212
- [`no-restricted-paths`]: add/restore glob pattern support ([#2219], thanks [@stropho])
13+
- [`no-unused-modules`]: support dynamic imports ([#1660], [#2212], thanks [@maxkomarychev], [@aladdin-add], [@Hypnosphi])
1314

1415
## [2.24.2] - 2021-08-24
1516

@@ -906,6 +907,7 @@ for info on changes for earlier releases.
906907
[`memo-parser`]: ./memo-parser/README.md
907908

908909
[#2219]: https://github.com/import-js/eslint-plugin-import/pull/2219
910+
[#2212]: https://github.com/import-js/eslint-plugin-import/pull/2212
909911
[#2196]: https://github.com/import-js/eslint-plugin-import/pull/2196
910912
[#2194]: https://github.com/import-js/eslint-plugin-import/pull/2194
911913
[#2184]: https://github.com/import-js/eslint-plugin-import/pull/2184
@@ -983,6 +985,7 @@ for info on changes for earlier releases.
983985
[#1676]: https://github.com/import-js/eslint-plugin-import/pull/1676
984986
[#1666]: https://github.com/import-js/eslint-plugin-import/pull/1666
985987
[#1664]: https://github.com/import-js/eslint-plugin-import/pull/1664
988+
[#1660]: https://github.com/import-js/eslint-plugin-import/pull/1660
986989
[#1658]: https://github.com/import-js/eslint-plugin-import/pull/1658
987990
[#1651]: https://github.com/import-js/eslint-plugin-import/pull/1651
988991
[#1626]: https://github.com/import-js/eslint-plugin-import/pull/1626
@@ -1487,6 +1490,7 @@ for info on changes for earlier releases.
14871490
[@MatthiasKunnen]: https://github.com/MatthiasKunnen
14881491
[@mattijsbliek]: https://github.com/mattijsbliek
14891492
[@Maxim-Mazurok]: https://github.com/Maxim-Mazurok
1493+
[@maxkomarychev]: https://github.com/maxkomarychev
14901494
[@maxmalov]: https://github.com/maxmalov
14911495
[@MikeyBeLike]: https://github.com/MikeyBeLike
14921496
[@mplewis]: https://github.com/mplewis

docs/rules/no-unused-modules.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
Reports:
44
- modules without any exports
55
- individual exports not being statically `import`ed or `require`ed from other modules in the same project
6+
- dynamic imports are supported if argument is a literal string
67

7-
Note: dynamic imports are currently not supported.
88

99
## Rule Details
1010

src/ExportMap.js

+46-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import debug from 'debug';
77
import { SourceCode } from 'eslint';
88

99
import parse from 'eslint-module-utils/parse';
10+
import visit from 'eslint-module-utils/visit';
1011
import resolve from 'eslint-module-utils/resolve';
1112
import isIgnored, { hasValidExtension } from 'eslint-module-utils/ignore';
1213

@@ -354,15 +355,57 @@ ExportMap.parse = function (path, content, context) {
354355
const isEsModuleInteropTrue = isEsModuleInterop();
355356

356357
let ast;
358+
let visitorKeys;
357359
try {
358-
ast = parse(path, content, context);
360+
const result = parse(path, content, context);
361+
ast = result.ast;
362+
visitorKeys = result.visitorKeys;
359363
} catch (err) {
360-
log('parse error:', path, err);
361364
m.errors.push(err);
362365
return m; // can't continue
363366
}
364367

365-
if (!unambiguous.isModule(ast)) return null;
368+
m.visitorKeys = visitorKeys;
369+
370+
let hasDynamicImports = false;
371+
372+
function processDynamicImport(source) {
373+
hasDynamicImports = true;
374+
if (source.type !== 'Literal') {
375+
return null;
376+
}
377+
const p = remotePath(source.value);
378+
if (p == null) {
379+
return null;
380+
}
381+
const importedSpecifiers = new Set();
382+
importedSpecifiers.add('ImportNamespaceSpecifier');
383+
const getter = thunkFor(p, context);
384+
m.imports.set(p, {
385+
getter,
386+
declarations: new Set([{
387+
source: {
388+
// capturing actual node reference holds full AST in memory!
389+
value: source.value,
390+
loc: source.loc,
391+
},
392+
importedSpecifiers,
393+
}]),
394+
});
395+
}
396+
397+
visit(ast, visitorKeys, {
398+
ImportExpression(node) {
399+
processDynamicImport(node.source);
400+
},
401+
CallExpression(node) {
402+
if (node.callee.type === 'Import') {
403+
processDynamicImport(node.arguments[0]);
404+
}
405+
},
406+
});
407+
408+
if (!unambiguous.isModule(ast) && !hasDynamicImports) return null;
366409

367410
const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'];
368411
const docStyleParsers = {};

src/rules/no-unused-modules.js

+34-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import Exports, { recursivePatternCapture } from '../ExportMap';
88
import { getFileExtensions } from 'eslint-module-utils/ignore';
99
import resolve from 'eslint-module-utils/resolve';
10+
import visit from 'eslint-module-utils/visit';
1011
import docsUrl from '../docsUrl';
1112
import { dirname, join } from 'path';
1213
import readPkgUp from 'read-pkg-up';
@@ -154,6 +155,8 @@ const importList = new Map();
154155
*/
155156
const exportList = new Map();
156157

158+
const visitorKeyMap = new Map();
159+
157160
const ignoredFiles = new Set();
158161
const filesOutsideSrc = new Set();
159162

@@ -193,8 +196,15 @@ const prepareImportsAndExports = (srcFiles, context) => {
193196
const imports = new Map();
194197
const currentExports = Exports.get(file, context);
195198
if (currentExports) {
196-
const { dependencies, reexports, imports: localImportList, namespace } = currentExports;
197-
199+
const {
200+
dependencies,
201+
reexports,
202+
imports: localImportList,
203+
namespace,
204+
visitorKeys,
205+
} = currentExports;
206+
207+
visitorKeyMap.set(file, visitorKeys);
198208
// dependencies === export * from
199209
const currentExportAll = new Set();
200210
dependencies.forEach(getDependency => {
@@ -675,6 +685,28 @@ module.exports = {
675685
});
676686
});
677687

688+
function processDynamicImport(source) {
689+
if (source.type !== 'Literal') {
690+
return null;
691+
}
692+
const p = resolve(source.value, context);
693+
if (p == null) {
694+
return null;
695+
}
696+
newNamespaceImports.add(p);
697+
}
698+
699+
visit(node, visitorKeyMap.get(file), {
700+
ImportExpression(child) {
701+
processDynamicImport(child.source);
702+
},
703+
CallExpression(child) {
704+
if (child.callee.type === 'Import') {
705+
processDynamicImport(child.arguments[0]);
706+
}
707+
},
708+
});
709+
678710
node.body.forEach(astNode => {
679711
let resolvedPath;
680712

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const importPath = './exports-for-dynamic-js';
2+
class A {
3+
method() {
4+
const c = import(importPath)
5+
}
6+
}
7+
8+
9+
class B {
10+
method() {
11+
const c = import('i-do-not-exist')
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class A {
2+
method() {
3+
const c = import('./exports-for-dynamic-js')
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const a = 10;
2+
export const b = 20;
3+
export const c = 30;
4+
const d = 40;
5+
export default d;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const a = 10
2+
export const b = 20
3+
export const c = 30
4+
const d = 40
5+
export default d
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class A {
2+
method() {
3+
const c = import('./exports-for-dynamic-ts')
4+
}
5+
}
6+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const ts_a = 10
2+
export const ts_b = 20
3+
export const ts_c = 30
4+
const ts_d = 40
5+
export default ts_d

tests/src/rules/no-unused-modules.js

+74-1
Original file line numberDiff line numberDiff line change
@@ -112,41 +112,49 @@ ruleTester.run('no-unused-modules', rule, {
112112
options: unusedExportsOptions,
113113
code: 'import { o2 } from "./file-o";export default () => 12',
114114
filename: testFilePath('./no-unused-modules/file-a.js'),
115+
parser: require.resolve('babel-eslint'),
115116
}),
116117
test({
117118
options: unusedExportsOptions,
118119
code: 'export const b = 2',
119120
filename: testFilePath('./no-unused-modules/file-b.js'),
121+
parser: require.resolve('babel-eslint'),
120122
}),
121123
test({
122124
options: unusedExportsOptions,
123125
code: 'const c1 = 3; function c2() { return 3 }; export { c1, c2 }',
124126
filename: testFilePath('./no-unused-modules/file-c.js'),
127+
parser: require.resolve('babel-eslint'),
125128
}),
126129
test({
127130
options: unusedExportsOptions,
128131
code: 'export function d() { return 4 }',
129132
filename: testFilePath('./no-unused-modules/file-d.js'),
133+
parser: require.resolve('babel-eslint'),
130134
}),
131135
test({
132136
options: unusedExportsOptions,
133137
code: 'export class q { q0() {} }',
134138
filename: testFilePath('./no-unused-modules/file-q.js'),
139+
parser: require.resolve('babel-eslint'),
135140
}),
136141
test({
137142
options: unusedExportsOptions,
138143
code: 'const e0 = 5; export { e0 as e }',
139144
filename: testFilePath('./no-unused-modules/file-e.js'),
145+
parser: require.resolve('babel-eslint'),
140146
}),
141147
test({
142148
options: unusedExportsOptions,
143149
code: 'const l0 = 5; const l = 10; export { l0 as l1, l }; export default () => {}',
144150
filename: testFilePath('./no-unused-modules/file-l.js'),
151+
parser: require.resolve('babel-eslint'),
145152
}),
146153
test({
147154
options: unusedExportsOptions,
148155
code: 'const o0 = 0; const o1 = 1; export { o0, o1 as o2 }; export default () => {}',
149156
filename: testFilePath('./no-unused-modules/file-o.js'),
157+
parser: require.resolve('babel-eslint'),
150158
}),
151159
],
152160
invalid: [
@@ -234,7 +242,72 @@ ruleTester.run('no-unused-modules', rule, {
234242
],
235243
});
236244

237-
// test for export from
245+
246+
describe('dynamic imports', () => {
247+
if (semver.satisfies(eslintPkg.version, '< 6')) {
248+
beforeEach(function () {
249+
this.skip();
250+
});
251+
return;
252+
}
253+
254+
// test for unused exports with `import()`
255+
ruleTester.run('no-unused-modules', rule, {
256+
valid: [
257+
test({
258+
options: unusedExportsOptions,
259+
code: `
260+
export const a = 10
261+
export const b = 20
262+
export const c = 30
263+
const d = 40
264+
export default d
265+
`,
266+
parser: require.resolve('babel-eslint'),
267+
filename: testFilePath('./no-unused-modules/exports-for-dynamic-js.js'),
268+
}),
269+
],
270+
invalid: [
271+
test({
272+
options: unusedExportsOptions,
273+
code: `
274+
export const a = 10
275+
export const b = 20
276+
export const c = 30
277+
const d = 40
278+
export default d
279+
`,
280+
parser: require.resolve('babel-eslint'),
281+
filename: testFilePath('./no-unused-modules/exports-for-dynamic-js-2.js'),
282+
errors: [
283+
error(`exported declaration 'a' not used within other modules`),
284+
error(`exported declaration 'b' not used within other modules`),
285+
error(`exported declaration 'c' not used within other modules`),
286+
error(`exported declaration 'default' not used within other modules`),
287+
] }),
288+
],
289+
});
290+
typescriptRuleTester.run('no-unused-modules', rule, {
291+
valid: [
292+
test({
293+
options: unusedExportsTypescriptOptions,
294+
code: `
295+
export const ts_a = 10
296+
export const ts_b = 20
297+
export const ts_c = 30
298+
const ts_d = 40
299+
export default ts_d
300+
`,
301+
parser: require.resolve('@typescript-eslint/parser'),
302+
filename: testFilePath('./no-unused-modules/typescript/exports-for-dynamic-ts.ts'),
303+
}),
304+
],
305+
invalid: [
306+
],
307+
});
308+
});
309+
310+
// // test for export from
238311
ruleTester.run('no-unused-modules', rule, {
239312
valid: [
240313
test({

0 commit comments

Comments
 (0)