fix(codegen): print matching quoted import names as identifiers (#26386)

Codegen omitted the alias whenever an imported name matched its local binding, comparing the decoded names without accounting for whether the imported name was a string literal. That produces invalid syntax for quoted import names, which require an explicit `as` binding:

```js
// Input
import { "foo" as foo } from "m";

// Before
import { "foo" } from "m";

// After
import { foo } from "m";

// After, minified
import{foo}from"m";
```

When a string-literal import name matches the emitted local binding name, print the local binding directly and use identifier shorthand. The matching binding already supplies a valid identifier, so this does not need a separate identifier-validity check or an AST mutation. The conversion applies in both default and minified output.

The comparison uses the decoded string value, so escaped names and Unicode names can also use shorthand. Declaration-level and inline type imports follow the same rule:

```ts
// Input
import { "a\u0062" as ab } from "m";
import { "π" as π } from "m";
import type { "Foo" as Foo } from "types";
import { type "Bar" as Bar } from "types";

// After
import { ab } from "m";
import { π } from "m";
import type { Foo } from "types";
import { type Bar } from "types";
```

Quoted names that differ from their local binding retain their quotes and aliases, including `"foo-bar" as foo`, `"" as foo`, and `"default" as foo`. Ordinary identifier imports retain the existing shorthand behavior.
This commit is contained in:
camc314 committed 2026-09-07 09:58:17 +00:00
1 parent 646c27cae3
commit 2da73b7824
3 files changed
+51 -3

No files matched your search

+7 -1
View File
@@ -988,9 +988,15 @@ impl Gen for ImportDeclaration<'_> {
p.print_str("type ");
}
spec.imported.print(p, ctx);
let local_name = p.get_binding_identifier_name(&spec.local);
let imported_name = get_module_export_name(&spec.imported, p);
if matches!(spec.imported, ModuleExportName::StringLiteral(_))
&& imported_name == local_name
{
spec.local.print(p, ctx);
continue;
}
spec.imported.print(p, ctx);
if imported_name != local_name {
p.print_soft_space();
p.print_space_before_identifier();
@@ -63,6 +63,28 @@ fn module_decl() {
test_minify("export { foo as bar }", "export{foo as bar};");
}
#[test]
fn quoted_import_names() {
test(r#"import { "foo" as foo } from "m";"#, "import { foo } from \"m\";\n");
test(r#"import { "a\u0062" as ab } from "m";"#, "import { ab } from \"m\";\n");
test(r#"import { "π" as π } from "m";"#, "import { π } from \"m\";\n");
test(r#"import { "type" as type } from "m";"#, "import { type } from \"m\";\n");
test_same("import { \"foo-bar\" as foo } from \"m\";\n");
test_same("import { \"\" as foo } from \"m\";\n");
test_same("import { \"default\" as foo } from \"m\";\n");
test_same("import { \"foo\" as bar } from \"m\";\n");
test(
r#"import { "foo" as foo, "bar" as bar, baz } from "m";"#,
"import { foo, bar, baz } from \"m\";\n",
);
test(r#"import { foo as foo, bar } from "m";"#, "import { foo, bar } from \"m\";\n");
test_minify(r#"import { "foo" as foo } from "m";"#, r#"import{foo}from"m";"#);
test_minify(r#"import { "a\u0062" as ab } from "m";"#, r#"import{ab}from"m";"#);
test_minify(r#"import { foo as foo, bar } from "m";"#, r#"import{foo,bar}from"m";"#);
}
#[test]
fn export_type() {
test_same("export type {} from \"mod\";\n");
+22 -2
View File
@@ -5,8 +5,8 @@ use oxc_span::SourceType;
use crate::{
snapshot, snapshot_options,
tester::{
default_options, test_idempotency, test_options_with_source_type, test_same, test_tsx,
test_with_parse_options,
default_options, test, test_idempotency, test_options_with_source_type, test_same,
test_tsx, test_with_parse_options,
},
};
@@ -55,6 +55,26 @@ fn cases() {
test_same("const Bar = class<T,> {};\n");
}
#[test]
fn quoted_type_import_names() {
test(r#"import type { "foo" as foo } from "m";"#, "import type { foo } from \"m\";\n");
test(r#"import { type "foo" as foo } from "m";"#, "import { type foo } from \"m\";\n");
test(r#"import type { "a\u0062" as ab } from "m";"#, "import type { ab } from \"m\";\n");
test(r#"import { type "a\u0062" as ab } from "m";"#, "import { type ab } from \"m\";\n");
test(r#"import type { foo as foo } from "m";"#, "import type { foo } from \"m\";\n");
test(r#"import { type foo as foo } from "m";"#, "import { type foo } from \"m\";\n");
let min = |source: &str, expected: &str| {
test_options_with_source_type(source, expected, SourceType::ts(), CodegenOptions::minify());
};
min(r#"import type { "foo" as foo } from "m";"#, r#"import type{foo}from"m";"#);
min(r#"import { type "foo" as foo } from "m";"#, r#"import{type foo}from"m";"#);
min(r#"import type { "a\u0062" as ab } from "m";"#, r#"import type{ab}from"m";"#);
min(r#"import { type "a\u0062" as ab } from "m";"#, r#"import{type ab}from"m";"#);
min(r#"import type { foo as foo } from "m";"#, r#"import type{foo}from"m";"#);
min(r#"import { type foo as foo } from "m";"#, r#"import{type foo}from"m";"#);
}
#[test]
fn export_default_interface() {
test_same("export default interface X {}\nconst y = 1;\n");