testguard-cli 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -1
- package/README.md +16 -4
- package/package.json +1 -1
- package/spec/GATE-SEMANTICS.md +65 -3
- package/spec/README.md +7 -0
- package/spec/schemas/evidence.schema.json +14 -0
- package/src/cli.mjs +4 -1
- package/src/command-budget.mjs +52 -0
- package/src/commands/probe.mjs +9 -0
- package/src/commands/replay.mjs +14 -1
- package/src/commands/sweep.mjs +15 -6
- package/src/git.mjs +4 -2
- package/src/probe/attribution.mjs +2 -0
- package/src/probe/discover.mjs +44 -8
- package/src/probe/imports.mjs +785 -0
- package/src/probe/mocks.mjs +13 -4
- package/src/probe/probe.mjs +176 -19
- package/src/probe/rank.mjs +28 -5
- package/src/probe/runners/discovery.mjs +634 -0
- package/src/probe/runners/index.mjs +51 -19
- package/src/probe/runners/jest.mjs +32 -1
- package/src/probe/runners/playwright.mjs +58 -2
- package/src/probe/runners/python.mjs +7 -7
- package/src/probe/runners/shared.mjs +55 -6
- package/src/probe/runners/vitest.mjs +21 -0
- package/src/replay/replay.mjs +108 -13
- package/src/sweep/sweep.mjs +5 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const IMPORT_LIMITS = Object.freeze({
|
|
5
|
+
maxFileBytes: 1_048_576,
|
|
6
|
+
maxDepth: 32,
|
|
7
|
+
maxModules: 256,
|
|
8
|
+
maxEdges: 1_024,
|
|
9
|
+
maxTotalBytes: 16_777_216,
|
|
10
|
+
maxWitnessPaths: 32,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const SOURCE_EXTENSIONS = Object.freeze(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
|
|
14
|
+
const SCRIPT_FALLBACKS = Object.freeze({ '.js': ['.ts', '.tsx'], '.jsx': ['.tsx'], '.mjs': ['.mts'], '.cjs': ['.cts'] });
|
|
15
|
+
|
|
16
|
+
const identifier = (token, value) => token?.type === 'id' && (value === undefined || token.value === value);
|
|
17
|
+
const punct = (token, value) => token?.type === 'punc' && token.value === value;
|
|
18
|
+
|
|
19
|
+
function decodeString(source, start) {
|
|
20
|
+
const quote = source[start];
|
|
21
|
+
let value = '';
|
|
22
|
+
for (let i = start + 1; i < source.length; i++) {
|
|
23
|
+
const ch = source[i];
|
|
24
|
+
if (ch === quote) return { value, end: i + 1 };
|
|
25
|
+
if (ch === '\n' || ch === '\r') throw new SyntaxError('unterminated string literal');
|
|
26
|
+
if (ch !== '\\') {
|
|
27
|
+
value += ch;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const next = source[++i];
|
|
31
|
+
if (next === undefined) throw new SyntaxError('unterminated string escape');
|
|
32
|
+
const simple = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v', 0: '\0', '\\': '\\', "'": "'", '"': '"' };
|
|
33
|
+
if (next in simple) {
|
|
34
|
+
value += simple[next];
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (next === 'x') {
|
|
38
|
+
const hex = source.slice(i + 1, i + 3);
|
|
39
|
+
if (!/^[0-9a-f]{2}$/i.test(hex)) throw new SyntaxError('invalid hexadecimal string escape');
|
|
40
|
+
value += String.fromCodePoint(Number.parseInt(hex, 16));
|
|
41
|
+
i += 2;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (next === 'u') {
|
|
45
|
+
if (source[i + 1] === '{') {
|
|
46
|
+
const close = source.indexOf('}', i + 2);
|
|
47
|
+
const hex = close === -1 ? '' : source.slice(i + 2, close);
|
|
48
|
+
if (!/^[0-9a-f]{1,6}$/i.test(hex)) throw new SyntaxError('invalid unicode string escape');
|
|
49
|
+
value += String.fromCodePoint(Number.parseInt(hex, 16));
|
|
50
|
+
i = close;
|
|
51
|
+
} else {
|
|
52
|
+
const hex = source.slice(i + 1, i + 5);
|
|
53
|
+
if (!/^[0-9a-f]{4}$/i.test(hex)) throw new SyntaxError('invalid unicode string escape');
|
|
54
|
+
value += String.fromCodePoint(Number.parseInt(hex, 16));
|
|
55
|
+
i += 4;
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (next === '\n') continue;
|
|
60
|
+
if (next === '\r' && source[i + 1] === '\n') {
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
value += next;
|
|
65
|
+
}
|
|
66
|
+
throw new SyntaxError('unterminated string literal');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A deliberately small lexer: enough grammar to read module boundaries, never executable code. */
|
|
70
|
+
export function tokenizeModule(source) {
|
|
71
|
+
const tokens = [];
|
|
72
|
+
let lineBreakBefore = false;
|
|
73
|
+
for (let i = 0; i < source.length;) {
|
|
74
|
+
const ch = source[i];
|
|
75
|
+
if (/\s/.test(ch)) {
|
|
76
|
+
if (ch === '\n' || ch === '\r') lineBreakBefore = true;
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
81
|
+
i = source.indexOf('\n', i + 2);
|
|
82
|
+
if (i === -1) break;
|
|
83
|
+
lineBreakBefore = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
87
|
+
const end = source.indexOf('*/', i + 2);
|
|
88
|
+
if (end === -1) throw new SyntaxError('unterminated block comment');
|
|
89
|
+
if (source.slice(i, end + 2).includes('\n')) lineBreakBefore = true;
|
|
90
|
+
i = end + 2;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (ch === '"' || ch === "'") {
|
|
94
|
+
const string = decodeString(source, i);
|
|
95
|
+
tokens.push({ type: 'string', value: string.value, index: i, lineBreakBefore });
|
|
96
|
+
lineBreakBefore = false;
|
|
97
|
+
i = string.end;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (ch === '`') {
|
|
101
|
+
const start = i++;
|
|
102
|
+
let closed = false;
|
|
103
|
+
while (i < source.length) {
|
|
104
|
+
if (source[i] === '\\') i += 2;
|
|
105
|
+
else if (source[i] === '`') { i++; closed = true; break; }
|
|
106
|
+
else i++;
|
|
107
|
+
}
|
|
108
|
+
if (!closed) throw new SyntaxError('unterminated template literal');
|
|
109
|
+
tokens.push({ type: 'template', value: null, index: start, lineBreakBefore });
|
|
110
|
+
lineBreakBefore = false;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (/[A-Za-z_$]/.test(ch)) {
|
|
114
|
+
let end = i + 1;
|
|
115
|
+
while (end < source.length && /[\w$]/.test(source[end])) end++;
|
|
116
|
+
tokens.push({ type: 'id', value: source.slice(i, end), index: i, lineBreakBefore });
|
|
117
|
+
lineBreakBefore = false;
|
|
118
|
+
i = end;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
tokens.push({ type: 'punc', value: ch, index: i, lineBreakBefore });
|
|
122
|
+
lineBreakBefore = false;
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
return tokens;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function statementEnd(tokens, start) {
|
|
129
|
+
let braces = 0;
|
|
130
|
+
let parens = 0;
|
|
131
|
+
let brackets = 0;
|
|
132
|
+
for (let i = start; i < tokens.length; i++) {
|
|
133
|
+
const t = tokens[i];
|
|
134
|
+
if (i > start && braces === 0 && parens === 0 && brackets === 0 && t.lineBreakBefore && (identifier(t, 'import') || identifier(t, 'export'))) return i;
|
|
135
|
+
if (punct(t, '{')) braces++;
|
|
136
|
+
else if (punct(t, '}')) { if (braces === 0) return i; braces--; }
|
|
137
|
+
else if (punct(t, '(')) parens++;
|
|
138
|
+
else if (punct(t, ')')) parens--;
|
|
139
|
+
else if (punct(t, '[')) brackets++;
|
|
140
|
+
else if (punct(t, ']')) brackets--;
|
|
141
|
+
else if (punct(t, ';') && braces === 0 && parens === 0 && brackets === 0) return i + 1;
|
|
142
|
+
}
|
|
143
|
+
return tokens.length;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function namedEntries(tokens, open, close) {
|
|
147
|
+
const entries = [];
|
|
148
|
+
let i = open + 1;
|
|
149
|
+
while (i < close) {
|
|
150
|
+
if (punct(tokens[i], ',')) { i++; continue; }
|
|
151
|
+
let typeOnly = false;
|
|
152
|
+
if (identifier(tokens[i], 'type')) { typeOnly = true; i++; }
|
|
153
|
+
if (!identifier(tokens[i])) return null;
|
|
154
|
+
const imported = tokens[i++].value;
|
|
155
|
+
let exported = imported;
|
|
156
|
+
if (identifier(tokens[i], 'as')) {
|
|
157
|
+
i++;
|
|
158
|
+
if (!identifier(tokens[i])) return null;
|
|
159
|
+
exported = tokens[i++].value;
|
|
160
|
+
}
|
|
161
|
+
entries.push({ imported, exported, typeOnly });
|
|
162
|
+
if (i < close && !punct(tokens[i], ',')) return null;
|
|
163
|
+
}
|
|
164
|
+
return entries;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function addExplicit(module, name, origin) {
|
|
168
|
+
const existing = module.explicit.get(name) ?? [];
|
|
169
|
+
existing.push(origin);
|
|
170
|
+
module.explicit.set(name, existing);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseStaticImport(tokens, start, module) {
|
|
174
|
+
const end = statementEnd(tokens, start);
|
|
175
|
+
if (punct(tokens[start + 1], '(')) return start + 1; // dynamic import: parsed below
|
|
176
|
+
if (tokens[start + 1]?.type === 'string') {
|
|
177
|
+
module.requests.push({ specifier: tokens[start + 1].value, mode: 'runtime' });
|
|
178
|
+
module.runtime.push(tokens[start + 1].value);
|
|
179
|
+
return end;
|
|
180
|
+
}
|
|
181
|
+
if (identifier(tokens[start + 1], 'type')) return end;
|
|
182
|
+
const from = tokens.findIndex((t, i) => i > start && i < end && identifier(t, 'from'));
|
|
183
|
+
if (from === -1 || tokens[from + 1]?.type !== 'string') {
|
|
184
|
+
module.uncertain.push('unsupported-syntax');
|
|
185
|
+
return end;
|
|
186
|
+
}
|
|
187
|
+
const specifier = tokens[from + 1].value;
|
|
188
|
+
module.runtime.push(specifier);
|
|
189
|
+
let i = start + 1;
|
|
190
|
+
if (identifier(tokens[i]) && !identifier(tokens[i], 'type')) {
|
|
191
|
+
module.localImports.set(tokens[i].value, { specifier, imported: 'default' });
|
|
192
|
+
module.requests.push({ specifier, symbol: 'default', local: tokens[i].value, mode: 'symbol' });
|
|
193
|
+
module.bindingTokenIndexes.add(i);
|
|
194
|
+
i++;
|
|
195
|
+
if (punct(tokens[i], ',')) i++;
|
|
196
|
+
}
|
|
197
|
+
if (punct(tokens[i], '*') && identifier(tokens[i + 1], 'as') && identifier(tokens[i + 2])) {
|
|
198
|
+
const local = tokens[i + 2].value;
|
|
199
|
+
module.namespaceImports.push({ specifier, local, tokenIndex: i + 2 });
|
|
200
|
+
module.bindingTokenIndexes.add(i + 2);
|
|
201
|
+
} else if (punct(tokens[i], '{')) {
|
|
202
|
+
const close = tokens.findIndex((t, n) => n > i && n < from && punct(t, '}'));
|
|
203
|
+
const entries = close === -1 ? null : namedEntries(tokens, i, close);
|
|
204
|
+
if (!entries) module.uncertain.push('unsupported-syntax');
|
|
205
|
+
else for (const entry of entries.filter((e) => !e.typeOnly)) {
|
|
206
|
+
module.localImports.set(entry.exported, { specifier, imported: entry.imported });
|
|
207
|
+
module.requests.push({ specifier, symbol: entry.imported, local: entry.exported, mode: 'symbol' });
|
|
208
|
+
const localIndex = tokens.findIndex((t, n) => n > i && n < close && identifier(t, entry.exported));
|
|
209
|
+
if (localIndex !== -1) module.bindingTokenIndexes.add(localIndex);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return end;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseExport(tokens, start, module) {
|
|
216
|
+
const end = statementEnd(tokens, start);
|
|
217
|
+
let i = start + 1;
|
|
218
|
+
if (identifier(tokens[i], 'type')) return end;
|
|
219
|
+
if (punct(tokens[i], '*')) {
|
|
220
|
+
i++;
|
|
221
|
+
let exported = null;
|
|
222
|
+
if (identifier(tokens[i], 'as') && identifier(tokens[i + 1])) { exported = tokens[i + 1].value; i += 2; }
|
|
223
|
+
if (!identifier(tokens[i], 'from') || tokens[i + 1]?.type !== 'string') module.uncertain.push('unsupported-syntax');
|
|
224
|
+
else if (exported) addExplicit(module, exported, { kind: 'namespace', specifier: tokens[i + 1].value });
|
|
225
|
+
else module.stars.push(tokens[i + 1].value);
|
|
226
|
+
if (tokens[i + 1]?.type === 'string') module.runtime.push(tokens[i + 1].value);
|
|
227
|
+
return end;
|
|
228
|
+
}
|
|
229
|
+
if (punct(tokens[i], '{')) {
|
|
230
|
+
const close = tokens.findIndex((t, n) => n > i && n < end && punct(t, '}'));
|
|
231
|
+
const entries = close === -1 ? null : namedEntries(tokens, i, close);
|
|
232
|
+
if (!entries) { module.uncertain.push('unsupported-syntax'); return end; }
|
|
233
|
+
const from = close + 1 < end && identifier(tokens[close + 1], 'from') && tokens[close + 2]?.type === 'string' ? tokens[close + 2].value : null;
|
|
234
|
+
if (from) module.runtime.push(from);
|
|
235
|
+
for (const entry of entries.filter((e) => !e.typeOnly)) {
|
|
236
|
+
addExplicit(module, entry.exported, from
|
|
237
|
+
? { kind: 'remote', specifier: from, imported: entry.imported }
|
|
238
|
+
: { kind: 'local', local: entry.imported });
|
|
239
|
+
}
|
|
240
|
+
return end;
|
|
241
|
+
}
|
|
242
|
+
if (identifier(tokens[i], 'default')) {
|
|
243
|
+
addExplicit(module, 'default', { kind: 'terminal', symbol: 'default' });
|
|
244
|
+
return end;
|
|
245
|
+
}
|
|
246
|
+
if (identifier(tokens[i], 'async') && identifier(tokens[i + 1], 'function') && identifier(tokens[i + 2])) {
|
|
247
|
+
addExplicit(module, tokens[i + 2].value, { kind: 'terminal', symbol: tokens[i + 2].value });
|
|
248
|
+
return end;
|
|
249
|
+
}
|
|
250
|
+
if (['const', 'let', 'var', 'function', 'class'].some((word) => identifier(tokens[i], word)) && identifier(tokens[i + 1])) {
|
|
251
|
+
addExplicit(module, tokens[i + 1].value, { kind: 'terminal', symbol: tokens[i + 1].value });
|
|
252
|
+
return end;
|
|
253
|
+
}
|
|
254
|
+
module.uncertain.push('unsupported-syntax');
|
|
255
|
+
return end;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function requireCall(tokens, i) {
|
|
259
|
+
if (!identifier(tokens[i], 'require') || !punct(tokens[i + 1], '(') || tokens[i + 2]?.type !== 'string' || !punct(tokens[i + 3], ')')) return null;
|
|
260
|
+
return { specifier: tokens[i + 2].value, end: i + 4 };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function dynamicImportCall(tokens, i) {
|
|
264
|
+
if (!identifier(tokens[i], 'import') || !punct(tokens[i + 1], '(') || tokens[i + 2]?.type !== 'string' || !punct(tokens[i + 3], ')')) return null;
|
|
265
|
+
return { specifier: tokens[i + 2].value, end: i + 4 };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function commonJsObjectEntries(tokens, open) {
|
|
269
|
+
const entries = [];
|
|
270
|
+
let i = open + 1;
|
|
271
|
+
while (i < tokens.length && !punct(tokens[i], '}')) {
|
|
272
|
+
if (punct(tokens[i], ',')) { i++; continue; }
|
|
273
|
+
const key = identifier(tokens[i]) || tokens[i]?.type === 'string' ? tokens[i].value : null;
|
|
274
|
+
if (!key) return null;
|
|
275
|
+
i++;
|
|
276
|
+
let local = key;
|
|
277
|
+
if (punct(tokens[i], ':')) {
|
|
278
|
+
i++;
|
|
279
|
+
if (!identifier(tokens[i])) return null;
|
|
280
|
+
local = tokens[i].value;
|
|
281
|
+
i++;
|
|
282
|
+
}
|
|
283
|
+
entries.push({ exported: key, local });
|
|
284
|
+
if (!punct(tokens[i], ',') && !punct(tokens[i], '}')) return null;
|
|
285
|
+
}
|
|
286
|
+
return punct(tokens[i], '}') ? entries : null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function isCommonJsExportsReference(tokens, i) {
|
|
290
|
+
if (identifier(tokens[i], 'exports')) return i + 1;
|
|
291
|
+
if (identifier(tokens[i], 'module') && punct(tokens[i + 1], '.') && identifier(tokens[i + 2], 'exports')) return i + 3;
|
|
292
|
+
return -1;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Generated CommonJS uses several export mutations that the deliberately
|
|
297
|
+
* small resolver does not model. Seeing one in a reached module must poison a
|
|
298
|
+
* negative result: otherwise a transpiler helper can hide a real re-export
|
|
299
|
+
* and turn uncertainty into `not-matched`.
|
|
300
|
+
*/
|
|
301
|
+
function hasUnsupportedCommonJsExportMutation(tokens) {
|
|
302
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
303
|
+
// Object.defineProperty(exports, "name", descriptor) and its
|
|
304
|
+
// module.exports variant can encode getters/re-exports.
|
|
305
|
+
if (identifier(tokens[i], 'Object') && punct(tokens[i + 1], '.')
|
|
306
|
+
&& ['defineProperty', 'defineProperties'].some((name) => identifier(tokens[i + 2], name))
|
|
307
|
+
&& punct(tokens[i + 3], '(') && isCommonJsExportsReference(tokens, i + 4) !== -1) return true;
|
|
308
|
+
|
|
309
|
+
// TypeScript/esbuild-style star forwarding. The helper's implementation
|
|
310
|
+
// is irrelevant; the call mutates the public export surface.
|
|
311
|
+
if (identifier(tokens[i]) && /(?:^|_)exportStar$/.test(tokens[i].value)
|
|
312
|
+
&& punct(tokens[i + 1], '(')) {
|
|
313
|
+
let depth = 0;
|
|
314
|
+
for (let n = i + 1; n < tokens.length; n++) {
|
|
315
|
+
if (punct(tokens[n], '(')) depth++;
|
|
316
|
+
else if (punct(tokens[n], ')')) {
|
|
317
|
+
depth--;
|
|
318
|
+
if (depth === 0) break;
|
|
319
|
+
} else if (depth > 0 && isCommonJsExportsReference(tokens, n) !== -1) return true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Computed export writes may be statically spelled today and become
|
|
324
|
+
// dynamic after another compiler transform. Do not partially interpret
|
|
325
|
+
// this family until the grammar is modelled as a whole.
|
|
326
|
+
const afterExports = isCommonJsExportsReference(tokens, i);
|
|
327
|
+
if (afterExports !== -1 && punct(tokens[afterExports], '[')) {
|
|
328
|
+
let depth = 1;
|
|
329
|
+
let n = afterExports + 1;
|
|
330
|
+
while (n < tokens.length && depth > 0) {
|
|
331
|
+
if (punct(tokens[n], '[')) depth++;
|
|
332
|
+
else if (punct(tokens[n], ']')) depth--;
|
|
333
|
+
n++;
|
|
334
|
+
}
|
|
335
|
+
if (depth === 0 && punct(tokens[n], '=')) return true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function parseCommonJsExports(tokens, module) {
|
|
342
|
+
if (hasUnsupportedCommonJsExportMutation(tokens)) module.uncertain.push('unsupported-syntax');
|
|
343
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
344
|
+
let exported;
|
|
345
|
+
let eq;
|
|
346
|
+
if (identifier(tokens[i], 'module') && punct(tokens[i + 1], '.') && identifier(tokens[i + 2], 'exports')) {
|
|
347
|
+
if (punct(tokens[i + 3], '.')) { exported = tokens[i + 4]?.value; eq = i + 5; }
|
|
348
|
+
else { exported = '*'; eq = i + 3; }
|
|
349
|
+
} else if (identifier(tokens[i], 'exports') && punct(tokens[i + 1], '.')) {
|
|
350
|
+
exported = tokens[i + 2]?.value;
|
|
351
|
+
eq = i + 3;
|
|
352
|
+
}
|
|
353
|
+
if (!exported || !punct(tokens[eq], '=')) continue;
|
|
354
|
+
const call = requireCall(tokens, eq + 1);
|
|
355
|
+
if (call) {
|
|
356
|
+
const imported = punct(tokens[call.end], '.') && identifier(tokens[call.end + 1]) ? tokens[call.end + 1].value : exported;
|
|
357
|
+
module.runtime.push(call.specifier);
|
|
358
|
+
if (exported === '*') module.cjsStars.push(call.specifier);
|
|
359
|
+
else addExplicit(module, exported, { kind: 'remote', specifier: call.specifier, imported });
|
|
360
|
+
} else if (exported === '*' && punct(tokens[eq + 1], '{')) {
|
|
361
|
+
const entries = commonJsObjectEntries(tokens, eq + 1);
|
|
362
|
+
if (!entries) module.uncertain.push('unsupported-syntax');
|
|
363
|
+
else for (const entry of entries) addExplicit(module, entry.exported, { kind: 'local', local: entry.local });
|
|
364
|
+
} else if (exported !== '*' && identifier(tokens[eq + 1])) {
|
|
365
|
+
addExplicit(module, exported, { kind: 'local', local: tokens[eq + 1].value });
|
|
366
|
+
} else if (exported !== '*') {
|
|
367
|
+
addExplicit(module, exported, { kind: 'terminal', symbol: exported });
|
|
368
|
+
} else {
|
|
369
|
+
module.uncertain.push('unsupported-syntax');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function destructuredNames(tokens, open, close) {
|
|
375
|
+
const out = [];
|
|
376
|
+
let i = open + 1;
|
|
377
|
+
while (i < close) {
|
|
378
|
+
if (punct(tokens[i], ',')) { i++; continue; }
|
|
379
|
+
if (!identifier(tokens[i])) return null;
|
|
380
|
+
const imported = tokens[i++].value;
|
|
381
|
+
let local = imported;
|
|
382
|
+
if (punct(tokens[i], ':')) {
|
|
383
|
+
i++;
|
|
384
|
+
if (!identifier(tokens[i])) return null;
|
|
385
|
+
local = tokens[i++].value;
|
|
386
|
+
}
|
|
387
|
+
out.push({ imported, local });
|
|
388
|
+
if (i < close && !punct(tokens[i], ',')) return null;
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function parseRuntimeImports(tokens, module) {
|
|
394
|
+
const consumed = new Set();
|
|
395
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
396
|
+
if (!['const', 'let', 'var'].some((word) => identifier(tokens[i], word))) continue;
|
|
397
|
+
if (punct(tokens[i + 1], '{')) {
|
|
398
|
+
const close = tokens.findIndex((t, n) => n > i + 1 && punct(t, '}'));
|
|
399
|
+
if (close === -1 || !punct(tokens[close + 1], '=')) continue;
|
|
400
|
+
let callStart = close + 2;
|
|
401
|
+
if (identifier(tokens[callStart], 'await')) callStart++;
|
|
402
|
+
const call = requireCall(tokens, callStart) ?? dynamicImportCall(tokens, callStart);
|
|
403
|
+
if (!call) continue;
|
|
404
|
+
const names = destructuredNames(tokens, i + 1, close);
|
|
405
|
+
if (!names) { module.uncertain.push('unsupported-syntax'); continue; }
|
|
406
|
+
module.runtime.push(call.specifier);
|
|
407
|
+
for (const name of names) {
|
|
408
|
+
module.requests.push({ specifier: call.specifier, symbol: name.imported, local: name.local, mode: 'symbol' });
|
|
409
|
+
module.localImports.set(name.local, { specifier: call.specifier, imported: name.imported });
|
|
410
|
+
const localIndex = tokens.findIndex((t, n) => n > i + 1 && n < close && identifier(t, name.local));
|
|
411
|
+
if (localIndex !== -1) module.bindingTokenIndexes.add(localIndex);
|
|
412
|
+
}
|
|
413
|
+
for (let n = callStart; n < call.end; n++) consumed.add(n);
|
|
414
|
+
} else if (identifier(tokens[i + 1]) && punct(tokens[i + 2], '=')) {
|
|
415
|
+
let callStart = i + 3;
|
|
416
|
+
if (identifier(tokens[callStart], 'await')) callStart++;
|
|
417
|
+
const call = requireCall(tokens, callStart) ?? dynamicImportCall(tokens, callStart);
|
|
418
|
+
if (!call) continue;
|
|
419
|
+
module.runtime.push(call.specifier);
|
|
420
|
+
const local = tokens[i + 1].value;
|
|
421
|
+
if (punct(tokens[call.end], '.') && identifier(tokens[call.end + 1])) {
|
|
422
|
+
const imported = tokens[call.end + 1].value;
|
|
423
|
+
module.requests.push({ specifier: call.specifier, symbol: imported, local, mode: 'symbol' });
|
|
424
|
+
module.localImports.set(local, { specifier: call.specifier, imported });
|
|
425
|
+
consumed.add(call.end);
|
|
426
|
+
consumed.add(call.end + 1);
|
|
427
|
+
} else {
|
|
428
|
+
module.namespaceImports.push({ specifier: call.specifier, local, tokenIndex: i + 1 });
|
|
429
|
+
}
|
|
430
|
+
module.bindingTokenIndexes.add(i + 1);
|
|
431
|
+
for (let n = callStart; n < call.end; n++) consumed.add(n);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
435
|
+
if (consumed.has(i)) continue;
|
|
436
|
+
const call = requireCall(tokens, i) ?? dynamicImportCall(tokens, i);
|
|
437
|
+
if (!call) continue;
|
|
438
|
+
module.runtime.push(call.specifier);
|
|
439
|
+
if (punct(tokens[call.end], '.') && identifier(tokens[call.end + 1])) module.requests.push({ specifier: call.specifier, symbol: tokens[call.end + 1].value, mode: 'symbol' });
|
|
440
|
+
else module.requests.push({ specifier: call.specifier, mode: 'runtime' });
|
|
441
|
+
i = call.end - 1;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function namespaceRequests(tokens, module) {
|
|
446
|
+
for (const ns of module.namespaceImports) {
|
|
447
|
+
const names = new Set();
|
|
448
|
+
let opaque = false;
|
|
449
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
450
|
+
if (!identifier(tokens[i], ns.local) || i === ns.tokenIndex || module.bindingTokenIndexes.has(i)) continue;
|
|
451
|
+
if (punct(tokens[i + 1], '.') && identifier(tokens[i + 2])) { names.add(tokens[i + 2].value); continue; }
|
|
452
|
+
if (punct(tokens[i + 1], '[') && tokens[i + 2]?.type === 'string' && punct(tokens[i + 3], ']')) { names.add(tokens[i + 2].value); continue; }
|
|
453
|
+
if (punct(tokens[i - 1], '=') && punct(tokens[i - 2], '}')) {
|
|
454
|
+
let open = i - 2;
|
|
455
|
+
while (open >= 0 && !punct(tokens[open], '{')) open--;
|
|
456
|
+
const entries = open >= 0 ? destructuredNames(tokens, open, i - 2) : null;
|
|
457
|
+
if (entries) { for (const entry of entries) names.add(entry.imported); continue; }
|
|
458
|
+
}
|
|
459
|
+
opaque = true;
|
|
460
|
+
}
|
|
461
|
+
if (opaque || names.size === 0) module.requests.push({ specifier: ns.specifier, mode: 'unknown-namespace', local: ns.local });
|
|
462
|
+
else for (const symbol of names) module.requests.push({ specifier: ns.specifier, symbol, local: ns.local, mode: 'symbol' });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Parse only the static module forms the resolver promises; unsupported reached forms stay explicit. */
|
|
467
|
+
export function parseModuleSource(source) {
|
|
468
|
+
const tokens = tokenizeModule(source);
|
|
469
|
+
const module = {
|
|
470
|
+
tokens,
|
|
471
|
+
requests: [], runtime: [], explicit: new Map(), stars: [], cjsStars: [],
|
|
472
|
+
localImports: new Map(), namespaceImports: [], bindingTokenIndexes: new Set(), uncertain: [],
|
|
473
|
+
};
|
|
474
|
+
let braces = 0;
|
|
475
|
+
for (let i = 0; i < tokens.length;) {
|
|
476
|
+
if (punct(tokens[i], '{')) { braces++; i++; continue; }
|
|
477
|
+
if (punct(tokens[i], '}')) { braces = Math.max(0, braces - 1); i++; continue; }
|
|
478
|
+
// `import.meta` is an expression, not a static import declaration.
|
|
479
|
+
if (braces === 0 && identifier(tokens[i], 'import') && !punct(tokens[i + 1], '(') && !punct(tokens[i + 1], '.')) { i = parseStaticImport(tokens, i, module); continue; }
|
|
480
|
+
if (braces === 0 && identifier(tokens[i], 'export')) { i = parseExport(tokens, i, module); continue; }
|
|
481
|
+
i++;
|
|
482
|
+
}
|
|
483
|
+
parseCommonJsExports(tokens, module);
|
|
484
|
+
parseRuntimeImports(tokens, module);
|
|
485
|
+
namespaceRequests(tokens, module);
|
|
486
|
+
for (const [name, origins] of module.explicit) {
|
|
487
|
+
module.explicit.set(name, origins.map((origin) => {
|
|
488
|
+
if (origin.kind !== 'local') return origin;
|
|
489
|
+
const imported = module.localImports.get(origin.local);
|
|
490
|
+
return imported ? { kind: 'remote', ...imported } : { kind: 'terminal', symbol: origin.local };
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
module.runtime = [...new Set(module.runtime)];
|
|
494
|
+
module.stars = [...new Set(module.stars)];
|
|
495
|
+
module.cjsStars = [...new Set(module.cjsStars)];
|
|
496
|
+
module.uncertain = [...new Set(module.uncertain)];
|
|
497
|
+
return module;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function inside(root, file) {
|
|
501
|
+
const rel = relative(root, file);
|
|
502
|
+
return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function candidateFile(path) {
|
|
506
|
+
try {
|
|
507
|
+
return statSync(path).isFile() ? realpathSync(path) : null;
|
|
508
|
+
} catch {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function expandBase(base) {
|
|
514
|
+
const extension = extname(base);
|
|
515
|
+
const exact = candidateFile(base);
|
|
516
|
+
if (exact) return [exact];
|
|
517
|
+
if (SCRIPT_FALLBACKS[extension]) {
|
|
518
|
+
const stem = base.slice(0, -extension.length);
|
|
519
|
+
return SCRIPT_FALLBACKS[extension].map((ext) => candidateFile(stem + ext)).filter(Boolean);
|
|
520
|
+
}
|
|
521
|
+
if (extension) return [];
|
|
522
|
+
return [...SOURCE_EXTENSIONS.map((ext) => candidateFile(base + ext)), ...SOURCE_EXTENSIONS.map((ext) => candidateFile(join(base, `index${ext}`)))].filter(Boolean);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function aliasMatches(specifier, aliases) {
|
|
526
|
+
const matched = aliases.filter((rule) => rule.wildcard
|
|
527
|
+
? specifier.startsWith(rule.prefix) && specifier.endsWith(rule.suffix ?? '') && specifier.length >= rule.prefix.length + (rule.suffix?.length ?? 0)
|
|
528
|
+
: specifier === rule.prefix);
|
|
529
|
+
const exact = matched.filter((rule) => !rule.wildcard);
|
|
530
|
+
if (exact.length) return exact;
|
|
531
|
+
const score = Math.max(-1, ...matched.map((rule) => rule.prefix.length + (rule.suffix?.length ?? 0)));
|
|
532
|
+
return matched.filter((rule) => rule.prefix.length + (rule.suffix?.length ?? 0) === score);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function issue(code, file, specifier) {
|
|
536
|
+
return { code, file, ...(specifier ? { specifier } : {}) };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function createImportResolver(projectDir, { aliases = [], configFiles = [], limits = {} } = {}) {
|
|
540
|
+
const root = realpathSync(resolve(projectDir));
|
|
541
|
+
const cap = { ...IMPORT_LIMITS, ...limits };
|
|
542
|
+
const parseCache = new Map();
|
|
543
|
+
|
|
544
|
+
const rel = (file) => relative(root, file).split(sep).join('/');
|
|
545
|
+
const display = (file) => inside(root, file) ? rel(file) : file;
|
|
546
|
+
|
|
547
|
+
function resolveSpecifier(fromFile, specifier) {
|
|
548
|
+
const aliasRules = aliasMatches(specifier, aliases);
|
|
549
|
+
let bases;
|
|
550
|
+
let usedAlias = false;
|
|
551
|
+
if (specifier.startsWith('.')) bases = [resolve(dirname(fromFile), specifier)];
|
|
552
|
+
else if (aliasRules.length) {
|
|
553
|
+
usedAlias = true;
|
|
554
|
+
bases = [];
|
|
555
|
+
for (const rule of aliasRules) {
|
|
556
|
+
const suffix = rule.suffix ?? '';
|
|
557
|
+
const star = rule.wildcard ? specifier.slice(rule.prefix.length, specifier.length - suffix.length) : '';
|
|
558
|
+
for (const target of rule.targets ?? []) {
|
|
559
|
+
const expanded = expandBase(target.replace('*', star));
|
|
560
|
+
if (expanded.length) { bases.push(...expanded); break; } // ordered TS-path fallback
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const unique = [...new Set(bases)];
|
|
564
|
+
if (unique.length > 1) return { status: 'ambiguous', candidates: unique, usedAlias };
|
|
565
|
+
if (unique.length === 1) return { status: 'resolved', file: unique[0], usedAlias };
|
|
566
|
+
return { status: 'missing', candidates: [], usedAlias };
|
|
567
|
+
} else return { status: 'external', candidates: [], usedAlias: false };
|
|
568
|
+
const candidates = [...new Set(bases.flatMap(expandBase))];
|
|
569
|
+
if (candidates.length > 1) return { status: 'ambiguous', candidates, usedAlias };
|
|
570
|
+
if (candidates.length === 1) return { status: 'resolved', file: candidates[0], usedAlias };
|
|
571
|
+
return { status: 'missing', candidates: [], usedAlias };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function context() {
|
|
575
|
+
return { modules: new Set(), edges: 0, bytes: 0, dependencies: new Set(), issues: [], active: new Set() };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function addIssue(ctx, code, file, specifier) {
|
|
579
|
+
const value = issue(code, display(file), specifier);
|
|
580
|
+
if (!ctx.issues.some((entry) => JSON.stringify(entry) === JSON.stringify(value))) ctx.issues.push(value);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function readModule(file, ctx) {
|
|
584
|
+
if (!inside(root, file)) return null;
|
|
585
|
+
if (!ctx.modules.has(file)) {
|
|
586
|
+
if (ctx.modules.size >= cap.maxModules) { addIssue(ctx, 'graph-limit', file); return null; }
|
|
587
|
+
let size;
|
|
588
|
+
try { size = statSync(file).size; } catch { addIssue(ctx, 'unreadable-module', file); return null; }
|
|
589
|
+
if (size > cap.maxFileBytes || ctx.bytes + size > cap.maxTotalBytes) { addIssue(ctx, 'graph-limit', file); return null; }
|
|
590
|
+
ctx.modules.add(file);
|
|
591
|
+
ctx.bytes += size;
|
|
592
|
+
}
|
|
593
|
+
if (parseCache.has(file)) return parseCache.get(file);
|
|
594
|
+
try {
|
|
595
|
+
const parsed = parseModuleSource(readFileSync(file, 'utf8'));
|
|
596
|
+
parseCache.set(file, parsed);
|
|
597
|
+
return parsed;
|
|
598
|
+
} catch {
|
|
599
|
+
addIssue(ctx, 'unsupported-syntax', file);
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function step(ctx, from, specifier) {
|
|
605
|
+
if (++ctx.edges > cap.maxEdges) { addIssue(ctx, 'graph-limit', from, specifier); return { status: 'indeterminate' }; }
|
|
606
|
+
// A bare specifier is resolved only after consulting alias configuration.
|
|
607
|
+
// Record that negative consultation too: adding a previously absent alias
|
|
608
|
+
// to an existing config must invalidate an earlier `not-matched` result.
|
|
609
|
+
if (!specifier.startsWith('.')) for (const file of configFiles) ctx.dependencies.add(resolve(root, file));
|
|
610
|
+
const resolved = resolveSpecifier(from, specifier);
|
|
611
|
+
if (resolved.status === 'ambiguous') {
|
|
612
|
+
addIssue(ctx, 'ambiguous-resolution', from, specifier);
|
|
613
|
+
return { status: 'indeterminate', candidates: resolved.candidates };
|
|
614
|
+
}
|
|
615
|
+
if (resolved.status === 'missing') {
|
|
616
|
+
addIssue(ctx, 'unreadable-module', from, specifier);
|
|
617
|
+
return { status: 'indeterminate' };
|
|
618
|
+
}
|
|
619
|
+
return resolved.status === 'resolved' && inside(root, resolved.file) ? resolved : { status: 'external' };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function origin(file, symbol, target, ctx, depth, path) {
|
|
623
|
+
if (depth > cap.maxDepth) { addIssue(ctx, 'graph-limit', file); return { origins: new Map(), cycle: false }; }
|
|
624
|
+
if (file === target) return { origins: new Map([[`${file}#${symbol}`, [...path, file]]]), cycle: false };
|
|
625
|
+
const key = `${file}\0${symbol}`;
|
|
626
|
+
if (ctx.active.has(key)) return { origins: new Map(), cycle: true };
|
|
627
|
+
const parsed = readModule(file, ctx);
|
|
628
|
+
if (!parsed) return { origins: new Map(), cycle: false };
|
|
629
|
+
ctx.dependencies.add(file);
|
|
630
|
+
ctx.active.add(key);
|
|
631
|
+
let origins = new Map();
|
|
632
|
+
let cycle = false;
|
|
633
|
+
const explicit = parsed.explicit.get(symbol);
|
|
634
|
+
const candidates = explicit ?? (symbol === 'default' ? [] : [
|
|
635
|
+
...parsed.stars.map((specifier) => ({ kind: 'remote', specifier, imported: symbol })),
|
|
636
|
+
...parsed.cjsStars.map((specifier) => ({ kind: 'remote', specifier, imported: symbol })),
|
|
637
|
+
]);
|
|
638
|
+
for (const candidate of candidates) {
|
|
639
|
+
if (candidate.kind === 'terminal') {
|
|
640
|
+
origins.set(`${file}#${candidate.symbol}`, [...path, file]);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const next = step(ctx, file, candidate.specifier);
|
|
644
|
+
if (next.status === 'indeterminate') continue;
|
|
645
|
+
if (next.status !== 'resolved') continue;
|
|
646
|
+
if (candidate.kind === 'namespace') {
|
|
647
|
+
if (next.file === target) origins.set(`${target}#*`, [...path, file, target]);
|
|
648
|
+
else {
|
|
649
|
+
const reached = runtimeReach(next.file, target, ctx, depth + 1, [...path, file], new Set());
|
|
650
|
+
if (Array.isArray(reached)) origins.set(`${target}#*`, reached);
|
|
651
|
+
else if (reached === false) origins.set(`${next.file}#*`, [...path, file, next.file]);
|
|
652
|
+
}
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const found = origin(next.file, candidate.imported, target, ctx, depth + 1, [...path, file]);
|
|
656
|
+
for (const [id, witness] of found.origins) origins.set(id, witness);
|
|
657
|
+
cycle ||= found.cycle;
|
|
658
|
+
}
|
|
659
|
+
if (!explicit?.length && candidates.length === 0 && parsed.uncertain.length) addIssue(ctx, 'unsupported-syntax', file);
|
|
660
|
+
ctx.active.delete(key);
|
|
661
|
+
return { origins, cycle };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function runtimeReach(file, target, ctx, depth, path, visited) {
|
|
665
|
+
if (depth > cap.maxDepth) { addIssue(ctx, 'graph-limit', file); return null; }
|
|
666
|
+
if (file === target) return [...path, file];
|
|
667
|
+
if (visited.has(file)) return false;
|
|
668
|
+
visited.add(file);
|
|
669
|
+
const parsed = readModule(file, ctx);
|
|
670
|
+
if (!parsed) return null;
|
|
671
|
+
ctx.dependencies.add(file);
|
|
672
|
+
let unknown = parsed.uncertain.length > 0;
|
|
673
|
+
for (const specifier of parsed.runtime) {
|
|
674
|
+
const next = step(ctx, file, specifier);
|
|
675
|
+
if (next.status === 'indeterminate') { unknown = true; continue; }
|
|
676
|
+
if (next.status !== 'resolved') continue;
|
|
677
|
+
const found = runtimeReach(next.file, target, ctx, depth + 1, [...path, file], visited);
|
|
678
|
+
if (Array.isArray(found)) return found;
|
|
679
|
+
if (found === null) unknown = true;
|
|
680
|
+
}
|
|
681
|
+
return unknown ? null : false;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function evaluateRequest(importer, request, target, ctx) {
|
|
685
|
+
const next = step(ctx, importer, request.specifier);
|
|
686
|
+
if (next.status === 'indeterminate') {
|
|
687
|
+
const outcomes = (next.candidates ?? []).map((file) => {
|
|
688
|
+
if (request.mode === 'unknown-namespace') {
|
|
689
|
+
const reached = runtimeReach(file, target, ctx, 1, [importer], new Set());
|
|
690
|
+
return { status: Array.isArray(reached) ? 'unknown' : reached === null ? 'unknown' : 'other', paths: [] };
|
|
691
|
+
}
|
|
692
|
+
if (file === target) return { status: 'match', paths: [[importer, target]] };
|
|
693
|
+
if (request.mode === 'runtime') {
|
|
694
|
+
const reached = runtimeReach(file, target, ctx, 1, [importer], new Set());
|
|
695
|
+
return { status: Array.isArray(reached) ? 'match' : reached === null ? 'unknown' : 'other', paths: Array.isArray(reached) ? [reached] : [] };
|
|
696
|
+
}
|
|
697
|
+
const result = origin(file, request.symbol, target, ctx, 1, [importer]);
|
|
698
|
+
const witnesses = [...result.origins].filter(([id]) => id.startsWith(`${target}#`)).map(([, witness]) => witness);
|
|
699
|
+
return { status: witnesses.length === 1 && result.origins.size === 1 ? 'match' : result.origins.size > 1 || result.cycle ? 'unknown' : 'other', paths: witnesses };
|
|
700
|
+
});
|
|
701
|
+
return outcomes.length && outcomes.every((value) => value.status === 'match')
|
|
702
|
+
? { status: 'matched', paths: outcomes.flatMap((value) => value.paths) }
|
|
703
|
+
: { status: 'indeterminate', paths: [] };
|
|
704
|
+
}
|
|
705
|
+
if (next.status !== 'resolved') return { status: 'not-matched', paths: [] };
|
|
706
|
+
if (request.mode === 'unknown-namespace') {
|
|
707
|
+
const reached = runtimeReach(next.file, target, ctx, 1, [importer], new Set());
|
|
708
|
+
return Array.isArray(reached) || reached === null ? { status: 'indeterminate', paths: [] } : { status: 'not-matched', paths: [] };
|
|
709
|
+
}
|
|
710
|
+
if (next.file === target) return { status: 'matched', paths: [[importer, target]] };
|
|
711
|
+
if (request.mode === 'runtime') {
|
|
712
|
+
const reached = runtimeReach(next.file, target, ctx, 1, [importer], new Set());
|
|
713
|
+
return Array.isArray(reached) ? { status: 'matched', paths: [reached] } : { status: reached === null ? 'indeterminate' : 'not-matched', paths: [] };
|
|
714
|
+
}
|
|
715
|
+
const found = origin(next.file, request.symbol, target, ctx, 1, [importer]);
|
|
716
|
+
if (found.origins.size > 1) {
|
|
717
|
+
addIssue(ctx, 'ambiguous-resolution', next.file, request.symbol);
|
|
718
|
+
return { status: 'indeterminate', paths: [] };
|
|
719
|
+
}
|
|
720
|
+
if (found.origins.size === 1) {
|
|
721
|
+
const [id, witness] = [...found.origins][0];
|
|
722
|
+
return id.startsWith(`${target}#`) ? { status: 'matched', paths: [witness] } : { status: 'not-matched', paths: [] };
|
|
723
|
+
}
|
|
724
|
+
if (found.cycle) {
|
|
725
|
+
addIssue(ctx, 'cyclic-export-without-origin', next.file, request.symbol);
|
|
726
|
+
return { status: 'indeterminate', paths: [] };
|
|
727
|
+
}
|
|
728
|
+
return ctx.issues.length ? { status: 'indeterminate', paths: [] } : { status: 'not-matched', paths: [] };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function relation(absImporter, targetRel) {
|
|
732
|
+
let importer;
|
|
733
|
+
try { importer = realpathSync(resolve(absImporter)); }
|
|
734
|
+
catch { return { status: 'indeterminate', bindings: [], paths: [], dependencies: [], reason: 'unreadable-module', issues: [issue('unreadable-module', display(resolve(absImporter)))] }; }
|
|
735
|
+
const targetPath = resolve(root, targetRel);
|
|
736
|
+
if (!existsSync(targetPath)) return { status: 'indeterminate', bindings: [], paths: [], dependencies: [], reason: 'unreadable-module', issues: [issue('unreadable-module', targetRel)] };
|
|
737
|
+
const target = realpathSync(targetPath);
|
|
738
|
+
const ctx = context();
|
|
739
|
+
const parsed = readModule(importer, ctx);
|
|
740
|
+
if (!parsed) return finish('indeterminate', [], [], ctx);
|
|
741
|
+
if (parsed.uncertain.length) addIssue(ctx, 'unsupported-syntax', importer);
|
|
742
|
+
const paths = [];
|
|
743
|
+
const bindings = new Set();
|
|
744
|
+
let indeterminate = parsed.uncertain.length > 0;
|
|
745
|
+
for (const request of parsed.requests) {
|
|
746
|
+
const result = evaluateRequest(importer, request, target, ctx);
|
|
747
|
+
if (result.status === 'matched') {
|
|
748
|
+
paths.push(...result.paths);
|
|
749
|
+
if (request.local) bindings.add(request.local);
|
|
750
|
+
} else if (result.status === 'indeterminate') indeterminate = true;
|
|
751
|
+
}
|
|
752
|
+
if (paths.length && !parsed.uncertain.length) return finish('matched', [...bindings], paths, ctx);
|
|
753
|
+
return finish(indeterminate ? 'indeterminate' : 'not-matched', [], [], ctx);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function finish(status, bindings, paths, ctx) {
|
|
757
|
+
const normalizedPaths = [];
|
|
758
|
+
const seen = new Set();
|
|
759
|
+
for (const path of paths) {
|
|
760
|
+
const normalized = path.map(display);
|
|
761
|
+
const key = normalized.join('\0');
|
|
762
|
+
if (!seen.has(key) && normalizedPaths.length < cap.maxWitnessPaths) { seen.add(key); normalizedPaths.push(normalized); }
|
|
763
|
+
}
|
|
764
|
+
const dependencies = [...ctx.dependencies]
|
|
765
|
+
.filter((file) => inside(root, file))
|
|
766
|
+
.map(rel)
|
|
767
|
+
.sort();
|
|
768
|
+
const issues = [...ctx.issues].sort((a, b) => `${a.file}\0${a.code}\0${a.specifier ?? ''}`.localeCompare(`${b.file}\0${b.code}\0${b.specifier ?? ''}`));
|
|
769
|
+
return {
|
|
770
|
+
status,
|
|
771
|
+
bindings: [...new Set(bindings)].sort(),
|
|
772
|
+
paths: normalizedPaths.sort((a, b) => a.join('\0').localeCompare(b.join('\0'))),
|
|
773
|
+
dependencies,
|
|
774
|
+
...(status === 'indeterminate' ? { reason: issues[0]?.code ?? 'unsupported-syntax' } : {}),
|
|
775
|
+
...(issues.length ? { issues } : {}),
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return { relation, resolveSpecifier };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** Convenience for callers that do not need to share the resolver's parse cache. */
|
|
783
|
+
export function fileImportRelation(projectDir, absImporter, targetRel, options) {
|
|
784
|
+
return createImportResolver(projectDir, options).relation(absImporter, targetRel);
|
|
785
|
+
}
|