Skip to content

Commit ab88f4d

Browse files
authored
Merge branch 'main' into patch-2
2 parents 47efdbe + c0ac54b commit ab88f4d

27 files changed

+752
-661
lines changed

.github/workflows/native-wsl.yml

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ name: Native and WSL
22

33
on: [push, pull_request]
44

5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.ref }}
7+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
8+
59
jobs:
610
build:
711
runs-on: ${{ matrix.os }}

.github/workflows/node-4+.yml

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ name: 'Tests: node.js'
22

33
on: [pull_request, push]
44

5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.ref }}
7+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
8+
59
permissions:
610
contents: read
711

.github/workflows/packages.yml

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ name: 'Tests: packages'
22

33
on: [pull_request, push]
44

5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.ref }}
7+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
8+
59
permissions:
610
contents: read
711

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1818
- [Tests] appveyor -> GHA (run tests on Windows in both pwsh and WSL + Ubuntu) ([#2987], thanks [@joeyguerra])
1919
- [actions] migrate OSX tests to GHA ([ljharb#37], thanks [@aks-])
2020
- [Refactor] `exportMapBuilder`: avoid hoisting ([#2989], thanks [@soryy708])
21+
- [Refactor] `ExportMap`: extract "builder" logic to separate files ([#2991], thanks [@soryy708])
2122

2223
## [2.29.1] - 2023-12-14
2324

@@ -1114,6 +1115,7 @@ for info on changes for earlier releases.
11141115

11151116
[`memo-parser`]: ./memo-parser/README.md
11161117

1118+
[#2991]: https://github.com/import-js/eslint-plugin-import/pull/2991
11171119
[#2989]: https://github.com/import-js/eslint-plugin-import/pull/2989
11181120
[#2987]: https://github.com/import-js/eslint-plugin-import/pull/2987
11191121
[#2985]: https://github.com/import-js/eslint-plugin-import/pull/2985

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"eslint-plugin-eslint-plugin": "^2.3.0",
8383
"eslint-plugin-import": "2.x",
8484
"eslint-plugin-json": "^2.1.2",
85+
"find-babel-config": "=1.2.0",
8586
"fs-copy-file-sync": "^1.1.1",
8687
"glob": "^7.2.3",
8788
"in-publish": "^2.0.1",

src/exportMap/builder.js

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import fs from 'fs';
2+
3+
import doctrine from 'doctrine';
4+
5+
import debug from 'debug';
6+
7+
import parse from 'eslint-module-utils/parse';
8+
import visit from 'eslint-module-utils/visit';
9+
import resolve from 'eslint-module-utils/resolve';
10+
import isIgnored, { hasValidExtension } from 'eslint-module-utils/ignore';
11+
12+
import { hashObject } from 'eslint-module-utils/hash';
13+
import * as unambiguous from 'eslint-module-utils/unambiguous';
14+
15+
import ExportMap from '.';
16+
import childContext from './childContext';
17+
import { isEsModuleInterop } from './typescript';
18+
import { RemotePath } from './remotePath';
19+
import ImportExportVisitorBuilder from './visitor';
20+
21+
const log = debug('eslint-plugin-import:ExportMap');
22+
23+
const exportCache = new Map();
24+
25+
/**
26+
* The creation of this closure is isolated from other scopes
27+
* to avoid over-retention of unrelated variables, which has
28+
* caused memory leaks. See #1266.
29+
*/
30+
function thunkFor(p, context) {
31+
// eslint-disable-next-line no-use-before-define
32+
return () => ExportMapBuilder.for(childContext(p, context));
33+
}
34+
35+
export default class ExportMapBuilder {
36+
static get(source, context) {
37+
const path = resolve(source, context);
38+
if (path == null) { return null; }
39+
40+
return ExportMapBuilder.for(childContext(path, context));
41+
}
42+
43+
static for(context) {
44+
const { path } = context;
45+
46+
const cacheKey = context.cacheKey || hashObject(context).digest('hex');
47+
let exportMap = exportCache.get(cacheKey);
48+
49+
// return cached ignore
50+
if (exportMap === null) { return null; }
51+
52+
const stats = fs.statSync(path);
53+
if (exportMap != null) {
54+
// date equality check
55+
if (exportMap.mtime - stats.mtime === 0) {
56+
return exportMap;
57+
}
58+
// future: check content equality?
59+
}
60+
61+
// check valid extensions first
62+
if (!hasValidExtension(path, context)) {
63+
exportCache.set(cacheKey, null);
64+
return null;
65+
}
66+
67+
// check for and cache ignore
68+
if (isIgnored(path, context)) {
69+
log('ignored path due to ignore settings:', path);
70+
exportCache.set(cacheKey, null);
71+
return null;
72+
}
73+
74+
const content = fs.readFileSync(path, { encoding: 'utf8' });
75+
76+
// check for and cache unambiguous modules
77+
if (!unambiguous.test(content)) {
78+
log('ignored path due to unambiguous regex:', path);
79+
exportCache.set(cacheKey, null);
80+
return null;
81+
}
82+
83+
log('cache miss', cacheKey, 'for path', path);
84+
exportMap = ExportMapBuilder.parse(path, content, context);
85+
86+
// ambiguous modules return null
87+
if (exportMap == null) {
88+
log('ignored path due to ambiguous parse:', path);
89+
exportCache.set(cacheKey, null);
90+
return null;
91+
}
92+
93+
exportMap.mtime = stats.mtime;
94+
95+
exportCache.set(cacheKey, exportMap);
96+
return exportMap;
97+
}
98+
99+
static parse(path, content, context) {
100+
const exportMap = new ExportMap(path);
101+
const isEsModuleInteropTrue = isEsModuleInterop(context);
102+
103+
let ast;
104+
let visitorKeys;
105+
try {
106+
const result = parse(path, content, context);
107+
ast = result.ast;
108+
visitorKeys = result.visitorKeys;
109+
} catch (err) {
110+
exportMap.errors.push(err);
111+
return exportMap; // can't continue
112+
}
113+
114+
exportMap.visitorKeys = visitorKeys;
115+
116+
let hasDynamicImports = false;
117+
118+
const remotePathResolver = new RemotePath(path, context);
119+
120+
function processDynamicImport(source) {
121+
hasDynamicImports = true;
122+
if (source.type !== 'Literal') {
123+
return null;
124+
}
125+
const p = remotePathResolver.resolve(source.value);
126+
if (p == null) {
127+
return null;
128+
}
129+
const importedSpecifiers = new Set();
130+
importedSpecifiers.add('ImportNamespaceSpecifier');
131+
const getter = thunkFor(p, context);
132+
exportMap.imports.set(p, {
133+
getter,
134+
declarations: new Set([{
135+
source: {
136+
// capturing actual node reference holds full AST in memory!
137+
value: source.value,
138+
loc: source.loc,
139+
},
140+
importedSpecifiers,
141+
dynamic: true,
142+
}]),
143+
});
144+
}
145+
146+
visit(ast, visitorKeys, {
147+
ImportExpression(node) {
148+
processDynamicImport(node.source);
149+
},
150+
CallExpression(node) {
151+
if (node.callee.type === 'Import') {
152+
processDynamicImport(node.arguments[0]);
153+
}
154+
},
155+
});
156+
157+
const unambiguouslyESM = unambiguous.isModule(ast);
158+
if (!unambiguouslyESM && !hasDynamicImports) { return null; }
159+
160+
// attempt to collect module doc
161+
if (ast.comments) {
162+
ast.comments.some((c) => {
163+
if (c.type !== 'Block') { return false; }
164+
try {
165+
const doc = doctrine.parse(c.value, { unwrap: true });
166+
if (doc.tags.some((t) => t.title === 'module')) {
167+
exportMap.doc = doc;
168+
return true;
169+
}
170+
} catch (err) { /* ignore */ }
171+
return false;
172+
});
173+
}
174+
175+
const visitorBuilder = new ImportExportVisitorBuilder(
176+
path,
177+
context,
178+
exportMap,
179+
ExportMapBuilder,
180+
content,
181+
ast,
182+
isEsModuleInteropTrue,
183+
thunkFor,
184+
);
185+
ast.body.forEach(function (astNode) {
186+
const visitor = visitorBuilder.build(astNode);
187+
188+
if (visitor[astNode.type]) {
189+
visitor[astNode.type].call(visitorBuilder);
190+
}
191+
});
192+
193+
if (
194+
isEsModuleInteropTrue // esModuleInterop is on in tsconfig
195+
&& exportMap.namespace.size > 0 // anything is exported
196+
&& !exportMap.namespace.has('default') // and default isn't added already
197+
) {
198+
exportMap.namespace.set('default', {}); // add default export
199+
}
200+
201+
if (unambiguouslyESM) {
202+
exportMap.parseGoal = 'Module';
203+
}
204+
return exportMap;
205+
}
206+
}

src/exportMap/captureDependency.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
export function captureDependency(
2+
{ source },
3+
isOnlyImportingTypes,
4+
remotePathResolver,
5+
exportMap,
6+
context,
7+
thunkFor,
8+
importedSpecifiers = new Set(),
9+
) {
10+
if (source == null) { return null; }
11+
12+
const p = remotePathResolver.resolve(source.value);
13+
if (p == null) { return null; }
14+
15+
const declarationMetadata = {
16+
// capturing actual node reference holds full AST in memory!
17+
source: { value: source.value, loc: source.loc },
18+
isOnlyImportingTypes,
19+
importedSpecifiers,
20+
};
21+
22+
const existing = exportMap.imports.get(p);
23+
if (existing != null) {
24+
existing.declarations.add(declarationMetadata);
25+
return existing.getter;
26+
}
27+
28+
const getter = thunkFor(p, context);
29+
exportMap.imports.set(p, { getter, declarations: new Set([declarationMetadata]) });
30+
return getter;
31+
}
32+
33+
const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']);
34+
35+
export function captureDependencyWithSpecifiers(
36+
n,
37+
remotePathResolver,
38+
exportMap,
39+
context,
40+
thunkFor,
41+
) {
42+
// import type { Foo } (TS and Flow); import typeof { Foo } (Flow)
43+
const declarationIsType = n.importKind === 'type' || n.importKind === 'typeof';
44+
// import './foo' or import {} from './foo' (both 0 specifiers) is a side effect and
45+
// shouldn't be considered to be just importing types
46+
let specifiersOnlyImportingTypes = n.specifiers.length > 0;
47+
const importedSpecifiers = new Set();
48+
n.specifiers.forEach((specifier) => {
49+
if (specifier.type === 'ImportSpecifier') {
50+
importedSpecifiers.add(specifier.imported.name || specifier.imported.value);
51+
} else if (supportedImportTypes.has(specifier.type)) {
52+
importedSpecifiers.add(specifier.type);
53+
}
54+
55+
// import { type Foo } (Flow); import { typeof Foo } (Flow)
56+
specifiersOnlyImportingTypes = specifiersOnlyImportingTypes
57+
&& (specifier.importKind === 'type' || specifier.importKind === 'typeof');
58+
});
59+
captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, remotePathResolver, exportMap, context, thunkFor, importedSpecifiers);
60+
}

src/exportMap/childContext.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { hashObject } from 'eslint-module-utils/hash';
2+
3+
let parserOptionsHash = '';
4+
let prevParserOptions = '';
5+
let settingsHash = '';
6+
let prevSettings = '';
7+
8+
/**
9+
* don't hold full context object in memory, just grab what we need.
10+
* also calculate a cacheKey, where parts of the cacheKey hash are memoized
11+
*/
12+
export default function childContext(path, context) {
13+
const { settings, parserOptions, parserPath } = context;
14+
15+
if (JSON.stringify(settings) !== prevSettings) {
16+
settingsHash = hashObject({ settings }).digest('hex');
17+
prevSettings = JSON.stringify(settings);
18+
}
19+
20+
if (JSON.stringify(parserOptions) !== prevParserOptions) {
21+
parserOptionsHash = hashObject({ parserOptions }).digest('hex');
22+
prevParserOptions = JSON.stringify(parserOptions);
23+
}
24+
25+
return {
26+
cacheKey: String(parserPath) + parserOptionsHash + settingsHash + String(path),
27+
settings,
28+
parserOptions,
29+
parserPath,
30+
path,
31+
};
32+
}

0 commit comments

Comments
 (0)