From 51185dd347a7c83904743433592b2f74d73e709d Mon Sep 17 00:00:00 2001 From: Ivan Rubinson Date: Wed, 13 Mar 2024 18:32:47 +0200 Subject: [PATCH] [Refactor] `ExportMap`: make procedures static instead of monkeypatching exportmap --- CHANGELOG.md | 2 + src/ExportMap.js | 908 +++++++++++++++++++++++------------------------ 2 files changed, 456 insertions(+), 454 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06cdb922e..9552774dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Changed - [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) - [`no-unused-modules`]: add console message to help debug [#2866] +- [Refactor] `ExportMap`: make procedures static instead of monkeypatching exportmap ([#2982], thanks [@soryy708]) ## [2.29.1] - 2023-12-14 @@ -1108,6 +1109,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#2982]: https://github.com/import-js/eslint-plugin-import/pull/2982 [#2944]: https://github.com/import-js/eslint-plugin-import/pull/2944 [#2942]: https://github.com/import-js/eslint-plugin-import/pull/2942 [#2919]: https://github.com/import-js/eslint-plugin-import/pull/2919 diff --git a/src/ExportMap.js b/src/ExportMap.js index f61d3c170..9ee65a504 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -26,6 +26,97 @@ const log = debug('eslint-plugin-import:ExportMap'); const exportCache = new Map(); const tsconfigCache = new Map(); +/** + * parse docs from the first node that has leading comments + */ +function captureDoc(source, docStyleParsers, ...nodes) { + const metadata = {}; + + // 'some' short-circuits on first 'true' + nodes.some((n) => { + try { + + let leadingComments; + + // n.leadingComments is legacy `attachComments` behavior + if ('leadingComments' in n) { + leadingComments = n.leadingComments; + } else if (n.range) { + leadingComments = source.getCommentsBefore(n); + } + + if (!leadingComments || leadingComments.length === 0) { return false; } + + for (const name in docStyleParsers) { + const doc = docStyleParsers[name](leadingComments); + if (doc) { + metadata.doc = doc; + } + } + + return true; + } catch (err) { + return false; + } + }); + + return metadata; +} + +/** + * parse JSDoc from leading comments + * @param {object[]} comments + * @return {{ doc: object }} + */ +function captureJsDoc(comments) { + let doc; + + // capture XSDoc + comments.forEach((comment) => { + // skip non-block comments + if (comment.type !== 'Block') { return; } + try { + doc = doctrine.parse(comment.value, { unwrap: true }); + } catch (err) { + /* don't care, for now? maybe add to `errors?` */ + } + }); + + return doc; +} + +/** + * parse TomDoc section from comments + */ +function captureTomDoc(comments) { + // collect lines up to first paragraph break + const lines = []; + for (let i = 0; i < comments.length; i++) { + const comment = comments[i]; + if (comment.value.match(/^\s*$/)) { break; } + lines.push(comment.value.trim()); + } + + // return doctrine-like object + const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/); + if (statusMatch) { + return { + description: statusMatch[2], + tags: [{ + title: statusMatch[1].toLowerCase(), + description: statusMatch[2], + }], + }; + } +} + +const availableDocStyleParsers = { + jsdoc: captureJsDoc, + tomdoc: captureTomDoc, +}; + +const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']); + export default class ExportMap { constructor(path) { this.path = path; @@ -203,534 +294,443 @@ export default class ExportMap { message: `Parse errors in imported module '${declaration.source.value}': ${msg}`, }); } -} -/** - * parse docs from the first node that has leading comments - */ -function captureDoc(source, docStyleParsers, ...nodes) { - const metadata = {}; + static get(source, context) { + const path = resolve(source, context); + if (path == null) { return null; } - // 'some' short-circuits on first 'true' - nodes.some((n) => { - try { + return ExportMap.for(childContext(path, context)); + } - let leadingComments; + static for(context) { + const { path } = context; - // n.leadingComments is legacy `attachComments` behavior - if ('leadingComments' in n) { - leadingComments = n.leadingComments; - } else if (n.range) { - leadingComments = source.getCommentsBefore(n); - } + const cacheKey = context.cacheKey || hashObject(context).digest('hex'); + let exportMap = exportCache.get(cacheKey); - if (!leadingComments || leadingComments.length === 0) { return false; } + // return cached ignore + if (exportMap === null) { return null; } - for (const name in docStyleParsers) { - const doc = docStyleParsers[name](leadingComments); - if (doc) { - metadata.doc = doc; - } + const stats = fs.statSync(path); + if (exportMap != null) { + // date equality check + if (exportMap.mtime - stats.mtime === 0) { + return exportMap; } - - return true; - } catch (err) { - return false; + // future: check content equality? } - }); - - return metadata; -} - -const availableDocStyleParsers = { - jsdoc: captureJsDoc, - tomdoc: captureTomDoc, -}; - -/** - * parse JSDoc from leading comments - * @param {object[]} comments - * @return {{ doc: object }} - */ -function captureJsDoc(comments) { - let doc; - // capture XSDoc - comments.forEach((comment) => { - // skip non-block comments - if (comment.type !== 'Block') { return; } - try { - doc = doctrine.parse(comment.value, { unwrap: true }); - } catch (err) { - /* don't care, for now? maybe add to `errors?` */ + // check valid extensions first + if (!hasValidExtension(path, context)) { + exportCache.set(cacheKey, null); + return null; } - }); - - return doc; -} - -/** - * parse TomDoc section from comments - */ -function captureTomDoc(comments) { - // collect lines up to first paragraph break - const lines = []; - for (let i = 0; i < comments.length; i++) { - const comment = comments[i]; - if (comment.value.match(/^\s*$/)) { break; } - lines.push(comment.value.trim()); - } - - // return doctrine-like object - const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/); - if (statusMatch) { - return { - description: statusMatch[2], - tags: [{ - title: statusMatch[1].toLowerCase(), - description: statusMatch[2], - }], - }; - } -} - -const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']); - -ExportMap.get = function (source, context) { - const path = resolve(source, context); - if (path == null) { return null; } - - return ExportMap.for(childContext(path, context)); -}; - -ExportMap.for = function (context) { - const { path } = context; - - const cacheKey = context.cacheKey || hashObject(context).digest('hex'); - let exportMap = exportCache.get(cacheKey); - // return cached ignore - if (exportMap === null) { return null; } - - const stats = fs.statSync(path); - if (exportMap != null) { - // date equality check - if (exportMap.mtime - stats.mtime === 0) { - return exportMap; + // check for and cache ignore + if (isIgnored(path, context)) { + log('ignored path due to ignore settings:', path); + exportCache.set(cacheKey, null); + return null; } - // future: check content equality? - } - - // check valid extensions first - if (!hasValidExtension(path, context)) { - exportCache.set(cacheKey, null); - return null; - } - // check for and cache ignore - if (isIgnored(path, context)) { - log('ignored path due to ignore settings:', path); - exportCache.set(cacheKey, null); - return null; - } - - const content = fs.readFileSync(path, { encoding: 'utf8' }); - - // check for and cache unambiguous modules - if (!unambiguous.test(content)) { - log('ignored path due to unambiguous regex:', path); - exportCache.set(cacheKey, null); - return null; - } + const content = fs.readFileSync(path, { encoding: 'utf8' }); - log('cache miss', cacheKey, 'for path', path); - exportMap = ExportMap.parse(path, content, context); + // check for and cache unambiguous modules + if (!unambiguous.test(content)) { + log('ignored path due to unambiguous regex:', path); + exportCache.set(cacheKey, null); + return null; + } - // ambiguous modules return null - if (exportMap == null) { - log('ignored path due to ambiguous parse:', path); - exportCache.set(cacheKey, null); - return null; - } + log('cache miss', cacheKey, 'for path', path); + exportMap = ExportMap.parse(path, content, context); - exportMap.mtime = stats.mtime; + // ambiguous modules return null + if (exportMap == null) { + log('ignored path due to ambiguous parse:', path); + exportCache.set(cacheKey, null); + return null; + } - exportCache.set(cacheKey, exportMap); - return exportMap; -}; + exportMap.mtime = stats.mtime; -ExportMap.parse = function (path, content, context) { - const m = new ExportMap(path); - const isEsModuleInteropTrue = isEsModuleInterop(); - - let ast; - let visitorKeys; - try { - const result = parse(path, content, context); - ast = result.ast; - visitorKeys = result.visitorKeys; - } catch (err) { - m.errors.push(err); - return m; // can't continue + exportCache.set(cacheKey, exportMap); + return exportMap; } - m.visitorKeys = visitorKeys; - - let hasDynamicImports = false; + static parse(path, content, context) { + const m = new ExportMap(path); + const isEsModuleInteropTrue = isEsModuleInterop(); - function processDynamicImport(source) { - hasDynamicImports = true; - if (source.type !== 'Literal') { - return null; - } - const p = remotePath(source.value); - if (p == null) { - return null; + let ast; + let visitorKeys; + try { + const result = parse(path, content, context); + ast = result.ast; + visitorKeys = result.visitorKeys; + } catch (err) { + m.errors.push(err); + return m; // can't continue } - const importedSpecifiers = new Set(); - importedSpecifiers.add('ImportNamespaceSpecifier'); - const getter = thunkFor(p, context); - m.imports.set(p, { - getter, - declarations: new Set([{ - source: { - // capturing actual node reference holds full AST in memory! - value: source.value, - loc: source.loc, - }, - importedSpecifiers, - dynamic: true, - }]), - }); - } - visit(ast, visitorKeys, { - ImportExpression(node) { - processDynamicImport(node.source); - }, - CallExpression(node) { - if (node.callee.type === 'Import') { - processDynamicImport(node.arguments[0]); - } - }, - }); + m.visitorKeys = visitorKeys; - const unambiguouslyESM = unambiguous.isModule(ast); - if (!unambiguouslyESM && !hasDynamicImports) { return null; } + let hasDynamicImports = false; - const docstyle = context.settings && context.settings['import/docstyle'] || ['jsdoc']; - const docStyleParsers = {}; - docstyle.forEach((style) => { - docStyleParsers[style] = availableDocStyleParsers[style]; - }); + function processDynamicImport(source) { + hasDynamicImports = true; + if (source.type !== 'Literal') { + return null; + } + const p = remotePath(source.value); + if (p == null) { + return null; + } + const importedSpecifiers = new Set(); + importedSpecifiers.add('ImportNamespaceSpecifier'); + const getter = thunkFor(p, context); + m.imports.set(p, { + getter, + declarations: new Set([{ + source: { + // capturing actual node reference holds full AST in memory! + value: source.value, + loc: source.loc, + }, + importedSpecifiers, + dynamic: true, + }]), + }); + } - // attempt to collect module doc - if (ast.comments) { - ast.comments.some((c) => { - if (c.type !== 'Block') { return false; } - try { - const doc = doctrine.parse(c.value, { unwrap: true }); - if (doc.tags.some((t) => t.title === 'module')) { - m.doc = doc; - return true; + visit(ast, visitorKeys, { + ImportExpression(node) { + processDynamicImport(node.source); + }, + CallExpression(node) { + if (node.callee.type === 'Import') { + processDynamicImport(node.arguments[0]); } - } catch (err) { /* ignore */ } - return false; + }, }); - } - const namespaces = new Map(); + const unambiguouslyESM = unambiguous.isModule(ast); + if (!unambiguouslyESM && !hasDynamicImports) { return null; } - function remotePath(value) { - return resolve.relative(value, path, context.settings); - } + const docstyle = context.settings && context.settings['import/docstyle'] || ['jsdoc']; + const docStyleParsers = {}; + docstyle.forEach((style) => { + docStyleParsers[style] = availableDocStyleParsers[style]; + }); - function resolveImport(value) { - const rp = remotePath(value); - if (rp == null) { return null; } - return ExportMap.for(childContext(rp, context)); - } + // attempt to collect module doc + if (ast.comments) { + ast.comments.some((c) => { + if (c.type !== 'Block') { return false; } + try { + const doc = doctrine.parse(c.value, { unwrap: true }); + if (doc.tags.some((t) => t.title === 'module')) { + m.doc = doc; + return true; + } + } catch (err) { /* ignore */ } + return false; + }); + } - function getNamespace(identifier) { - if (!namespaces.has(identifier.name)) { return; } + const namespaces = new Map(); - return function () { - return resolveImport(namespaces.get(identifier.name)); - }; - } + function remotePath(value) { + return resolve.relative(value, path, context.settings); + } - function addNamespace(object, identifier) { - const nsfn = getNamespace(identifier); - if (nsfn) { - Object.defineProperty(object, 'namespace', { get: nsfn }); + function resolveImport(value) { + const rp = remotePath(value); + if (rp == null) { return null; } + return ExportMap.for(childContext(rp, context)); } - return object; - } + function getNamespace(identifier) { + if (!namespaces.has(identifier.name)) { return; } - function processSpecifier(s, n, m) { - const nsource = n.source && n.source.value; - const exportMeta = {}; - let local; - - switch (s.type) { - case 'ExportDefaultSpecifier': - if (!nsource) { return; } - local = 'default'; - break; - case 'ExportNamespaceSpecifier': - m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', { - get() { return resolveImport(nsource); }, - })); - return; - case 'ExportAllDeclaration': - m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.source.value)); - return; - case 'ExportSpecifier': - if (!n.source) { - m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.local)); - return; - } - // else falls through - default: - local = s.local.name; - break; + return function () { + return resolveImport(namespaces.get(identifier.name)); + }; } - // todo: JSDoc - m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(nsource) }); - } - - function captureDependencyWithSpecifiers(n) { - // import type { Foo } (TS and Flow); import typeof { Foo } (Flow) - const declarationIsType = n.importKind === 'type' || n.importKind === 'typeof'; - // import './foo' or import {} from './foo' (both 0 specifiers) is a side effect and - // shouldn't be considered to be just importing types - let specifiersOnlyImportingTypes = n.specifiers.length > 0; - const importedSpecifiers = new Set(); - n.specifiers.forEach((specifier) => { - if (specifier.type === 'ImportSpecifier') { - importedSpecifiers.add(specifier.imported.name || specifier.imported.value); - } else if (supportedImportTypes.has(specifier.type)) { - importedSpecifiers.add(specifier.type); + function addNamespace(object, identifier) { + const nsfn = getNamespace(identifier); + if (nsfn) { + Object.defineProperty(object, 'namespace', { get: nsfn }); } - // import { type Foo } (Flow); import { typeof Foo } (Flow) - specifiersOnlyImportingTypes = specifiersOnlyImportingTypes - && (specifier.importKind === 'type' || specifier.importKind === 'typeof'); - }); - captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, importedSpecifiers); - } + return object; + } - function captureDependency({ source }, isOnlyImportingTypes, importedSpecifiers = new Set()) { - if (source == null) { return null; } + function processSpecifier(s, n, m) { + const nsource = n.source && n.source.value; + const exportMeta = {}; + let local; + + switch (s.type) { + case 'ExportDefaultSpecifier': + if (!nsource) { return; } + local = 'default'; + break; + case 'ExportNamespaceSpecifier': + m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', { + get() { return resolveImport(nsource); }, + })); + return; + case 'ExportAllDeclaration': + m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.source.value)); + return; + case 'ExportSpecifier': + if (!n.source) { + m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.local)); + return; + } + // else falls through + default: + local = s.local.name; + break; + } - const p = remotePath(source.value); - if (p == null) { return null; } + // todo: JSDoc + m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(nsource) }); + } - const declarationMetadata = { - // capturing actual node reference holds full AST in memory! - source: { value: source.value, loc: source.loc }, - isOnlyImportingTypes, - importedSpecifiers, - }; + function captureDependencyWithSpecifiers(n) { + // import type { Foo } (TS and Flow); import typeof { Foo } (Flow) + const declarationIsType = n.importKind === 'type' || n.importKind === 'typeof'; + // import './foo' or import {} from './foo' (both 0 specifiers) is a side effect and + // shouldn't be considered to be just importing types + let specifiersOnlyImportingTypes = n.specifiers.length > 0; + const importedSpecifiers = new Set(); + n.specifiers.forEach((specifier) => { + if (specifier.type === 'ImportSpecifier') { + importedSpecifiers.add(specifier.imported.name || specifier.imported.value); + } else if (supportedImportTypes.has(specifier.type)) { + importedSpecifiers.add(specifier.type); + } - const existing = m.imports.get(p); - if (existing != null) { - existing.declarations.add(declarationMetadata); - return existing.getter; + // import { type Foo } (Flow); import { typeof Foo } (Flow) + specifiersOnlyImportingTypes = specifiersOnlyImportingTypes + && (specifier.importKind === 'type' || specifier.importKind === 'typeof'); + }); + captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, importedSpecifiers); } - const getter = thunkFor(p, context); - m.imports.set(p, { getter, declarations: new Set([declarationMetadata]) }); - return getter; - } + function captureDependency({ source }, isOnlyImportingTypes, importedSpecifiers = new Set()) { + if (source == null) { return null; } - const source = makeSourceCode(content, ast); + const p = remotePath(source.value); + if (p == null) { return null; } - function readTsConfig(context) { - const tsconfigInfo = tsConfigLoader({ - cwd: context.parserOptions && context.parserOptions.tsconfigRootDir || process.cwd(), - getEnv: (key) => process.env[key], - }); - try { - if (tsconfigInfo.tsConfigPath !== undefined) { - // Projects not using TypeScript won't have `typescript` installed. - if (!ts) { ts = require('typescript'); } // eslint-disable-line import/no-extraneous-dependencies - - const configFile = ts.readConfigFile(tsconfigInfo.tsConfigPath, ts.sys.readFile); - return ts.parseJsonConfigFileContent( - configFile.config, - ts.sys, - dirname(tsconfigInfo.tsConfigPath), - ); - } - } catch (e) { - // Catch any errors - } + const declarationMetadata = { + // capturing actual node reference holds full AST in memory! + source: { value: source.value, loc: source.loc }, + isOnlyImportingTypes, + importedSpecifiers, + }; - return null; - } + const existing = m.imports.get(p); + if (existing != null) { + existing.declarations.add(declarationMetadata); + return existing.getter; + } - function isEsModuleInterop() { - const cacheKey = hashObject({ - tsconfigRootDir: context.parserOptions && context.parserOptions.tsconfigRootDir, - }).digest('hex'); - let tsConfig = tsconfigCache.get(cacheKey); - if (typeof tsConfig === 'undefined') { - tsConfig = readTsConfig(context); - tsconfigCache.set(cacheKey, tsConfig); + const getter = thunkFor(p, context); + m.imports.set(p, { getter, declarations: new Set([declarationMetadata]) }); + return getter; } - return tsConfig && tsConfig.options ? tsConfig.options.esModuleInterop : false; - } + const source = makeSourceCode(content, ast); - ast.body.forEach(function (n) { - if (n.type === 'ExportDefaultDeclaration') { - const exportMeta = captureDoc(source, docStyleParsers, n); - if (n.declaration.type === 'Identifier') { - addNamespace(exportMeta, n.declaration); + function readTsConfig(context) { + const tsconfigInfo = tsConfigLoader({ + cwd: context.parserOptions && context.parserOptions.tsconfigRootDir || process.cwd(), + getEnv: (key) => process.env[key], + }); + try { + if (tsconfigInfo.tsConfigPath !== undefined) { + // Projects not using TypeScript won't have `typescript` installed. + if (!ts) { ts = require('typescript'); } // eslint-disable-line import/no-extraneous-dependencies + + const configFile = ts.readConfigFile(tsconfigInfo.tsConfigPath, ts.sys.readFile); + return ts.parseJsonConfigFileContent( + configFile.config, + ts.sys, + dirname(tsconfigInfo.tsConfigPath), + ); + } + } catch (e) { + // Catch any errors } - m.namespace.set('default', exportMeta); - return; - } - if (n.type === 'ExportAllDeclaration') { - const getter = captureDependency(n, n.exportKind === 'type'); - if (getter) { m.dependencies.add(getter); } - if (n.exported) { - processSpecifier(n, n.exported, m); - } - return; + return null; } - // capture namespaces in case of later export - if (n.type === 'ImportDeclaration') { - captureDependencyWithSpecifiers(n); - - const ns = n.specifiers.find((s) => s.type === 'ImportNamespaceSpecifier'); - if (ns) { - namespaces.set(ns.local.name, n.source.value); + function isEsModuleInterop() { + const cacheKey = hashObject({ + tsconfigRootDir: context.parserOptions && context.parserOptions.tsconfigRootDir, + }).digest('hex'); + let tsConfig = tsconfigCache.get(cacheKey); + if (typeof tsConfig === 'undefined') { + tsConfig = readTsConfig(context); + tsconfigCache.set(cacheKey, tsConfig); } - return; + + return tsConfig && tsConfig.options ? tsConfig.options.esModuleInterop : false; } - if (n.type === 'ExportNamedDeclaration') { - captureDependencyWithSpecifiers(n); - - // capture declaration - if (n.declaration != null) { - switch (n.declaration.type) { - case 'FunctionDeclaration': - case 'ClassDeclaration': - case 'TypeAlias': // flowtype with babel-eslint parser - case 'InterfaceDeclaration': - case 'DeclareFunction': - case 'TSDeclareFunction': - case 'TSEnumDeclaration': - case 'TSTypeAliasDeclaration': - case 'TSInterfaceDeclaration': - case 'TSAbstractClassDeclaration': - case 'TSModuleDeclaration': - m.namespace.set(n.declaration.id.name, captureDoc(source, docStyleParsers, n)); - break; - case 'VariableDeclaration': - n.declaration.declarations.forEach((d) => { - recursivePatternCapture( - d.id, - (id) => m.namespace.set(id.name, captureDoc(source, docStyleParsers, d, n)), - ); - }); - break; - default: + ast.body.forEach(function (n) { + if (n.type === 'ExportDefaultDeclaration') { + const exportMeta = captureDoc(source, docStyleParsers, n); + if (n.declaration.type === 'Identifier') { + addNamespace(exportMeta, n.declaration); } + m.namespace.set('default', exportMeta); + return; } - n.specifiers.forEach((s) => processSpecifier(s, n, m)); - } + if (n.type === 'ExportAllDeclaration') { + const getter = captureDependency(n, n.exportKind === 'type'); + if (getter) { m.dependencies.add(getter); } + if (n.exported) { + processSpecifier(n, n.exported, m); + } + return; + } - const exports = ['TSExportAssignment']; - if (isEsModuleInteropTrue) { - exports.push('TSNamespaceExportDeclaration'); - } + // capture namespaces in case of later export + if (n.type === 'ImportDeclaration') { + captureDependencyWithSpecifiers(n); - // This doesn't declare anything, but changes what's being exported. - if (includes(exports, n.type)) { - const exportedName = n.type === 'TSNamespaceExportDeclaration' - ? (n.id || n.name).name - : n.expression && n.expression.name || n.expression.id && n.expression.id.name || null; - const declTypes = [ - 'VariableDeclaration', - 'ClassDeclaration', - 'TSDeclareFunction', - 'TSEnumDeclaration', - 'TSTypeAliasDeclaration', - 'TSInterfaceDeclaration', - 'TSAbstractClassDeclaration', - 'TSModuleDeclaration', - ]; - const exportedDecls = ast.body.filter(({ type, id, declarations }) => includes(declTypes, type) && ( - id && id.name === exportedName || declarations && declarations.find((d) => d.id.name === exportedName) - )); - if (exportedDecls.length === 0) { - // Export is not referencing any local declaration, must be re-exporting - m.namespace.set('default', captureDoc(source, docStyleParsers, n)); + const ns = n.specifiers.find((s) => s.type === 'ImportNamespaceSpecifier'); + if (ns) { + namespaces.set(ns.local.name, n.source.value); + } return; } - if ( - isEsModuleInteropTrue // esModuleInterop is on in tsconfig - && !m.namespace.has('default') // and default isn't added already - ) { - m.namespace.set('default', {}); // add default export - } - exportedDecls.forEach((decl) => { - if (decl.type === 'TSModuleDeclaration') { - if (decl.body && decl.body.type === 'TSModuleDeclaration') { - m.namespace.set(decl.body.id.name, captureDoc(source, docStyleParsers, decl.body)); - } else if (decl.body && decl.body.body) { - decl.body.body.forEach((moduleBlockNode) => { - // Export-assignment exports all members in the namespace, - // explicitly exported or not. - const namespaceDecl = moduleBlockNode.type === 'ExportNamedDeclaration' - ? moduleBlockNode.declaration - : moduleBlockNode; - - if (!namespaceDecl) { - // TypeScript can check this for us; we needn't - } else if (namespaceDecl.type === 'VariableDeclaration') { - namespaceDecl.declarations.forEach((d) => recursivePatternCapture(d.id, (id) => m.namespace.set( - id.name, - captureDoc(source, docStyleParsers, decl, namespaceDecl, moduleBlockNode), - )), + + if (n.type === 'ExportNamedDeclaration') { + captureDependencyWithSpecifiers(n); + + // capture declaration + if (n.declaration != null) { + switch (n.declaration.type) { + case 'FunctionDeclaration': + case 'ClassDeclaration': + case 'TypeAlias': // flowtype with babel-eslint parser + case 'InterfaceDeclaration': + case 'DeclareFunction': + case 'TSDeclareFunction': + case 'TSEnumDeclaration': + case 'TSTypeAliasDeclaration': + case 'TSInterfaceDeclaration': + case 'TSAbstractClassDeclaration': + case 'TSModuleDeclaration': + m.namespace.set(n.declaration.id.name, captureDoc(source, docStyleParsers, n)); + break; + case 'VariableDeclaration': + n.declaration.declarations.forEach((d) => { + recursivePatternCapture( + d.id, + (id) => m.namespace.set(id.name, captureDoc(source, docStyleParsers, d, n)), ); - } else { - m.namespace.set( - namespaceDecl.id.name, - captureDoc(source, docStyleParsers, moduleBlockNode)); - } - }); + }); + break; + default: } - } else { - // Export as default - m.namespace.set('default', captureDoc(source, docStyleParsers, decl)); } - }); - } - }); - if ( - isEsModuleInteropTrue // esModuleInterop is on in tsconfig - && m.namespace.size > 0 // anything is exported - && !m.namespace.has('default') // and default isn't added already - ) { - m.namespace.set('default', {}); // add default export - } + n.specifiers.forEach((s) => processSpecifier(s, n, m)); + } + + const exports = ['TSExportAssignment']; + if (isEsModuleInteropTrue) { + exports.push('TSNamespaceExportDeclaration'); + } + + // This doesn't declare anything, but changes what's being exported. + if (includes(exports, n.type)) { + const exportedName = n.type === 'TSNamespaceExportDeclaration' + ? (n.id || n.name).name + : n.expression && n.expression.name || n.expression.id && n.expression.id.name || null; + const declTypes = [ + 'VariableDeclaration', + 'ClassDeclaration', + 'TSDeclareFunction', + 'TSEnumDeclaration', + 'TSTypeAliasDeclaration', + 'TSInterfaceDeclaration', + 'TSAbstractClassDeclaration', + 'TSModuleDeclaration', + ]; + const exportedDecls = ast.body.filter(({ type, id, declarations }) => includes(declTypes, type) && ( + id && id.name === exportedName || declarations && declarations.find((d) => d.id.name === exportedName) + )); + if (exportedDecls.length === 0) { + // Export is not referencing any local declaration, must be re-exporting + m.namespace.set('default', captureDoc(source, docStyleParsers, n)); + return; + } + if ( + isEsModuleInteropTrue // esModuleInterop is on in tsconfig + && !m.namespace.has('default') // and default isn't added already + ) { + m.namespace.set('default', {}); // add default export + } + exportedDecls.forEach((decl) => { + if (decl.type === 'TSModuleDeclaration') { + if (decl.body && decl.body.type === 'TSModuleDeclaration') { + m.namespace.set(decl.body.id.name, captureDoc(source, docStyleParsers, decl.body)); + } else if (decl.body && decl.body.body) { + decl.body.body.forEach((moduleBlockNode) => { + // Export-assignment exports all members in the namespace, + // explicitly exported or not. + const namespaceDecl = moduleBlockNode.type === 'ExportNamedDeclaration' + ? moduleBlockNode.declaration + : moduleBlockNode; + + if (!namespaceDecl) { + // TypeScript can check this for us; we needn't + } else if (namespaceDecl.type === 'VariableDeclaration') { + namespaceDecl.declarations.forEach((d) => recursivePatternCapture(d.id, (id) => m.namespace.set( + id.name, + captureDoc(source, docStyleParsers, decl, namespaceDecl, moduleBlockNode), + )), + ); + } else { + m.namespace.set( + namespaceDecl.id.name, + captureDoc(source, docStyleParsers, moduleBlockNode)); + } + }); + } + } else { + // Export as default + m.namespace.set('default', captureDoc(source, docStyleParsers, decl)); + } + }); + } + }); - if (unambiguouslyESM) { - m.parseGoal = 'Module'; + if ( + isEsModuleInteropTrue // esModuleInterop is on in tsconfig + && m.namespace.size > 0 // anything is exported + && !m.namespace.has('default') // and default isn't added already + ) { + m.namespace.set('default', {}); // add default export + } + + if (unambiguouslyESM) { + m.parseGoal = 'Module'; + } + return m; } - return m; -}; +} /** * The creation of this closure is isolated from other scopes