From aad8134131c6a65f39c3198cb9ad97c01220a51f Mon Sep 17 00:00:00 2001 From: divybot Date: Mon, 1 Jun 2026 21:37:02 +0000 Subject: [PATCH] feat: support JSDoc @event, @fires, @emits, and @listens tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three new JsDocTag variants for documenting events on classes that fire or listen for them, e.g. subclasses of EventTarget: - `@event ` — declares an event a class can emit - `@fires ` / `@emits ` — documents which events a method/function fires - `@listens ` — documents which events a symbol listens for Names follow the JSDoc convention (e.g. `Hurl#snowball`) and an optional trailing description is captured into `doc`. Refs denoland/deno#26697 Co-Authored-By: Divy Srivastava --- src/diff/js_doc.rs | 3 ++ src/js_doc.rs | 121 ++++++++++++++++++++++++++++++++++++++++++++- src/printer.rs | 30 +++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) diff --git a/src/diff/js_doc.rs b/src/diff/js_doc.rs index d8dec0192..256b17181 100644 --- a/src/diff/js_doc.rs +++ b/src/diff/js_doc.rs @@ -107,11 +107,14 @@ fn tags_same_kind(a: &JsDocTag, b: &JsDocTag) -> bool { (Default { value: v1, .. }, Default { value: v2, .. }) => v1 == v2, (Deprecated { .. }, Deprecated { .. }) => true, (Enum { ts_type: t1, .. }, Enum { ts_type: t2, .. }) => t1 == t2, + (Event { name: n1, .. }, Event { name: n2, .. }) => n1 == n2, (Example { .. }, Example { .. }) => true, (Experimental, Experimental) => true, (Extends { ts_type: t1, .. }, Extends { ts_type: t2, .. }) => t1 == t2, + (Fires { name: n1, .. }, Fires { name: n2, .. }) => n1 == n2, (Ignore, Ignore) => true, (Internal, Internal) => true, + (Listens { name: n1, .. }, Listens { name: n2, .. }) => n1 == n2, (Module { .. }, Module { .. }) => true, (Param { name: n1, .. }, Param { name: n2, .. }) => n1 == n2, (Public, Public) => true, diff --git a/src/js_doc.rs b/src/js_doc.rs index 7bd458a48..567825c36 100644 --- a/src/js_doc.rs +++ b/src/js_doc.rs @@ -21,7 +21,7 @@ lazy_static! { /// @tag value static ref JS_DOC_TAG_WITH_VALUE_RE: Regex = Regex::new(r"(?s)^\s*@(category|group|see|example|tags|since|priority|summary|description)(?:\s+(.+))").unwrap(); /// @tag name maybe_value - static ref JS_DOC_TAG_NAMED_WITH_MAYBE_VALUE_RE: Regex = Regex::new(r"(?s)^\s*@(callback|template|typeparam|typeParam)\s+([a-zA-Z_$]\S*)(?:\s+(?:-\s+)?(.+))?").unwrap(); + static ref JS_DOC_TAG_NAMED_WITH_MAYBE_VALUE_RE: Regex = Regex::new(r"(?s)^\s*@(callback|template|typeparam|typeParam|event|fires|emits|listens)\s+([a-zA-Z_$]\S*)(?:\s+(?:-\s+)?(.+))?").unwrap(); /// @tag {type} name maybe_value static ref JS_DOC_TAG_NAMED_TYPED_RE: Regex = Regex::new(r"(?s)^\s*@(prop(?:erty)?|typedef)\s+\{([^}]+)\}\s+([a-zA-Z_$]\S*)(?:\s+(?:-\s+)?(.+))?").unwrap(); /// @tag {type} name maybe_value @@ -254,6 +254,12 @@ pub enum JsDocTag { #[serde(skip_serializing_if = "Option::is_none", default)] doc: Option>, }, + /// `@event name comment` + Event { + name: Box, + #[serde(skip_serializing_if = "Option::is_none", default)] + doc: Option>, + }, /// `@example comment` Example { #[serde(default)] @@ -268,10 +274,22 @@ pub enum JsDocTag { #[serde(skip_serializing_if = "Option::is_none", default)] doc: Option>, }, + /// `@fires name comment` or `@emits name comment` + Fires { + name: Box, + #[serde(skip_serializing_if = "Option::is_none", default)] + doc: Option>, + }, /// `@ignore` Ignore, /// `@internal` Internal, + /// `@listens name comment` + Listens { + name: Box, + #[serde(skip_serializing_if = "Option::is_none", default)] + doc: Option>, + }, /// `@module` /// `@module name` Module { @@ -405,6 +423,9 @@ impl JsDocTag { match kind { "callback" => Self::Callback { name, doc }, "template" | "typeparam" | "typeParam" => Self::Template { name, doc }, + "event" => Self::Event { name, doc }, + "fires" | "emits" => Self::Fires { name, doc }, + "listens" => Self::Listens { name, doc }, _ => unreachable!("kind unexpected: {}", kind), } } else if let Some(caps) = @@ -766,6 +787,70 @@ class Foo {} ] }) ); + assert_eq!( + serde_json::to_value(parse_jsdoc( + "@event Hurl#snowball Fired when a snowball is thrown" + )) + .unwrap(), + serde_json::json!({ + "tags": [ + { + "kind": "event", + "name": "Hurl#snowball", + "doc": "Fired when a snowball is thrown", + } + ] + }) + ); + assert_eq!( + serde_json::to_value(parse_jsdoc("@event open")).unwrap(), + serde_json::json!({ + "tags": [ + { + "kind": "event", + "name": "open", + } + ] + }) + ); + assert_eq!( + serde_json::to_value(parse_jsdoc("@fires Hurl#snowball")).unwrap(), + serde_json::json!({ + "tags": [ + { + "kind": "fires", + "name": "Hurl#snowball", + } + ] + }) + ); + assert_eq!( + serde_json::to_value(parse_jsdoc( + "@emits open - when the connection opens" + )) + .unwrap(), + serde_json::json!({ + "tags": [ + { + "kind": "fires", + "name": "open", + "doc": "when the connection opens", + } + ] + }) + ); + assert_eq!( + serde_json::to_value(parse_jsdoc("@listens module:hurler~snowball")) + .unwrap(), + serde_json::json!({ + "tags": [ + { + "kind": "listens", + "name": "module:hurler~snowball", + } + ] + }) + ); } #[test] @@ -1566,6 +1651,40 @@ multi-line "name": "T", }) ); + assert_eq!( + serde_json::to_value(JsDocTag::Event { + name: "snowball".into(), + doc: Some("fired when a snowball is thrown".into()), + }) + .unwrap(), + json!({ + "kind": "event", + "name": "snowball", + "doc": "fired when a snowball is thrown", + }) + ); + assert_eq!( + serde_json::to_value(JsDocTag::Fires { + name: "Hurl#snowball".into(), + doc: None, + }) + .unwrap(), + json!({ + "kind": "fires", + "name": "Hurl#snowball", + }) + ); + assert_eq!( + serde_json::to_value(JsDocTag::Listens { + name: "module:hurler~snowball".into(), + doc: None, + }) + .unwrap(), + json!({ + "kind": "listens", + "name": "module:hurler~snowball", + }) + ); assert_eq!( serde_json::to_value(JsDocTag::This { ts_type: TsTypeDef { diff --git a/src/printer.rs b/src/printer.rs index 46206f331..60b1eedc5 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -273,6 +273,16 @@ impl DocPrinter<'_> { )?; self.format_jsdoc_tag_maybe_doc(w, doc, indent) } + JsDocTag::Event { name, doc } => { + writeln!( + w, + "{}@{} {}", + Indent(indent), + colors::magenta("event"), + colors::bold(name) + )?; + self.format_jsdoc_tag_maybe_doc(w, doc, indent) + } JsDocTag::Example { doc } => { writeln!(w, "{}@{}", Indent(indent), colors::magenta("example"))?; self.format_jsdoc_tag_doc(w, doc, indent) @@ -290,12 +300,32 @@ impl DocPrinter<'_> { )?; self.format_jsdoc_tag_maybe_doc(w, doc, indent) } + JsDocTag::Fires { name, doc } => { + writeln!( + w, + "{}@{} {}", + Indent(indent), + colors::magenta("fires"), + colors::bold(name) + )?; + self.format_jsdoc_tag_maybe_doc(w, doc, indent) + } JsDocTag::Ignore => { writeln!(w, "{}@{}", Indent(indent), colors::magenta("ignore")) } JsDocTag::Internal => { writeln!(w, "{}@{}", Indent(indent), colors::magenta("internal")) } + JsDocTag::Listens { name, doc } => { + writeln!( + w, + "{}@{} {}", + Indent(indent), + colors::magenta("listens"), + colors::bold(name) + )?; + self.format_jsdoc_tag_maybe_doc(w, doc, indent) + } JsDocTag::Module { name } => { writeln!(w, "{}@{}", Indent(indent), colors::magenta("module"))?; self.format_jsdoc_tag_maybe_doc(w, name, indent)