Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

separate export map from its builder #2985

Merged
merged 1 commit into from Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc
Expand Up @@ -229,7 +229,7 @@
{
"files": [
"utils/**", // TODO
"src/ExportMap.js", // TODO
"src/exportMapBuilder.js", // TODO
],
"rules": {
"no-use-before-define": "off",
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
178 changes: 178 additions & 0 deletions 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}`,
});
}
}