Skip to content

Commit

Permalink
feat(semantic/jsdoc): Handle optional type syntax for type name part (#…
Browse files Browse the repository at this point in the history
…2960)

It seems `JSDocTypeNamePart` can contain whitespace like...

```js
/** @Property [cfg.n12="default value"] Config... */
```
  • Loading branch information
leaysgur authored Apr 15, 2024
1 parent ec1ca3a commit 40af2b1
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 16 deletions.
22 changes: 18 additions & 4 deletions crates/oxc_semantic/src/jsdoc/parser/jsdoc_parts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand All @@ -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
}
}
Expand Down Expand Up @@ -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]"),
] {
Expand All @@ -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);
Expand Down
30 changes: 25 additions & 5 deletions crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
85 changes: 78 additions & 7 deletions crates/oxc_semantic/src/jsdoc/parser/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {
Expand All @@ -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);
}
Expand Down

0 comments on commit 40af2b1

Please sign in to comment.