mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 04:42:10 +00:00
feat(semantic/jsdoc): Add Span for JSDoc, JSDocTag (#2815)
This commit is contained in:
parent
c5ccd5e7a1
commit
df744b205a
5 changed files with 318 additions and 106 deletions
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<JSDocTag<'a>>)>,
|
||||
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<JSDocTag<'a>> {
|
||||
pub fn tags(&self) -> &Vec<(Span, JSDocTag<'a>)> {
|
||||
&self.parse().1
|
||||
}
|
||||
|
||||
fn parse(&self) -> &(String, Vec<JSDocTag<'a>>) {
|
||||
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<SourceType>,
|
||||
) -> 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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<JSDocTag>) {
|
||||
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<JSDocTag>) {
|
|||
// 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<JSDocTag>) {
|
|||
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<JSDocTag>) {
|
|||
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<JSDocTag>) {
|
||||
fn parse_from_full_text(full_text: &str) -> (String, Vec<super::JSDocTag>) {
|
||||
// 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(
|
||||
|
|
|
|||
|
|
@ -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::<Vec<_>>()
|
||||
.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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue