11

用 F# 手写 TypeScript 转 C# 类型绑定生成器

 4 years ago
source link: http://www.cnblogs.com/hez2010/p/12246841.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

我们经常会遇到这样的事情:有时候我们找到了一个库,但是这个库是用 TypeScript 写的,但是我们想在 C# 调用,于是我们需要设法将原来的 TypeScript 类型声明翻译成 C# 的代码,然后如果是 UI 组件的话,我们需要将其封装到一个 WebView 里面,然后通过 JavaScript 和 C# 的互操作功能来调用该组件的各种方法,支持该组件的各种事件等等。

但是这是一个苦力活,尤其是类型翻译这一步。

这个是我最近在帮助维护一个开源 UWP 项目 monaco-editor-uwp 所需要的,该项目将微软的 monaco 编辑器封装成了 UWP 组件。

然而它的 monaco.d.ts 足足有 1.5 mb,并且 API 经常会变化,如果人工翻译,不仅工作量十分大,还可能会漏掉新的变化,但是如果有一个自动生成器的话,那么人工的工作就会少很多。

目前 GitHub 上面有一个叫做 QuickType 的项目,但是这个项目对 TypeScript 的支持极其有限,仍然停留在 TypeScript 3.2,而且遇到不认识的类型就会报错,比如 DOM 类型等等。

因此我决定手写一个代码生成器 TypedocConverter:https://github.com/hez2010/TypedocConverter

构思

本来是打算从 TypeScript 词法和语义分析开始做的,但是发现有一个叫做 Typedoc 的项目已经帮我们完成了这一步,而且支持输出 JSON schema,那么剩下的事情就简单了:我们只需要将 TypeScript 的 AST 转换成 C# 的 AST,然后再将 AST 还原成代码即可。

那么话不多说,这就开写。

构建 Typescipt AST 类型绑定

借助于 F# 更加强大的类型系统,类型的声明和使用非常简单,并且具有完善的recursive pattern。pattern matching、option types 等支持,这也是该项目选用 F# 而不是 C# 的原因,虽然 C# 也支持这些,也有一定的 FP 能力,但是它还是偏 OOP,写起来会有很多的样板代码,非常的繁琐。

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

首先是 ReflectionKind 枚举,该枚举表示了 JSON Schema 中各节点的类型:

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,注意该 record 所有的成员都是 option 的

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,由于每一种类型的 Reflection 都可以由 ReflectionKind 来区分,因此我选择将所有类型的 Reflection 合并成为一个 record,而不是采用 Union Types,因为后者虽然看上去清晰,但是在实际 parse AST 的时候会需要大量 pattern matching 的代码。

由于部分 records 相互引用,因此我们使用 and 来定义 recursive records。

type Reflection = {
    Id: int
    Name: string
    OriginalName: string
    Kind: ReflectionKind
    KindString: string option
    Flags: ReflectionFlags
    Parent: Reflection option
    Comment: Comment option
    Sources: SourceReference list option
    Decorators: Decorator option
    Decorates: Type list option
    Url: string option
    Anchor: string option
    HasOwnDocument: bool option
    CssClasses: string option
    DefaultValue: string option
    Type: Type option
    TypeParameter: Reflection list option
    Signatures: Reflection list option
    IndexSignature: Reflection list option
    GetSignature: Reflection list option
    SetSignature: Reflection list option
    Overwrites: Type option
    InheritedFrom: Type option
    ImplementationOf: Type option
    ExtendedTypes: Type list option
    ExtendedBy: Type list option
    ImplementedTypes: Type list option
    ImplementedBy: Type list option
    TypeHierarchy: DeclarationHierarchy option
    Children: Reflection list option
    Groups: ReflectionGroup list option
    Categories: ReflectionCategory list option
    Reflections: Map<int, Reflection> option
    Directory: SourceDirectory option
    Files: SourceFile list option
    Readme: string option
    PackageInfo: obj option
    Parameters: Reflection list option
}
and DeclarationHierarchy = {
    Type: Type list
    Next: DeclarationHierarchy option
    IsTarget: bool option
}
and 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
}
and Decorator = {
    Name: string
    Type: Type option
    Arguments: obj option
}
and ReflectionGroup = {
    Title: string
    Kind: ReflectionKind
    Children: int list
    CssClasses: string option
    AllChildrenHaveOwnDocument: bool option
    AllChildrenAreInherited: bool option
    AllChildrenArePrivate: bool option
    AllChildrenAreProtectedOrPrivate: bool option
    AllChildrenAreExternal: bool option
    SomeChildrenAreExported: bool option
    Categories: ReflectionCategory list option
}
and ReflectionCategory = {
    Title: string
    Children: int list
    AllChildrenHaveOwnDocument: bool option
}
and SourceDirectory = {
    Parent: SourceDirectory option
    Directories: Map<string, SourceDirectory>
    Groups: ReflectionGroup list option
    Files: SourceFile list
    Name: string option
    DirName: string option
    Url: string option
}
and SourceFile = {
    FullFileName: string
    FileName: string
    Name: string
    Url: string option
    Parent: SourceDirectory option
    Reflections: Reflection list option
    Groups: ReflectionGroup list option
}
and SourceReference = {
    File: SourceFile option
    FileName: string
    Line: int
    Character: int
    Url: string option
}
and Comment = {
    ShortText: string
    Text: string option
    Returns: string option
    Tags: CommentTag list option
}
and CommentTag = {
    TagName: string
    ParentName: string
    Text: string
}

