Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(semantic/jsdoc): Handle optional type syntax for type name part #2960

Merged
merged 6 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading