用 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 的

 ReflectionFlags = {
    IsPrivate: bool option
    IsProtected:  option
    IsPublic:  option
    IsStatic:  option
    IsExported:  option
    IsExternal:  option
    IsOptional:  option
    IsReset:  option
    HasExportAssignment:  option
    IsConstructorProperty:  option
    IsAbstract:  option
    IsConst:  option
    IsLet:  option
}

 

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

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

 Reflection = {
    Id: int
    Name: string
    OriginalName: 
    Kind: ReflectionKind
    KindString:  option
    Flags: ReflectionFlags
    Parent: Reflection option
    Comment: Comment option
    Sources: SourceReference list option
    Decorators: Decorator option
    Decorates: Type list option
    Url:  option
    Anchor:  option
    HasOwnDocument:  option
    CssClasses:  option
    DefaultValue:  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<,Reflection> option
    Directory: SourceDirectory option
    Files: SourceFile list option
    Readme:  option
    PackageInfo: obj option
    Parameters: Reflection list option
}
and DeclarationHierarchy = {
    Type: Type list
    Next: DeclarationHierarchy option
    IsTarget:  option
}
 Type = {
    Type: 
    Id:  option
    Name:  option
    ElementType: Type option
    Value:  option
    Types: Type list option
    TypeArguments: Type list option
    Constraint: Type option
    Declaration: Reflection option
}
 Decorator = {
    Name: 
    Type: Type option
    Arguments: obj option
}
 ReflectionGroup = {
    Title: 
    Kind: ReflectionKind
    Children:  list
    CssClasses:  option
    AllChildrenHaveOwnDocument:  option
    AllChildrenAreInherited:  option
    AllChildrenArePrivate:  option
    AllChildrenAreProtectedOrPrivate:  option
    AllChildrenAreExternal:  option
    SomeChildrenAreExported:  option
    Categories: ReflectionCategory list option
}
 ReflectionCategory = {
    Title: 
    Children:  list
    AllChildrenHaveOwnDocument:  SourceDirectory = {
    Parent: SourceDirectory option
    Directories: Map< option
    DirName:  option
    Url:  SourceFile = {
    FullFileName: 
    FileName: 
    Url:  option
    Parent: SourceDirectory option
    Reflections: Reflection list option
    Groups: ReflectionGroup list option
}
 SourceReference = {
    File: SourceFile option
    FileName: 
    Line: 
    Character:  Comment = {
    ShortText: 
    Text:  option
    Returns:  option
    Tags: CommentTag list option
}
 CommentTag = {
    TagName: 
    ParentName: 
}

 

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

反序列化

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

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

 OptionConverter() =
    inherit JsonConverter()
    override __.CanConvert(objectType: Type) :  = 
        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]
        )
     __.ReadJson(reader: JsonReader,objectType: Type,_existingValue: obj,serializer: JsonSerializer) : obj = 
        let innerType = objectType.GetGenericArguments().[]
         value = 
            serializer.Deserialize(
                reader,1)">if innerType.IsValueType 
                then (typedefof<_ Nullable>).MakeGenericType([|innerType|])
                else innerType
        )
         cases = FSharpType.GetUnionCases objectType
        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 就足够了。

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

 EntityBodyType = {
    Type:  option
    InnerTypes: EntityBodyType list
}

 EntityMethod = {
    Comment: 
    Modifier:  list
    Type: EntityBodyType
    Name: 
    TypeParameter:  list
    Parameter: EntityBodyType list
}

 EntityProperty = {
    Comment:  list
    Name: 
    Type: EntityBodyType
    WithGet: 
    WithSet: 
    IsOptional: 
    InitialValue:  option
}

 EntityEvent = {
    Comment:  list
    DelegateType: EntityBodyType
    Name: 
}

 EntityEnum = {
    Comment: 
    Value: int64 option
}

 EntityType = 