这样,我们就简单的完成了类型绑定的翻译,接下来要做的就是将 Typedoc 生成的 JSON 反序列化成我们所需要的东西即可。

反序列化

虽然想着好像一切都很顺利,但是实际上 System.Text.Json、Newtonsoft.JSON 等均不支持 F# 的 option types,所需我们还需要一个 JsonConverter 处理 option types。

本项目采用 Newtonsoft.Json,因为 System.Text.Json 目前尚不成熟。得益于 F# 对 OOP 的兼容,我们可以很容易的实现一个 OptionConverter

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

这样所有的工作就完成了。

我们可以去 monaco-editor 仓库下载 monaco.d.ts 测试一下我们的 JSON Schema deserializer,可以发现 JSON Sechma 都被正确地反序列化了。

nIbQVb3.jpg!web

反序列化结果

构建 C# AST 类型

当然,此 "AST" 非彼 AST,我们没有必要其细化到语句层面,因为我们只是要写一个简单的代码生成器,我们只需要构建实体结构即可。

我们将实体结构定义到 Entity.fs 中,在此我们只需支持 interface、class、enum 即可,对于 class 和 interface,我们只需要支持 method、property 和 event 就足够了。

当然,代码中存在泛型的可能,这一点我们也需要考虑。

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
    Type: EntityType
    TypeParameter: string list
    Modifier: string list
}

文档化注释生成器

文档化注释也是少不了的东西,能极大方便开发者后续使用生成的类型绑定,而无需参照原 typescript 类型声明上的注释。

代码很简单,只需要将文本处理成 xml 即可。

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 = "/// <summary>\n"
    let suffix = "\n/// </summary>"
    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/// <returns>\n" + toCommentText text + "\n/// </returns>"
        | _ -> ""
    summary + returns

类型生成器

Typescript 的类型系统较为灵活,包括 union types、intersect types 等等,这些即使是目前的 C# 8 都不能直接表达,需要等到 C# 9 才行。当然我们可以生成一个 struct 并为其编写隐式转换操作符重载,支持 union types,但是目前尚未实现,我们就先用 union types 中的第一个类型代替,而对于 intersect types,我们姑且先使用 object。

然而 union types 有一个特殊情况:string literals types alias。就是这样的东西:

type Size = "XS" | "S" | "M" | "L" | "XL";

即纯 string 值组合的 type alias,这个我们还是有必要支持的,因为在 typescript 中用的非常广泛。

C# 在没有对应语法的时候要怎么支持呢?很简单,我们创建一个 enum,该 enum 包含该类型中的所有元素,然后我们为其编写 JsonConverter,这样就能确保序列化后,typescript 方能正确识别类型,而在 C# 又有 type sound 的编码体验。

