From 6b3f98c608e8f7e426afc684d85b90e8bc2222b1 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 22 May 2026 23:33:52 +0000 Subject: [PATCH 01/11] Report duplicate declarations for import bindings kept in TypeScript output TypeScript allows a later declaration to take over the name of an import binding because the import may be type-only. When the import is not elided (Bun.Transpiler keeps unused imports by default), both bindings were printed, producing output that fails to re-parse. Record the collision in declare_symbol and report "has already been declared" from ImportScanner when the import binding survives import elision. --- src/js_parser/p.rs | 19 ++++++++++ src/js_parser/scan/scan_imports.rs | 38 +++++++++++++++++++ test/bundler/transpiler/transpiler.test.js | 43 ++++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 875a16b9fec..733d3446c08 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -432,6 +432,13 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { pub enclosing_class_keyword: bun_ast::Range, pub import_items_for_namespace: HashMap, pub is_import_item: RefMap, + /// Import bindings whose name was re-declared by a later declaration in the + /// same scope. TypeScript allows the collision because the import may be + /// type-only and elided (see `Scope::can_merge_symbol_kinds`), but if the + /// import binding survives import elision it would be printed next to the + /// other declaration, so `ImportScanner::scan` reports a duplicate + /// declaration error using the re-declaration's location stored here. + pub redeclared_import_bindings: HashMap, pub named_imports: NamedImportsType<'a>, pub named_exports: bun_ast::ast_result::NamedExports, pub import_namespace_cc_map: Map, @@ -4989,6 +4996,17 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if kind.is_function() && self.symbols[symbol_idx].kind.is_function() { self.symbols[symbol_idx].remove_overwritten_function_declaration = true; } + + // In TypeScript, an import binding may be silently re-declared + // because the import might be type-only and elided (see + // `Scope::can_merge_symbol_kinds`). Remember the collision so + // `ImportScanner::scan` can report a duplicate declaration error + // if the import binding is actually kept in the output. + if TYPESCRIPT + && self.symbols[symbol_idx].kind == js_ast::symbol::Kind::Import + { + self.redeclared_import_bindings.insert(existing.ref_, loc); + } } MR::BecomePrivateGetSetPair => { ref_ = existing.ref_; @@ -9301,6 +9319,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O enclosing_class_keyword: bun_ast::Range::NONE, import_items_for_namespace: Default::default(), is_import_item: Default::default(), + redeclared_import_bindings: Default::default(), import_namespace_cc_map: Default::default(), scope_order_to_visit: &[], module_scope_directive_loc: bun_ast::Loc::default(), diff --git a/src/js_parser/scan/scan_imports.rs b/src/js_parser/scan/scan_imports.rs index 4e87cc0d48e..57c142dd3d5 100644 --- a/src/js_parser/scan/scan_imports.rs +++ b/src/js_parser/scan/scan_imports.rs @@ -322,6 +322,44 @@ impl<'a> ImportScanner<'a> { st.star_name_loc = None; } + // In TypeScript, a later declaration is allowed to re-declare the + // name of an import binding on the assumption that the import is + // type-only and will be elided (see `Scope::can_merge_symbol_kinds`). + // Any such import binding that is still around at this point will be + // printed next to the other declaration, so report the duplicate + // declaration instead of emitting output that fails to re-parse. + if is_typescript_enabled && !p.redeclared_import_bindings.is_empty() { + let default_binding = st + .default_name + .map(|name| (name.ref_.expect("infallible: ref bound"), name.loc)); + let star_binding = st.star_name_loc.map(|loc| (st.namespace_ref, loc)); + let item_bindings = st.items.slice().iter().map(|item| { + (item.name.ref_.expect("infallible: ref bound"), item.name.loc) + }); + + for (name_ref, import_loc) in default_binding + .into_iter() + .chain(star_binding) + .chain(item_bindings) + { + // `remove` so a second scan pass doesn't report it again. + if let Some(redeclared_loc) = + p.redeclared_import_bindings.remove(&name_ref) + { + // SAFETY: arena-owned slice valid for 'p. + let name = p.symbols[name_ref.inner_index() as usize] + .original_name + .slice(); + p.log().add_symbol_already_declared_error( + p.source, + name, + redeclared_loc, + import_loc, + ); + } + } + } + if st.default_name.is_some() { record!() .flags diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index a296381a6f0..d72edc8cd82 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -927,6 +927,49 @@ class Test extends Bar { ts.expectPrinted_("export import Foo = Baz.Bar;", "export const Foo = Baz.Bar"); }); + it("re-declaring an import binding that is kept in the output is an error", () => { + const err = ts.expectParseError; + + // TypeScript allows another declaration to take over the name of an + // import binding because the import may be type-only. These imports are + // not elided (trimUnusedImports defaults to false for Bun.Transpiler), + // so printing them would produce output that fails to re-parse. They + // must error instead. + err('import{Observable}from""\nimport{Observable} from "x"', '"Observable" has already been declared'); + err('import { Foo } from "./x";\nexport class Foo {}', '"Foo" has already been declared'); + err('import Foo from "./x";\nclass Foo {}', '"Foo" has already been declared'); + err('import * as Foo from "./x";\nclass Foo {}', '"Foo" has already been declared'); + err('import { Foo } from "./x";\nfunction Foo() {}', '"Foo" has already been declared'); + err('import { Foo } from "./x";\nvar Foo = 1;', '"Foo" has already been declared'); + err('import { Foo } from "./x";\nlet Foo = 1;', '"Foo" has already been declared'); + err('import { Foo } from "./x";\nenum Foo {}', '"Foo" has already been declared'); + err('import { Foo } from "./x";\nimport Foo = require("./y");', '"Foo" has already been declared'); + err('import { Foo, Foo } from "./x";', '"Foo" has already been declared'); + }); + + it("re-declaring an elided import binding is allowed", () => { + const exp = ts.expectPrinted_; + + // Type-only imports and declarations that emit no code don't conflict + // with anything in the output, so they are still allowed to collide. + exp('import type { Foo } from "./x";\nclass Foo {}', "class Foo {\n}"); + exp( + 'import { type Foo, Bar } from "./x";\nclass Foo {}\nconsole.log(Bar);', + 'import { Bar } from "./x";\n\nclass Foo {\n}\nconsole.log(Bar);\n', + ); + exp('import { Foo } from "./x";\ndeclare class Foo {}\nnew Foo();', 'import { Foo } from "./x";\nnew Foo;\n'); + exp('import { foo } from "./x";\nfunction foo(): void;', 'import { foo } from "./x";\n'); + + // When unused imports are trimmed (the default for the runtime and the + // bundler), the re-declared import is elided and there is no error. + const trimming = new Bun.Transpiler({ loader: "ts", trimUnusedImports: true }); + expect(trimming.transformSync('import { Foo } from "./x";\nexport class Foo {}')).toBe("export class Foo {\n}\n"); + expect(trimming.transformSync('import{Observable}from""\nimport{Observable} from "x"')).toBe(""); + expect(trimming.transformSync('import { Foo } from "./x";\nnamespace Foo { export const x = 1 }')).toBe( + "var Foo;\n((Foo) => {\n Foo.x = 1;\n})(Foo ||= {});\n", + ); + }); + it("export = {foo: 123}", () => { ts.expectPrinted_("export = {foo: 123}", "module.exports = { foo: 123 }"); }); From 24dd355490c21728d8b0f0fb2fb94fcaa1bdf456 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 23:36:27 +0000 Subject: [PATCH 02/11] [autofix.ci] apply automated fixes --- src/js_parser/scan/scan_imports.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/js_parser/scan/scan_imports.rs b/src/js_parser/scan/scan_imports.rs index 57c142dd3d5..3b0fc9c9ca2 100644 --- a/src/js_parser/scan/scan_imports.rs +++ b/src/js_parser/scan/scan_imports.rs @@ -334,7 +334,10 @@ impl<'a> ImportScanner<'a> { .map(|name| (name.ref_.expect("infallible: ref bound"), name.loc)); let star_binding = st.star_name_loc.map(|loc| (st.namespace_ref, loc)); let item_bindings = st.items.slice().iter().map(|item| { - (item.name.ref_.expect("infallible: ref bound"), item.name.loc) + ( + item.name.ref_.expect("infallible: ref bound"), + item.name.loc, + ) }); for (name_ref, import_loc) in default_binding From b955b25bf92a4b3e6b72591f2a656f9c4eed5bc1 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 23 May 2026 00:00:21 +0000 Subject: [PATCH 03/11] Remove redundant comments --- src/js_parser/p.rs | 13 ++----------- src/js_parser/scan/scan_imports.rs | 7 +------ test/bundler/transpiler/transpiler.test.js | 9 --------- 3 files changed, 3 insertions(+), 26 deletions(-) diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 733d3446c08..eda05baa020 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -432,12 +432,8 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { pub enclosing_class_keyword: bun_ast::Range, pub import_items_for_namespace: HashMap, pub is_import_item: RefMap, - /// Import bindings whose name was re-declared by a later declaration in the - /// same scope. TypeScript allows the collision because the import may be - /// type-only and elided (see `Scope::can_merge_symbol_kinds`), but if the - /// import binding survives import elision it would be printed next to the - /// other declaration, so `ImportScanner::scan` reports a duplicate - /// declaration error using the re-declaration's location stored here. + /// Import bindings replaced by a later declaration (allowed in TS since the + /// import may be type-only), mapped to the replacing declaration's location. pub redeclared_import_bindings: HashMap, pub named_imports: NamedImportsType<'a>, pub named_exports: bun_ast::ast_result::NamedExports, @@ -4997,11 +4993,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.symbols[symbol_idx].remove_overwritten_function_declaration = true; } - // In TypeScript, an import binding may be silently re-declared - // because the import might be type-only and elided (see - // `Scope::can_merge_symbol_kinds`). Remember the collision so - // `ImportScanner::scan` can report a duplicate declaration error - // if the import binding is actually kept in the output. if TYPESCRIPT && self.symbols[symbol_idx].kind == js_ast::symbol::Kind::Import { diff --git a/src/js_parser/scan/scan_imports.rs b/src/js_parser/scan/scan_imports.rs index 3b0fc9c9ca2..331294ff799 100644 --- a/src/js_parser/scan/scan_imports.rs +++ b/src/js_parser/scan/scan_imports.rs @@ -322,12 +322,7 @@ impl<'a> ImportScanner<'a> { st.star_name_loc = None; } - // In TypeScript, a later declaration is allowed to re-declare the - // name of an import binding on the assumption that the import is - // type-only and will be elided (see `Scope::can_merge_symbol_kinds`). - // Any such import binding that is still around at this point will be - // printed next to the other declaration, so report the duplicate - // declaration instead of emitting output that fails to re-parse. + // Report import bindings that were re-declared but not elided. if is_typescript_enabled && !p.redeclared_import_bindings.is_empty() { let default_binding = st .default_name diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index d72edc8cd82..050e219036b 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -930,11 +930,6 @@ class Test extends Bar { it("re-declaring an import binding that is kept in the output is an error", () => { const err = ts.expectParseError; - // TypeScript allows another declaration to take over the name of an - // import binding because the import may be type-only. These imports are - // not elided (trimUnusedImports defaults to false for Bun.Transpiler), - // so printing them would produce output that fails to re-parse. They - // must error instead. err('import{Observable}from""\nimport{Observable} from "x"', '"Observable" has already been declared'); err('import { Foo } from "./x";\nexport class Foo {}', '"Foo" has already been declared'); err('import Foo from "./x";\nclass Foo {}', '"Foo" has already been declared'); @@ -950,8 +945,6 @@ class Test extends Bar { it("re-declaring an elided import binding is allowed", () => { const exp = ts.expectPrinted_; - // Type-only imports and declarations that emit no code don't conflict - // with anything in the output, so they are still allowed to collide. exp('import type { Foo } from "./x";\nclass Foo {}', "class Foo {\n}"); exp( 'import { type Foo, Bar } from "./x";\nclass Foo {}\nconsole.log(Bar);', @@ -960,8 +953,6 @@ class Test extends Bar { exp('import { Foo } from "./x";\ndeclare class Foo {}\nnew Foo();', 'import { Foo } from "./x";\nnew Foo;\n'); exp('import { foo } from "./x";\nfunction foo(): void;', 'import { foo } from "./x";\n'); - // When unused imports are trimmed (the default for the runtime and the - // bundler), the re-declared import is elided and there is no error. const trimming = new Bun.Transpiler({ loader: "ts", trimUnusedImports: true }); expect(trimming.transformSync('import { Foo } from "./x";\nexport class Foo {}')).toBe("export class Foo {\n}\n"); expect(trimming.transformSync('import{Observable}from""\nimport{Observable} from "x"')).toBe(""); From ddd6a9fc98136a9b94e2c698f9740801ca6d858c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 23 May 2026 00:25:16 +0000 Subject: [PATCH 04/11] Test import-equals re-declaring an import binding --- test/bundler/transpiler/transpiler.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 050e219036b..bc47f22eb9b 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -939,6 +939,7 @@ class Test extends Bar { err('import { Foo } from "./x";\nlet Foo = 1;', '"Foo" has already been declared'); err('import { Foo } from "./x";\nenum Foo {}', '"Foo" has already been declared'); err('import { Foo } from "./x";\nimport Foo = require("./y");', '"Foo" has already been declared'); + err('import { Foo } from "./x";\nimport Foo = Bar.Baz;', '"Foo" has already been declared'); err('import { Foo, Foo } from "./x";', '"Foo" has already been declared'); }); From 542d120f7310578c1413b79fd50895f4c80b2698 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 23 May 2026 02:12:18 +0000 Subject: [PATCH 05/11] ci: retrigger From 7cad1b112a6ddf7a9d6b5ae2f55515508fbb9a0e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 23 May 2026 05:08:27 +0000 Subject: [PATCH 06/11] Detect re-declared import bindings via scope members instead of a side table --- src/js_parser/p.rs | 10 --------- src/js_parser/scan/scan_imports.rs | 33 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index eda05baa020..875a16b9fec 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -432,9 +432,6 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { pub enclosing_class_keyword: bun_ast::Range, pub import_items_for_namespace: HashMap, pub is_import_item: RefMap, - /// Import bindings replaced by a later declaration (allowed in TS since the - /// import may be type-only), mapped to the replacing declaration's location. - pub redeclared_import_bindings: HashMap, pub named_imports: NamedImportsType<'a>, pub named_exports: bun_ast::ast_result::NamedExports, pub import_namespace_cc_map: Map, @@ -4992,12 +4989,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if kind.is_function() && self.symbols[symbol_idx].kind.is_function() { self.symbols[symbol_idx].remove_overwritten_function_declaration = true; } - - if TYPESCRIPT - && self.symbols[symbol_idx].kind == js_ast::symbol::Kind::Import - { - self.redeclared_import_bindings.insert(existing.ref_, loc); - } } MR::BecomePrivateGetSetPair => { ref_ = existing.ref_; @@ -9310,7 +9301,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O enclosing_class_keyword: bun_ast::Range::NONE, import_items_for_namespace: Default::default(), is_import_item: Default::default(), - redeclared_import_bindings: Default::default(), import_namespace_cc_map: Default::default(), scope_order_to_visit: &[], module_scope_directive_loc: bun_ast::Loc::default(), diff --git a/src/js_parser/scan/scan_imports.rs b/src/js_parser/scan/scan_imports.rs index 331294ff799..a5ea0e79ee7 100644 --- a/src/js_parser/scan/scan_imports.rs +++ b/src/js_parser/scan/scan_imports.rs @@ -322,8 +322,7 @@ impl<'a> ImportScanner<'a> { st.star_name_loc = None; } - // Report import bindings that were re-declared but not elided. - if is_typescript_enabled && !p.redeclared_import_bindings.is_empty() { + if is_typescript_enabled { let default_binding = st .default_name .map(|name| (name.ref_.expect("infallible: ref bound"), name.loc)); @@ -340,20 +339,22 @@ impl<'a> ImportScanner<'a> { .chain(star_binding) .chain(item_bindings) { - // `remove` so a second scan pass doesn't report it again. - if let Some(redeclared_loc) = - p.redeclared_import_bindings.remove(&name_ref) - { - // SAFETY: arena-owned slice valid for 'p. - let name = p.symbols[name_ref.inner_index() as usize] - .original_name - .slice(); - p.log().add_symbol_already_declared_error( - p.source, - name, - redeclared_loc, - import_loc, - ); + // SAFETY: arena-owned slice valid for 'p. + let name = p.symbols[name_ref.inner_index() as usize] + .original_name + .slice(); + let member = p + .module_scope() + .get_member_with_hash(name, js_ast::Scope::get_member_hash(name)); + if let Some(member) = member { + if !member.ref_.eql(name_ref) { + p.log().add_symbol_already_declared_error( + p.source, + name, + member.loc, + import_loc, + ); + } } } } From 543b408b941ba650b175eb67eee8126e29634993 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 05:10:22 +0000 Subject: [PATCH 07/11] [autofix.ci] apply automated fixes --- src/js_parser/scan/scan_imports.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/js_parser/scan/scan_imports.rs b/src/js_parser/scan/scan_imports.rs index a5ea0e79ee7..a1a4f7194b7 100644 --- a/src/js_parser/scan/scan_imports.rs +++ b/src/js_parser/scan/scan_imports.rs @@ -349,10 +349,7 @@ impl<'a> ImportScanner<'a> { if let Some(member) = member { if !member.ref_.eql(name_ref) { p.log().add_symbol_already_declared_error( - p.source, - name, - member.loc, - import_loc, + p.source, name, member.loc, import_loc, ); } } From 77147dcd3491b90fa3996406e4ce1205f9d5dbad Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 23 May 2026 05:47:41 +0000 Subject: [PATCH 08/11] Only report import bindings replaced by a source declaration Generated symbols (the JSX runtime auto-imports in bundle mode) reuse raw names and take over the scope member without replacing the import, which false-positived the duplicate check on valid TSX. --- src/js_parser/scan/scan_imports.rs | 15 +++++++++++---- test/bundler/bundler_jsx.test.ts | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/js_parser/scan/scan_imports.rs b/src/js_parser/scan/scan_imports.rs index a1a4f7194b7..77139b5cd8c 100644 --- a/src/js_parser/scan/scan_imports.rs +++ b/src/js_parser/scan/scan_imports.rs @@ -339,15 +339,22 @@ impl<'a> ImportScanner<'a> { .chain(star_binding) .chain(item_bindings) { + // Only report source-level re-declarations: `ReplaceWithNew` + // links the import to the symbol that took over the scope + // member, while generated symbols (e.g. JSX runtime imports) + // link the other way. + let symbol = &p.symbols[name_ref.inner_index() as usize]; + let link = symbol.link.get(); + if !link.is_valid() { + continue; + } // SAFETY: arena-owned slice valid for 'p. - let name = p.symbols[name_ref.inner_index() as usize] - .original_name - .slice(); + let name = symbol.original_name.slice(); let member = p .module_scope() .get_member_with_hash(name, js_ast::Scope::get_member_hash(name)); if let Some(member) = member { - if !member.ref_.eql(name_ref) { + if member.ref_.eql(link) { p.log().add_symbol_already_declared_error( p.source, name, member.loc, import_loc, ); diff --git a/test/bundler/bundler_jsx.test.ts b/test/bundler/bundler_jsx.test.ts index a4251eb154f..200835604f2 100644 --- a/test/bundler/bundler_jsx.test.ts +++ b/test/bundler/bundler_jsx.test.ts @@ -184,6 +184,22 @@ describe("bundler", () => { {"$$typeof":"Symbol(jsx)","type":"Symbol("jsx.fragment")","key":"null","ref":"null","props":{"children":"Fragment"},"_owner":"null"} `, }); + // A used `Fragment` import must not be reported as a duplicate of the + // auto-imported JSX `Fragment` helper that shares its name at module scope. + itBundledDevAndProd("jsx/AutomaticFragmentNamedImport", { + files: { + "/index.tsx": /* tsx */ ` + import { print } from 'bun-test-helpers' + import { Fragment } from 'react' + const el = <>hi + print([typeof Fragment, el.type !== Fragment]) + `, + ...helpers, + }, + target: "bun", + devStdout: `["symbol",true]`, + prodStdout: `["symbol",true]`, + }); itBundledDevAndProd("jsx/ImportSource", { prodTodo: true, files: { From 9a2069526aa0822b91865ea9341fe8cff54c8823 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 23 May 2026 06:39:35 +0000 Subject: [PATCH 09/11] Follow the replacement chain when checking re-declared imports --- src/js_parser/scan/scan_imports.rs | 10 +++++++++- test/bundler/transpiler/transpiler.test.js | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/js_parser/scan/scan_imports.rs b/src/js_parser/scan/scan_imports.rs index 77139b5cd8c..aeba45454fb 100644 --- a/src/js_parser/scan/scan_imports.rs +++ b/src/js_parser/scan/scan_imports.rs @@ -344,10 +344,18 @@ impl<'a> ImportScanner<'a> { // member, while generated symbols (e.g. JSX runtime imports) // link the other way. let symbol = &p.symbols[name_ref.inner_index() as usize]; - let link = symbol.link.get(); + let mut link = symbol.link.get(); if !link.is_valid() { continue; } + // Follow the chain of replacements to the live symbol. + loop { + let next = p.symbols[link.inner_index() as usize].link.get(); + if !next.is_valid() { + break; + } + link = next; + } // SAFETY: arena-owned slice valid for 'p. let name = symbol.original_name.slice(); let member = p diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index bc47f22eb9b..04da79f1de9 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -936,6 +936,7 @@ class Test extends Bar { err('import * as Foo from "./x";\nclass Foo {}', '"Foo" has already been declared'); err('import { Foo } from "./x";\nfunction Foo() {}', '"Foo" has already been declared'); err('import { Foo } from "./x";\nvar Foo = 1;', '"Foo" has already been declared'); + err('import { Foo } from "./x";\nvar Foo = 1;\nvar Foo = 2;', '"Foo" has already been declared'); err('import { Foo } from "./x";\nlet Foo = 1;', '"Foo" has already been declared'); err('import { Foo } from "./x";\nenum Foo {}', '"Foo" has already been declared'); err('import { Foo } from "./x";\nimport Foo = require("./y");', '"Foo" has already been declared'); From a3f0a105df8514ffb4f257e92334fa58014a2308 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 23 May 2026 07:01:41 +0000 Subject: [PATCH 10/11] Use the Fragment import before the first JSX element in the bundler test --- test/bundler/bundler_jsx.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/bundler/bundler_jsx.test.ts b/test/bundler/bundler_jsx.test.ts index 200835604f2..08064060f4b 100644 --- a/test/bundler/bundler_jsx.test.ts +++ b/test/bundler/bundler_jsx.test.ts @@ -191,8 +191,9 @@ describe("bundler", () => { "/index.tsx": /* tsx */ ` import { print } from 'bun-test-helpers' import { Fragment } from 'react' + const F = Fragment const el = <>hi - print([typeof Fragment, el.type !== Fragment]) + print([typeof F, el.type !== F]) `, ...helpers, }, From 23bc6fbe28ff1eca155d92e71a9723edd9eb27db Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 23 May 2026 07:32:22 +0000 Subject: [PATCH 11/11] Assert only types in the bundler Fragment test --- test/bundler/bundler_jsx.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/bundler/bundler_jsx.test.ts b/test/bundler/bundler_jsx.test.ts index 08064060f4b..b04373d1930 100644 --- a/test/bundler/bundler_jsx.test.ts +++ b/test/bundler/bundler_jsx.test.ts @@ -193,13 +193,13 @@ describe("bundler", () => { import { Fragment } from 'react' const F = Fragment const el = <>hi - print([typeof F, el.type !== F]) + print([typeof F, typeof el]) `, ...helpers, }, target: "bun", - devStdout: `["symbol",true]`, - prodStdout: `["symbol",true]`, + devStdout: `["symbol","object"]`, + prodStdout: `["symbol","object"]`, }); itBundledDevAndProd("jsx/ImportSource", { prodTodo: true,