## What
`a-- > b` was minified as `a-- >b` (a space before `>`) to avoid producing the `-->` token sequence. But `-->` is only an HTML close comment (`SingleLineHTMLCloseComment`) at the **start of a line**, and a postfix `--` always has an operand before it — so `a-->b` is never a comment and always parses as `(a--) > b`.
| Input | Before | After |
| --- | --- | --- |
| `while (i-- > 0) f()` | `while(i-- >0)f()` | `while(i-->0)f()` |
| `x = a-- > b` | `x=a-- >b` | `x=a-->b` |
Verified in node (`while(i-->0)` yields `[2,1,0]`). This goes one step beyond esbuild, which keeps the conservative space.
The `< ! --` guard (avoiding `<!--`, which *can* appear mid-line) is left untouched.
## What
Found with the whitespace analyzer. The postfix branch of `UpdateExpression::gen_expr` called `print_space_before_operator(operator)` **before** printing the operand. For a postfix `x++` the `++` comes *after* the operand, so this treated the trailing `++` as if it immediately followed a preceding `+` (the `+`/`++` guard exists to avoid `+++` in prefix `x + ++y`).
Result, when minifying:
| Input | Before | After |
| --- | --- | --- |
| `x + y++` | `x+ y++` | `x+y++` |
| ``a + `-` + i[1]++`` | ``a+`-`+ i[1]++`` | ``a+`-`+i[1]++`` |
## Fix
Matches esbuild's `EUnary` printer: for postfix, print the operand first, then call `printSpaceBeforeOperator` immediately before the operator text. The guard `prev_op_end == code.len()` then makes it a no-op (the operand was printed since the last operator), so `x+y++` stays tight. Required spaces from the prefix/binary guard are unaffected:
- `x + ++y` → `x+ ++y`, `x - --y` → `x- --y`, `b + +c` → `b+ +c`
Output matches esbuild exactly. Codegen (116) + minifier (506) pass; minsize updated.
## What
A `for` init is printed with `Context::FORBID_IN` (so a top-level `in` gets parenthesized). That flag leaked into arrow functions, adding unnecessary parentheses around `in` in the arrow's param defaults and concise body:
```js
for (x = (() => a in b);;); // was: for (x = (() => (a in b));;);
for (x = ((a = b in c) => 1);;); // was: for (x = ((a = (b in c)) => 1);;);
```
## Why the parens aren't needed
- **Param defaults** are always inside the parameter list's `( )`, so `in` is always allowed there — `FORBID_IN` is cleared unconditionally.
- **The concise body** inherits `[?In]` (`ConciseBody[?In]`), so `in` is only allowed unparenthesized when the arrow itself is parenthesized — which resets the restriction. So `FORBID_IN` is cleared for the body only when the arrow is wrapped; an unwrapped arrow at a `for`-init top still propagates it (otherwise the output would be a parse error).
## Validation
All affected outputs pass `node --check` in both normal and minify mode. An existing esbuild-mirrored test (`for ((x => a in b);;)`) is updated to drop the inner parens — matching esbuild's own output.
## Summary
- Stash `@__PURE__` / `@__NO_SIDE_EFFECTS__` (and `#`-prefixed) comments by `attached_to` during comment build, then emit the original source text at call / new / function-decl sites. Falls back to the canonical literal (`/* @__PURE__ */`, `/* @__NO_SIDE_EFFECTS__ */`) only when source text is unavailable or the stashed comment's kind doesn't match the emission site.
- Inputs like `/* #__PURE__ */`, `/*#__PURE__*/`, and `/* #__PURE__ -- @preserve */` round-trip verbatim instead of being normalized to `/* @__PURE__ */` ([rolldown#9408](https://github.com/rolldown/rolldown/issues/9408)).
- Verified by `cargo test -p oxc_codegen` (105 tests; includes new regression for mixed-kind `attached_to` collision in `pure_comment`).
## Problem
Codegen previously dropped pure / no-side-effects comments and emitted a canonical literal at every site marked with the AST `pure` flag. This lost the original text:
- `/* #__PURE__ */ foo()` → `/* @__PURE__ */ foo()` — bundlers / tools that prefer the `#` form lost it.
- `/*#__PURE__*/ foo()` (no spaces) was expanded to `/* @__PURE__ */ foo()`.
- `/* #__PURE__ -- @preserve */ foo()` lost the trailing `-- @preserve` marker that some tools key on (rolldown#9408).
## Fix
A new `annotation_comments: FxHashMap<u32, Comment>` stashes `is_pure()` / `is_no_side_effects()` comments keyed by `attached_to`. `Codegen::print_annotation_comment(start, kind, newline_after)` emits the verbatim source when available, otherwise prints the canonical literal owned by `AnnotationKind::canonical(newline_after)`.
Why a separate map from the existing `comments` (legal / JSDoc / not-applied / normal): applied pure annotations must be emitted **inline at the `pure` site** with a trailing space — not as a leading comment with a newline — and only when `node.pure == true`. Sharing storage would either double-print or conflate two formatting modes inside `print_comments`. Comments where the parser couldn't apply the annotation (e.g. `/* #__PURE__ */ function foo() {}` — wrong target; classified as `CommentContent::PureNotApplied`) still flow through `comments` and the existing leading-comment behavior is preserved.
### Mixed-kind collision
When multiple annotation comments share an `attached_to` (e.g. `/* @__PURE__ */ /* @__NO_SIDE_EFFECTS__ */ foo()`), the map is last-write-wins. The `AnnotationKind` filter on lookup ensures a `@__NO_SIDE_EFFECTS__` slot can't be emitted in place of `@__PURE__` (or vice versa); the dropped comment falls back to the canonical literal. Per the [compiler-notations-spec](https://github.com/javascript-compiler-hints/compiler-notations-spec), the two annotations only apply to their corresponding node kinds (call/new vs function declaration), so mixing them in front of a `CallExpression` would be a real semantic regression.
## Before / after
```js
// input
/* #__PURE__ -- @preserve */ pureOperation();
/*#__PURE__*/ foo(bar);
```
```js
// before
/* @__PURE__ */ pureOperation();
/* @__PURE__ */ foo(bar);
// after
/* #__PURE__ -- @preserve */ pureOperation();
/*#__PURE__*/ foo(bar);
```
AI assisted.
Made function expression PIFEs to be kept. Similar to #12353 but for function expressions.
The minsize for victory is bigger because webpack's output contains many PIFEs (some shouldn't be written as PIFEs though).
refs #12353
Fixes#10334.
Replace #10340 to cover the escape for both template literals and comments. We don’t need to handle regex, which is already covered by existing codegen.
Please let me know anything needed to merge this PR. Thanks.
---------
Co-authored-by: overlookmotel <theoverlookmotel@gmail.com>
This reverts commit 10e1018267.
After experimenting with downstream crates like Rolldown, I discovered
this introduces a footgun.
It's really easy to change `StringLiteral::value` without also changing
`StringLiteral::raw`, which results in incorrect codegen.
After some consideration, I've decided against introducing an extra
option for printing the raw value, it adds a significant amount of
maintenance burden. (This issue alone is already consuming a lot of our
time). We will need to three modes in the codegen: normal, minify and
raw.
For now, let's assume our codegen will not preserve the original source
text. If preserving the original source text is a requirement, please
use magic string or any other direct string manipulation methods.
Closes#10214.
When `options.minify` is `false`, print raw `StringLiteral` as unchanged as possible from how it appeared in source code. But still obey `options.single_quotes`.
* If `options.single_quotes == true` and string was originally quoted with `'`, print raw string unchanged.
* If `options.single_quotes == false` and string was originally quoted with `"`, print raw string unchanged.
* Otherwise, alter the quotes to those requested, and escape any quotes as required within the raw string content. Do not escape or unescape any other characters.
* If `StringLiteral` has no `raw` representation, encode from `value`, using the quote character requested and escape characters as required (like when `options.minify` is `true`).
#10041 changed how lone surrogates are handled in `StringLiteral`s.
`StringLiteral`s which include lone surrogates now have the `lone_surrogates` flag set, and `value` encodes lone surrogates as `\u{FFFD}XXXX`, where `XXXX` is the code unit encoded as hex.
Codegen check the `lone_surrogates` flag and decode the lone surrogates if they're present. This means that:
1. A `StringLiteral` no longer needs to have `raw` field populated, so you can (if you choose to for some reason) create a new `StringLiteral` containing lone surrogates.
2. `StringLiteral`s containing lone surrogates now have any other characters escaped same as how `StringLiteral`s without lone surrogates are printed.
close: #6385
The fixing way was referenced from `esbuild`, it is according to these [states](https://github.com/evanw/esbuild/blob/332727499e62315cff4ecaff9fa8b86336555e46/internal/js_ast/js_ast.go#L590-L604
)(determined in the parser) to determine whether to print parenthesis
Due to differences in implementation details, we are unable to record certain information like `esbuild` does in its parser. But fortunately, The `ParenthesisExpression` AST is actually like what `esbuild` stored states.