另外,我们需要提供一些常用的类型转换:

  • Array<T>  ->  T[]  
  • Set<T>  ->  System.Collections.Generic.ISet<T>
  • Map<T>  ->  System.Collections.Generic.IDictionary<T>  
  • Promise<T>  ->  System.Threading.Tasks.Task<T>  
  • callbacks ->  System.Func<T...>System.Action<T...>  
  • Tuple 类型
  • 其他的数组类型如  Uint32Array  
  • 对于  <void> ,我们需要解除泛型,即  T<void>  ->  T

那么实现如下:

let rec getType (typeInfo: Type): EntityBodyType = 
    let genericType =
        match typeInfo.Type with
        | "intrinsic" -> 
            match typeInfo.Name with
            | Some name -> 
                match name with
                | "number" -> { Type = "double"; InnerTypes = []; Name = None }
                | "boolean" -> { Type = "bool"; InnerTypes = []; Name = None }
                | "string" -> { Type = "string"; InnerTypes = []; Name = None }
                | "void" -> { Type = "void"; InnerTypes = []; Name = None }
                | _ -> { Type = "object"; InnerTypes = []; Name = None }
            | _ -> { Type = "object"; InnerTypes = []; Name = None }
        | "reference" | "typeParameter" -> 
            match typeInfo.Name with
            | Some name -> 
                match name with
                | "Promise" -> { Type = "System.Threading.Tasks.Task"; InnerTypes = []; Name = None }
                | "Set" -> { Type = "System.Collections.Generic.ISet"; InnerTypes = []; Name = None }
                | "Map" -> { Type = "System.Collections.Generic.IDictionary"; InnerTypes = []; Name = None }
                | "Array" -> { Type = "System.Array"; InnerTypes = []; Name = None }
                | "BigUint64Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "ulong"; InnerTypes = [ ]; Name = None };]; Name = None };
                | "Uint32Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "uint"; InnerTypes = [ ]; Name = None };]; Name = None };
                | "Uint16Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "ushort"; InnerTypes = [ ]; Name = None };]; Name = None };
                | "Uint8Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "byte"; InnerTypes = [ ]; Name = None };]; Name = None };
                | "BigInt64Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "long"; InnerTypes = [ ]; Name = None };]; Name = None };
                | "Int32Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "int"; InnerTypes = [ ]; Name = None };]; Name = None };
                | "Int16Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "short"; InnerTypes = [ ]; Name = None };]; Name = None };
                | "Int8Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "char"; InnerTypes = [ ]; Name = None };]; Name = None };
                | "RegExp" -> { Type = "string"; InnerTypes = []; Name = None };
                | x -> { Type = x; InnerTypes = []; Name = None };
            | _ -> { Type = "object"; InnerTypes = []; Name = None }
        | "array" -> 
            match typeInfo.ElementType with
            | Some elementType -> { Type = "System.Array"; InnerTypes = [getType elementType]; Name = None }
            | _ -> { Type = "System.Array"; InnerTypes = [{ Type = "object"; InnerTypes = []; Name = None }]; Name = None }
        | "stringLiteral" -> { Type = "string"; InnerTypes = []; Name = None }
        | "tuple" ->
            match typeInfo.Types with
            | Some innerTypes -> 
                match innerTypes with
                | [] -> { Type = "object"; InnerTypes = []; Name = None }
                | _ -> { Type = "System.ValueTuple"; InnerTypes = innerTypes |> List.map getType; Name = None }
            | _ -> { Type = "object"; InnerTypes = []; Name = None }
        | "union" -> 
            match typeInfo.Types with
            | Some innerTypes -> 
                match innerTypes with
                | [] -> { Type = "object"; InnerTypes = []; Name = None }
                | _ -> 
                    printWarning ("Taking only the first type " + innerTypes.[0].Type + " for the entire union type.")
                    getType innerTypes.[0] // TODO: generate unions
| _ ->{ Type = "object"; InnerTypes = []; Name = None }
        | "intersection" -> { Type = "object"; InnerTypes = []; Name = None } // TODO: generate intersections
