查看原文
其他

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

小面 DotNet 2021-09-23

(给DotNet加星标,提升.Net技能

转自:hez2010
cnblogs.com/hez2010/p/12246841.html

前言


我们经常会遇到这样的事情:有时候我们找到了一个库,但是这个库是用 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 "\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 = "" |})


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


修饰生成器


例如 public、private、protected、static 等等。这一步很简单,直接将 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

https://link.zhihu.com/?target=https%3A//github.com/hez2010/TypedocConverter。


当然,如果能 star 一波甚至贡献代码,我会非常感谢的!


推荐阅读  点击标题可跳转
ASP.NET 开源导入导出库Magicodes.IE导出Excel
.NET Core常见设计模式-IChangeToken传说中的孪生兄弟?Memory and Span


看完本文有收获?请转发分享给更多人

关注「DotNet」加星标,提升.Net技能 

好文章,我在看❤️

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存