Skip to content

Commit 773d0de

Browse files
committed
fix(semantic): correctly handle nested brackets in jsdoc parsing (#10922)
i doubt anyone writes stuff link this, but if you had ``` [[] @foo] ``` `@foo` would be parsed as being within a tag dispite it being inside braces
1 parent 6ad9d4f commit 773d0de

File tree

2 files changed

+49
-14
lines changed

2 files changed

+49
-14
lines changed

crates/oxc_linter/src/rules/jsdoc/check_tag_names.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,16 @@ fn test() {
573573
Some(serde_json::json!([ { "definedTags": [] } ])),
574574
None,
575575
),
576+
(
577+
"
578+
/**
579+
* @see [[[[]@foo]
580+
*/
581+
function quux (foo) { }
582+
",
583+
None,
584+
None,
585+
),
576586
];
577587

578588
let fail = vec![

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

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,41 @@ pub fn parse_jsdoc(source_text: &str, jsdoc_span_start: u32) -> (JSDocCommentPar
1717
// - Both can be optional
1818
// - Each tag is also separated by whitespace + `@`
1919
let mut comment = None;
20+
21+
// This will collect all the @tags found in the JSDoc
2022
let mut tags = vec![];
2123

22-
// So, find `@` to split comment and each tag.
23-
// But `@` can be found inside of `{}` (e.g. `{@see link}`), it should be distinguished.
24-
let mut in_braces = false;
25-
let mut in_square_braces = false;
26-
// Also, `@` is often found inside of backtick(` or ```), like markdown.
24+
// Tracks how deeply nested we are inside curly braces `{}`.
25+
// Used to ignore `@` characters inside objects or inline tag syntax like {@link ...}
26+
let mut curly_brace_depth: i32 = 0;
27+
28+
let mut brace_depth: i32 = 0;
29+
30+
// Tracks nesting inside square brackets `[]`.
31+
// Used to avoid interpreting `@` inside optional param syntax like `[param=@default]`
32+
let mut square_brace_depth: i32 = 0;
33+
34+
// Tracks whether we're currently inside backticks `...`
35+
// This includes inline code blocks or markdown-style code inside comments.
2736
let mut in_backticks = false;
37+
38+
// This flag tells us if we have already found the main comment block.
39+
// The first part before any @tags is considered the comment. Everything after is a tag.
2840
let mut comment_found = false;
29-
// Parser local offsets, not for global span
41+
42+
// These mark the current span of the "draft" being read (a comment or tag block)
3043
let (mut start, mut end) = (0, 0);
3144

45+
// Turn the source into a character iterator we can peek at
3246
let mut chars = source_text.chars().peekable();
47+
48+
// Iterate through every character in the input string
3349
while let Some(ch) = chars.next() {
34-
let can_parse = !(in_braces || in_backticks || in_square_braces);
50+
// A `@` is only considered the start of a tag if we are not nested inside
51+
// braces, square brackets, or backtick-quoted sections
52+
let can_parse =
53+
curly_brace_depth == 0 && square_brace_depth == 0 && brace_depth == 0 && !in_backticks;
54+
3555
match ch {
3656
// NOTE: For now, only odd backtick(s) are handled.
3757
// - 1 backtick: inline code
@@ -44,10 +64,13 @@ pub fn parse_jsdoc(source_text: &str, jsdoc_span_start: u32) -> (JSDocCommentPar
4464
in_backticks = !in_backticks;
4565
}
4666
}
47-
'{' => in_braces = true,
48-
'}' => in_braces = false,
49-
'[' => in_square_braces = true,
50-
']' => in_square_braces = false,
67+
'{' => curly_brace_depth += 1,
68+
'}' => curly_brace_depth = curly_brace_depth.saturating_sub(1),
69+
'(' => brace_depth += 1,
70+
')' => brace_depth = brace_depth.saturating_sub(1),
71+
'[' => square_brace_depth += 1,
72+
']' => square_brace_depth = square_brace_depth.saturating_sub(1),
73+
5174
'@' if can_parse => {
5275
let part = &source_text[start..end];
5376
let span = Span::new(
@@ -56,22 +79,24 @@ pub fn parse_jsdoc(source_text: &str, jsdoc_span_start: u32) -> (JSDocCommentPar
5679
);
5780

5881
if comment_found {
82+
// We've already seen the main comment — this is a tag
5983
tags.push(parse_jsdoc_tag(part, span));
6084
} else {
85+
// This is the first `@` we've encountered — treat what came before as the comment
6186
comment = Some(JSDocCommentPart::new(part, span));
6287
comment_found = true;
6388
}
6489

65-
// Prepare for the next draft
6690
start = end;
6791
}
6892
_ => {}
6993
}
70-
// Update the current draft
94+
95+
// Move the `end` pointer forward by the character's length
7196
end += ch.len_utf8();
7297
}
7398

74-
// If `@` not found, flush the last draft
99+
// After the loop ends, we may have one final segment left to capture
75100
if start != end {
76101
let part = &source_text[start..end];
77102
let span = Span::new(

0 commit comments

Comments
 (0)