| "reflection" -> 
            match typeInfo.Declaration with
            | Some dec -> 
                match dec.Signatures with
                | Some [signature] -> 
                    let paras = 
                        match signature.Parameters with
                        | Some p -> 
                            p 
                            |> List.map
                                (fun pi -> 
                                    match pi.Type with 
                                    | Some pt -> Some (getType pt)
                                    | _ -> None
                                )
                            |> List.collect
                                (fun x -> 
                                    match x with
                                    | Some s -> [s]
                                    | _ -> []
                                )
                        | _ -> []
                    let rec getDelegateParas (paras: EntityBodyType list): EntityBodyType list =
                        match paras with
                        | [x] -> [{ Type = x.Type; InnerTypes = x.InnerTypes; Name = None }]
                        | (front::tails) -> [front] @ getDelegateParas tails
                        | _ -> []
                    let returnsType = 
                        match signature.Type with
                        | Some t -> getType t
                        | _ -> { Type = "void"; InnerTypes = []; Name = None }
                    let typeParas = getDelegateParas paras
                    match typeParas with
                    | [] -> { Type = "System.Action"; InnerTypes = []; Name = None }
                    | _ -> 
                        if returnsType.Type = "void" 
                        then { Type = "System.Action"; InnerTypes = typeParas; Name = None } 
                        else { Type = "System.Func"; InnerTypes = typeParas @ [returnsType]; Name = None }
                | _ -> { Type = "object"; InnerTypes = []; Name = None }
            | _ -> { Type = "object"; InnerTypes = []; Name = None }
        | _ -> { Type = "object"; InnerTypes = []; Name = None }
    let mutable innerTypes = 
        match typeInfo.TypeArguments with
        | Some args -> getGenericTypeArguments args
        | _ -> []
    if genericType.Type = "System.Threading.Tasks.Task"
    then 
        match innerTypes with
        | (front::_) -> if front.Type = "void" then innerTypes <- [] else ()
        | _ -> ()
    else ()
    { 
        Type = genericType.Type; 
        Name = None; 
        InnerTypes = if innerTypes = [] then genericType.InnerTypes else innerTypes; 
    }
and getGenericTypeArguments (typeInfos: Type list): EntityBodyType list = 
    typeInfos |> List.map getType
and getGenericTypeParameters (nodes: Reflection list) = // TODO: generate constaints
let types = 
        nodes 
        |> List.where(fun x -> x.Kind = ReflectionKind.TypeParameter)
        |> List.map (fun x -> x.Name)
    types |> List.map (fun x -> {| Type = x; Constraint = "" |})

当然,目前尚不支持生成泛型约束,如果以后有时间的话会考虑添加。

修饰生成器

例如 publicprivateprotectedstatic 等等。这一步很简单,直接将 ReflectionFlags 转换一下即可,个人觉得使用 mutable 代码会让代码变得非常不优雅,但是有的时候还是需要用一下的,不然会极大地提高代码的复杂度。

let getModifier (flags: ReflectionFlags) = 
    let mutable modifier = []
    match flags.IsPublic with
    | Some flag -> if flag then modifier <- modifier |> List.append [ "public" ] else ()
    | _ -> ()
    match flags.IsAbstract with
    | Some flag -> if flag then modifier <- modifier |> List.append [ "abstract" ] else ()
    | _ -> ()
    match flags.IsPrivate with
    | Some flag -> if flag then modifier <- modifier |> List.append [ "private" ] else ()
    | _ -> ()
    match flags.IsProtected with
    | Some flag -> if flag then modifier <- modifier |> List.append [ "protected" ] else ()
    | _ -> ()
    match flags.IsStatic with
    | Some flag -> if flag then modifier <- modifier |> List.append [ "static" ] else ()
    | _ -> ()
    modifier

Enum 生成器

终于到 parse 实体的部分了,我们先从最简单的做起:枚举。 代码很简单,直接将原 AST 中的枚举部分转换一下即可。

let parseEnum (section: string) (node: Reflection): Entity =
    let values = match node.Children with
                 | Some children ->
                     children
                     |> List.where (fun x -> x.Kind = ReflectionKind.EnumMember)
                 | None -> []
    { 
        Type = EntityType.Enum;
        Namespace = if section = "" then "TypeDocGenerator" 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; }
            | _ -> { Comment = comment; Name = x.Name; Value = None; }
        );
        TypeParameter = []
    }

