fix(linter/jsdoc): false positive in check-tag-names for @ in email addresses and npm scopes (#19021)

## Summary
- Only recognize `@` as a JSDoc tag start when it appears at the logical start of a line (after optional whitespace and `*` markers), matching the JSDoc specification for block tags
- Fixes false positives where email addresses (e.g. `<dcode@dcode.io>`) and npm scoped packages (e.g. `@vue/shared`) in tag descriptions were incorrectly parsed as new tags

Closes #18380

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Boshen committed 2026-02-06 04:59:45 +00:00
1 parent b13bb70db7
commit 6067a49acf
4 files changed
+49 -35

No files matched your search

@@ -631,6 +631,27 @@ fn test() {
None,
None,
),
(
"
/**
* @license bcrypt.js (c) 2013 Daniel Wirtz <dcode@dcode.io>
* Released under the Apache License, Version 2.0
*/
function quux () { }
",
None,
None,
),
(
"
/**
* @see Uses @vue/shared package
*/
function quux () { }
",
None,
None,
),
];
let fail = vec![
@@ -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();
@@ -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 */ ", ("", " ")),
(" /**@*/ ", ("", "")),
+18 -1
View File
@@ -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();
}