Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/js_parser/parse/parse_typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,12 +391,28 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// SAFETY: current_scope is an arena-owned Scope pointer valid for 'a.
if p.current_scope().members.contains_key(name_text) {
// Add a "_" to make tests easier to read, since non-bundler tests don't
// run the renamer. For external-facing things the renamer will avoid
// collisions automatically so this isn't important for correctness.
// PERF(port): strings::cat heap-allocates; Zig allocated into p.arena.
// TODO(perf): route through bump arena.
let prefixed = strings::cat(b"_", name_text).expect("unreachable");
let prefixed: &'a [u8] = p.arena.alloc_slice_copy(&prefixed);
// run the renamer. Keep adding "_" until the argument does not collide
// with a symbol declared in the namespace body: paths that skip the
// renamer (runtime transpiler, Bun.Transpiler, `bun build --no-bundle`)
// print symbols by their original name, so a colliding argument would
// re-declare a block-scoped member:
//
// namespace m { class m {} class _m {} }
//
// Candidates are built in the parse arena (Zig: `p.allocator`); the
// chosen one becomes the symbol's original name and is freed together
// with the rest of the AST arena.
let mut underscores: usize = 1;
let prefixed: &'a [u8] = loop {
let candidate = p
.arena
.alloc_slice_fill_copy(underscores + name_text.len(), b'_');
candidate[underscores..].copy_from_slice(name_text);
if !p.current_scope().members.contains_key(candidate) {
break candidate;
}
underscores += 1;
};
arg_ref = p
.new_symbol(SymbolKind::Hoisted, prefixed)
.expect("unreachable");
Expand Down
59 changes: 59 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,65 @@ export default class {
it("exported enum", () => {
ts.expectPrinted_(input4, output4);
});

const input5 = `namespace ns {
export class ns {}
}`;
const output5 = `var ns;
((_ns) => {

class ns {
}
_ns.ns = ns;
})(ns ||= {})`;

it("namespace argument renamed to avoid a member with the same name", () => {
ts.expectPrinted_(input5, output5);
});

const input6 = `namespace m2 {
class m2 {}
class _m2 {}
}`;
const output6 = `var m2;
((__m2) => {

class m2 {
}

class _m2 {
}
})(m2 ||= {})`;

it("namespace argument does not collide with declarations in the namespace body", () => {
ts.expectPrinted_(input6, output6);
});

// The runtime transpiler does not run a renamer, so the generated closure
// argument must not shadow declarations inside the namespace body.
it("namespace closure argument does not redeclare members at runtime", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`namespace m2 {
class m2 {}
class _m2 {}
export const names = [m2.name, _m2.name];
}
console.log(JSON.stringify(m2.names));`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).toBe("");
expect(stdout).toBe('["m2","_m2"]\n');
expect(exitCode).toBe(0);
});
});

describe("exports.replace", () => {
Expand Down
Loading