perf(#313): dedupe intel export extraction with a Set (#393)

intelExtractExports deduped export names with `if (!arr.includes(x)) arr.push(x)`
across ~8 extraction loops (one doubly-nested over an export block) — O(n^2).
Accumulate into Sets (add/has/size) and materialize to an array once at return.
Set dedups by value and preserves insertion order, so the returned export list
and its first-seen order are identical. Adds behavior-lock tests for dedup + order.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-27 20:21:39 -04:00
committed by GitHub
parent d77170a25d
commit 269ed3e3b5
3 changed files with 120 additions and 16 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 313
---
Dedupe extracted exports with a Set instead of O(n^2) array.includes scans in intelExtractExports (#313).

View File

@@ -509,7 +509,7 @@ function intelExtractExports(filePath) {
if (content === null) {
return { file: filePath, exports: [], method: 'none' };
}
let exports = [];
const exports = new Set();
let method = 'none';
// Try module.exports = { ... } pattern (handle multi-line)
@@ -537,7 +537,7 @@ function intelExtractExports(filePath) {
// Match identifier at start of line (before comma, colon, end of line)
const keyMatch = trimmed.match(/^(\w+)\s*[,}:]/) || trimmed.match(/^(\w+)$/);
if (keyMatch) {
exports.push(keyMatch[1]);
exports.add(keyMatch[1]);
}
}
}
@@ -546,46 +546,46 @@ function intelExtractExports(filePath) {
const individualPattern = /^exports\.(\w+)\s*=/gm;
let im;
while ((im = individualPattern.exec(content)) !== null) {
if (!exports.includes(im[1])) {
exports.push(im[1]);
if (!exports.has(im[1])) {
exports.add(im[1]);
if (method === 'none') method = 'exports.X';
}
}
const hadCjs = exports.length > 0;
const hadCjs = exports.size > 0;
// ESM patterns
const esmExports = [];
const esmExports = new Set();
// export default function X / export default class X
const defaultNamedPattern = /^export\s+default\s+(?:function|class)\s+(\w+)/gm;
let em;
while ((em = defaultNamedPattern.exec(content)) !== null) {
if (!esmExports.includes(em[1])) esmExports.push(em[1]);
esmExports.add(em[1]);
}
// export default (without named function/class)
const defaultAnonPattern = /^export\s+default\s+(?!function\s|class\s)/gm;
if (defaultAnonPattern.test(content) && esmExports.length === 0) {
if (!esmExports.includes('default')) esmExports.push('default');
if (defaultAnonPattern.test(content) && esmExports.size === 0) {
esmExports.add('default');
}
// export function X( / export async function X(
const exportFnPattern = /^export\s+(?:async\s+)?function\s+(\w+)\s*\(/gm;
while ((em = exportFnPattern.exec(content)) !== null) {
if (!esmExports.includes(em[1])) esmExports.push(em[1]);
esmExports.add(em[1]);
}
// export const X = / export let X = / export var X =
const exportVarPattern = /^export\s+(?:const|let|var)\s+(\w+)\s*=/gm;
while ((em = exportVarPattern.exec(content)) !== null) {
if (!esmExports.includes(em[1])) esmExports.push(em[1]);
esmExports.add(em[1]);
}
// export class X
const exportClassPattern = /^export\s+class\s+(\w+)/gm;
while ((em = exportClassPattern.exec(content)) !== null) {
if (!esmExports.includes(em[1])) esmExports.push(em[1]);
esmExports.add(em[1]);
}
// export { X, Y, Z } — strip "as alias" parts
@@ -597,24 +597,24 @@ function intelExtractExports(filePath) {
if (!trimmed) continue;
// "foo as bar" -> extract "foo"
const name = trimmed.split(/\s+as\s+/)[0].trim();
if (name && !esmExports.includes(name)) esmExports.push(name);
if (name) esmExports.add(name);
}
}
// Merge ESM exports into the result
for (const e of esmExports) {
if (!exports.includes(e)) exports.push(e);
exports.add(e);
}
// Determine method
const hadEsm = esmExports.length > 0;
const hadEsm = esmExports.size > 0;
if (hadCjs && hadEsm) {
method = 'mixed';
} else if (hadEsm && !hadCjs) {
method = 'esm';
}
return { file: filePath, exports, method };
return { file: filePath, exports: [...exports], method };
}
// ─── Exports ─────────────────────────────────────────────────────────────────

View File

@@ -558,6 +558,105 @@ describe('intelExtractExports', () => {
assert.deepStrictEqual(result.exports, []);
assert.strictEqual(result.method, 'none');
});
// ── Behavior-lock: dedup + order (green before AND after Set conversion) ──
test('dedup: duplicate exports.X assignments yield each name exactly once', () => {
// exports.foo appears twice — result must contain 'foo' exactly once
const filePath = path.join(tmpDir, 'dedup-exports-x.cjs');
fs.writeFileSync(filePath, [
"'use strict';",
'exports.foo = 1;',
'exports.bar = 2;',
'exports.foo = 3;',
].join('\n'), 'utf8');
const result = intelExtractExports(filePath);
assert.strictEqual(result.method, 'exports.X');
assert.deepStrictEqual(result.exports, ['foo', 'bar']);
});
test('order: CJS exports.X preserves first-seen insertion order', () => {
// Names appear in source order: charlie, alpha, bravo
const filePath = path.join(tmpDir, 'order-cjs.cjs');
fs.writeFileSync(filePath, [
"'use strict';",
'exports.charlie = 1;',
'exports.alpha = 2;',
'exports.bravo = 3;',
].join('\n'), 'utf8');
const result = intelExtractExports(filePath);
assert.strictEqual(result.method, 'exports.X');
assert.deepStrictEqual(result.exports, ['charlie', 'alpha', 'bravo']);
});
test('dedup: ESM export block with repeated name yields name exactly once', () => {
// export { foo, foo } — foo must appear once
const filePath = path.join(tmpDir, 'dedup-esm-block.mjs');
fs.writeFileSync(filePath, [
'function foo() {}',
'export { foo, foo };',
].join('\n'), 'utf8');
const result = intelExtractExports(filePath);
assert.strictEqual(result.method, 'esm');
assert.deepStrictEqual(result.exports, ['foo']);
});
test('merge order: CJS exports appear before ESM exports, each name once', () => {
// exports.X = CJS side; export function / export const = ESM side
// Expected order: CJS-first then ESM additions
const filePath = path.join(tmpDir, 'merge-order.mjs');
fs.writeFileSync(filePath, [
"exports.cjsFirst = 1;",
"export function esmSecond() {}",
"export const esmThird = 3;",
].join('\n'), 'utf8');
const result = intelExtractExports(filePath);
assert.strictEqual(result.method, 'mixed');
assert.deepStrictEqual(result.exports, ['cjsFirst', 'esmSecond', 'esmThird']);
});
test('export default collapse: only export default (anon) yields ["default"]', () => {
// A file with only `export default <value>` — no named exports, no default fn/class
// The collapse guard (esmExports.length === 0 at time of check) produces ["default"]
const filePath = path.join(tmpDir, 'default-only.mjs');
fs.writeFileSync(filePath, 'export default 42;', 'utf8');
const result = intelExtractExports(filePath);
assert.strictEqual(result.method, 'esm');
assert.deepStrictEqual(result.exports, ['default']);
});
test('export default collapse: export default fn + named exports — no "default" collapse', () => {
// export default function myFunc() {} → myFunc is extracted (named default fn)
// export const named → also extracted
// "default" literal does NOT appear because esmExports is not empty when anon-default check runs
const filePath = path.join(tmpDir, 'default-fn-plus-named.mjs');
fs.writeFileSync(filePath, [
'export default function myFunc() {}',
'export const named = 1;',
].join('\n'), 'utf8');
const result = intelExtractExports(filePath);
assert.strictEqual(result.method, 'esm');
assert.deepStrictEqual(result.exports, ['myFunc', 'named']);
});
test('return shape: exports is a plain Array (callers use .includes/.length)', () => {
const filePath = path.join(tmpDir, 'shape-check.cjs');
fs.writeFileSync(filePath, [
"'use strict';",
'exports.foo = 1;',
].join('\n'), 'utf8');
const result = intelExtractExports(filePath);
assert.ok(Array.isArray(result.exports), 'exports must be a plain Array');
assert.ok('file' in result, 'result must have file field');
assert.ok('method' in result, 'result must have method field');
});
});
// ─── CLI routing via gsd-tools ──────────────────────────────────────────────