diff --git a/src/js_parser/scan/scan_imports.rs b/src/js_parser/scan/scan_imports.rs index 4e87cc0d48e..aeba45454fb 100644 --- a/src/js_parser/scan/scan_imports.rs +++ b/src/js_parser/scan/scan_imports.rs @@ -322,6 +322,55 @@ impl<'a> ImportScanner<'a> { st.star_name_loc = None; } + if is_typescript_enabled { + 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) + { + // 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 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 + .module_scope() + .get_member_with_hash(name, js_ast::Scope::get_member_hash(name)); + if let Some(member) = member { + if member.ref_.eql(link) { + p.log().add_symbol_already_declared_error( + p.source, name, member.loc, import_loc, + ); + } + } + } + } + if st.default_name.is_some() { record!() .flags diff --git a/test/bundler/bundler_jsx.test.ts b/test/bundler/bundler_jsx.test.ts index a4251eb154f..b04373d1930 100644 --- a/test/bundler/bundler_jsx.test.ts +++ b/test/bundler/bundler_jsx.test.ts @@ -184,6 +184,23 @@ 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 F = Fragment + const el = <>hi + print([typeof F, typeof el]) + `, + ...helpers, + }, + target: "bun", + devStdout: `["symbol","object"]`, + prodStdout: `["symbol","object"]`, + }); itBundledDevAndProd("jsx/ImportSource", { prodTodo: true, files: { diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index a296381a6f0..04da79f1de9 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -927,6 +927,42 @@ 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; + + 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";\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'); + 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'); + }); + + it("re-declaring an elided import binding is allowed", () => { + const exp = ts.expectPrinted_; + + 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'); + + 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 }"); });