diff --git a/crates/oxc_semantic/src/jsdoc/finder.rs b/crates/oxc_semantic/src/jsdoc/finder.rs index b3902e785..12c918993 100644 --- a/crates/oxc_semantic/src/jsdoc/finder.rs +++ b/crates/oxc_semantic/src/jsdoc/finder.rs @@ -10,9 +10,6 @@ pub struct JSDocFinder<'a> { not_attached: Vec>, } -// NOTE: We may need to provide `get_jsdoc_comments(node)`, and also `get_jsdoc_tags(node)`. -// But, how to get parent here...? Leave it to utils/jsdoc? -// Refs: https://github.com/microsoft/TypeScript/issues/7393#issuecomment-413285773 impl<'a> JSDocFinder<'a> { pub fn new(attached: BTreeMap>>, not_attached: Vec>) -> Self { Self { attached, not_attached } diff --git a/crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs b/crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs index 91c59de22..ba7538ed8 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs @@ -1,5 +1,5 @@ use super::jsdoc_tag::JSDocTag; -use super::parse::JSDocParser; +use super::parse::parse_jsdoc; use std::cell::OnceCell; #[derive(Debug, Clone)] @@ -16,12 +16,14 @@ impl<'a> JSDoc<'a> { } pub fn comment(&self) -> &str { - let cache = self.cached.get_or_init(|| JSDocParser::new(self.raw).parse()); - &cache.0 + &self.parse().0 } - pub fn tags<'b>(&'b self) -> &'b Vec> { - let cache = self.cached.get_or_init(|| JSDocParser::new(self.raw).parse()); - &cache.1 + pub fn tags(&self) -> &Vec> { + &self.parse().1 + } + + fn parse(&self) -> &(String, Vec>) { + self.cached.get_or_init(|| parse_jsdoc(self.raw)) } } diff --git a/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs b/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs index e81553108..b073cd2cf 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs @@ -1,95 +1,211 @@ -use std::str::FromStr; +use super::utils; +// Initially, I attempted to parse into specific structures such as: +// - `@param {type} name comment`: `JSDocParameterTag { type, name, comment }` +// - `@returns {type} comment`: `JSDocReturnsTag { type, comment }` +// - `@whatever comment`: `JSDocUnknownTag { comment }` +// - etc... // -// JSDocTypeExpression +// However, I discovered that some use cases, like `eslint-plugin-jsdoc`, provide an option to create an alias for the tag kind. +// .e.g. Preferring `@foo` instead of `@param` // - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ParamTypeKind { - Any, - Repeated, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ParamType<'a> { - pub value: &'a str, -} - -impl<'a> ParamType<'a> { - #[allow(unused)] - pub fn kind(&self) -> Option { - ParamTypeKind::from_str(self.value).map(Option::Some).unwrap_or_default() - } -} - -impl FromStr for ParamTypeKind { - type Err = (); - - fn from_str(s: &str) -> Result { - // TODO: This might be inaccurate if the type is listed as {....string} or some variant - if s.len() > 3 && &s[0..3] == "..." { - return Ok(Self::Repeated); - } - - if s == "*" { - return Ok(Self::Any); - } - - Err(()) - } -} - -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] -pub struct Param<'a> { - pub name: &'a str, - pub r#type: Option>, -} - +// This means that: +// - We cannot parse a tag exactly as it was written +// - We cannot assume that `@param` will always map to `JSDocParameterTag` // -// Structs +// Therefore, I decided to provide a generic structure with helper methods to parse the tag according to the needs. // +// I also considered providing an API with methods like `as_param() -> JSDocParameterTag` or `as_return() -> JSDocReturnTag`, etc. +// +// However: +// - There are many kinds of tags, but most of them have a similar structure +// - JSDoc is not a strict format; it's just a comment +// - Users can invent their own tags like `@whatever {type}` and may want to parse its type +// +// As a result, I ended up providing helper methods that are fit for purpose. -// See https://github.com/microsoft/TypeScript/blob/2d70b57df4b64a3daef252abb014562e6ccc8f3c/src/compiler/types.ts#L397 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum JSDocTagKind<'a> { - Deprecated, // JSDocDeprecatedTag - Parameter(Param<'a>), // JSDocParameterTag - Unknown(&'a str), // JSDocTag -} - +/// General struct for JSDoc tag. +/// +/// `kind` can be any string like `param`, `type`, `whatever`, ...etc. +/// `raw_body` is kept as is, you can use helper methods according to your needs. #[derive(Debug, Clone, PartialEq, Eq)] pub struct JSDocTag<'a> { - pub kind: JSDocTagKind<'a>, - pub comment: String, + raw_body: &'a str, + pub kind: &'a str, } impl<'a> JSDocTag<'a> { - pub fn tag_name(&self) -> &'a str { - match self.kind { - JSDocTagKind::Deprecated => "deprecated", - JSDocTagKind::Parameter(_) => "param", - JSDocTagKind::Unknown(tag_name) => tag_name, - } + /// kind: Does not contain the `@` prefix + /// raw_body: The body part of the tag, after the `@kind {HERE_MAY_BE_MULTILINE...}` + pub fn new(kind: &'a str, raw_body: &'a str) -> JSDocTag<'a> { + debug_assert!(!kind.starts_with('@')); + Self { raw_body, kind } } - pub fn is_deprecated(&self) -> bool { - self.kind == JSDocTagKind::Deprecated + /// Use for various simple tags like `@access`, `@deprecated`, ...etc. + /// comment can be multiline. + /// + /// Variants: + /// ``` + /// @kind comment + /// @kind + /// ``` + pub fn comment(&self) -> String { + utils::trim_multiline_comment(self.raw_body) + } + + /// Use for `@type`, `@satisfies`, ...etc. + /// + /// Variants: + /// ``` + /// @kind {type} + /// @kind + /// ``` + pub fn r#type(&self) -> Option<&str> { + utils::find_type_range(self.raw_body).map(|(start, end)| &self.raw_body[start..end]) + } + + /// Use for `@yields`, `@returns`, ...etc. + /// comment can be multiline. + /// + /// Variants: + /// ``` + /// @kind {type} comment + /// @kind {type} + /// @kind comment + /// @kind + /// ``` + pub fn type_comment(&self) -> (Option<&str>, String) { + let (type_part, comment_part) = match utils::find_type_range(self.raw_body) { + Some((t_start, t_end)) => { + // +1 for `}`, +1 for whitespace + let c_start = self.raw_body.len().min(t_end + 2); + (Some(&self.raw_body[t_start..t_end]), &self.raw_body[c_start..]) + } + None => (None, self.raw_body), + }; + + (type_part, utils::trim_multiline_comment(comment_part)) + } + + /// Use for `@param`, `@property`, `@typedef`, ...etc. + /// comment can be multiline. + /// + /// Variants: + /// ``` + /// @kind {type} name comment + /// @kind {type} name + /// @kind {type} + /// @kind name comment + /// @kind name + /// @kind + /// ``` + pub fn type_name_comment(&self) -> (Option<&str>, Option<&str>, String) { + let (type_part, name_comment_part) = match utils::find_type_range(self.raw_body) { + Some((t_start, t_end)) => { + // +1 for `}`, +1 for whitespace + let c_start = self.raw_body.len().min(t_end + 2); + (Some(&self.raw_body[t_start..t_end]), &self.raw_body[c_start..]) + } + None => (None, self.raw_body), + }; + + let (name_part, comment_part) = match utils::find_token_range(name_comment_part) { + Some((n_start, n_end)) => { + // +1 for whitespace + let c_start = name_comment_part.len().min(n_end + 1); + (Some(&name_comment_part[n_start..n_end]), &name_comment_part[c_start..]) + } + None => (None, ""), + }; + + (type_part, name_part, utils::trim_multiline_comment(comment_part)) } } #[cfg(test)] mod test { - use super::{Param, ParamType, ParamTypeKind}; + use super::JSDocTag; #[test] - fn deduces_correct_param_kind() { - let param = Param { name: "a", r#type: Some(ParamType { value: "string" }) }; - assert_eq!(param.r#type.and_then(|t| t.kind()), None); + fn parses_comment() { + assert_eq!(JSDocTag::new("a", "").comment(), ""); + assert_eq!(JSDocTag::new("a", "c1").comment(), "c1"); + assert_eq!(JSDocTag::new("a", " c2 \n z ").comment(), "c2\nz"); + assert_eq!(JSDocTag::new("a", "* c3\n * \n z \n\n").comment(), "c3\nz"); + assert_eq!( + JSDocTag::new("a", "comment4 and {@inline tag}!").comment(), + "comment4 and {@inline tag}!" + ); + } - let param = Param { name: "a", r#type: Some(ParamType { value: "...string" }) }; - assert_eq!(param.r#type.and_then(|t| t.kind()), Some(ParamTypeKind::Repeated)); + #[test] + fn parses_type() { + assert_eq!(JSDocTag::new("t", "{t1}").r#type(), Some("t1")); + assert_eq!(JSDocTag::new("t", "{t2} foo").r#type(), Some("t2")); + assert_eq!(JSDocTag::new("t", " {t3 } ").r#type(), Some("t3 ")); + assert_eq!(JSDocTag::new("t", " ").r#type(), None); + assert_eq!(JSDocTag::new("t", "t4").r#type(), None); + assert_eq!(JSDocTag::new("t", "{t5 ").r#type(), None); + assert_eq!(JSDocTag::new("t", "{t6}\nx").r#type(), Some("t6")); + } - let param = Param { name: "a", r#type: Some(ParamType { value: "*" }) }; - assert_eq!(param.r#type.and_then(|t| t.kind()), Some(ParamTypeKind::Any)); + #[test] + fn parses_type_comment() { + assert_eq!(JSDocTag::new("r", "{t1} c1").type_comment(), (Some("t1"), "c1".to_string())); + assert_eq!(JSDocTag::new("r", "{t2}").type_comment(), (Some("t2"), String::new())); + assert_eq!(JSDocTag::new("r", "c3").type_comment(), (None, "c3".to_string())); + assert_eq!(JSDocTag::new("r", "c4 foo").type_comment(), (None, "c4 foo".to_string())); + assert_eq!(JSDocTag::new("r", "").type_comment(), (None, String::new())); + assert_eq!( + JSDocTag::new("r", "{t5}\nc5\n...").type_comment(), + (Some("t5"), "c5\n...".to_string()) + ); + assert_eq!( + JSDocTag::new("r", "{t6} - c6").type_comment(), + (Some("t6"), "- c6".to_string()) + ); + assert_eq!( + JSDocTag::new("r", "{{ 型: t7 }} : c7").type_comment(), + (Some("{ 型: t7 }"), ": c7".to_string()) + ); + } + + #[test] + fn parses_type_name_comment() { + assert_eq!( + JSDocTag::new("p", "{t1} n1 c1").type_name_comment(), + (Some("t1"), Some("n1"), "c1".to_string()) + ); + assert_eq!( + JSDocTag::new("p", "{t2} n2").type_name_comment(), + (Some("t2"), Some("n2"), String::new()) + ); + assert_eq!( + JSDocTag::new("p", "n3 c3").type_name_comment(), + (None, Some("n3"), "c3".to_string()) + ); + assert_eq!(JSDocTag::new("p", "").type_name_comment(), (None, None, String::new())); + assert_eq!(JSDocTag::new("p", "\n\n").type_name_comment(), (None, None, String::new())); + assert_eq!( + JSDocTag::new("p", "{t4} n4 c4\n...").type_name_comment(), + (Some("t4"), Some("n4"), "c4\n...".to_string()) + ); + assert_eq!( + JSDocTag::new("p", "{t5} n5 - c5").type_name_comment(), + (Some("t5"), Some("n5"), "- c5".to_string()) + ); + assert_eq!( + JSDocTag::new("p", "{t6}\nn6\nc6").type_name_comment(), + (Some("t6"), Some("n6"), "c6".to_string()) + ); + assert_eq!( + JSDocTag::new("p", "{t7}\nn7\nc\n7").type_name_comment(), + (Some("t7"), Some("n7"), "c\n7".to_string()) + ); + assert_eq!( + JSDocTag::new("p", "{t8}").type_name_comment(), + (Some("t8"), None, String::new()) + ); } } diff --git a/crates/oxc_semantic/src/jsdoc/parser/parse.rs b/crates/oxc_semantic/src/jsdoc/parser/parse.rs index 00dbc4173..aa709341f 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/parse.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/parse.rs @@ -1,167 +1,97 @@ -use super::jsdoc_tag::{JSDocTag, JSDocTagKind}; -use super::jsdoc_tag::{Param, ParamType}; +use super::jsdoc_tag::JSDocTag; use super::utils; -#[derive(Debug)] -pub struct JSDocParser<'a> { - source_text: &'a str, - current: usize, +/// source_text: Inside of /**HERE*/, NOT includes `/**` and `*/` +pub fn parse_jsdoc(source_text: &str) -> (String, Vec) { + debug_assert!(!source_text.starts_with("/**")); + debug_assert!(!source_text.ends_with("*/")); + + // JSDoc consists of comment and tags. + // - Comment goes first, and tags(`@xxx`) follow + // - Both can be optional + // - Each tag is also separated by whitespace + `@` + let mut comment = ""; + let mut tags = vec![]; + + // So, find `@` to split comment and each tag. + // But `@` can be found inside of `{}` (e.g. `{@see link}`), it should be distinguished. + let mut in_braces = false; + let mut comment_found = false; + let (mut start, mut end) = (0, 0); + for ch in source_text.chars() { + match ch { + '{' => in_braces = true, + '}' => in_braces = false, + '@' if !in_braces => { + let part = &source_text[start..end]; + + if comment_found { + tags.push(parse_jsdoc_tag(part)); + } else { + comment = part; + comment_found = true; + } + // Prepare for the next draft + start = end; + } + _ => {} + } + // Update the current draft + end += ch.len_utf8(); + } + + // If `@` not found, flush the last draft + if start != end { + let part = &source_text[start..end]; + + if comment_found { + tags.push(parse_jsdoc_tag(part)); + } else { + comment = part; + } + } + + (utils::trim_multiline_comment(comment), tags) } -// Refs: `parseJSDocCommentWorker()` and `doJSDocScan()` from TypeScript -// https://github.com/microsoft/TypeScript/blob/df8d755c1d76eaf0a8f1c1046a46061b53315718/src/compiler/parser.ts#L8814 -impl<'a> JSDocParser<'a> { - /// source_text: Inside of /**HERE*/, NOT includes `/**` and `*/` - pub fn new(source_text: &'a str) -> Self { - // Outer spaces can be trimmed - Self { source_text: source_text.trim(), current: 0 } - } +// TODO: Manage `Span` +// - with (start, end) + global comment span.start +// - add kind only span? +/// tag_content: Starts with `@`, may be mulitline +fn parse_jsdoc_tag(tag_content: &str) -> JSDocTag { + debug_assert!(tag_content.starts_with('@')); - pub fn parse(mut self) -> (String, Vec>) { - let comment = self.parse_comment(); - let tags = self.parse_tags(); + // This surely exists, at least `@` itself + let (k_start, k_end) = utils::find_token_range(tag_content).unwrap(); + // +1 for whitespace, may be empty + let b_start = tag_content.len().min(k_end + 1); - (comment, tags) - } - - // JSDoc comment starts with description comment until the first `@` appears - fn parse_comment(&mut self) -> String { - // TODO: Should ignore inside of inline tags like `{@link}`? - let comment = self.take_until(|c| c == '@'); - utils::trim_multiline_comment(comment) - } - - fn parse_tags(&mut self) -> Vec> { - let mut tags = vec![]; - - // Let's start with the first `@` - while let Some(c) = self.source_text[self.current..].chars().next() { - match c { - '@' => { - self.current += c.len_utf8(); - tags.push(self.parse_tag()); - } - _ => { - self.current += c.len_utf8(); - } - } - } - - tags - } - - fn parse_tag(&mut self) -> JSDocTag<'a> { - let tag_name = self.take_until(|c| c == ' ' || c == '\n' || c == '@'); - match tag_name { - // TODO: Add more tags - "arg" | "argument" | "param" => self.parse_parameter_tag(), - "deprecated" => self.parse_simple_tag(JSDocTagKind::Deprecated), - _ => self.parse_simple_tag(JSDocTagKind::Unknown(tag_name)), - } - } - - // @tag_name [] - fn parse_simple_tag(&mut self, kind: JSDocTagKind<'a>) -> JSDocTag<'a> { - let comment = self.take_until(|c| c == '@'); - let comment = utils::trim_multiline_comment(comment); - JSDocTag { kind, comment } - } - - // @param name - // @param {type} name - // @param {type} name comment - // @param {type} name - comment - fn parse_parameter_tag(&mut self) -> JSDocTag<'a> { - self.skip_whitespace(); - - let mut r#type = None; - if self.at('{') { - // If we hit a space, then treat it as the end of the type annotation. - let type_annotation = self.take_until(|c| c == '}' || c == ' ' || c == '@'); - r#type = Some(ParamType { value: type_annotation }); - if self.at('}') { - self.skip_whitespace(); - } - self.skip_whitespace(); - } - - let name = self.take_until(|c| c == ' ' || c == '\n' || c == '@'); - let param = Param { name, r#type }; - - self.skip_whitespace(); - - // JSDoc.app ignores `-` char between name and comment, but TS doesn't - // Some people use `:` as separator - if self.at('-') || self.at(':') { - self.skip_whitespace(); - } - - let comment = self.take_until(|c| c == '@'); - let comment = utils::trim_multiline_comment(comment); - JSDocTag { kind: JSDocTagKind::Parameter(param), comment } - } - - // - // Parser utils - // - fn skip_whitespace(&mut self) { - while let Some(c) = self.source_text[self.current..].chars().next() { - if c != ' ' { - break; - } - self.current += c.len_utf8(); - } - } - - fn advance(&mut self) { - if let Some(c) = self.source_text[self.current..].chars().next() { - self.current += c.len_utf8(); - } - } - - fn at(&mut self, c: char) -> bool { - if let Some(ch) = self.source_text[self.current..].chars().next() { - if ch == c { - self.advance(); - true - } else { - false - } - } else { - false - } - } - - fn take_until(&mut self, predicate: fn(char) -> bool) -> &'a str { - let start = self.current; - while let Some(c) = self.source_text[self.current..].chars().next() { - if predicate(c) { - break; - } - self.current += c.len_utf8(); - } - &self.source_text[start..self.current] - } + // Omit the first `@` + JSDocTag::new(&tag_content[k_start + 1..k_end], &tag_content[b_start..]) } #[cfg(test)] mod test { - use super::JSDocParser; - use super::{JSDocTag, JSDocTagKind}; - use super::{Param, ParamType}; + use super::parse_jsdoc; + use super::parse_jsdoc_tag; + use super::JSDocTag; fn parse_from_full_text(full_text: &str) -> (String, Vec) { // Outside of markers can be trimmed let source_text = full_text.trim().trim_start_matches("/**").trim_end_matches("*/"); - JSDocParser::new(source_text).parse() + parse_jsdoc(source_text) } #[test] fn parses_jsdoc_comment() { - assert_eq!(JSDocParser::new("hello source").parse().0, "hello source"); - assert_eq!(parse_from_full_text("/** hello full */").0, "hello full"); + assert_eq!(parse_jsdoc("hello source"), ("hello source".to_string(), vec![])); + assert_eq!( + parse_from_full_text("/** hello full_text */"), + ("hello full_text".to_string(), vec![]) + ); + assert_eq!(parse_from_full_text("/***/"), (String::new(), vec![])); - assert_eq!(JSDocParser::new(" <- trim -> ").parse().0, "<- trim ->"); + assert_eq!(parse_jsdoc(" <- trim -> ").0, "<- trim ->"); assert_eq!( parse_from_full_text( " @@ -178,65 +108,63 @@ mod test { parse_from_full_text( "/** this is -comment +comment {@link link} ... @x */" ) .0, - "this is\ncomment" + "this is\ncomment {@link link} ..." ); assert_eq!( parse_from_full_text( "/**          * 日本語とか -         * multibyte文字はどう? +         * multibyte文字はどう⁉️ */" ) .0, - "日本語とか\nmultibyte文字はどう?" + "日本語とか\nmultibyte文字はどう⁉️" ); + + assert_eq!( + parse_jsdoc("hello {@see inline} source {@a 2}").0, + "hello {@see inline} source {@a 2}" + ); + + assert_eq!(parse_jsdoc("").0, ""); } #[test] fn parses_single_line_1_jsdoc() { - assert_eq!( - JSDocParser::new("@deprecated").parse().1, - parse_from_full_text("/** @deprecated */").1, - ); - assert_eq!( - JSDocParser::new("@deprecated").parse().1, - vec![JSDocTag { kind: JSDocTagKind::Deprecated, comment: String::new() }] - ); + assert_eq!(parse_jsdoc("@deprecated"), parse_from_full_text("/** @deprecated*/")); + assert_eq!(parse_jsdoc("@deprecated").1, vec![parse_jsdoc_tag("@deprecated")]); + + assert_eq!(parse_jsdoc("").1, vec![]); assert_eq!( parse_from_full_text("/**@foo since 2024 */").1, - vec![JSDocTag { - kind: JSDocTagKind::Unknown("foo"), - comment: "since 2024".to_string() - }] - ); - assert_eq!( - parse_from_full_text("/**@*/").1, - vec![JSDocTag { kind: JSDocTagKind::Unknown(""), comment: String::new() }] + vec![parse_jsdoc_tag("@foo since 2024 ")] ); + assert_eq!(parse_from_full_text("/**@*/").1, vec![JSDocTag::new("", "")]); } #[test] fn parses_single_line_n_jsdocs() { assert_eq!( parse_from_full_text("/** @foo @bar */").1, - vec![ - JSDocTag { kind: JSDocTagKind::Unknown("foo"), comment: String::new() }, - JSDocTag { kind: JSDocTagKind::Unknown("bar"), comment: String::new() } - ] + vec![JSDocTag::new("foo", ""), JSDocTag::new("bar", "")] + ); + assert_eq!( + parse_from_full_text("/** @aiue あいうえ @o お*/").1, + vec![JSDocTag::new("aiue", "あいうえ "), JSDocTag::new("o", "お")] ); assert_eq!( parse_from_full_text("/** @a @@ @d */").1, vec![ - JSDocTag { kind: JSDocTagKind::Unknown("a"), comment: String::new() }, - JSDocTag { kind: JSDocTagKind::Unknown(""), comment: String::new() }, - JSDocTag { kind: JSDocTagKind::Unknown(""), comment: String::new() }, - JSDocTag { kind: JSDocTagKind::Unknown("d"), comment: String::new() } + JSDocTag::new("a", ""), + JSDocTag::new("", ""), + JSDocTag::new("", ""), + JSDocTag::new("d", "") ] ); } @@ -246,48 +174,42 @@ comment assert_eq!( parse_from_full_text( "/** @yo -*/" + */" ) .1, - vec![JSDocTag { kind: JSDocTagKind::Unknown("yo"), comment: String::new() }] + vec![JSDocTag::new("yo", " ")] ); assert_eq!( parse_from_full_text( "/** - * @foo - */" + * @foo + */" ) .1, - vec![JSDocTag { kind: JSDocTagKind::Unknown("foo"), comment: String::new() }] + vec![JSDocTag::new("foo", " ")] ); assert_eq!( parse_from_full_text( " - /** - * @x with asterisk - */ - " + /** + * @x with asterisk + */ + " ) .1, - vec![JSDocTag { - kind: JSDocTagKind::Unknown("x"), - comment: "with asterisk".to_string() - }] + vec![JSDocTag::new("x", "with asterisk\n ")] ); assert_eq!( parse_from_full_text( " - /** - @y without -asterisk - */ - " + /** + @y without + asterisk + */ + " ) .1, - vec![JSDocTag { - kind: JSDocTagKind::Unknown("y"), - comment: "without\nasterisk".to_string() - }] + vec![JSDocTag::new("y", "without\n asterisk\n ")] ); } @@ -297,16 +219,16 @@ asterisk parse_from_full_text( " /** - @foo @bar + @foo@bar * @baz */ " ) .1, vec![ - JSDocTag { kind: JSDocTagKind::Unknown("foo"), comment: String::new() }, - JSDocTag { kind: JSDocTagKind::Unknown("bar"), comment: String::new() }, - JSDocTag { kind: JSDocTagKind::Unknown("baz"), comment: String::new() }, + JSDocTag::new("foo", ""), + JSDocTag::new("bar", " * "), + JSDocTag::new("baz", " ") ] ); assert_eq!( @@ -316,13 +238,12 @@ asterisk * * ... * - * @two - */" + * @two */" ) .1, vec![ - JSDocTag { kind: JSDocTagKind::Unknown("one"), comment: "...".to_string() }, - JSDocTag { kind: JSDocTagKind::Unknown("two"), comment: String::new() }, + JSDocTag::new("one", " *\n * ...\n *\n * "), + JSDocTag::new("two", ""), ] ); assert_eq!( @@ -336,126 +257,11 @@ asterisk ) .1, vec![ - JSDocTag { - kind: JSDocTagKind::Unknown("hey"), - comment: "you!\nAre you OK?".to_string() - }, - JSDocTag { kind: JSDocTagKind::Unknown("yes"), comment: "I'm fine".to_string() }, - ] - ); - } - - #[test] - fn parses_parameter_tag() { - assert_eq!( - parse_from_full_text("/** @param */").1, - vec![JSDocTag { - kind: JSDocTagKind::Parameter(Param { name: "", r#type: None }), - comment: String::new(), - },] - ); - assert_eq!( - parse_from_full_text("/** @param @noop */").1, - vec![ - JSDocTag { - kind: JSDocTagKind::Parameter(Param { name: "", r#type: None }), - comment: String::new(), - }, - JSDocTag { kind: JSDocTagKind::Unknown("noop"), comment: String::new() }, - ] - ); - assert_eq!( - parse_from_full_text("/** @param name */").1, - vec![JSDocTag { - kind: JSDocTagKind::Parameter(Param { name: "name", r#type: None }), - comment: String::new(), - },] - ); - assert_eq!( - parse_from_full_text("/** @param {str} name */").1, - vec![JSDocTag { - kind: JSDocTagKind::Parameter(Param { - name: "name", - r#type: Some(ParamType { value: "str" }) - }), - comment: String::new(), - },] - ); - assert_eq!( - parse_from_full_text("/** @param {str} name comment */").1, - vec![JSDocTag { - kind: JSDocTagKind::Parameter(Param { - name: "name", - r#type: Some(ParamType { value: "str" }) - }), - comment: "comment".to_string(), - },] - ); - assert_eq!( - parse_from_full_text("/** @param {str} name comment */"), - parse_from_full_text("/** @param {str} name - comment */"), - ); - assert_eq!( - parse_from_full_text("/** @param {str} name comment */"), - parse_from_full_text( - "/** @param {str} name -comment */" - ), - ); - assert_eq!( - parse_from_full_text( - "/** @param {str} name -comment */" - ), - parse_from_full_text( - "/** - * @param {str} name - * comment - */" - ), - ); - - assert_eq!( - parse_from_full_text( - " - /** - * @param {boolean} a - * @param {string b - * @param {string} c comment - * @param {Num} d - comment2 - */ - " - ) - .1, - vec![ - JSDocTag { - kind: JSDocTagKind::Parameter(Param { - name: "a", - r#type: Some(ParamType { value: "boolean" }) - }), - comment: String::new(), - }, - JSDocTag { - kind: JSDocTagKind::Parameter(Param { - name: "b", - r#type: Some(ParamType { value: "string" }) - }), - comment: String::new(), - }, - JSDocTag { - kind: JSDocTagKind::Parameter(Param { - name: "c", - r#type: Some(ParamType { value: "string" }) - }), - comment: "comment".to_string(), - }, - JSDocTag { - kind: JSDocTagKind::Parameter(Param { - name: "d", - r#type: Some(ParamType { value: "Num" }) - }), - comment: "comment2".to_string(), - }, + JSDocTag::new( + "hey", + "you!\n * Are you OK?\n * " + ), + JSDocTag::new("yes", "I'm fine\n ") ] ); } @@ -476,44 +282,44 @@ comment */" */", ); assert_eq!(jsdoc.0, "flat tree data on expanded state"); + let mut tags = jsdoc.1.iter(); + assert_eq!(tags.len(), 7); + + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "export"); + assert_eq!(tag.comment(), ""); + + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "template"); + assert_eq!(tag.comment(), "T"); + + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "param"); + assert_eq!(tag.type_name_comment(), (Some("*"), Some("data"), ": table data".to_string())); + + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "param"); assert_eq!( - jsdoc.1, - vec![ - JSDocTag { kind: JSDocTagKind::Unknown("export"), comment: String::new() }, - JSDocTag { kind: JSDocTagKind::Unknown("template"), comment: "T".to_string() }, - JSDocTag { - kind: JSDocTagKind::Parameter(Param { - name: "data", - r#type: Some(ParamType { value: "*" }) - }), - comment: "table data".to_string(), - }, - JSDocTag { - kind: JSDocTagKind::Parameter(Param { - name: "childrenColumnName", - r#type: Some(ParamType { value: "string" }) - }), - comment: "指定树形结构的列名".to_string(), - }, - JSDocTag { - kind: JSDocTagKind::Parameter(Param { - name: "expandedKeys", - r#type: Some(ParamType { value: "Set" }) - }), - comment: "展开的行对应的keys".to_string(), - }, - JSDocTag { - kind: JSDocTagKind::Parameter(Param { - name: "getRowKey", - r#type: Some(ParamType { value: "GetRowKey" }) - }), - comment: "获取当前rowKey的方法".to_string(), - }, - JSDocTag { - kind: JSDocTagKind::Unknown("returns"), - comment: "flattened data".to_string(), - }, - ] + tag.type_name_comment(), + (Some("string"), Some("childrenColumnName"), ": 指定树形结构的列名".to_string()) ); + + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "param"); + assert_eq!( + tag.type_name_comment(), + (Some("Set"), Some("expandedKeys"), ": 展开的行对应的keys".to_string()) + ); + + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "param"); + assert_eq!( + tag.type_name_comment(), + (Some("GetRowKey"), Some("getRowKey"), ": 获取当前rowKey的方法".to_string()) + ); + + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "returns"); + assert_eq!(tag.type_comment(), (None, "flattened data".to_string())); } } diff --git a/crates/oxc_semantic/src/jsdoc/parser/utils.rs b/crates/oxc_semantic/src/jsdoc/parser/utils.rs index 0ed7fc884..853c6d4e9 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/utils.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/utils.rs @@ -7,13 +7,68 @@ pub fn trim_multiline_comment(s: &str) -> String { .join("\n") } +// For now, just returns inside of most outer braces +pub fn find_type_range(s: &str) -> Option<(usize, usize)> { + let mut start = None; + let mut brace_count = 0; + for (idx, ch) in s.char_indices() { + match ch { + '{' => { + brace_count += 1; + + if start.is_none() { + start = Some(idx + 1); + } + } + '}' => { + brace_count -= 1; + + if brace_count == 0 { + if let Some(start) = start { + return Some((start, idx)); + } + } + } + _ => {} + } + } + None +} + +// Find inline token string as range +pub fn find_token_range(s: &str) -> Option<(usize, usize)> { + let mut start = None; + for (idx, ch) in s.char_indices() { + if ch.is_whitespace() { + if let Some(start) = start { + return Some((start, idx)); + } + } else if start.is_none() { + start = Some(idx); + } + } + + // Everything is a name + if let Some(start) = start { + return Some((start, s.len())); + } + + None +} + #[cfg(test)] mod test { - use super::trim_multiline_comment; + use super::{find_token_range, find_type_range, trim_multiline_comment}; #[test] fn trim_multiline_jsdoc_comments() { for (actual, expect) in [ + ("", ""), + ( + " + +", "", + ), ("hello", "hello"), ( " @@ -55,4 +110,37 @@ mod test { assert_eq!(trim_multiline_comment(actual), expect); } } + + #[test] + fn extract_type_part_range() { + for (actual, expect) in [ + ("{t1}", Some("t1")), + ("{t2 }", Some("t2 ")), + ("{{ t3: string }}", Some("{ t3: string }")), + ("{t4} name", Some("t4")), + (" {t5} ", Some("t5")), + ("{t6 x", None), + ("t7", None), + ("{{t8}", None), + ("", None), + ("{[ true, false ]}", Some("[ true, false ]")), + ] { + assert_eq!(find_type_range(actual).map(|(s, e)| &actual[s..e]), expect); + } + } + + #[test] + fn extract_name_part_range() { + for (actual, expect) in [ + ("n1", Some("n1")), + ("n2 x", Some("n2")), + (" n3 ", Some("n3")), + ("n4\ny", Some("n4")), + ("", None), + ("名前5", Some("名前5")), + ("\nn6\nx", Some("n6")), + ] { + assert_eq!(find_token_range(actual).map(|(s, e)| &actual[s..e]), expect); + } + } }