你会注意到一个上面我有一处标了个 ????? ,这是在干什么呢?

其实,TypeScript 的 enum 是 recursive 的,也就意味着定义的时候,一个元素可以引用另一个元素,比如这样:

enum MyEnum {
    A = 1,
    B = 2,
    C = A
}

这个时候,我们需要查找它引用的枚举值,比如在上面的例子里面,处理 C 的时候,需要将它的值 A 用真实值 1 代替。所以我们还需要一个查找函数:

let rec getEnumReferencedValue (nodes: Reflection list) value name = 
    match nodes 
          |> List.where(fun x -> 
              match x.DefaultValue with
              | Some v -> v <> value && not (name = x.Name)
              | _ -> true
          ) 
          |> List.where(fun x -> x.Name = value)
          |> List.tryFind(fun x -> 
                            let mutable intValue = 0
                            match x.DefaultValue with
                            | Some y -> Int32.TryParse(y, &intValue)
                            | _ -> true
           ) with
    | Some t -> t.DefaultValue
    | _ -> None

这样我们的 Enum parser 就完成了。

Interface 和 Class 生成器

下面到了重头戏,interface 和 class 才是类型绑定的关键。

我们的函数签名是这样的:

let parseInterfaceAndClass (section: string) (node: Reflection) (isInterface: bool): Entity = ...

首先我们从 Reflection 节点中查找并生成注释、修饰、名称、泛型参数、继承关系、方法、属性和事件:

let comment = 
    match node.Comment with
    | Some comment -> getXmlDocComment comment
    | _ -> ""
let exts = 
    (match node.ExtendedTypes with
    | Some types -> types |> List.map(fun x -> getType x)
    | _ -> []) @
    (match node.ImplementedTypes with
    | Some types -> types |> List.map(fun x -> getType x)
    | _ -> [])
let genericType =
    let types = 
          match node.TypeParameter with
          | Some tp -> Some (getGenericTypeParameters tp)
          | _ -> None
    match types with
    | Some result -> result
    | _ -> []
let 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) // exclude inhreited properties
            |> List.where(fun x -> x.Overwrites = None) // exclude overrites properties
        else children |> List.where(fun x -> x.Kind = ReflectionKind.Property)
    | _ -> []
let 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) // exclude inhreited events
            |> List.where(fun x -> x.Overwrites = None) // exclude overrites events
        else children |> List.where(fun x -> x.Kind = ReflectionKind.Event)
    | _ -> []
let 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) // exclude inhreited methods
            |> List.where(fun x -> x.Overwrites = None) // exclude overrites methods
        else children |> List.where(fun x -> x.Kind = ReflectionKind.Method)
    | _ -> []

有一点要注意,就是对于 interface 来说,子 interface 无需重复父 interface 的成员,因此需要排除。

然后我们直接返回一个 record,代表该节点的实体即可。

{
    Type = if isInterface then EntityType.Interface else EntityType.Class;
    Namespace = if section = "" then "TypedocConverter" else section;
    Name = node.Name;
    Comment = comment;
    Modifier = getModifier node.Flags;
    InheritedFrom = exts;
    Methods = 
        methods 
        |> List.map (
            fun x -> 
                let retType = 
                    match (
                            match x.Signatures with
                            | Some signatures -> 
                                signatures |> List.where(fun x -> x.Kind = ReflectionKind.CallSignature)
                            | _ -> []) 
                        with
                        | [] -> { Type = "object"; InnerTypes = []; Name = None }
                        | (front::_) ->
                            match front.Type with
                            | Some typeInfo -> getType typeInfo
                            | _ -> { Type = "object"; InnerTypes = []; Name = None }
                let typeParameter = 
                    match x.Signatures with
                    | Some (sigs::_) -> 
                        let types = 
                              match sigs.TypeParameter with
                              | Some tp -> Some (getGenericTypeParameters tp)
                              | _ -> None
                        match types with
                        | Some result -> result
                        | _ -> []
                    | _ -> []
                    |> List.map (fun x -> x.Type)
                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)
                                    | _ -> []
                                )
                            |> 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 = parameters
                }
        );
    Events = 
        events
        |> List.map (
            fun x -> 
                let paras = 
                    match x.Signatures with
                    | Some sigs -> 
                        sigs 
                        |> List.where (fun x -> x.Kind = ReflectionKind.Event)
                        |> List.map(fun x -> x.Parameters)
                        |> List.collect (fun x ->
                            match x with
                            | Some paras -> paras
                            | _ -> [])
                    | _ -> []
                { 
                    Name = x.Name; 
                    IsOptional = 
                        match x.Flags.IsOptional with
                        | Some optional -> optional
                        | _ -> false
                        ;
                    DelegateType = 
                        match paras with
                        | (front::_) -> 
                            match front.Type with
                            | Some typeInfo -> getType typeInfo
                            | _ -> { Type = "System.Delegate"; Name = None; InnerTypes = [] }
                        | _ -> 
                            match x.Type with
                            | Some typeInfo -> getType typeInfo
                            | _ -> { Type = "System.Delegate"; Name = None; InnerTypes = [] }
                        ;
                    Comment = 
                        match x.Comment with
                        | Some comment -> getXmlDocComment comment
                        | _ -> ""
                        ;
                    Modifier = if isInterface then [] else getModifier x.Flags;
                }
        );
    Properties = 
        properties 
        |> List.map (
            fun x -> 
                {
                    Comment = 
                        match x.Comment with
                        | Some comment -> getXmlDocComment comment
                        | _ -> ""
                    Modifier = if isInterface then [] else getModifier x.Flags;
                    Name = x.Name
                    Type = 
                        match x.Type with
                        | Some typeInfo -> getType typeInfo
                        | _ -> { Type = "object"; Name = None; InnerTypes = [] }
                    WithGet = true;
                    WithSet = true;
                    IsOptional =
                        match x.Flags.IsOptional with
                        | Some optional -> optional
                        | _ -> false
                        ;
                    InitialValue = 
                        match x.DefaultValue with
                        | Some value -> Some value
                        | _ -> None
                }
        );
    Enums = [];
    TypeParameter = genericType |> List.map(fun x -> x.Type);
}

注意处理 event 的时候,委托的类型需要特殊处理一下。

Type alias 生诚器

还记得我们最上面说的一种特殊的 union types 吗?这里就是处理纯 string 的 type alias 的。

let parseUnionTypeAlias (section: string) (node: Reflection) (nodes: Type list): Entity list =
    let notStringLiteral = nodes |> List.tryFind(fun x -> x.Type <> "stringLiteral")
    let enums = 
        match notStringLiteral with
        | Some _ -> 
            printWarning ("Type alias " + node.Name + " is not supported.")
            []
        | None ->
            nodes 
            |> List.collect
                (fun x ->
                    match x.Value with
                    | Some value -> 
                        [{
                            Name = toPascalCase value
                            Comment = "///<summary>\n" + toCommentText value + "\n///</summary>"
                            Value = None
                        }]
                    | _ -> []
                )
    if enums = [] then []
    else 
        [
            {
                Namespace = section
                Name = node.Name
                Comment = 
                    match node.Comment with
                    | Some comment -> getXmlDocComment comment
                    | _ -> ""
                Methods = []
                Events = []
                Properties = []
                Enums = enums
                InheritedFrom = []
                Type = EntityType.StringEnum
                TypeParameter = []
                Modifier = getModifier node.Flags
            }
        ]

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.")
                []
        | _ ->
            printWarning ("Type alias " + node.Name + " is not supported.")
            []
    | _ -> []

组合 Prasers

我们最后将以上 parsers 组合起来就 ojbk 了:

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
        | _ -> []
    | _ -> []

and parseNodes section (nodes: Reflection list): Entity list =
    match nodes with
    | ([ front ]) -> parseNode section front
    | (front :: tails) ->
        parseNode section front @ parseNodes section tails
    | _ -> []

至此,我们的 parse 工作全部搞定,完结撒花~~~

代码生成

有了 C# 的实体类型,代码生成还困难吗?

