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>>,
|
||||
}
|
||||
|
||||
// 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 }
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue