Skip to content

Commit ed952eb

Browse files
proggeramlugRalph
andauthored
fix(compile): #5918 — import_function_prefixes collides on exported name across unrelated imports (#5919)
import_function_prefixes (imported name -> source module prefix, consumed by ExternFuncRef codegen to form perry_fn_<prefix>__<name>) was populated by inserting BOTH the exported/origin name and the local alias for every renamed Named import. Per the #35/#321 precedent in the same function, a renamed import's ExternFuncRef in the HIR always carries the LOCAL name (unique per import site) -- the exported_name insert only ever mattered in the no-rename case (local == exported). Running it unconditionally meant: when two DIFFERENT import statements in the same file rename to different locals from modules whose ORIGIN export names happen to collide (common with short, minifier-style names -- "a"/"b"/"c" -- which is exactly what esbuild's chunk-splitting produces), the second insert silently overwrote the first's entry. The overwritten entry could be a completely unrelated LOCAL alias from an earlier import, repointing that alias's ExternFuncRef at the wrong module entirely. Confirmed via a real-world source compile of sst/opencode (compilePackages: ["*"]): remeda's real dist/chunk-*.js build output has this exact shape, and nearly every remeda function transitively imports the four affected chunks, so this single bug blocked the entire package. Fix: only insert under exported_name in the local_name == exported_name case; the aliased case inserts under local_name only, keeping every key in the map unique per file (local identifiers can't collide with each other within one file; export names from different origin modules can and do). Added a 4-file regression test reproducing the exact collision shape, lifted from remeda's real build output. Verified test_gap_renamed_ class_export_namespace.ts, test_issue_836_zod_class_reexports.ts, and test_issue_678_reexport_default.ts (the existing tests covering this same code path) are unaffected. Co-authored-by: Ralph <ralph@skelpo.com>
1 parent 20c21d4 commit ed952eb

6 files changed

Lines changed: 99 additions & 3 deletions

File tree

crates/perry/src/commands/compile/run_pipeline.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2668,9 +2668,30 @@ pub fn run_with_parse_cache(
26682668
source_prefix.clone()
26692669
};
26702670