不过有一点要注意的是,我们需要将名称转换为 Pascal Case,还需要生成 string literals union types 的 JsonConverter。不过这些都是样板代码,非常简单。

这里就不放代码了,感兴趣的同学可以自行去我的 GitHub 仓库查看。

测试效果

原 typescipt 代码:

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
     * @event
     */
    onTest(listener: (e: MyInterface1) => void): void;
    /**
     * An property
     */
    readonly testProp: string;
  }

  /**
   * Another declaration of an interface
   */
  export interface MyInterface2<T> {
    /**
     * A method
     */
    testMethod(arg: T, callback: () => void): T;
    /**
     * An event
     * @event
     */
    onTest(listener: (e: MyInterface2<T>) => void): void;
    /**
     * An property
     */
    readonly testProp: T;
  }

  /**
   * The declaration of a class
   */
  export class MyClass1<T> implements MyInterface1 {
    /**
     * A method
     */
    testMethod(arg: string, callback: () => void): string;
    /**
     * An event
     * @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<T> implements MyInterface2<T> {
    /**
     * A method
     */
    testMethod(arg: T, callback: () => void): T;
    /**
     * An event
     * @event
     */
    onTest(listener: (e: MyInterface2<T>) => 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";
}

Typedoc 生成的 JSON 后,将其作为输入,生成 C# 代码:

namespace TypedocConverter.Test
{

    /// <summary>
    /// The declaration of an enum
    /// </summary>
    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
    }
}

namespace TypedocConverter.Test
{

    /// <summary>
    /// The declaration of a class
    /// </summary>
    class MyClass1<T> : MyInterface1
    {
        /// <summary>
        /// An property
        /// </summary>
        [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        string TestProp { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
        event System.Action<MyInterface1> OnTest;
        string TestMethod(string arg, System.Action callback) => throw new System.NotImplementedException();
        static UnionStr StaticMethod(string value, bool isOption) => throw new System.NotImplementedException();
    }
}

namespace TypedocConverter.Test
{

    /// <summary>
    /// Another declaration of a class
    /// </summary>
    class MyClass2<T> : MyInterface2<T>
    {
        /// <summary>
        /// An property
        /// </summary>
        [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        T TestProp { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
        event System.Action<MyInterface2<T>> OnTest;
        T TestMethod(T arg, System.Action callback) => throw new System.NotImplementedException();
        static UnionStr StaticMethod(string value, bool isOption) => throw new System.NotImplementedException();
    }
}

namespace TypedocConverter.Test
{

    /// <summary>
    /// The declaration of an interface
    /// </summary>
    interface MyInterface1
    {
        /// <summary>
        /// An property
        /// </summary>
        [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        string TestProp { get; set; }
        event System.Action<MyInterface1> OnTest;
        string TestMethod(string arg, System.Action callback);
    }
}

namespace TypedocConverter.Test
{

    /// <summary>
    /// Another declaration of an interface
    /// </summary>
    interface MyInterface2<T>
    {
        /// <summary>
        /// An property
        /// </summary>
        [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        T TestProp { get; set; }
        event System.Action<MyInterface2<T>> OnTest;
        T TestMethod(T arg, System.Action callback);
    }
}

namespace TypedocConverter.Test
{

    /// <summary>
    /// The declaration of a type alias
    /// </summary>
    [Newtonsoft.Json.JsonConverter(typeof(UnionStrConverter))]
    enum UnionStr
    {
        ///<summary>
        /// A
        ///</summary>
        [Newtonsoft.Json.JsonProperty("A", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        A,
        ///<summary>
        /// B
        ///</summary>
        [Newtonsoft.Json.JsonProperty("B", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        B,
        ///<summary>
        /// C
        ///</summary>
        [Newtonsoft.Json.JsonProperty("C", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        C,
        ///<summary>
        /// other
        ///</summary>
        [Newtonsoft.Json.JsonProperty("Other", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        Other
    }
    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<string>(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# 的类型绑定,然后进行封装,非常方便。

感谢大家看到这里,最后,欢迎大家使用 TypedocConverter 。当然,如果能 star 一波甚至贡献代码,我会非常感谢的!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK