(一)一款好用的代码结构解析工具-Tree-sitter

2,518 阅读5分钟

缘起

目前AI浪潮席卷各行各业,感觉对各行各业冲击很大;特别是Github Copilot的普及使用,对自己所处的软件开发这个工作岗位的工作习惯和开发效率带来了前所未有的体验;AI模型可以用来进行代码补充,代码语言转换,代码review并提出意见等内容,极大的提高了目前的开发效率和质量;今年有机会接触了一段时间的AI CodeReview 相关的项目,AI项目的落地质量基本就取决于其中AI模型训练的质量,其中训练语料的提取就是AI模型训练模型的前提,起着决定性作用;

下面主要讲述一下我在项目中对源代码进行代码结构分析从而进行代码语料提取的工具Tree-sitter

介绍

Tree-sitter 是一个开源的解析器生成工具和增量解析库,它能够为源代码文件构建出该文件具体的语法树内容;例如下面对一个C++方法结构进行解析:

// 函数返回两个数中较大的那个数
int max(int num1, int num2) 
{
   // 局部变量声明
   int result;
   if (num1 > num2) {
      result = num1;
   } else {
      result = num2;
   }
   return result; 
}

例如Tree-sitter对该方法的结构进行解析后得到具体结构为:

translation_unit [0, 0] - [12, 0]
  comment [0, 0] - [0, 17]
  function_definition [1, 0] - [11, 1]
    type: primitive_type [1, 0] - [1, 3]
    declarator: function_declarator [1, 4] - [1, 27]
      declarator: identifier [1, 4] - [1, 7]
      parameters: parameter_list [1, 7] - [1, 27]
        parameter_declaration [1, 8] - [1, 16]
          type: primitive_type [1, 8] - [1, 11]
          declarator: identifier [1, 12] - [1, 16]
        parameter_declaration [1, 18] - [1, 26]
          type: primitive_type [1, 18] - [1, 21]
          declarator: identifier [1, 22] - [1, 26]
    body: compound_statement [2, 0] - [11, 1]
      comment [3, 3] - [3, 12]
      declaration [4, 3] - [4, 14]
        type: primitive_type [4, 3] - [4, 6]
        declarator: identifier [4, 7] - [4, 13]
      if_statement [5, 3] - [9, 4]
        condition: condition_clause [5, 6] - [5, 19]
          value: binary_expression [5, 7] - [5, 18]
            left: identifier [5, 7] - [5, 11]
            right: identifier [5, 14] - [5, 18]
        consequence: compound_statement [5, 20] - [7, 4]
          expression_statement [6, 6] - [6, 20]
            assignment_expression [6, 6] - [6, 19]
              left: identifier [6, 6] - [6, 12]
              right: identifier [6, 15] - [6, 19]
        alternative: else_clause [7, 5] - [9, 4]
          compound_statement [7, 10] - [9, 4]
            expression_statement [8, 6] - [8, 20]
              assignment_expression [8, 6] - [8, 19]
                left: identifier [8, 6] - [8, 12]
                right: identifier [8, 15] - [8, 19]
      return_statement [10, 3] - [10, 17]
        identifier [10, 10] - [10, 16]

上述得到结构中可以获取这个方法中的方法名字,入参,出参,返回值类型等所有方法体包含的信息,每种信息都有对应的节点名称对应,以及该节点所处的位置信息等,其主要优点:

  1. 足够通用:能用于任何编程语言。
  2. 足够迅速: 能够在文本编辑器中每次按键时进行解析。
  3. 鲁棒性: 即使在存在语法错误的情况下也能提供有用的结果。
  4. 无依赖: 纯C编写的运行时库可以嵌入任何应用程序。

使用教程

本文主要以java-tree-sitter为例介绍使用,使用Tree-sitter进行语言结构分析的前提是生成可以解析你想要解析语言的解析动态库,然后在项目中加载该动态库解析目标源码;

编译java-tree-sitter的动态解析so库:

1、克隆java-tree-sitter源码,java-tree-sitter官方github地址:github.com/serenadeai/…;注意拉下来的代码有个子目录tree-sitter,里面的代码是tree-sitter的源码,也需要拉下来,参照readme:

git clone https://github.com/serenadeai/java-tree-sitter.git --recursive
or
git clone https://github.com/serenadeai/java-tree-sitter.git
git submodule update --init --recursive

2、选择需要解析的语言,例如这里我们选择java、kotlin、objc、swift,需要将对应仓库的源码clone下来放在步骤1的java-tree-sitter源码目录中,跟tree-sitter子目录平级:

tree-sitter-java:https://github.com/tree-sitter/tree-sitter-java
tree-sitter-kotlin:https://github.com/fwcd/tree-sitter-kotlin
tree-sitter-objc:https://github.com/jiyee/tree-sitter-objc
tree-sitter-swift:https://github.com/alex-pinkus/tree-sitter-swift

3、进入java-tree-sitter目录,调用build.py脚本编译共享库:

python3 build.py -o libjava-tree-sitter tree-sitter-java tree-sitter-swift tree-sitter-objc tree-sitter-kotlin

4、运行完上面python脚本后就会在同级目录中生成一个可以解析目标语言源码的动态so库;

开始解析

1、在自己项目中添加tree-sitter三方库,例如python项目使用pip命令下载:

pip3 install tree_sitter

java项目可以在第三方库管理工具gradle或者Maven中添加对应的依赖:

// Gradle
dependencies {
    // add tree sitter
    implementation 'io.github.bonede:tree-sitter:0.24.3'
    // add json parser
    implementation 'io.github.bonede:tree-sitter-json:0.23.0'
}

<!-- Maven -->
<dpendencies>
    <!-- add tree sitter -->
    <dependency>
        <groupId>io.github.bonede</groupId>
        <artifactId>tree-sitter</artifactId>
        <version>0.24.3</version>
    </dependency>
    <!-- add json parser -->
    <dependency>
        <groupId>io.github.bonede</groupId>
        <artifactId>tree-sitter-json</artifactId>
        <version>0.23.0</version>
    </dependency>
</dpendencies>

2、添加对应依赖后,再把上述步骤编译出来的so库添加到项目目录中,就可以进行解析,以解析OC语言为例:

package ai.serenade.treesitter;

import java.io.UnsupportedEncodingException;
import java.util.*;

public class Test {
    public static void main(String[] args) throws Exception {
        // 使用解析功能前需要先在项目中加载解析so库
        System.load("/Users/spl/Documents/java-tree-sitter/src/main/java/resources/libjava-tree-sitter.dylib");
        analyseObjc();
    }

    public static void analyseObjc() {
        try (Parser parser = new Parser()) {
            parser.setLanguage(Languages.objc());
            String str = "//\n" +
                    "//  IFHXEventCaptureManager.m\n" +
                    "//  IphoneIJiJin\n" +
                    "//  HXEventCapture埋点sdk注册管理类\n" +
                    "//  Created by songpenglong on 2023/10/19.\n" +
                    "//\n" +
                    "\n" +
                    "#import "IFHXEventCaptureManager.h"\n" +
                    "#import "IFABTestOptBusiness.h"\n" +
                    "\n" +
                    "@implementation IFHXEventCaptureManager\n" +
                    "\n" +
                    "+ (instancetype)shareInstance {\n" +
                    "    static IFHXEventCaptureManager *shareInstance = nil;\n" +
                    "    static dispatch_once_t onceToken;\n" +
                    "    dispatch_once(&onceToken, ^{\n" +
                    "        shareInstance = [[IFHXEventCaptureManager alloc] init];\n" +
                    "    });\n" +
                    "    return shareInstance;\n" +
                    "}\n" +
                    "\n" +
                    "\n" +
                    "- (void)registerHXEventCapture {\n" +
                    "\n" +
                    "    // 设置appKey key为基金业务树APPid\n" +
                    "    NSString *appKey = @"1110792a61";\n" +
                    "     // 初始化统一埋点\n" +
                    "    _eventManager = [[HXECManager alloc] initWithAppKey:appKey];\n" +
                    "    // 更新下当前用户userid\n" +
                    "    [self updateUserid];\n" +
                    "}\n" +
                    "\n" +
                    "\n" +
                    "/// 更新userid\n" +
                    "- (void)updateUserid {\n" +
                    "    NSString *userId = IFAccountInfoServiceBridge.hexinUserId;\n" +
                    "    [_eventManager updateUserId:userId];\n" +
                    "}\n" +
                    "\n" +
                    "@end";
            try (Tree tree = parser.parseString(str)) {
                System.out.println(tree.getRootNode().getNodeString());
            } catch (UnsupportedEncodingException e) {

            }

        }
    }
}

3、通过以上步骤就可以将上面一个OC语言的类解析出类似文章开头部分的结构内容了;

结语

上面基本步骤只是可以将一个类使用Tree-sitter将其结构解析出来,我们可以在获取到的结构内容接触上,获取里面包含的方法,变量等上下文内容,进行语料提取等更复杂的操作;后续会继续更新基于基本结构解析而进一步的复杂操作,如语料提取和上下文提取等内容;