Skip to content

Commit 8ad3430

Browse files
committed
fix(semantic/jsdoc): handle even-numbered backtick sequences in JSDoc parsing (#19664)
## Summary Fixes #19137. - The JSDoc parser only handled odd-numbered backtick sequences (1, 3, 5, ...) for toggling in/out of code sections. Double backticks (`` `` ``) used in CommonMark to escape backticks inside inline code were silently ignored, causing the parser to stay in "code mode" and miss all subsequent `@tags`. - Replace the boolean `in_backticks` flag with a `backtick_count` tracker that records the opening sequence length and only closes when an equal-length closing sequence is found, matching CommonMark semantics. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 655c38f commit 8ad3430

File tree

2 files changed

+49
-9
lines changed

2 files changed

+49
-9
lines changed

crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,33 @@ line2
329329
assert_eq!(tag.kind.parsed(), "example");
330330
}
331331

332+
// https://github.com/oxc-project/oxc/issues/19137
333+
#[test]
334+
fn parses_with_double_backticks() {
335+
let source = "\
336+
* Handle whitespace rendering based on meta string
337+
*
338+
* `` ```js :whitespace[=all|boundary|trailing] ``
339+
*
340+
* @param parser - Code parser instance
341+
* @param meta - Meta string
342+
* @param globalOption - Global whitespace option
343+
*
344+
* @example
345+
* ```ts
346+
* metaWhitespace(parser, ':whitespace=all', true)
347+
* ```
348+
";
349+
#[expect(clippy::cast_possible_truncation)]
350+
let jsdoc = super::JSDoc::new(source, Span::new(0, source.len() as u32));
351+
let tags = jsdoc.tags();
352+
assert_eq!(tags.len(), 4);
353+
assert_eq!(tags[0].kind.parsed(), "param");
354+
assert_eq!(tags[1].kind.parsed(), "param");
355+
assert_eq!(tags[2].kind.parsed(), "param");
356+
assert_eq!(tags[3].kind.parsed(), "example");
357+
}
358+
332359
#[test]
333360
fn parses_issue_11992() {
334361
let allocator = Allocator::default();

crates/oxc_semantic/src/jsdoc/parser/parse.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ pub fn parse_jsdoc(
3636

3737
// Tracks whether we're currently inside backticks `...`
3838
// This includes inline code blocks or markdown-style code inside comments.
39-
let mut in_backticks = false;
39+
// When 0, we're outside backticks. When > 0, stores the count of opening backticks,
40+
// so we can match the closing sequence (per CommonMark, backtick strings must match).
41+
let mut backtick_count: u32 = 0;
4042

4143
// Track whether we're currently inside quotes '...' or "..."
4244
// This includes package names when doing a @import
@@ -67,21 +69,32 @@ pub fn parse_jsdoc(
6769
let can_parse = curly_brace_depth == 0
6870
&& square_brace_depth == 0
6971
&& brace_depth == 0
70-
&& !in_backticks
72+
&& backtick_count == 0
7173
&& !in_double_quotes
7274
&& !in_single_quotes;
7375

7476
match ch {
75-
// NOTE: For now, only odd backtick(s) are handled.
77+
// Handle backtick sequences of any length (per CommonMark):
7678
// - 1 backtick: inline code
77-
// - 3, 5, ... backticks: code fence
78-
// Not so common but technically, major markdown parser can handle 3 or more backticks as code fence.
79-
// (for nested code blocks)
80-
// But for now, 4, 6, ... backticks are not handled here to keep things simple...
79+
// - 2 backticks: inline code (used to escape backticks inside)
80+
// - 3+ backticks: code fence (for nested code blocks)
81+
// Opening and closing sequences must have the same number of backticks.
8182
'`' => {
82-
if chars.peek().is_some_and(|&c| c != '`') {
83-
in_backticks = !in_backticks;
83+
// Count consecutive backticks
84+
let mut count: u32 = 1;
85+
while chars.peek() == Some(&'`') {
86+
chars.next();
87+
end += 1;
88+
count += 1;
8489
}
90+
if backtick_count == 0 {
91+
// Opening a new backtick section
92+
backtick_count = count;
93+
} else if backtick_count == count {
94+
// Closing backtick section with matching count
95+
backtick_count = 0;
96+
}
97+
// Mismatched count inside a backtick section: ignore
8598
}
8699
'"' => in_double_quotes = !in_double_quotes,
87100
'\'' => in_single_quotes = !in_single_quotes,

0 commit comments

Comments
 (0)