内容简介:我们经常会遇到这样的事情:有时候我们找到了一个库,但是这个库是用 TypeScript 写的,但是我们想在 C# 调用,于是我们需要设法将原来的 TypeScript 类型声明翻译成 C# 的代码,然后如果是 UI 组件的话,我们需要将其封装到一个 WebView 里面,然后通过 JavaScript 和 C# 的互操作功能来调用该组件的各种方法,支持该组件的各种事件等等。但是这是一个苦力活,尤其是类型翻译这一步。这个是我最近在帮助维护一个开源 UWP 项目
前言
我们经常会遇到这样的事情:有时候我们找到了一个库,但是这个库是用 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("&", "&")
.Replace("<", "<")
.Replace(">", ">")
let toCommentText (text: string) =
if isNull text then ""
else text.Split "\n" |> Array.map (fun t -> "/// " + escapeSymbols t) |> Array.reduce(fun accu next -> accu + "\n" + next)
let getXmlDocComment (comment: Comment) =
let prefix = "/// <summary>\n"
let suffix = "\n/// </summary>"
let summary =
match comment.Text with
| Some text -> prefix + toCommentText comment.ShortText + toCommentText text + suffix
| _ ->
match comment.ShortText with
| "" -> ""
| _ -> prefix + toCommentText comment.ShortText + suffix
let returns =
match comment.Returns with
| Some text -> "\n/// <returns>\n" + toCommentText text + "\n/// </returns>"
| _ -> ""
summary + returns
类型生成器
Typescript 的类型系统较为灵活,包括 union types、intersect types 等等,这些即使是目前的 C# 8 都不能直接表达,需要等到 C# 9 才行。当然我们可以生成一个 struct 并为其编写隐式转换操作符重载,支持 union types,但是目前尚未实现,我们就先用 union types 中的第一个类型代替,而对于 intersect types,我们姑且先使用 object。
然而 union types 有一个特殊情况:string literals types alias。就是这样的东西:
type Size = "XS" | "S" | "M" | "L" | "XL";
即纯 string 值组合的 type alias,这个我们还是有必要支持的,因为在 typescript 中用的非常广泛。
C# 在没有对应语法的时候要怎么支持呢?很简单,我们创建一个 enum,该 enum 包含该类型中的所有元素,然后我们为其编写 JsonConverter,这样就能确保序列化后,typescript 方能正确识别类型,而在 C# 又有 type sound 的编码体验。
另外,我们需要提供一些常用的类型转换:
-
Array<T>->T[] -
Set<T>->System.Collections.Generic.ISet<T> -
Map<T>->System.Collections.Generic.IDictionary<T> -
Promise<T>->System.Threading.Tasks.Task<T> - callbacks ->
System.Func<T...>,System.Action<T...> - Tuple 类型
- 其他的数组类型如
Uint32Array - 对于
<void>,我们需要解除泛型,即T<void>->T
那么实现如下:
let rec getType (typeInfo: Type): EntityBodyType =
let genericType =
match typeInfo.Type with
| "intrinsic" ->
match typeInfo.Name with
| Some name ->
match name with
| "number" -> { Type = "double"; InnerTypes = []; Name = None }
| "boolean" -> { Type = "bool"; InnerTypes = []; Name = None }
| "string" -> { Type = "string"; InnerTypes = []; Name = None }
| "void" -> { Type = "void"; InnerTypes = []; Name = None }
| _ -> { Type = "object"; InnerTypes = []; Name = None }
| _ -> { Type = "object"; InnerTypes = []; Name = None }
| "reference" | "typeParameter" ->
match typeInfo.Name with
| Some name ->
match name with
| "Promise" -> { Type = "System.Threading.Tasks.Task"; InnerTypes = []; Name = None }
| "Set" -> { Type = "System.Collections.Generic.ISet"; InnerTypes = []; Name = None }
| "Map" -> { Type = "System.Collections.Generic.IDictionary"; InnerTypes = []; Name = None }
| "Array" -> { Type = "System.Array"; InnerTypes = []; Name = None }
| "BigUint64Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "ulong"; InnerTypes = [ ]; Name = None };]; Name = None };
| "Uint32Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "uint"; InnerTypes = [ ]; Name = None };]; Name = None };
| "Uint16Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "ushort"; InnerTypes = [ ]; Name = None };]; Name = None };
| "Uint8Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "byte"; InnerTypes = [ ]; Name = None };]; Name = None };
| "BigInt64Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "long"; InnerTypes = [ ]; Name = None };]; Name = None };
| "Int32Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "int"; InnerTypes = [ ]; Name = None };]; Name = None };
| "Int16Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "short"; InnerTypes = [ ]; Name = None };]; Name = None };
| "Int8Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "char"; InnerTypes = [ ]; Name = None };]; Name = None };
| "RegExp" -> { Type = "string"; InnerTypes = []; Name = None };
| x -> { Type = x; InnerTypes = []; Name = None };
| _ -> { Type = "object"; InnerTypes = []; Name = None }
| "array" ->
match typeInfo.ElementType with
| Some elementType -> { Type = "System.Array"; InnerTypes = [getType elementType]; Name = None }
| _ -> { Type = "System.Array"; InnerTypes = [{ Type = "object"; InnerTypes = []; Name = None }]; Name = None }
| "stringLiteral" -> { Type = "string"; InnerTypes = []; Name = None }
| "tuple" ->
match typeInfo.Types with
| Some innerTypes ->
match innerTypes with
| [] -> { Type = "object"; InnerTypes = []; Name = None }
| _ -> { Type = "System.ValueTuple"; InnerTypes = innerTypes |> List.map getType; Name = None }
| _ -> { Type = "object"; InnerTypes = []; Name = None }
| "union" ->
match typeInfo.Types with
| Some innerTypes ->
match innerTypes with
| [] -> { Type = "object"; InnerTypes = []; Name = None }
| _ ->
printWarning ("Taking only the first type " + innerTypes.[0].Type + " for the entire union type.")
getType innerTypes.[0] // TODO: generate unions
| _ ->{ Type = "object"; InnerTypes = []; Name = None }
| "intersection" -> { Type = "object"; InnerTypes = []; Name = None } // TODO: generate intersections
| "reflection" ->
match typeInfo.Declaration with
| Some dec ->
match dec.Signatures with
| Some [signature] ->
let paras =
match signature.Parameters with
| Some p ->
p
|> List.map
(fun pi ->
match pi.Type with
| Some pt -> Some (getType pt)
| _ -> None
)
|> List.collect
(fun x ->
match x with
| Some s -> [s]
| _ -> []
)
| _ -> []
let rec getDelegateParas (paras: EntityBodyType list): EntityBodyType list =
match paras with
| [x] -> [{ Type = x.Type; InnerTypes = x.InnerTypes; Name = None }]
| (front::tails) -> [front] @ getDelegateParas tails
| _ -> []
let returnsType =
match signature.Type with
| Some t -> getType t
| _ -> { Type = "void"; InnerTypes = []; Name = None }
let typeParas = getDelegateParas paras
match typeParas with
| [] -> { Type = "System.Action"; InnerTypes = []; Name = None }
| _ ->
if returnsType.Type = "void"
then { Type = "System.Action"; InnerTypes = typeParas; Name = None }
else { Type = "System.Func"; InnerTypes = typeParas @ [returnsType]; Name = None }
| _ -> { Type = "object"; InnerTypes = []; Name = None }
| _ -> { Type = "object"; InnerTypes = []; Name = None }
| _ -> { Type = "object"; InnerTypes = []; Name = None }
let mutable innerTypes =
match typeInfo.TypeArguments with
| Some args -> getGenericTypeArguments args
| _ -> []
if genericType.Type = "System.Threading.Tasks.Task"
then
match innerTypes with
| (front::_) -> if front.Type = "void" then innerTypes <- [] else ()
| _ -> ()
else ()
{
Type = genericType.Type;
Name = None;
InnerTypes = if innerTypes = [] then genericType.InnerTypes else innerTypes;
}
and getGenericTypeArguments (typeInfos: Type list): EntityBodyType list =
typeInfos |> List.map getType
and getGenericTypeParameters (nodes: Reflection list) = // TODO: generate constaints
let types =
nodes
|> List.where(fun x -> x.Kind = ReflectionKind.TypeParameter)
|> List.map (fun x -> x.Name)
types |> List.map (fun x -> {| Type = x; Constraint = "" |})
当然,目前尚不支持生成泛型约束,如果以后有时间的话会考虑添加。
修饰生成器
例如 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 。当然,如果能 star 一波甚至贡献代码,我会非常感谢的!
以上所述就是小编给大家介绍的《用 F# 手写 TypeScript 转 C# 类型绑定生成器》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
High Performance Python
Micha Gorelick、Ian Ozsvald / O'Reilly Media / 2014-9-10 / USD 39.99
If you're an experienced Python programmer, High Performance Python will guide you through the various routes of code optimization. You'll learn how to use smarter algorithms and leverage peripheral t......一起来看看 《High Performance Python》 这本书的介绍吧!