Files
oxc-project__oxc/packages/codegen/src-js/print/assignment_target.ts
camc314 300763f328 feat(packages/codegen): add full source map support (#25585)
## Summary

Align `oxc-codegen` source maps with Rust `oxc_codegen`, using the Rust implementation as the conformance oracle.

This PR:

- changes `printSync` to return `{ code, map? }`;
- returns a standard Source Map v3 object;
- exposes Rust’s generated source map through `codegen_conformance`;
- compares the complete JS and Rust source maps;
- supports Oxc `start`/`end` offsets and TS-ESTree `range` offsets;
- uses `sourceText` to convert UTF-16 offsets and recover original identifier spellings;
- validates both maps and no-maps builds in both `preserveParens` modes;
- aligns mapping placement across JavaScript, TypeScript, JSX, templates, imports, destructuring, delimiters, and postfix chains;
- handles astral characters and all ECMAScript line terminators.
`loc`-only ASTs are not supported for source maps.

## API change

```ts
const { code, map } = printSync(program, {
  sourcemap: true,
  sourceFileName: "input.js",
  sourceText,
});
```

`map` is a Source Map v3 object:

```ts
interface SourceMap {
  version: 3;
  mappings: string;
  names: string[];
  sources: string[];
  sourcesContent?: string[];
}
```

## Implementation

Mapped writes collect only:

- the generated UTF-16 offset;
- the original source offset;
- the optional original identifier name.

After printing, the generator builds line tables for the generated output and `sourceText`, converts the offsets to line/column positions, suppresses duplicate source positions, and encodes the result as base64 VLQ.

The no-maps builds continue to remove mapping-specific writes at build time.

## Validation flow

```mermaid
flowchart TB
    fixture["Fixture<br/>filename + sourceText + parse options"]

    fixture --> rustParse
    fixture --> jsParse

    subgraph rust["Rust oracle"]
        direction TB
        rustParse["Parse with oxc_parser"]
        normalize["Normalize Rust-only AST information"]
        rustPrint["Print with oxc_codegen<br/>source maps enabled"]
        rustResult["Expected result<br/>{ code, map }"]

        rustParse --> normalize
        normalize --> rustPrint
        rustPrint --> rustResult
    end

    subgraph js["JavaScript candidate"]
        direction TB
        jsParse["Parse ESTree with oxc-parser<br/>start/end or range offsets"]

        plainPrint["Print with no-maps build"]
        plainResult["Actual code"]

        mappedPrint["Print with maps-enabled build"]
        deferred["Record generated offsets,<br/>source offsets, and names"]
        encode["Convert UTF-16 offsets to positions<br/>deduplicate and encode VLQ"]
        mappedResult["Actual result<br/>{ code, map }"]

        jsParse --> plainPrint
        plainPrint --> plainResult

        jsParse --> mappedPrint
        mappedPrint --> deferred
        deferred --> encode
        encode --> mappedResult
    end

    rustParse --> parseGate
    jsParse --> parseGate

    rustResult --> codeGate
    plainResult --> codeGate
    mappedResult --> codeGate

    rustResult --> mapGate
    mappedResult --> mapGate

    parseGate["Parse agreement<br/>Rust success requires no JS errors"]
    codeGate["Code equality<br/>both JS builds equal Rust"]
    mapGate["Source Map v3 equality<br/>mappings + names + sources + content"]

    parseGate --> pass["Fixture passes"]
    codeGate --> pass
    mapGate --> pass
```

Each fixture runs with both `preserveParens: false` and `preserveParens: true`.

## AST normalization

Rust’s AST contains information that ESTree cannot represent. The Rust AST is normalized before printing so the comparison measures code generation rather than representation differences.

This includes:

- parenthesized function and arrow-expression flags;
- `import "m"` versus `import {} from "m"`;
- absent versus empty import-attribute clauses;
- `assert` versus `with` attribute keywords;
- the unavailable ESTree span for a complete `with` clause.
2026-08-18 11:41:34 +00:00

164 lines
5.3 KiB
TypeScript

// Assignment targets.
import { typeAssertIs } from "../asserts.ts";
import { printPropertyKey } from "./binding_pattern.ts";
import { CAT_IDENT, CAT_OTHER, write, writeWithMap } from "./write.ts";
import { printExpression, printMemberExpression } from "./expression.ts";
import { printSpaceBeforeIdentifier } from "./space.ts";
import { CTX_NONE } from "./operators.ts";
import { PREC_COMMA, PREC_COMPARE, PREC_PREFIX } from "./precedence.ts";
import type { State } from "../state.ts";
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
/**
* Print the left hand side of an assignment.
*
* Destructuring targets look like object and array literals but are a separate set of node types,
* with their own rules - which is why they are printed here rather than by the expression printer.
*/
export function printAssignmentTarget(node: ESTree.AssignmentTarget, state: State): void {
switch (node.type) {
// A simple target is only ever a name, a member access, or (in TS) one of the assertion expressions below.
// For speed, the two common ones are printed here instead of through the expression printer's own dispatch.
case "Identifier":
printSpaceBeforeIdentifier(state);
writeWithMap(state, node.name, CAT_IDENT, node);
break;
case "MemberExpression":
printMemberExpression(node, state, CTX_NONE);
break;
case "ObjectPattern":
printObjectAssignmentTarget(node, state);
break;
case "ArrayPattern":
printArrayAssignmentTarget(node, state);
break;
/* IF TS */
case "TSAsExpression":
case "TSSatisfiesExpression":
// `(x as T) = y` - the target must be parenthesized
printExpression(node, state, PREC_COMPARE, CTX_NONE);
break;
case "TSTypeAssertion":
printExpression(node, state, PREC_PREFIX, CTX_NONE);
break;
/* END_IF */
default:
printExpression(node, state, PREC_COMMA, CTX_NONE);
}
}
/**
* Print an object destructuring target, as in `({ a, b: c } = obj)`.
*/
function printObjectAssignmentTarget(node: ESTree.ObjectAssignmentTarget, state: State): void {
writeWithMap(state, "{", CAT_OTHER, node);
const { properties } = node;
const { length } = properties;
for (let i = 0; i < length; i++) {
if (i > 0) write(state, ", ", CAT_OTHER);
const property = properties[i];
if (property.type === "RestElement") {
writeWithMap(state, "...", CAT_OTHER, property);
printAssignmentTarget(property.argument, state);
} else {
printAssignmentTargetProperty(property, state);
}
}
write(state, "}", CAT_OTHER);
}
/**
* Print one property of an object destructuring target, in shorthand where the AST says so.
*/
function printAssignmentTargetProperty(node: ESTree.AssignmentTargetProperty, state: State): void {
// Shorthand from the AST flag (assignment targets are not re-derived).
if (node.shorthand) {
// `{ a }` or `{ a = 1 }`
const { value } = node;
if (value.type === "AssignmentPattern") {
typeAssertIs<ESTree.IdentifierReference>(value.left);
printSpaceBeforeIdentifier(state);
writeWithMap(state, value.left.name, CAT_IDENT, value.left);
write(state, " = ", CAT_OTHER);
printExpression(value.right, state, PREC_COMMA, CTX_NONE);
} else {
printSpaceBeforeIdentifier(state);
writeWithMap(state, value.name, CAT_IDENT, value);
}
} else {
const { key } = node;
if (node.computed) {
write(state, "[", CAT_OTHER);
typeAssertIs<ESTree.Expression>(key);
printExpression(key, state, PREC_COMMA, CTX_NONE);
write(state, "]", CAT_OTHER);
} else {
printPropertyKey(key, state);
}
write(state, ": ", CAT_OTHER);
printAssignmentTargetMaybeDefault(node.value, state);
}
}
/**
* Print a destructuring target which may carry a default, as the `a = 1` in `({ a = 1 } = obj)`.
*/
function printAssignmentTargetMaybeDefault(
node: ESTree.AssignmentTargetMaybeDefault,
state: State,
): void {
if (node.type === "AssignmentPattern") {
printAssignmentTarget(node.left, state);
write(state, " = ", CAT_OTHER);
printExpression(node.right, state, PREC_COMMA, CTX_NONE);
} else {
printAssignmentTarget(node, state);
}
}
/**
* Print an array destructuring target, holes and rest element included.
*/
function printArrayAssignmentTarget(node: ESTree.ArrayAssignmentTarget, state: State): void {
const { elements } = node;
let { length } = elements;
let rest: ESTree.BindingRestElement | ESTree.AssignmentTargetRest | null = null;
if (length > 0) {
const lastElement = elements[length - 1];
if (lastElement != null && lastElement.type === "RestElement") {
rest = lastElement;
length--;
}
}
writeWithMap(state, "[", CAT_OTHER, node);
for (let i = 0; i < length; i++) {
if (i !== 0) write(state, ", ", CAT_OTHER);
const element = elements[i];
if (element != null) {
typeAssertIs<ESTree.AssignmentTargetMaybeDefault>(element);
printAssignmentTargetMaybeDefault(element, state);
}
if (i === length - 1 && (element == null || rest !== null)) {
write(state, ",", CAT_OTHER);
}
}
if (rest !== null) {
if (length > 0) write(state, " ", CAT_OTHER);
writeWithMap(state, "...", CAT_OTHER, rest);
printAssignmentTarget(rest.argument, state);
}
write(state, "]", CAT_OTHER);
}