本文共 16052 字,大约阅读时间需要 53 分钟。
TypedocConverter:一个将 TypeScript 类型声明转换为 C# 的代码生成器
在开发过程中,我们经常会遇到需要将 TypeScript 库转换为 C# 的情况。特别是当库是 UI 组件时,我们还需要将其封装到 WebView 中,并通过 JavaScript 与 C# 调用。然而,这一过程中的类型翻译是一个艰巨的任务,尤其是当 TypeScript 类型声明庞大且频繁变化时。因此,我决定开发一个自动化代码生成器 TypedocConverter。
我原本打算从 TypeScript 的词法和语义分析开始,但发现有一个项目已经完成了这一步骤,并且支持输出 JSON schema。因此,剩下的任务相对简单:只需将 TypeScript 的 AST 转换成 C# 的 AST,然后再还原成代码。
借助于 F# 的强大类型系统,类型的声明和使用非常简单,并且具有完善的递归模式。pattern matching、可选项类型等支持,这也是选择 F# 而不是 C# 的原因,虽然 C# 也支持这些功能,但其以 OOP 为主,编写代码会有很多繁琐的样板代码。
我将 TypeScript 的类型绑定定义到 Definition.fs 中,这一步直接将 Typedoc 的定义翻译到 F# 即可:
type ReflectionKind = | Global = 0 | ExternalModule = 1 | Module = 2 | Enum = 4 | EnumMember = 16 | Variable = 32 | Function = 64 | Class = 128 | Interface = 256 | Constructor = 512 | Property = 1024 | Method = 2048 | CallSignature = 4096 | IndexSignature = 8192 | ConstructorSignature = 16384 | Parameter = 32768 | TypeLiteral = 65536 | TypeParameter = 131072 | Accessor = 262144 | GetSignature = 524288 | SetSignature = 1048576 | ObjectLiteral = 2097152 | TypeAlias = 4194304 | Event = 8388608 | Reference = 16777216
type ReflectionFlags = { IsPrivate: bool option; IsProtected: bool option; IsPublic: bool option; IsStatic: bool option; IsExported: bool option; IsExternal: bool option; IsOptional: bool option; IsReset: bool option; HasExportAssignment: bool option; IsConstructorProperty: bool option; IsAbstract: bool option; IsConst: bool option; IsLet: bool option } 我选择将所有类型的 Reflection 合并成一个记录,而不是采用 Union Types,因为后者在 parse AST 时需要大量的 pattern matching 代码。
System.Text.Json 和 Newtonsoft.Json 都不支持 F# 的可选项类型,因此需要自定义一个 JsonConverter 来处理:
type OptionConverter() = inherit JsonConverter()override __.CanConvert(objectType: Type) : bool = match objectType.IsGenericType with | false -> false | true -> typedefof<_ option> = objectType.GetGenericTypeDefinition()override __.WriteJson(writer: JsonWriter, value: obj, serializer: JsonSerializer) : unit = serializer.Serialize(writer, if isNull value then null else let _, fields = FSharpValue.GetUnionFields(value, value.GetType()) then fields.[0])override __.ReadJson(reader: JsonReader, objectType: Type, _existingValue: obj, serializer: JsonSerializer) : obj = let innerType = objectType.GetGenericArguments().[0] let value = serializer.Deserialize(reader, if innerType.IsValueType then (typedefof<_ Nullable>).MakeGenericType([|innerType|]) else innerType) let cases = FSharpType.GetUnionCases objectType if isNull value then FSharpValue.MakeUnion(cases.[0], [||]) else FSharpValue.MakeUnion(cases.[1], [|value|])
我们将实体结构定义到 Entity.fs 中,支持接口、类、枚举等:
type EntityBodyType = { Type: string; Name: string option; InnerTypes: EntityBodyType list }type EntityMethod = { Comment: string; Modifier: string list; Type: EntityBodyType; Name: string; TypeParameter: string list; Parameter: EntityBodyType list }type EntityProperty = { Comment: string; Modifier: string list; Name: string; Type: EntityBodyType; WithGet: bool; WithSet: bool; IsOptional: bool; InitialValue: string option }type EntityEvent = { Comment: string; Modifier: string list; DelegateType: EntityBodyType; Name: string; IsOptional: bool }type EntityEnum = { Comment: string; Name: string; Value: int64 option }type EntityType = | Interface | Class | Enum | StringEnumtype Entity = { Namespace: string; Name: string; Comment: string; Methods: EntityMethod list; Properties: EntityProperty list; Events: EntityEvent list; Enums: EntityEnum list; InheritedFrom: EntityBodyType list; TypeParameter: string list; Modifier: string list } 文档化注释是开发过程中需要的重要部分,可以极大方便开发者后续使用生成的类型绑定:
let escapeSymbols (text: string) = if isNull text then "" else text.Replace("&", "&").Replace("<", "<").Replace(">", ">")let toCommentText (text: string) = if isNull text then "" else text.Split "\n" |> Array.map (fun t -> "/// " + escapeSymbols t) |> Array.reduce (fun accu next -> accu + "\n" + next)let getXmlDocComment (comment: Comment) = let prefix = "/// \n" let suffix = "\n/// " let summary = match comment.Text with | Some text -> prefix + toCommentText comment.ShortText + toCommentText text + suffix | _ -> match comment.ShortText with | "" -> "" | _ -> prefix + toCommentText comment.ShortText + suffix let returns = match comment.Returns with | Some text -> "\n/// \n" + toCommentText text + "\n/// " | _ -> "" summary + returns TypeScript 的类型系统非常灵活,包括联合类型和交叉类型等,这些在 C# 8 中无法直接表达,需要等到 C# 9 才行。目前我们只能生成第一个类型代替联合类型,交叉类型暂时不支持:
type Type = { Type: string; Id: int option; Name: string option; ElementType: Type option; Value: string option; Types: Type list option; TypeArguments: Type list option; Constraint: Type option; Declaration: Reflection option } 枚举的生成相对简单,只需将 AST 中的枚举部分转换:
let parseEnum (section: string) (node: Reflection): Entity = let values = match node.Children with | Some children -> children | None -> [] { Type = EntityType.Enum; Namespace = if section = "" then "TypedocConverter" else section; Modifier = getModifier node.Flags; Name = node.Name; Comment = match node.Comment with | Some comment -> getXmlDocComment comment | _ -> ""; Methods = []; Properties = []; Events = []; InheritedFrom = []; Enums = values |> List.map (fun x -> let comment = match x.Comment with | Some comment -> getXmlDocComment comment | _ -> ""; let mutable intValue = 0L; match x.DefaultValue with | Some value -> if Int64.TryParse(value, intValue) then { Comment = comment; Name = toPascalCase x.Name; Value = Some intValue } else match getEnumReferencedValue values value x.Name with | Some t -> { Comment = comment; Name = x.Name; Value = Some (int64 t) } | _ -> { Comment = comment; Name = x.Name; Value = None } ); TypeParameter = [] } 处理接口和类的生成是重头戏,函数签名如下:
let parseInterfaceAndClass (section: string) (node: Reflection) (isInterface: bool): Entity = let comment = match node.Comment with | Some comment -> getXmlDocComment comment | _ -> ""; exts = match node.ExtendedTypes with | Some types -> types |> List.map getType | _ -> []; genericType = match node.TypeParameter with | Some tp -> Some (getGenericTypeParameters tp) | _ -> None; properties = match node.Children with | Some children -> if isInterface then children |> List.where (fun x -> x.Kind = ReflectionKind.Property) |> List.where (fun x -> x.InheritedFrom = None) |> List.where (fun x -> x.Overwrites = None) | _ -> children |> List.where (fun x -> x.Kind = ReflectionKind.Property); events = match node.Children with | Some children -> if isInterface then children |> List.where (fun x -> x.Kind = ReflectionKind.Event) |> List.where (fun x -> x.InheritedFrom = None) |> List.where (fun x -> x.Overwrites = None) | _ -> children |> List.where (fun x -> x.Kind = ReflectionKind.Event); methods = match node.Children with | Some children -> if isInterface then children |> List.where (fun x -> x.Kind = ReflectionKind.Method) |> List.where (fun x -> x.InheritedFrom = None) |> List.where (fun x -> x.Overwrites = None) | _ -> children |> List.where (fun x -> x.Kind = ReflectionKind.Method); methods = methods |> List.map (fun x -> let retType = match x.Signatures with | Some signatures -> signatures |> List.where (fun x -> x.Kind = ReflectionKind.CallSignature) | _ -> [] let typeParameter = match x.Signatures with | Some (sigs::_) -> match sigs.TypeParameter with | Some tp -> Some (getGenericTypeParameters tp) | _ -> None | _ -> [] let parameters = getMethodParameters (match x.Signatures with | Some signatures -> signatures |> List.where (fun x -> x.Kind = ReflectionKind.CallSignature) |> List.map (fun x -> match x.Parameters with | Some parameters -> parameters |> List.where (fun p -> p.Kind = ReflectionKind.Parameter) | _ -> []) | _ -> []) let parameter = parameters |> List.reduce (fun accu next -> accu @ next) { Comment = match x.Comment with | Some comment -> getXmlDocComment comment | _ -> ""; Modifier = if isInterface then [] else getModifier x.Flags; Type = retType; Name = x.Name; TypeParameter = typeParameter; Parameter = parameter }) 处理纯字符串的类型别名:
let parseTypeAlias (section: string) (node: Reflection): Entity list = let typeInfo = node.Type match typeInfo with | Some aliasType -> match aliasType.Type with | "union" -> match aliasType.Types with | Some types -> parseUnionTypeAlias section node types | _ -> [] | _ -> printWarning ("Type alias " + node.Name + " is not supported.") [] | _ -> [] 最后,将所有解析器组合在一起:
let rec parseNode (section: string) (node: Reflection): Entity list = match node.Kind with | ReflectionKind.Global -> match node.Children with | Some children -> parseNodes section children | _ -> []; | ReflectionKind.Module -> match node.Children with | Some children -> parseNodes (if section = "" then node.Name else section + "." + node.Name) children | _ -> []; | ReflectionKind.ExternalModule -> match node.Children with | Some children -> parseNodes section children | _ -> []; | ReflectionKind.Enum -> [parseEnum section node]; | ReflectionKind.Interface -> [parseInterfaceAndClass section node true]; | ReflectionKind.Class -> [parseInterfaceAndClass section node false]; | ReflectionKind.TypeAlias -> match node.Type with | Some _ -> parseTypeAlias section node | _ -> []; | _ -> []
生成的C#代码需要包括Newtonsoft.Json的JsonConverter来支持联合类型:
using System;using System.Collections.Generic;using System.Linq;using Newtonsoft.Json;namespace TypedocConverter.Test{ public class MyClass1 : MyInterface1 { public string TestProp { get; set; } event System.Action OnTest; public string TestMethod(string arg, System.Action callback) { throw new NotImplementedException(); } static UnionStr StaticMethod(string value, bool isOption) { throw new NotImplementedException(); } }} 输入的TypeScript代码:
declare namespace test { /** * The declaration of an enum */ export enum MyEnum { A = 0, B = 1, C = 2, D = C } /** * The declaration of an interface */ export interface MyInterface1 { /** * A method */ testMethod(arg: string, callback: () => void): string; /** * An event */ onTest(listener: (e: MyInterface1) => void): void; /** * An property */ readonly testProp: string; } /** * Another declaration of an interface */ export interface MyInterface2 { /** * A method */ testMethod(arg: T, callback: () => void): T; /** * An event */ onTest(listener: (e: MyInterface2 ) => void): void; /** * An property */ readonly testProp: T; } /** * The declaration of a class */ export class MyClass1 implements MyInterface1 { /** * A method */ testMethod(arg: string, callback: () => void): string; /** * An event */ onTest(listener: (e: MyInterface1) => void): void; /** * An property */ readonly testProp: string; static staticMethod(value: string, isOption?: boolean): UnionStr; } /** * Another declaration of a class */ export class MyClass2 implements MyInterface2 { /** * A method */ testMethod(arg: T, callback: () => void): T; /** * An event */ onTest(listener: (e: MyInterface2 ) => void): void; /** * An property */ readonly testProp: T; static staticMethod(value: string, isOption?: boolean): UnionStr; } /** * The declaration of a type alias */ export type UnionStr = "A" | "B" | "C" | "other";} 输出的C#代码:
using System;using System.Collections.Generic;using System.Linq;using Newtonsoft.Json;namespace TypedocConverter.Test{ namespace test { /// /// The declaration of an enum /// enum MyEnum { [Newtonsoft.Json.JsonProperty("A", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] A = 0, [Newtonsoft.Json.JsonProperty("B", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] B = 1, [Newtonsoft.Json.JsonProperty("C", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] C = 2, [Newtonsoft.Json.JsonProperty("D", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] D = 2 } /// /// The declaration of a class /// class MyClass1 : MyInterface1 { /// /// An property /// [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string TestProp { get; set; } public event System.Action OnTest; public string TestMethod(string arg, System.Action callback) { throw new NotImplementedException(); } static UnionStr StaticMethod(string value, bool isOption) { throw new NotImplementedException(); } } /// /// Another declaration of a class /// class MyClass2 : MyInterface2 { /// /// An property /// [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public T TestProp { get; set; } public event System.Action > OnTest; public T TestMethod(T arg, System.Action callback) { throw new NotImplementedException(); } static UnionStr StaticMethod(string value, bool isOption) { throw new NotImplementedException(); } } /// /// The declaration of an interface /// public interface MyInterface1 { [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] string TestProp { get; set; } event System.Action OnTest; string TestMethod(string arg, System.Action callback); } /// /// Another declaration of an interface /// public interface MyInterface2 { [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] T TestProp { get; set; } event System.Action > OnTest; T TestMethod(T arg, System.Action callback); } /// /// The declaration of a type alias /// [Newtonsoft.Json.JsonConverter(typeof(UnionStrConverter))] enum UnionStr { [Newtonsoft.Json.JsonProperty("A", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] A, [Newtonsoft.Json.JsonProperty("B", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] B, [Newtonsoft.Json.JsonProperty("C", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] C, [Newtonsoft.Json.JsonProperty("Other", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] Other } public class UnionStrConverter : Newtonsoft.Json.JsonConverter { public override bool CanConvert(System.Type t) => t == typeof(UnionStr) || t == typeof(UnionStr?); public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type t, object? existingValue, Newtonsoft.Json.JsonSerializer serializer) => reader.TokenType switch { Newtonsoft.Json.JsonToken.String => serializer.Deserialize (reader) switch { "A" => UnionStr.A, "B" => UnionStr.B, "C" => UnionStr.C, "Other" => UnionStr.Other, _ => throw new System.Exception("Cannot unmarshal type UnionStr") }, _ => throw new System.Exception("Cannot unmarshal type UnionStr") }; public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object? untypedValue, Newtonsoft.Json.JsonSerializer serializer) { if (untypedValue is null) { serializer.Serialize(writer, null); return; } var value = (UnionStr)untypedValue; switch (value) { case UnionStr.A: serializer.Serialize(writer, "A"); return; case UnionStr.B: serializer.Serialize(writer, "B"); return; case UnionStr.C: serializer.Serialize(writer, "C"); return; case UnionStr.Other: serializer.Serialize(writer, "Other"); return; default: break; } throw new System.Exception("Cannot marshal type UnionStr"); } } } } 有了这个工具后,妈妈再也不用担心我封装 TypeScript 的库了。有了 TypedocConverter,任何 TypeScript 的库都能轻而易举地转换成 C# 的类型绑定,然后进行封装,非常方便。
转载地址:http://enouz.baihongyu.com/