Skip to content

Commit 4f338bf

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 '...'`) Co-authored-by: Max Komarychev <[email protected]> Co-authored-by: Filipp Riabchun <[email protected]> Co-authored-by: 薛定谔的猫 <[email protected]>
1 parent 513bb0b commit 4f338bf

15 files changed

+348
-11
lines changed

CHANGELOG.md

+3
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
@@ -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

+44-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,55 @@ 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) {
360-
log('parse error:', path, err);
361362
m.errors.push(err);
362363
return m; // can't continue
363364
}
364365

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

367408
const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'];
368409
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';
@@ -144,6 +145,8 @@ const importList = new Map();
144145
*/
145146
const exportList = new Map();
146147

148+
const visitorKeyMap = new Map();
149+
147150
const ignoredFiles = new Set();
148151
const filesOutsideSrc = new Set();
149152

@@ -183,8 +186,15 @@ const prepareImportsAndExports = (srcFiles, context) => {
183186
const imports = new Map();
184187
const currentExports = Exports.get(file, context);
185188
if (currentExports) {
186-
const { dependencies, reexports, imports: localImportList, namespace } = currentExports;
187-
189+
const {
190+
dependencies,
191+
reexports,
192+
imports: localImportList,
193+
namespace,
194+
visitorKeys,
195+
} = currentExports;
196+
197+
visitorKeyMap.set(file, visitorKeys);
188198
// dependencies === export * from
189199
const currentExportAll = new Set();
190200
dependencies.forEach(getDependency => {
@@ -665,6 +675,28 @@ module.exports = {
665675
});
666676
});
667677

678+
function processDynamicImport(source) {
679+
if (source.type !== 'Literal') {
680+
return null;
681+
}
682+
const p = resolve(source.value, context);
683+
if (p == null) {
684+
return null;
685+
}
686+
newNamespaceImports.add(p);
687+
}
688+
689+
visit(node, visitorKeyMap.get(file), {
690+
ImportExpression(child) {
691+
processDynamicImport(child.source);
692+
},
693+
CallExpression(child) {
694+
if (child.callee.type === 'Import') {
695+
processDynamicImport(child.arguments[0]);
696+
}
697+
},
698+
});
699+
668700
node.body.forEach(astNode => {
669701
let resolvedPath;
670702

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

+148
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,59 @@ ruleTester.run('no-unused-modules', rule, {
148148
code: 'const o0 = 0; const o1 = 1; export { o0, o1 as o2 }; export default () => {}',
149149
filename: testFilePath('./no-unused-modules/file-o.js'),
150150
}),
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+
}),
199+
test({ options: unusedExportsOptions,
200+
code: 'export class q { q0() {} }',
201+
filename: testFilePath('./no-unused-modules/file-q.js'),
202+
parser: require.resolve('babel-eslint'),
203+
}),
151204
],
152205
invalid: [
153206
test({
@@ -234,6 +287,89 @@ ruleTester.run('no-unused-modules', rule, {
234287
],
235288
});
236289

290+
// test for export from
291+
ruleTester.run('no-unused-modules', rule, {
292+
valid: [
293+
test({
294+
options: unusedExportsOptions,
295+
code: `
296+
export const a = 10
297+
export const b = 20
298+
export const c = 30
299+
const d = 40
300+
export default d
301+
`,
302+
parser: require.resolve('babel-eslint'),
303+
filename: testFilePath('./no-unused-modules/exports-for-dynamic-js.js'),
304+
}),
305+
],
306+
invalid: [
307+
test({
308+
options: unusedExportsOptions,
309+
code: `
310+
export const a = 10
311+
export const b = 20
312+
export const c = 30
313+
const d = 40
314+
export default d
315+
`,
316+
parser: require.resolve('babel-eslint'),
317+
filename: testFilePath('./no-unused-modules/exports-for-dynamic-js-2.js'),
318+
errors: [
319+
error(`exported declaration 'a' not used within other modules`),
320+
error(`exported declaration 'b' not used within other modules`),
321+
error(`exported declaration 'c' not used within other modules`),
322+
error(`exported declaration 'default' not used within other modules`),
323+
],
324+
}),
325+
],
326+
});
327+
328+
describe('dynamic imports', () => {
329+
if (semver.satisfies(eslintPkg.version, '< 6')) {
330+
this.skip();
331+
return;
332+
}
333+
334+
// test for unused exports with `import()`
335+
ruleTester.run('no-unused-modules', rule, {
336+
valid: [
337+
test({
338+
options: unusedExportsOptions,
339+
code: `
340+
export const a = 10
341+
export const b = 20
342+
export const c = 30
343+
const d = 40
344+
export default d
345+
`,
346+
parser: require.resolve('babel-eslint'),
347+
filename: testFilePath('./no-unused-modules/exports-for-dynamic-js.js'),
348+
}),
349+
],
350+
invalid: [
351+
test({
352+
options: unusedExportsOptions,
353+
code: `
354+
export const a = 10
355+
export const b = 20
356+
export const c = 30
357+
const d = 40
358+
export default d
359+
`,
360+
parser: require.resolve('babel-eslint'),
361+
filename: testFilePath('./no-unused-modules/exports-for-dynamic-js-2.js'),
362+
errors: [
363+
error(`exported declaration 'a' not used within other modules`),
364+
error(`exported declaration 'b' not used within other modules`),
365+
error(`exported declaration 'c' not used within other modules`),
366+
error(`exported declaration 'default' not used within other modules`),
367+
],
368+
}),
369+
],
370+
});
371+
});
372+
237373
// test for export from
238374
ruleTester.run('no-unused-modules', rule, {
239375
valid: [
@@ -951,6 +1087,18 @@ context('TypeScript', function () {
9511087
getTSParsers().forEach((parser) => {
9521088
typescriptRuleTester.run('no-unused-modules', rule, {
9531089
valid: [].concat(
1090+
test({
1091+
options: unusedExportsTypescriptOptions,
1092+
code: `
1093+
export const ts_a = 10;
1094+
export const ts_b = 20;
1095+
export const ts_c = 30;
1096+
const ts_d = 40;
1097+
export default ts_d;
1098+
`,
1099+
parser: require.resolve('@typescript-eslint/parser'),
1100+
filename: testFilePath('./no-unused-modules/typescript/exports-for-dynamic-ts.ts'),
1101+
}),
9541102
test({
9551103
options: unusedExportsTypescriptOptions,
9561104
code: `

0 commit comments

Comments
 (0)