import fs from 'fs';
import { dirname } from 'path';
import doctrine from 'doctrine';
import debug from 'debug';
import { SourceCode } from 'eslint';
import parse from 'eslint-module-utils/parse';
import visit from 'eslint-module-utils/visit';
import resolve from 'eslint-module-utils/resolve';
import isIgnored, { hasValidExtension } from 'eslint-module-utils/ignore';
import { hashObject } from 'eslint-module-utils/hash';
import * as unambiguous from 'eslint-module-utils/unambiguous';
import { tsConfigLoader } from 'tsconfig-paths/lib/tsconfig-loader';
import includes from 'array-includes';
let ts;
const log = debug('eslint-plugin-import:ExportMap');
const exportCache = new Map();
const tsConfigCache = new Map();
export default class ExportMap {
constructor(path) {
this.path = path;
this.namespace = new Map();
this.reexports = new Map();
this.dependencies = new Set();
this.imports = new Map();
this.errors = [];
}
get hasDefault() { return this.get('default') != null; }
get size() {
let size = this.namespace.size + this.reexports.size;
this.dependencies.forEach(dep => {
const d = dep();
if (d == null) return;
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;
}
}
return undefined;
}
forEach(callback, thisArg) {
this.namespace.forEach((v, n) =>
callback.call(thisArg, v, n, this));
this.reexports.forEach((reexports, name) => {
const reexported = reexports.getImport();
callback.call(thisArg, reexported && reexported.get(reexports.local), name, this);
});
this.dependencies.forEach(dep => {
const d = dep();
if (d == null) return;
d.forEach((v, n) =>
n !== 'default' && callback.call(thisArg, v, n, this));
});
}
reportErrors(context, declaration) {
context.report({
node: declaration.source,
message: `Parse errors in imported module '${declaration.source.value}': ` +
`${this.errors
.map(e => `${e.message} (${e.lineNumber}:${e.column})`)
.join(', ')}`,
});
}
}
function captureDoc(source, docStyleParsers, ...nodes) {
const metadata = {};
nodes.some(n => {
try {
let leadingComments;
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;
}
const availableDocStyleParsers = {
jsdoc: captureJsDoc,
tomdoc: captureTomDoc,
};
function captureJsDoc(comments) {
let doc;
comments.forEach(comment => {
if (comment.type !== 'Block') return;
try {
doc = doctrine.parse(comment.value, { unwrap: true });
} catch (err) {
}
});
return doc;
}
function captureTomDoc(comments) {
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());
}
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 = hashObject(context).digest('hex');
let exportMap = exportCache.get(cacheKey);
if (exportMap === null) return null;
const stats = fs.statSync(path);
if (exportMap != null) {
if (exportMap.mtime - stats.mtime === 0) {
return exportMap;
}
}
if (!hasValidExtension(path, context)) {
exportCache.set(cacheKey, null);
return null;
}
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' });
if (!unambiguous.test(content)) {
log('ignored path due to unambiguous regex:', path);
exportCache.set(cacheKey, null);
return null;
}
log('cache miss', cacheKey, 'for path', path);
exportMap = ExportMap.parse(path, content, context);
if (exportMap == null) return null;
exportMap.mtime = stats.mtime;
exportCache.set(cacheKey, exportMap);
return exportMap;
};
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;
}
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 importedSpecifiers = new Set();
importedSpecifiers.add('ImportNamespaceSpecifier');
const getter = thunkFor(p, context);
m.imports.set(p, {
getter,
declarations: new Set([{
source: {
value: source.value,
loc: source.loc,
},
importedSpecifiers,
}]),
});
}
visit(ast, visitorKeys, {
ImportExpression(node) {
processDynamicImport(node.source);
},
CallExpression(node) {
if (node.callee.type === 'Import') {
processDynamicImport(node.arguments[0]);
}
},
});
if (!unambiguous.isModule(ast) && !hasDynamicImports) return null;
const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'];
const docStyleParsers = {};
docstyle.forEach(style => {
docStyleParsers[style] = availableDocStyleParsers[style];
});
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) { }
return false;
});
}
const namespaces = new Map();
function remotePath(value) {
return resolve.relative(value, path, context.settings);
}
function resolveImport(value) {
const rp = remotePath(value);
if (rp == null) return null;
return ExportMap.for(childContext(rp, context));
}
function getNamespace(identifier) {
if (!namespaces.has(identifier.name)) return;
return function () {
return resolveImport(namespaces.get(identifier.name));
};
}
function addNamespace(object, identifier) {
const nsfn = getNamespace(identifier);
if (nsfn) {
Object.defineProperty(object, 'namespace', { get: nsfn });
}
return object;
}
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, addNamespace(exportMeta, s.source.value));
return;
case 'ExportSpecifier':
if (!n.source) {
m.namespace.set(s.exported.name, addNamespace(exportMeta, s.local));
return;
}
default:
local = s.local.name;
break;
}
m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(nsource) });
}
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 = makeSourceCode(content, ast);
function readTsConfig() {
const tsConfigInfo = tsConfigLoader({
cwd:
(context.parserOptions && context.parserOptions.tsconfigRootDir) ||
process.cwd(),
getEnv: (key) => process.env[key],
});
try {
if (tsConfigInfo.tsConfigPath !== undefined) {
if (!ts) { ts = require('typescript'); }
const configFile = ts.readConfigFile(tsConfigInfo.tsConfigPath, ts.sys.readFile);
return ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
dirname(tsConfigInfo.tsConfigPath),
);
}
} catch (e) {
}
return null;
}
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 tsConfig && tsConfig.options ? tsConfig.options.esModuleInterop : false;
}
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;
}
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;
}
if (n.type === 'ImportDeclaration') {
const declarationIsType = n.importKind === 'type';
let specifiersOnlyImportingTypes = n.specifiers.length;
const importedSpecifiers = new Set();
n.specifiers.forEach(specifier => {
if (supportedImportTypes.has(specifier.type)) {
importedSpecifiers.add(specifier.type);
}
if (specifier.type === 'ImportSpecifier') {
importedSpecifiers.add(specifier.imported.name);
}
specifiersOnlyImportingTypes =
specifiersOnlyImportingTypes && specifier.importKind === 'type';
});
captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, importedSpecifiers);
const ns = n.specifiers.find(s => s.type === 'ImportNamespaceSpecifier');
if (ns) {
namespaces.set(ns.local.name, n.source.value);
}
return;
}
if (n.type === 'ExportNamedDeclaration') {
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.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;
}
}
n.specifiers.forEach((s) => processSpecifier(s, n, m));
}
const exports = ['TSExportAssignment'];
if (isEsModuleInteropTrue) {
exports.push('TSNamespaceExportDeclaration');
}
if (includes(exports, n.type)) {
const exportedName = n.type === 'TSNamespaceExportDeclaration'
? n.id.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) {
m.namespace.set('default', captureDoc(source, docStyleParsers, n));
return;
}
if (
isEsModuleInteropTrue
&& !m.namespace.has('default')
) {
m.namespace.set('default', {});
}
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) => {
const namespaceDecl = moduleBlockNode.type === 'ExportNamedDeclaration' ?
moduleBlockNode.declaration :
moduleBlockNode;
if (!namespaceDecl) {
} 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 {
m.namespace.set('default', captureDoc(source, docStyleParsers, decl));
}
});
}
});
if (
isEsModuleInteropTrue
&& m.namespace.size > 0
&& !m.namespace.has('default')
) {
m.namespace.set('default', {});
}
return m;
};
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':
pattern.properties.forEach(p => {
if (p.type === 'ExperimentalRestProperty' || p.type === 'RestElement') {
callback(p.argument);
return;
}
recursivePatternCapture(p.value, callback);
});
break;
case 'ArrayPattern':
pattern.elements.forEach((element) => {
if (element == null) return;
if (element.type === 'ExperimentalRestProperty' || element.type === 'RestElement') {
callback(element.argument);
return;
}
recursivePatternCapture(element, callback);
});
break;
case 'AssignmentPattern':
callback(pattern.left);
break;
}
}
function childContext(path, context) {
const { settings, parserOptions, parserPath } = context;
return {
settings,
parserOptions,
parserPath,
path,
};
}
function makeSourceCode(text, ast) {
if (SourceCode.length > 1) {
return new SourceCode(text, ast);
} else {
return new SourceCode({ text, ast });
}
}