Skip to content

feat(formatter): add JSDoc comment formatting support#19828

Draft
Dunqing wants to merge 67 commits intomainfrom
feat/jsdoc-comment-formatting
Draft

feat(formatter): add JSDoc comment formatting support#19828
Dunqing wants to merge 67 commits intomainfrom
feat/jsdoc-comment-formatting

Conversation

@Dunqing
Copy link
Member

@Dunqing Dunqing commented Feb 28, 2026

Summary

Implements native JSDoc comment formatting in oxfmt, porting behavior from prettier-plugin-jsdoc (v1.8.0). Tracked in #19702.

Conformance

145/145 (100%) compatibility with prettier-plugin-jsdoc test suite.

  • 1 fixture intentionally skipped (descriptions/032-jsx-tsx-css) — requires embedded CSS/HTML formatter callbacks not available in the standalone conformance runner
  • Run with: cargo run -p oxc_prettier_conformance -- --jsdoc

Architecture

The formatting pipeline processes each JSDoc comment through these stages:

  1. Parse — Extract JSDoc structure via oxc_jsdoc (description, tags with type/name/comment)
  2. Normalize — Canonicalize tag aliases (@return@returns), normalize types (Array.<T>T[], ?TypeType | null), normalize markdown emphasis (__bold__**bold**, *italic*_italic_)
  3. Sort & Group — Sort tags by upstream priority weights within @typedef/@callback groups; reorder @param tags to match function signature
  4. Format — Route each tag to its specific formatter (type+name+comment, type+comment, generic, example); format descriptions via markdown AST with word wrapping; format embedded code blocks
  5. Serialize — Assemble final comment with /** ... */ wrapper, * prefixes, and single-line optimization

Key modules:

  • serialize.rs — Main pipeline orchestrator (JsdocFormatter struct)
  • tag_formatters.rs — Tag-specific formatters (type+name+comment, type+comment, generic, example)
  • normalize.rs — Type/emphasis/capitalization normalization
  • mdast_serialize.rs — Markdown AST → formatted text serialization with tag_string_length offset
  • wrap.rs — Word wrapping with first_line_offset, atomic JSDoc link tokens, table formatting
  • embedded.rs — Embedded JS/TS/JSX/TSX code formatting in @example blocks
  • line_buffer.rs — Single-allocation line accumulator

Options Support

All 11 options from prettier-plugin-jsdoc are implemented:

Option Default Status
jsdocCapitalizeDescription true
jsdocCommentLineStrategy "singleLine"
jsdocSeparateTagGroups false
jsdocSeparateReturnsFromParam false
jsdocBracketSpacing false
jsdocDescriptionWithDot false
jsdocAddDefaultToDescription true
jsdocPreferCodeFences false
jsdocLineWrappingStyle "greedy" ✅ (greedy + balance)
jsdocDescriptionTag false
jsdocKeepUnParseAbleExampleIndent false

Not yet ported (no conformance fixtures): jsdocVerticalAlignment, tsdoc mode — can be added when needed.

Features

  • Description formatting: Capitalize first letter, normalize markdown emphasis, wrap text to print width
  • Markdown AST processing: Parse descriptions via mdast for heading normalization, list formatting, reference links, code blocks, blockquotes, tables
  • Tag description wrapping: Matches upstream's tagStringLength architecture — full description passes through markdown AST with first-line offset, no custom word-fitting
  • Tag normalization: 17 tag synonyms, sort by upstream priority weights within groups
  • Type formatting: Normalize JSDoc types, format via the formatter's TS parser (simulating upstream's formatType()), handle rest params, bracket spacing
  • @example formatting: Format embedded JS/TS/JSX/TSX code; delegate CSS/HTML/GraphQL/Markdown to external formatter callbacks
  • @import handling: Parse, merge by module path, sort (third-party before relative), format consolidated imports
  • @param reordering: Reorder tags to match function signature parameter order
  • Default value handling: Extract [name=value] defaults with "Default is `value`" suffix

