Historically, only JS formatter has had the `detect_code_removal` feature, which uses CI to check for missing nodes or comments.
While this would be useful in other languages (especially CSS), but setting up the mechanism is difficult. For now, just start with something simple that just guarantees the number of comments.
That said, even with a mechanism in place, it can't detect anything unless the cases are fully covered by fixtures, so this only tells us that the current state is problem-free.
Audited every formatter crate against the trailing-comment width discipline.
- same-line trailing line comments ride `line_suffix` and are excluded from the fits measurement
- while trailing block comments print inline and count
Corrected the prose that misstated it.
Adds `ParseOptions::enable_ident_hashes` (default `true`) and `Ident::new_unhashed` in `oxc_str`, and turns it off in the parse-only paths of the `oxc-parser` napi crate and the JS/JSON formatters.
### Why
`Ident`'s precomputed hash (#19143) is a parse-time investment that pays off in semantic analysis — but parse-only consumers (parse + serialize, formatting) pay it for nothing. Profiling against other native parsers showed identifier hashing at 3.5-4.6% of parse time.
Measured on M-series (yuku-style native harness, 50 warmup + 300 runs, median):
| fixture | default | option off |
|---|---|---|
| react.development.js | 0.156 ms | 0.152 ms (−2.3%) |
| binder.ts | 0.383 ms | 0.370 ms (−3.5%) |
| App.tsx | 1.140 ms | 1.111 ms (−2.5%) |
| checker.ts | 7.862 ms | 7.499 ms (−4.6%) |
| typescript.js | 25.07 ms | 24.16 ms (−3.6%) |
### Contract
Unhashed `Ident`s store hash `0`; `Eq`/`Hash`/`ContentEq` include the stored hash, so unhashed and hashed `Ident`s of the same string do not compare equal. Semantic analysis (or anything relying on `Ident` hashing) must not run on an AST parsed with the option disabled. Default behavior is unchanged.
Every `Ident` the parser creates funnels through the `ParserImpl::ident()` helper, so the option applies uniformly. This covers identifiers lexed directly (`parse_identifier_kind`, `parse_private_identifier`) as well as those reconstructed from a `&str`/`Str` — import/export specifier locals, JSX element/member names, the TS `intrinsic` type name, TS import-type option keys (`with`/`assert`), and the `import x = this` recovery path — which would otherwise always be hashed through `Into<Ident>`. Exhaustively checked by parsing the test262/babel/typescript/misc/prettier corpora (96.5k files, 5.6M identifiers) in both option states and walking every `Ident` in the AST.
On 64-bit platforms `Ident::new_unhashed` compiles to a no-op: an unhashed `Ident` has the same layout and bit representation as `&str`.
### Opt-outs
- **napi `oxc-parser`** ties `enable_ident_hashes` to whether semantic errors are requested. Both parse entry points run `SemanticBuilder` only when `showSemanticErrors` is set, so the common parse-and-serialize path skips hashing while the semantic-errors path keeps it. Serialized output is unchanged — the hash is internal to `Ident` and never emitted.
- **JS and JSON formatters** turn it off: they never run semantic analysis and only read `Ident::as_str` / compare idents against string literals. Exception: the JS formatter keeps hashes when built with the `detect_code_removal` feature, whose check runs `SemanticBuilder` on the formatter-parsed AST.
Part of #23043.
`Vec::new_in`, `Vec::with_capacity_in`, `Vec::from_array_in` etc previously took an `&'a Allocator`. Instead make them take an `&A where A: GetAllocator<'a>`.
This makes them more flexible - you can pass a reference to any type which implements `GetAllocator` e.g. `ParserImpl`, `TraverseCtx` etc.
```rust
// Before (`ctx` here is `&mut TraverseCtx`)
let vec = ArenaVec::new_in(ctx.ast.allocator);
// After
let vec = ArenaVec::new_in(ctx);
```
The main motivation is to make them workable replacements for what are currently methods on `AstBuilder`, but will be removed in the new `AstBuilder`:
```rust
// Before (`ctx` here is `&mut TraverseCtx`)
let vec = ctx.ast.vec();
// After
let vec = ArenaVec::new_in(ctx);
// Instead of
let vec = ArenaVec::new_in(ctx.ast.allocator);
```
This is still longer code than the original, which is not ideal, but it's as short as can make them.
The main downside of this change is that when calling these methods with an `&Allocator`, you have to double reference:
```rust
let allocator: &'a Allocator = get_allocator_somehow();
// Passing `&&Allocator`
let vec = ArenaVec::new_in(&allocator);
```
This has no perf impact - these methods are inlined, and compiler collapses the double-reference like it wasn't there, but it's just ugly. Unfortunately, there's no way to implement `GetAllocator` so it'll accept both `&Allocator` and `&mut TraverseCtx`, and the latter is the much more common use case.
Pure refactor.
Use `ArenaBox` and `ArenaVec` everywhere for the arena-allocated types from `oxc_allocator`, instead of plain `Box` and `Vec`.
This disambiguates when `Vec` is `oxc_allocator::Vec` and when it's `std::vec::Vec`, making code clearer to follow.
Previously it was easy to confuse the two, and such confusion led to UB in `Traverse` (fixed in #23745).
Ditto other arena types - `ArenaHashMap`, `ArenaHashSet`, `ArenaStringBuilder`.
The only exceptions are:
1. Inside `oxc_allocator` crate itself.
2. AST type definitions.
3. Generated code (migrated separately in #23749).
Pure refactor.
It's a common pattern in repo to import `Vec` and `Box` etc from `oxc_allocator` and alias them to `ArenaBox` / `ArenaVec`.
```rust
use oxc_allocator::{Box as ArenaBox, Vec as ArenaVec};
```
Make this pattern less verbose by re-exporting `Box` and `Vec` as these aliases from `oxc_allocator`.
Ditto other arena types: `ArenaHashMap`, `ArenaHashSet`, `ArenaStringBuilder`.
Use those aliases in all other crates to shorten code.
```rust
use oxc_allocator::{ArenaBox, ArenaVec};
```
- The `formatter_json` uses the `oxc_parser` to parse content
- Because a valid JSON `{}` is considered invalid JS, it is wrapped in `({})` before parsing
- As a result, the spans reported in parsing errors are offset by the width of that opening parenthesis 1
## Summary
Introduces `oxc_diagnostics::Diagnostics` — a `Vec<OxcDiagnostic>` newtype that knows the difference between errors and warnings — and makes it the standard diagnostics return value across oxc.
- **New type**: `Diagnostics` with `has_errors()`, `has_warnings()`, `errors()`, `warnings()`, plus `Vec`-like ergonomics (Deref, `IntoIterator`, `Extend`, `From`/`Into<Vec>`) so most call sites are unchanged.
- **Return types**: `ParserReturn`, `SemanticBuilderReturn`, `IsolatedDeclarationsReturn`, the React Compiler `TransformResult`/`LintResult`, and `TransformerReturn` now expose `diagnostics: Diagnostics` instead of `errors: Vec<OxcDiagnostic>` (the React Compiler's separate `errors`/`warnings` are merged into one severity-tagged list).
- **Behaviour**: a React Compiler **error** now stops the transform (no output), while **warnings** flow through and the transform continues. `compile()` aborts codegen only when `diagnostics.has_errors()` — warnings alone no longer abort (previously any diagnostic did).
## Breaking change
Consumers reading `*.errors` on these return values must read `*.diagnostics`. React Compiler consumers that read `.errors`/`.warnings` separately should use `diagnostics.has_errors()` / `diagnostics.errors()` / `diagnostics.warnings()`.
Parser/semantic/isolated-declarations fatality is otherwise unchanged. Builds on the React Compiler transform feature (#23201).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
I know this is also a rare, but it's better to be precise.
This also makes that the core no longer needs to be aware of line terminators, which can vary from language to language.
The Printer never consumes these; they belong with each language formatter.
- Evict `Quote` (each formatter owns `QuoteStyle`)
- `normalize_string` takes a quote byte
- Evict `Expand` / `BracketSpacing` / `TrailingCommas` into oxc_formatter and oxc_formatter_json
- JSON's `TrailingCommas` is now two-variant (`Always`/`Never`)
- collapse Prettier's `all`/`es5` at each translation layer
- Rename `util/` -> `spec/` (pure shared helpers)
- Split `SourceText`: mechanical access stays in core, JS/TS lexis moves to `SourceTextExt`
- Drop the `oxc_syntax` dependency from core
All behaviros, Prettier conformance are unchanged.
Implement `oxc_formatter_json`'s `Jsonc` variant.
The difference between `json` and `jsonc`:
- `jsonc` allows comment only input, `json` does not
- `jsonc` refers `trailingComma` option, `json` does not
Input:
```json
[1, 2, 3] // if this comment is too long and >= printWidth
```
Before:
```json
[
1,
2,
3
] // if this comment is too long and >= printWidth
```
After:
```json
[1, 2, 3] // if this comment is too long and >= printWidth
```
`oxc_formatter(_js)` and `oxc_formatter_json` are peers, but both need the same source-text scanning. `SourceText` is essentially a `&str` wrapper with a generic `u32` UTF-8 offset API (the oxc-wide convention) and language-neutral, so it belongs in core.
This lets JS and JSON share the code instead of `oxc_formatter_json` hand-rolling its own byte/string slicing.
NOTE:
One method (`get_lines_before`) still encodes JS-specific trivia rules. Moving it out would force its private primitives to become public, so it stays in core in a separate, clearly-marked language-specific impl block.
Part of #19704
In Prettier, there are 4 variants for JSON formatting.
- json 👈🏻 this
- jsonc
- json5
- json-stringify
Although Prettier calls "JSON", it doesn't strictly adhere to the JSON specification; in practice, it behaves more like JS...
To begin with, it uses `babel.parseExpression()`, which makes the input parsing very lenient. The output also doesn't necessarily conform to the specification—for instance, it allows comments and sometimes omits double quotes on keys.
Because of this, we are re-implementing it using `oxc_parser`. Since `parse_expression()` cannot capture comments, I've achieved this by wrapping the input in `()`.
Also considered adding JSON support to `oxc_formatter`, but I have decided NOT to for now. We may reconsider this once we have bothered on various edge cases.