博客
关于我
用 F# 手写 TypeScript 转 C# 类型绑定生成器
阅读量:428 次
发布时间:2019-03-06

本文共 16052 字,大约阅读时间需要 53 分钟。

TypedocConverter:一个将 TypeScript 类型声明转换为 C# 的代码生成器

在开发过程中,我们经常会遇到需要将 TypeScript 库转换为 C# 的情况。特别是当库是 UI 组件时,我们还需要将其封装到 WebView 中,并通过 JavaScript 与 C# 调用。然而,这一过程中的类型翻译是一个艰巨的任务,尤其是当 TypeScript 类型声明庞大且频繁变化时。因此,我决定开发一个自动化代码生成器 TypedocConverter。

构思

我原本打算从 TypeScript 的词法和语义分析开始,但发现有一个项目已经完成了这一步骤,并且支持输出 JSON schema。因此,剩下的任务相对简单:只需将 TypeScript 的 AST 转换成 C# 的 AST,然后再还原成代码。

构建 TypeScript AST 类型绑定

借助于 F# 的强大类型系统,类型的声明和使用非常简单,并且具有完善的递归模式。pattern matching、可选项类型等支持,这也是选择 F# 而不是 C# 的原因,虽然 C# 也支持这些功能,但其以 OOP 为主,编写代码会有很多繁琐的样板代码。

我将 TypeScript 的类型绑定定义到 Definition.fs 中,这一步直接将 Typedoc 的定义翻译到 F# 即可:

ReflectionKind 枚举

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

ReflectionFlags 记录

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|])

构建 C# AST 类型

我们将实体结构定义到 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 | StringEnum
type 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/

你可能感兴趣的文章
Node.js卸载超详细步骤(附图文讲解)
查看>>
Node.js卸载超详细步骤(附图文讲解)
查看>>
Node.js基于Express框架搭建一个简单的注册登录Web功能
查看>>
node.js学习之npm 入门 —8.《怎样创建,发布,升级你的npm,node模块》
查看>>
Node.js安装与配置指南:轻松启航您的JavaScript服务器之旅
查看>>
Node.js安装及环境配置之Windows篇
查看>>
Node.js安装和入门 - 2行代码让你能够启动一个Server
查看>>
node.js安装方法
查看>>
Node.js官网无法正常访问时安装NodeJS的方法
查看>>
node.js模块、包
查看>>
node.js的express框架用法(一)
查看>>
Node.js的交互式解释器(REPL)
查看>>
Node.js的循环与异步问题
查看>>
Node.js高级编程:用Javascript构建可伸缩应用(1)1.1 介绍和安装-安装Node
查看>>
nodejs + socket.io 同时使用http 和 https
查看>>
NodeJS @kubernetes/client-node连接到kubernetes集群的方法
查看>>
NodeJS API简介
查看>>
Nodejs express 获取url参数,post参数的三种方式
查看>>
nodejs http小爬虫
查看>>
nodejs libararies
查看>>