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


    前言

    我们经常会遇到这样的事情:有时候我们找到了一个库,但是这个库是用 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 都被正确地反序列化了。

    反序列化结果

     

    构建 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("&", "&amp;")
                .Replace("<", "&lt;")
                .Replace(">", "&gt;")
    
    let toCommentText (text: string) = 
        if isNull text then ""
        else text.Split "
    " |> Array.map (fun t -> "/// " + escapeSymbols t) |> Array.reduce(fun accu next -> accu + "
    " + next)
    
    let getXmlDocComment (comment: Comment) =
        let prefix = "/// <summary>
    "
        let suffix = "
    /// </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 -> "
    /// <returns>
    " + toCommentText text + "
    /// </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>
    " + toCommentText value + "
    ///</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 一波甚至贡献代码,我会非常感谢的!

  • 相关阅读:
    《Dubbo》接入与调用流程
    SpringBoot自定义配置实现与解析
    Docker使用笔记记录
    Elasticsearch(一):介绍与安装
    索引的建立与使用规范
    RocketMQ(一):介绍与安装
    NETCORE使用MINIO的PRESIGNEDPUTOBJECTASYNC方法,实现通过浏览器直接上传图片到MINIO服务器
    Minio 使用.NET + Vue 实现断点续传、秒传
    VS Code中小程序与Vue常用插件合集(前端合集)
    [瞎想笔记] 浅浅浅谈期望
  • 原文地址:https://www.cnblogs.com/hez2010/p/12246841.html
Copyright © 2020-2023  润新知