diff --git a/crates/oxc_linter/src/rules/jsdoc/check_tag_names.rs b/crates/oxc_linter/src/rules/jsdoc/check_tag_names.rs index 6195ef9f02665..4eeeca9d18db0 100644 --- a/crates/oxc_linter/src/rules/jsdoc/check_tag_names.rs +++ b/crates/oxc_linter/src/rules/jsdoc/check_tag_names.rs @@ -631,6 +631,27 @@ fn test() { None, None, ), + ( + " + /** + * @license bcrypt.js (c) 2013 Daniel Wirtz + * Released under the Apache License, Version 2.0 + */ + function quux () { } + ", + None, + None, + ), + ( + " + /** + * @see Uses @vue/shared package + */ + function quux () { } + ", + None, + None, + ), ]; let fail = vec![ diff --git a/crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs b/crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs index 5a25d58db67e2..5b7b8a5ba9630 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs @@ -82,7 +82,12 @@ line2 #[test] fn jsdoc_comment() { for (source_text, parsed, span_text, tag_len) in [ - ("/** single line @k1 c1 @k2 */", "single line", " single line ", 2), + ( + "/** single line @k1 c1 @k2 */", + "single line @k1 c1 @k2", + " single line @k1 c1 @k2 ", + 0, + ), ( "/** * multi @@ -162,9 +167,9 @@ line2 " /** ハロー @comment だよ*/ ", - "ハロー", - " ハロー ", - 1, + "ハロー @comment だよ", + " ハロー @comment だよ", + 0, ), ] { let allocator = Allocator::default(); diff --git a/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs b/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs index 8c0a97498cb6b..1951d0dff3a46 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs @@ -200,15 +200,6 @@ mod test { #[test] fn jsdoc_tag_span() { for (source_text, tag_span_text) in [ - ( - " - /** - * multi - * line @k1 c1 - */ - ", - "@k1 c1\n ", - ), ( " /** @@ -228,7 +219,6 @@ mod test { ", "@k3 c3\n ", ), - ("/** single line @k4 c4 */", "@k4 c4 "), ( " /** @@ -257,8 +247,6 @@ mod test { #[test] fn jsdoc_tag_kind() { for (source_text, tag_kind, tag_kind_span_text) in [ - ("/** single line @k1 c1 */", "k1", "@k1"), - ("/** single line @k2*/", "k2", "@k2"), ( "/** * multi @@ -269,16 +257,8 @@ mod test { "k3", "@k3", ), - ( - "/** - * multi - * line @k4 - */", - "k4", - "@k4", - ), (" /**@*/ ", "", "@"), - (" /**@@*/ ", "", "@"), + (" /**@@*/ ", "@", "@@"), (" /** @あいう え */ ", "あいう", "@あいう"), ] { let allocator = Allocator::default(); @@ -294,8 +274,6 @@ mod test { #[test] fn jsdoc_tag_comment() { for (source_text, parsed_comment_part) in [ - ("/** single line @k1 c1 */", ("c1", " c1 ")), - ("/** single line @k2*/", ("", "")), ( "/** * multi @@ -305,13 +283,6 @@ mod test { */", ("c3a\nc3b", " c3a\n * c3b\n "), ), - ( - "/** - * multi - * line @k4 - */", - ("", "\n "), - ), ("/**@k5 c5 w/ {@inline}!*/", ("c5 w/ {@inline}!", " c5 w/ {@inline}!")), (" /**@k6 */ ", ("", " ")), (" /**@*/ ", ("", "")), diff --git a/crates/oxc_semantic/src/jsdoc/parser/parse.rs b/crates/oxc_semantic/src/jsdoc/parser/parse.rs index a7392827830a4..8bb3c9c376333 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/parse.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/parse.rs @@ -43,6 +43,13 @@ pub fn parse_jsdoc( let mut in_double_quotes = false; let mut in_single_quotes = false; + // Tracks whether we're at the logical start of a line. + // Only `@` at the start of a line (after optional whitespace and `*` markers) + // should be treated as a new tag. This prevents false positives from `@` in + // email addresses (e.g. `user@example.com`) or npm scoped packages + // (e.g. `@vue/shared`) appearing mid-line in tag descriptions. + let mut at_line_start = true; + // This flag tells us if we have already found the main comment block. // The first part before any @tags is considered the comment. Everything after is a tag. let mut comment_found = false; @@ -89,7 +96,7 @@ pub fn parse_jsdoc( '[' => square_brace_depth += 1, ']' => square_brace_depth = square_brace_depth.saturating_sub(1), - '@' if can_parse => { + '@' if can_parse && at_line_start => { let part = &source_text[start..end]; let span = Span::new( jsdoc_span_start + u32::try_from(start).unwrap_or_default(), @@ -110,6 +117,16 @@ pub fn parse_jsdoc( _ => {} } + // Update line-start tracking: + // - `\n` resets to true (new line) + // - Whitespace and `*` preserve true (JSDoc line leaders like ` * `) + // - Everything else sets to false + if ch == '\n' { + at_line_start = true; + } else if at_line_start && !matches!(ch, ' ' | '\t' | '\r' | '*') { + at_line_start = false; + } + // Move the `end` pointer forward by the character's length end += ch.len_utf8(); }