mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
300763f328
## 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.
198 lines
6.1 KiB
TypeScript
198 lines
6.1 KiB
TypeScript
// Binding patterns.
|
|
|
|
import { typeAssertIs } from "../asserts.ts";
|
|
import {
|
|
CAT_IDENT,
|
|
CAT_OTHER,
|
|
CAT_QUESTION,
|
|
write,
|
|
writeWithMap,
|
|
writeWithMapNoLast,
|
|
} from "./write.ts";
|
|
import { printExpression } from "./expression.ts";
|
|
import { printSpaceBeforeIdentifier } from "./space.ts";
|
|
import { printLiteral } from "./literal.ts";
|
|
import { CTX_NONE } from "./operators.ts";
|
|
import { PREC_COMMA } from "./precedence.ts";
|
|
import { printString } from "./string.ts";
|
|
import { printTypeAnnotation } from "./typescript.ts";
|
|
|
|
import type { State } from "../state.ts";
|
|
import type { LiteralExtras, UnknownNode } from "./types.ts";
|
|
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
|
|
|
/**
|
|
* A binding pattern, or one of the nodes which can stand in for one.
|
|
*
|
|
* `RestElement` appears in object and array patterns.
|
|
*/
|
|
type BindingPatternNode =
|
|
| ESTree.BindingPattern
|
|
| ESTree.BindingRestElement
|
|
| ESTree.FormalParameterRest;
|
|
|
|
/**
|
|
* Print anything which can be bound to - a name, a destructuring pattern, or one carrying a default.
|
|
*/
|
|
export function printBindingPattern(node: BindingPatternNode | UnknownNode, state: State): void {
|
|
switch (node.type) {
|
|
case "Identifier":
|
|
printSpaceBeforeIdentifier(state);
|
|
writeWithMap(state, node.name, CAT_IDENT, node);
|
|
if (TS && node.optional) write(state, "?", CAT_QUESTION);
|
|
if (TS && node.typeAnnotation != null) printTypeAnnotation(node.typeAnnotation, state);
|
|
break;
|
|
case "ObjectPattern":
|
|
printObjectBindingPattern(node, state);
|
|
if (TS && node.optional) write(state, "?", CAT_QUESTION);
|
|
if (TS && node.typeAnnotation != null) printTypeAnnotation(node.typeAnnotation, state);
|
|
break;
|
|
case "ArrayPattern":
|
|
printArrayBindingPattern(node, state);
|
|
if (TS && node.optional) write(state, "?", CAT_QUESTION);
|
|
if (TS && node.typeAnnotation != null) printTypeAnnotation(node.typeAnnotation, state);
|
|
break;
|
|
case "AssignmentPattern":
|
|
printBindingPattern(node.left, state);
|
|
write(state, " = ", CAT_OTHER);
|
|
printExpression(node.right, state, PREC_COMMA, CTX_NONE);
|
|
break;
|
|
case "RestElement":
|
|
writeWithMap(state, "...", CAT_OTHER, node);
|
|
printBindingPattern(node.argument, state);
|
|
if (TS && node.typeAnnotation != null) printTypeAnnotation(node.typeAnnotation, state);
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown binding pattern type: ${node.type}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Print an object destructuring pattern, as in `const { a, b } = obj`.
|
|
*/
|
|
function printObjectBindingPattern(node: ESTree.ObjectPattern, state: State): void {
|
|
const { properties } = node;
|
|
const { length } = properties;
|
|
|
|
if (length === 0) {
|
|
writeWithMap(state, "{}", CAT_OTHER, node);
|
|
return;
|
|
}
|
|
|
|
writeWithMap(state, "{ ", CAT_OTHER, node);
|
|
|
|
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);
|
|
printBindingPattern(property.argument, state);
|
|
} else {
|
|
printBindingProperty(property, state);
|
|
}
|
|
}
|
|
|
|
write(state, " }", CAT_OTHER);
|
|
}
|
|
|
|
/**
|
|
* Print one property of an object pattern, in shorthand where the key and the binding agree.
|
|
*/
|
|
function printBindingProperty(node: ESTree.BindingProperty, state: State): void {
|
|
// Shorthand is re-derived from names (matching Oxc), not from the flag.
|
|
const { key, value } = node;
|
|
|
|
let shorthand = false;
|
|
if (!node.computed && key.type === "Identifier") {
|
|
if (value.type === "Identifier" && key.name === value.name) {
|
|
shorthand = true;
|
|
} else if (
|
|
value.type === "AssignmentPattern" &&
|
|
value.left.type === "Identifier" &&
|
|
key.name === value.left.name
|
|
) {
|
|
shorthand = true;
|
|
}
|
|
}
|
|
|
|
if (!shorthand) {
|
|
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);
|
|
}
|
|
|
|
printBindingPattern(value, state);
|
|
}
|
|
|
|
/**
|
|
* Print a property key which is not computed - a name, a private name, a string or a number.
|
|
*
|
|
* The caller decides between this and printing the key as an expression in brackets,
|
|
* because whether a key must be computed depends on what it is, not on how the source wrote it.
|
|
*/
|
|
export function printPropertyKey(key: ESTree.PropertyKey, state: State): void {
|
|
switch (key.type) {
|
|
case "Identifier":
|
|
printSpaceBeforeIdentifier(state);
|
|
writeWithMap(state, key.name, CAT_IDENT, key);
|
|
break;
|
|
case "PrivateIdentifier":
|
|
writeWithMapNoLast(state, "#", key);
|
|
write(state, key.name, CAT_IDENT);
|
|
break;
|
|
case "Literal":
|
|
typeAssertIs<LiteralExtras>(key);
|
|
if (typeof key.value === "string") {
|
|
printString(state, key.value, key);
|
|
} else {
|
|
printLiteral(key, state, PREC_COMMA, CTX_NONE);
|
|
}
|
|
break;
|
|
default:
|
|
printExpression(key, state, PREC_COMMA, CTX_NONE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Print an array destructuring pattern, holes and rest element included.
|
|
*/
|
|
function printArrayBindingPattern(node: ESTree.ArrayPattern, state: State): void {
|
|
// Oxc stores the rest element separately from the other elements.
|
|
// A trailing comma is printed before it, and a space always precedes it.
|
|
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) printBindingPattern(element, state);
|
|
|
|
if (i === length - 1 && (element == null || rest !== null)) write(state, ",", CAT_OTHER);
|
|
}
|
|
|
|
if (rest !== null) {
|
|
write(state, " ", CAT_OTHER);
|
|
printBindingPattern(rest, state);
|
|
}
|
|
|
|
write(state, "]", CAT_OTHER);
|
|
}
|