Upstream Fidelity

Verified against prettier-plugin-jsdoc v1.8.0 source:

  • Identical: Tag synonyms (17), sort priorities (46 tags), grouping logic (TAGS_GROUP_HEAD, TAGS_GROUP_CONDITION), type normalization pipeline, rest param handling, JSDoc link protection, emphasis normalization, capitalization rules
  • Architecture aligned: Tag description wrapping uses tag_string_length / first_line_offset (equivalent to upstream's !...? prefix trick), balance mode effective width calculation, greedy fallback

Performance

  • Single LineBuffer allocation per comment (one contiguous String, not Vec<String>)
  • Cow<'_, str> throughout normalization — borrows when unchanged, allocates only when modified
  • Fast-path skip of mdast parsing for plain-text descriptions (needs_mdast_parsing() heuristic)
  • Fast-path skip of TS formatter for simple types (needs_formatter_pass() check)
  • SmallVec<[usize; 4]> for import tag indices (stack allocation for common case)

Test plan

  • cargo test -p oxc_formatter — 203 tests pass
  • cargo run -p oxc_prettier_conformance -- --jsdoc — 145/145 (100%)
  • cargo clippy -p oxc_formatter --all-targets — clean
  • cargo fmt -- --check — clean
  • Benchmark: cargo run -p oxc_formatter --example formatter -- --jsdoc <file>

🤖 Generated with Claude Code

@github-actions github-actions bot added A-cli Area - CLI A-formatter Area - Formatter C-enhancement Category - New feature or request labels Feb 28, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Feb 28, 2026

Merging this PR will degrade performance by 8.54%

❌ 2 regressed benchmarks
✅ 51 untouched benchmarks
⏩ 3 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation formatter[next.ts] 2.7 ms 2.8 ms -4.29%
Simulation formatter[handle-comments.js] 3.5 ms 3.8 ms -8.54%

Comparing feat/jsdoc-comment-formatting (870ffbf) with main (542a04a)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copilot AI review requested due to automatic review settings March 1, 2026 02:51
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements native JSDoc comment formatting support in oxc_formatter, porting behavior from prettier-plugin-jsdoc. It adds a configurable JsdocOptions struct, string-based JSDoc comment reformatting (tag normalization, description capitalization, type normalization, word wrapping, etc.), and a dedicated JSDoc conformance test runner. The PR also integrates JSDoc support into the oxfmt app configuration.

Changes:

  • Adds JsdocOptions to FormatOptions and implements JSDoc comment formatting in the Comment::fmt() hook in trivia.rs
  • Introduces a new JsdocTestRunner for running prettier-plugin-jsdoc conformance tests, with a large fixture set (115 input/output pairs)
  • Extends oxfmtrc.rs with a jsdoc: Option<bool> config field and wires it through to the formatter options

Reviewed changes

Copilot reviewed 277 out of 280 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
crates/oxc_formatter/src/options.rs Adds JsdocOptions struct and jsdoc field on FormatOptions
crates/oxc_formatter/src/formatter/trivia.rs Integrates JSDoc formatting in Comment::fmt()
crates/oxc_formatter/src/formatter/jsdoc/ New submodules: normalize, wrap, serialize, mdast_serialize
crates/oxc_jsdoc/src/parser/jsdoc_parts.rs Adds parsed_preserving_whitespace() and raw() methods
tasks/prettier_conformance/src/jsdoc.rs New JsdocTestRunner with fixture discovery and option loading
tasks/prettier_conformance/jsdoc/fixtures/ Large set of input/output fixture pairs for conformance testing
tasks/prettier_conformance/jsdoc/snapshots/jsdoc.snap.md Generated snapshot showing 115/115 compatibility
apps/oxfmt/src/core/oxfmtrc.rs Adds jsdoc: Option<bool> config field
apps/oxfmt/test/api/jsdoc.test.ts API test for CSS/HTML fenced block formatting
IMPLEMENTATION_PLAN.md Internal planning document committed to the repo
crates/oxc_formatter/build.rs Fixes has_test_files() to avoid unused import warnings
crates/oxc_formatter/Cargo.toml Adds oxc_jsdoc and markdown dependencies
Files not reviewed (1)
  • tasks/prettier_conformance/jsdoc/scripts/package-lock.json: Language not supported

@Boshen
Copy link
Member

Boshen commented Mar 1, 2026

Is this good slop?

@Boshen Boshen marked this pull request as draft March 1, 2026 03:02
@Dunqing
Copy link
Member Author

Dunqing commented Mar 1, 2026

Is this good slop?

I think it is! I steered Claude code and Codex to the right direction; now all JSDoc tests have passed, and mostly the code is aligned with upstream. But now I just looked at less code, so I assume there are sill room to improve, and also I plan to let them move some logic to oxc_jsdoc when possible so that it could benefit all that crate dependent.

@Dunqing Dunqing force-pushed the feat/jsdoc-comment-formatting branch 7 times, most recently from d572f1b to d66867f Compare March 5, 2026 04:31
@Dunqing
Copy link
Member Author

Dunqing commented Mar 9, 2026

JSDoc Benchmark Regression Analysis

Instrumented the JSDoc formatting path with per-comment timing (release build) to understand the 8.46% regression on handle-comments.js and 4.11% on next.ts.

Benchmark Files — JSDoc Comment Density

File Lines JSDoc Count Multi-line With Tags JSDoc % of file Regression
handle-comments.js 1092 10 9 9 4.2% -8.46%
next.ts 619 6 1 2 2.1% -4.11%
App.tsx 11181 27 19 4 1.0% untouched
types.ts 2371 1 1 1 0.1% untouched
core.js 427 0 0 0 0.0% untouched

Per-Comment Timing

handle-comments.js (total JSDoc time: ~900µs out of 3.5ms baseline → ~25% overhead)

# Lines Chars Result Time Note
1 L29 52 SKIP ~260µs First-call init overhead (1-line @import)
2 L31-41 326 SKIP ~55µs 11-line @typedef with 9 tags
3-7 L47-142 55-64 SKIP ~2µs each Simple @param/@returns
8 L953-968 290 FORMAT ~590µs 16-line comment with description + code block — the big one
9 L989-1002 141 FORMAT ~6µs 14-line code example
10 L1069-1072 51 SKIP ~2µs Simple 4-line

next.ts (total JSDoc time: ~420µs out of 2.7ms baseline → ~15% overhead)

# Lines Chars Result Time Note
1 L56 42 SKIP ~110µs First-call init overhead
2 L58 39 SKIP ~93µs Second-call warmup
3-4 L60,125 37-46 SKIP ~1-2µs Single-line
5 L160-163 133 FORMAT ~215µs 4-line @internal with description
6 L365 81 SKIP ~2µs Single-line

Root Causes

  1. First-call overhead (~250-300µs): The first JSDoc comment processed per file pays an initialization cost — likely lazy statics, regex compilation, or allocator warmup in the markdown/JSDoc parsing pipeline. This shows up even for SKIP'd (unchanged) comments.

  2. Markdown parsing for descriptions (~200-600µs per comment): Comments containing descriptions with paragraphs or code blocks go through format_description_mdast which does full markdown AST parsing + word wrapping. Comment Diagnostics Handling and Printing #8 in handle-comments.js alone costs ~590µs — roughly 17% of the 3.5ms baseline.

  3. Even SKIP'd comments pay a cost: format_jsdoc_comment must parse the JSDoc, sort tags, and build formatted output before comparing against the original to decide nothing changed — then returning None. The 11-line @typedef (L31-41, 326 chars, 9 tags) takes ~55µs even though it's ultimately unchanged.

Optimization Opportunities

  1. Cache/amortize first-call init: The ~250µs first-call penalty per file could be reduced by pre-initializing regex/statics.
  2. Fast-path for tags-only JSDoc: Comments with only @param/@returns tags and no description (most of handle-comments.js) could skip markdown parsing entirely.
  3. Early bail-out for unchanged comments: A quick structural fingerprint before doing full formatting could avoid the full parse→format→compare cycle for comments that won't change.

Copy link
Member Author

Dunqing commented Mar 9, 2026


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

Dunqing and others added 10 commits March 9, 2026 16:16
Implement native JSDoc comment formatting in oxfmt, inspired by
prettier-plugin-jsdoc. When enabled via `jsdoc: true` in config,
JSDoc comments are normalized and reformatted.

Features:
- Tag normalization (@return@returns, @arg@param, etc.)
- Description capitalization (configurable)
- Type whitespace normalization
- Word wrapping to printWidth
- Single-line conversion when possible
- Structured content preservation (markdown lists, code fences, tables)
- @example block preservation (verbatim)
- Optional param syntax preservation ([name], [name = default])

Closes #19702

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add conformance tests against the prettier-plugin-jsdoc test suite to
validate our JSDoc formatting implementation. Extracts 116 test cases
from the plugin's test files and compares oxfmt output against the
plugin's expected output.

Initial result: 24/116 (20.69%) compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Major improvements to JSDoc comment formatting to match prettier-plugin-jsdoc:
- Advanced type normalization (Array.<T> → T[], nullable, rest, union types)
- Type quote normalization and object property unquoting
- Markdown emphasis normalization (__bold__ → **bold**)
- Backslash unescaping in descriptions
- Table formatting with column alignment
- Fenced/indented code block handling
- List item wrapping with proper continuation indent
- {@link} atomic token preservation during wrapping
- Tag sort order matching plugin's TAGS_ORDER weights
- Description capitalization with code/URL awareness
- Default value formatting and optional type handling
- Tag group/blank line separation logic
- Object type wrapping for multi-line types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add embedded code formatting for @example tags and fenced code blocks:
- Format JS/TS/JSX/TSX code inside @example and fenced code blocks
  using the parent formatter's options for consistent behavior
- Handle object literals by wrapping in parens with TrailingCommas::None
- Handle @description tag merging into main description
- Fix fenced code blocks in param descriptions (don't join with tag line)
- Add blank line between multi-line @example and next different tag
- Use `ret.panicked` instead of `!ret.errors.is_empty()` for parse
  error detection (allows files with semantic errors to be formatted)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
format_indented_code_blocks was re-processing lines inside fenced code
blocks and @example tags, because the formatted output from those
processors can have 4+ space indentation that looks like a markdown
indented code block. This caused spurious semicolons on JSX closing
tags and other unwanted re-formatting.

Fix: skip lines inside fenced code blocks (``` ... ```) and @example
tag regions when looking for indented code blocks to format.

JSDoc conformance: 104/116 (89.66%) → 108/116 (93.10%)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d code blocks

Thread ExternalCallbacks through to the JSDoc fenced code block
formatter so that non-JS/TS languages (CSS, HTML, GraphQL, Markdown)
can be formatted via the external Prettier callback when available.

JS/TS/JSX/TSX code continues to use the native formatter. Other
supported languages are mapped to existing external language
identifiers (e.g., "css" → "tagged-css") and delegated to Prettier.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix fenced code blocks in @example tags being parsed as JS template
  literals (triple backticks are valid JS), causing lost indentation
- Add template literal depth tracking in format_example_code to preserve
  verbatim content inside template literals
- Fix markdown emphasis markers (*word*) being stripped by JSDoc parser
  when they look like comment continuation prefixes
- Add singleQuote support for @default value formatting via QuoteStyle
- Add per-fixture options support for single_quote in conformance runner

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Convert setext headings to ATX format (Header/====== → # Header)
- Normalize single-asterisk emphasis to underscores (*text* → _text_)
- Remove space in fenced code block language tags (``` js → ```js)
- Skip code blocks when detecting setext headings to avoid false positives

Improves test 017-markdown-format similarity from 76.4% to 90.9%.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add @param reordering by function signature, horizontal rule removal,
reference link normalization, and reference definition title stripping.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dunqing and others added 22 commits March 9, 2026 16:16
…n with upstream

Format fenced and indented code blocks during MDAST traversal (in
serialize_code) instead of post-processing flat content_lines. This
matches upstream prettier-plugin-jsdoc which formats code inline when
visiting Node::Code.

Deletes format_fenced_code_blocks (~65 lines) and
format_indented_code_blocks (~90 lines) post-processing functions,
replacing them with a single format_code_value helper called from
serialize_code during MDAST serialization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…er-plugin-jsdoc

- Case-insensitive language matching (upstream's `mdAst.lang.toLowerCase()`)
- Unknown languages fall back to JS formatting (upstream's default "babel" parser)
- Add yaml/yml to external language mapping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The snapshot was stale (reported 115/115 but actual was 103/115).
No regressions — all 12 failures are pre-existing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add global `*` → `any` replacement in normalize_type_inner(), matching
  upstream's convertToModernType() which does `type.replace(/\*/g, " any ")`
- Enable format_type_via_formatter for @type/@Satisfies object literal types
  while preserving no-space-before-type for non-object types like imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…jsdoc

- Add "this" and "extends" to type_comment tags (TAGS_NAMELESS + TAGS_TYPE_NEEDED)
- Fix import specifier sort key to use alias (sort by `a.alias ?? a.name`)
- Preserve unparseable @import tags (only skip successfully parsed ones)
- Add TAGS_GROUP_CONDITION gating for group splitting
- Separate defaultValue (46) and typeParam (56) sort weights
- Match upstream TAGS_PEV_FORMATE_DESCRIPTION for capitalization skip list
- Handle named generic tags: extract name field, only capitalize description
- Use format_options.line_width instead of hardcoded 80 for type formatting
- Remove non-upstream "memberOf" synonym mapping
- Fix incorrect test fixture (011-bad-defined-name: upstream capitalizes typedef descriptions)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…redundant work

- Hoist FormatOptions clone in format_embedded_js (1 clone instead of up to 4)
- Pre-build type-formatter options once per comment instead of per tag
- Cache type_name_comment() results in reorder_param_tags (1 parse vs 4 per tag)
- Add single-group fast path in sort_tags_by_groups (skip Vec-of-Vec)
- Replace FxHashSet<usize> with SmallVec<[usize; 4]> for import indices
- Add lazy allocation in normalize_markdown_emphasis via dry-run scan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…and add --jsdoc flag to example

- Thread allocator from format_jsdoc_comment into format_type_via_formatter
  to avoid creating a new bump arena per complex type
- Make FormatOptions clone lazy (only when needs_formatter_pass returns true)
- Add --jsdoc flag to formatter example for easy JSDoc formatting testing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract LineBuffer to replace Vec<String> for content_lines, eliminating
  per-line heap allocations and enabling direct writes via begin_line()
- Replace format!/write! with direct push/push_str throughout serialize.rs
- Use alloc_concat_strs_array for arena-allocated string concatenation
  (bracket-wrapped names, type formatter input, embedded JS wrappers)
- Convert Cow<str> to &str for name_str and default_value in tag formatting
- Convert Vec<String> to Vec<&str> for jsdoc link placeholders
- Pre-build type_format_options once to eliminate per-tag FormatOptions clone
- Add itoa dependency for zero-allocation integer formatting in mdast
- Build table rows directly into String instead of via Vec<String> + join
- Replace format! with String::with_capacity in normalize.rs type handling
- Fix clippy warnings: inclusive ranges, needless borrows, needless range loops
- Fix bugs: push_indented_desc empty desc handling, dead branch removal,
  trailing blank line trimming

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d fix URL capitalization

Split the 2700-line serialize.rs into 5 focused modules:
- serialize.rs: orchestrator, tag classification, sorting, type wrapping
- param_order.rs: @param tag reordering and function param extraction
- tag_formatters.rs: format_type_name_comment_tag, format_type_comment_tag, format_generic_tag, format_example_tag
- embedded.rs: format_embedded_js, format_type_via_formatter, language detection
- imports.rs: @import tag parsing, merging, and formatting

Also fix capitalize_first URL check to be case-insensitive, matching
upstream's /^https?:\/\//i regex (e.g. "HTTP://example.com" is no
longer incorrectly capitalized).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ter passing

Replace the pattern of threading 9-11 parameters through every jsdoc
formatting function with a JsdocFormatter struct that owns the shared
per-comment state (options, format_options, allocator, wrap_width,
content_lines, etc.).

- Define JsdocFormatter<'a, 'o> in serialize.rs with two lifetimes:
  'a for allocator (tied to output) and 'o for options/callbacks
- Convert push_indented_desc and wrap_type_expression to methods
- Convert all 6 tag_formatters.rs functions to impl JsdocFormatter methods
- Simplify format_jsdoc_comment to accept &Formatter directly,
  reducing the call site from 8 arguments to 5

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add support for: jsdocAddDefaultToDescription, jsdocCommentLineStrategy,
jsdocDescriptionTag, jsdocDescriptionWithDot, jsdocKeepUnParseAbleExampleIndent,
jsdocLineWrappingStyle (balance mode), and jsdocPreferCodeFences.

Includes 145 conformance test fixtures and harness updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…hitecture

Replace custom word-fitting loops in tag_formatters.rs with upstream's
approach: pass full description through wrap_text with a tag_string_length
parameter that controls first-line offset via the markdown AST pipeline.

- Add first_line_offset param to wrap_paragraph for first-line width reduction
- Add tag_string_length to format_description_mdast/SerializeOptions/wrap_text
- Thread first_para_offset through serialize_children → serialize_node →
  serialize_paragraph (greedy wrapping uses offset; balance mode does not)
- Replace word-fitting in format_type_name_comment_tag, format_type_comment_tag,
  and format_generic_tag with wrap_text(..., tag_str_len, ...)
- Delete is_structural_line_start() and remove join_words/tokenize_words imports
- Fix balance mode to measure restored placeholder widths (not short placeholders)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Null out sort_imports/sort_tailwindcss in FormatOptions for embedded
code since they are unused and their Vec fields make cloning expensive.
Merge the `=>` check into the single-pass byte loop in
needs_formatter_pass to avoid a second string scan.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- str_width: fast path for ASCII strings (skip encode_utf16)
- type_format_options: null out Vec-containing fields (sort_imports,
  sort_tailwindcss) to make FormatOptions::clone() cheap
- format_type_via_formatter: early return None when result equals input
- jsdoc_parts parsed()/parsed_preserving_whitespace(): eliminate
  intermediate Vec allocation and double iteration
- sort_tags_by_groups: skip sort when tags already in priority order
- effective_tags: pre-allocate with correct capacity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pts`)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ad of string

`format_jsdoc_comment` now returns `Option<FormattedJsdoc<'a>>` — an enum
that implements `Format` and emits `/** ... */` wrappers as IR tokens directly.

This eliminates the wasteful roundtrip where the caller in `Comment::fmt()`
had to split the assembled string back into lines and re-emit them as IR.
The comparison against the original comment uses a temporary heap `String`
that is freed immediately (not arena-allocated).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Dunqing Dunqing force-pushed the feat/jsdoc-comment-formatting branch 2 times, most recently from 23ca71a to 2ccdc3b Compare March 9, 2026 09:01
Dunqing and others added 3 commits March 9, 2026 21:10
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…orld testing

1. Fix UTF-8 byte boundary panic in `mdast_serialize.rs` placeholder
   replacement — add `is_char_boundary` check before slicing to prevent
   crash on multi-byte chars like `©`, `—`, `'` in JSDoc comments.

2. Fix `& any` injection in multi-line intersection types — strip JSDoc
   `*` continuation prefixes from raw type content before normalization,
   preventing the `*` → `any` replacement from corrupting type expressions
   that span multiple comment lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ting

- Defer placeholder restoration to end of formatting pipeline so word
  wrapping operates on narrow placeholders instead of expanded `{@link}`
  text. Fixes spurious space insertion (`{@link Foo} -related`) and
  overly aggressive line breaking around `{@link}` tokens.
- Stop formatting unknown-language and unlabeled fenced code blocks as
  JavaScript. Fixes JSON quotes being stripped, TypeScript generics
  mangled (`createOrder<number>` → `createOrder < number >`), and
  numbered text reformatted (`64-bit` → `64. Bit`).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Dunqing
Copy link
Member Author

Dunqing commented Mar 9, 2026

Real-World Testing Report

Tested oxfmt's JSDoc formatting against Prettier + prettier-plugin-jsdoc v1.8.0 on 5 real-world repositories to validate correctness after the latest fixes.

Correctness Results

Repository Stars JSDoc Tags JSDoc Diffs Non-JSDoc Diffs
evoluhq/evolu 3.7k ~11 0 0 files
wxt-dev/wxt 9.3k ~325 0 8 files
TypeStrong/typedoc 7.7k ~478 0 254 files
chartjs/Chart.js 65k ~495 0 66 files
sveltejs/svelte 82k ~5,733 0 3,161 files

Zero JSDoc formatting differences across ~7,000+ JSDoc tags in 5 repositories.

Non-JSDoc diffs are expected general code formatting differences (line wrapping heuristics, quote style, trailing commas) unrelated to this PR.

Performance Overhead

Benchmarked with hyperfine (multi-threaded, --check mode where available):

Repository Files JSDoc Comments Without JSDoc With JSDoc Overhead
evolu 248 ~11 16.5 ms 22.9 ms +6.4 ms (38%)
wxt 325 ~325 15.8 ms 19.5 ms +3.7 ms (23%)
TypeDoc 629 ~478 23.0 ms 27.0 ms +4.0 ms (17%)
Chart.js 739 ~1,075 21.3 ms 23.7 ms +2.4 ms (11%)
Svelte 3,235 ~5,733 92.9 ms 89.7 ms ~0 ms (noise)

Absolute overhead is 0–6 ms across all repos. All repos format in under 93 ms with JSDoc enabled. On larger codebases like Svelte (5,733 JSDoc comments), the overhead is within noise margin.

Methodology

For each repo:

  1. Shallow clone, install dependencies
  2. Install prettier-plugin-jsdoc@latest, configure .prettierrc with printWidth: 80 and the plugin
  3. Run npx prettier --write to create baseline, commit
  4. Run oxfmt --write on same files
  5. git diff to compare

Bugs Fixed in This Session

Three bugs were found and fixed during testing against evolu:

  1. {@link} spurious space{@link Buffer}-related{@link Buffer} -related and {@link Foo}){@link Foo} ). Root cause: placeholders were restored before word wrapping, so tokenize_words split them into separate tokens with space inserted between.

  2. {@link} over-aggressive wrapping — Lines with {@link ...} tokens wrapped earlier than Prettier because width was measured on the expanded text (14 chars for {@link Buffer}) instead of the placeholder (~7 chars). Root cause: same as above.

  3. Embedded code formatting — JSON code blocks had quotes stripped ("scripts"scripts), TypeScript generics mangled (createOrder<number>createOrder < number >), plain text reformatted (64-bit64. Bit). Root cause: format_code_value() fell through to JS formatting for unknown languages and unlabeled code blocks.

Fix: Deferred all placeholder restoration to the end of the formatting pipeline (matching upstream behavior), and stopped formatting unknown-language/unlabeled fenced code blocks as JavaScript.

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-enhancement Category - New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants