Skip to content

Commit 2f2125a

Browse files
authored
feat: JSDoc @throws, @SInCE, @experimental, @internal tags (#563)
1 parent e6c05bc commit 2f2125a

7 files changed

+152
-23
lines changed

src/js_doc.rs

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ use serde::Serialize;
66

77
lazy_static! {
88
static ref JS_DOC_TAG_MAYBE_DOC_RE: Regex = Regex::new(r"(?s)^\s*@(deprecated)(?:\s+(.+))?").unwrap();
9-
static ref JS_DOC_TAG_DOC_RE: Regex = Regex::new(r"(?s)^\s*@(category|see|example|tags)(?:\s+(.+))").unwrap();
9+
static ref JS_DOC_TAG_DOC_RE: Regex = Regex::new(r"(?s)^\s*@(category|see|example|tags|since)(?:\s+(.+))").unwrap();
1010
static ref JS_DOC_TAG_NAMED_RE: Regex = Regex::new(r"(?s)^\s*@(callback|template|typeparam|typeParam)\s+([a-zA-Z_$]\S*)(?:\s+(.+))?").unwrap();
1111
static ref JS_DOC_TAG_NAMED_TYPED_RE: Regex = Regex::new(r"(?s)^\s*@(prop(?:erty)?|typedef)\s+\{([^}]+)\}\s+([a-zA-Z_$]\S*)(?:\s+(.+))?").unwrap();
12-
static ref JS_DOC_TAG_ONLY_RE: Regex = Regex::new(r"^\s*@(constructor|class|ignore|module|public|private|protected|readonly)").unwrap();
12+
static ref JS_DOC_TAG_ONLY_RE: Regex = Regex::new(r"^\s*@(constructor|class|ignore|internal|module|public|private|protected|readonly|experimental)").unwrap();
1313
static ref JS_DOC_TAG_PARAM_RE: Regex = Regex::new(
1414
r"(?s)^\s*@(?:param|arg(?:ument)?)(?:\s+\{(?P<type>[^}]+)\})?\s+(?:(?:\[(?P<nameWithDefault>[a-zA-Z_$]\S*?)(?:\s*=\s*(?P<default>[^]]+))?\])|(?P<name>[a-zA-Z_$]\S*))(?:\s+(?P<doc>.+))?"
1515
)
1616
.unwrap();
1717
static ref JS_DOC_TAG_RE: Regex = Regex::new(r"(?s)^\s*@(\S+)").unwrap();
18-
static ref JS_DOC_TAG_RETURN_RE: Regex = Regex::new(r"(?s)^\s*@returns?(?:\s+\{([^}]+)\})?(?:\s+(.+))?").unwrap();
18+
static ref JS_DOC_TAG_OPTIONAL_TYPE_AND_DOC_RE: Regex = Regex::new(r"(?s)^\s*@(returns?|throws|exception)(?:\s+\{([^}]+)\})?(?:\s+(.+))?").unwrap();
1919
static ref JS_DOC_TAG_TYPED_RE: Regex = Regex::new(r"(?s)^\s*@(enum|extends|augments|this|type|default)\s+\{([^}]+)\}(?:\s+(.+))?").unwrap();
2020
}
2121

@@ -115,6 +115,8 @@ pub enum JsDocTag {
115115
#[serde(default)]
116116
doc: String,
117117
},
118+
/// `@experimental`
119+
Experimental,
118120
/// `@extends {type} comment`
119121
Extends {
120122
#[serde(rename = "type")]
@@ -124,6 +126,8 @@ pub enum JsDocTag {
124126
},
125127
/// `@ignore`
126128
Ignore,
129+
/// `@internal`
130+
Internal,
127131
/// `@module`
128132
Module,
129133
/// `@param`, `@arg` or `argument`, in format of `@param {type} name comment`
@@ -182,6 +186,13 @@ pub enum JsDocTag {
182186
#[serde(skip_serializing_if = "Option::is_none", default)]
183187
doc: Option<String>,
184188
},
189+
/// `@throws {type} comment` or `@exception {type} comment`
190+
Throws {
191+
#[serde(rename = "type")]
192+
type_ref: Option<String>,
193+
#[serde(skip_serializing_if = "Option::is_none", default)]
194+
doc: Option<String>,
195+
},
185196
/// `@typedef {type} name comment`
186197
TypeDef {
187198
name: String,
@@ -202,6 +213,10 @@ pub enum JsDocTag {
202213
See {
203214
doc: String,
204215
},
216+
/// `@since version`
217+
Since {
218+
doc: String,
219+
},
205220
Unsupported {
206221
value: String,
207222
},
@@ -213,7 +228,9 @@ impl From<String> for JsDocTag {
213228
let kind = caps.get(1).unwrap().as_str();
214229
match kind {
215230
"constructor" | "class" => Self::Constructor,
231+
"experimental" => Self::Experimental,
216232
"ignore" => Self::Ignore,
233+
"internal" => Self::Internal,
217234
"module" => Self::Module,
218235
"public" => Self::Public,
219236
"private" => Self::Private,
@@ -280,6 +297,7 @@ impl From<String> for JsDocTag {
280297
tags: doc.split(',').map(|i| i.trim().to_string()).collect(),
281298
},
282299
"see" => Self::See { doc },
300+
"since" => Self::Since { doc },
283301
_ => unreachable!("kind unexpected: {}", kind),
284302
}
285303
} else if let Some(caps) = JS_DOC_TAG_PARAM_RE.captures(&value) {
@@ -300,10 +318,17 @@ impl From<String> for JsDocTag {
300318
default,
301319
doc,
302320
}
303-
} else if let Some(caps) = JS_DOC_TAG_RETURN_RE.captures(&value) {
304-
let type_ref = caps.get(1).map(|m| m.as_str().to_string());
305-
let doc = caps.get(2).map(|m| m.as_str().to_string());
306-
Self::Return { type_ref, doc }
321+
} else if let Some(caps) =
322+
JS_DOC_TAG_OPTIONAL_TYPE_AND_DOC_RE.captures(&value)
323+
{
324+
let kind = caps.get(1).unwrap().as_str();
325+
let type_ref = caps.get(2).map(|m| m.as_str().to_string());
326+
let doc = caps.get(3).map(|m| m.as_str().to_string());
327+
match kind {
328+
"return" | "returns" => Self::Return { type_ref, doc },
329+
"throws" | "exception" => Self::Throws { type_ref, doc },
330+
_ => unreachable!("kind unexpected: {}", kind),
331+
}
307332
} else {
308333
Self::Unsupported { value }
309334
}
@@ -325,6 +350,11 @@ mod tests {
325350
serde_json::to_value(JsDoc::from("@class more".to_string())).unwrap(),
326351
json!({ "tags": [ { "kind": "constructor" } ] }),
327352
);
353+
assert_eq!(
354+
serde_json::to_value(JsDoc::from("@experimental more".to_string()))
355+
.unwrap(),
356+
json!({ "tags": [ { "kind": "experimental" } ] }),
357+
);
328358
assert_eq!(
329359
serde_json::to_value(JsDoc::from("@ignore more".to_string())).unwrap(),
330360
json!({ "tags": [ { "kind": "ignore" } ] }),
@@ -629,6 +659,15 @@ if (true) {
629659
}]
630660
})
631661
);
662+
assert_eq!(
663+
serde_json::to_value(JsDoc::from("@since 1.0.0".to_string())).unwrap(),
664+
json!({
665+
"tags": [{
666+
"kind": "since",
667+
"doc": "1.0.0"
668+
}]
669+
})
670+
);
632671

633672
assert_eq!(
634673
serde_json::to_value(JsDoc::from(
@@ -637,6 +676,7 @@ if (true) {
637676
const a = "a";
638677
@category foo
639678
@see bar
679+
@since 1.0.0
640680
"#
641681
.to_string()
642682
))
@@ -654,6 +694,9 @@ const a = "a";
654694
}, {
655695
"kind": "see",
656696
"doc": "bar"
697+
}, {
698+
"kind": "since",
699+
"doc": "1.0.0"
657700
}]
658701

659702
})
@@ -798,6 +841,49 @@ const a = "a";
798841
);
799842
}
800843

844+
#[test]
845+
fn test_js_doc_tag_throws() {
846+
assert_eq!(
847+
serde_json::to_value(JsDoc::from(
848+
"@throws {string} maybe doc\n\nnew paragraph".to_string()
849+
))
850+
.unwrap(),
851+
json!({
852+
"tags": [{
853+
"kind": "throws",
854+
"type": "string",
855+
"doc": "maybe doc\n\nnew paragraph",
856+
}]
857+
})
858+
);
859+
assert_eq!(
860+
serde_json::to_value(JsDoc::from(
861+
"@throws maybe doc\n\nnew paragraph".to_string()
862+
))
863+
.unwrap(),
864+
json!({
865+
"tags": [{
866+
"kind": "throws",
867+
"type": null,
868+
"doc": "maybe doc\n\nnew paragraph",
869+
}]
870+
})
871+
);
872+
assert_eq!(
873+
serde_json::to_value(JsDoc::from(
874+
"@throws {string} maybe doc\n\nnew paragraph".to_string()
875+
))
876+
.unwrap(),
877+
json!({
878+
"tags": [{
879+
"kind": "throws",
880+
"type": "string",
881+
"doc": "maybe doc\n\nnew paragraph",
882+
}]
883+
})
884+
);
885+
}
886+
801887
#[test]
802888
fn test_js_doc_from_str() {
803889
assert_eq!(

src/printer.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ impl<'a> DocPrinter<'a> {
255255
writeln!(w, "{}@{}", Indent(indent), colors::magenta("example"))?;
256256
self.format_jsdoc_tag_doc(w, doc, indent)
257257
}
258+
JsDocTag::Experimental => {
259+
writeln!(w, "{}@{}", Indent(indent), colors::magenta("experimental"))
260+
}
258261
JsDocTag::Extends { type_ref, doc } => {
259262
writeln!(
260263
w,
@@ -268,6 +271,9 @@ impl<'a> DocPrinter<'a> {
268271
JsDocTag::Ignore => {
269272
writeln!(w, "{}@{}", Indent(indent), colors::magenta("ignore"))
270273
}
274+
JsDocTag::Internal => {
275+
writeln!(w, "{}@{}", Indent(indent), colors::magenta("internal"))
276+
}
271277
JsDocTag::Module => {
272278
writeln!(w, "{}@{}", Indent(indent), colors::magenta("module"))
273279
}
@@ -395,6 +401,19 @@ impl<'a> DocPrinter<'a> {
395401
writeln!(w, "{}@{}", Indent(indent), colors::magenta("see"))?;
396402
self.format_jsdoc_tag_doc(w, doc, indent)
397403
}
404+
JsDocTag::Since { doc } => {
405+
writeln!(w, "{}@{}", Indent(indent), colors::magenta("since"))?;
406+
self.format_jsdoc_tag_doc(w, doc, indent)
407+
}
408+
JsDocTag::Throws { type_ref, doc } => {
409+
write!(w, "{}@{}", Indent(indent), colors::magenta("return"))?;
410+
if let Some(type_ref) = type_ref {
411+
writeln!(w, " {{{}}}", colors::italic_cyan(type_ref))?;
412+
} else {
413+
writeln!(w)?;
414+
}
415+
self.format_jsdoc_tag_maybe_doc(w, doc, indent)
416+
}
398417
}
399418
}
400419

src/util/swc.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@ pub fn module_export_name_value(
124124

125125
/// If the jsdoc has an `@internal` or `@ignore` tag.
126126
pub fn has_ignorable_js_doc_tag(js_doc: &JsDoc) -> bool {
127-
js_doc.tags.iter().any(|t| matches!(t, JsDocTag::Ignore) || matches!(t, JsDocTag::Unsupported { value } if value == "@internal" || value.starts_with("@internal ")))
127+
js_doc
128+
.tags
129+
.iter()
130+
.any(|t| *t == JsDocTag::Ignore || *t == JsDocTag::Internal)
128131
}
129132

130133
#[cfg(test)]

tests/specs/internal_tag.txt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,7 @@ type Test = OtherPrivateType
9191
"jsDoc": {
9292
"tags": [
9393
{
94-
"kind": "unsupported",
95-
"value": "@internal"
94+
"kind": "internal"
9695
}
9796
]
9897
},
@@ -190,8 +189,7 @@ type Test = OtherPrivateType
190189
"jsDoc": {
191190
"tags": [
192191
{
193-
"kind": "unsupported",
194-
"value": "@internal"
192+
"kind": "internal"
195193
}
196194
]
197195
},
@@ -216,8 +214,7 @@ type Test = OtherPrivateType
216214
"jsDoc": {
217215
"tags": [
218216
{
219-
"kind": "unsupported",
220-
"value": "@internal"
217+
"kind": "internal"
221218
}
222219
]
223220
},
@@ -284,8 +281,7 @@ type Test = OtherPrivateType
284281
"jsDoc": {
285282
"tags": [
286283
{
287-
"kind": "unsupported",
288-
"value": "@internal"
284+
"kind": "internal"
289285
}
290286
]
291287
},

tests/specs/jsdoc_tags.txt

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
* @param [c=1] additional doc
77
* @param [d] more doc
88
* @returns {string} returning doc
9+
* @throws {number} throw doc
10+
* @since 1.0.0
11+
* @experimental
12+
* @internal
913
*/
1014
export function a(b, c, d) {}
1115

1216
# output.txt
13-
Defined in file:///mod.ts:9:1
17+
Defined in file:///mod.ts:13:1
1418

1519
function a(b, c, d): void
1620
a is a function
@@ -27,6 +31,14 @@ function a(b, c, d): void
2731
@return {string}
2832
returning doc
2933

34+
@return {number}
35+
throw doc
36+
37+
@since
38+
1.0.0
39+
40+
@experimental
41+
@internal
3042

3143

3244
# output.json
@@ -36,9 +48,9 @@ function a(b, c, d): void
3648
"name": "a",
3749
"location": {
3850
"filename": "file:///mod.ts",
39-
"line": 9,
51+
"line": 13,
4052
"col": 0,
41-
"byteIndex": 149
53+
"byteIndex": 225
4254
},
4355
"declarationKind": "export",
4456
"jsDoc": {
@@ -66,6 +78,21 @@ function a(b, c, d): void
6678
"kind": "return",
6779
"type": "string",
6880
"doc": "returning doc"
81+
},
82+
{
83+
"kind": "throws",
84+
"type": "number",
85+
"doc": "throw doc"
86+
},
87+
{
88+
"kind": "since",
89+
"doc": "1.0.0"
90+
},
91+
{
92+
"kind": "experimental"
93+
},
94+
{
95+
"kind": "internal"
6996
}
7097
]
7198
},

tests/specs/private_type_ignored_class_not_namespace.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,7 @@ private namespace MyNamespace
7373
"jsDoc": {
7474
"tags": [
7575
{
76-
"kind": "unsupported",
77-
"value": "@internal"
76+
"kind": "internal"
7877
}
7978
]
8079
},

tests/specs/private_type_re_export_referencing.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,7 @@ class Data
138138
"jsDoc": {
139139
"tags": [
140140
{
141-
"kind": "unsupported",
142-
"value": "@internal"
141+
"kind": "internal"
143142
}
144143
]
145144
},

0 commit comments

Comments
 (0)