Files
oxc-project__oxc/packages/codegen/src-js/print/module.ts
camc314 6e15ad5cfb fix(packages/codegen): print matching quoted import names as identifiers (#26404)
Oxc codegen npm package emits invalid JavaScript when a quoted imported name matches its local binding. The printer compares the decoded names and omits the alias, but leaves the imported name quoted. A quoted import name requires an explicit `as` binding.

For this valid input:

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

The JavaScript printer previously emitted:

```js
import { "foo" } from "m";
```

It now prints the matching local binding as identifier shorthand:

```js
import { foo } from "m";
```

`printImportDeclaration` checks for a string-literal imported name whose decoded value equals the local binding name before printing the imported name. In that case, it prints the local identifier directly, including its source-map mapping. The local binding already supplies a valid identifier, so this requires neither a separate identifier-validity check nor an AST mutation.

Comparing the decoded value also handles escaped and Unicode import names:

```js
// Input
import { "a\u0062" as ab } from "m";
import { "π" as π } from "m";

// Output
import { ab } from "m";
import { π } from "m";
```

Declaration-level and inline TypeScript type imports use the same shorthand:

```ts
// Input
import type { "Foo" as Foo } from "m";
import { type "Bar" as Bar } from "m";

// Output
import type { Foo } from "m";
import { type Bar } from "m";
```

Quoted names that differ from their local bindings retain their quotes and aliases, including `"foo-bar" as foo`, `"" as foo`, and `"default" as foo`. Ordinary identifier imports keep their existing shorthand behavior. The correction applies to both the ordinary and source-map-enabled builds and brings the JavaScript printer in line with the Rust codegen fix in #26386.
2026-09-07 15:28:07 +00:00

362 lines
11 KiB
TypeScript

// Modules.
import { CAT_IDENT, CAT_OTHER, CAT_START_OF_DEFAULT_EXPORT } from "./categories.ts";
import { write, writeIdent, writeNoLast, writeWithMap, writeWithMapNamed } from "./write.ts";
import { printClass } from "./class.ts";
import { printExpression } from "./expression.ts";
import { printFunction } from "./function.ts";
import { printSpaceBeforeIdentifier } from "./space.ts";
import { printIndent } from "./indent.ts";
import { CTX_NONE } from "./operators.ts";
import { PREC_COMMA } from "./precedence.ts";
import { printVariableDeclaration } from "./statement.ts";
import { printString } from "./string.ts";
import {
printTSEnumDeclaration,
printTSImportEqualsDeclaration,
printTSInterfaceDeclaration,
printTSModuleDeclaration,
printTSTypeAliasDeclaration,
} from "./typescript.ts";
import type { State } from "../state.ts";
import type { ExportNamedDeclarationNode } from "./types.ts";
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
/**
* Print an `import` statement, in any of its forms.
*
* Default, namespace and named specifiers can be combined, and the named ones are the only kind in
* braces, so the braces are opened and closed around a run rather than around the whole list.
*/
export function printImportDeclaration(node: ESTree.ImportDeclaration, state: State): void {
printIndent(state);
printSpaceBeforeIdentifier(state);
writeWithMap(state, "import", CAT_IDENT, node.start, node.end, node);
if (TS && node.importKind === "type") writeIdent(state, " type");
if (node.phase != null) {
writeNoLast(state, " ");
writeIdent(state, node.phase);
}
const { specifiers } = node;
const { length } = specifiers;
if (length === 0) {
// `import "source";`
write(state, " ", CAT_OTHER);
printString(state, node.source.value, node.source.start, node.source.end, node.source);
printImportAttributes(node.attributes, state);
write(state, ";\n", CAT_OTHER);
return;
}
let inBlock = false;
for (let i = 0; i < length; i++) {
const specifier = specifiers[i];
switch (specifier.type) {
case "ImportDefaultSpecifier":
if (inBlock) {
write(state, " },", CAT_OTHER);
inBlock = false;
} else if (i === 0) {
write(state, " ", CAT_OTHER);
} else {
write(state, ", ", CAT_OTHER);
}
printSpaceBeforeIdentifier(state);
writeWithMap(
state,
specifier.local.name,
CAT_IDENT,
specifier.start,
specifier.end,
specifier,
);
if (i === length - 1) write(state, " ", CAT_OTHER);
break;
case "ImportNamespaceSpecifier":
if (inBlock) {
write(state, " },", CAT_OTHER);
inBlock = false;
} else if (i === 0) {
write(state, " ", CAT_OTHER);
} else {
write(state, ", ", CAT_OTHER);
}
write(state, "* as ", CAT_OTHER);
writeWithMapNamed(
state,
specifier.local.name,
specifier.local.start,
specifier.local.end,
specifier.local,
);
write(state, " ", CAT_OTHER);
break;
default: {
// ImportSpecifier
if (inBlock) {
write(state, ", ", CAT_OTHER);
} else {
if (i !== 0) {
write(state, ",", CAT_OTHER);
}
inBlock = true;
write(state, " { ", CAT_OTHER);
}
if (TS && specifier.importKind === "type") write(state, "type ", CAT_OTHER);
const { imported, local } = specifier;
if (imported.type === "Literal" && imported.value === local.name) {
printSpaceBeforeIdentifier(state);
writeWithMapNamed(state, local.name, local.start, local.end, local);
break;
}
const importedName = moduleExportName(imported, state);
if (importedName !== local.name) {
write(state, " as ", CAT_OTHER);
writeWithMapNamed(state, local.name, local.start, local.end, local);
}
break;
}
}
}
write(state, inBlock ? " } from " : "from ", CAT_OTHER);
printString(state, node.source.value, node.source.start, node.source.end, node.source);
printImportAttributes(node.attributes, state);
write(state, ";\n", CAT_OTHER);
}
/**
* Print a `with { … }` clause, and nothing at all when there are no attributes.
*
* An empty clause is written where the AST says the source had one, since `with {}`
* and no clause are different programs.
*/
function printImportAttributes(
attributes: ESTree.ImportAttribute[] | null | undefined,
state: State,
): void {
if (attributes == null) return;
const { length } = attributes;
if (length === 0) return;
// ESTree omits the `WithClause` wrapper. The Rust reference normalizes its mapping anchor
// to the first attribute, which is the first location both representations carry.
writeNoLast(state, " ");
writeWithMap(state, "with { ", CAT_OTHER, attributes[0].start, attributes[0].end, attributes[0]);
for (let i = 0; i < length; i++) {
if (i > 0) write(state, ", ", CAT_OTHER);
const attribute = attributes[i];
const { key } = attribute;
if (key.type === "Identifier") {
writeIdent(state, key.name);
} else {
printString(state, key.value, key.start, key.end, key);
}
write(state, ": ", CAT_OTHER);
printString(
state,
attribute.value.value,
attribute.value.start,
attribute.value.end,
attribute.value,
);
}
write(state, " }", CAT_OTHER);
}
/**
* Print the name on one side of an import or export specifier, and return it.
*
* The name comes back because the caller compares the two sides to decide whether an `as` clause
* is needed at all, and it is cheaper to return it than to work it out twice.
*
* @returns The name printed
*/
function moduleExportName(node: ESTree.ModuleExportName, state: State): string {
if (node.type === "Identifier") {
printSpaceBeforeIdentifier(state);
writeWithMapNamed(state, node.name, node.start, node.end, node);
return node.name;
}
printString(state, node.value, node.start, node.end, node);
return node.value;
}
/**
* Print an `export` statement which names what it exports, whether that is a declaration,
* a list of specifiers, or a list re-exported from another module.
*/
export function printExportNamedDeclaration(node: ExportNamedDeclarationNode, state: State): void {
printIndent(state);
writeWithMap(state, "export ", CAT_OTHER, node.start, node.end, node);
const { declaration } = node;
if (declaration != null) {
switch (declaration.type) {
case "VariableDeclaration":
printVariableDeclaration(declaration, state, CTX_NONE);
write(state, ";\n", CAT_OTHER);
break;
case "FunctionDeclaration":
/* IF TS */
case "TSDeclareFunction":
/* END_IF */
printFunction(declaration, state);
write(state, "\n", CAT_OTHER);
break;
case "ClassDeclaration":
printClass(declaration, state);
write(state, "\n", CAT_OTHER);
break;
/* IF TS */
case "TSModuleDeclaration":
printTSModuleDeclaration(declaration, state);
write(state, "\n", CAT_OTHER);
break;
case "TSInterfaceDeclaration":
printTSInterfaceDeclaration(declaration, state);
write(state, "\n", CAT_OTHER);
break;
case "TSEnumDeclaration":
printTSEnumDeclaration(declaration, state);
write(state, "\n", CAT_OTHER);
break;
case "TSTypeAliasDeclaration":
printTSTypeAliasDeclaration(declaration, state);
write(state, ";\n", CAT_OTHER);
break;
case "TSImportEqualsDeclaration":
printTSImportEqualsDeclaration(declaration, state);
write(state, ";\n", CAT_OTHER);
break;
/* END_IF */
default:
throw new Error(`Unknown export declaration type: ${declaration.type}`);
}
return;
}
if (TS && node.exportKind === "type") write(state, "type ", CAT_OTHER);
write(state, "{", CAT_OTHER);
const { specifiers } = node;
const { length } = specifiers;
if (length > 0) {
write(state, " ", CAT_OTHER);
for (let i = 0; i < length; i++) {
if (i > 0) write(state, ", ", CAT_OTHER);
const specifier = specifiers[i];
if (TS && specifier.exportKind === "type") {
write(state, "type ", CAT_OTHER);
}
const localName = moduleExportName(specifier.local, state);
const exportedName =
specifier.exported.type === "Identifier"
? specifier.exported.name
: specifier.exported.value;
if (localName !== exportedName) {
write(state, " as ", CAT_OTHER);
moduleExportName(specifier.exported, state);
}
}
write(state, " ", CAT_OTHER);
}
write(state, "}", CAT_OTHER);
if (node.source != null) {
write(state, " from ", CAT_OTHER);
printString(state, node.source.value, node.source.start, node.source.end, node.source);
printImportAttributes(node.attributes, state);
}
write(state, ";\n", CAT_OTHER);
}
/**
* Print `export * from "…"`, with the `as name` form where the AST has one.
*/
export function printExportAllDeclaration(node: ESTree.ExportAllDeclaration, state: State): void {
printIndent(state);
writeWithMap(
state,
TS && node.exportKind === "type" ? "export type *" : "export *",
CAT_OTHER,
node.start,
node.end,
node,
);
if (node.exported != null) {
write(state, " as ", CAT_OTHER);
moduleExportName(node.exported, state);
}
write(state, " from ", CAT_OTHER);
printString(state, node.source.value, node.source.start, node.source.end, node.source);
printImportAttributes(node.attributes, state);
write(state, ";\n", CAT_OTHER);
}
/**
* Print `export default ...`.
*
* The output position is recorded before the value is printed, so that a function or class expression
* starting there knows to parenthesize itself.
*/
export function printExportDefaultDeclaration(
node: ESTree.ExportDefaultDeclaration,
state: State,
): void {
printIndent(state);
writeWithMap(state, "export default ", CAT_OTHER, node.start, node.end, node);
const { declaration } = node;
switch (declaration.type) {
case "FunctionDeclaration":
/* IF TS */
case "TSDeclareFunction":
/* END_IF */
printFunction(declaration, state);
write(state, "\n", CAT_OTHER);
break;
case "ClassDeclaration":
printClass(declaration, state);
write(state, "\n", CAT_OTHER);
break;
/* IF TS */
case "TSInterfaceDeclaration":
printTSInterfaceDeclaration(declaration, state);
write(state, "\n", CAT_OTHER);
break;
/* END_IF */
default:
state.last = CAT_START_OF_DEFAULT_EXPORT;
printExpression(declaration, state, PREC_COMMA, CTX_NONE);
write(state, ";\n", CAT_OTHER);
break;
}
}