feat(formatter): add JSDoc comment formatting support#19828
feat(formatter): add JSDoc comment formatting support#19828
Conversation
Merging this PR will degrade performance by 8.54%
Performance Changes
Comparing Footnotes
|
There was a problem hiding this comment.
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
JsdocOptionstoFormatOptionsand implements JSDoc comment formatting in theComment::fmt()hook intrivia.rs - Introduces a new
JsdocTestRunnerfor running prettier-plugin-jsdoc conformance tests, with a large fixture set (115 input/output pairs) - Extends
oxfmtrc.rswith ajsdoc: 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
|
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 |
d572f1b to
d66867f
Compare
JSDoc Benchmark Regression AnalysisInstrumented the JSDoc formatting path with per-comment timing (release build) to understand the 8.46% regression on Benchmark Files — JSDoc Comment Density
Per-Comment Timinghandle-comments.js (total JSDoc time: ~900µs out of 3.5ms baseline → ~25% overhead)
next.ts (total JSDoc time: ~420µs out of 2.7ms baseline → ~15% overhead)
Root Causes
Optimization Opportunities
|
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
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. |
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>
…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>
23ca71a to
2ccdc3b
Compare
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>
Real-World Testing ReportTested 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
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 OverheadBenchmarked with
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. MethodologyFor each repo:
Bugs Fixed in This SessionThree bugs were found and fixed during testing against evolu:
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. |

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.
descriptions/032-jsx-tsx-css) — requires embedded CSS/HTML formatter callbacks not available in the standalone conformance runnercargo run -p oxc_prettier_conformance -- --jsdocArchitecture
The formatting pipeline processes each JSDoc comment through these stages:
oxc_jsdoc(description, tags with type/name/comment)@return→@returns), normalize types (Array.<T>→T[],?Type→Type | null), normalize markdown emphasis (__bold__→**bold**,*italic*→_italic_)@typedef/@callbackgroups; reorder@paramtags to match function signature/** ... */wrapper,*prefixes, and single-line optimizationKey modules:
serialize.rs— Main pipeline orchestrator (JsdocFormatterstruct)tag_formatters.rs— Tag-specific formatters (type+name+comment, type+comment, generic, example)normalize.rs— Type/emphasis/capitalization normalizationmdast_serialize.rs— Markdown AST → formatted text serialization withtag_string_lengthoffsetwrap.rs— Word wrapping withfirst_line_offset, atomic JSDoc link tokens, table formattingembedded.rs— Embedded JS/TS/JSX/TSX code formatting in @example blocksline_buffer.rs— Single-allocation line accumulatorOptions Support
All 11 options from prettier-plugin-jsdoc are implemented:
jsdocCapitalizeDescriptiontruejsdocCommentLineStrategy"singleLine"jsdocSeparateTagGroupsfalsejsdocSeparateReturnsFromParamfalsejsdocBracketSpacingfalsejsdocDescriptionWithDotfalsejsdocAddDefaultToDescriptiontruejsdocPreferCodeFencesfalsejsdocLineWrappingStyle"greedy"jsdocDescriptionTagfalsejsdocKeepUnParseAbleExampleIndentfalseNot yet ported (no conformance fixtures):
jsdocVerticalAlignment,tsdocmode — can be added when needed.Features
tagStringLengtharchitecture — full description passes through markdown AST with first-line offset, no custom word-fittingformatType()), handle rest params, bracket spacing[name=value]defaults with "Default is `value`" suffixUpstream Fidelity
Verified against prettier-plugin-jsdoc v1.8.0 source:
TAGS_GROUP_HEAD,TAGS_GROUP_CONDITION), type normalization pipeline, rest param handling, JSDoc link protection, emphasis normalization, capitalization rulestag_string_length/first_line_offset(equivalent to upstream's!...?prefix trick), balance mode effective width calculation, greedy fallbackPerformance
LineBufferallocation per comment (one contiguousString, notVec<String>)Cow<'_, str>throughout normalization — borrows when unchanged, allocates only when modifiedneeds_mdast_parsing()heuristic)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 passcargo run -p oxc_prettier_conformance -- --jsdoc— 145/145 (100%)cargo clippy -p oxc_formatter --all-targets— cleancargo fmt -- --check— cleancargo run -p oxc_formatter --example formatter -- --jsdoc <file>🤖 Generated with Claude Code