.NET手擼繪制TypeScript類圖——上篇
近年來隨着交互界面的精細化,TypeScript
越來越流行,前端的設計也越來復雜,而類圖
正是用簡單的箭頭和方塊,反映對象與對象之間關系/依賴的好方式。許多工具都能生成C#
類圖,有些工具也能生成TypeScript
類圖,如tsuml
,但存在一些局限性。
我們都是.NET
開發,為啥不干脆就用.NET
擼一個TypeScript
類圖呢?
說干就干!為了搞到類圖,一共分兩步走:
- 解析
.ts
文件,生成抽象語法樹(AST
),並轉換為簡單的類
、屬性
、方法
等對象 - 將這個對象繪制出來
本文將分上下兩部分,上篇將介紹我移植的一個.NET Standard 2.0的TypeScript解析庫,下篇將介紹如何將AST轉換為真正的圖,並實現一些基本的交互。
.ts文件生成抽象語法樹
正常來說編譯原理挺難的,但好在有人趕在了我的前頭😁。
TypeScript解析庫
我在Github
上找到了一個叫TypeScriptAST
的項目,它剛好就能將.ts
文件轉換為AST
。但它僅提供了.NET Framework
版本。我看了一下實現方式,它是從微軟官方的TypeScript倉庫按源代碼翻譯的。其中Parse.cs
高達近8000
行代碼,能把如此巨大的工作翻譯完成,可見作者花了不少時間。
我拿了過來,稍微改造了一下,移植到了.NET Core
。NuGet
包地址為:
我移植的這個版本源代碼也開放到了Github
,使用相同的Apache-2.0
協議開源,開源項目鏈接如下:
雖然不知道是不是第一個移植的,但可以確定的是今后.NET Core
也能解析TypeScript
了:)
注意:官方沒有提供
TypeScript
的.NET
解析工具,也沒建議用.NET
,使用ts
解析是正常做法,官方的包用起來顯然也更有自信——但這就是騷操作
,不挑戰一下怎么知道極限在哪呢?
簡單使用
假如有如下TypeScript
代碼:
class Class1
{
td: number = 3;
ts: string = 'hello';
doWork(): string {
return `${3+this.td}-${this.ts}`;
}
}
var tc = new Class1();
我們可以使用TypeScriptAST
的類進行分析,只需使用TypeScriptAST
類:
var ast = new TypeScriptAST(source: tsSourceStringContent);
該類有許多對象,提供了豐富的解析方式,使用如下代碼,即可將代碼中的類抽出來:
var classAsts = ast.OfKind(SyntaxKind.ClassDeclaration);
由於AST
中的屬性太多,我們調試時抽重要的顯示出來,並轉換為JSON
:
JsonSerializer.Serialize(classAsts.Select(c => new
{
c.IdentifierStr,
Children = c.Children.Skip(1).Select(x => x.IdentifierStr),
}), new JsonSerializerOptions { WriteIndented = true}).Dump();
結果如下:
[
{
"IdentifierStr": "Class1",
"Children": [
"td",
"ts",
"doWork"
]
}
]
有了這個,我們即可定義一些類型,用於后續繪制AST
:
class ClassDef
{
public string Name { get; set; }
public List<PropertyDef> Properties { get; set; }
public List<MethodDef> Methods { get; set; }
}
class PropertyDef
{
public string Name { get; set; }
public bool IsPublic { get; set; }
public bool IsStatic { get; set; }
public string Type { get; set; }
public override string ToString() => (IsPublic ? "+" : "-") + $" {Name}: " + (String.IsNullOrWhiteSpace(Type) ? "any" : Type);
}
class MethodDef
{
public string Name { get; set; }
public bool IsPublic { get; set; }
public bool IsStatic { get; set; }
public List<ParameterDef> Parameters { get; set; }
public string ReturnType { get; set; }
public override string ToString() =>
(IsPublic ? "+" : "-")
+ $" {Name}({String.Join(", ", Parameters)})"
+ (Name == ".ctor" ? "" : $": {ReturnType}");
}
class ParameterDef
{
public string Name { get; set; }
public string Type { get; set; }
public override string ToString() => $"{Name}: {Type}";
}
借助於.NET
強大的LINQ
,可以將代碼寫得特別精練,最后可以達到“一行代碼*”完成.ts
到AST
的轉換:
static Dictionary<string, ClassDef> ParseFiles(IEnumerable<string> files) =>
files
.Select(x => new TypeScriptAST(File.ReadAllText(x), x))
.SelectMany(x => x.OfKind(SyntaxKind.ClassDeclaration))
.Select(x => new ClassDef
{
Name = x.OfKind(SyntaxKind.Identifier).FirstOrDefault().GetText(),
Properties = x.OfKind(SyntaxKind.PropertyDeclaration)
.Select(x => new PropertyDef
{
Name = x.IdentifierStr,
IsPublic = x.First.Kind != SyntaxKind.PrivateKeyword,
IsStatic = x.OfKind(SyntaxKind.StaticKeyword).Any(),
Type = GetType(x),
}).ToList(),
Methods = x.OfKind(SyntaxKind.Constructor).Concat(x.OfKind(SyntaxKind.MethodDeclaration))
.Select(x => new MethodDef
{
Name = x is ConstructorDeclaration ctor ? ".ctor" : x.IdentifierStr,
IsPublic = x.First.Kind != SyntaxKind.PrivateKeyword,
IsStatic = x.OfKind(SyntaxKind.StaticKeyword).Any(),
Parameters = ((ISignatureDeclaration)x).Parameters.Select(x => new ParameterDef
{
Name = x.OfKind(SyntaxKind.Identifier).FirstOrDefault().GetText(),
Type = GetType(x),
}).ToList(),
ReturnType = GetReturnType(x),
}).ToList(),
}).ToDictionary(x => x.Name, v => v);
兩個函數稍微提取一下,代碼能更精練:
static string GetReturnType(Node node) => node.Children.OfType<TypeNode>().FirstOrDefault()?.GetText();
static string GetType(Node node) => node switch
{
var x when x.OfKind(SyntaxKind.TypeReference).Any() => x.OfKind(SyntaxKind.TypeReference).First().GetText(),
_ => node.Last switch
{
LiteralExpression literal => literal.Kind.ToString()[..^7].ToLower() switch
{
"numeric" => "number",
var x => x,
},
var x => x.GetText(),
},
};
使用
我對這個ShootR項目進行了分析,分析代碼如下:
ParseFiles(Directory.EnumerateFiles(
path: @"C:\Users\dotnet-lover\source\repos\ShootR\ShootR\ShootR\Client\Ships", "*.ts")
).Dump();
分析結果:
成功找到了完整的7
個類,並將類名
、字段名
、字段類型
、方法名
、方法參數
和返回值
等信息都解析出來了。
總結
在本篇我們介紹了如何使用.NET
解析TypeScript
,並推薦了我移植的一個NuGet
包:Sdcb.TypeScriptAST
。
下篇將在這篇的基礎上,介紹如何使用代碼將類圖渲染出來。
本文所用到的完整代碼,可以在我的Github
倉庫中下載:
https://github.com/sdcb/blog-data/tree/master/2019/20191113-ts-uml-with-dotnet
喜歡的朋友 請關注我的微信公眾號:【DotNet騷操作】