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... */
```
This commit is contained in:
Yuji Sugiura 2024-04-15 11:18:39 +09:00 committed by GitHub
parent ec1ca3ac98
commit 40af2b1662
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 121 additions and 16 deletions

View file

@ -124,7 +124,7 @@ impl<'a> JSDocTagTypePart<'a> {
/// Returns the type content without `{` and `}`. /// Returns the type content without `{` and `}`.
pub fn parsed(&self) -> &'a str { pub fn parsed(&self) -> &'a str {
// +1 for `{`, -1 for `}` // +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. /// Returns the type name itself.
/// `.raw` may be like `[foo = var]`, so extract the name
pub fn parsed(&self) -> &'a str { 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 self.raw
} }
} }
@ -269,8 +275,8 @@ mod test {
("{}", ""), ("{}", ""),
("{-}", "-"), ("{-}", "-"),
("{string}", "string"), ("{string}", "string"),
("{ string}", " string"), ("{ string}", "string"),
("{ bool }", " bool "), ("{ bool }", "bool"),
("{{x:1}}", "{x:1}"), ("{{x:1}}", "{x:1}"),
("{[1,2,3]}", "[1,2,3]"), ("{[1,2,3]}", "[1,2,3]"),
] { ] {
@ -282,7 +288,15 @@ mod test {
#[test] #[test]
fn type_name_part_parsed() { 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 // `Span` is not used in this test
let type_name_part = JSDocTagTypeNamePart::new(actual, SPAN); let type_name_part = JSDocTagTypeNamePart::new(actual, SPAN);
assert_eq!(type_name_part.parsed(), expect); assert_eq!(type_name_part.parsed(), expect);

View file

@ -146,7 +146,7 @@ impl<'a> JSDocTag<'a> {
None => (None, self.body_raw, self.body_span.start), 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)) => { Some((n_start, n_end)) => {
// Include whitespace for comment trimming // Include whitespace for comment trimming
let c_start = n_end; let c_start = n_end;
@ -327,7 +327,7 @@ mod test {
{t2} */", {t2} */",
Some(("t2", "{t2}")), Some(("t2", "{t2}")),
), ),
("/** @k3 { t3 } */", Some((" t3 ", "{ t3 }"))), ("/** @k3 { t3 } */", Some(("t3", "{ t3 }"))),
("/** @k4 x{t4}y */", Some(("t4", "{t4}"))), ("/** @k4 x{t4}y */", Some(("t4", "{t4}"))),
("/** @k5 {t5}} */", Some(("t5", "{t5}"))), ("/** @k5 {t5}} */", Some(("t5", "{t5}"))),
("/** @k6 */", None), ("/** @k6 */", None),
@ -403,10 +403,10 @@ c5 */",
("c4\n...", " c4\n..."), ("c4\n...", " c4\n..."),
), ),
( (
"/** @k5 {t5} n5 - c5 */", "/** @k5 {t5} n5 - c5 */",
Some(("t5", "{t5}")), Some(("t5", "{t5}")),
Some(("n5", "n5")), Some(("n5", "n5")),
("- c5", " - c5 "), ("- c5", " - c5 "),
), ),
( (
"/** @k6 "/** @k6
@ -430,7 +430,27 @@ c7 */",
("c7", "\n\nc7 "), ("c7", "\n\nc7 "),
), ),
("/** @k8 {t8} */", Some(("t8", "{t8}")), None, ("", "")), ("/** @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 allocator = Allocator::default();
let semantic = build_semantic(&allocator, source_text); let semantic = build_semantic(&allocator, source_text);

View file

@ -26,6 +26,47 @@ pub fn find_type_range(s: &str) -> Option<(usize, usize)> {
None 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 // Find inline token string as range
pub fn find_token_range(s: &str) -> Option<(usize, usize)> { pub fn find_token_range(s: &str) -> Option<(usize, usize)> {
let mut start = None; let mut start = None;
@ -49,7 +90,7 @@ pub fn find_token_range(s: &str) -> Option<(usize, usize)> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{find_token_range, find_type_range}; use super::{find_token_range, find_type_name_range, find_type_range};
#[test] #[test]
fn extract_type_part_range() { fn extract_type_part_range() {
@ -64,21 +105,51 @@ mod test {
("{{t8}", None), ("{{t8}", None),
("", None), ("", None),
("{[ true, false ]}", Some("{[ true, false ]}")), ("{[ 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); assert_eq!(find_type_range(actual).map(|(s, e)| &actual[s..e]), expect);
} }
} }
#[test]
fn extract_type_name_part_range() {
for (actual, expect) in [
("", None),
("n1", Some("n1")),
(" 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] #[test]
fn extract_token_part_range() { fn extract_token_part_range() {
for (actual, expect) in [ for (actual, expect) in [
("n1", Some("n1")), ("k1", Some("k1")),
("n2 x", Some("n2")), ("k2 x", Some("k2")),
(" n3 ", Some("n3")), (" k3 ", Some("k3")),
("n4\ny", Some("n4")), ("k4\ny", Some("k4")),
("", None), ("", None),
(" 名前5\n", Some("名前5")), (" トークン5\n", Some("トークン5")),
("\nn6\nx", Some("n6")), ("\nk6\nx", Some("k6")),
] { ] {
assert_eq!(find_token_range(actual).map(|(s, e)| &actual[s..e]), expect); assert_eq!(find_token_range(actual).map(|(s, e)| &actual[s..e]), expect);
} }