From 40af2b16625feffd5b83df38de019ec4f4d61eaa Mon Sep 17 00:00:00 2001 From: Yuji Sugiura <6259812+leaysgur@users.noreply.github.com> Date: Mon, 15 Apr 2024 11:18:39 +0900 Subject: [PATCH] feat(semantic/jsdoc): Handle optional type syntax for type name part (#2960) It seems `JSDocTypeNamePart` can contain whitespace like... ```js /** @property [cfg.n12="default value"] Config... */ ``` --- .../src/jsdoc/parser/jsdoc_parts.rs | 22 ++++- .../src/jsdoc/parser/jsdoc_tag.rs | 30 +++++-- crates/oxc_semantic/src/jsdoc/parser/utils.rs | 85 +++++++++++++++++-- 3 files changed, 121 insertions(+), 16 deletions(-) diff --git a/crates/oxc_semantic/src/jsdoc/parser/jsdoc_parts.rs b/crates/oxc_semantic/src/jsdoc/parser/jsdoc_parts.rs index e69f273bd70c8..0a89bd7c80987 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/jsdoc_parts.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/jsdoc_parts.rs @@ -124,7 +124,7 @@ impl<'a> JSDocTagTypePart<'a> { /// Returns the type content without `{` and `}`. pub fn parsed(&self) -> &'a str { // +1 for `{`, -1 for `}` - &self.raw[1..self.raw.len() - 1] + self.raw[1..self.raw.len() - 1].trim() } } @@ -141,7 +141,13 @@ impl<'a> JSDocTagTypeNamePart<'a> { } /// Returns the type name itself. + /// `.raw` may be like `[foo = var]`, so extract the name pub fn parsed(&self) -> &'a str { + if self.raw.starts_with('[') { + let inner = self.raw.trim_start_matches('[').trim_end_matches(']').trim(); + return inner.split_once('=').map_or(inner, |(v, _)| v.trim()); + } + self.raw } } @@ -269,8 +275,8 @@ mod test { ("{}", ""), ("{-}", "-"), ("{string}", "string"), - ("{ string}", " string"), - ("{ bool }", " bool "), + ("{ string}", "string"), + ("{ bool }", "bool"), ("{{x:1}}", "{x:1}"), ("{[1,2,3]}", "[1,2,3]"), ] { @@ -282,7 +288,15 @@ mod test { #[test] fn type_name_part_parsed() { - for (actual, expect) in [("foo", "foo"), ("Bar", "Bar"), ("変数", "変数")] { + for (actual, expect) in [ + ("foo", "foo"), + ("Bar", "Bar"), + ("変数", "変数"), + ("[opt]", "opt"), + ("[ opt2 ]", "opt2"), + ("[def1 = [ 1 ]]", "def1"), + (r#"[def2 = "foo bar"]"#, "def2"), + ] { // `Span` is not used in this test let type_name_part = JSDocTagTypeNamePart::new(actual, SPAN); assert_eq!(type_name_part.parsed(), expect); diff --git a/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs b/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs index 94849cf4cb457..84c5d1c42b735 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs @@ -146,7 +146,7 @@ impl<'a> JSDocTag<'a> { None => (None, self.body_raw, self.body_span.start), }; - let (name_part, comment_part) = match utils::find_token_range(name_comment_content) { + let (name_part, comment_part) = match utils::find_type_name_range(name_comment_content) { Some((n_start, n_end)) => { // Include whitespace for comment trimming let c_start = n_end; @@ -327,7 +327,7 @@ mod test { {t2} */", Some(("t2", "{t2}")), ), - ("/** @k3 { t3 } */", Some((" t3 ", "{ t3 }"))), + ("/** @k3 { t3 } */", Some(("t3", "{ t3 }"))), ("/** @k4 x{t4}y */", Some(("t4", "{t4}"))), ("/** @k5 {t5}} */", Some(("t5", "{t5}"))), ("/** @k6 */", None), @@ -403,10 +403,10 @@ c5 */", ("c4\n...", " c4\n..."), ), ( - "/** @k5 {t5} n5 - c5 */", + "/** @k5 {t5} n5 - c5 */", Some(("t5", "{t5}")), Some(("n5", "n5")), - ("- c5", " - c5 "), + ("- c5", " - c5 "), ), ( "/** @k6 @@ -430,7 +430,27 @@ c7 */", ("c7", "\n\nc7 "), ), ("/** @k8 {t8} */", Some(("t8", "{t8}")), None, ("", "")), - ("/** @k8 n8 */", None, Some(("n8", "n8")), ("", " ")), + ("/** @k9 n9 */", None, Some(("n9", "n9")), ("", " ")), + ("/** @property n[].n10 */", None, Some(("n[].n10", "n[].n10")), ("", " ")), + ("/** @property n.n11 */", None, Some(("n.n11", "n.n11")), ("", " ")), + ( + r#"/** @property [cfg.n12="default value"] */"#, + None, + Some(("cfg.n12", r#"[cfg.n12="default value"]"#)), + ("", " "), + ), + ( + "/** @property {t13} [n = 13] c13 */", + Some(("t13", "{t13}")), + Some(("n", "[n = 13]")), + ("c13", " c13 "), + ), + ( + "/** @param {t14} [n14] - opt */", + Some(("t14", "{t14}")), + Some(("n14", "[n14]")), + ("- opt", " - opt "), + ), ] { let allocator = Allocator::default(); let semantic = build_semantic(&allocator, source_text); diff --git a/crates/oxc_semantic/src/jsdoc/parser/utils.rs b/crates/oxc_semantic/src/jsdoc/parser/utils.rs index 789ff33b5b449..ab4195be16436 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/utils.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/utils.rs @@ -26,6 +26,47 @@ pub fn find_type_range(s: &str) -> Option<(usize, usize)> { None } +// Like a token but whitespace may appear inside of optional type syntax +// e.g. `[foo = 1]`, `[bar="here inside of string"]`, `[ baz = [ "a b", "c" ] ]` +pub fn find_type_name_range(s: &str) -> Option<(usize, usize)> { + // Not optional type syntax + if !s.trim_start().starts_with('[') { + return find_token_range(s); + } + + let mut bracket = 0; + let mut start = None; + for (idx, ch) in s.char_indices() { + if ch.is_whitespace() { + if bracket != 0 { + continue; + } + + if let Some(start) = start { + return Some((start, idx)); + } + } else { + if ch == '[' { + bracket += 1; + } + if ch == ']' { + bracket -= 1; + } + + if start.is_none() { + start = Some(idx); + } + } + } + + // Everything is a token + if let Some(start) = start { + return Some((start, s.len())); + } + + None +} + // Find inline token string as range pub fn find_token_range(s: &str) -> Option<(usize, usize)> { let mut start = None; @@ -49,7 +90,7 @@ pub fn find_token_range(s: &str) -> Option<(usize, usize)> { #[cfg(test)] mod test { - use super::{find_token_range, find_type_range}; + use super::{find_token_range, find_type_name_range, find_type_range}; #[test] fn extract_type_part_range() { @@ -64,21 +105,51 @@ mod test { ("{{t8}", None), ("", None), ("{[ true, false ]}", Some("{[ true, false ]}")), + ( + "{{ +t9a: string; +t9b: number; +}}", + Some("{{\nt9a: string;\nt9b: number;\n}}"), + ), ] { assert_eq!(find_type_range(actual).map(|(s, e)| &actual[s..e]), expect); } } #[test] - fn extract_token_part_range() { + fn extract_type_name_part_range() { for (actual, expect) in [ + ("", None), ("n1", Some("n1")), - ("n2 x", Some("n2")), - (" n3 ", Some("n3")), - ("n4\ny", Some("n4")), + (" n2 ", Some("n2")), + (" n3 n3", Some("n3")), + ("[n4]\n", Some("[n4]")), + ("[n5 = 1]", Some("[n5 = 1]")), + (" [n6 = [1,[2, [3]]]] ", Some("[n6 = [1,[2, [3]]]]")), + (r#"[n7 = "foo bar"]"#, Some(r#"[n7 = "foo bar"]"#)), + ("n.n8", Some("n.n8")), + ("n[].n9", Some("n[].n9")), + (r#"[ n10 = ["{}", "[]"] ]"#, Some(r#"[ n10 = ["{}", "[]"] ]"#)), + ("[n11... c11", Some("[n11... c11")), + ("[n12[]\nc12", Some("[n12[]\nc12")), + ("n12.n12", Some("n12.n12")), + ("n13[].n13", Some("n13[].n13")), + ] { + assert_eq!(find_type_name_range(actual).map(|(s, e)| &actual[s..e]), expect); + } + } + + #[test] + fn extract_token_part_range() { + for (actual, expect) in [ + ("k1", Some("k1")), + ("k2 x", Some("k2")), + (" k3 ", Some("k3")), + ("k4\ny", Some("k4")), ("", None), - (" 名前5\n", Some("名前5")), - ("\nn6\nx", Some("n6")), + (" トークン5\n", Some("トークン5")), + ("\nk6\nx", Some("k6")), ] { assert_eq!(find_token_range(actual).map(|(s, e)| &actual[s..e]), expect); }