diff --git a/packages/adapters/python/src/usage/imports.ts b/packages/adapters/python/src/usage/imports.ts index 6079306f..65b7d1b8 100644 --- a/packages/adapters/python/src/usage/imports.ts +++ b/packages/adapters/python/src/usage/imports.ts @@ -388,9 +388,26 @@ function importlibAliases( for (let s = scope; s >= 0; s = parent[s]!) if (s === of) return true; return false; }; + // Identifier -> statements that mention it. A statement can only rebind a + // name it contains, so each alias checks just those statements instead of + // rescanning the whole file once per alias (quadratic on many aliases). + const mentions = new Map(); + const identifier = /[\p{XID_Start}_][\p{XID_Continue}]*/gu; + texts.forEach((raw, index) => { + const seenHere = new Set(); + for (const form of new Set([raw, raw.normalize("NFKC")])) { + for (const m of form.matchAll(identifier)) seenHere.add(m[0]); + } + for (const word of seenHere) { + const list = mentions.get(word); + if (list) list.push(index); + else mentions.set(word, [index]); + } + }); for (const [scope, names] of bound) { for (const name of [...names.keys()]) { - const dropped = texts.some((raw, index) => { + const dropped = (mentions.get(name) ?? []).some((index) => { + const raw = texts[index]!; if (!inside(scopeOf[index]!, scope)) return false; const own = defs.get(index)?.filter((d) => d.name === name) ?? []; const parsed = statementParts(raw); diff --git a/packages/adapters/python/src/usage/usage.test.ts b/packages/adapters/python/src/usage/usage.test.ts index a76ca59a..680e4afb 100644 --- a/packages/adapters/python/src/usage/usage.test.ts +++ b/packages/adapters/python/src/usage/usage.test.ts @@ -124,6 +124,23 @@ describe("extractPythonImports", () => { ); }); + it("scans many importlib aliases in linear time", () => { + const count = 1000; + const source = + Array.from({ length: count }, (_, i) => `import importlib as il${i}`).join("\n") + + '\nil0.import_module("yaml")\nil999.import_module("toml")\n' + + 'il5 = other\nil5.import_module("dropped")\n'; + const started = performance.now(); + const dynamic = extractPythonImports(source) + .imports.filter((i) => i.form === "dynamic") + .map((i) => i.module); + const elapsed = performance.now() - started; + // Correctness first: a rebound alias is still dropped, the others credited. + assert.deepEqual(dynamic, ["yaml", "toml"]); + // 1000 aliases took ~5s when every alias rescanned every statement. + assert.ok(elapsed < 1500, `took ${Math.round(elapsed)}ms`); + }); + it("credits aliased importlib, import_module and __import__ calls", () => { const dyn = (src: string) => extractPythonImports(src)