Skip to content

Commit

Permalink
refactor(semantic/jsdoc): JSDocTag parser rework (#2765)
Browse files Browse the repository at this point in the history
  • Loading branch information
leaysgur authored Mar 22, 2024
1 parent 7d604e5 commit 4a42c5f
Show file tree
Hide file tree
Showing 5 changed files with 442 additions and 433 deletions.
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

0 comments on commit 4a42c5f

Please sign in to comment.