mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
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 */ ", ("", " ")),
|
||||
(" /**@*/ ", ("", "")),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user