Follow-up to #28744, same root cause in other places.
With `sortTailwindcss` on and `preserveWhitespace` off, Tailwind class
strings and templates are printed with their whitespace collapsed, but
these layout decisions read the source text, so formatting took two
runs:
```ts
// Sole template argument of a Tailwind call
const a = clsx(`
p-4 flex items-center ${active ? "bg-white" : "bg-black"} text-black
`);
// Arrow body tagged with a Tailwind function
const b = () => tw`
p-4 flex items-center bg-white text-black
`;
// Short object key (tabWidth: 4)
const c = clsx({ "a b": someLongCondition });
```
- `is_multiline_template_starting_on_same_line` now returns `false` for
a template whose line breaks are collapsed
- It reads the current Tailwind context (what `TemplateElement` reads)
or the one a Tailwind tag pushes, so the arrow body, sole argument and
member chain routing callers follow the print
- `write_tailwind_string_literal` returns its printed width, and
`write_member_name` uses it for the short key decision
- An upper bound: the sorter also drops duplicates of known classes
- `tailwind_context_for_callee` is shared by the call and tagged
template pushes and the layout check, like
`tailwind_context_for_jsx_attribute` in #28744
Outputs match Prettier + prettier-plugin-tailwindcss and are idempotent.
Not covered here: `is_short_argument` (#28745), Tailwind calls through
`MemberChain` never pushing the context, and computed keys.
AI assistance (Claude Code) was used for this change. I reviewed and
tested it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The leading parens only guard a statement or arrow body start, so
Prettier prints the same object lookup idiom without them elsewhere
(`const get = ({ a: 1, b: 2 } as const)[key];`).
List that sibling and the assignment arrow body in `Why`, and replace
the `export default` example with a measured one.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`should_add_parens` skipped the parens of an arrow's conditional body
when it starts with an object, function or class, assuming the leading
node prints its own.
Only an object does in an arrow body, so `() => (function () {}).a ? b :
c` printed with no parens at all.
Matches Prettier: skip them only for a leading object.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- bump oxc-markdown-parser 0.0.4 which skips autolink literal attempts that cannot match
- Compute a text's next words only under `proseWrap: always`, the only mode that reads them
- Print link destinations with nothing to escape as is
About 15% CPU instruction reduction.
There are 2 types of Oxfmt conformance tests:
- those that compare against static Prettier snapshots
- those that dynamically execute `format()` for comparison
From the latter, remove any cases already covered by the former.
Up until now, conformance with Prettier was handled separately by each
formatter crate and oxfmt. This was because the individual formatters
were unable to handle embedded formatting.
While that approach worked fine for languages like JSON and CSS, I
realized it would become inconvenient as we moved forward with
supporting languages like Vue.
Since HTML formatter supports `htm`'s `<//>` (special closing tag), we
should also support other cases like:
```js
html`
<${For} each=${() => items}>
<li class="item">${() => content}</li>
<li class="item">more items</li>
</${For}>
`;
```
```html
<script type="text/html">
<div style="color:red;background:blue" onclick="foo( 1 )" class=" a b ">x</div>
</script>
```
will be formatted:
```html
<script type="text/html">
<div style="color:red;background:blue" onclick="foo( 1 )" class=" a b ">
x
</div>
</script>
```
Note attributes are NOT.
`crates/oxc_formatter_htmlish/tests/fixtures/html/nul/` (added in
#28272) uses `nul`, a reserved device name on Windows, which breaks all
Windows jobs on main:
- `Test Windows`: `error: invalid path
'crates/oxc_formatter_htmlish/tests/fixtures/html/nul/nul-as-written.html'`
- `Test NAPI * (windows)`: `Could not find a part of the path '\\.\nul'`
while downloading the repo as a local action
Renamed the directory to `nul-byte/` and updated the `.gitattributes`
entry and the `DIVERGENCES.md` pin. The snapshot content is unchanged.
`cargo test -p oxc_formatter_htmlish` passes
(`fixtures::html::nul_byte::nul_as_written` included).
This PR was created with AI assistance (Claude Code).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Fixes#28263
- Before this PR: `.html` and also html-in-{js,md,etc} are formatted by
Prettier.
- Because of that, we had to run JS, which became an overhead.
- After this PR: All of them are formatted natively by
`oxc_formatter_htmlish`.
- No overhead.
At the same time, since we are fixing Prettier bugs and inconsistent
behavior, please refer to DIVERGENCES.md for the differences.
Reading a file happens outside the `oxfmt::format` span, so time spent in I/O did not show up with `OXC_LOG=debug`.
On a machine where an endpoint security agent scans every `open()`/`close()`, most of the wall time was spent there, but the log only showed formatting time.
```
DEBUG oxfmt::read{path=src/foo.tsx}: oxfmt::cli::service: close time.busy=92.6µs time.idle=2.38µs
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Fixes#28123, closes#28124.
- Keep parens around `(a ?? b)?.#x`
- Add a helper to handle similar cases together
- `PrivateFieldExpression` was often missed...
- Use `>=` for trailing comment bound
- Remove the explicit trailing comment prints that worked around it in `JSXExpressionContainer`, `write_jsx_spread` and `FormatOpeningElement`
Closes#28104.
`can_concisely_print_array_list` shared one cursor-based comments iterator across elements.
Checking an element consumed the comments of later ones, so a comment inside a later signed number was missed and the array fill-printed on the second pass.
- Check each element with the position-based `has_any_comment_in_range`, and the trailing line comment check with `all_comments_in_range` over the array
- Layout decisions use the position-based queries, see `crates/oxc_formatter/AGENTS.md`
- `js/arrays/numbers-negative-comment-after-minus.js` is now idempotent
Closes#28065
Prettier's `shouldPrintSemicolonAfterClassProperty` (and also biome) already has this rule, but our port missed it.
- Treat an `in` / `instanceof` key like a computed key in `needs_semicolon`, with the same modifier exclusions
- Check a string key by its value, since `"in"` may be printed unquoted
- Prettier unquotes it and drops the `;`, producing invalid code
- DIVERGENCES.md#string-binary-keyword-class-key
Fixes#28154
Range ignore is supported only at the top level.
Prettier does not support directives, so the contents of `:::` end up at the top level and the range works there.
We parse `:::` as a container directive, so its contents were not top-level and the range was ignored.
This PR treats a `:::` directive with no blockquote / list item / footnote above it as top-level.
Supporting ranges everywhere is possible, but it has concerns such as indentation drift in verbatim output. So this starts small.
Fast path for short strings.
---
According to local benchmark, it seemed to be effective for the formatter as well, but on CodSpeed, it appears to be effective only for codegen... 🤔
Before: conformance fixtures (may or may not be formatted) are directly compared
After: For may not be formatted fixtures, compare with prettier-formatted
Stop allocating every `AstNode` wrapper in the arena, following the approach of #21931.
### What changes
- `AstNode<'parent, 'a, T> { inner, parent, following_span_start }` is `Copy` and has no `&Allocator`.
- `'parent` is the lifetime of the parent `AstNode` that `parent` refers to.
- Generated child accessors return an owned `AstNode` that borrows its parent.
- The parent's `write` keeps its children on its stack frame.
- The unsafe lifetime extension (`transmute_self`) is gone.
- The arena-backed `as_ast_nodes()` is replaced by a generated owned `AstNodeKind` (`as_kind()`).
- `NeedsParentheses for AstNode<Expression>` is generated as one direct match.
### Sites that are not a plain recursive descent
A loop that walks down a chain cannot keep each node on the stack, as the next node refers to it as its parent.
#21931 hit this at about 10 sites.
- Chain walks (binary-like flattening, member chains, arrow chains, assignment-like, nested namespaces) keep their loops.
- Only the links that the walk continues past go into the arena, through `AstNode::in_arena` / `AstNodeKind::in_arena`.
- The rule is documented once on `AstNode`.
- Checks that never ask for a parent read the AST directly, with no wrapper.
- Left sides: one table, `LeftSide::of`, which `left_span_of`, `leftmost` and the `AstNode` walk `left_of` derive from.
- The shape checks of assignment-like chains.
- The binary-like split and the flattened logical expression are loops too, so long chains need no more stack than on `main`.
### Compared to #21931 (Jim's PoC)
- Enum nodes resolve through the generated `as_kind()`, so call sites keep the `match` shape of `as_ast_nodes()`.
- PoC rewrote each of them by hand, as a `match` on `inner` with `with_inner`.
- The arena is used only through `in_arena`, and only for the links a chain walk continues past.
- PoC put an `f.allocator().alloc(..)` at each site that broke, kept the recursion, and allocated at the entry of a flatten.
- Checks that never ask for a parent use no wrapper.
- PoC allocated a wrapper at each step of e.g. `get_innermost_expression`.
- `ImportExpression::arguments` takes the allocator, instead of leaving `to_arguments` as a TODO.
### Alternatives tried
Both follow directions suggested in #21931.
- #27958: no arena at all
- As suggested "replace loop with recursive function call".
- The chain walks recurse, and a walk that needs the whole chain keeps it in a `StackList` on the stack, consumed through continuation callbacks.
- The recursion uses stack in proportion to the chain length: with 2 MiB workers, `return a.b().b()…` overflows at 4000 calls, where `main` formats 32000.
- So it needs a workaround, e.g. 8 MiB `oxfmt` workers, which still leaves the napi API and the LSP on default stacks.
- About 0.5 points faster than this PR on CodSpeed, but more code.
- #27959: no parent pointer
- As suggested "Alternative approach".
- Ancestors on a `NonEmptyStack` like `Traverse`, and `following_span_start` computed on demand.
- 2-4% more instructions than #27958, quadratic on long chains, and the ancestry is only checked in debug builds.
Revert #24440 and reclassify DIVERGENCES.md#arrow-exact-fill-signature as "uniform-rule".
---
> It seems like oxc is correct here and Prettier has a bug?
> https://github.com/oxc-project/oxc/issues/24416
Having the opportunity to revisit the details..., I found that this is indeed the truth.
Check overflow at every `Fits::Yes` exit like Prettier's loop head, replacing the expanded-line pending space resolution.
Seems a side effect of #20954.
Fixes#27852, closes#27925.
- Recognize Markdown code spans after quotes in JSDoc descriptions, so a span wrapping onto the next line does not swallow subsequent tags.
- Preserve quoted backticks inside types, optional parameter defaults, parenthesized function arguments, and `@import` specifiers.
- Add parser regressions and formatter input/expected-output fixtures for single and double quotes.
In `is_quiet`, the preceding check already guarantees no unprinted comment ends before the node, so the `comments_before_iter` scan in `is_suppressed` always returns `false`.
- Exclude Prettier's `_errors_/` and `range/` fixtures from `js-in-vue`
- Rename `issue-19667.ts` to `.tsx` so it parses
- Treat both sides failing to parse as a failure
- Bump `oxc-yaml-parser` to 0.0.7
- handles NUL as content, BOM only outside a document, markers only at column 0
- Keep `---`/`...` content on the `---` line, keep `?` on a root key starting with a marker, and indent a flow scalar continuation line starting with a marker
- Add `---` before a document whose first text is U+FEFF