ox_jsdoc 0.0.16

High-performance JSDoc parser with Binary AST format and lazy decoder, inspired by oxc
Documentation

ox_jsdoc

Crates.io Docs.rs License

High-performance JSDoc parser with a parser-integrated Binary AST writer and a lazy decoder, inspired by the oxc project.

ox_jsdoc parses /** ... */ comment blocks on the Rust side and emits a compact byte stream (the Binary AST). The same crate provides a Rust-side lazy decoder so callers can walk the tree without materializing every node up front. The byte stream is also the format consumed by the JS-side @ox-jsdoc/decoder package, making the same parser usable from Rust, Node.js (via ox-jsdoc), and the browser (via @ox-jsdoc/wasm).

Features

  • Binary AST — parser-integrated writer that produces a self-contained, 8-byte-aligned byte stream with shared string interning across batched comments
  • Lazy Rust-side decoder — walk the AST on demand (LazyJsdocBlock, LazySourceFile, …) without eagerly building intermediate node objects
  • Batch parsing — amortize parser setup, intern common strings (*, */, tag names) once across many comments
  • Standalone type expression parserparse_type_expression / parse_type_check for parsing JSDoc type strings without a surrounding comment
  • compat_mode — opt-in @es-joy/jsdoccomment-compatible output for downstream tooling

Installation

[dependencies]
ox_jsdoc = "0.0.13"
oxc_allocator = "0.123"

Quick start

Parse a single block

use oxc_allocator::Allocator;
use ox_jsdoc::parser::{parse, ParseOptions};

let arena = Allocator::default();
let result = parse(
    &arena,
    "/** @param {string} id - The user ID */",
    ParseOptions::default(),
);

let root = result.lazy_root.expect("parsed");
let tag = root.tags().next().expect("tag");
assert_eq!(tag.tag().value(), "param");
assert_eq!(tag.description(), Some("The user ID"));
assert!(result.diagnostics.is_empty());

Parse a batch of blocks

use oxc_allocator::Allocator;
use ox_jsdoc::parser::{parse_batch, BatchItem, ParseOptions};

let arena = Allocator::default();
let items = [
    BatchItem { source_text: "/** @param {string} a */", base_offset: 0 },
    BatchItem { source_text: "/** @returns {void} */",   base_offset: 100 },
];
let result = parse_batch(&arena, &items, ParseOptions::default());

for (i, root) in result.lazy_roots.iter().enumerate() {
    let Some(root) = root else {
        // parse failure for items[i]; check `result.diagnostics`.
        continue;
    };
    if let Some(tag) = root.tags().next() {
        println!("item {i}: @{}", tag.tag().value());
    }
}

Reuse a BinaryWriter

For hot loops (lint runners, watch mode), construct a BinaryWriter once and reuse it across calls so the per-call writer setup cost (string-table prelude, arena buffer init) is amortized:

use oxc_allocator::Allocator;
use ox_jsdoc::parser::{parse_into, ParseOptions};
use ox_jsdoc::writer::BinaryWriter;

let arena = Allocator::default();
let mut writer = BinaryWriter::new(&arena);

for src in ["/** ok */", "/** @param {string} id */"] {
    let result = parse_into(&arena, src, ParseOptions::default(), &mut writer);
    let _ = result.lazy_root;
}

The matching batch entry is parse_batch_into.

Bytes-only API (binding-friendly)

parse_to_bytes and parse_batch_to_bytes skip the arena round-trip and return an owned Vec<u8> directly. This is the API the NAPI / WASM bindings consume so the bytes can be moved into a Uint8Array without an extra copy:

use ox_jsdoc::parser::{parse_to_bytes, ParseOptions};

let result = parse_to_bytes("/** ok */", ParseOptions::default());
let _bytes: Vec<u8> = result.binary_bytes;
// `bytes` is the canonical Binary AST byte stream — the JS-side
// `@ox-jsdoc/decoder` package can lazily read it.

Standalone type expressions

use ox_jsdoc::parser::{parse_type_check, parse_type_expression, type_data::ParseMode};

assert_eq!(
    parse_type_expression("string | number", ParseMode::Typescript),
    Some("string | number".to_string()),
);
assert!(parse_type_check("Array<string>", ParseMode::Typescript));
assert!(!parse_type_check("not a type {{", ParseMode::Jsdoc));

Public API map

Module / item Purpose
parser::parse / parse_into Parse one comment, return a ParseResult (lazy root + diagnostics + source file). parse_into reuses a caller-supplied BinaryWriter.
parser::parse_to_bytes Per-comment bytes-only entry point (Vec<u8> + diagnostics). Used by NAPI / WASM bindings.
parser::parse_batch / parse_batch_into Parse N comments into one shared Binary AST buffer + lazy roots.
parser::parse_batch_to_bytes Per-batch bytes-only entry point.
parser::parse_type_expression / parse_type_check Parse a standalone type expression (no surrounding comment).
parser::ParseOptions compat_mode, parse_types, type_parse_mode, preserve_whitespace, fence_aware, base_offset.
decoder::source_file::LazySourceFile Wraps the Binary AST byte buffer and exposes the cached header.
decoder::nodes::comment_ast::LazyJsdocBlock Lazy root accessors (tags(), description(), inline_tags(), …).
writer::BinaryWriter Re-usable writer; arena-backed, shared by parse_into / parse_batch_into.
format::* Wire-format constants (header, node record layout, kind tags, extended-data).

See the full API documentation on docs.rs for the lazy decoder node hierarchy and per-node accessors.

Design

ox_jsdoc is the canonical Rust core of the ox-jsdoc workspace, which also publishes the corresponding NAPI binding (ox-jsdoc on npm) and WASM binding (@ox-jsdoc/wasm on npm). All three share this crate's parser, byte format, and decoder.

The design rationale (Binary AST format, lazy decoder, batch sharing, NAPI / WASM transport, jsdoccomment compatibility) lives under design/:

The original typed AST + JSON implementation is preserved as the workspace-internal ox_jsdoc_origin crate (publish = false) for benchmark / reference comparison only.

Minimum Supported Rust Version (MSRV)

Tracks the workspace rust-version. See the workspace Cargo.toml.

License

MIT