Skip to content

fix(oxfmt): use literalline for template literals in textToDoc#20085

Closed
vinayakkulkarni wants to merge 1 commit intooxc-project:mainfrom
vinayakkulkarni:fix/oxfmt-template-literal-idempotency
Closed

fix(oxfmt): use literalline for template literals in textToDoc#20085
vinayakkulkarni wants to merge 1 commit intooxc-project:mainfrom
vinayakkulkarni:fix/oxfmt-template-literal-idempotency

Conversation

@vinayakkulkarni
Copy link

Summary

Fixes #20084

When vueIndentScriptAndStyle: true, formatting template literals inside Vue SFC <script> blocks is non-idempotent — each oxfmt --write . pass adds +2 spaces of indentation to template literal content, growing infinitely.

Root Cause

In run_full() (text_to_doc_api.rs), the formatted code was converted to a Prettier Doc by joining all newlines with hardline. When Prettier wraps the textToDoc() result with indent() for vueIndentScriptAndStyle, hardline respects the parent indent — so template literal text content gets +2 spaces per pass.

Fix

After formatting the code to a string (preserving all formatting decisions), re-parse the output to identify TemplateElement spans via an AST visitor. Then build the Prettier Doc using:

  • literalline for newlines inside template literal spans (ignores parent indent)
  • hardline for all other newlines (respects parent indent, as before)

This matches how Prettier itself handles template literals in its own textToDoc path.

Changes

  • apps/oxfmt/Cargo.toml — Added oxc_ast_visit dependency
  • apps/oxfmt/src/api/text_to_doc_api.rs — Call new function instead of printed_string_to_hardline_doc
  • apps/oxfmt/src/prettier_compat/to_prettier_doc.rs — Added printed_string_to_doc_with_template_literals(), TemplateElementCollector visitor, removed old printed_string_to_hardline_doc()
  • apps/oxfmt/test/api/js-in-vue.test.ts — Added idempotency test

Reproduction

https://github.com/vinayakkulkarni/oxfmt-template-literal-repro

Verification

  • cargo c
  • cargo c --no-default-features
  • cargo c --features detect_code_removal
  • cargo clippy -p oxfmt
  • cargo t -p oxfmt — 32/32 passed ✅
  • pnpm build-test && pnpm t — 241/241 passed ✅

@github-actions github-actions bot added A-cli Area - CLI A-formatter Area - Formatter C-bug Category - Bug labels Mar 6, 2026
@vinayakkulkarni vinayakkulkarni force-pushed the fix/oxfmt-template-literal-idempotency branch 3 times, most recently from 7032f60 to 52b611b Compare March 6, 2026 23:07
When `vueIndentScriptAndStyle: true`, Prettier wraps the `textToDoc()`
result with `indent()`. Template literal newlines joined with `hardline`
would respect this indent, causing +2 spaces per format pass (infinite
indentation growth).

Re-parse the formatted output to identify `TemplateElement` spans and
use `literalline` (which ignores parent indent) for newlines within
template literals, while keeping `hardline` for all other newlines.

Closes oxc-project#20084.
@vinayakkulkarni vinayakkulkarni force-pushed the fix/oxfmt-template-literal-idempotency branch from 52b611b to 49c2309 Compare March 7, 2026 08:02
@Dunqing Dunqing requested a review from leaysgur March 9, 2026 01:01
graphite-app bot pushed a commit that referenced this pull request Mar 12, 2026
Fixes #20084, closes #20085

Currently, in the context of `js-in-vue`, there are 2 types of code paths:

- Script block: `run_full()`
  - Converts a **string** formatted by `oxc_formatter` into Prettier's Doc format by splitting it into lines, joining with `hardline`
- Other cases: `run_fragment()`
  - Converts the **IR** formatted by `oxc_formatter` into Prettier's Doc format through an IR -> Doc implementation

The current issue can be described as a bug in the former.

As a result of simply splitting by lines, newlines within strings (newlines within `TemplateLiteral`s) are also treated as `hardline` (which are affected by parent indentation, it should be `literalline`), causing the nesting to deepen with each format.

To fix this, the former code path now also goes through an IR -> Doc conversion process.

Additionally, by doing this, the `printWidth` is also handled correctly.

In #20085, which involves re-converting a formatted string into an AST and then traversing it, was deemed undesirable.
@graphite-app graphite-app bot closed this in 88ee826 Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-cli Area - CLI A-formatter Area - Formatter C-bug Category - Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

oxfmt: non-idempotent formatting of template literals in Vue SFC with vueIndentScriptAndStyle

2 participants