UNPKG

eslint-plugin-import-x

Version:
788 lines 29.6 kB
import fs from 'node:fs'; import path from 'node:path'; import debug from 'debug'; import { SourceCode } from 'eslint'; import { getTsconfig } from 'get-tsconfig'; import { stableHash } from 'stable-hash'; import { cjsRequire } from '../require.js'; import { getValue } from './get-value.js'; import { hasValidExtension, ignore } from './ignore.js'; import { lazy, defineLazyProperty } from './lazy-value.js'; import { parse } from './parse.js'; import { relative, resolve } from './resolve.js'; import { isMaybeUnambiguousModule, isUnambiguousModule } from './unambiguous.js'; import { visit } from './visit.js'; const log = debug('eslint-plugin-import-x:ExportMap'); const exportCache = new Map(); const tsconfigCache = new Map(); const declTypes = new Set([ 'VariableDeclaration', 'ClassDeclaration', 'TSDeclareFunction', 'TSEnumDeclaration', 'TSTypeAliasDeclaration', 'TSInterfaceDeclaration', 'TSAbstractClassDeclaration', 'TSModuleDeclaration', ]); const fixup = new Set(['deprecated', 'module']); let parseComment_; const parseComment = (comment) => { parseComment_ ??= cjsRequire('comment-parser').parse; const restored = `/**${comment.split(/\r?\n/).reduce((acc, line) => { line = line.trim(); return line && line !== '*' ? acc + '\n ' + line : acc; }, '')} */`; const [doc] = parseComment_(restored); return { ...doc, tags: doc.tags.map(t => t.name && fixup.has(t.tag) ? { ...t, description: `${t.name} ${t.description}` } : t), }; }; export class ExportMap { static for(context) { const filepath = context.path; const cacheKey = context.cacheKey; let exportMap = exportCache.get(cacheKey); const stats = lazy(() => fs.statSync(filepath)); if (exportCache.has(cacheKey)) { const exportMap = exportCache.get(cacheKey); if (exportMap === null) { return null; } if (exportMap != null && exportMap.mtime - stats().mtime.valueOf() === 0) { return exportMap; } } if (!hasValidExtension(filepath, context)) { exportCache.set(cacheKey, null); return null; } if (ignore(filepath, context, true)) { log('ignored path due to ignore settings:', filepath); exportCache.set(cacheKey, null); return null; } const content = fs.readFileSync(filepath, { encoding: 'utf8' }); if (!isMaybeUnambiguousModule(content)) { log('ignored path due to unambiguous regex:', filepath); exportCache.set(cacheKey, null); return null; } log('cache miss', cacheKey, 'for path', filepath); exportMap = ExportMap.parse(filepath, content, context); if (exportMap === null) { log('ignored path due to ambiguous parse:', filepath); exportCache.set(cacheKey, null); return null; } exportMap.mtime = stats().mtime.valueOf(); if (exportMap.visitorKeys) { exportCache.set(cacheKey, exportMap); } return exportMap; } static get(source, context) { const path = resolve(source, context); if (path == null) { return null; } return ExportMap.for(childContext(path, context)); } static parse(filepath, content, context) { const m = new ExportMap(filepath); const isEsModuleInteropTrue = lazy(isEsModuleInterop); let ast; let visitorKeys; try { ; ({ ast, visitorKeys } = parse(filepath, content, context)); } catch (error) { m.errors.push(error); return m; } m.visitorKeys = visitorKeys; let hasDynamicImports = false; function processDynamicImport(source) { hasDynamicImports = true; if (source.type !== 'Literal') { return null; } const p = remotePath(source.value); if (p == null) { return null; } const getter = thunkFor(p, context); m.imports.set(p, { getter, declarations: new Set([ { source: { value: source.value, loc: source.loc, }, importedSpecifiers: new Set(['ImportNamespaceSpecifier']), dynamic: true, }, ]), }); } visit(ast, visitorKeys, { ImportExpression(node) { processDynamicImport(node.source); }, CallExpression(_node) { const node = _node; if (node.callee.type === 'Import') { processDynamicImport(node.arguments[0]); } }, }); const unambiguouslyESM = lazy(() => isUnambiguousModule(ast)); if (!hasDynamicImports && !unambiguouslyESM()) { return null; } const docStyles = (context.settings && context.settings['import-x/docstyle']) || ['jsdoc']; const docStyleParsers = {}; for (const style of docStyles) { docStyleParsers[style] = availableDocStyleParsers[style]; } const namespaces = new Map(); function remotePath(value) { return relative(value, filepath, context.settings); } function resolveImport(value) { const rp = remotePath(value); if (rp == null) { return null; } return ExportMap.for(childContext(rp, context)); } function getNamespace(namespace) { if (!namespaces.has(namespace)) { return; } return function () { return resolveImport(namespaces.get(namespace)); }; } function addNamespace(object, identifier) { const nsfn = getNamespace(getValue(identifier)); if (nsfn) { Object.defineProperty(object, 'namespace', { get: nsfn }); } return object; } function processSpecifier(s, n, m) { const nsource = ('source' in n && n.source && n.source.value); const exportMeta = {}; let local; switch (s.type) { case 'ExportDefaultSpecifier': { if (!nsource) { return; } local = 'default'; break; } case 'ExportNamespaceSpecifier': { m.exports.set(s.exported.name, n); m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', { get() { return resolveImport(nsource); }, })); return; } case 'ExportAllDeclaration': { m.exports.set(getValue(s.exported), n); m.namespace.set(getValue(s.exported), addNamespace(exportMeta, s.exported)); return; } case 'ExportSpecifier': { if (!('source' in n && n.source)) { m.exports.set(getValue(s.exported), n); m.namespace.set(getValue(s.exported), addNamespace(exportMeta, s.local)); return; } } default: { if ('local' in s) { local = getValue(s.local); } else { throw new Error('Unknown export specifier type'); } break; } } if ('exported' in s) { m.reexports.set(getValue(s.exported), { local, getImport: () => resolveImport(nsource), }); } } function captureDependencyWithSpecifiers(n) { const declarationIsType = 'importKind' in n && (n.importKind === 'type' || n.importKind === 'typeof'); let specifiersOnlyImportingTypes = n.specifiers.length > 0; const importedSpecifiers = new Set(); for (const specifier of n.specifiers) { if (specifier.type === 'ImportSpecifier') { importedSpecifiers.add(getValue(specifier.imported)); } else if (supportedImportTypes.has(specifier.type)) { importedSpecifiers.add(specifier.type); } specifiersOnlyImportingTypes = specifiersOnlyImportingTypes && 'importKind' in specifier && (specifier.importKind === 'type' || specifier.importKind === 'typeof'); } captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, importedSpecifiers); } function captureDependency({ source, }, isOnlyImportingTypes, importedSpecifiers = new Set()) { if (source == null) { return null; } const p = remotePath(source.value); if (p == null) { return null; } const declarationMetadata = { source: { value: source.value, loc: source.loc, }, isOnlyImportingTypes, importedSpecifiers, }; const existing = m.imports.get(p); if (existing != null) { existing.declarations.add(declarationMetadata); return existing.getter; } const getter = thunkFor(p, context); m.imports.set(p, { getter, declarations: new Set([declarationMetadata]) }); return getter; } const source = new SourceCode({ text: content, ast: ast }); function isEsModuleInterop() { const parserOptions = context.parserOptions || {}; let tsconfigRootDir = parserOptions.tsconfigRootDir; const project = parserOptions.project; const cacheKey = stableHash({ tsconfigRootDir, project }); let tsConfig; if (tsconfigCache.has(cacheKey)) { tsConfig = tsconfigCache.get(cacheKey); } else { tsconfigRootDir = tsconfigRootDir || process.cwd(); let tsconfigResult; if (project) { const projects = Array.isArray(project) ? project : [project]; for (const project of projects) { tsconfigResult = getTsconfig(project === true ? context.filename : path.resolve(tsconfigRootDir, project)); if (tsconfigResult) { break; } } } else { tsconfigResult = getTsconfig(tsconfigRootDir); } tsConfig = tsconfigResult?.config; tsconfigCache.set(cacheKey, tsConfig); } return tsConfig?.compilerOptions?.esModuleInterop ?? false; } for (const n of ast.body) { if (n.type === 'ExportDefaultDeclaration') { const exportMeta = captureDoc(source, docStyleParsers, n); if (n.declaration.type === 'Identifier') { addNamespace(exportMeta, n.declaration); } m.exports.set('default', n); m.namespace.set('default', exportMeta); continue; } if (n.type === 'ExportAllDeclaration') { if (n.exported) { namespaces.set(n.exported.name, n.source.value); processSpecifier(n, n.exported, m); } else { const getter = captureDependency(n, n.exportKind === 'type'); if (getter) { m.dependencies.add(getter); } } continue; } 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); } continue; } if (n.type === 'ExportNamedDeclaration') { captureDependencyWithSpecifiers(n); if (n.declaration != null) { switch (n.declaration.type) { case 'FunctionDeclaration': case 'ClassDeclaration': case 'TypeAlias': case 'InterfaceDeclaration': case 'DeclareFunction': case 'TSDeclareFunction': case 'TSEnumDeclaration': case 'TSTypeAliasDeclaration': case 'TSInterfaceDeclaration': case 'TSAbstractClassDeclaration': case 'TSModuleDeclaration': { m.exports.set(n.declaration.id.name, n); m.namespace.set(n.declaration.id.name, captureDoc(source, docStyleParsers, n)); break; } case 'VariableDeclaration': { for (const d of n.declaration.declarations) { recursivePatternCapture(d.id, id => { m.exports.set(id.name, n); m.namespace.set(id.name, captureDoc(source, docStyleParsers, d, n)); }); } break; } default: } } for (const s of n.specifiers) { processSpecifier(s, n, m); } } const exports = ['TSExportAssignment']; if (isEsModuleInteropTrue()) { exports.push('TSNamespaceExportDeclaration'); } if (exports.includes(n.type)) { const exportedName = n.type === 'TSNamespaceExportDeclaration' ? (n.id || n.name).name : ('expression' in n && n.expression && (('name' in n.expression && n.expression.name) || ('id' in n.expression && n.expression.id && n.expression.id.name))) || null; const getRoot = (node) => { if (node.left.type === 'TSQualifiedName') { return getRoot(node.left); } return node.left; }; const exportedDecls = ast.body.filter(node => { return (declTypes.has(node.type) && (('id' in node && node.id && ('name' in node.id ? node.id.name === exportedName : 'left' in node.id && getRoot(node.id).name === exportedName)) || ('declarations' in node && node.declarations.find(d => 'name' in d.id && d.id.name === exportedName)))); }); if (exportedDecls.length === 0) { m.exports.set('default', n); m.namespace.set('default', captureDoc(source, docStyleParsers, n)); continue; } if (isEsModuleInteropTrue() && !m.namespace.has('default')) { m.exports.set('default', n); m.namespace.set('default', {}); } for (const decl of exportedDecls) { if (decl.type === 'TSModuleDeclaration') { const type = decl.body?.type; if (type === 'TSModuleDeclaration') { m.exports.set(decl.body.id.name, n); m.namespace.set(decl.body.id.name, captureDoc(source, docStyleParsers, decl.body)); continue; } else if (type === 'TSModuleBlock' && decl.kind === 'namespace') { const metadata = captureDoc(source, docStyleParsers, decl.body); if ('name' in decl.id) { m.namespace.set(decl.id.name, metadata); } else { m.namespace.set(decl.id.right.name, metadata); } } if (decl.body?.body) { for (const moduleBlockNode of decl.body.body) { const namespaceDecl = moduleBlockNode.type === 'ExportNamedDeclaration' ? moduleBlockNode.declaration : moduleBlockNode; if (!namespaceDecl) { } else if (namespaceDecl.type === 'VariableDeclaration') { for (const d of namespaceDecl.declarations) recursivePatternCapture(d.id, id => { m.exports.set(id.name, n); m.namespace.set(id.name, captureDoc(source, docStyleParsers, decl, namespaceDecl, moduleBlockNode)); }); } else if ('id' in namespaceDecl) { m.exports.set(namespaceDecl.id.name, n); m.namespace.set(namespaceDecl.id.name, captureDoc(source, docStyleParsers, moduleBlockNode)); } } } } else { m.exports.set('default', n); m.namespace.set('default', captureDoc(source, docStyleParsers, decl)); } } } } defineLazyProperty(m, 'doc', () => { if (!ast.comments?.length) { return; } for (const c of ast.comments) { if (c.type !== 'Block') { continue; } try { const doc = parseComment(c.value); if (doc.tags.some(t => t.tag === 'module')) { return doc; } } catch { } } }); if (isEsModuleInteropTrue() && m.namespace.size > 0 && !m.namespace.has('default')) { m.exports.set('default', ast.body[0]); m.namespace.set('default', {}); } const prevParseGoal = m.parseGoal; defineLazyProperty(m, 'parseGoal', () => { if (prevParseGoal !== 'Module' && unambiguouslyESM()) { return 'Module'; } return prevParseGoal; }); return m; } constructor(path) { this.path = path; this.namespace = new Map(); this.reexports = new Map(); this.dependencies = new Set(); this.imports = new Map(); this.exports = new Map(); this.errors = []; this.parseGoal = 'ambiguous'; } get hasDefault() { return this.get('default') != null; } get size() { let size = this.namespace.size + this.reexports.size; for (const dep of this.dependencies) { const d = dep(); if (d == null) { continue; } size += d.size; } return size; } has(name) { if (this.namespace.has(name)) { return true; } if (this.reexports.has(name)) { return true; } if (name !== 'default') { for (const dep of this.dependencies) { const innerMap = dep(); if (!innerMap) { continue; } if (innerMap.has(name)) { return true; } } } return false; } hasDeep(name) { if (this.namespace.has(name)) { return { found: true, path: [this] }; } if (this.reexports.has(name)) { const reexports = this.reexports.get(name); const imported = reexports.getImport(); if (imported == null) { return { found: true, path: [this] }; } if (imported.path === this.path && reexports.local === name) { return { found: false, path: [this] }; } const deep = imported.hasDeep(reexports.local); deep.path.unshift(this); return deep; } if (name !== 'default') { for (const dep of this.dependencies) { const innerMap = dep(); if (innerMap == null) { return { found: true, path: [this] }; } if (!innerMap) { continue; } if (innerMap.path === this.path) { continue; } const innerValue = innerMap.hasDeep(name); if (innerValue.found) { innerValue.path.unshift(this); return innerValue; } } } return { found: false, path: [this] }; } get(name) { if (this.namespace.has(name)) { return this.namespace.get(name); } if (this.reexports.has(name)) { const reexports = this.reexports.get(name); const imported = reexports.getImport(); if (imported == null) { return null; } if (imported.path === this.path && reexports.local === name) { return undefined; } return imported.get(reexports.local); } if (name !== 'default') { for (const dep of this.dependencies) { const innerMap = dep(); if (!innerMap) { continue; } if (innerMap.path === this.path) { continue; } const innerValue = innerMap.get(name); if (innerValue !== undefined) { return innerValue; } } } } $forEach(callback, thisArg) { for (const [n, v] of this.namespace.entries()) { callback.call(thisArg, v, n, this); } for (const [name, reexports] of this.reexports.entries()) { const reexported = reexports.getImport(); callback.call(thisArg, reexported?.get(reexports.local), name, this); } this.dependencies.forEach(dep => { const d = dep(); if (d == null) { return; } d.$forEach((v, n) => { if (n !== 'default') { callback.call(thisArg, v, n, this); } }); }); } reportErrors(context, declaration) { if (!declaration.source) { throw new Error('declaration.source is null'); } const msg = this.errors .map(err => `${err.message} (${err.lineNumber}:${err.column})`) .join(', '); context.report({ node: declaration.source, message: `Parse errors in imported module '${declaration.source.value}': ${msg}`, }); } } function captureDoc(source, docStyleParsers, ...nodes) { const metadata = {}; defineLazyProperty(metadata, 'doc', () => { for (let i = 0, len = nodes.length; i < len; i++) { const n = nodes[i]; if (!n) { continue; } try { let leadingComments; if ('leadingComments' in n && Array.isArray(n.leadingComments)) { leadingComments = n.leadingComments; } else if (n.range) { leadingComments = source.getCommentsBefore(n); } if (!leadingComments || leadingComments.length === 0) { continue; } for (const parser of Object.values(docStyleParsers)) { const doc = parser(leadingComments); if (doc) { return doc; } } return; } catch { continue; } } }); return metadata; } const availableDocStyleParsers = { jsdoc: captureJsDoc, tomdoc: captureTomDoc, }; function captureJsDoc(comments) { for (let i = comments.length - 1; i >= 0; i--) { const comment = comments[i]; if (comment.type !== 'Block') { continue; } try { return parseComment(comment.value); } catch { } } } function captureTomDoc(comments) { const lines = []; for (const comment of comments) { if (/^\s*$/.test(comment.value)) { break; } lines.push(comment.value.trim()); } const statusMatch = lines .join(' ') .match(/^(Public|Internal|Deprecated):\s*(.+)/); if (statusMatch) { return { description: statusMatch[2], tags: [ { tag: statusMatch[1].toLowerCase(), description: statusMatch[2], }, ], }; } } const supportedImportTypes = new Set([ 'ImportDefaultSpecifier', 'ImportNamespaceSpecifier', ]); function thunkFor(p, context) { return () => ExportMap.for(childContext(p, context)); } export function recursivePatternCapture(pattern, callback) { switch (pattern.type) { case 'Identifier': { callback(pattern); break; } case 'ObjectPattern': { for (const p of pattern.properties) { if (p.type === 'ExperimentalRestProperty' || p.type === 'RestElement') { callback(p.argument); continue; } recursivePatternCapture(p.value, callback); } break; } case 'ArrayPattern': { for (const element of pattern.elements) { if (element == null) { continue; } if (element.type === 'ExperimentalRestProperty' || element.type === 'RestElement') { callback(element.argument); continue; } recursivePatternCapture(element, callback); } break; } case 'AssignmentPattern': { callback(pattern.left); break; } default: } } function childContext(path, context) { const { settings, parserOptions, parserPath, languageOptions } = context; return { cacheKey: makeContextCacheKey(context) + path, settings, parserOptions, parserPath, languageOptions, path, filename: 'physicalFilename' in context ? context.physicalFilename : context.filename, }; } function makeContextCacheKey(context) { const { settings, parserPath, parserOptions, languageOptions } = context; let hash = stableHash(settings) + stableHash(languageOptions?.parserOptions ?? parserOptions); if (languageOptions) { hash += String(languageOptions.ecmaVersion) + String(languageOptions.sourceType); } hash += stableHash(parserPath ?? languageOptions?.parser?.meta ?? languageOptions?.parser); return hash; } //# sourceMappingURL=export-map.js.map