使用 F# 手寫一個 Typedoc 轉 C# 代碼生成器,友善一切 C# 項目對 TypeScript 項目的封裝。
前言
我們經常會遇到這樣的事情:有時候我們找到了一個庫,但是這個庫是用 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 一波甚至貢獻代碼,我會非常感謝的!