Commit Graph

34 Commits

Author SHA1 Message Date
Boshen 37169ffa75 fix(codegen): don't emit space between postfix -- and > when minifying (#23036)
## 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.
2026-06-07 02:52:51 +00:00
Boshen 28467cea77 fix(codegen): don't emit space before a postfix update operand when minifying (#23027)
## 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.
2026-06-06 16:45:37 +00:00
Boshen c08407eeca fix(codegen): don't over-parenthesize in inside an arrow in a for-init (#23009)
## 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.
2026-06-06 10:27:17 +00:00
Dunqing d61e1d7caa fix(codegen): preserve verbatim text of pure/no-side-effects comments (#22525)
## 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.
2026-05-19 10:13:25 +00:00
Boshen 48b0542ca1 fix(span)!: SourceType::ts should set module to unambigious (#18873) 2026-02-03 17:25:52 +08:00
Boshen 34d3cde705 fix(rust): fix clippy issues (#13540) 2025-09-04 07:00:33 +00:00
sapphi-red c135beb1b5 feat(codegen): keep function expression PIFEs (#12470)
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
2025-07-24 00:49:59 +00:00
sapphi-red 0920e9827e feat(codegen): keep arrow function PIFEs (#12353)
PIFE is the abbreviation of "Possibly-Invoked Function Expressions". It is a function expression wrapped with a parenthesized expression.
PIFEs annotate functions that are likely to be invoked eagerly. When v8 encounters such expressions, it compiles them eagerly (rather than compiling it later). See [v8's blog post](https://v8.dev/blog/preparser#pife) for more details.

The blog post only mentions regular FunctionExpressions, but ArrowFunctions are also supported. The cases that will be eagerly compiled are:

- when `!` comes before function literals ([code](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L3730-L3733)) (e.g. `!function(){}`)
- when a function expression is wrapped with parenthesis ([code](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L2243-L2248)) (e.g. `(function () {})`, `(async function () {})`)
- when an arrow function is wrapped with parenthesis ([code1](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L2173-L2174), [code2](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L2232-L2259), [code3](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L3297-L3298)) (e.g. `console.log((() => {}))`)
- when `()` or ``` `` ``` (tagged templates) comes after function literals (only in some cases) ([code1](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L3987-L3992), [code2](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L4344-L4348)) (e.g. `~function(){}()`, ```~function(){}`` ```)
- when [explicit compile hints](https://v8.dev/blog/explicit-compile-hints) are used ([code1](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser.cc#L2717-L2720), [code2](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L5070-L5073)) (e.g. `//# allFunctionsCalledOnLoad`)

Keeping PIFEs as-is in the code generation will unlock optimizations like https://github.com/rolldown/rolldown/pull/5319.

_Note: this PR only implements arrow function PIFEs for now._
2025-07-20 13:13:12 +00:00
Cheng Xu 22799c3f41 fix(codegen): escape </script (#11782)
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>
2025-07-02 18:10:54 +01:00
Boshen d78c04e06a feat(codegen): Revert "feat(codegen)!: print StringLiteral raw if minify option disabled (#10553) (#10569)
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.
2025-04-23 22:38:04 +08:00
overlookmotel 10e1018267 feat(codegen)!: print StringLiteral raw if minify option disabled (#10553)
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`).
2025-04-23 03:26:30 +00:00
overlookmotel 14bb2be524 test(codegen): add more tests for strings (#10552)
We lacked a bit of test coverage for printing strings. Add more tests.
2025-04-23 03:26:30 +00:00
overlookmotel 719742b952 refactor(codegen): print string literals containing lone surrogates without reference to raw (#10044)
#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.
2025-03-29 12:48:01 +00:00
Boshen 59c8f71c4a feat(parser,codegen): handle lone surrogate in string literal (#9918)
closes #3526
2025-03-20 09:04:09 +00:00
Boshen 9b7017c2ae feat(parser,codegen): pure annotations (#9351)
This PR implements the pure annotation spec https://github.com/javascript-compiler-hints/compiler-notations-spec/blob/main/pure-notation-spec.md

No side effects will be in the next PR.

Changes to the minifier will come last, in a later PR.
2025-02-26 13:41:35 +00:00
Boshen 51c4491698 test(codegen): clean up esbuild tests 2025-02-17 23:19:27 +08:00
Boshen e3f78fba0b feat(codegen): new Foo() -> new Foo when minify (#8077) 2024-12-23 15:34:31 +00:00
Boshen d84d60a3a9 feat(codegen): minify numbers with large exponents (#8074) 2024-12-23 13:35:52 +00:00
Boshen 7fb9d47460 style(rust): cargo +nightly fmt (#7877) 2024-12-14 06:03:31 +00:00
Boshen e787e9d41a fix(codegen): print parentheses correctly for ClassHeritage (#7637)
closes #7631
2024-12-04 06:43:32 +00:00
Song Gao cf3415b0e4 chore(doc): replace main/master to tag/commit to make the url always accessible (#7298) 2024-11-16 21:00:30 +08:00
Boshen e5ed6a56a8 feat(codegen): print negative numbers (#6624) 2024-10-16 22:38:54 +00:00
Dunqing e7c89a596b test(codegen): uncomment passed tests that are related to trailing comments (#6589)
Just uncomment them and replace whitespace with `\t`
2024-10-15 16:23:41 +00:00
Dunqing 02bfbfeef5 fix(codegen): preserve parenthesis for ChainExpression (#6430)
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.
2024-10-13 08:03:37 +00:00
Dunqing cca433f84a feat(codegen): print vite / webpack special comments (#6021)
Related: https://github.com/oxc-project/oxc/issues/1046#issuecomment-2347345319
Close: #6024
2024-09-26 15:19:31 +00:00
Boshen d901772daa feat(codegen): implement minify number from terser (#5929) 2024-09-20 14:42:28 +00:00
Boshen 1bed5ce2a5 chore: run cargo +nightly fmt to sort imports (#5503)
They are never going to be stable are they ... https://github.com/oxc-project/oxc/blob/cedf7a4daa4084523050d9ae1b4555e5174c6347/.rustfmt.toml#L8-L16
2024-09-06 04:04:26 +00:00
Boshen 21f576252a fix(codegen): minify large numbers (#4889) 2024-08-14 06:52:17 +00:00
Boshen b58ed80a55 fix(codegen): enable more test cases (#4585) 2024-07-31 12:58:28 +00:00
Boshen a558492bf9 feat(codegen): implement BinaryExpressionVisitor (#4548)
part of https://github.com/oxc-project/backlog/issues/58

`monitor-oxc` run: https://github.com/oxc-project/monitor-oxc/actions/runs/10179047831
binary expression stack length tally using `counts` in top 100 npm packages from monitor-oxc:

```
29772 counts
(  1)    17652 (59.3%, 59.3%): 0
(  2)     5772 (19.4%, 78.7%): 1
(  3)     3204 (10.8%, 89.4%): 2
(  4)     1276 ( 4.3%, 93.7%): 3
(  5)      616 ( 2.1%, 95.8%): 4
(  6)      308 ( 1.0%, 96.8%): 5
(  7)      202 ( 0.7%, 97.5%): 6
(  8)      168 ( 0.6%, 98.1%): 7
(  9)      114 ( 0.4%, 98.5%): 9
( 10)       90 ( 0.3%, 98.8%): 8
( 11)       84 ( 0.3%, 99.0%): 13
( 12)       58 ( 0.2%, 99.2%): 10
( 13)       48 ( 0.2%, 99.4%): 12
( 14)       32 ( 0.1%, 99.5%): 11
( 15)       20 ( 0.1%, 99.6%): 134
( 16)       16 ( 0.1%, 99.6%): 18
( 17)       16 ( 0.1%, 99.7%): 20
( 18)       12 ( 0.0%, 99.7%): 19
( 19)       12 ( 0.0%, 99.8%): 35
( 20)       12 ( 0.0%, 99.8%): 51
( 21)       10 ( 0.0%, 99.8%): 15
( 22)        6 ( 0.0%, 99.9%): 17
( 23)        6 ( 0.0%, 99.9%): 21
( 24)        6 ( 0.0%, 99.9%): 45
( 25)        4 ( 0.0%, 99.9%): 14
( 26)        4 ( 0.0%, 99.9%): 26
( 27)        4 ( 0.0%, 99.9%): 53
( 28)        2 ( 0.0%, 99.9%): 172
( 29)        2 ( 0.0%, 99.9%): 214
( 30)        2 ( 0.0%,100.0%): 22
( 31)        2 ( 0.0%,100.0%): 27
( 32)        2 ( 0.0%,100.0%): 28
( 33)        2 ( 0.0%,100.0%): 29
( 34)        2 ( 0.0%,100.0%): 31
( 35)        2 ( 0.0%,100.0%): 36
( 36)        2 ( 0.0%,100.0%): 46
( 37)        2 ( 0.0%,100.0%): 55
```
2024-07-31 12:44:19 +00:00
Boshen 7446e986c3 feat(codegen): align more esbuild implementations (#4510) 2024-07-28 13:35:37 +00:00
Boshen 35654e665c feat(codegen): align operator precedence with esbuild (#4509)
closes #4339
2024-07-28 11:48:51 +00:00
Boshen 6a94e3f573 fix(codegen): fixes for esbuild test cases (#4503) 2024-07-28 08:57:15 +00:00
Boshen e1ca4122d8 chore(codegen): port over esbuild codegen tests (all ignored right now) (#4457) 2024-07-25 10:00:34 +00:00