.NET手擼繪制TypeScript類圖——上篇


.NET手擼繪制TypeScript類圖——上篇

近年來隨着交互界面的精細化,TypeScript越來越流行,前端的設計也越來復雜,而類圖正是用簡單的箭頭和方塊,反映對象與對象之間關系/依賴的好方式。許多工具都能生成C#類圖,有些工具也能生成TypeScript類圖,如tsuml,但存在一些局限性。

我們都是.NET開發,為啥不干脆就用.NET擼一個TypeScript類圖呢?

說干就干!為了搞到類圖,一共分兩步走:

  1. 解析.ts文件,生成抽象語法樹(AST),並轉換為簡單的屬性方法等對象
  2. 將這個對象繪制出來

本文將分上下兩部分,上篇將介紹我移植的一個.NET Standard 2.0的TypeScript解析庫,下篇將介紹如何將AST轉換為真正的圖,並實現一些基本的交互。

.ts文件生成抽象語法樹

正常來說編譯原理挺難的,但好在有人趕在了我的前頭😁。

TypeScript解析庫

我在Github上找到了一個叫TypeScriptAST的項目,它剛好就能將.ts文件轉換為AST。但它僅提供了.NET Framework版本。我看了一下實現方式,它是從微軟官方的TypeScript倉庫按源代碼翻譯的。其中Parse.cs高達近8000行代碼,能把如此巨大的工作翻譯完成,可見作者花了不少時間。

我拿了過來,稍微改造了一下,移植到了.NET CoreNuGet包地址為:

https://www.nuget.org/packages/Sdcb.TypeScriptAST/

我移植的這個版本源代碼也開放到了Github,使用相同的Apache-2.0協議開源,開源項目鏈接如下:

https://github.com/sdcb/TypeScriptAST

雖然不知道是不是第一個移植的,但可以確定的是今后.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,可以將代碼寫得特別精練,最后可以達到“一行代碼*”完成.tsAST的轉換:

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騷操作】

DotNet騷操作


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM