From df744b205aa3c100f415efa4a748d4e098d1c8ad Mon Sep 17 00:00:00 2001 From: Yuji Sugiura <6259812+leaysgur@users.noreply.github.com> Date: Tue, 26 Mar 2024 19:40:31 +0900 Subject: [PATCH] feat(semantic/jsdoc): Add `Span` for JSDoc, JSDocTag (#2815) --- crates/oxc_semantic/src/jsdoc/builder.rs | 3 +- crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs | 107 ++++++++- .../src/jsdoc/parser/jsdoc_tag.rs | 58 ++--- crates/oxc_semantic/src/jsdoc/parser/parse.rs | 205 +++++++++++++----- crates/oxc_semantic/src/jsdoc/parser/utils.rs | 51 +++-- 5 files changed, 318 insertions(+), 106 deletions(-) diff --git a/crates/oxc_semantic/src/jsdoc/builder.rs b/crates/oxc_semantic/src/jsdoc/builder.rs index 75b8d0c0a4945..070635ffd6683 100644 --- a/crates/oxc_semantic/src/jsdoc/builder.rs +++ b/crates/oxc_semantic/src/jsdoc/builder.rs @@ -156,7 +156,8 @@ impl<'a> JSDocBuilder<'a> { } // Remove the very first `*` - Some(JSDoc::new(&comment_content[1..])) + let jsdoc_span = Span::new(comment_span.start + 1, comment_span.end); + Some(JSDoc::new(&comment_content[1..], jsdoc_span)) } } diff --git a/crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs b/crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs index ba7538ed82170..45658210af940 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs @@ -1,29 +1,124 @@ use super::jsdoc_tag::JSDocTag; use super::parse::parse_jsdoc; +use oxc_span::Span; use std::cell::OnceCell; #[derive(Debug, Clone)] pub struct JSDoc<'a> { raw: &'a str, /// Cached+parsed JSDoc comment and tags - cached: OnceCell<(String, Vec>)>, + cached: OnceCell<(String, Vec<(Span, JSDocTag<'a>)>)>, + pub span: Span, } impl<'a> JSDoc<'a> { /// comment_content: Inside of /**HERE*/, not include `/**` and `*/` - pub fn new(comment_content: &'a str) -> JSDoc<'a> { - Self { raw: comment_content, cached: OnceCell::new() } + /// span: `Span` for this JSDoc comment, range for `/**HERE*/` + pub fn new(comment_content: &'a str, span: Span) -> JSDoc<'a> { + Self { raw: comment_content, cached: OnceCell::new(), span } } pub fn comment(&self) -> &str { &self.parse().0 } - pub fn tags(&self) -> &Vec> { + pub fn tags(&self) -> &Vec<(Span, JSDocTag<'a>)> { &self.parse().1 } - fn parse(&self) -> &(String, Vec>) { - self.cached.get_or_init(|| parse_jsdoc(self.raw)) + fn parse(&self) -> &(String, Vec<(Span, JSDocTag<'a>)>) { + self.cached.get_or_init(|| parse_jsdoc(self.raw, self.span.start)) + } +} + +#[cfg(test)] +mod test { + use crate::{Semantic, SemanticBuilder}; + use oxc_allocator::Allocator; + use oxc_parser::Parser; + use oxc_span::SourceType; + + fn build_semantic<'a>( + allocator: &'a Allocator, + source_text: &'a str, + source_type: Option, + ) -> Semantic<'a> { + let source_type = source_type.unwrap_or_default(); + let ret = Parser::new(allocator, source_text, source_type).parse(); + let program = allocator.alloc(ret.program); + let semantic = SemanticBuilder::new(source_text, source_type) + .with_trivias(ret.trivias) + .build(program) + .semantic; + semantic + } + + #[test] + fn get_jsdoc_span() { + let allocator = Allocator::default(); + let semantic = build_semantic( + &allocator, + r" + /** single line */ + /** + * multi + * line + */ + /** +multi +line + */ + ", + Some(SourceType::default()), + ); + + let mut jsdocs = semantic.jsdoc().iter_all(); + + let jsdoc = jsdocs.next().unwrap(); + assert_eq!(jsdoc.span.source_text(semantic.source_text), " single line "); + let jsdoc = jsdocs.next().unwrap(); + assert_eq!( + jsdoc.span.source_text(semantic.source_text), + "\n * multi\n * line\n " + ); + let jsdoc = jsdocs.next().unwrap(); + assert_eq!(jsdoc.span.source_text(semantic.source_text), "\nmulti\nline\n "); + } + + #[test] + fn get_jsdoc_tag_span() { + let allocator = Allocator::default(); + let semantic = build_semantic( + &allocator, + r" + /** single line @k1 d1 */ + /** + * multi + * line + * @k2 d2 + * d2 + * @k3 d3 + * @k4 d4 + * d4 + */ + ", + Some(SourceType::default()), + ); + + let mut jsdocs = semantic.jsdoc().iter_all(); + + let jsdoc = jsdocs.next().unwrap(); + let mut tags = jsdoc.tags().iter(); + let (span, _) = tags.next().unwrap(); + assert_eq!(span.source_text(semantic.source_text), "@k1"); + + let jsdoc = jsdocs.next().unwrap(); + let mut tags = jsdoc.tags().iter(); + let (span, _) = tags.next().unwrap(); + assert_eq!(span.source_text(semantic.source_text), "@k2"); + let (span, _) = tags.next().unwrap(); + assert_eq!(span.source_text(semantic.source_text), "@k3"); + let (span, _) = tags.next().unwrap(); + assert_eq!(span.source_text(semantic.source_text), "@k4"); } } diff --git a/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs b/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs index b073cd2cf7bc3..babae889f97d4 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs @@ -36,7 +36,7 @@ pub struct JSDocTag<'a> { impl<'a> JSDocTag<'a> { /// kind: Does not contain the `@` prefix - /// raw_body: The body part of the tag, after the `@kind {HERE_MAY_BE_MULTILINE...}` + /// 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 } @@ -51,7 +51,7 @@ impl<'a> JSDocTag<'a> { /// @kind /// ``` pub fn comment(&self) -> String { - utils::trim_multiline_comment(self.raw_body) + utils::trim_comment(self.raw_body) } /// Use for `@type`, `@satisfies`, ...etc. @@ -85,7 +85,7 @@ impl<'a> JSDocTag<'a> { None => (None, self.raw_body), }; - (type_part, utils::trim_multiline_comment(comment_part)) + (type_part, utils::trim_comment(comment_part)) } /// Use for `@param`, `@property`, `@typedef`, ...etc. @@ -119,7 +119,7 @@ impl<'a> JSDocTag<'a> { None => (None, ""), }; - (type_part, name_part, utils::trim_multiline_comment(comment_part)) + (type_part, name_part, utils::trim_comment(comment_part)) } } @@ -130,43 +130,43 @@ mod test { #[test] 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", " c1").comment(), "c1"); + assert_eq!(JSDocTag::new("a", "\nc2 \n z ").comment(), "c2\nz"); + assert_eq!(JSDocTag::new("a", "\n* c3\n * \n z \n\n").comment(), "c3\nz"); assert_eq!( - JSDocTag::new("a", "comment4 and {@inline tag}!").comment(), + JSDocTag::new("a", " comment4 and {@inline tag}!").comment(), "comment4 and {@inline tag}!" ); } #[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", " {t1}").r#type(), Some("t1")); + assert_eq!(JSDocTag::new("t", "\n{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")); + 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")); } #[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", " {t1} c1").type_comment(), (Some("t1"), "c1".to_string())); + assert_eq!(JSDocTag::new("r", "\n{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(), + JSDocTag::new("r", "\n{t5}\nc5\n...").type_comment(), (Some("t5"), "c5\n...".to_string()) ); assert_eq!( - JSDocTag::new("r", "{t6} - c6").type_comment(), + JSDocTag::new("r", " {t6} - c6").type_comment(), (Some("t6"), "- c6".to_string()) ); assert_eq!( - JSDocTag::new("r", "{{ 型: t7 }} : c7").type_comment(), + JSDocTag::new("r", " {{ 型: t7 }} : c7").type_comment(), (Some("{ 型: t7 }"), ": c7".to_string()) ); } @@ -174,37 +174,37 @@ mod test { #[test] fn parses_type_name_comment() { assert_eq!( - JSDocTag::new("p", "{t1} n1 c1").type_name_comment(), + 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(), + JSDocTag::new("p", " {t2} n2").type_name_comment(), (Some("t2"), Some("n2"), String::new()) ); assert_eq!( - JSDocTag::new("p", "n3 c3").type_name_comment(), + 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(), + 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(), + 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(), + JSDocTag::new("p", "\n{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(), + JSDocTag::new("p", "\n\n{t7}\nn7\nc\n7").type_name_comment(), (Some("t7"), Some("n7"), "c\n7".to_string()) ); assert_eq!( - JSDocTag::new("p", "{t8}").type_name_comment(), + 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 aa709341f0840..395cc7a13adae 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/parse.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/parse.rs @@ -1,9 +1,11 @@ use super::jsdoc_tag::JSDocTag; use super::utils; +use oxc_span::Span; /// source_text: Inside of /**HERE*/, NOT includes `/**` and `*/` -pub fn parse_jsdoc(source_text: &str) -> (String, Vec) { - debug_assert!(!source_text.starts_with("/**")); +/// span_start: Global positioned `Span` start for this JSDoc comment +pub fn parse_jsdoc(source_text: &str, jsdoc_span_start: u32) -> (String, Vec<(Span, JSDocTag)>) { + debug_assert!(!source_text.starts_with("/*")); debug_assert!(!source_text.ends_with("*/")); // JSDoc consists of comment and tags. @@ -17,6 +19,7 @@ pub fn parse_jsdoc(source_text: &str) -> (String, Vec) { // But `@` can be found inside of `{}` (e.g. `{@see link}`), it should be distinguished. let mut in_braces = false; let mut comment_found = false; + // Parser local offsets, not for global span let (mut start, mut end) = (0, 0); for ch in source_text.chars() { match ch { @@ -26,11 +29,15 @@ pub fn parse_jsdoc(source_text: &str) -> (String, Vec) { let part = &source_text[start..end]; if comment_found { - tags.push(parse_jsdoc_tag(part)); + tags.push(( + get_tag_kind_span(part, (start, end), jsdoc_span_start), + parse_jsdoc_tag(part), + )); } else { comment = part; comment_found = true; } + // Prepare for the next draft start = end; } @@ -45,53 +52,108 @@ pub fn parse_jsdoc(source_text: &str) -> (String, Vec) { let part = &source_text[start..end]; if comment_found { - tags.push(parse_jsdoc_tag(part)); + tags.push(( + get_tag_kind_span(part, (start, end), jsdoc_span_start), + parse_jsdoc_tag(part), + )); } else { comment = part; } } - (utils::trim_multiline_comment(comment), tags) + (utils::trim_comment(comment), tags) +} + +// Use `Span` for `@kind` part instead of whole tag. +// +// For example, whole `tag.span` in the following JSDoc will be: +// /** +// * @kind1 bar +// * baz... +// * @kind2 +// */ +// for `@kind1`: `@kind1 bar\n * baz...\n * ` +// for `@kind2`: `@kind2\n ` +// +// It's too verbose and may not fit for linter diagnostics span. +fn get_tag_kind_span( + tag_content: &str, + (tag_offset_start, _): (usize, usize), + jsdoc_span_start: u32, +) -> Span { + debug_assert!(tag_content.starts_with('@')); + // This surely exists, at least `@` itself + let (k_start, k_end) = utils::find_token_range(tag_content).unwrap(); + + let k_len = k_end - k_start; + let (start, end) = ( + u32::try_from(tag_offset_start + k_start).unwrap_or_default(), + u32::try_from(tag_offset_start + k_start + k_len).unwrap_or_default(), + ); + + Span::new(jsdoc_span_start + start, jsdoc_span_start + end) } -// 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('@')); - // 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); - // Omit the first `@` - JSDocTag::new(&tag_content[k_start + 1..k_end], &tag_content[b_start..]) + JSDocTag::new( + // Omit the first `@` + &tag_content[k_start + 1..k_end], + // Includes splitter whitespace to distinguish these cases: + // ``` + // /** + // * @k * <- should not omit + // */ + // + // /** + // * @k + // * <- should omit + // */ + // ``` + // If not included, both body_part will starts with `* <- ...`! + // + // It does not affect the output since it will be trimmed later. + &tag_content[k_end..], + ) } #[cfg(test)] mod test { - use super::parse_jsdoc; use super::parse_jsdoc_tag; - use super::JSDocTag; - fn parse_from_full_text(full_text: &str) -> (String, Vec) { + 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("*/"); - parse_jsdoc(source_text) + let (comment, tags) = super::parse_jsdoc(source_text, 0); + (comment, tags.iter().map(|(_, t)| t).cloned().collect()) } #[test] fn parses_jsdoc_comment() { - assert_eq!(parse_jsdoc("hello source"), ("hello source".to_string(), vec![])); + assert_eq!(parse_from_full_text("/**hello*/"), ("hello".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!(parse_from_full_text("/****/"), ("*".to_string(), vec![])); + assert_eq!(parse_from_full_text("/*****/"), ("**".to_string(), vec![])); + assert_eq!( + parse_from_full_text( + "/** + * * x + ** y + */" + ) + .0, + "* x\n* y" + ); - assert_eq!(parse_jsdoc(" <- trim -> ").0, "<- trim ->"); + assert_eq!(parse_from_full_text("/** <- trim -> */").0, "<- trim ->"); assert_eq!( parse_from_full_text( " @@ -127,57 +189,52 @@ comment {@link link} ... ); assert_eq!( - parse_jsdoc("hello {@see inline} source {@a 2}").0, + parse_from_full_text("/**\nhello {@see inline} source {@a 2}\n*/").0, "hello {@see inline} source {@a 2}" ); - assert_eq!(parse_jsdoc("").0, ""); + assert_eq!(parse_from_full_text("/** ハロー @comment だよ*/").0, "ハロー"); } #[test] - fn parses_single_line_1_jsdoc() { - 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![]); - + fn parses_jsdoc_tags() { + assert_eq!( + parse_from_full_text("/**@deprecated*/").1, + vec![parse_jsdoc_tag("@deprecated")] + ); assert_eq!( parse_from_full_text("/**@foo since 2024 */").1, 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::new("foo", ""), JSDocTag::new("bar", "")] + vec![parse_jsdoc_tag("@foo "), parse_jsdoc_tag("@bar ")] ); + + assert_eq!(parse_from_full_text("/**@*/").1, vec![parse_jsdoc_tag("@")]); + assert_eq!( parse_from_full_text("/** @aiue あいうえ @o お*/").1, - vec![JSDocTag::new("aiue", "あいうえ "), JSDocTag::new("o", "お")] + vec![parse_jsdoc_tag("@aiue あいうえ "), parse_jsdoc_tag("@o お")], ); assert_eq!( parse_from_full_text("/** @a @@ @d */").1, vec![ - JSDocTag::new("a", ""), - JSDocTag::new("", ""), - JSDocTag::new("", ""), - JSDocTag::new("d", "") - ] + parse_jsdoc_tag("@a "), + parse_jsdoc_tag("@"), + parse_jsdoc_tag("@ "), + parse_jsdoc_tag("@d ") + ], ); - } - #[test] - fn parses_multiline_1_jsdoc() { assert_eq!( parse_from_full_text( "/** @yo */" ) .1, - vec![JSDocTag::new("yo", " ")] + vec![parse_jsdoc_tag("@yo\n ")] ); assert_eq!( parse_from_full_text( @@ -186,7 +243,7 @@ comment {@link link} ... */" ) .1, - vec![JSDocTag::new("foo", " ")] + vec![parse_jsdoc_tag("@foo\n ")] ); assert_eq!( parse_from_full_text( @@ -197,7 +254,7 @@ comment {@link link} ... " ) .1, - vec![JSDocTag::new("x", "with asterisk\n ")] + vec![parse_jsdoc_tag("@x with asterisk\n ")] ); assert_eq!( parse_from_full_text( @@ -209,12 +266,9 @@ comment {@link link} ... " ) .1, - vec![JSDocTag::new("y", "without\n asterisk\n ")] + vec![parse_jsdoc_tag("@y without\n asterisk\n ")] ); - } - #[test] - fn parses_multiline_n_jsdocs() { assert_eq!( parse_from_full_text( " @@ -226,9 +280,9 @@ comment {@link link} ... ) .1, vec![ - JSDocTag::new("foo", ""), - JSDocTag::new("bar", " * "), - JSDocTag::new("baz", " ") + parse_jsdoc_tag("@foo"), + parse_jsdoc_tag("@bar\n * "), + parse_jsdoc_tag("@baz\n ") ] ); assert_eq!( @@ -242,8 +296,8 @@ comment {@link link} ... ) .1, vec![ - JSDocTag::new("one", " *\n * ...\n *\n * "), - JSDocTag::new("two", ""), + parse_jsdoc_tag("@one\n *\n * ...\n *\n * "), + parse_jsdoc_tag("@two ") ] ); assert_eq!( @@ -257,15 +311,52 @@ comment {@link link} ... ) .1, vec![ - JSDocTag::new( - "hey", - "you!\n * Are you OK?\n * " + parse_jsdoc_tag( + "@hey you!\n * Are you OK?\n * " ), - JSDocTag::new("yes", "I'm fine\n ") + parse_jsdoc_tag("@yes I'm fine\n ") ] ); } + #[test] + fn parses_practical() { + let jsdoc = parse_from_full_text( + " +/** + * @typedef {Object} User - a User account + * @property {string} displayName - the name used to show the user + * @property {number} id - a unique id + */ +", + ); + let mut tags = jsdoc.1.iter(); + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "typedef"); + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "property"); + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "property"); + + let jsdoc = parse_from_full_text( + " +/** + * Adds two numbers together + * @param {number} a The first number + * @param {number} b The second number + * @returns {number} + */ +", + ); + let mut tags = jsdoc.1.iter(); + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "param"); + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "param"); + let tag = tags.next().unwrap(); + assert_eq!(tag.kind, "returns"); + } + #[test] fn parses_practical_with_multibyte() { let jsdoc = parse_from_full_text( diff --git a/crates/oxc_semantic/src/jsdoc/parser/utils.rs b/crates/oxc_semantic/src/jsdoc/parser/utils.rs index 853c6d4e914ca..bd5b4601c7dab 100644 --- a/crates/oxc_semantic/src/jsdoc/parser/utils.rs +++ b/crates/oxc_semantic/src/jsdoc/parser/utils.rs @@ -1,7 +1,14 @@ -pub fn trim_multiline_comment(s: &str) -> String { - s.trim() - .lines() - .map(|line| line.trim().trim_start_matches('*').trim()) +pub fn trim_comment(s: &str) -> String { + let lines = s.lines(); + + // If single line, there is no leading `*` + if lines.clone().count() == 1 { + return s.trim().to_string(); + } + + s.lines() + // Trim leading the first `*` in each line + .map(|line| line.trim().strip_prefix('*').unwrap_or(line).trim()) .filter(|line| !line.is_empty()) .collect::>() .join("\n") @@ -58,22 +65,33 @@ pub fn find_token_range(s: &str) -> Option<(usize, usize)> { #[cfg(test)] mod test { - use super::{find_token_range, find_type_range, trim_multiline_comment}; + use super::{find_token_range, find_type_range, trim_comment}; #[test] - fn trim_multiline_jsdoc_comments() { + fn trim_jsdoc_comments() { for (actual, expect) in [ ("", ""), + ("hello ", "hello"), + (" * single line", "* single line"), + (" * ", "*"), + (" * * ", "* *"), + ("***", "***"), + ( + " + trim +", "trim", + ), ( " ", "", ), - ("hello", "hello"), ( " - trim -", "trim", + * + * + ", + "", ), ( " @@ -97,6 +115,13 @@ mod test { ), ( " + * * 1 + ** 2 +", + "* 1\n* 2", + ), + ( + " 1 2 @@ -107,7 +132,7 @@ mod test { "1\n2\n3", ), ] { - assert_eq!(trim_multiline_comment(actual), expect); + assert_eq!(trim_comment(actual), expect); } } @@ -115,7 +140,7 @@ mod test { fn extract_type_part_range() { for (actual, expect) in [ ("{t1}", Some("t1")), - ("{t2 }", Some("t2 ")), + (" { t2 } ", Some(" t2 ")), ("{{ t3: string }}", Some("{ t3: string }")), ("{t4} name", Some("t4")), (" {t5} ", Some("t5")), @@ -130,14 +155,14 @@ mod test { } #[test] - fn extract_name_part_range() { + fn extract_token_part_range() { for (actual, expect) in [ ("n1", Some("n1")), ("n2 x", Some("n2")), (" n3 ", Some("n3")), ("n4\ny", Some("n4")), ("", None), - ("名前5", Some("名前5")), + (" 名前5\n", Some("名前5")), ("\nn6\nx", Some("n6")), ] { assert_eq!(find_token_range(actual).map(|(s, e)| &actual[s..e]), expect);