Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(semantic/jsdoc): JSDocTag parser rework #2765

Merged
merged 14 commits into from
Mar 22, 2024
3 changes: 0 additions & 3 deletions crates/oxc_semantic/src/jsdoc/finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
14 changes: 8 additions & 6 deletions crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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))
}
}
258 changes: 187 additions & 71 deletions crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs
Original file line number Diff line number Diff line change
@@ -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 }
}

/// 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))
}

pub fn is_deprecated(&self) -> bool {
self.kind == JSDocTagKind::Deprecated
/// 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 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}!"
);
}

#[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_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: "...string" }) };
assert_eq!(param.r#type.and_then(|t| t.kind()), Some(ParamTypeKind::Repeated));
#[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())
);
}

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_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())
);
}
}
Loading
Loading