Skip to content

Commit 060ea2c

Browse files
committed
no-unused-modules: support dynamic imports
This is still work in progress but I'd like to receive initial thoughts on this implementation before I proceed. This introduces support of dynamic imports for a rule `no-unused-modules`. So far only "await" form is implemented: ```js const a = await import("a") ``` is equivalent to default import ```js import a from "a" ``` ```js const {a,b,c} = await import("a") ``` is equivalent to named imports ```js import {a,b,c} from "a" ``` Support import('name').then(a) and import('name').then({a,b,c}) to be addded soon. TODO/Open questions - [ ] Existing code is relying on the fact that all imports/reexports happen at top level of the file while dynamic import can happen anywhere - that's why I had to implement visitor against visitor keys of the parser myself not very happy about it - but couldn't figure out quickly how to use existing visitor (if any?) supplied by a parser. I also learned that different parsers have visitor keys defined in different places so this has to be dealt with as well.
1 parent 2beec94 commit 060ea2c

File tree

7 files changed

+146
-18
lines changed

7 files changed

+146
-18
lines changed

src/ExportMap.js

+60-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import debug from 'debug'
66

77
import { SourceCode } from 'eslint'
88

9-
import parse from 'eslint-module-utils/parse'
9+
import parse, { visit } from 'eslint-module-utils/parse'
1010
import resolve from 'eslint-module-utils/resolve'
1111
import isIgnored, { hasValidExtension } from 'eslint-module-utils/ignore'
1212

@@ -351,9 +351,66 @@ ExportMap.parse = function (path, content, context) {
351351
return m // can't continue
352352
}
353353

354-
if (!unambiguous.isModule(ast)) return null
354+
let hasDynamicImports = false
355355

