Skip to content

Commit eee0cc8

Browse files
maxkomarychevljharb
authored andcommitted
[New] no-unused-modules: support dynamic imports
All occurences of `import('...')` are treated as namespace imports (`import * as X from '...'`)
1 parent b02885d commit eee0cc8

14 files changed

+315
-12
lines changed

CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
1616
- [`named`]: add `commonjs` option ([#1222], thanks [@vikr01])
1717
- [`no-namespace`]: Add `ignore` option ([#2112], thanks [@aberezkin])
1818
- [`max-dependencies`]: add option `ignoreTypeImports` ([#1847], thanks [@rfermann])
19+
- [`no-unused-modules`]: support dynamic imports ([#1660], thanks [@maxkomarychev])
1920

2021
### Fixed
2122
- [`no-duplicates`]: ensure autofix avoids excessive newlines ([#2028], thanks [@ertrzyiks])
@@ -959,6 +960,7 @@ for info on changes for earlier releases.
959960
[#1676]: https://github.com/import-js/eslint-plugin-import/pull/1676
960961
[#1666]: https://github.com/import-js/eslint-plugin-import/pull/1666
961962
[#1664]: https://github.com/import-js/eslint-plugin-import/pull/1664
963+
[#1660]: https://github.com/import-js/eslint-plugin-import/pull/1660
962964
[#1658]: https://github.com/import-js/eslint-plugin-import/pull/1658
963965
[#1651]: https://github.com/import-js/eslint-plugin-import/pull/1651
964966
[#1626]: https://github.com/import-js/eslint-plugin-import/pull/1626
@@ -1427,8 +1429,8 @@ for info on changes for earlier releases.
14271429
[@kiwka]: https://github.com/kiwka
14281430
[@klimashkin]: https://github.com/klimashkin
14291431
[@kmui2]: https://github.com/kmui2
1430-
[@KostyaZgara]: https://github.com/KostyaZgara
14311432
[@knpwrs]: https://github.com/knpwrs
1433+
[@KostyaZgara]: https://github.com/KostyaZgara
14321434
[@laysent]: https://github.com/laysent
14331435
[@le0nik]: https://github.com/le0nik
14341436
[@lemonmade]: https://github.com/lemonmade
@@ -1454,6 +1456,7 @@ for info on changes for earlier releases.
14541456
[@MatthiasKunnen]: https://github.com/MatthiasKunnen
14551457
[@mattijsbliek]: https://github.com/mattijsbliek
14561458
[@Maxim-Mazurok]: https://github.com/Maxim-Mazurok
1459+
[@maxkomarychev]: https://github.com/maxkomarychev
14571460
[@maxmalov]: https://github.com/maxmalov
14581461
[@MikeyBeLike]: https://github.com/MikeyBeLike
14591462
[@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

+34-2
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,46 @@ 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+
({ ast, visitorKeys } = parse(path, content, context));
359361
} catch (err) {
360362
log('parse error:', path, err);
361363
m.errors.push(err);
362364
return m; // can't continue
363365
}
364366

365-
if (!unambiguous.isModule(ast)) return null;
367+
let hasDynamicImports = false;
368+
369+
visit(ast, visitorKeys, {
370+
CallExpression(node) {
371+
if (node.callee.type === 'Import') {
372+
hasDynamicImports = true;
373+
const firstArgument = node.arguments[0];
374+
if (firstArgument.type !== 'Literal') {
375+
return null;
376+
}
377+
const p = remotePath(firstArgument.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+
source: {
387+
// capturing actual node reference holds full AST in memory!
388+
value: firstArgument.value,
389+
loc: firstArgument.loc,
390+
},
391+
importedSpecifiers,
392+
});
393+
}
394+
},
395+
});
396+
397+
if (!unambiguous.isModule(ast) && !hasDynamicImports) return null;
366398

367399
const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'];
368400
const docStyleParsers = {};
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

+166-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import typescriptConfig from '../../../config/typescript';
44

55
import { RuleTester } from 'eslint';
66
import fs from 'fs';
7-
import semver from 'semver';
87
import eslintPkg from 'eslint/package.json';
8+
import semver from 'semver';
99

1010
// TODO: figure out why these tests fail in eslint 4
1111
const isESLint4TODO = semver.satisfies(eslintPkg.version, '^4');
@@ -108,7 +108,6 @@ ruleTester.run('no-unused-modules', rule, {
108108
// tests for exports
109109
ruleTester.run('no-unused-modules', rule, {
110110
valid: [
111-
112111
test({
113112
options: unusedExportsOptions,
114113
code: 'import { o2 } from "./file-o";export default () => 12',
@@ -149,6 +148,54 @@ ruleTester.run('no-unused-modules', rule, {
149148
code: 'const o0 = 0; const o1 = 1; export { o0, o1 as o2 }; export default () => {}',
150149
filename: testFilePath('./no-unused-modules/file-o.js'),
151150
}),
151+
test({
152+
options: unusedExportsOptions,
153+
code: 'import { o2 } from "./file-o";export default () => 12',
154+
filename: testFilePath('./no-unused-modules/file-a.js'),
155+
parser: require.resolve('babel-eslint'),
156+
}),
157+
test({
158+
options: unusedExportsOptions,
159+
code: 'export const b = 2',
160+
filename: testFilePath('./no-unused-modules/file-b.js'),
161+
parser: require.resolve('babel-eslint'),
162+
}),
163+
test({
164+
options: unusedExportsOptions,
165+
code: 'const c1 = 3; function c2() { return 3 }; export { c1, c2 }',
166+
filename: testFilePath('./no-unused-modules/file-c.js'),
167+
parser: require.resolve('babel-eslint'),
168+
}),
169+
test({
170+
options: unusedExportsOptions,
171+
code: 'export function d() { return 4 }',
172+
filename: testFilePath('./no-unused-modules/file-d.js'),
173+
parser: require.resolve('babel-eslint'),
174+
}),
175+
test({
176+
options: unusedExportsOptions,
177+
code: 'export class q { q0() {} }',
178+
filename: testFilePath('./no-unused-modules/file-q.js'),
179+
parser: require.resolve('babel-eslint'),
180+
}),
181+
test({
182+
options: unusedExportsOptions,
183+
code: 'const e0 = 5; export { e0 as e }',
184+
filename: testFilePath('./no-unused-modules/file-e.js'),
185+
parser: require.resolve('babel-eslint'),
186+
}),
187+
test({
188+
options: unusedExportsOptions,
189+
code: 'const l0 = 5; const l = 10; export { l0 as l1, l }; export default () => {}',
190+
filename: testFilePath('./no-unused-modules/file-l.js'),
191+
parser: require.resolve('babel-eslint'),
192+
}),
193+
test({
194+
options: unusedExportsOptions,
195+
code: 'const o0 = 0; const o1 = 1; export { o0, o1 as o2 }; export default () => {}',
196+
filename: testFilePath('./no-unused-modules/file-o.js'),
197+
parser: require.resolve('babel-eslint'),
198+
}),
152199
],
153200
invalid: [
154201
test({
@@ -235,7 +282,123 @@ ruleTester.run('no-unused-modules', rule, {
235282
],
236283
});
237284

238-
// // test for export from
285+
// test for unused exports with `import()`
286+
ruleTester.run('no-unused-modules', rule, {
287+
valid: [
288+
test({
289+
options: unusedExportsOptions,
290+
code: `
291+
export const a = 10
292+
export const b = 20
293+
export const c = 30
294+
const d = 40
295+
export default d
296+
`,
297+
parser: require.resolve('babel-eslint'),
298+
filename: testFilePath('./no-unused-modules/exports-for-dynamic-js.js'),
299+
}),
300+
],
301+
invalid: [
302+
test({
303+
options: unusedExportsOptions,
304+
code: `
305+
export const a = 10
306+
export const b = 20
307+
export const c = 30
308+
const d = 40
309+
export default d
310+
`,
311+
parser: require.resolve('babel-eslint'),
312+
filename: testFilePath('./no-unused-modules/exports-for-dynamic-js-2.js'),
313+
errors: [
314+
error(`exported declaration 'a' not used within other modules`),
315+
error(`exported declaration 'b' not used within other modules`),
316+
error(`exported declaration 'c' not used within other modules`),
317+
error(`exported declaration 'default' not used within other modules`),
318+
],
319+
}),
320+
],
321+
});
322+
typescriptRuleTester.run('no-unused-modules', rule, {
323+
valid: [
324+
test({
325+
options: unusedExportsTypescriptOptions,
326+
code: `
327+
export const ts_a = 10
328+
export const ts_b = 20
329+
export const ts_c = 30
330+
const ts_d = 40
331+
export default ts_d
332+
`,
333+
parser: require.resolve('@typescript-eslint/parser'),
334+
filename: testFilePath('./no-unused-modules/typescript/exports-for-dynamic-ts.ts'),
335+
}),
336+
],
337+
invalid: [
338+
],
339+
});
340+
341+
describe('dynamic imports', () => {
342+
if (semver.satisfies(eslintPkg.version, '< 6')) {
343+
this.skip();
344+
return;
345+
}
346+
347+
// test for unused exports with `import()`
348+
ruleTester.run('no-unused-modules', rule, {
349+
valid: [
350+
test({ options: unusedExportsOptions,
351+
code: `
352+
export const a = 10
353+
export const b = 20
354+
export const c = 30
355+
const d = 40
356+
export default d
357+
`,
358+
parser: require.resolve('babel-eslint'),
359+
filename: testFilePath('./no-unused-modules/exports-for-dynamic-js.js'),
360+
}),
361+
],
362+
invalid: [
363+
test({ options: unusedExportsOptions,
364+
code: `
365+
export const a = 10
366+
export const b = 20
367+
export const c = 30
368+
const d = 40
369+
export default d
370+
`,
371+
parser: require.resolve('babel-eslint'),
372+
filename: testFilePath('./no-unused-modules/exports-for-dynamic-js-2.js'),
373+
errors: [
374+
error(`exported declaration 'a' not used within other modules`),
375+
error(`exported declaration 'b' not used within other modules`),
376+
error(`exported declaration 'c' not used within other modules`),
377+
error(`exported declaration 'default' not used within other modules`),
378+
],
379+
}),
380+
],
381+
});
382+
typescriptRuleTester.run('no-unused-modules', rule, {
383+
valid: [
384+
test({ options: unusedExportsTypescriptOptions,
385+
code: `
386+
export const ts_a = 10
387+
export const ts_b = 20
388+
export const ts_c = 30
389+
const ts_d = 40
390+
export default ts_d
391+
`,
392+
parser: require.resolve('@typescript-eslint/parser'),
393+
filename: testFilePath('./no-unused-modules/typescript/exports-for-dynamic-ts.ts'),
394+
}),
395+
],
396+
invalid: [
397+
],
398+
});
399+
});
400+
401+
// test for export from
239402
ruleTester.run('no-unused-modules', rule, {
240403
valid: [
241404
test({

utils/CHANGELOG.md

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

66
## Unreleased
77

8+
### Added
9+
- [`no-unused-modules`]: support dynamic imports ([#1660], thanks [@maxkomarychev])
10+
811
## v2.6.2 - 2021-08-08
912

1013
### Fixed
@@ -94,6 +97,7 @@ Yanked due to critical issue with cache key resulting from #839.
9497
[#2026]: https://github.com/import-js/eslint-plugin-import/pull/2026
9598
[#1786]: https://github.com/import-js/eslint-plugin-import/pull/1786
9699
[#1671]: https://github.com/import-js/eslint-plugin-import/pull/1671
100+
[#1660]: https://github.com/import-js/eslint-plugin-import/pull/1660
97101
[#1606]: https://github.com/import-js/eslint-plugin-import/pull/1606
98102
[#1602]: https://github.com/import-js/eslint-plugin-import/pull/1602
99103
[#1591]: https://github.com/import-js/eslint-plugin-import/pull/1591
@@ -118,6 +122,7 @@ Yanked due to critical issue with cache key resulting from #839.
118122
[@JounQin]: https://github.com/JounQin
119123
[@kaiyoma]: https://github.com/kaiyoma
120124
[@manuth]: https://github.com/manuth
125+
[@maxkomarychev]: https://github.com/maxkomarychev
121126
[@pmcelhaney]: https://github.com/pmcelhaney
122127
[@sompylasar]: https://github.com/sompylasar
123128
[@timkraut]: https://github.com/timkraut

0 commit comments

Comments
 (0)