1#![recursion_limit = "256"]
4#![deny(clippy::disallowed_methods)]
5#![deny(clippy::disallowed_types)]
6#![deny(clippy::print_stderr)]
7#![deny(clippy::print_stdout)]
8
9#[macro_use]
10extern crate cfg_if;
11#[macro_use]
12extern crate lazy_static;
13#[cfg(test)]
14#[macro_use]
15extern crate serde_json;
16
17pub mod class;
18mod decorators;
19mod diagnostics;
20pub mod diff;
21mod display;
22pub mod r#enum;
23pub mod function;
24pub mod html;
25pub mod interface;
26pub mod js_doc;
27pub mod node;
28mod params;
29mod parser;
30pub mod ts_type;
31pub mod ts_type_param;
32pub mod type_alias;
33mod util;
34pub mod variable;
35mod visibility;
36
37pub use node::Declaration;
38pub use node::DeclarationDef;
39pub use node::Document;
40pub use node::Location;
41pub use node::Symbol;
42
43use params::ParamDef;
44
45cfg_if! {
46 if #[cfg(feature = "rust")] {
47 mod printer;
48 pub use diagnostics::DocDiagnostic;
49 pub use diagnostics::DocDiagnosticKind;
50 pub use printer::DocPrinter;
51 }
52}
53
54pub use parser::DocError;
55pub use parser::DocParser;
56pub use parser::DocParserOptions;
57pub use parser::ParseOutput;
58
59#[cfg(test)]
60mod tests;
61
62#[cfg(feature = "rust")]
63pub fn find_nodes_by_name_recursively(
64 symbols: Vec<std::sync::Arc<Symbol>>,
65 name: &str,
66) -> Vec<Symbol> {
67 let mut parts = name.splitn(2, '.');
68 let name = parts.next();
69 let leftover = parts.next();
70 if name.is_none() {
71 return symbols
72 .into_iter()
73 .map(std::sync::Arc::unwrap_or_clone)
74 .collect();
75 }
76
77 let name = name.unwrap();
78 let symbol = symbols.into_iter().find(|symbol| &*symbol.name == name);
79
80 let mut found: Vec<Symbol> = vec![];
81
82 if let Some(symbol) = symbol {
83 let symbol = std::sync::Arc::unwrap_or_clone(symbol);
84 match leftover {
85 Some(leftover) => {
86 let children = get_children_of_node(symbol);
87 found.extend(find_nodes_by_name_recursively(children, leftover));
88 }
89 None => found.push(symbol),
90 }
91 }
92
93 found
94}
95
96#[cfg(feature = "rust")]
97fn get_children_of_node(node: Symbol) -> Vec<std::sync::Arc<Symbol>> {
98 use node::DeclarationDef;
99
100 let mut doc_nodes: Vec<std::sync::Arc<Symbol>> = vec![];
101 for decl in node.declarations {
102 match decl.def {
103 DeclarationDef::Namespace(namespace_def) => {
104 doc_nodes.extend(namespace_def.elements);
105 }
106 DeclarationDef::Interface(interface_def) => {
107 for method in interface_def.methods {
108 doc_nodes.push(std::sync::Arc::new(method.into()));
109 }
110 for property in interface_def.properties {
111 doc_nodes.push(std::sync::Arc::new(property.into()));
112 }
113 }
114 DeclarationDef::Class(class_def) => {
115 for method in class_def.methods.into_vec().into_iter() {
116 doc_nodes.push(std::sync::Arc::new(method.into()));
117 }
118 for property in class_def.properties.into_vec().into_iter() {
119 doc_nodes.push(std::sync::Arc::new(property.into()));
120 }
121 }
122 _ => {}
123 }
124 }
125 doc_nodes
126}
127
128pub fn docnodes_v1_to_v2(value: serde_json::Value) -> Document {
129 let serde_json::Value::Array(arr) = value else {
130 return Document::default();
131 };
132
133 let mut module_doc = js_doc::JsDoc::default();
138 let mut imports = Vec::new();
139 let mut symbols: indexmap::IndexMap<Box<str>, Symbol> =
140 indexmap::IndexMap::new();
141
142 for item in arr {
143 let serde_json::Value::Object(mut obj) = item else {
144 continue;
145 };
146
147 let kind = obj
148 .get("kind")
149 .and_then(|v| v.as_str())
150 .map(|s| s.to_string());
151
152 if kind.as_deref() == Some("moduleDoc") {
154 if let Some(mut js_doc_val) = obj.remove("jsDoc") {
155 migrate_js_doc_tags(&mut js_doc_val);
156 if let Ok(js_doc) = serde_json::from_value::<js_doc::JsDoc>(js_doc_val)
157 {
158 module_doc = js_doc;
159 }
160 }
161 continue;
162 }
163
164 if kind.as_deref() == Some("import") {
166 let imported_name: Box<str> = obj
167 .remove("name")
168 .and_then(|v| v.as_str().map(|s| s.into()))
169 .unwrap_or_else(|| "".into());
170 let import_def = obj.remove("importDef").unwrap_or_default();
171 let src = import_def
172 .get("src")
173 .and_then(|v| v.as_str())
174 .unwrap_or("")
175 .to_string();
176 let original_name = import_def
177 .get("imported")
178 .and_then(|v| v.as_str())
179 .map(|s| s.to_string());
180 let js_doc = obj
181 .remove("jsDoc")
182 .and_then(|mut v| {
183 migrate_js_doc_tags(&mut v);
184 serde_json::from_value::<js_doc::JsDoc>(v).ok()
185 })
186 .unwrap_or_default();
187 imports.push(node::Import {
188 imported_name,
189 original_name,
190 src,
191 js_doc,
192 });
193 continue;
194 }
195
196 let name: Box<str> = obj
197 .remove("name")
198 .and_then(|v| v.as_str().map(|s| s.into()))
199 .unwrap_or_else(|| "".into());
200
201 let is_default = obj
202 .remove("isDefault")
203 .and_then(|v| v.as_bool())
204 .unwrap_or(false);
205
206 let mut declaration = serde_json::Value::Object(obj);
207 migrate_declaration(&mut declaration);
208
209 let symbol = symbols.entry(name.clone()).or_insert_with(|| Symbol {
210 name,
211 is_default,
212 declarations: vec![],
213 });
214 if is_default {
216 symbol.is_default = true;
217 }
218 if let Ok(decl) = serde_json::from_value::<Declaration>(declaration) {
219 symbol.declarations.push(decl);
220 }
221 }
222
223 Document {
224 module_doc,
225 imports,
226 symbols: symbols.into_values().map(std::sync::Arc::new).collect(),
227 }
228}
229
230const V1_DEF_FIELDS: &[&str] = &[
234 "functionDef",
235 "variableDef",
236 "enumDef",
237 "classDef",
238 "typeAliasDef",
239 "namespaceDef",
240 "interfaceDef",
241 "referenceDef",
242 "reference_def",
243];
244
245fn migrate_declaration(value: &mut serde_json::Value) {
253 let serde_json::Value::Object(obj) = value else {
254 return;
255 };
256
257 for field in V1_DEF_FIELDS {
260 if let Some(val) = obj.remove(*field) {
261 obj.insert("def".to_string(), val);
262 break;
263 }
264 }
265
266 if let Some(serde_json::Value::Object(def)) = obj.get_mut("def")
271 && let Some(serde_json::Value::Array(elements)) = def.remove("elements")
272 {
273 let symbols = v1_nodes_to_symbols(elements);
274 def.insert("elements".to_string(), symbols);
275 }
276
277 migrate_ts_type_defs(value);
280 migrate_js_doc_tags(value);
283}
284
285fn v1_nodes_to_symbols(nodes: Vec<serde_json::Value>) -> serde_json::Value {
288 let mut symbols: indexmap::IndexMap<String, serde_json::Value> =
289 indexmap::IndexMap::new();
290
291 for node in nodes {
292 let serde_json::Value::Object(mut obj) = node else {
293 continue;
294 };
295
296 if obj.get("kind").and_then(|v| v.as_str()) == Some("import") {
298 continue;
299 }
300
301 let name = obj
302 .remove("name")
303 .and_then(|v| v.as_str().map(|s| s.to_string()))
304 .unwrap_or_default();
305
306 let is_default = obj
307 .remove("isDefault")
308 .and_then(|v| v.as_bool())
309 .unwrap_or(false);
310
311 let mut decl = serde_json::Value::Object(obj);
313 migrate_declaration(&mut decl);
314
315 let symbol = symbols.entry(name.clone()).or_insert_with(|| {
316 serde_json::json!({
317 "name": name,
318 "declarations": []
319 })
320 });
321
322 if is_default {
323 symbol["isDefault"] = serde_json::Value::Bool(true);
324 }
325
326 symbol["declarations"].as_array_mut().unwrap().push(decl);
327 }
328
329 serde_json::Value::Array(symbols.into_values().collect())
330}
331
332const V1_TS_TYPE_CONTENT_KEY_OVERRIDES: &[(&str, &str)] =
335 &[("conditional", "conditionalType"), ("mapped", "mappedType")];
336
337fn migrate_ts_type_defs(value: &mut serde_json::Value) {
341 match value {
342 serde_json::Value::Object(obj) => {
343 if obj.contains_key("repr")
346 && let Some(kind_str) = obj
347 .get("kind")
348 .and_then(|k| k.as_str())
349 .map(|s| s.to_string())
350 {
351 if kind_str == "this" || kind_str == "unsupported" {
355 obj.remove(&kind_str);
356 } else {
357 let content_key = V1_TS_TYPE_CONTENT_KEY_OVERRIDES
359 .iter()
360 .find(|(k, _)| *k == kind_str)
361 .map(|(_, v)| *v)
362 .unwrap_or(&kind_str);
363
364 if let Some(content) = obj.remove(content_key) {
365 obj.insert("value".to_string(), content);
366 }
367 }
368 }
369
370 for val in obj.values_mut() {
371 migrate_ts_type_defs(val);
372 }
373 }
374 serde_json::Value::Array(arr) => {
375 for val in arr.iter_mut() {
376 migrate_ts_type_defs(val);
377 }
378 }
379 _ => {}
380 }
381}
382
383fn migrate_js_doc_tags(value: &mut serde_json::Value) {
387 match value {
388 serde_json::Value::Object(obj) => {
389 if !obj.contains_key("repr") {
393 let is_type_bearing_tag =
394 obj.get("kind").and_then(|k| k.as_str()).is_some_and(|s| {
395 matches!(
396 s,
397 "enum"
398 | "extends"
399 | "param"
400 | "property"
401 | "return"
402 | "this"
403 | "throws"
404 | "typedef"
405 | "type"
406 )
407 });
408
409 if is_type_bearing_tag && let Some(type_val) = obj.remove("type") {
410 match type_val {
411 serde_json::Value::String(s) => {
412 obj.insert(
413 "tsType".to_string(),
414 serde_json::json!({
415 "repr": s,
416 "kind": "unsupported"
417 }),
418 );
419 }
420 serde_json::Value::Null => {
421 }
423 _ => {
424 }
428 }
429 }
430 }
431
432 for val in obj.values_mut() {
433 migrate_js_doc_tags(val);
434 }
435 }
436 serde_json::Value::Array(arr) => {
437 for val in arr.iter_mut() {
438 migrate_js_doc_tags(val);
439 }
440 }
441 _ => {}
442 }
443}
444
445#[cfg(test)]
446mod v1_to_v2_tests {
447 use super::*;
448 use serde_json::json;
449
450 #[test]
451 fn non_array_returns_default() {
452 let doc = docnodes_v1_to_v2(json!({}));
453 assert!(doc.symbols.is_empty());
454 assert!(doc.imports.is_empty());
455 assert!(doc.module_doc.is_empty());
456 }
457
458 #[test]
459 fn empty_array() {
460 let doc = docnodes_v1_to_v2(json!([]));
461 assert!(doc.symbols.is_empty());
462 }
463
464 #[test]
465 fn module_doc() {
466 let doc = docnodes_v1_to_v2(json!([
467 {
468 "kind": "moduleDoc",
469 "jsDoc": {
470 "doc": "Module documentation"
471 }
472 }
473 ]));
474 assert_eq!(doc.module_doc.doc.as_deref(), Some("Module documentation"));
475 assert!(doc.symbols.is_empty());
476 }
477
478 #[test]
479 fn import_node() {
480 let doc = docnodes_v1_to_v2(json!([
481 {
482 "kind": "import",
483 "name": "Foo",
484 "importDef": {
485 "src": "./foo.ts",
486 "imported": "Foo"
487 }
488 }
489 ]));
490 assert_eq!(doc.imports.len(), 1);
491 assert_eq!(&*doc.imports[0].imported_name, "Foo");
492 assert_eq!(doc.imports[0].src, "./foo.ts");
493 assert_eq!(doc.imports[0].original_name.as_deref(), Some("Foo"));
494 }
495
496 #[test]
497 fn variable_declaration() {
498 let doc = docnodes_v1_to_v2(json!([
499 {
500 "name": "myVar",
501 "kind": "variable",
502 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
503 "declarationKind": "export",
504 "variableDef": {
505 "kind": "const"
506 }
507 }
508 ]));
509 assert_eq!(doc.symbols.len(), 1);
510 assert_eq!(&*doc.symbols[0].name, "myVar");
511 assert_eq!(doc.symbols[0].declarations.len(), 1);
512 assert!(matches!(
513 doc.symbols[0].declarations[0].def,
514 node::DeclarationDef::Variable(_)
515 ));
516 }
517
518 #[test]
519 fn function_declaration() {
520 let doc = docnodes_v1_to_v2(json!([
521 {
522 "name": "myFunc",
523 "kind": "function",
524 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
525 "declarationKind": "export",
526 "functionDef": {
527 "params": [],
528 "returnType": {
529 "repr": "string",
530 "kind": "keyword",
531 "keyword": "string"
532 }
533 }
534 }
535 ]));
536 assert_eq!(doc.symbols.len(), 1);
537 assert_eq!(&*doc.symbols[0].name, "myFunc");
538 let decl = &doc.symbols[0].declarations[0];
539 match &decl.def {
540 node::DeclarationDef::Function(f) => {
541 let rt = f.return_type.as_ref().unwrap();
542 assert_eq!(rt.repr, "string");
543 }
544 other => panic!("expected Function, got {:?}", other),
545 }
546 }
547
548 #[test]
549 fn ts_type_def_migration() {
550 let doc = docnodes_v1_to_v2(json!([
552 {
553 "name": "x",
554 "kind": "variable",
555 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
556 "declarationKind": "export",
557 "variableDef": {
558 "kind": "const",
559 "tsType": {
560 "repr": "string",
561 "kind": "keyword",
562 "keyword": "string"
563 }
564 }
565 }
566 ]));
567 let decl = &doc.symbols[0].declarations[0];
568 match &decl.def {
569 node::DeclarationDef::Variable(v) => {
570 let ts_type = v.ts_type.as_ref().unwrap();
571 assert_eq!(ts_type.repr, "string");
572 }
573 other => panic!("expected Variable, got {:?}", other),
574 }
575 }
576
577 #[test]
578 fn is_default_flag() {
579 let doc = docnodes_v1_to_v2(json!([
580 {
581 "name": "default",
582 "isDefault": true,
583 "kind": "variable",
584 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
585 "declarationKind": "export",
586 "variableDef": { "kind": "const" }
587 }
588 ]));
589 assert!(doc.symbols[0].is_default);
590 }
591
592 #[test]
593 fn multiple_declarations_same_name() {
594 let doc = docnodes_v1_to_v2(json!([
595 {
596 "name": "myFunc",
597 "kind": "function",
598 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
599 "declarationKind": "export",
600 "functionDef": { "params": [] }
601 },
602 {
603 "name": "myFunc",
604 "kind": "function",
605 "location": { "filename": "test.ts", "line": 3, "col": 0, "byteIndex": 20 },
606 "declarationKind": "export",
607 "functionDef": { "params": [] }
608 }
609 ]));
610 assert_eq!(doc.symbols.len(), 1);
611 assert_eq!(doc.symbols[0].declarations.len(), 2);
612 }
613
614 #[test]
615 fn jsdoc_tag_type_to_ts_type_param() {
616 let doc = docnodes_v1_to_v2(json!([
617 {
618 "name": "myFunc",
619 "kind": "function",
620 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
621 "declarationKind": "export",
622 "jsDoc": {
623 "tags": [
624 { "kind": "param", "name": "x", "type": "string", "doc": "a param" }
625 ]
626 },
627 "functionDef": { "params": [] }
628 }
629 ]));
630 let decl = &doc.symbols[0].declarations[0];
631 let tag = &decl.js_doc.tags[0];
632 match tag {
633 js_doc::JsDocTag::Param { ts_type, name, .. } => {
634 assert_eq!(&**name, "x");
635 let ts = ts_type.as_ref().unwrap();
636 assert_eq!(ts.repr, "string");
637 }
638 other => panic!("expected Param, got {:?}", other),
639 }
640 }
641
642 #[test]
643 fn jsdoc_tag_type_to_ts_type_return() {
644 let doc = docnodes_v1_to_v2(json!([
645 {
646 "name": "myFunc",
647 "kind": "function",
648 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
649 "declarationKind": "export",
650 "jsDoc": {
651 "tags": [
652 { "kind": "return", "type": "number" }
653 ]
654 },
655 "functionDef": { "params": [] }
656 }
657 ]));
658 let tag = &doc.symbols[0].declarations[0].js_doc.tags[0];
659 match tag {
660 js_doc::JsDocTag::Return { ts_type, .. } => {
661 let ts = ts_type.as_ref().unwrap();
662 assert_eq!(ts.repr, "number");
663 }
664 other => panic!("expected Return, got {:?}", other),
665 }
666 }
667
668 #[test]
669 fn jsdoc_tag_type_to_ts_type_enum() {
670 let doc = docnodes_v1_to_v2(json!([
671 {
672 "name": "x",
673 "kind": "variable",
674 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
675 "declarationKind": "export",
676 "jsDoc": {
677 "tags": [
678 { "kind": "enum", "type": "number" }
679 ]
680 },
681 "variableDef": { "kind": "const" }
682 }
683 ]));
684 let tag = &doc.symbols[0].declarations[0].js_doc.tags[0];
685 match tag {
686 js_doc::JsDocTag::Enum { ts_type, .. } => {
687 assert_eq!(ts_type.repr, "number");
688 }
689 other => panic!("expected Enum, got {:?}", other),
690 }
691 }
692
693 #[test]
694 fn jsdoc_tag_type_to_ts_type_extends() {
695 let doc = docnodes_v1_to_v2(json!([
696 {
697 "name": "x",
698 "kind": "variable",
699 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
700 "declarationKind": "export",
701 "jsDoc": {
702 "tags": [
703 { "kind": "extends", "type": "Foo" }
704 ]
705 },
706 "variableDef": { "kind": "const" }
707 }
708 ]));
709 let tag = &doc.symbols[0].declarations[0].js_doc.tags[0];
710 match tag {
711 js_doc::JsDocTag::Extends { ts_type, .. } => {
712 assert_eq!(ts_type.repr, "Foo");
713 }
714 other => panic!("expected Extends, got {:?}", other),
715 }
716 }
717
718 #[test]
719 fn jsdoc_tag_type_to_ts_type_this() {
720 let doc = docnodes_v1_to_v2(json!([
721 {
722 "name": "x",
723 "kind": "variable",
724 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
725 "declarationKind": "export",
726 "jsDoc": {
727 "tags": [
728 { "kind": "this", "type": "Window" }
729 ]
730 },
731 "variableDef": { "kind": "const" }
732 }
733 ]));
734 let tag = &doc.symbols[0].declarations[0].js_doc.tags[0];
735 match tag {
736 js_doc::JsDocTag::This { ts_type, .. } => {
737 assert_eq!(ts_type.repr, "Window");
738 }
739 other => panic!("expected This, got {:?}", other),
740 }
741 }
742
743 #[test]
744 fn jsdoc_tag_type_to_ts_type_throws() {
745 let doc = docnodes_v1_to_v2(json!([
746 {
747 "name": "x",
748 "kind": "function",
749 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
750 "declarationKind": "export",
751 "jsDoc": {
752 "tags": [
753 { "kind": "throws", "type": "Error" }
754 ]
755 },
756 "functionDef": { "params": [] }
757 }
758 ]));
759 let tag = &doc.symbols[0].declarations[0].js_doc.tags[0];
760 match tag {
761 js_doc::JsDocTag::Throws { ts_type, .. } => {
762 let ts = ts_type.as_ref().unwrap();
763 assert_eq!(ts.repr, "Error");
764 }
765 other => panic!("expected Throws, got {:?}", other),
766 }
767 }
768
769 #[test]
770 fn jsdoc_tag_type_to_ts_type_typedef() {
771 let doc = docnodes_v1_to_v2(json!([
772 {
773 "name": "x",
774 "kind": "variable",
775 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
776 "declarationKind": "export",
777 "jsDoc": {
778 "tags": [
779 { "kind": "typedef", "name": "MyType", "type": "object" }
780 ]
781 },
782 "variableDef": { "kind": "const" }
783 }
784 ]));
785 let tag = &doc.symbols[0].declarations[0].js_doc.tags[0];
786 match tag {
787 js_doc::JsDocTag::TypeDef { ts_type, name, .. } => {
788 assert_eq!(&**name, "MyType");
789 assert_eq!(ts_type.repr, "object");
790 }
791 other => panic!("expected TypeDef, got {:?}", other),
792 }
793 }
794
795 #[test]
796 fn jsdoc_tag_type_to_ts_type_typeref() {
797 let doc = docnodes_v1_to_v2(json!([
798 {
799 "name": "x",
800 "kind": "variable",
801 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
802 "declarationKind": "export",
803 "jsDoc": {
804 "tags": [
805 { "kind": "type", "type": "Record<string, unknown>" }
806 ]
807 },
808 "variableDef": { "kind": "const" }
809 }
810 ]));
811 let tag = &doc.symbols[0].declarations[0].js_doc.tags[0];
812 match tag {
813 js_doc::JsDocTag::TypeRef { ts_type, .. } => {
814 assert_eq!(ts_type.repr, "Record<string, unknown>");
815 }
816 other => panic!("expected TypeRef, got {:?}", other),
817 }
818 }
819
820 #[test]
821 fn jsdoc_tag_type_to_ts_type_property() {
822 let doc = docnodes_v1_to_v2(json!([
823 {
824 "name": "x",
825 "kind": "variable",
826 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
827 "declarationKind": "export",
828 "jsDoc": {
829 "tags": [
830 { "kind": "property", "name": "foo", "type": "string" }
831 ]
832 },
833 "variableDef": { "kind": "const" }
834 }
835 ]));
836 let tag = &doc.symbols[0].declarations[0].js_doc.tags[0];
837 match tag {
838 js_doc::JsDocTag::Property { ts_type, name, .. } => {
839 assert_eq!(&**name, "foo");
840 assert_eq!(ts_type.repr, "string");
841 }
842 other => panic!("expected Property, got {:?}", other),
843 }
844 }
845
846 #[test]
847 fn jsdoc_tag_null_type_becomes_none() {
848 let doc = docnodes_v1_to_v2(json!([
849 {
850 "name": "myFunc",
851 "kind": "function",
852 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
853 "declarationKind": "export",
854 "jsDoc": {
855 "tags": [
856 { "kind": "return", "type": null }
857 ]
858 },
859 "functionDef": { "params": [] }
860 }
861 ]));
862 let tag = &doc.symbols[0].declarations[0].js_doc.tags[0];
863 match tag {
864 js_doc::JsDocTag::Return { ts_type, .. } => {
865 assert!(ts_type.is_none());
866 }
867 other => panic!("expected Return, got {:?}", other),
868 }
869 }
870
871 #[test]
872 fn module_doc_jsdoc_tags_migrated() {
873 let doc = docnodes_v1_to_v2(json!([
874 {
875 "kind": "moduleDoc",
876 "jsDoc": {
877 "doc": "Module docs",
878 "tags": [
879 { "kind": "type", "type": "module" }
880 ]
881 }
882 }
883 ]));
884 let tag = &doc.module_doc.tags[0];
885 match tag {
886 js_doc::JsDocTag::TypeRef { ts_type, .. } => {
887 assert_eq!(ts_type.repr, "module");
888 }
889 other => panic!("expected TypeRef, got {:?}", other),
890 }
891 }
892
893 #[test]
894 fn import_jsdoc_tags_migrated() {
895 let doc = docnodes_v1_to_v2(json!([
896 {
897 "kind": "import",
898 "name": "Foo",
899 "importDef": { "src": "./foo.ts" },
900 "jsDoc": {
901 "tags": [
902 { "kind": "type", "type": "Foo" }
903 ]
904 }
905 }
906 ]));
907 let tag = &doc.imports[0].js_doc.tags[0];
908 match tag {
909 js_doc::JsDocTag::TypeRef { ts_type, .. } => {
910 assert_eq!(ts_type.repr, "Foo");
911 }
912 other => panic!("expected TypeRef, got {:?}", other),
913 }
914 }
915
916 #[test]
917 fn migrate_ts_type_defs_nested() {
918 let doc = docnodes_v1_to_v2(json!([
920 {
921 "name": "myFunc",
922 "kind": "function",
923 "location": { "filename": "test.ts", "line": 1, "col": 0, "byteIndex": 0 },
924 "declarationKind": "export",
925 "functionDef": {
926 "params": [],
927 "returnType": {
928 "repr": "string[]",
929 "kind": "array",
930 "array": {
931 "repr": "string",
932 "kind": "keyword",
933 "keyword": "string"
934 }
935 }
936 }
937 }
938 ]));
939 let decl = &doc.symbols[0].declarations[0];
940 match &decl.def {
941 node::DeclarationDef::Function(f) => {
942 let rt = f.return_type.as_ref().unwrap();
943 assert_eq!(rt.repr, "string[]");
944 }
945 other => panic!("expected Function, got {:?}", other),
946 }
947 }
948
949 #[test]
950 fn zod_v1_fixture() {
951 let raw = std::fs::read_to_string(
952 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
953 .join("tests/testdata/@zod_zod_4.3.6_raw.json"),
954 )
955 .unwrap();
956 let fixture: serde_json::Map<String, serde_json::Value> =
957 serde_json::from_str(&raw).unwrap();
958
959 for (url, v1_nodes) in fixture {
960 let doc = docnodes_v1_to_v2(v1_nodes);
961 assert!(
962 !doc.symbols.is_empty(),
963 "{url}: expected at least one symbol"
964 );
965 for symbol in &doc.symbols {
966 assert!(
967 !symbol.declarations.is_empty(),
968 "{url}: symbol '{}' has no declarations",
969 symbol.name
970 );
971 }
972 let json = serde_json::to_value(&doc).unwrap();
974 let _: Document = serde_json::from_value(json).unwrap();
975 }
976 }
977}