Yuji Sugiura 2024-03-23 01:16:59 +09:00 committed by GitHub
parent 7d604e57b0
commit 4a42c5fd7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 450 additions and 441 deletions

View file

@ -10,9 +10,6 @@ pub struct JSDocFinder<'a> {
not_attached: Vec<JSDoc<'a>>,
}
// 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<Span, Vec<JSDoc<'a>>>, not_attached: Vec<JSDoc<'a>>) -> Self {
Self { attached, not_attached }

View file

@ -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<JSDocTag<'a>> {
let cache = self.cached.get_or_init(|| JSDocParser::new(self.raw).parse());
&cache.1
pub fn tags(&self) -> &Vec<JSDocTag<'a>> {
&self.parse().1
}
fn parse(&self) -> &(String, Vec<JSDocTag<'a>>) {
self.cached.get_or_init(|| parse_jsdoc(self.raw))
}
}

View file

@ -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> {
ParamTypeKind::from_str(self.value).map(Option::Some).unwrap_or_default()
}
}
impl FromStr for ParamTypeKind {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
// 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<ParamType<'a>>,
}
// 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())
);
}
}

View file

@ -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<JSDocTag>) {
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<JSDocTag<'a>>) {
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<JSDocTag<'a>> {
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 [<some text>]
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<JSDocTag>) {
// 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<Key>" })
}),
comment: "展开的行对应的keys".to_string(),
},
JSDocTag {
kind: JSDocTagKind::Parameter(Param {
name: "getRowKey",
r#type: Some(ParamType { value: "GetRowKey<T>" })
}),
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<Key>"), Some("expandedKeys"), ": 展开的行对应的keys".to_string())
);
let tag = tags.next().unwrap();
assert_eq!(tag.kind, "param");
assert_eq!(
tag.type_name_comment(),
(Some("GetRowKey<T>"), 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()));
}
}

View file

@ -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);
}
}
}