356-
const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc']
356+
let declarator = null
357+
visit(ast, path, context, {
358+
VariableDeclarator(node) {
359+
declarator = node
360+
if (node.id.type === 'Identifier') {
361+
log('Declarator', node.id.name)
362+
} else if (node.id.type === 'ObjectPattern') {
363+
log('Object pattern')
364+
}
365+
},
366+
'VariableDeclarator:Exit': function() {
367+
declarator = null
368+
},
369+
CallExpression(node) {
370+
log('CALL', node.callee.type)
371+
// log(JSON.stringify(node))
372+
if (node.callee.type === 'Import') {
373+
hasDynamicImports = true
374+
const p = remotePath(node.arguments[0].value)
375+
if (p == null) return null
376+
if (declarator) {
377+
log('IDDDDDDDD', declarator.id.name)
378+
}
379+
const importLiteral = node.arguments[0]
380+
const importedSpecifiers = new Set()
381+
if (declarator) {
382+
if (declarator.id.type === 'Identifier') {
383+
importedSpecifiers.add('ImportDefaultSpecifier')
384+
} else if (declarator.id.type === 'ObjectPattern') {
385+
for (const property of declarator.id.properties) {
386+
log('ADD PROPERTY', property.key.name)
387+
importedSpecifiers.add(property.key.name)
388+
}
389+
}
390+
}
391+
const getter = thunkFor(p, context)
392+
m.imports.set(p, {
393+
getter,
394+
source: {
395+
// capturing actual node reference holds full AST in memory!
396+
value: importLiteral.value,
397+
loc: importLiteral.loc,
398+
},
399+
importedSpecifiers,
400+
})
401+
}
402+
},
403+
})
404+
405+
if (!unambiguous.isModule(ast) && !hasDynamicImports) {
406+
log('is not a module', path, hasDynamicImports)
407+
return null
408+
} else {
409+
log('is a module!', path)
410+
}
411+
412+
const docstyle = (context.settings &&
413+
context.settings['import/docstyle']) || ['jsdoc']
357414
const docStyleParsers = {}
358415
docstyle.forEach(style => {
359416
docStyleParsers[style] = availableDocStyleParsers[style]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
async function main() {
2+
const value = await import("./exports-for-dynamic")
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
async function main() {
2+
const { importMeDynamicallyA, } = await import("./exports-for-dynamic")
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const importMeDynamicallyA = 100;
2+
const importMeDynamicallyB = 200;
3+
export const importMeDynamicallyC = 100;
4+
export default importMeDynamicallayB;

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

+42-13
Original file line numberDiff line numberDiff line change
@@ -71,32 +71,47 @@ ruleTester.run('no-unused-modules', rule, {
7171
// tests for exports
7272
ruleTester.run('no-unused-modules', rule, {
7373
valid: [
74-
7574
test({ options: unusedExportsOptions,
76-
code: 'import { o2 } from "./file-o";export default () => 12',
77-
filename: testFilePath('./no-unused-modules/file-a.js')}),
75+
code: 'import { o2 } from "./file-o";export default () => 12',
76+
filename: testFilePath('./no-unused-modules/file-a.js'),
77+
parser: require.resolve('babel-eslint')}),
7878
test({ options: unusedExportsOptions,
7979
code: 'export const b = 2',
80-
filename: testFilePath('./no-unused-modules/file-b.js')}),
80+
filename: testFilePath('./no-unused-modules/file-b.js'),
81+
parser: require.resolve('babel-eslint')}),
8182
test({ options: unusedExportsOptions,
8283
code: 'const c1 = 3; function c2() { return 3 }; export { c1, c2 }',
83-
filename: testFilePath('./no-unused-modules/file-c.js')}),
84+
filename: testFilePath('./no-unused-modules/file-c.js'),
85+
parser: require.resolve('babel-eslint')}),
8486
test({ options: unusedExportsOptions,
8587
code: 'export function d() { return 4 }',
86-
filename: testFilePath('./no-unused-modules/file-d.js')}),
88+
filename: testFilePath('./no-unused-modules/file-d.js'),
89+
parser: require.resolve('babel-eslint')}),
8790
test({ options: unusedExportsOptions,
8891
code: 'export class q { q0() {} }',
89-
filename: testFilePath('./no-unused-modules/file-q.js')}),
92+
filename: testFilePath('./no-unused-modules/file-q.js'),
93+
parser: require.resolve('babel-eslint')}),
9094
test({ options: unusedExportsOptions,
9195
code: 'const e0 = 5; export { e0 as e }',
92-
filename: testFilePath('./no-unused-modules/file-e.js')}),
96+
filename: testFilePath('./no-unused-modules/file-e.js'),
97+
parser: require.resolve('babel-eslint')}),
9398
test({ options: unusedExportsOptions,
9499
code: 'const l0 = 5; const l = 10; export { l0 as l1, l }; export default () => {}',
95-
filename: testFilePath('./no-unused-modules/file-l.js')}),
100+
filename: testFilePath('./no-unused-modules/file-l.js'),
101+
parser: require.resolve('babel-eslint')}),
96102
test({ options: unusedExportsOptions,
97103
code: 'const o0 = 0; const o1 = 1; export { o0, o1 as o2 }; export default () => {}',
98-
filename: testFilePath('./no-unused-modules/file-o.js')}),
99-
],
104+
filename: testFilePath('./no-unused-modules/file-o.js'),
105+
parser: require.resolve('babel-eslint')}),
106+
test({ options: unusedExportsOptions,
107+
code: `
108+
export const importMeDynamicallyA = 100;
109+
const importMeDynamicallyB = 200;
110+
export default importMeDynamicallayB;
111+
`,
112+
parser: require.resolve('babel-eslint'),
113+
filename: testFilePath('./no-unused-modules/exports-for-dynamic.js')}),
114+
],
100115
invalid: [
101116
test({ options: unusedExportsOptions,
102117
code: `import eslint from 'eslint'
@@ -117,11 +132,25 @@ ruleTester.run('no-unused-modules', rule, {
117132
error(`exported declaration 'o0' not used within other modules`),
118133
error(`exported declaration 'o3' not used within other modules`),
119134
error(`exported declaration 'p' not used within other modules`),
120-
]}),
135+
],
136+
}),
121137
test({ options: unusedExportsOptions,
122138
code: `const n0 = 'n0'; const n1 = 42; export { n0, n1 }; export default () => {}`,
123139
filename: testFilePath('./no-unused-modules/file-n.js'),
124-
errors: [error(`exported declaration 'default' not used within other modules`)]}),
140+
errors: [
141+
error(`exported declaration 'default' not used within other modules`),
142+
]}),
143+
test({ options: unusedExportsOptions,
144+
code: `
145+
export const importMeDynamicallyC = 100;
146+
`,
147+
parser: require.resolve('babel-eslint'),
148+
filename: testFilePath('./no-unused-modules/exports-for-dynamic.js'),
149+
errors: [
150+
error(
151+
`exported declaration 'importMeDynamicallyC' not used within other modules`,
152+
),
153+
]}),
125154
],
126155
})
127156

utils/parse.js

+33
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,39 @@ exports.default = function parse(path, content, context) {
6363
return parser.parse(content, parserOptions)
6464
}
6565

66+
function __visit(node, keys, visitorSpec) {
67+
if (!node) {
68+
return
69+
}
70+
const type = node.type
71+
if (typeof visitorSpec[type] === 'function') {
72+
visitorSpec[type](node)
73+
}
74+
const childFields = keys[type]
75+
if (!childFields) {
76+
return
77+
}
78+
for (const fieldName of childFields) {
79+
const field = node[fieldName]
80+
if (Array.isArray(field)) {
81+
for (const item of field) {
82+
__visit(item, keys, visitorSpec)
83+
}
84+
} else {
85+
__visit(field, keys, visitorSpec)
86+
}
87+
}
88+
if (typeof visitorSpec[`${type}:Exit`] === 'function') {
89+
visitorSpec[`${type}:Exit`](node)
90+
}
91+
}
92+
93+
exports.visit = function (ast, path, context, visitorSpec) {
94+
const parserPath = getParserPath(path, context)
95+
const keys = moduleRequire(parserPath.replace('index.js', 'visitor-keys.js'))
96+
__visit(ast, keys, visitorSpec)
97+
}
98+
6699
function getParserPath(path, context) {
67100
const parsers = context.settings['import/parsers']
68101
if (parsers != null) {

utils/unambiguous.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
'use strict'
22
exports.__esModule = true
33

4-
5-
const pattern = /(^|;)\s*(export|import)((\s+\w)|(\s*[{*=]))/m
4+
const pattern = /(^|;)\s*(export|import)((\s+\w)|(\s*[{*=]))|import\(/m
65
/**
76
* detect possible imports/exports without a full parse.
87
*

0 commit comments

Comments
 (0)