mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
refactor(semantic/jsdoc): JSDocTag parser rework (#2765)
Address
https://github.com/oxc-project/oxc/pull/2642#issuecomment-2001950723
0fd67cb874/crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs (L3-L25)
This commit is contained in:
parent
7d604e57b0
commit
4a42c5fd7d
5 changed files with 450 additions and 441 deletions
|
|
@ -10,9 +10,6 @@ pub struct JSDocFinder<'a> {
|
||||||
not_attached: Vec<JSDoc<'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> {
|
impl<'a> JSDocFinder<'a> {
|
||||||
pub fn new(attached: BTreeMap<Span, Vec<JSDoc<'a>>>, not_attached: Vec<JSDoc<'a>>) -> Self {
|
pub fn new(attached: BTreeMap<Span, Vec<JSDoc<'a>>>, not_attached: Vec<JSDoc<'a>>) -> Self {
|
||||||
Self { attached, not_attached }
|
Self { attached, not_attached }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use super::jsdoc_tag::JSDocTag;
|
use super::jsdoc_tag::JSDocTag;
|
||||||
use super::parse::JSDocParser;
|
use super::parse::parse_jsdoc;
|
||||||
use std::cell::OnceCell;
|
use std::cell::OnceCell;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -16,12 +16,14 @@ impl<'a> JSDoc<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn comment(&self) -> &str {
|
pub fn comment(&self) -> &str {
|
||||||
let cache = self.cached.get_or_init(|| JSDocParser::new(self.raw).parse());
|
&self.parse().0
|
||||||
&cache.0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tags<'b>(&'b self) -> &'b Vec<JSDocTag<'a>> {
|
pub fn tags(&self) -> &Vec<JSDocTag<'a>> {
|
||||||
let cache = self.cached.get_or_init(|| JSDocParser::new(self.raw).parse());
|
&self.parse().1
|
||||||
&cache.1
|
}
|
||||||
|
|
||||||
|
fn parse(&self) -> &(String, Vec<JSDocTag<'a>>) {
|
||||||
|
self.cached.get_or_init(|| parse_jsdoc(self.raw))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
//
|
//
|
||||||
|
// This means that:
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
// - We cannot parse a tag exactly as it was written
|
||||||
pub enum ParamTypeKind {
|
// - We cannot assume that `@param` will always map to `JSDocParameterTag`
|
||||||
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>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// 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
|
/// General struct for JSDoc tag.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
///
|
||||||
pub enum JSDocTagKind<'a> {
|
/// `kind` can be any string like `param`, `type`, `whatever`, ...etc.
|
||||||
Deprecated, // JSDocDeprecatedTag
|
/// `raw_body` is kept as is, you can use helper methods according to your needs.
|
||||||
Parameter(Param<'a>), // JSDocParameterTag
|
|
||||||
Unknown(&'a str), // JSDocTag
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct JSDocTag<'a> {
|
pub struct JSDocTag<'a> {
|
||||||
pub kind: JSDocTagKind<'a>,
|
raw_body: &'a str,
|
||||||
pub comment: String,
|
pub kind: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> JSDocTag<'a> {
|
impl<'a> JSDocTag<'a> {
|
||||||
pub fn tag_name(&self) -> &'a str {
|
/// kind: Does not contain the `@` prefix
|
||||||
match self.kind {
|
/// raw_body: The body part of the tag, after the `@kind {HERE_MAY_BE_MULTILINE...}`
|
||||||
JSDocTagKind::Deprecated => "deprecated",
|
pub fn new(kind: &'a str, raw_body: &'a str) -> JSDocTag<'a> {
|
||||||
JSDocTagKind::Parameter(_) => "param",
|
debug_assert!(!kind.starts_with('@'));
|
||||||
JSDocTagKind::Unknown(tag_name) => tag_name,
|
Self { raw_body, kind }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_deprecated(&self) -> bool {
|
/// Use for various simple tags like `@access`, `@deprecated`, ...etc.
|
||||||
self.kind == JSDocTagKind::Deprecated
|
/// 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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{Param, ParamType, ParamTypeKind};
|
use super::JSDocTag;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deduces_correct_param_kind() {
|
fn parses_comment() {
|
||||||
let param = Param { name: "a", r#type: Some(ParamType { value: "string" }) };
|
assert_eq!(JSDocTag::new("a", "").comment(), "");
|
||||||
assert_eq!(param.r#type.and_then(|t| t.kind()), None);
|
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" }) };
|
#[test]
|
||||||
assert_eq!(param.r#type.and_then(|t| t.kind()), Some(ParamTypeKind::Repeated));
|
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: "*" }) };
|
#[test]
|
||||||
assert_eq!(param.r#type.and_then(|t| t.kind()), Some(ParamTypeKind::Any));
|
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())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,167 +1,97 @@
|
||||||
use super::jsdoc_tag::{JSDocTag, JSDocTagKind};
|
use super::jsdoc_tag::JSDocTag;
|
||||||
use super::jsdoc_tag::{Param, ParamType};
|
|
||||||
use super::utils;
|
use super::utils;
|
||||||
|
|
||||||
#[derive(Debug)]
|
/// source_text: Inside of /**HERE*/, NOT includes `/**` and `*/`
|
||||||
pub struct JSDocParser<'a> {
|
pub fn parse_jsdoc(source_text: &str) -> (String, Vec<JSDocTag>) {
|
||||||
source_text: &'a str,
|
debug_assert!(!source_text.starts_with("/**"));
|
||||||
current: usize,
|
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
|
// TODO: Manage `Span`
|
||||||
// https://github.com/microsoft/TypeScript/blob/df8d755c1d76eaf0a8f1c1046a46061b53315718/src/compiler/parser.ts#L8814
|
// - with (start, end) + global comment span.start
|
||||||
impl<'a> JSDocParser<'a> {
|
// - add kind only span?
|
||||||
/// source_text: Inside of /**HERE*/, NOT includes `/**` and `*/`
|
/// tag_content: Starts with `@`, may be mulitline
|
||||||
pub fn new(source_text: &'a str) -> Self {
|
fn parse_jsdoc_tag(tag_content: &str) -> JSDocTag {
|
||||||
// Outer spaces can be trimmed
|
debug_assert!(tag_content.starts_with('@'));
|
||||||
Self { source_text: source_text.trim(), current: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse(mut self) -> (String, Vec<JSDocTag<'a>>) {
|
// This surely exists, at least `@` itself
|
||||||
let comment = self.parse_comment();
|
let (k_start, k_end) = utils::find_token_range(tag_content).unwrap();
|
||||||
let tags = self.parse_tags();
|
// +1 for whitespace, may be empty
|
||||||
|
let b_start = tag_content.len().min(k_end + 1);
|
||||||
|
|
||||||
(comment, tags)
|
// Omit the first `@`
|
||||||
}
|
JSDocTag::new(&tag_content[k_start + 1..k_end], &tag_content[b_start..])
|
||||||
|
|
||||||
// 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]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::JSDocParser;
|
use super::parse_jsdoc;
|
||||||
use super::{JSDocTag, JSDocTagKind};
|
use super::parse_jsdoc_tag;
|
||||||
use super::{Param, ParamType};
|
use super::JSDocTag;
|
||||||
|
|
||||||
fn parse_from_full_text(full_text: &str) -> (String, Vec<JSDocTag>) {
|
fn parse_from_full_text(full_text: &str) -> (String, Vec<JSDocTag>) {
|
||||||
// Outside of markers can be trimmed
|
// Outside of markers can be trimmed
|
||||||
let source_text = full_text.trim().trim_start_matches("/**").trim_end_matches("*/");
|
let source_text = full_text.trim().trim_start_matches("/**").trim_end_matches("*/");
|
||||||
JSDocParser::new(source_text).parse()
|
parse_jsdoc(source_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_jsdoc_comment() {
|
fn parses_jsdoc_comment() {
|
||||||
assert_eq!(JSDocParser::new("hello source").parse().0, "hello source");
|
assert_eq!(parse_jsdoc("hello source"), ("hello source".to_string(), vec![]));
|
||||||
assert_eq!(parse_from_full_text("/** hello full */").0, "hello full");
|
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!(
|
assert_eq!(
|
||||||
parse_from_full_text(
|
parse_from_full_text(
|
||||||
"
|
"
|
||||||
|
|
@ -178,65 +108,63 @@ mod test {
|
||||||
parse_from_full_text(
|
parse_from_full_text(
|
||||||
"/**
|
"/**
|
||||||
this is
|
this is
|
||||||
comment
|
comment {@link link} ...
|
||||||
@x
|
@x
|
||||||
*/"
|
*/"
|
||||||
)
|
)
|
||||||
.0,
|
.0,
|
||||||
"this is\ncomment"
|
"this is\ncomment {@link link} ..."
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_from_full_text(
|
parse_from_full_text(
|
||||||
"/**
|
"/**
|
||||||
* 日本語とか
|
* 日本語とか
|
||||||
* multibyte文字はどう?
|
* multibyte文字はどう⁉️
|
||||||
*/"
|
*/"
|
||||||
)
|
)
|
||||||
.0,
|
.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]
|
#[test]
|
||||||
fn parses_single_line_1_jsdoc() {
|
fn parses_single_line_1_jsdoc() {
|
||||||
assert_eq!(
|
assert_eq!(parse_jsdoc("@deprecated"), parse_from_full_text("/** @deprecated*/"));
|
||||||
JSDocParser::new("@deprecated").parse().1,
|
assert_eq!(parse_jsdoc("@deprecated").1, vec![parse_jsdoc_tag("@deprecated")]);
|
||||||
parse_from_full_text("/** @deprecated */").1,
|
|
||||||
);
|
assert_eq!(parse_jsdoc("").1, vec![]);
|
||||||
assert_eq!(
|
|
||||||
JSDocParser::new("@deprecated").parse().1,
|
|
||||||
vec![JSDocTag { kind: JSDocTagKind::Deprecated, comment: String::new() }]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_from_full_text("/**@foo since 2024 */").1,
|
parse_from_full_text("/**@foo since 2024 */").1,
|
||||||
vec![JSDocTag {
|
vec![parse_jsdoc_tag("@foo since 2024 ")]
|
||||||
kind: JSDocTagKind::Unknown("foo"),
|
|
||||||
comment: "since 2024".to_string()
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parse_from_full_text("/**@*/").1,
|
|
||||||
vec![JSDocTag { kind: JSDocTagKind::Unknown(""), comment: String::new() }]
|
|
||||||
);
|
);
|
||||||
|
assert_eq!(parse_from_full_text("/**@*/").1, vec![JSDocTag::new("", "")]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_single_line_n_jsdocs() {
|
fn parses_single_line_n_jsdocs() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_from_full_text("/** @foo @bar */").1,
|
parse_from_full_text("/** @foo @bar */").1,
|
||||||
vec![
|
vec![JSDocTag::new("foo", ""), JSDocTag::new("bar", "")]
|
||||||
JSDocTag { kind: JSDocTagKind::Unknown("foo"), comment: String::new() },
|
);
|
||||||
JSDocTag { kind: JSDocTagKind::Unknown("bar"), comment: String::new() }
|
assert_eq!(
|
||||||
]
|
parse_from_full_text("/** @aiue あいうえ @o お*/").1,
|
||||||
|
vec![JSDocTag::new("aiue", "あいうえ "), JSDocTag::new("o", "お")]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_from_full_text("/** @a @@ @d */").1,
|
parse_from_full_text("/** @a @@ @d */").1,
|
||||||
vec![
|
vec![
|
||||||
JSDocTag { kind: JSDocTagKind::Unknown("a"), comment: String::new() },
|
JSDocTag::new("a", ""),
|
||||||
JSDocTag { kind: JSDocTagKind::Unknown(""), comment: String::new() },
|
JSDocTag::new("", ""),
|
||||||
JSDocTag { kind: JSDocTagKind::Unknown(""), comment: String::new() },
|
JSDocTag::new("", ""),
|
||||||
JSDocTag { kind: JSDocTagKind::Unknown("d"), comment: String::new() }
|
JSDocTag::new("d", "")
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -246,48 +174,42 @@ comment
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_from_full_text(
|
parse_from_full_text(
|
||||||
"/** @yo
|
"/** @yo
|
||||||
*/"
|
*/"
|
||||||
)
|
)
|
||||||
.1,
|
.1,
|
||||||
vec![JSDocTag { kind: JSDocTagKind::Unknown("yo"), comment: String::new() }]
|
vec![JSDocTag::new("yo", " ")]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_from_full_text(
|
parse_from_full_text(
|
||||||
"/**
|
"/**
|
||||||
* @foo
|
* @foo
|
||||||
*/"
|
*/"
|
||||||
)
|
)
|
||||||
.1,
|
.1,
|
||||||
vec![JSDocTag { kind: JSDocTagKind::Unknown("foo"), comment: String::new() }]
|
vec![JSDocTag::new("foo", " ")]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_from_full_text(
|
parse_from_full_text(
|
||||||
"
|
"
|
||||||
/**
|
/**
|
||||||
* @x with asterisk
|
* @x with asterisk
|
||||||
*/
|
*/
|
||||||
"
|
"
|
||||||
)
|
)
|
||||||
.1,
|
.1,
|
||||||
vec![JSDocTag {
|
vec![JSDocTag::new("x", "with asterisk\n ")]
|
||||||
kind: JSDocTagKind::Unknown("x"),
|
|
||||||
comment: "with asterisk".to_string()
|
|
||||||
}]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_from_full_text(
|
parse_from_full_text(
|
||||||
"
|
"
|
||||||
/**
|
/**
|
||||||
@y without
|
@y without
|
||||||
asterisk
|
asterisk
|
||||||
*/
|
*/
|
||||||
"
|
"
|
||||||
)
|
)
|
||||||
.1,
|
.1,
|
||||||
vec![JSDocTag {
|
vec![JSDocTag::new("y", "without\n asterisk\n ")]
|
||||||
kind: JSDocTagKind::Unknown("y"),
|
|
||||||
comment: "without\nasterisk".to_string()
|
|
||||||
}]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -297,16 +219,16 @@ asterisk
|
||||||
parse_from_full_text(
|
parse_from_full_text(
|
||||||
"
|
"
|
||||||
/**
|
/**
|
||||||
@foo @bar
|
@foo@bar
|
||||||
* @baz
|
* @baz
|
||||||
*/
|
*/
|
||||||
"
|
"
|
||||||
)
|
)
|
||||||
.1,
|
.1,
|
||||||
vec![
|
vec![
|
||||||
JSDocTag { kind: JSDocTagKind::Unknown("foo"), comment: String::new() },
|
JSDocTag::new("foo", ""),
|
||||||
JSDocTag { kind: JSDocTagKind::Unknown("bar"), comment: String::new() },
|
JSDocTag::new("bar", " * "),
|
||||||
JSDocTag { kind: JSDocTagKind::Unknown("baz"), comment: String::new() },
|
JSDocTag::new("baz", " ")
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -316,13 +238,12 @@ asterisk
|
||||||
*
|
*
|
||||||
* ...
|
* ...
|
||||||
*
|
*
|
||||||
* @two
|
* @two */"
|
||||||
*/"
|
|
||||||
)
|
)
|
||||||
.1,
|
.1,
|
||||||
vec![
|
vec![
|
||||||
JSDocTag { kind: JSDocTagKind::Unknown("one"), comment: "...".to_string() },
|
JSDocTag::new("one", " *\n * ...\n *\n * "),
|
||||||
JSDocTag { kind: JSDocTagKind::Unknown("two"), comment: String::new() },
|
JSDocTag::new("two", ""),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -336,126 +257,11 @@ asterisk
|
||||||
)
|
)
|
||||||
.1,
|
.1,
|
||||||
vec![
|
vec![
|
||||||
JSDocTag {
|
JSDocTag::new(
|
||||||
kind: JSDocTagKind::Unknown("hey"),
|
"hey",
|
||||||
comment: "you!\nAre you OK?".to_string()
|
"you!\n * Are you OK?\n * "
|
||||||
},
|
),
|
||||||
JSDocTag { kind: JSDocTagKind::Unknown("yes"), comment: "I'm fine".to_string() },
|
JSDocTag::new("yes", "I'm fine\n ")
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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(),
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -476,44 +282,44 @@ comment */"
|
||||||
*/",
|
*/",
|
||||||
);
|
);
|
||||||
assert_eq!(jsdoc.0, "flat tree data on expanded state");
|
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!(
|
assert_eq!(
|
||||||
jsdoc.1,
|
tag.type_name_comment(),
|
||||||
vec![
|
(Some("string"), Some("childrenColumnName"), ": 指定树形结构的列名".to_string())
|
||||||
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(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,68 @@ pub fn trim_multiline_comment(s: &str) -> String {
|
||||||
.join("\n")
|
.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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::trim_multiline_comment;
|
use super::{find_token_range, find_type_range, trim_multiline_comment};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn trim_multiline_jsdoc_comments() {
|
fn trim_multiline_jsdoc_comments() {
|
||||||
for (actual, expect) in [
|
for (actual, expect) in [
|
||||||
|
("", ""),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
|
||||||
|
", "",
|
||||||
|
),
|
||||||
("hello", "hello"),
|
("hello", "hello"),
|
||||||
(
|
(
|
||||||
"
|
"
|
||||||
|
|
@ -55,4 +110,37 @@ mod test {
|
||||||
assert_eq!(trim_multiline_comment(actual), expect);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue