美文网首页selector
[转]Swift自动生成UML类图

[转]Swift自动生成UML类图

作者: tom__zhu | 来源:发表于2023-03-11 12:53 被阅读0次

    1. 方案调研

    swift-syntax

    github - https://github.com/apple/swift-syntax

    这是苹果官方的工具,Xcode工具包里有,可以扫描Swift源文件,生成AST树(抽象语法树)
    用法也很简单,命令行输入

    xcrun swiftc -frontend -emit-syntax ./a.swift | python3 -m json.tool
    
    

    a.swift是创建的测试文件

    class Demo {
    
        var a;
    
        func foo() {
    
        }
    }
    
    

    生成如下

    {
        "kind": "SourceFile",
        "layout": [
            {
                "kind": "CodeBlockItemList",
                "layout": [
                    {
                        "kind": "CodeBlockItem",
                        "layout": [
                            {
                                "kind": "ClassDecl",
                                "layout": [
                                    null,
                                    null,
                                    {
                                        "tokenKind": {
                                            "kind": "kw_class"
                                        },
                                        "leadingTrivia": "",
                                        "trailingTrivia": " ",
                                        "presence": "Present"
                                    },
                                    {
                                        "tokenKind": {
                                            "kind": "identifier",
                                            "text": "Demo"
                                        },
                                        "leadingTrivia": "",
                                        "trailingTrivia": " ",
                                        "presence": "Present"
                                    }, // 下面省略几百行
    
    

    可以看到,AST树的嵌套层级非常深,即使代码非常简单,结果也很复杂。因为它是官方工具,要非常完备地分析文件,但是对于UML类图来说并不需要。

    swift-ast-explorer

    github - https://github.com/SwiftFiddle/swift-ast-explorer
    测试地址 - https://swift-ast-explorer.com/

    这是把swift代码生成AST树的网页,也是用swift-syntax生成的

    image.png

    swift-auto-diagram

    github - https://github.com/yoshimkd/swift-auto-diagram

    它可以把目录下的swift文件生成UML类图,自动生成一个网页,可以缩放拖动。只需要输入一句命令行(YourSwiftDir代表包含swift文件的目录)

    ruby generateEntityDiagram.rb ~/YourSwiftDir                     
    
    
    image.png

    它没有用AST,而是直接用正则表达式扫描分析文件中的类、对象和方法,优点是速度快,缺点是生成结果摆放非常乱,类型关系基本找不到。但是它使用的vis-network网页绘图方案很不错,支持任意摆放、拖动、缩放和添加箭头。

    SourceKitten

    github - https://github.com/jpsim/SourceKitten

    Sourcekitten 是基于 Apple 的 SourceKit 封装的命令行工具,SourceKitten 链接并与 sourcekitd.framework 通信以解析 Swift AST 树,最终提取 Swift 或 ObjC 文件的类结构和方法等。

    2. 实现方案

    根据前面的研究,我采用Sourcekitten解析Swift文件,vis-network生成类图的方案。效果如下:

    image.png
    • 右边是类图主体,包括类名和类里面所有的属性和方法。类之间的关系:继承、实现、关联和依赖分别用不用的线段和箭头表示。
    • 左边是文件夹的树状结构,可以点击隐藏和显示文件夹里的全部或单个文件,这样就不会因为类太多显示太乱。
    • 左上角是继承、实现、关联和依赖四种关系线段的开关,也可以点击隐藏或显示某种关系。

    2.1 解析Swift文件

    主要是用python遍历文件夹并调用'sourcekitten structure --file '解析文件,然后生成json文件,供网页读取。

    遍历文件中的类,取出类名、父类名、属性类名和方法的参数返回值类名

    
    def visitFile(path):
        structure = os.popen('sourcekitten structure --file ' + path).read()
        if printStructrue:
            print(structure)
        try:
            dict = json.loads(structure)
        except Exception as e:
            print('Exception in file ' + path)
            print(e)
            return
    
        for sub in dict['key.substructure']:
            kind = sub['key.kind']
            validKinds = ['source.lang.swift.decl.class', 'source.lang.swift.decl.struct', 'source.lang.swift.decl.protocol', 'source.lang.swift.decl.enum']
            if kind not in validKinds:
                continue
    
            varDetails = []
            funcDetails = []
    
            print('-visit: ' + name)
            if 'key.inheritedtypes' in sub: # ParentClass and protocols
                i = 0
                for s in sub['key.inheritedtypes']:
                    i += 1
                    name = s['key.name'] # ParentClass
                    if i == 1:
                        j = name.find('<') # ParentClass<T1, T2>
                        if j != -1:
                            arr = name[j+1:-1].split(', ')
                            for a in arr:
                                variables.append(a)
                            name = name[:j]
                        parents.append(name)
                    else:
                        protocols.append(s['key.name'])
    
            if 'key.substructure' in sub: # class members
                for s in sub['key.substructure']:
                    if s['key.kind'].startswith('source.lang.swift.decl.var'): # .instance/.static/.class
                        type = s['key.kind'].split('.')[-1]
                        type = type if type != 'instance' else ''
                        typename = '' if 'key.typename' not in s else s['key.typename']
                        if typename != '':
                            if type == '':
                                variables.append(typename)
                            else:
                                temporaries.append(typename)
                        s1 = '' if typename == '' else ': ' + typename
                        s2 = '' if type == '' else ' (' + type + ')'
                        varDetails.append('- ' + s['key.name'] + s1 + s2)
                    if s['key.kind'] == 'source.lang.swift.expr.call':
                        name = s['key.name']
                        i = name.find('.')
                        if i != -1: # MyClass.staticFunc
                            name = name[:i]
                        variables.append(name)
                    if s['key.kind'].startswith('source.lang.swift.decl.function.method'): # .instance/.static/.class
                        type = s['key.kind'].split('.')[-1]
                        type = type if type != 'instance' else ''
                        typename = '' if 'key.typename' not in s else s['key.typename']
                        if typename != '':
                            temporaries.append(typename)
    
                        s1 = '' if typename == '' else ': ' + typename
                        s2 = '' if type == '' else ' (' + type + ')'
                        funcDetails.append('+ ' + s['key.name'] + s1 + s2)
                        visitMethod(s, temporaries)
                    if s['key.kind'] == 'source.lang.swift.decl.enumcase':
                        varDetails.append('.' + s['key.substructure'][0]['key.name'])
    
            s1 = '\n'.join(varDetails)
            s2 = '\n'.join(funcDetails)
            data['detail'] = '\n-------------------------\n'.join([data['name'], s1, s2])
    
    

    递归遍历方法,取出方法里使用到的局部变量的类名

    def visitMethod(sub, temporaries):
        if 'key.substructure' in sub:
            for s in sub['key.substructure']:
                if s['key.kind'] == 'source.lang.swift.decl.var.parameter':
                    if 'key.typename' in s:
                        temporaries.append(s['key.typename'])
                if s['key.kind'] == 'source.lang.swift.decl.var.local':
                    if 'key.typename' in s:
                        temporaries.append(s['key.typename'])
                if s['key.kind'] == 'source.lang.swift.expr.call':
                        name = s['key.name']
                        i = name.find('.')
                        if i != -1: # MyClass.staticFunc
                            name = name[:i]
                        temporaries.append(name)
                visitMethod(s, temporaries)
    
    

    打开浏览器,启动一个简单的服务器(因为要访问json文件,所以要访问服务器的网页。我开发用的是VSCode和Live Preview插件,非常方便)。

            webbrowser.open('http://localhost:8080/diagram.html')
            os.system('python3 -m http.server 8080')
    
    

    2.2 网页展示

    用JavaScript读取json文件,生成vis-network需要的点node和线段edge就可以展示了。

    读取json文件

    function readTextFile(path) {
        var rawFile = new XMLHttpRequest();
        rawFile.open("GET", path, false);
        rawFile.onreadystatechange = function ()
        {
            if(rawFile.readyState === 4)
            {
                if(rawFile.status === 200 || rawFile.status == 0)
                {
                    var allText = rawFile.responseText;
                    handleJsonStr(path, allText);
                }
            }
        }
        rawFile.send(null);
    }
    
    

    生成目录树

    function generateTree(obj) {
        if (typeof obj == 'string') {
            return `<div id='file'>${obj}</div>`
        } else {
            var str = `<div id='dir'>${obj['name']}</div><ul>`
            for (var o of obj['list']) {
                str += '<li>' + generateTree(o) + '</li>'
            }
            return str + '</ul>'
        }
    }
    
    

    生成类图

    
    function handleDataJson(dataArr) {
        var nodeArr = []
        var edgeArr = []
    
        var nodeTypes = ['class','struct','protocol','enum']
        var edgeTypes = ['parents','protocols','variables','temporaries']
    
        generateCheck(edgeTypes)
    
        var nameIdDict = {}
        for (var data of dataArr) {
            nameIdDict[data['name']] = data['id']
        }
    
        for (var data of dataArr) {
            var node = createNode(data['id'], data['detail'], data['kind'], data['file'])
            nodeArr.push(node)
    
            var from = data['id']
            for (var type of edgeTypes) {
                for (var to of data[type]) {
                    to = nameIdDict[to]
                    if (to != undefined) {
                        var edge = createEdge(from, to, type)
                        edgeArr.push(edge)
                    }
                }
            }
        }
    
        let nodes = new vis.DataSet(nodeArr)
        let edges = new vis.DataSet(edgeArr)
    
        const nodesFilter = (node) => {
            if (hideFiles[node.file] == true) {
                return false
            }
            return true;
        };
    
        const edgesFilter = (edge) => {
            if (hideEdges[edge.type] == true) {
                return false
            }
            return true;
        };
    
        nodesView = new vis.DataView(nodes, { filter: nodesFilter });
        edgesView = new vis.DataView(edges, { filter: edgesFilter });
    
        // create a network
        var container = document.getElementById("mynetwork");
        var data = {
        nodes: nodesView,
        edges: edgesView,
        };
        var options = {
            physics: createPhysicsConfig(),
    
            // layout: {
            //     hierarchical: {
            //       direction: 'Up-Down',
            //     },
            // },
        };
        var network = new vis.Network(container, data, options);
    }
    
    

    3 代码和运行

    完整代码都在github仓库

    运行方法:下载或克隆仓库,进入主目录,运行命令即可
    python3 runSwift.py /YourSwiftProjectDir

    调式方法:用VSCode(安装Python插件和Live Preview插件)打开项目,运行runSwift.pypython文件即可。

    注意:运行需要Xcode环境,并且用brew安装好sourcekitten

    引用

    Swift自动生成UML类图

    相关文章

      网友评论

        本文标题:[转]Swift自动生成UML类图

        本文链接:https://www.haomeiwen.com/subject/vpsxrdtx.html