Repository: import-js/eslint-plugin-import
Pull request #2985 (2024-03-18 19:21:17)

separate export map from its builder
`ExportMap` is currently a very large file that is hard to follow, and therefore hard to maintain.

I identified that its static methods, which are responsible for building instances of `ExportMap`, can be generalized to the [builder software design pattern](https://en.wikipedia.org/wiki/Builder_pattern).

Lets make the separation, to make it clear that instances of `ExportMap` aren't coupled to any of the globals or procedures that are currently in `ExportMap.js`.

It's also good to rename the files and classes appropriately.


Patch:
diff --git a/.eslintrc b/.eslintrc
index 224ffcb0..1bbbba01 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -229,7 +229,7 @@
         {
             "files": [
                 "utils/**", // TODO
-                "src/ExportMap.js", // TODO
+                "src/exportMapBuilder.js", // TODO
             ],
             "rules": {
                 "no-use-before-define": "off",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9552774d..7ad93fbd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 - [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])
+- [Refactor] `ExportMap`: separate ExportMap instance from its builder logic ([#2985], thanks [@soryy708])
 
 ## [2.29.1] - 2023-12-14
 
@@ -1109,6 +1110,7 @@ for info on changes for earlier releases.
 
 [`memo-parser`]: ./memo-parser/README.md
 
+[#2985]: https://github.com/import-js/eslint-plugin-import/pull/2985
 [#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
diff --git a/src/exportMap.js b/src/exportMap.js
new file mode 100644
index 00000000..e4d61638
--- /dev/null
+++ b/src/exportMap.js
@@ -0,0 +1,178 @@
+export default class ExportMap {
+  constructor(path) {
+    this.path = path;
+    this.namespace = new Map();
+    // todo: restructure to key on path, value is resolver + map of names
+    this.reexports = new Map();
+    /**
+     * star-exports
+     * @type {Set<() => ExportMap>}
+     */
+    this.dependencies = new Set();
+    /**
+     * dependencies of this module that are not explicitly re-exported
+     * @type {Map<string, () => ExportMap>}
+     */
+    this.imports = new Map();
+    this.errors = [];
+    /**
+     * type {'ambiguous' | 'Module' | 'Script'}
+     */
+    this.parseGoal = 'ambiguous';
+  }
+
+  get hasDefault() { return this.get('default') != null; } // stronger than this.has
+
+  get size() {
+    let size = this.namespace.size + this.reexports.size;
+    this.dependencies.forEach((dep) => {
+      const d = dep();
+      // CJS / ignored dependencies won't exist (#717)
+      if (d == null) { return; }
+      size += d.size;
+    });
+    return size;
+  }
+
+  /**
+   * Note that this does not check explicitly re-exported names for existence
+   * in the base namespace, but it will expand all `export * from '...'` exports
+   * if not found in the explicit namespace.
+   * @param  {string}  name
+   * @return {boolean} true if `name` is exported by this module.
+   */
+  has(name) {
+    if (this.namespace.has(name)) { return true; }
+    if (this.reexports.has(name)) { return true; }
+
+    // default exports must be explicitly re-exported (#328)
+    if (name !== 'default') {
+      for (const dep of this.dependencies) {
+        const innerMap = dep();
+
+        // todo: report as unresolved?
+        if (!innerMap) { continue; }
+
+        if (innerMap.has(name)) { return true; }
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * ensure that imported name fully resolves.
+   * @param  {string} name
+   * @return {{ found: boolean, path: ExportMap[] }}
+   */
+  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 import is ignored, return explicit 'null'
+      if (imported == null) { return { found: true, path: [this] }; }
+
+      // safeguard against cycles, only if name matches
+      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;
+    }
+
+    // default exports must be explicitly re-exported (#328)
+    if (name !== 'default') {
+      for (const dep of this.dependencies) {
+        const innerMap = dep();
+        if (innerMap == null) { return { found: true, path: [this] }; }
+        // todo: report as unresolved?
+        if (!innerMap) { continue; }
+
+        // safeguard against cycles
+        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 import is ignored, return explicit 'null'
+      if (imported == null) { return null; }
+
+      // safeguard against cycles, only if name matches
+      if (imported.path === this.path && reexports.local === name) { return undefined; }
+
+      return imported.get(reexports.local);
+    }
+
+    // default exports must be explicitly re-exported (#328)
+    if (name !== 'default') {
+      for (const dep of this.dependencies) {
+        const innerMap = dep();
+        // todo: report as unresolved?
+        if (!innerMap) { continue; }
+
+        // safeguard against cycles
+        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();
+      // can't look up meta for ignored re-exports (#348)
+      callback.call(thisArg, reexported && reexported.get(reexports.local), name, this);
+    });
+
+    this.dependencies.forEach((dep) => {
+      const d = dep();
+      // CJS / ignored dependencies won't exist (#717)
+      if (d == null) { return; }
+
+      d.forEach((v, n) => {
+        if (n !== 'default') {
+          callback.call(thisArg, v, n, this);
+        }
+      });
+    });
+  }
+
+  // todo: keys, values, entries?
+
+  reportErrors(context, declaration) {
+    const msg = this.errors
+      .map((e) => `${e.message} (${e.lineNumber}:${e.column})`)
+      .join(', ');
+    context.report({
+      node: declaration.source,
+      message: `Parse errors in imported module '${declaration.source.value}': ${msg}`,
+    });
+  }
+}
diff --git a/src/ExportMap.js b/src/exportMapBuilder.js
similarity index 78%
rename from src/ExportMap.js
rename to src/exportMapBuilder.js
index 9ee65a50..f2b40e7b 100644
--- a/src/ExportMap.js
+++ b/src/exportMapBuilder.js
@@ -18,6 +18,7 @@ import * as unambiguous from 'eslint-module-utils/unambiguous';
 import { tsConfigLoader } from 'tsconfig-paths/lib/tsconfig-loader';
 
 import includes from 'array-includes';
+import ExportMap from './exportMap';
 
 let ts;
 
@@ -117,189 +118,12 @@ const availableDocStyleParsers = {
 
 const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']);
 
-export default class ExportMap {
-  constructor(path) {
-    this.path = path;
-    this.namespace = new Map();
-    // todo: restructure to key on path, value is resolver + map of names
-    this.reexports = new Map();
-    /**
-     * star-exports
-     * @type {Set} of () => ExportMap
-     */
-    this.dependencies = new Set();
-    /**
-     * dependencies of this module that are not explicitly re-exported
-     * @type {Map} from path = () => ExportMap
-     */
-    this.imports = new Map();
-    this.errors = [];
-    /**
-     * type {'ambiguous' | 'Module' | 'Script'}
-     */
-    this.parseGoal = 'ambiguous';
-  }
-
-  get hasDefault() { return this.get('default') != null; } // stronger than this.has
-
-  get size() {
-    let size = this.namespace.size + this.reexports.size;
-    this.dependencies.forEach((dep) => {
-      const d = dep();
-      // CJS / ignored dependencies won't exist (#717)
-      if (d == null) { return; }
-      size += d.size;
-    });
-    return size;
-  }
-
-  /**
-   * Note that this does not check explicitly re-exported names for existence
-   * in the base namespace, but it will expand all `export * from '...'` exports
-   * if not found in the explicit namespace.
-   * @param  {string}  name
-   * @return {Boolean} true if `name` is exported by this module.
-   */
-  has(name) {
-    if (this.namespace.has(name)) { return true; }
-    if (this.reexports.has(name)) { return true; }
-
-    // default exports must be explicitly re-exported (#328)
-    if (name !== 'default') {
-      for (const dep of this.dependencies) {
-        const innerMap = dep();
-
-        // todo: report as unresolved?
-        if (!innerMap) { continue; }
-
-        if (innerMap.has(name)) { return true; }
-      }
-    }
-
-    return false;
-  }
-
-  /**
-   * ensure that imported name fully resolves.
-   * @param  {string} name
-   * @return {{ found: boolean, path: ExportMap[] }}
-   */
-  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 import is ignored, return explicit 'null'
-      if (imported == null) { return { found: true, path: [this] }; }
-
-      // safeguard against cycles, only if name matches
-      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;
-    }
-
-    // default exports must be explicitly re-exported (#328)
-    if (name !== 'default') {
-      for (const dep of this.dependencies) {
-        const innerMap = dep();
-        if (innerMap == null) { return { found: true, path: [this] }; }
-        // todo: report as unresolved?
-        if (!innerMap) { continue; }
-
-        // safeguard against cycles
-        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 import is ignored, return explicit 'null'
-      if (imported == null) { return null; }
-
-      // safeguard against cycles, only if name matches
-      if (imported.path === this.path && reexports.local === name) { return undefined; }
-
-      return imported.get(reexports.local);
-    }
-
-    // default exports must be explicitly re-exported (#328)
-    if (name !== 'default') {
-      for (const dep of this.dependencies) {
-        const innerMap = dep();
-        // todo: report as unresolved?
-        if (!innerMap) { continue; }
-
-        // safeguard against cycles
-        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();
-      // can't look up meta for ignored re-exports (#348)
-      callback.call(thisArg, reexported && reexported.get(reexports.local), name, this);
-    });
-
-    this.dependencies.forEach((dep) => {
-      const d = dep();
-      // CJS / ignored dependencies won't exist (#717)
-      if (d == null) { return; }
-
-      d.forEach((v, n) => {
-        if (n !== 'default') {
-          callback.call(thisArg, v, n, this);
-        }
-      });
-    });
-  }
-
-  // todo: keys, values, entries?
-
-  reportErrors(context, declaration) {
-    const msg = this.errors
-      .map((e) => `${e.message} (${e.lineNumber}:${e.column})`)
-      .join(', ');
-    context.report({
-      node: declaration.source,
-      message: `Parse errors in imported module '${declaration.source.value}': ${msg}`,
-    });
-  }
-
+export default class ExportMapBuilder {
   static get(source, context) {
     const path = resolve(source, context);
     if (path == null) { return null; }
 
-    return ExportMap.for(childContext(path, context));
+    return ExportMapBuilder.for(childContext(path, context));
   }
 
   static for(context) {
@@ -343,7 +167,7 @@ export default class ExportMap {
     }
 
     log('cache miss', cacheKey, 'for path', path);
-    exportMap = ExportMap.parse(path, content, context);
+    exportMap = ExportMapBuilder.parse(path, content, context);
 
     // ambiguous modules return null
     if (exportMap == null) {
@@ -447,7 +271,7 @@ export default class ExportMap {
     function resolveImport(value) {
       const rp = remotePath(value);
       if (rp == null) { return null; }
-      return ExportMap.for(childContext(rp, context));
+      return ExportMapBuilder.for(childContext(rp, context));
     }
 
     function getNamespace(identifier) {
@@ -738,7 +562,7 @@ export default class ExportMap {
  * caused memory leaks. See #1266.
  */
 function thunkFor(p, context) {
-  return () => ExportMap.for(childContext(p, context));
+  return () => ExportMapBuilder.for(childContext(p, context));
 }
 
 /**
diff --git a/src/rules/default.js b/src/rules/default.js
index 297a80c4..cbaa49f1 100644
--- a/src/rules/default.js
+++ b/src/rules/default.js
@@ -1,4 +1,4 @@
-import Exports from '../ExportMap';
+import ExportMapBuilder from '../exportMapBuilder';
 import docsUrl from '../docsUrl';
 
 module.exports = {
@@ -19,7 +19,7 @@ module.exports = {
       );
 
       if (!defaultSpecifier) { return; }
-      const imports = Exports.get(node.source.value, context);
+      const imports = ExportMapBuilder.get(node.source.value, context);
       if (imports == null) { return; }
 
       if (imports.errors.length) {
diff --git a/src/rules/export.js b/src/rules/export.js
index c540f1e3..b1dc5ca9 100644
--- a/src/rules/export.js
+++ b/src/rules/export.js
@@ -1,4 +1,4 @@
-import ExportMap, { recursivePatternCapture } from '../ExportMap';
+import ExportMapBuilder, { recursivePatternCapture } from '../exportMapBuilder';
 import docsUrl from '../docsUrl';
 import includes from 'array-includes';
 import flatMap from 'array.prototype.flatmap';
@@ -197,7 +197,7 @@ module.exports = {
         // `export * as X from 'path'` does not conflict
         if (node.exported && node.exported.name) { return; }
 
-        const remoteExports = ExportMap.get(node.source.value, context);
+        const remoteExports = ExportMapBuilder.get(node.source.value, context);
         if (remoteExports == null) { return; }
 
         if (remoteExports.errors.length) {
diff --git a/src/rules/named.js b/src/rules/named.js
index e7fe4e4d..043d72ea 100644
--- a/src/rules/named.js
+++ b/src/rules/named.js
@@ -1,5 +1,5 @@
 import * as path from 'path';
-import Exports from '../ExportMap';
+import ExportMapBuilder from '../exportMapBuilder';
 import docsUrl from '../docsUrl';
 
 module.exports = {
@@ -41,7 +41,7 @@ module.exports = {
         return; // no named imports/exports
       }
 
-      const imports = Exports.get(node.source.value, context);
+      const imports = ExportMapBuilder.get(node.source.value, context);
       if (imports == null || imports.parseGoal === 'ambiguous') {
         return;
       }
@@ -93,7 +93,7 @@ module.exports = {
       const call = node.init;
       const [source] = call.arguments;
       const variableImports = node.id.properties;
-      const variableExports = Exports.get(source.value, context);
+      const variableExports = ExportMapBuilder.get(source.value, context);
 
       if (
         // return if it's not a commonjs require statement
diff --git a/src/rules/namespace.js b/src/rules/namespace.js
index 77a3ea90..e1ca2870 100644
--- a/src/rules/namespace.js
+++ b/src/rules/namespace.js
@@ -1,5 +1,6 @@
 import declaredScope from 'eslint-module-utils/declaredScope';
-import Exports from '../ExportMap';
+import ExportMapBuilder from '../exportMapBuilder';
+import ExportMap from '../exportMap';
 import importDeclaration from '../importDeclaration';
 import docsUrl from '../docsUrl';
 
@@ -8,7 +9,7 @@ function processBodyStatement(context, namespaces, declaration) {
 
   if (declaration.specifiers.length === 0) { return; }
 
-  const imports = Exports.get(declaration.source.value, context);
+  const imports = ExportMapBuilder.get(declaration.source.value, context);
   if (imports == null) { return null; }
 
   if (imports.errors.length > 0) {
@@ -88,7 +89,7 @@ module.exports = {
       ExportNamespaceSpecifier(namespace) {
         const declaration = importDeclaration(context);
 
-        const imports = Exports.get(declaration.source.value, context);
+        const imports = ExportMapBuilder.get(declaration.source.value, context);
         if (imports == null) { return null; }
 
         if (imports.errors.length) {
@@ -122,7 +123,7 @@ module.exports = {
         let namespace = namespaces.get(dereference.object.name);
         const namepath = [dereference.object.name];
         // while property is namespace and parent is member expression, keep validating
-        while (namespace instanceof Exports && dereference.type === 'MemberExpression') {
+        while (namespace instanceof ExportMap && dereference.type === 'MemberExpression') {
           if (dereference.computed) {
             if (!allowComputed) {
               context.report(
@@ -161,7 +162,7 @@ module.exports = {
 
         // DFS traverse child namespaces
         function testKey(pattern, namespace, path = [init.name]) {
-          if (!(namespace instanceof Exports)) { return; }
+          if (!(namespace instanceof ExportMap)) { return; }
 
           if (pattern.type !== 'ObjectPattern') { return; }
 
diff --git a/src/rules/no-cycle.js b/src/rules/no-cycle.js
index 11c2f44f..b7b907b0 100644
--- a/src/rules/no-cycle.js
+++ b/src/rules/no-cycle.js
@@ -4,7 +4,7 @@
  */
 
 import resolve from 'eslint-module-utils/resolve';
-import Exports from '../ExportMap';
+import ExportMapBuilder from '../exportMapBuilder';
 import { isExternalModule } from '../core/importType';
 import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor';
 import docsUrl from '../docsUrl';
@@ -88,7 +88,7 @@ module.exports = {
         return; // ignore type imports
       }
 
-      const imported = Exports.get(sourceNode.value, context);
+      const imported = ExportMapBuilder.get(sourceNode.value, context);
 
       if (imported == null) {
         return;  // no-unresolved territory
diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js
index 06eeff8e..50072f3f 100644
--- a/src/rules/no-deprecated.js
+++ b/src/rules/no-deprecated.js
@@ -1,5 +1,6 @@
 import declaredScope from 'eslint-module-utils/declaredScope';
-import Exports from '../ExportMap';
+import ExportMapBuilder from '../exportMapBuilder';
+import ExportMap from '../exportMap';
 import docsUrl from '../docsUrl';
 
 function message(deprecation) {
@@ -31,7 +32,7 @@ module.exports = {
       if (node.type !== 'ImportDeclaration') { return; }
       if (node.source == null) { return; } // local export, ignore
 
-      const imports = Exports.get(node.source.value, context);
+      const imports = ExportMapBuilder.get(node.source.value, context);
       if (imports == null) { return; }
 
       const moduleDeprecation = imports.doc && imports.doc.tags.find((t) => t.title === 'deprecated');
@@ -114,7 +115,7 @@ module.exports = {
         let namespace = namespaces.get(dereference.object.name);
         const namepath = [dereference.object.name];
         // while property is namespace and parent is member expression, keep validating
-        while (namespace instanceof Exports && dereference.type === 'MemberExpression') {
+        while (namespace instanceof ExportMap && dereference.type === 'MemberExpression') {
           // ignore computed parts for now
           if (dereference.computed) { return; }
 
diff --git a/src/rules/no-named-as-default-member.js b/src/rules/no-named-as-default-member.js
index e00a4cbc..d594c584 100644
--- a/src/rules/no-named-as-default-member.js
+++ b/src/rules/no-named-as-default-member.js
@@ -4,7 +4,7 @@
  * @copyright 2016 Desmond Brand. All rights reserved.
  * See LICENSE in root directory for full license.
  */
-import Exports from '../ExportMap';
+import ExportMapBuilder from '../exportMapBuilder';
 import importDeclaration from '../importDeclaration';
 import docsUrl from '../docsUrl';
 
@@ -36,7 +36,7 @@ module.exports = {
     return {
       ImportDefaultSpecifier(node) {
         const declaration = importDeclaration(context);
-        const exportMap = Exports.get(declaration.source.value, context);
+        const exportMap = ExportMapBuilder.get(declaration.source.value, context);
         if (exportMap == null) { return; }
 
         if (exportMap.errors.length) {
diff --git a/src/rules/no-named-as-default.js b/src/rules/no-named-as-default.js
index 40b1e175..3e73ff2f 100644
--- a/src/rules/no-named-as-default.js
+++ b/src/rules/no-named-as-default.js
@@ -1,4 +1,4 @@
-import Exports from '../ExportMap';
+import ExportMapBuilder from '../exportMapBuilder';
 import importDeclaration from '../importDeclaration';
 import docsUrl from '../docsUrl';
 
@@ -20,7 +20,7 @@ module.exports = {
 
       const declaration = importDeclaration(context);
 
-      const imports = Exports.get(declaration.source.value, context);
+      const imports = ExportMapBuilder.get(declaration.source.value, context);
       if (imports == null) { return; }
 
       if (imports.errors.length) {
diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js
index ec3425da..812efffb 100644
--- a/src/rules/no-unused-modules.js
+++ b/src/rules/no-unused-modules.js
@@ -13,7 +13,7 @@ import values from 'object.values';
 import includes from 'array-includes';
 import flatMap from 'array.prototype.flatmap';
 
-import Exports, { recursivePatternCapture } from '../ExportMap';
+import ExportMapBuilder, { recursivePatternCapture } from '../exportMapBuilder';
 import docsUrl from '../docsUrl';
 
 let FileEnumerator;
@@ -194,7 +194,7 @@ const prepareImportsAndExports = (srcFiles, context) => {
   srcFiles.forEach((file) => {
     const exports = new Map();
     const imports = new Map();
-    const currentExports = Exports.get(file, context);
+    const currentExports = ExportMapBuilder.get(file, context);
     if (currentExports) {
       const {
         dependencies,


Test patch:
diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js
index 1dd6e880..611a1305 100644
--- a/tests/src/core/getExports.js
+++ b/tests/src/core/getExports.js
@@ -4,7 +4,7 @@ import sinon from 'sinon';
 import eslintPkg from 'eslint/package.json';
 import typescriptPkg from 'typescript/package.json';
 import * as tsConfigLoader from 'tsconfig-paths/lib/tsconfig-loader';
-import ExportMap from '../../../src/ExportMap';
+import ExportMapBuilder from '../../../src/exportMapBuilder';
 
 import * as fs from 'fs';
 
@@ -28,7 +28,7 @@ describe('ExportMap', function () {
   it('handles ExportAllDeclaration', function () {
     let imports;
     expect(function () {
-      imports = ExportMap.get('./export-all', fakeContext);
+      imports = ExportMapBuilder.get('./export-all', fakeContext);
     }).not.to.throw(Error);
 
     expect(imports).to.exist;
@@ -37,25 +37,25 @@ describe('ExportMap', function () {
   });
 
   it('returns a cached copy on subsequent requests', function () {
-    expect(ExportMap.get('./named-exports', fakeContext))
-      .to.exist.and.equal(ExportMap.get('./named-exports', fakeContext));
+    expect(ExportMapBuilder.get('./named-exports', fakeContext))
+      .to.exist.and.equal(ExportMapBuilder.get('./named-exports', fakeContext));
   });
 
   it('does not return a cached copy after modification', (done) => {
-    const firstAccess = ExportMap.get('./mutator', fakeContext);
+    const firstAccess = ExportMapBuilder.get('./mutator', fakeContext);
     expect(firstAccess).to.exist;
 
     // mutate (update modified time)
     const newDate = new Date();
     fs.utimes(getFilename('mutator.js'), newDate, newDate, (error) => {
       expect(error).not.to.exist;
-      expect(ExportMap.get('./mutator', fakeContext)).not.to.equal(firstAccess);
+      expect(ExportMapBuilder.get('./mutator', fakeContext)).not.to.equal(firstAccess);
       done();
     });
   });
 
   it('does not return a cached copy with different settings', () => {
-    const firstAccess = ExportMap.get('./named-exports', fakeContext);
+    const firstAccess = ExportMapBuilder.get('./named-exports', fakeContext);
     expect(firstAccess).to.exist;
 
     const differentSettings = {
@@ -63,7 +63,7 @@ describe('ExportMap', function () {
       parserPath: 'espree',
     };
 
-    expect(ExportMap.get('./named-exports', differentSettings))
+    expect(ExportMapBuilder.get('./named-exports', differentSettings))
       .to.exist.and
       .not.to.equal(firstAccess);
   });
@@ -71,7 +71,7 @@ describe('ExportMap', function () {
   it('does not throw for a missing file', function () {
     let imports;
     expect(function () {
-      imports = ExportMap.get('./does-not-exist', fakeContext);
+      imports = ExportMapBuilder.get('./does-not-exist', fakeContext);
     }).not.to.throw(Error);
 
     expect(imports).not.to.exist;
@@ -81,7 +81,7 @@ describe('ExportMap', function () {
   it('exports explicit names for a missing file in exports', function () {
     let imports;
     expect(function () {
-      imports = ExportMap.get('./exports-missing', fakeContext);
+      imports = ExportMapBuilder.get('./exports-missing', fakeContext);
     }).not.to.throw(Error);
 
     expect(imports).to.exist;
@@ -92,7 +92,7 @@ describe('ExportMap', function () {
   it('finds exports for an ES7 module with babel-eslint', function () {
     const path = getFilename('jsx/FooES7.js');
     const contents = fs.readFileSync(path, { encoding: 'utf8' });
-    const imports = ExportMap.parse(
+    const imports = ExportMapBuilder.parse(
       path,
       contents,
       { parserPath: 'babel-eslint', settings: {} },
@@ -112,7 +112,7 @@ describe('ExportMap', function () {
         before('parse file', function () {
           const path = getFilename('deprecated.js');
           const contents = fs.readFileSync(path, { encoding: 'utf8' }).replace(/[\r]\n/g, lineEnding);
-          imports = ExportMap.parse(path, contents, parseContext);
+          imports = ExportMapBuilder.parse(path, contents, parseContext);
 
           // sanity checks
           expect(imports.errors).to.be.empty;
@@ -181,7 +181,7 @@ describe('ExportMap', function () {
         before('parse file', function () {
           const path = getFilename('deprecated-file.js');
           const contents = fs.readFileSync(path, { encoding: 'utf8' });
-          imports = ExportMap.parse(path, contents, parseContext);
+          imports = ExportMapBuilder.parse(path, contents, parseContext);
 
           // sanity checks
           expect(imports.errors).to.be.empty;
@@ -243,7 +243,7 @@ describe('ExportMap', function () {
     it('works with espree & traditional namespace exports', function () {
       const path = getFilename('deep/a.js');
       const contents = fs.readFileSync(path, { encoding: 'utf8' });
-      const a = ExportMap.parse(path, contents, espreeContext);
+      const a = ExportMapBuilder.parse(path, contents, espreeContext);
       expect(a.errors).to.be.empty;
       expect(a.get('b').namespace).to.exist;
       expect(a.get('b').namespace.has('c')).to.be.true;
@@ -252,7 +252,7 @@ describe('ExportMap', function () {
     it('captures namespace exported as default', function () {
       const path = getFilename('deep/default.js');
       const contents = fs.readFileSync(path, { encoding: 'utf8' });
-      const def = ExportMap.parse(path, contents, espreeContext);
+      const def = ExportMapBuilder.parse(path, contents, espreeContext);
       expect(def.errors).to.be.empty;
       expect(def.get('default').namespace).to.exist;
       expect(def.get('default').namespace.has('c')).to.be.true;
@@ -261,7 +261,7 @@ describe('ExportMap', function () {
     it('works with babel-eslint & ES7 namespace exports', function () {
       const path = getFilename('deep-es7/a.js');
       const contents = fs.readFileSync(path, { encoding: 'utf8' });
-      const a = ExportMap.parse(path, contents, babelContext);
+      const a = ExportMapBuilder.parse(path, contents, babelContext);
       expect(a.errors).to.be.empty;
       expect(a.get('b').namespace).to.exist;
       expect(a.get('b').namespace.has('c')).to.be.true;
@@ -278,7 +278,7 @@ describe('ExportMap', function () {
 
       const path = getFilename('deep/cache-1.js');
       const contents = fs.readFileSync(path, { encoding: 'utf8' });
-      a = ExportMap.parse(path, contents, espreeContext);
+      a = ExportMapBuilder.parse(path, contents, espreeContext);
       expect(a.errors).to.be.empty;
 
       expect(a.get('b').namespace).to.exist;
@@ -304,10 +304,10 @@ describe('ExportMap', function () {
   context('Map API', function () {
     context('#size', function () {
 
-      it('counts the names', () => expect(ExportMap.get('./named-exports', fakeContext))
+      it('counts the names', () => expect(ExportMapBuilder.get('./named-exports', fakeContext))
         .to.have.property('size', 12));
 
-      it('includes exported namespace size', () => expect(ExportMap.get('./export-all', fakeContext))
+      it('includes exported namespace size', () => expect(ExportMapBuilder.get('./export-all', fakeContext))
         .to.have.property('size', 1));
 
     });
@@ -315,14 +315,14 @@ describe('ExportMap', function () {
 
   context('issue #210: self-reference', function () {
     it(`doesn't crash`, function () {
-      expect(() => ExportMap.get('./narcissist', fakeContext)).not.to.throw(Error);
+      expect(() => ExportMapBuilder.get('./narcissist', fakeContext)).not.to.throw(Error);
     });
     it(`'has' circular reference`, function () {
-      expect(ExportMap.get('./narcissist', fakeContext))
+      expect(ExportMapBuilder.get('./narcissist', fakeContext))
         .to.exist.and.satisfy((m) => m.has('soGreat'));
     });
     it(`can 'get' circular reference`, function () {
-      expect(ExportMap.get('./narcissist', fakeContext))
+      expect(ExportMapBuilder.get('./narcissist', fakeContext))
         .to.exist.and.satisfy((m) => m.get('soGreat') != null);
     });
   });
@@ -335,7 +335,7 @@ describe('ExportMap', function () {
 
     let imports;
     before('load imports', function () {
-      imports = ExportMap.get('./typescript.ts', context);
+      imports = ExportMapBuilder.get('./typescript.ts', context);
     });
 
     it('returns nothing for a TypeScript file', function () {
@@ -372,7 +372,7 @@ describe('ExportMap', function () {
         before('load imports', function () {
           this.timeout(20e3);  // takes a long time :shrug:
           sinon.spy(tsConfigLoader, 'tsConfigLoader');
-          imports = ExportMap.get('./typescript.ts', context);
+          imports = ExportMapBuilder.get('./typescript.ts', context);
         });
         after('clear spies', function () {
           tsConfigLoader.tsConfigLoader.restore();
@@ -414,9 +414,9 @@ describe('ExportMap', function () {
             },
           };
           expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(0);
-          ExportMap.parse('./baz.ts', 'export const baz = 5', customContext);
+          ExportMapBuilder.parse('./baz.ts', 'export const baz = 5', customContext);
           expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(1);
-          ExportMap.parse('./baz.ts', 'export const baz = 5', customContext);
+          ExportMapBuilder.parse('./baz.ts', 'export const baz = 5', customContext);
           expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(1);
 
           const differentContext = {
@@ -426,17 +426,17 @@ describe('ExportMap', function () {
             },
           };
 
-          ExportMap.parse('./baz.ts', 'export const baz = 5', differentContext);
+          ExportMapBuilder.parse('./baz.ts', 'export const baz = 5', differentContext);
           expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(2);
         });
 
         it('should cache after parsing for an ambiguous module', function () {
           const source = './typescript-declare-module.ts';
-          const parseSpy = sinon.spy(ExportMap, 'parse');
+          const parseSpy = sinon.spy(ExportMapBuilder, 'parse');
 
-          expect(ExportMap.get(source, context)).to.be.null;
+          expect(ExportMapBuilder.get(source, context)).to.be.null;
 
-          ExportMap.get(source, context);
+          ExportMapBuilder.get(source, context);
 
           expect(parseSpy.callCount).to.equal(1);