2671-
import_function_prefixes
2672-
.insert(exported_name.clone(), effective_prefix.clone());
2673-
if local_name != exported_name {
2671+
// Issue #<TBD>: only key by `exported_name` in the
2672+
// no-rename case (`local_name == exported_name`), where
2673+
// the HIR's `ExternFuncRef` genuinely carries that
2674+
// string. In the aliased case (#35/#321 above:
2675+
// `ExternFuncRef` carries the LOCAL name, unique per
2676+
// import site), inserting under `exported_name` too is
2677+
// not just redundant — `exported_name` is whatever the
2678+
// ORIGIN module happens to call it, so it can collide
2679+
// with an unrelated LOCAL alias elsewhere in the same
2680+
// file. Concrete repro: `import { a as n, c as a } from
2681+
// "./x"; import { a as t } from "./y"` — the second
2682+
// specifier's exported name "a" overwrote the first
2683+
// import's *local* alias "a" (from `c as a`), silently
2684+
// repointing `ExternFuncRef { name: "a" }` at module y
2685+
// instead of x. Only inserting the ALIASED case under
2686+
// `local_name` (never also under `exported_name`) keeps
2687+
// every key in this map unique per file — local names
2688+
// can't collide with each other (each `let`/import
2689+
// binding needs a distinct identifier), but exported
2690+
// names from different source modules can and do.
2691+
if local_name == exported_name {
2692+
import_function_prefixes
2693+
.insert(exported_name.clone(), effective_prefix.clone());
2694+
} else {
26742695
import_function_prefixes
26752696
.insert(local_name.clone(), effective_prefix.clone());
26762697
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Mirrors remeda's dist/chunk-ANXBDSUI.js shape (shared "done"/"hasNext"
2+
// iterator-result singletons + constructors). Exports THREE bindings under
3+
// short names "a"/"b"/"c" — "c" is the one a sibling chunk's local alias
4+
// "a" will point at, which is what the colliding-key bug clobbers.
5+
const e = { done: true, hasNext: false }
6+
const s = { done: false, hasNext: false }
7+
const a = () => e
8+
const o = (t: unknown) => ({ hasNext: true, next: t, done: false })
9+
export { s as a, a as b, o as c }
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Mirrors remeda's dist/chunk-D6FCK2GA.js shape (a `purry`-style curry
2+
// helper). Exported under the short, minifier-style name "a" on purpose —
3+
// the bug this fixture reproduces only fires when multiple chunks in the
4+
// same import graph happen to export under colliding short names.
5+
function u(o: (...args: any[]) => any, n: any[], a: unknown) {
6+
const t = (r: any) => o(r, ...n)
7+
return a === void 0 ? t : Object.assign(t, { lazy: a, lazyArgs: n })
8+
}
9+
export { u as a }
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Mirrors remeda's dist/chunk-WIMGWYZL.js shape (arity-dispatching purry
2+
// core). Imports "a" from chunk_d6fck2ga (renamed to "t" here) and
3+
// exports its own "u" as "a" — this file's EXPORTED name "a" is the one
4+
// that, pre-fix, clobbered an unrelated LOCAL alias "a" in a sibling
5+
// chunk that also imports from here.
6+
import { a as t } from "./chunk_d6fck2ga.ts"
7+
8+
function u(r: (...args: any[]) => any, n: any[], o: unknown): any {
9+
const a = r.length - n.length
10+
if (a === 0) return r(...n)
11+
if (a === 1) return t(r, n, o)
12+
throw new Error("Wrong number of arguments")
13+
}
14+
export { u as a }
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Mirrors remeda's dist/chunk-WMCGP7PY.js shape (a `dropFirstBy`-style
2+
// helper built on the two chunks above). This is where issue #5918 fires:
3+
//
4+
// import { a as n, c as a } from "./chunk_anxbdsui.ts" <- local "a" -> anxbdsui's "c"
5+
// import { a as t } from "./chunk_wimgwyzl.ts" <- EXPORTED name "a" (wimgwyzl's)
6+
//
7+
// Pre-fix, `import_function_prefixes` was keyed by BOTH the local alias
8+
// AND the exported/origin name for every named import. The second
9+
// import's exported name "a" (from chunk_wimgwyzl) overwrote the first
10+
// import's *local* alias "a" (bound to chunk_anxbdsui's "c") in that same
11+
// map, so codegen resolved local `a` — used inside `o` below — against
12+
// chunk_wimgwyzl instead of chunk_anxbdsui, which doesn't export anything
13+
// named "c" there. Link failure ensued.
14+
import { a as n, c as a } from "./chunk_anxbdsui.ts"
15+
import { a as t } from "./chunk_wimgwyzl.ts"
16+
17+
function s(...e: unknown[]) {
18+
return t(p, e, o)
19+
}
20+
const p = (e: unknown[], r: number) => (r < 0 ? [...e] : e.slice(r))
21+
function o(e: number): any {
22+
if (e <= 0) return a
23+
let r = e
24+
return (i: unknown) => (r > 0 ? ((r -= 1), n) : { done: false, hasNext: true, next: i })
25+
}
26+
export { s as a }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Issue #5918 — `import_function_prefixes` was keyed by both the local
2+
// alias AND the exported/origin name for every named import. The origin
3+
// name isn't unique per file (it's whatever the SOURCE module happens to
4+
// call it), so two unrelated imports in the same file whose origin names
5+
// collide (very common in minifier/esbuild chunk-splitting output, where
6+
// nearly everything is named "a"/"b"/"c") silently overwrite each other's
7+
// map entry — even when the collision has nothing to do with the local
8+
// alias the HIR's `ExternFuncRef` actually carries.
9+
//
10+
// This shape is lifted directly from `remeda`'s real (unmodified)
11+
// dist/chunk-*.js build output — found via a real-world source compile of
12+
// `sst/opencode`, where nearly every remeda function transitively imports
13+
// these exact four chunks.
14+
import { a as dropFrom } from "./fixtures/issue_5918_pkg/chunk_wmcgp7py.ts"
15+
16+
console.log(JSON.stringify(dropFrom([1, 2, 3, 4, 5], 2)))
17+
console.log(JSON.stringify(dropFrom([1, 2, 3, 4, 5], -1)))

0 commit comments

Comments
 (0)