背景
上一篇文章(即python环境解析任意编程语言 tree-sitter使用方法(1))介绍了tree-sitter
功能特点、安装流程以及可能发生的安装错误。
同时还介绍了一些使用tree-sitter科研相关的论文(GraphCodeBert、UnixCoder、CodeT5、TreeBert以及SynCoBert【没开源】),顺带着给出了一个tree-sitter
用于Tokenizer分词器样例(来自GraphCodeBert)。
这一篇文章将会介绍如何利用tree-sitter
,编写query
,定位语法树节点。以节省书写dfs+回溯等复杂python代码的时间。
我想这有可能对以下研究提供些许帮助:
- 分析代码功能【深度学习分析局部节点特征】
- 提取代码节点类型【代码token表征】
- 获取特定标识符
- 检索数据流【可定位到代码中不同的位置】
- 研究控制依赖【if、while、dowhile和switch语法树节点解析】
当然,这个query
还是需要自己来写的(就像写sql)一样。这里给出了一些简单的样例作为参考。
准备工作
- 合适python环境下安装tree-sitter
没有安装看我写的这里(推荐),或者看官网py-tree-sitter这里(也推荐)。
建议打开,因为官网的playground做的语法树可视化界面直观,方便代码语法树调试。
个人尽管科研一年多的时间,也用过不少语法树工具:
- pycparser(c语言)
- javalang(java语言)
- antlr(10种语言)
- python环境ast(python语言)
- SPT generator(C++、Python、Java)
- srcML(C++、Python、Java、JS、Go)
这些语法树解析工具都很好,但是以上大多工具无法解析语法错误代码【难以接受】。更何况这些工具基本都没有在线的可视化调试工具。
所以建议打开playground,在Code
、Query
以及Tree
的可视化界面中调试代码!
准备写在playground中写query
官方语法介绍在这里,先搬运过来:Pattern Matching with Queries
query格式
query是一个S-表达式,该表达式由一对嵌套结构的阔号组成,阔号内包含两部分:本节点类型 和 0个或多个子节点的S-表达式。
(节点类型
(子节点类型_1)
字段: (字节点类型_2)
)
节点类型在Tree中以蓝色突出显示,后面紧跟[行, 列]-[行, 列]
起止位置。
比如样例代码中:
// 来自b站
int mian{
piantf("hell world");
remake O;
}
根节点是translation_unit
节点类型,可写作:
( translation_unit ) @1
样例代码行"piantf("hell world");"
,如果选择实参列表,在Tree中对应的是一个arguments: argument_list
【前面是字段,后面网页标出蓝色的argument_list才是节点类型】,可写作:
( argument_list ) @2
其中@1 @2
是节点选择表达式【@自定义符号
】,写在成对的括号外面。
网页中,选择的节点在Code界面高亮显示。
后面在写代码时,会通过自定义符号
获取节点。
带有字段的query
前两个例子只选择了某个节点,但是没有区分其上下文,可能选择意想不到的部分。
int mian{
long main;
remake O;
}
现在,要选择函数定义语句的int的节点,可以发现只写( primitive_type )
是不行的,会选择main的long
,所以,加入字段以后,可以发现选中了正确的本部分:
( function_definition
type:(primitive_type) @3
)
// 或者写仔细点
( function_definition
type: (primitive_type) @3
declarator: (identifier)
)
// 字段是有先后顺序的,不可以颠倒type和decalarator顺序。
// 官方界面可以很直观地显示出错误
// 这是错误的表达式:
( function_definition
declarator: (identifier)
type: (primitive_type) @3
)
像这样,写得越细致,越能避免选择错误的节点。同时也避免了手写dfs+回溯代码的困难场景。
注意:选择哪个节点,就在哪个节点括号对后加@自定义符号
,如果:
( function_definition
type:(primitive_type) @3
) @4
就会选择代码声明
以及原始类型
两种节点。
匿名节点
该匿名节点指的是代码中,像+, -, *, /, [, ],=等等符号,tree-sitter没有标志出他们的具体名称。【可能有些语法树节点就蕴含了语法构成吧,所以作者认为这些符号很琐碎,没有取名字】
a++;
双引号包含他们即可:
( update_expression
"++" @6
)
通配符节点
类似正则表达式,在节点类型阔号后使用+或*
:
+
表示一个或多个*
表示0个或多个
节点如果没有限制,可以通配符_
( _ ) @all
还有其他使用方法,官方介绍得很仔细,我估计个人还要继续做相关介绍😅。
在Python代码中获取节点
cpp代码:
#include <stdio.h>
int cmp(a, b){
return a > b? 1: 0;
}
int main(){
int arr[] = {5, 2, 1, 3, 0};
char s[] = {'h', 'e', 'l', 'l', 'o'};
int n = 5;
int i, j, tmp;
for(i = 0; i < n; i++){
for(j = n - i - 1; j > 0; i--) {
if(cmp(arr[j - 1], arr[j])){
tmp = arr[j-1];
arr[j-1] = arr[j];
arr[j] = tmp;
}
}
}
printf("%s\n", s);
return 0;
希望有人能看到这里,🤗这是代码中错误的片段。
}
需求:
- 获取函数名称
- 获取数组初始化列表
- 获取函数调用语句
- 获取赋值语句的右侧
- 获取错误(error)
(function_declarator declarator: (identifier)@1 )
(initializer_list) @2
( call_expression ) @3
(assignment_expression right:(_) @4)
(ERROR) @error
需要使用的python代码语句:
# 需要使用的语句
from tree_sitter import Language, Parser
# 实例化
CPP_LANGUAGE = Language('build/my-languages.so', 'cpp')
# 书写query
query_text = '( translation_unit ) @1'
# 构建query
query = CPP_LANGUAGE.query(cpp_query_text)
# 获取节点【root_node来源与Parser解析的代码文本】
root_node = xxx_parser.parse(bytes(code_snippet, 'utf8)).root_node
# capture: list[Node, str]
capture = query.captures(root_node)
for node, alias in capture: # node为代码节点,alias为自定义符号
print(node.type, alias)
所以可以这么写:
from tree_sitter import Language, Parser
# 声明CPP代码解析器
CPP_LANGUAGE = Language('build/my-languages.so', 'cpp')
cpp_parser = Parser()
cpp_parser.set_language(CPP_LANGUAGE)
cpp_code_snippet = '''
// 把上面cpp代码赋值粘贴到这里
'''
# 定义query
cpp_query_text = '''
(function_declarator declarator: (identifier)@1 )
(initializer_list) @2
( call_expression ) @3
(assignment_expression right:(_) @4)
(ERROR) @error
'''
query = CPP_LANGUAGE.query(cpp_query_text)
# 获取具体语法树
tree = cpp_parser.parse(bytes(cpp_code_snippet, "utf8"))
root_node = tree.root_node
# 获取节点
# capture: list[Node, str]
capture = query.captures(root_node)
for node, alias in capture:
print(node.type, alias)
后序
希望对大家有帮助。还有很多节点这里没做介绍,官网写得很详细。
希望大家多多支持🤗