| Interface
| Class
| Enum
| StringEnum

 Entity = {
    Namespace: 
    Comment: 
    Methods: EntityMethod list
    Properties: EntityProperty list
    Events: EntityEvent list
    Enums: EntityEnum list
    InheritedFrom: EntityBodyType list
    Type: EntityType
    TypeParameter:  list
    Modifier:  list
}

 

文档化注释生成器

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

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

let escapeSymbols (text: ) = 
    if isNull text then ""
     text
            .Replace("&",&amp;")
            .Replace(<&lt;>&gt;)

let toCommentText (text: else text.Split \n" |> Array.map (fun t -> /// " + escapeSymbols t) |> Array.reduce(fun accu next -> accu +  + next)

 getXmlDocComment (comment: Comment) =
    let prefix = /// <summary>\n"
    let suffix = \n/// </summary> summary = 
        match comment.Text with
        | Some text -> prefix + toCommentText comment.ShortText + toCommentText text + suffix
        | _ -> 
            match comment.ShortText with
            | "" -> ""
            | _ -> prefix + toCommentText comment.ShortText + suffix
     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 = 
     genericType =
        match typeInfo.Type with
        | intrinsic" -> 
            match typeInfo.Name with
            | Some name -> 
                match name with
                | number" -> { Type = double; InnerTypes = []; Name = None }
                | booleanboolstringvoid; InnerTypes = []; Name = None }
                | _ -> { Type = object; InnerTypes = []; Name = None }
            | _ -> { Type = ; InnerTypes = []; Name = None }
        | reference" | typeParameterPromiseSystem.Threading.Tasks.TaskSetSystem.Collections.Generic.ISetMapSystem.Collections.Generic.IDictionaryArraySystem.ArrayBigUint64Array"; InnerTypes = [{ Type = ulong; InnerTypes = [ ]; Name = None };]; Name = None };
                | Uint32ArrayuintUint16ArrayushortUint8ArraybyteBigInt64ArraylongInt32ArrayintInt16ArrayshortInt8ArraycharRegExp; InnerTypes = []; Name = None };
                | x -> { Type = x; InnerTypes = []; Name = None };
            | _ -> { Type = arraymatch typeInfo.ElementType with
            | Some elementType -> { Type = ; InnerTypes = [getType elementType]; Name = None }
            | _ -> { Type = ; InnerTypes = []; Name = None }]; Name = None }
        | stringLiteraltuple" ->
            match typeInfo.Types with
            | Some innerTypes -> 
                match innerTypes with
                | [] -> { Type = System.ValueTuple"; InnerTypes = innerTypes |> List.map getType; Name = None }
            | _ -> { Type = union; InnerTypes = []; Name = None }
                | _ -> 
                    printWarning (Taking only the first type " + innerTypes.[0].Type +  for the entire union type.)
                    getType innerTypes.[0] // TODO: generate unions
| _ ->{ Type = intersection"; InnerTypes = []; Name = None }  TODO: generate intersections
| reflectionmatch typeInfo.Declaration with
            | Some dec -> 
                match dec.Signatures with
                | Some [signature] -> 
                     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]
                                    | _ -> []
                                )
                        | _ -> []
                     getDelegateParas (paras: EntityBodyType list): EntityBodyType list =
                        match paras with
                        | [x] -> [{ Type = x.Type; InnerTypes = x.InnerTypes; Name = None }]
                        | (front::tails) -> [front] @ getDelegateParas tails
                        | _ -> returnsType = 
                        match signature.Type with
                        | Some t -> getType t
                        | _ -> { Type = ; InnerTypes = []; Name = None }
                     typeParas = getDelegateParas paras
                    match typeParas with
                    | [] -> { Type = System.Action; InnerTypes = []; Name = None }
                    | _ -> 
                        if returnsType.Type = " 
                        then { Type = ; InnerTypes = typeParas; Name = None } 
                        else { Type = System.Func; InnerTypes = typeParas @ [returnsType]; Name = None }
                | _ -> { Type = ; InnerTypes = []; Name = None }
        | _ -> { Type = ; InnerTypes = []; Name = None }
    mutable innerTypes = 
        match typeInfo.TypeArguments with
        | Some args -> getGenericTypeArguments args
        | _ -> []
    if genericType.Type = then 
        with
        | (front::_) -> if front.Type = " then innerTypes <- []  ()
        | _ -> ()
     ()
    { 
        Type = genericType.Type; 
        Name = None; 
        InnerTypes = if innerTypes = [] then genericType.InnerTypes  innerTypes; 
    }
 getGenericTypeArguments (typeInfos: Type list): EntityBodyType list = 
    typeInfos |> List.map getType
and getGenericTypeParameters (nodes: Reflection list) =  TODO: generate constaints
 types = 
        nodes 
        |> List.where(fun x -> x.Kind = ReflectionKind.TypeParameter)
        |> List.map ( x.Name)
    types |> List.map (fun x -> {| Type = x; Constraint = "" |})

 

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

修饰生成器

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

 getModifier (flags: ReflectionFlags) = 
     modifier = []
    match flags.IsPublic with
    | Some flag -> if flag then modifier <- modifier |> List.append [ public" ]  ()
    | _ ->match flags.IsAbstract abstractmatch flags.IsPrivate privatematch flags.IsProtected protectedmatch flags.IsStatic static ()
    modifier

 

Enum 生成器

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

let parseEnum (section: ) (node: Reflection): Entity =
    let values = match node.Children with
                 | Some children ->
                     children
                     |> List.where ( x.Kind = ReflectionKind.EnumMember)
                 | None -> []
    { 
        Type = EntityType.Enum;
        Namespace = if section = "" TypeDocGenerator 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 ->
             comment = 
                match x.Comment with
                | Some comment -> getXmlDocComment comment
                | _ -> ""
            mutable intValue = 0L
            match x.DefaultValue with
             ?????
            | Some value -> if Int64.TryParse(value,&intValue) then { Comment = comment; Name = toPascalCase x.Name; Value = Some intValue; }
                            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 代替。所以我们还需要一个查找函数:

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

 

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

Interface 和 Class 生成器

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

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

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

 

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

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

 

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

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

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

 

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

Type alias 生诚器

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

let parseUnionTypeAlias (section: ) (node: Reflection) (nodes: Type list): Entity list =
    let notStringLiteral = nodes |> List.tryFind(fun x -> x.Type <> )
     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 = []  
        [
            {
                Namespace = section
                Name = node.Name
                Comment = 
                    with
                    | Some comment -> getXmlDocComment comment
                    | _ -> 
                Methods = []
                Events = []
                Properties = []
                Enums = enums
                InheritedFrom = []
                Type = EntityType.StringEnum
                TypeParameter = []
                Modifier = getModifier node.Flags
            }
        ]

let parseTypeAlias (section: ) (node: Reflection): Entity list =
     typeInfo = node.Type
    match typeInfo with
    | Some aliasType ->
        match aliasType.Type match aliasType.Types with
            | Some types -> parseUnionTypeAlias section node types
            | _ -> 
                printWarning ()
                []
        | _ ->
            printWarning ()
            []
    | _ -> []

 

组合 Prasers

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

rec parseNode (section: match node.Kind with
    | ReflectionKind.Global ->
        with
        | Some children -> parseNodes section children
        | _ -> []
    | ReflectionKind.Module ->
        
            parseNodes (then node.Name else section + . + node.Name) children
        | _ -> []
    | ReflectionKind.ExternalModule ->
         []
    | ReflectionKind.Enum -> [parseEnum section node]
    | ReflectionKind.Interface -> [parseInterfaceAndClass section node ]
    | ReflectionKind.Class -> [parseInterfaceAndClass section node ]
    | ReflectionKind.TypeAlias -> 
        match node.Type  parseTypeAlias section node
        | _ -> []
    | _ -> []

 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= 2 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): ;
    *
     * An property
     
    readonly testProp: string;
  }

  *
   * Another declaration of an interface
   
  export interface MyInterface2<T> {
    
    testMethod(arg: T,1)">): T;
    
    onTest(listener: (e: MyInterface2<T>) => 
    readonly testProp: T;
  }

  *
   * The declaration of a class
   
  export class MyClass1<T> implements MyInterface1 {
    
    readonly testProp: string;
    static staticMethod(value: string,isOption?: boolean): UnionStr;
  }

  *
   * Another declaration of a class
   
  export class MyClass2<T> implements MyInterface2<T>
    readonly testProp: T;
    static staticMethod(value: string,1)">*
   * 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 Newtonsoft.Json.NullValueHandling.Ignore)]
        A = B Newtonsoft.Json.NullValueHandling.Ignore)]
        B = 1C Newtonsoft.Json.NullValueHandling.Ignore)]
        C = 2D Newtonsoft.Json.NullValueHandling.Ignore)]
        D = 
    }
}

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

 Another declaration of a class
    class MyClass2<T> : MyInterface2<T>
    {
         Newtonsoft.Json.NullValueHandling.Ignore)]
        T TestProp { event System.Action<MyInterface2<T>> OnTest;
        T TestMethod(T arg,System.Action callback) =>  The declaration of an interface
    interface MyInterface1
    {
        get; set; }
         arg,System.Action callback);
    }
}

 Another declaration of an interface
    interface MyInterface2<T> The declaration of a type alias
    </summary>
    [Newtonsoft.Json.JsonConverter(typeof(UnionStrConverter))]
     UnionStr
    {
        /// A
         Newtonsoft.Json.NullValueHandling.Ignore)]
        A, B
         Newtonsoft.Json.NullValueHandling.Ignore)]
        B,1)"> C
         Newtonsoft.Json.NullValueHandling.Ignore)]
        C,1)"> other
        Other Newtonsoft.Json.NullValueHandling.Ignore)]
        Other
    }
    class UnionStrConverter : Newtonsoft.Json.JsonConverter
    {
        public override bool CanConvert(System.Type t) => t == typeof(UnionStr) || t == typeof(UnionStr?);
        object ReadJson(Newtonsoft.Json.JsonReader reader,System.Type t,1)">object? existingValue,Newtonsoft.Json.JsonSerializer serializer)
            => reader.TokenType switch
            {
                Newtonsoft.Json.JsonToken.String =>
                    serializer.Deserialize<string>(reader) 
                    {
                        " => UnionStr.A,1)"> UnionStr.B,1)"> UnionStr.C,1)"> UnionStr.Other,_ => new System.Exception(Cannot unmarshal type UnionStr)
                    },1)">)
            };
        void WriteJson(Newtonsoft.Json.JsonWriter writer,1)"> untypedValue,Newtonsoft.Json.JsonSerializer serializer)
        {
            if (untypedValue is null) { serializer.Serialize(writer,1)">null); return; }
            var value = (UnionStr)untypedValue;
             (value)
            {
                case UnionStr.A: serializer.Serialize(writer,1)">"); ;
                case UnionStr.B: serializer.Serialize(writer,1)">case UnionStr.C: serializer.Serialize(writer,1)">case UnionStr.Other: serializer.Serialize(writer,1)">default: break;
            }
            Cannot marshal type UnionStr);
        }
    }
}

 

后记

有了这个工具后,妈妈再也不用担心我封装 TypeScript 的库了。有了 TypedocConverter,任何 TypeScript 的库都能轻而易举地转换成 C# 的类型绑定,然后进行封装,非常方便。

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

相关文章

在上文中,我介绍了事件驱动型架构的一种简单的实现,并演示...
上文已经介绍了Identity Service的实现过程。今天我们继续,...
最近我为我自己的应用开发框架Apworks设计了一套案例应用程序...
HAL(Hypertext Application Language,超文本应用语言)是一...
在前面两篇文章中,我详细介绍了基本事件系统的实现,包括事...
HAL,全称为Hypertext Application Language,它是一种简单的...