// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use super::DocParser; use crate::colors; use serde_json::json; use super::parser::DocFileLoader; use crate::op_error::OpError; use std::collections::HashMap; use futures::Future; use futures::FutureExt; use std::pin::Pin; pub struct TestLoader { files: HashMap, } impl TestLoader { pub fn new(files_vec: Vec<(String, String)>) -> Box { let mut files = HashMap::new(); for file_tuple in files_vec { files.insert(file_tuple.0, file_tuple.1); } Box::new(Self { files }) } } impl DocFileLoader for TestLoader { fn load_source_code( &self, specifier: &str, ) -> Pin>>> { let res = match self.files.get(specifier) { Some(source_code) => Ok(source_code.to_string()), None => Err(OpError::other("not found".to_string())), }; async move { res }.boxed_local() } } #[tokio::test] async fn export_fn() { let source_code = r#"/** * Hello there, this is a multiline JSdoc. * * It has many lines * * Or not that many? */ export function foo(a: string, b?: number, cb: (...cbArgs: unknown[]) => void, ...args: unknown[]): void { console.log("Hello world"); } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let entry = &entries[0]; let expected_json = json!({ "functionDef": { "isAsync": false, "isGenerator": false, "typeParams": [], "params": [ { "name": "a", "kind": "identifier", "optional": false, "tsType": { "keyword": "string", "kind": "keyword", "repr": "string", }, }, { "name": "b", "kind": "identifier", "optional": true, "tsType": { "keyword": "number", "kind": "keyword", "repr": "number", }, }, { "name": "cb", "kind": "identifier", "optional": false, "tsType": { "repr": "", "kind": "fnOrConstructor", "fnOrConstructor": { "constructor": false, "tsType": { "keyword": "void", "kind": "keyword", "repr": "void" }, "typeParams": [], "params": [{ "kind": "rest", "name": "cbArgs", "optional": false, "tsType": { "repr": "", "kind": "array", "array": { "repr": "unknown", "kind": "keyword", "keyword": "unknown" } }, }] } }, }, { "name": "args", "kind": "rest", "optional": false, "tsType": { "repr": "", "kind": "array", "array": { "repr": "unknown", "kind": "keyword", "keyword": "unknown" } } } ], "returnType": { "keyword": "void", "kind": "keyword", "repr": "void", }, }, "jsDoc": "Hello there, this is a multiline JSdoc.\n\nIt has many lines\n\nOr not that many?", "kind": "function", "location": { "col": 0, "filename": "test.ts", "line": 8, }, "name": "foo", }); let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!(colors::strip_ansi_codes( super::printer::format(entries.clone()).as_str() ) .contains("Hello there")); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("b?: number") ); } #[tokio::test] async fn export_fn2() { let source_code = r#" interface AssignOpts { a: string; b: number; } export function foo([e,,f, ...g]: number[], { c, d: asdf, i = "asdf", ...rest}, ops: AssignOpts = {}): void { console.log("Hello world"); } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let entry = &entries[0]; let expected_json = json!({ "functionDef": { "isAsync": false, "isGenerator": false, "typeParams": [], "params": [ { "name": "", "kind": "array", "optional": false, "tsType": { "repr": "", "kind": "array", "array": { "repr": "number", "kind": "keyword", "keyword": "number" } } }, { "name": "", "kind": "object", "optional": false, "tsType": null }, { "name": "ops", "kind": "identifier", "optional": false, "tsType": { "repr": "AssignOpts", "kind": "typeRef", "typeRef": { "typeName": "AssignOpts", "typeParams": null, } } }, ], "returnType": { "keyword": "void", "kind": "keyword", "repr": "void", }, }, "jsDoc": null, "kind": "function", "location": { "col": 0, "filename": "test.ts", "line": 7, }, "name": "foo", }); let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("foo") ); } #[tokio::test] async fn export_const() { let source_code = r#" /** Something about fizzBuzz */ export const fizzBuzz = "fizzBuzz"; export const env: { /** get doc */ get(key: string): string | undefined; /** set doc */ set(key: string, value: string): void; } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 2); let expected_json = json!([ { "kind":"variable", "name":"fizzBuzz", "location":{ "filename":"test.ts", "line":3, "col":0 }, "jsDoc":"Something about fizzBuzz", "variableDef":{ "tsType":null, "kind":"const" } }, { "kind":"variable", "name":"env", "location":{ "filename":"test.ts", "line":5, "col":0 }, "jsDoc":null, "variableDef":{ "tsType":{ "repr":"", "kind":"typeLiteral", "typeLiteral":{ "methods":[{ "name":"get", "params":[ { "name":"key", "kind":"identifier", "optional":false, "tsType":{ "repr":"string", "kind":"keyword", "keyword":"string" } } ], "returnType":{ "repr":"", "kind":"union", "union":[ { "repr":"string", "kind":"keyword", "keyword":"string" }, { "repr":"undefined", "kind":"keyword", "keyword":"undefined" } ] }, "typeParams":[] }, { "name":"set", "params":[ { "name":"key", "kind":"identifier", "optional":false, "tsType":{ "repr":"string", "kind":"keyword", "keyword":"string" } }, { "name":"value", "kind":"identifier", "optional":false, "tsType":{ "repr":"string", "kind":"keyword", "keyword":"string" } } ], "returnType":{ "repr":"void", "kind":"keyword", "keyword":"void" }, "typeParams":[] } ], "properties":[], "callSignatures":[] } }, "kind":"const" } } ] ); let actual = serde_json::to_value(entries.clone()).unwrap(); assert_eq!(actual, expected_json); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("Something about fizzBuzz") ); } #[tokio::test] async fn export_class() { let source_code = r#" /** Class doc */ export class Foobar extends Fizz implements Buzz, Aldrin { private private1?: boolean; protected protected1: number; public public1: boolean; public2: number; /** Constructor js doc */ constructor(name: string, private private2: number, protected protected2: number) {} /** Async foo method */ async foo(): Promise { // } /** Sync bar method */ bar?(): void { // } } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let expected_json = json!({ "kind": "class", "name": "Foobar", "location": { "filename": "test.ts", "line": 3, "col": 0 }, "jsDoc": "Class doc", "classDef": { "isAbstract": false, "extends": "Fizz", "implements": ["Buzz", "Aldrin"], "typeParams": [], "constructors": [ { "jsDoc": "Constructor js doc", "accessibility": null, "name": "constructor", "params": [ { "name": "name", "kind": "identifier", "optional": false, "tsType": { "repr": "string", "kind": "keyword", "keyword": "string" } }, { "name": "private2", "kind": "identifier", "optional": false, "tsType": { "repr": "number", "kind": "keyword", "keyword": "number" } }, { "name": "protected2", "kind": "identifier", "optional": false, "tsType": { "repr": "number", "kind": "keyword", "keyword": "number" } } ], "location": { "filename": "test.ts", "line": 10, "col": 4 } } ], "properties": [ { "jsDoc": null, "tsType": { "repr": "boolean", "kind": "keyword", "keyword": "boolean" }, "readonly": false, "accessibility": "private", "optional": true, "isAbstract": false, "isStatic": false, "name": "private1", "location": { "filename": "test.ts", "line": 4, "col": 4 } }, { "jsDoc": null, "tsType": { "repr": "number", "kind": "keyword", "keyword": "number" }, "readonly": false, "accessibility": "protected", "optional": false, "isAbstract": false, "isStatic": false, "name": "protected1", "location": { "filename": "test.ts", "line": 5, "col": 4 } }, { "jsDoc": null, "tsType": { "repr": "boolean", "kind": "keyword", "keyword": "boolean" }, "readonly": false, "accessibility": "public", "optional": false, "isAbstract": false, "isStatic": false, "name": "public1", "location": { "filename": "test.ts", "line": 6, "col": 4 } }, { "jsDoc": null, "tsType": { "repr": "number", "kind": "keyword", "keyword": "number" }, "readonly": false, "accessibility": null, "optional": false, "isAbstract": false, "isStatic": false, "name": "public2", "location": { "filename": "test.ts", "line": 7, "col": 4 } } ], "methods": [ { "jsDoc": "Async foo method", "accessibility": null, "optional": false, "isAbstract": false, "isStatic": false, "name": "foo", "kind": "method", "functionDef": { "params": [], "returnType": { "repr": "Promise", "kind": "typeRef", "typeRef": { "typeParams": [ { "repr": "void", "kind": "keyword", "keyword": "void" } ], "typeName": "Promise" } }, "typeParams": [], "isAsync": true, "isGenerator": false }, "location": { "filename": "test.ts", "line": 13, "col": 4 } }, { "jsDoc": "Sync bar method", "accessibility": null, "optional": true, "isAbstract": false, "isStatic": false, "name": "bar", "kind": "method", "functionDef": { "params": [], "returnType": { "repr": "void", "kind": "keyword", "keyword": "void" }, "isAsync": false, "isGenerator": false, "typeParams": [] }, "location": { "filename": "test.ts", "line": 18, "col": 4 } } ] } }); let entry = &entries[0]; let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!(colors::strip_ansi_codes( super::printer::format_details(entry.clone()).as_str() ) .contains("bar?(): void")); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("class Foobar extends Fizz implements Buzz, Aldrin") ); } #[tokio::test] async fn export_interface() { let source_code = r#" interface Foo { foo(): void; } interface Bar { bar(): void; } /** * Interface js doc */ export interface Reader extends Foo, Bar { /** Read n bytes */ read?(buf: Uint8Array, something: unknown): Promise } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let entry = &entries[0]; let expected_json = json!({ "kind": "interface", "name": "Reader", "location": { "filename": "test.ts", "line": 11, "col": 0 }, "jsDoc": "Interface js doc", "interfaceDef": { "extends": ["Foo", "Bar"], "methods": [ { "name": "read", "location": { "filename": "test.ts", "line": 13, "col": 4 }, "optional": true, "jsDoc": "Read n bytes", "params": [ { "name": "buf", "kind": "identifier", "optional": false, "tsType": { "repr": "Uint8Array", "kind": "typeRef", "typeRef": { "typeParams": null, "typeName": "Uint8Array" } } }, { "name": "something", "kind": "identifier", "optional": false, "tsType": { "repr": "unknown", "kind": "keyword", "keyword": "unknown" } } ], "typeParams": [], "returnType": { "repr": "Promise", "kind": "typeRef", "typeRef": { "typeParams": [ { "repr": "number", "kind": "keyword", "keyword": "number" } ], "typeName": "Promise" } } } ], "properties": [], "callSignatures": [], "typeParams": [], } }); let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("interface Reader extends Foo, Bar") ); } #[tokio::test] async fn export_interface2() { let source_code = r#" export interface TypedIface { something(): T } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let entry = &entries[0]; let expected_json = json!({ "kind": "interface", "name": "TypedIface", "location": { "filename": "test.ts", "line": 2, "col": 0 }, "jsDoc": null, "interfaceDef": { "extends": [], "methods": [ { "name": "something", "location": { "filename": "test.ts", "line": 3, "col": 4 }, "jsDoc": null, "optional": false, "params": [], "typeParams": [], "returnType": { "repr": "T", "kind": "typeRef", "typeRef": { "typeParams": null, "typeName": "T" } } } ], "properties": [], "callSignatures": [], "typeParams": [ { "name": "T" } ], } }); let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("interface TypedIface") ); } #[tokio::test] async fn export_type_alias() { let source_code = r#" /** Array holding numbers */ export type NumberArray = Array; "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let entry = &entries[0]; let expected_json = json!({ "kind": "typeAlias", "name": "NumberArray", "location": { "filename": "test.ts", "line": 3, "col": 0 }, "jsDoc": "Array holding numbers", "typeAliasDef": { "typeParams": [], "tsType": { "repr": "Array", "kind": "typeRef", "typeRef": { "typeParams": [ { "repr": "number", "kind": "keyword", "keyword": "number" } ], "typeName": "Array" } } } }); let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("Array holding numbers") ); } #[tokio::test] async fn export_enum() { let source_code = r#" /** * Some enum for good measure */ export enum Hello { World = "world", Fizz = "fizz", Buzz = "buzz", } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let entry = &entries[0]; let expected_json = json!({ "kind": "enum", "name": "Hello", "location": { "filename": "test.ts", "line": 5, "col": 0 }, "jsDoc": "Some enum for good measure", "enumDef": { "members": [ { "name": "World" }, { "name": "Fizz" }, { "name": "Buzz" } ] } }); let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!(colors::strip_ansi_codes( super::printer::format(entries.clone()).as_str() ) .contains("Some enum for good measure")); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("enum Hello") ); } #[tokio::test] async fn export_namespace() { let source_code = r#" /** Namespace JSdoc */ export namespace RootNs { export const a = "a"; /** Nested namespace JSDoc */ export namespace NestedNs { export enum Foo { a = 1, b = 2, c = 3, } } } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let entry = &entries[0]; let expected_json = json!({ "kind": "namespace", "name": "RootNs", "location": { "filename": "test.ts", "line": 3, "col": 0 }, "jsDoc": "Namespace JSdoc", "namespaceDef": { "elements": [ { "kind": "variable", "name": "a", "location": { "filename": "test.ts", "line": 4, "col": 4 }, "jsDoc": null, "variableDef": { "tsType": null, "kind": "const" } }, { "kind": "namespace", "name": "NestedNs", "location": { "filename": "test.ts", "line": 7, "col": 4 }, "jsDoc": "Nested namespace JSDoc", "namespaceDef": { "elements": [ { "kind": "enum", "name": "Foo", "location": { "filename": "test.ts", "line": 8, "col": 6 }, "jsDoc": null, "enumDef": { "members": [ { "name": "a" }, { "name": "b" }, { "name": "c" } ] } } ] } } ] } }); let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("namespace RootNs") ); } #[tokio::test] async fn declare_namespace() { let source_code = r#" /** Namespace JSdoc */ declare namespace RootNs { declare const a = "a"; /** Nested namespace JSDoc */ declare namespace NestedNs { declare enum Foo { a = 1, b = 2, c = 3, } } } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let entry = &entries[0]; let expected_json = json!({ "kind": "namespace", "name": "RootNs", "location": { "filename": "test.ts", "line": 3, "col": 0 }, "jsDoc": "Namespace JSdoc", "namespaceDef": { "elements": [ { "kind": "variable", "name": "a", "location": { "filename": "test.ts", "line": 4, "col": 12 }, "jsDoc": null, "variableDef": { "tsType": null, "kind": "const" } }, { "kind": "namespace", "name": "NestedNs", "location": { "filename": "test.ts", "line": 7, "col": 4 }, "jsDoc": "Nested namespace JSDoc", "namespaceDef": { "elements": [ { "kind": "enum", "name": "Foo", "location": { "filename": "test.ts", "line": 8, "col": 6 }, "jsDoc": null, "enumDef": { "members": [ { "name": "a" }, { "name": "b" }, { "name": "c" } ] } } ] } } ] } }); let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("namespace RootNs") ); } #[tokio::test] async fn export_default_fn() { let source_code = r#" export default function foo(a: number) { return a; } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let entry = &entries[0]; let expected_json = json!({ "kind": "function", "name": "default", "location": { "filename": "test.ts", "line": 2, "col": 15 }, "jsDoc": null, "functionDef": { "params": [ { "name": "a", "kind": "identifier", "optional": false, "tsType": { "keyword": "number", "kind": "keyword", "repr": "number", }, } ], "typeParams": [], "returnType": null, "isAsync": false, "isGenerator": false } }); let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("function default(a: number)") ); } #[tokio::test] async fn export_default_class() { let source_code = r#" /** Class doc */ export default class Foobar { /** Constructor js doc */ constructor(name: string, private private2: number, protected protected2: number) {} } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let expected_json = json!({ "kind": "class", "name": "default", "location": { "filename": "test.ts", "line": 3, "col": 0 }, "jsDoc": "Class doc", "classDef": { "isAbstract": false, "extends": null, "implements": [], "typeParams": [], "constructors": [ { "jsDoc": "Constructor js doc", "accessibility": null, "name": "constructor", "params": [ { "name": "name", "kind": "identifier", "optional": false, "tsType": { "repr": "string", "kind": "keyword", "keyword": "string" } }, { "name": "private2", "kind": "identifier", "optional": false, "tsType": { "repr": "number", "kind": "keyword", "keyword": "number" } }, { "name": "protected2", "kind": "identifier", "optional": false, "tsType": { "repr": "number", "kind": "keyword", "keyword": "number" } } ], "location": { "filename": "test.ts", "line": 5, "col": 4 } } ], "properties": [], "methods": [] } }); let entry = &entries[0]; let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("class default") ); } #[tokio::test] async fn export_default_interface() { let source_code = r#" /** * Interface js doc */ export default interface Reader { /** Read n bytes */ read?(buf: Uint8Array, something: unknown): Promise } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let entry = &entries[0]; let expected_json = json!({ "kind": "interface", "name": "default", "location": { "filename": "test.ts", "line": 5, "col": 0 }, "jsDoc": "Interface js doc", "interfaceDef": { "extends": [], "methods": [ { "name": "read", "location": { "filename": "test.ts", "line": 7, "col": 4 }, "optional": true, "jsDoc": "Read n bytes", "params": [ { "name": "buf", "kind": "identifier", "optional": false, "tsType": { "repr": "Uint8Array", "kind": "typeRef", "typeRef": { "typeParams": null, "typeName": "Uint8Array" } } }, { "name": "something", "kind": "identifier", "optional": false, "tsType": { "repr": "unknown", "kind": "keyword", "keyword": "unknown" } } ], "typeParams": [], "returnType": { "repr": "Promise", "kind": "typeRef", "typeRef": { "typeParams": [ { "repr": "number", "kind": "keyword", "keyword": "number" } ], "typeName": "Promise" } } } ], "properties": [], "callSignatures": [], "typeParams": [], } }); let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("interface default") ); } #[tokio::test] async fn optional_return_type() { let source_code = r#" export function foo(a: number) { return a; } "#; let loader = TestLoader::new(vec![("test.ts".to_string(), source_code.to_string())]); let entries = DocParser::new(loader).parse("test.ts").await.unwrap(); assert_eq!(entries.len(), 1); let entry = &entries[0]; let expected_json = json!({ "kind": "function", "name": "foo", "location": { "filename": "test.ts", "line": 2, "col": 2 }, "jsDoc": null, "functionDef": { "params": [ { "name": "a", "kind": "identifier", "optional": false, "tsType": { "keyword": "number", "kind": "keyword", "repr": "number", }, } ], "typeParams": [], "returnType": null, "isAsync": false, "isGenerator": false } }); let actual = serde_json::to_value(entry).unwrap(); assert_eq!(actual, expected_json); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("function foo(a: number)") ); } #[tokio::test] async fn reexports() { let nested_reexport_source_code = r#" /** * JSDoc for bar */ export const bar = "bar"; export default 42; "#; let reexport_source_code = r#" import { bar } from "./nested_reexport.ts"; /** * JSDoc for const */ export const foo = "foo"; "#; let test_source_code = r#" export { default, foo as fooConst } from "./reexport.ts"; /** JSDoc for function */ export function fooFn(a: number) { return a; } "#; let loader = TestLoader::new(vec![ ("file:///test.ts".to_string(), test_source_code.to_string()), ( "file:///reexport.ts".to_string(), reexport_source_code.to_string(), ), ( "file:///nested_reexport.ts".to_string(), nested_reexport_source_code.to_string(), ), ]); let entries = DocParser::new(loader) .parse_with_reexports("file:///test.ts") .await .unwrap(); assert_eq!(entries.len(), 2); let expected_json = json!([ { "kind": "variable", "name": "fooConst", "location": { "filename": "file:///reexport.ts", "line": 7, "col": 0 }, "jsDoc": "JSDoc for const", "variableDef": { "tsType": null, "kind": "const" } }, { "kind": "function", "name": "fooFn", "location": { "filename": "file:///test.ts", "line": 5, "col": 0 }, "jsDoc": "JSDoc for function", "functionDef": { "params": [ { "name": "a", "kind": "identifier", "optional": false, "tsType": { "keyword": "number", "kind": "keyword", "repr": "number", }, } ], "typeParams": [], "returnType": null, "isAsync": false, "isGenerator": false } } ]); let actual = serde_json::to_value(entries.clone()).unwrap(); assert_eq!(actual, expected_json); assert!( colors::strip_ansi_codes(super::printer::format(entries).as_str()) .contains("function fooFn(a: number)") ); }