mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
refactor(semantic/jsdoc): Misc fixes for JSDoc related things (#2531)
Sorry for the rather large size of PR. 😓 But essentially, not changed so much. #### 1. Reorganize directories and namings ``` src/jsdoc ├── builder.rs 👈🏻 for SemanticBuilder ├── finder.rs 👈🏻 `semantic.jsdoc()` ├── mod.rs └── parser ├── jsdoc.rs 👈🏻 `JSDoc` struct which has `comment` and `tags` ├── jsdoc_tag.rs 👈🏻 `JSDocTag` struct ├── mod.rs ├── parse.rs 👈🏻 parsing logic by `JSDocParser` └── utils.rs ``` Now `mod.rs` has only export things. #### 2. Introduce `JSDocTagKind::Unknown(name)` We need to keep their name as-is to check valid tag names are used.(e.g. `jsdoc/check-tag-names` rule) #### 3. Support multiline description - Comment for JSDoc - Comment for each JSDocTag ```js /** * @foo this comment continues * here but leading * should be ignored!! */ ``` - - - Please correct me if I am doing something wrong... 🐰
This commit is contained in:
parent
fe777f330f
commit
1391e4a86b
11 changed files with 712 additions and 396 deletions
|
|
@ -2,7 +2,7 @@ use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc};
|
|||
|
||||
use oxc_codegen::{Codegen, CodegenOptions};
|
||||
use oxc_diagnostics::Error;
|
||||
use oxc_semantic::{AstNodes, JSDoc, ScopeTree, Semantic, SymbolTable};
|
||||
use oxc_semantic::{AstNodes, JSDocFinder, ScopeTree, Semantic, SymbolTable};
|
||||
use oxc_span::SourceType;
|
||||
|
||||
use crate::{
|
||||
|
|
@ -155,7 +155,7 @@ impl<'a> LintContext<'a> {
|
|||
}
|
||||
|
||||
/* JSDoc */
|
||||
pub fn jsdoc(&self) -> &JSDoc<'a> {
|
||||
pub fn jsdoc(&self) -> &JSDocFinder<'a> {
|
||||
self.semantic().jsdoc()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::parser::JSDoc;
|
||||
use crate::jsdoc::JSDocFinder;
|
||||
use oxc_ast::{AstKind, Comment, TriviasMap};
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use super::{JSDoc, JSDocComment};
|
||||
|
||||
pub struct JSDocBuilder<'a> {
|
||||
source_text: &'a str,
|
||||
trivias: Rc<TriviasMap>,
|
||||
attached_docs: BTreeMap<Span, Vec<JSDocComment<'a>>>,
|
||||
attached_docs: BTreeMap<Span, Vec<JSDoc<'a>>>,
|
||||
leading_comments_seen: FxHashSet<u32>,
|
||||
}
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ impl<'a> JSDocBuilder<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn build(self) -> JSDoc<'a> {
|
||||
pub fn build(self) -> JSDocFinder<'a> {
|
||||
let not_attached_docs = self
|
||||
.trivias
|
||||
.comments()
|
||||
|
|
@ -33,7 +33,7 @@ impl<'a> JSDocBuilder<'a> {
|
|||
.filter_map(|(start, comment)| self.parse_if_jsdoc_comment(*start, *comment))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
JSDoc::new(self.attached_docs, not_attached_docs)
|
||||
JSDocFinder::new(self.attached_docs, not_attached_docs)
|
||||
}
|
||||
|
||||
// This process is done in conjunction with the `semantic.build()`.
|
||||
|
|
@ -55,6 +55,9 @@ impl<'a> JSDocBuilder<'a> {
|
|||
// (But, default is only about function related nodes.)
|
||||
// > https://github.com/gajus/eslint-plugin-jsdoc/blob/e948bee821e964a92fbabc01574eca226e9e1252/src/iterateJsdoc.js#L2517-L2536
|
||||
//
|
||||
// `eslint-plugin-import` does the similar but more casual way.
|
||||
// > https://github.com/import-js/eslint-plugin-import/blob/df751e0d004aacc34f975477163fb221485a85f6/src/ExportMap.js#L211
|
||||
//
|
||||
// Q. How do we attach JSDoc to that node?
|
||||
// A. Also depends on the implementation.
|
||||
//
|
||||
|
|
@ -70,6 +73,7 @@ impl<'a> JSDocBuilder<'a> {
|
|||
//
|
||||
// Of course, this can be changed in the future.
|
||||
pub fn retrieve_attached_jsdoc(&mut self, kind: &AstKind<'a>) -> bool {
|
||||
// For perf reasons, we should limit the target nodes to attach JSDoc
|
||||
// This may be diffed compare to TypeScript's `canHaveJSDoc()`, should adjust if needed
|
||||
if !(kind.is_statement()
|
||||
|| kind.is_declaration()
|
||||
|
|
@ -103,11 +107,7 @@ impl<'a> JSDocBuilder<'a> {
|
|||
false
|
||||
}
|
||||
|
||||
fn parse_if_jsdoc_comment(
|
||||
&self,
|
||||
span_start: u32,
|
||||
comment: Comment,
|
||||
) -> Option<JSDocComment<'a>> {
|
||||
fn parse_if_jsdoc_comment(&self, span_start: u32, comment: Comment) -> Option<JSDoc<'a>> {
|
||||
if !comment.is_multi_line() {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -120,8 +120,8 @@ impl<'a> JSDocBuilder<'a> {
|
|||
return None;
|
||||
}
|
||||
|
||||
// Should remove the very first `*`?
|
||||
Some(JSDocComment::new(comment_content))
|
||||
// Remove the very first `*`
|
||||
Some(JSDoc::new(&comment_content[1..]))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +131,8 @@ mod test {
|
|||
use oxc_parser::Parser;
|
||||
use oxc_span::{SourceType, Span};
|
||||
|
||||
use crate::{jsdoc::JSDocComment, Semantic, SemanticBuilder};
|
||||
use super::JSDoc;
|
||||
use crate::{Semantic, SemanticBuilder};
|
||||
|
||||
fn build_semantic<'a>(
|
||||
allocator: &'a Allocator,
|
||||
|
|
@ -154,7 +155,7 @@ mod test {
|
|||
source_text: &'a str,
|
||||
symbol: &'a str,
|
||||
source_type: Option<SourceType>,
|
||||
) -> Option<Vec<JSDocComment<'a>>> {
|
||||
) -> Option<Vec<JSDoc<'a>>> {
|
||||
let semantic = build_semantic(allocator, source_text, source_type);
|
||||
let start = source_text.find(symbol).unwrap_or(0) as u32;
|
||||
let span = Span::new(start, start + symbol.len() as u32);
|
||||
|
|
@ -187,6 +188,7 @@ mod test {
|
|||
("/** test */ ; function foo() {}", "function foo() {}"),
|
||||
("/** test */ function foo1() {} function foo() {}", "function foo() {}"),
|
||||
("function foo() {} /** test */", "function foo() {}"),
|
||||
("/** test */ (() => {})", "() => {}"),
|
||||
];
|
||||
for (source_text, target) in source_texts {
|
||||
test_jsdoc_not_found(source_text, target);
|
||||
|
|
@ -236,6 +238,9 @@ mod test {
|
|||
"function foo() {}",
|
||||
),
|
||||
("/** foo1 */ function foo1() {} /** test */ function foo() {}", "function foo() {}"),
|
||||
("/** test */ 1", "1"),
|
||||
("/** test */ (1)", "(1)"),
|
||||
("/** test */ (() => {})", "(() => {})"),
|
||||
];
|
||||
for (source_text, target) in source_texts {
|
||||
test_jsdoc_found(source_text, target, None);
|
||||
|
|
@ -282,10 +287,10 @@ mod test {
|
|||
// Should be [farthest, ..., nearest]
|
||||
let mut iter = jsdocs.iter();
|
||||
let c1 = iter.next().unwrap();
|
||||
assert!(c1.comment.contains("c1"));
|
||||
assert!(c1.comment().contains("c1"));
|
||||
let _c2 = iter.next().unwrap();
|
||||
let c3 = iter.next().unwrap();
|
||||
assert!(c3.comment.contains("c3"));
|
||||
assert!(c3.comment().contains("c3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -309,6 +314,8 @@ mod test {
|
|||
|
||||
let x;
|
||||
|
||||
/**/ // noop and noop
|
||||
|
||||
/** 7. Not attached but collected! */
|
||||
",
|
||||
Some(SourceType::default()),
|
||||
|
|
|
|||
47
crates/oxc_semantic/src/jsdoc/finder.rs
Normal file
47
crates/oxc_semantic/src/jsdoc/finder.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use super::parser::JSDoc;
|
||||
use crate::AstNode;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JSDocFinder<'a> {
|
||||
/// JSDocs by Span
|
||||
attached: BTreeMap<Span, 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> {
|
||||
pub fn new(attached: BTreeMap<Span, Vec<JSDoc<'a>>>, not_attached: Vec<JSDoc<'a>>) -> Self {
|
||||
Self { attached, not_attached }
|
||||
}
|
||||
|
||||
pub fn get_one_by_node<'b>(&'b self, node: &AstNode<'a>) -> Option<JSDoc<'a>> {
|
||||
let Some(jsdocs) = self.get_all_by_node(node) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// If flagged, at least 1 JSDoc is attached
|
||||
// If multiple JSDocs are attached, return the last = nearest
|
||||
jsdocs.last().cloned()
|
||||
}
|
||||
|
||||
pub fn get_all_by_node<'b>(&'b self, node: &AstNode<'a>) -> Option<Vec<JSDoc<'a>>> {
|
||||
if !node.flags().has_jsdoc() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let span = node.kind().span();
|
||||
self.get_all_by_span(span)
|
||||
}
|
||||
|
||||
pub fn get_all_by_span<'b>(&'b self, span: Span) -> Option<Vec<JSDoc<'a>>> {
|
||||
self.attached.get(&span).cloned()
|
||||
}
|
||||
|
||||
pub fn iter_all<'b>(&'b self) -> impl Iterator<Item = &JSDoc<'a>> + 'b {
|
||||
self.attached.values().flatten().chain(self.not_attached.iter())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +1,6 @@
|
|||
mod builder;
|
||||
|
||||
use std::{cell::OnceCell, collections::BTreeMap};
|
||||
|
||||
pub use builder::JSDocBuilder;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
|
||||
use self::parser::JSDocParser;
|
||||
pub use self::parser::JSDocTag;
|
||||
use crate::AstNode;
|
||||
|
||||
mod finder;
|
||||
mod parser;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JSDoc<'a> {
|
||||
/// JSDocs by Span
|
||||
attached: BTreeMap<Span, Vec<JSDocComment<'a>>>,
|
||||
not_attached: Vec<JSDocComment<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JSDocComment<'a> {
|
||||
comment: &'a str,
|
||||
/// Cached JSDocTags
|
||||
tags: OnceCell<Vec<JSDocTag<'a>>>,
|
||||
}
|
||||
|
||||
impl<'a> JSDoc<'a> {
|
||||
pub fn new(
|
||||
attached: BTreeMap<Span, Vec<JSDocComment<'a>>>,
|
||||
not_attached: Vec<JSDocComment<'a>>,
|
||||
) -> Self {
|
||||
Self { attached, not_attached }
|
||||
}
|
||||
|
||||
pub fn get_one_by_node<'b>(&'b self, node: &AstNode<'a>) -> Option<JSDocComment<'a>> {
|
||||
let Some(jsdocs) = self.get_all_by_node(node) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// If flagged, at least 1 JSDoc is attached
|
||||
// If multiple JSDocs are attached, return the last = nearest
|
||||
jsdocs.last().cloned()
|
||||
}
|
||||
|
||||
pub fn get_all_by_node<'b>(&'b self, node: &AstNode<'a>) -> Option<Vec<JSDocComment<'a>>> {
|
||||
if !node.flags().has_jsdoc() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let span = node.kind().span();
|
||||
self.get_all_by_span(span)
|
||||
}
|
||||
|
||||
pub fn get_all_by_span<'b>(&'b self, span: Span) -> Option<Vec<JSDocComment<'a>>> {
|
||||
self.attached.get(&span).cloned()
|
||||
}
|
||||
|
||||
pub fn iter_all<'b>(&'b self) -> impl Iterator<Item = &JSDocComment<'a>> + 'b {
|
||||
self.attached.values().flatten().chain(self.not_attached.iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> JSDocComment<'a> {
|
||||
pub fn new(comment: &'a str) -> JSDocComment<'a> {
|
||||
Self { comment, tags: OnceCell::new() }
|
||||
}
|
||||
|
||||
pub fn tags<'b>(&'b self) -> &'b Vec<JSDocTag<'a>> {
|
||||
self.tags.get_or_init(|| JSDocParser::new(self.comment).parse())
|
||||
}
|
||||
}
|
||||
pub use builder::JSDocBuilder;
|
||||
pub use finder::JSDocFinder;
|
||||
|
|
|
|||
|
|
@ -1,306 +0,0 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ParamTypeKind {
|
||||
Any,
|
||||
Repeated,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ParamType<'a> {
|
||||
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> {
|
||||
name: &'a str,
|
||||
r#type: Option<ParamType<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum JSDocTagKind<'a> {
|
||||
Deprecated,
|
||||
Param(Param<'a>),
|
||||
}
|
||||
|
||||
impl<'a> FromStr for JSDocTagKind<'a> {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"deprecated" => Ok(Self::Deprecated),
|
||||
"param" => Ok(Self::Param(Param::default())),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct JSDocTag<'a> {
|
||||
pub kind: JSDocTagKind<'a>,
|
||||
pub description: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> JSDocTag<'a> {
|
||||
pub fn is_deprecated(&self) -> bool {
|
||||
matches!(self.kind, JSDocTagKind::Deprecated)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JSDocParser<'a> {
|
||||
source_text: &'a str,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl<'a> JSDocParser<'a> {
|
||||
pub fn new(source_text: &'a str) -> Self {
|
||||
Self { source_text, current: 0 }
|
||||
}
|
||||
|
||||
pub fn parse(mut self) -> Vec<JSDocTag<'a>> {
|
||||
self.parse_comment(self.source_text)
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
if self.current < self.source_text.len() {
|
||||
self.current += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn at(&mut self, c: char) -> bool {
|
||||
let Some(ch) = self.source_text.chars().nth(self.current) else { return false };
|
||||
if ch == c {
|
||||
self.advance();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn take_until(&mut self, s: &'a str, predicate: fn(char) -> bool) -> &'a str {
|
||||
let start = self.current;
|
||||
while let Some(c) = s.chars().nth(self.current) {
|
||||
if predicate(c) {
|
||||
break;
|
||||
}
|
||||
self.current += 1;
|
||||
}
|
||||
&s[start..self.current]
|
||||
}
|
||||
|
||||
fn skip_whitespace(&mut self, s: &'a str) {
|
||||
while let Some(c) = s.chars().nth(self.current) {
|
||||
if c != ' ' {
|
||||
break;
|
||||
}
|
||||
self.current += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_comment(&mut self, comment: &'a str) -> Vec<JSDocTag<'a>> {
|
||||
let mut tags = vec![];
|
||||
|
||||
while let Some(c) = comment.chars().nth(self.current) {
|
||||
match c {
|
||||
'@' => {
|
||||
self.current += 1;
|
||||
let Some(tag) = self.parse_tag(comment) else { break };
|
||||
self.current += tag.description.len();
|
||||
tags.push(tag);
|
||||
}
|
||||
_ => {
|
||||
self.current += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags
|
||||
}
|
||||
|
||||
fn parse_tag(&mut self, comment: &'a str) -> Option<JSDocTag<'a>> {
|
||||
let tag = self.take_until(comment, |c| c == ' ' || c == '\n');
|
||||
JSDocTagKind::from_str(tag).map_or(None, |kind| match kind {
|
||||
JSDocTagKind::Deprecated => Some(self.parse_deprecated_tag(comment)),
|
||||
JSDocTagKind::Param { .. } => Some(self.parse_param_tag(comment)),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_deprecated_tag(&mut self, comment: &'a str) -> JSDocTag<'a> {
|
||||
self.skip_whitespace(comment);
|
||||
let description = self.take_until(comment, |c| c == '\n' || c == '*');
|
||||
JSDocTag { kind: JSDocTagKind::Deprecated, description }
|
||||
}
|
||||
|
||||
fn parse_param_tag(&mut self, comment: &'a str) -> JSDocTag<'a> {
|
||||
self.skip_whitespace(comment);
|
||||
|
||||
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(comment, |c| c == '}' || c == ' ');
|
||||
r#type = Some(ParamType { value: type_annotation });
|
||||
if self.at('}') {
|
||||
self.skip_whitespace(comment);
|
||||
}
|
||||
self.skip_whitespace(comment);
|
||||
}
|
||||
|
||||
let name = self.take_until(comment, |c| c == ' ' || c == '\n');
|
||||
|
||||
self.skip_whitespace(comment);
|
||||
if self.at('-') {
|
||||
self.skip_whitespace(comment);
|
||||
}
|
||||
|
||||
let description = self.take_until(comment, |c| c == '\n' || c == '*');
|
||||
|
||||
JSDocTag { kind: JSDocTagKind::Param(Param { name, r#type }), description }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::JSDocParser;
|
||||
use crate::jsdoc::parser::{JSDocTag, JSDocTagKind, Param, ParamType, ParamTypeKind};
|
||||
|
||||
#[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);
|
||||
|
||||
let param = Param { name: "a", r#type: Some(ParamType { value: "...string" }) };
|
||||
assert_eq!(param.r#type.and_then(|t| t.kind()), Some(ParamTypeKind::Repeated));
|
||||
|
||||
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_single_line_jsdoc() {
|
||||
let source = "/** @deprecated */";
|
||||
|
||||
let tags = JSDocParser::new(source).parse();
|
||||
assert_eq!(tags.len(), 1);
|
||||
assert_eq!(tags, vec![JSDocTag { kind: JSDocTagKind::Deprecated, description: "" }]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_multi_line_disjoint_jsdoc() {
|
||||
let source = r"/** @deprecated
|
||||
*/
|
||||
";
|
||||
|
||||
let tags = JSDocParser::new(source).parse();
|
||||
assert_eq!(tags.len(), 1);
|
||||
assert_eq!(tags, vec![JSDocTag { kind: JSDocTagKind::Deprecated, description: "" }]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_multiline_jsdoc() {
|
||||
let source = r"/**
|
||||
* @param a
|
||||
* @deprecated
|
||||
*/
|
||||
";
|
||||
|
||||
let tags = JSDocParser::new(source).parse();
|
||||
assert_eq!(tags.len(), 2);
|
||||
assert_eq!(
|
||||
tags,
|
||||
vec![
|
||||
JSDocTag {
|
||||
kind: JSDocTagKind::Param(Param { name: "a", r#type: None }),
|
||||
description: ""
|
||||
},
|
||||
JSDocTag { kind: JSDocTagKind::Deprecated, description: "" },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_multiline_jsdoc_with_descriptions() {
|
||||
let source = r"/**
|
||||
* @param a
|
||||
* @deprecated since version 1.0
|
||||
*/
|
||||
";
|
||||
|
||||
let tags = JSDocParser::new(source).parse();
|
||||
assert_eq!(tags.len(), 2);
|
||||
assert_eq!(
|
||||
tags,
|
||||
vec![
|
||||
JSDocTag {
|
||||
kind: JSDocTagKind::Param(Param { name: "a", r#type: None }),
|
||||
description: ""
|
||||
},
|
||||
JSDocTag { kind: JSDocTagKind::Deprecated, description: "since version 1.0" },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_param_type_annotation() {
|
||||
let source = r"/**
|
||||
* @param {string} a
|
||||
* @param {string b
|
||||
* @param {string} c - description
|
||||
*/
|
||||
";
|
||||
|
||||
let tags = JSDocParser::new(source).parse();
|
||||
assert_eq!(tags.len(), 3);
|
||||
assert_eq!(
|
||||
tags,
|
||||
vec![
|
||||
JSDocTag {
|
||||
kind: JSDocTagKind::Param(Param {
|
||||
name: "a",
|
||||
r#type: Some(ParamType { value: "string" })
|
||||
}),
|
||||
description: ""
|
||||
},
|
||||
JSDocTag {
|
||||
kind: JSDocTagKind::Param(Param {
|
||||
name: "b",
|
||||
r#type: Some(ParamType { value: "string" })
|
||||
}),
|
||||
description: ""
|
||||
},
|
||||
JSDocTag {
|
||||
kind: JSDocTagKind::Param(Param {
|
||||
name: "c",
|
||||
r#type: Some(ParamType { value: "string" })
|
||||
}),
|
||||
description: "description"
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
27
crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs
Normal file
27
crates/oxc_semantic/src/jsdoc/parser/jsdoc.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use super::jsdoc_tag::JSDocTag;
|
||||
use super::parse::JSDocParser;
|
||||
use std::cell::OnceCell;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JSDoc<'a> {
|
||||
raw: &'a str,
|
||||
/// Cached+parsed JSDoc comment and tags
|
||||
cached: OnceCell<(String, Vec<JSDocTag<'a>>)>,
|
||||
}
|
||||
|
||||
impl<'a> JSDoc<'a> {
|
||||
/// comment_content: Inside of /**HERE*/, not include `/**` and `*/`
|
||||
pub fn new(comment_content: &'a str) -> JSDoc<'a> {
|
||||
Self { raw: comment_content, cached: OnceCell::new() }
|
||||
}
|
||||
|
||||
pub fn comment(&self) -> &str {
|
||||
let cache = self.cached.get_or_init(|| JSDocParser::new(self.raw).parse());
|
||||
&cache.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
|
||||
}
|
||||
}
|
||||
95
crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs
Normal file
95
crates/oxc_semantic/src/jsdoc/parser/jsdoc_tag.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
//
|
||||
// JSDocTypeExpression
|
||||
//
|
||||
|
||||
#[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>>,
|
||||
}
|
||||
|
||||
//
|
||||
// Structs
|
||||
//
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct JSDocTag<'a> {
|
||||
pub kind: JSDocTagKind<'a>,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_deprecated(&self) -> bool {
|
||||
self.kind == JSDocTagKind::Deprecated
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Param, ParamType, ParamTypeKind};
|
||||
|
||||
#[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);
|
||||
|
||||
let param = Param { name: "a", r#type: Some(ParamType { value: "...string" }) };
|
||||
assert_eq!(param.r#type.and_then(|t| t.kind()), Some(ParamTypeKind::Repeated));
|
||||
|
||||
let param = Param { name: "a", r#type: Some(ParamType { value: "*" }) };
|
||||
assert_eq!(param.r#type.and_then(|t| t.kind()), Some(ParamTypeKind::Any));
|
||||
}
|
||||
}
|
||||
6
crates/oxc_semantic/src/jsdoc/parser/mod.rs
Normal file
6
crates/oxc_semantic/src/jsdoc/parser/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
mod jsdoc;
|
||||
mod jsdoc_tag;
|
||||
mod parse;
|
||||
mod utils;
|
||||
|
||||
pub use jsdoc::JSDoc;
|
||||
448
crates/oxc_semantic/src/jsdoc/parser/parse.rs
Normal file
448
crates/oxc_semantic/src/jsdoc/parser/parse.rs
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
use super::jsdoc_tag::{JSDocTag, JSDocTagKind};
|
||||
use super::jsdoc_tag::{Param, ParamType};
|
||||
use super::utils;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JSDocParser<'a> {
|
||||
source_text: &'a str,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
// 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 }
|
||||
}
|
||||
|
||||
pub fn parse(mut self) -> (String, Vec<JSDocTag<'a>>) {
|
||||
let comment = self.parse_comment();
|
||||
let tags = self.parse_tags();
|
||||
|
||||
(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.chars().nth(self.current) {
|
||||
match c {
|
||||
'@' => {
|
||||
self.current += 1;
|
||||
tags.push(self.parse_tag());
|
||||
}
|
||||
_ => {
|
||||
self.current += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if 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.chars().nth(self.current) {
|
||||
if c != ' ' {
|
||||
break;
|
||||
}
|
||||
self.current += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
if self.current < self.source_text.len() {
|
||||
self.current += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn at(&mut self, c: char) -> bool {
|
||||
let Some(ch) = self.source_text.chars().nth(self.current) else { return false };
|
||||
if ch == c {
|
||||
self.advance();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn take_until(&mut self, predicate: fn(char) -> bool) -> &'a str {
|
||||
let start = self.current;
|
||||
while let Some(c) = self.source_text.chars().nth(self.current) {
|
||||
if predicate(c) {
|
||||
break;
|
||||
}
|
||||
self.current += 1;
|
||||
}
|
||||
&self.source_text[start..self.current]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::JSDocParser;
|
||||
use super::{JSDocTag, JSDocTagKind};
|
||||
use super::{Param, ParamType};
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
#[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!(JSDocParser::new(" <- trim -> ").parse().0, "<- trim ->");
|
||||
assert_eq!(
|
||||
parse_from_full_text(
|
||||
"
|
||||
/**
|
||||
* <- omit this, keep this -> *
|
||||
*/
|
||||
"
|
||||
)
|
||||
.0,
|
||||
"<- omit this, keep this -> *"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_from_full_text(
|
||||
"/**
|
||||
this is comment
|
||||
@x
|
||||
*/"
|
||||
)
|
||||
.0,
|
||||
"this is comment"
|
||||
);
|
||||
}
|
||||
|
||||
#[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_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() }]
|
||||
);
|
||||
}
|
||||
|
||||
#[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() }
|
||||
]
|
||||
);
|
||||
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() }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_multiline_1_jsdoc() {
|
||||
assert_eq!(
|
||||
parse_from_full_text(
|
||||
"/** @yo
|
||||
*/"
|
||||
)
|
||||
.1,
|
||||
vec![JSDocTag { kind: JSDocTagKind::Unknown("yo"), comment: String::new() }]
|
||||
);
|
||||
assert_eq!(
|
||||
parse_from_full_text(
|
||||
"/**
|
||||
* @foo
|
||||
*/"
|
||||
)
|
||||
.1,
|
||||
vec![JSDocTag { kind: JSDocTagKind::Unknown("foo"), comment: String::new() }]
|
||||
);
|
||||
assert_eq!(
|
||||
parse_from_full_text(
|
||||
"
|
||||
/**
|
||||
* @x with asterisk
|
||||
*/
|
||||
"
|
||||
)
|
||||
.1,
|
||||
vec![JSDocTag {
|
||||
kind: JSDocTagKind::Unknown("x"),
|
||||
comment: "with asterisk".to_string()
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
parse_from_full_text(
|
||||
"
|
||||
/**
|
||||
@y without
|
||||
asterisk
|
||||
*/
|
||||
"
|
||||
)
|
||||
.1,
|
||||
vec![JSDocTag {
|
||||
kind: JSDocTagKind::Unknown("y"),
|
||||
comment: "without\nasterisk".to_string()
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_multiline_n_jsdocs() {
|
||||
assert_eq!(
|
||||
parse_from_full_text(
|
||||
"
|
||||
/**
|
||||
@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() },
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
parse_from_full_text(
|
||||
"/**
|
||||
* @one
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* @two
|
||||
*/"
|
||||
)
|
||||
.1,
|
||||
vec![
|
||||
JSDocTag { kind: JSDocTagKind::Unknown("one"), comment: "...".to_string() },
|
||||
JSDocTag { kind: JSDocTagKind::Unknown("two"), comment: String::new() },
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
parse_from_full_text(
|
||||
"/**
|
||||
* ...
|
||||
* @hey you!
|
||||
* Are you OK?
|
||||
* @yes I'm fine
|
||||
*/"
|
||||
)
|
||||
.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(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
58
crates/oxc_semantic/src/jsdoc/parser/utils.rs
Normal file
58
crates/oxc_semantic/src/jsdoc/parser/utils.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
pub fn trim_multiline_comment(s: &str) -> String {
|
||||
s.trim()
|
||||
.split('\n')
|
||||
.map(|line| line.trim().trim_start_matches('*').trim())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::trim_multiline_comment;
|
||||
|
||||
#[test]
|
||||
fn trim_multiline_jsdoc_comments() {
|
||||
for (actual, expect) in [
|
||||
("hello", "hello"),
|
||||
(
|
||||
"
|
||||
trim
|
||||
", "trim",
|
||||
),
|
||||
(
|
||||
"
|
||||
* asterisk
|
||||
",
|
||||
"asterisk",
|
||||
),
|
||||
(
|
||||
"
|
||||
* * li
|
||||
* * li
|
||||
",
|
||||
"* li\n* li",
|
||||
),
|
||||
(
|
||||
"
|
||||
* list
|
||||
* list
|
||||
",
|
||||
"list\nlist",
|
||||
),
|
||||
(
|
||||
"
|
||||
1
|
||||
|
||||
2
|
||||
|
||||
|
||||
3
|
||||
",
|
||||
"1\n2\n3",
|
||||
),
|
||||
] {
|
||||
assert_eq!(trim_multiline_comment(actual), expect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ pub use petgraph;
|
|||
|
||||
pub use builder::{SemanticBuilder, SemanticBuilderReturn};
|
||||
use class::ClassTable;
|
||||
pub use jsdoc::{JSDoc, JSDocComment, JSDocTag};
|
||||
pub use jsdoc::JSDocFinder;
|
||||
use oxc_ast::{ast::IdentifierReference, AstKind, TriviasMap};
|
||||
use oxc_span::SourceType;
|
||||
pub use oxc_syntax::{
|
||||
|
|
@ -60,7 +60,7 @@ pub struct Semantic<'a> {
|
|||
|
||||
module_record: Arc<ModuleRecord>,
|
||||
|
||||
jsdoc: JSDoc<'a>,
|
||||
jsdoc: JSDocFinder<'a>,
|
||||
|
||||
unused_labels: FxHashSet<AstNodeId>,
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ impl<'a> Semantic<'a> {
|
|||
&self.trivias
|
||||
}
|
||||
|
||||
pub fn jsdoc(&self) -> &JSDoc<'a> {
|
||||
pub fn jsdoc(&self) -> &JSDocFinder<'a> {
|
||||
&self.jsdoc
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue