0%

Tree-sitter 快速入门

Tree-sitter 是一个用于解析源代码的工具和库,核心作者是 Max Brunsfeld。它的核心目标是:快速、增量地把代码转换成语法树(Syntax Tree)。它现在被很多编辑器、IDE、代码分析工具使用,例如 Atom(最初主要使用者)、Zed、GitHub 的代码高亮/代码跳转等等。上篇文章介绍的 Bearer SAST 工具就依赖于 Tree-sitter。

Tree-sitter 简介

在 Tree-sitter 出现之前,大部分编辑器(比如老版本的 Vim)对代码进行语法高亮时,用的是正则表达式(Regex)。正则的缺点是:它只能一行一行、一段一段地去匹配字符串,根本不理解代码的 上下文结构Tree-sitter 则把源代码转成具体语法树(Concrete Syntax Tree,CST),并在编辑时增量更新,而不是全量重解析,适合:

  • 编辑器实时高亮、折叠、括号匹配
  • 代码导航(跳定义、找引用)
  • 轻量静态检查、格式化、AI 代码理解

Tree-sitter 的核心特性:

特性 说明
增量解析 代码修改后,只重新解析变更部分,而非整个文件
容错解析 语法错误不会导致整个解析失败,会生成部分 AST 并标记错误节点
多语言支持 已有 50+ 语言grammar,覆盖主流编程语言
高性能 C 语言实现,性能极快
无依赖性 运行时库(用纯C11编写)可以嵌入任何应用程序中
查询语言 提供类 S-expression 的模式匹配语言,精确检索 AST 结构

AST vs CST

AST(Abstract Syntax Tree,抽象语法树)和 CST(Concrete Syntax Tree,具体语法树,也叫 Parse Tree)是编译器、解释器以及代码分析工具中的两个核心概念。

通常编译器(如 GCC、Clang、Go 编译器)产出的 AST(Abstract Syntax Tree,抽象语法树)是抽象的:它只保留程序的语义结构,丢掉了括号、逗号、分号等 语法糖。这对编译优化是合理的,但对代码分析工具来说却是不行的。来看同一个函数,两种树的对比:

源码:

1
2
3
func add(a int, b int) int {
return a + b
}

AST(抽象语法树)只保留语义:注意:(),{} 这些语法符号都消失了。位置信息可能带也可能不带:

1
2
3
4
5
6
7
8
9
FunctionDecl
├── name: "add"
├── params:
│ ├── Param(name="a", type=int)
│ └── Param(name="b", type=int)
├── return_type: int
└── body:
└── ReturnStmt
└── BinaryOp(op="+", left=Ident("a"), right=Ident("b"))

而 Tree-sitter CST(具体语法树)则尽量保留一切信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(source_file
(function_declaration ← 命名节点:有语义
(identifier) ← "add"
(parameter_list ← 参数列表是一个完整的子树
( ← 匿名节点:左括号
(parameter_declaration
name: (identifier) ← "a"
type: (type_identifier)) ← "int"
, ← 匿名节点:逗号
(parameter_declaration
name: (identifier) ← "b"
type: (type_identifier)) ← "int"
)) ← 匿名节点:右括号
result: (type_identifier) ← "int"
body: (block
{ ← 匿名节点:左花括号
(return_statement
(binary_expression
left: (identifier) ← "a"
right: (identifier)) ← "b"
) ← return语句结束
}))) ← 匿名节点:右花括号
特性 传统 AST Tree-sitter CST
括号 ( ) { } 丢弃 保留为匿名节点
逗号 , 分号 ; 丢弃 保留为匿名节点
每个节点有位置信息 不一定 必定有(行号、列号、字节偏移)
语法错误 解析失败 容错:插入 ERROR/MISSING 节点,继续解析
注释 通常丢弃 保留为 extra 节点
精确还原源码 不能 (从 CST 可以完全还原原始源码)

为什么代码安全扫描工具需要 CST 而不是 AST?

  1. 精确位置标注:安全报告必须指出漏洞在第几行第几列,CST 每个节点都有位置;AST 可能丢失这个信息
  2. 注释中蕴含安全线索// TODO: fix SQL injection// nosemgrep// bearer:disable 等注释在 AST 中被丢弃,但在 CST 中完整保留
  3. 精确还原代码片段:安全报告需要展示漏洞代码片段,CST 通过 node.Content(source) 精确提取,AST 做不到
  4. 容错解析:被扫描的代码很可能有语法错误(开发者还没写完),AST 解析器直接失败,CST 尽可能继续并标记错误

Tree-sitter 实际使用

go-tree-sitter 是 tree-sitter C 库的 Go 语言封装,通过 CGO 调用 C 实现。Bearer 项目就是基于这个库构建了完整的 SAST 扫描引擎。注意,tree-sitter 官方也提供了 Go 封装,但是由于 Bearer 项目使用的是 smacker/go-tree-sitter 来快速感受一下 Tree-sitter

基础 API 速览

首先我们来理解其提供的一些基础概念和 API。

核心对象关系

  • Parser:解析器,需要设置 Language
  • Language:语法定义,每种语言一个(如 javascript.GetLanguage())
  • Tree:解析结果树,包含所有节点
  • Node:树中的单个节点
  • Query:预编译的模式匹配查询
  • QueryCursor:查询执行器,迭代匹配结果
  • TreeCursor:树遍历器,高效深度优先遍历

如下则展示了使用 go-tree-sitter 的核心流程:

1
2
3
4
5
6
7
8
9
10
11
parser := sitter.NewParser()
parser.SetLanguage(javascript.GetLanguage())

tree := parser.Parse(nil, []byte(sourceCode))
root := tree.RootNode()

// ... 操作 root ...


tree.Close() // 释放内存
parser.Close()

示例:获取 CST/AST 结构

接下来我们通过一个实际例子,通过解析 Go 代码,获取其 CST 结构,从直观上感受下 CST 结构是怎么样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package main

import (
"fmt"
"strings"

sitter "github.com/smacker/go-tree-sitter"
"github.com/smacker/go-tree-sitter/golang"
)

func main() {
sourceCode := `package main

import "fmt"

func add(a int, b int) int {
return a + b
}

func main() {
result := add(3, 5)
fmt.Println(result)
}
`

parser := sitter.NewParser()
parser.SetLanguage(golang.GetLanguage())

tree := parser.Parse(nil, []byte(sourceCode))

root := tree.RootNode()

fmt.Println("=== Go AST 示例 ===")
fmt.Println()
fmt.Printf("根节点类型: %s\n", root.Type())
fmt.Printf("根节点子节点数: %d\n", root.ChildCount())
fmt.Println()
fmt.Println("=== S-Expression 格式的 AST ===")
fmt.Println(root.String())
fmt.Println()
fmt.Println("=== 遍历顶层命名子节点 ===")
for i := uint32(0); i < root.NamedChildCount(); i++ {
child := root.NamedChild(int(i))
fmt.Printf(" [%d] type=%s, content=\"%s\"\n",
i, child.Type(), truncate(child.Content([]byte(sourceCode)), 40))
}
fmt.Println()
fmt.Println("=== 深入分析 func_declaration ===")
for i := uint32(0); i < root.NamedChildCount(); i++ {
child := root.NamedChild(int(i))
if child.Type() == "function_declaration" {
printFunctionDeclaration(child, []byte(sourceCode))
}
}

tree.Close()
parser.Close()
}

func printFunctionDeclaration(node *sitter.Node, source []byte) {
name := node.ChildByFieldName("name")
if name != nil {
fmt.Printf(" 函数名: %s\n", name.Content(source))
}

params := node.ChildByFieldName("parameters")
if params != nil {
fmt.Printf(" 参数列表: %s\n", params.Content(source))
fmt.Printf(" 参数节点子节点数: %d\n", params.ChildCount())
for j := uint32(0); j < params.ChildCount(); j++ {
p := params.Child(int(j))
fmt.Printf(" [%d] type=%s, content=\"%s\"\n",
j, p.Type(), p.Content(source))
}
}

returnType := node.ChildByFieldName("result")
if returnType != nil {
fmt.Printf(" 返回类型: %s\n", returnType.Content(source))
}

body := node.ChildByFieldName("body")
if body != nil {
fmt.Printf(" 函数体: %s\n", truncate(body.Content(source), 60))
fmt.Printf(" 函数体子节点数: %d\n", body.NamedChildCount())
}
fmt.Println()
}

func truncate(s string, maxLen int) string {
if len(s) > maxLen {
return s[:maxLen] + "..."
}
// Replace newlines for display
return strings.ReplaceAll(s, "\n", "\\n")
}

程序的运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
=== Go AST 示例 ===

根节点类型: source_file
根节点子节点数: 8

=== S-Expression 格式的 AST ===
(source_file (package_clause (package_identifier)) (import_declaration (import_spec path: (interpreted_string_literal))) (function_declaration name: (identifier) parameters: (parameter_list (parameter_declaration name: (identifier) type: (type_identifier)) (parameter_declaration name: (identifier) type: (type_identifier))) result: (type_identifier) body: (block (return_statement (expression_list (binary_expression left: (identifier) right: (identifier)))))) (function_declaration name: (identifier) parameters: (parameter_list) body: (block (short_var_declaration left: (expression_list (identifier)) right: (expression_list (call_expression function: (identifier) arguments: (argument_list (int_literal) (int_literal))))) (expression_statement (call_expression function: (selector_expression operand: (identifier) field: (field_identifier)) arguments: (argument_list (identifier)))))))

=== 遍历顶层命名子节点 ===
[0] type=package_clause, content="package main"
[1] type=import_declaration, content="import "fmt""
[2] type=function_declaration, content="func add(a int, b int) int {
return a +..."
[3] type=function_declaration, content="func main() {
result := add(3, 5)
fmt...."

=== 深入分析 func_declaration ===
函数名: add
参数列表: (a int, b int)
参数节点子节点数: 5
[0] type=(, content="("
[1] type=parameter_declaration, content="a int"
[2] type=,, content=","
[3] type=parameter_declaration, content="b int"
[4] type=), content=")"
返回类型: int
函数体: {\n return a + b\n}
函数体子节点数: 1

函数名: main
参数列表: ()
参数节点子节点数: 2
[0] type=(, content="("
[1] type=), content=")"
函数体: {\n result := add(3, 5)\n fmt.Println(result)\n}
函数体子节点数: 2
  • tree-sitter 的 String() 方法输出的是类 S-Expression 格式,自动省略了匿名节点(括号、逗号等)以减少视觉噪声。但通过 ChildCount()Child(i) 遍历时,匿名节点是存在的。这正是 CST 比 AST 的地方
  • ChildCount() 包含匿名节点,NamedChildCount() 只算命名节点
  • ChildByFieldName() 通过语义字段名精确获取子节点(如 namebody
  • node.Content(source) 从原始源码中提取节点对应的文本片段
  • 参数列表的匿名子节点是 (,),命名子节点是 parameter_declaration

S-Expression

上文说过,Tree-sitter 可以用类 S-Expression 来表示。S-Expression(全称 Symbolic Expression,中文常译为 S-表达式)是一种用于表示树状结构数据的文本格式。它最初由计算机科学家 John McCarthy 在 1950 年代为 Lisp 编程语言设计。虽然它历史悠久,但由于其结构极其简单且表现力强,至今仍活跃在许多现代技术领域中。

S-表达式的语法规则非常简单,主要由两部分组成:

  • 原子(Atom):最小的数据单元。可以是数字(如 42, 3.14)、字符串(如 “hello”)或符号/标识符(如 x, +, print)
  • 列表(List):用圆括号 () 包裹、由空格分隔的元素集合。列表中的元素可以是原子,也可以是另一个列表(嵌套列表)
1
2
3
4
5
; 这是一个简单的列表,包含三个原子
(apple banana orange)

; 这是一个嵌套列表
(fruits (apple banana) (orange grape))

在 Lisp 家族的编程语言中,S-Expression 不仅用来表示数据,还用来表示程序代码。S-Expression 的第一个元素通常是操作符/函数,后面的元素是参数(即前缀表示法)。例如对于数据表达式 $2 + 3 \times 4$,其 S-Expression 写法对应于:

1
(+ 2 (* 3 4))

Tree-sitter 对传统 S-表达式 进行了扩展,使用 : 来表示字段名(Field Name)。标准的 S-表达式(如 Lisp 中使用的)只关心元素的顺序和嵌套,不包含键值对的概念。但在解析复杂的编程语言源代码时,一个语法节点可能会包含很多子节点。为了更精确地定位和区分这些子节点的角色,Tree-sitter 引入了 field_name: (node)。这就像在标准的树状结构上打上了标签(Label),让程序(或人类)在遍历这棵树时,能一眼看出这个子节点扮演的是什么角色

  • 语法结构: (父节点 字段名: (子节点))
  • 例如对于 import_spec path: (interpreted_string_literal))
    • import_spec 是父节点(导入规范)
    • path字段名(Field),明确告诉你后面紧跟的这个字符串是导入的 路径,而不是其他东西
    • (interpreted_string_literal) 是子节点(解释型字符串字面量)

Query 语法

基础语法

Tree-sitter Query(树查询)是一套基于 S-表达式 的模式匹配语法,用于在 CST(具体语法树)中筛选、定位、提取指定语法节点,广泛用于代码高亮、语法检查、代码分析、代码片段提取等场景。

查询语句由一个或多个模式(Pattern) 组成,每个模式都是标准 S-表达式,作用是匹配语法树中的节点及层级关系。基础格式:

1
(节点类型 [子节点模式...])
  • 外层括号包裹目标节点
  • 括号内第一个值为节点类型,节点类型都是预定义好的,例如 call_expression、`
  • 后续可选拼接子节点的匹配表达式,用于约束子节点结构

例如:

1
2
3
4
5
; 匹配一个二元表达式,且它的两个子节点都必须是数字字面量
(binary_expression (number_literal) (number_literal))

; 匹配任意一个包含字符串字面量子节点的二元表达式(其余子节点是什么无所谓)
(binary_expression (string_literal))

为了让模式更精确,可以在子节点前加上字段名和冒号 :

1
2
; 匹配一个赋值表达式,其左边(left)必须是一个成员表达式,且该成员表达式的对象(object)必须是一个调用表达式
(assignment_expression left: (member_expression object: (call_expression)))

如果你想限制某个节点绝对不能包含某个字段,可以在字段名前面加上感叹号 !

1
2
; 匹配一个类声明,要求它有名字(name),但【绝对不能有】类型参数(!type_parameters)
(class_declaration name: (identifier) @class_name !type_parameters)

上文提到的括号语法只适用于 命名节点(如变量、表达式)。对于代码中的字面量符号、操作符(即匿名节点),需要使用双引号 "" 包裹起来匹配。

1
2
; 匹配一个二元表达式,要求它的操作符必须是 "!=",且右边(right)必须是 null
(binary_expression operator: "!=" right: (null))

还有一些特殊节点:

  • 通配符(Wildcard _):类似于正则表达式中的点号 .,用来匹配任意节点
    • (_):匹配任意命名节点(Named Node)。
    • _:匹配任何节点
1
2
; 匹配一个函数调用(call)内部的任意命名节点,并将其捕获为 call.inner
(call (_) @call.inner)
  • 错误节点(ERROR):当解析器遇到无法识别的代码时会生成 (ERROR) 节点。它也可以被直接查询,非常适合用来写语法检查工具。
1
(ERROR) @error-node
  • 缺失节点(MISSING):当代码漏掉了某些符号(例如漏了分号),Tree-sitter 能够自动修复并在树中插入一个长度为 0 的虚拟节点。这些节点可以用 (MISSING) 捕获。
1
2
3
(MISSING) @missing-node
(MISSING identifier) @missing-identifier ; 捕获缺失的标识符
(MISSING ";") @missing-semicolon ; 捕获缺失的分号

运算符(Operators)

Tree-sitter 查询中支持各类运算符,包含节点捕获、量词、分组、多选、锚点五大类核心能力,用于拓展模式匹配的规则与范围。

在编写匹配规则时,你通常需要对匹配到的特定节点做后续处理。捕获(Capture)可以为模式中的节点绑定自定义名称,后续就能通过该名称引用对应的节点。捕获名称写在目标节点之后,以符号 @ 开头

1
2
3
4
5
该规则匹配「将函数赋值给标识符」的语法结构,并把左侧标识符捕获为 the-function-name:

(assignment_expression
left: (identifier) @the-function-name
right: (function))

可以使用后置量词 +、*、? 匹配连续的同级重复节点,用法与正则表达式中的同名量词高度一致。

量词 作用 等价规则
+ 匹配 1 个及以上 重复的目标模式 至少出现一次
* 匹配 0 个及以上 重复的目标模式 可选,可出现多次
? 标记模式为可选 0 个 或 1 个

可以使用圆括号将一组同级节点打包为一个整体,实现多节点连续匹配

1
2
3
4
5
6
匹配「一条注释紧跟一个函数声明」的组合结构:

(
(comment)
(function_declaration)
)

使用方括号 [] 定义多选分支,括号内编写多个互斥的匹配模式,只要命中其中任意一个分支,即判定匹配成功。逻辑等价于正则中的 字符集 [] 或分支 |

1
2
3
4
5
6
7
8
匹配函数调用语句,函数主体可以是「普通标识符」或「对象属性」,并分别做不同捕获:

(call_expression
function: [
(identifier) @function
(member_expression
property: (property_identifier) @method)
])

锚点运算符为英文句点 .,用于约束子节点的匹配位置与相邻关系。锚点会忽略语法树中的 匿名节点(标点、空格、括号等),仅针对命名节点生效。根据 . 在规则中的位置,分为三种使用场景:

  • 场景 1:锚点放在第一个子节点前,匹配「父节点的第一个命名子节点」
1
(父节点 . (子节点))
  • 场景 2:锚点放在最后一个子节点后,匹配「父节点的最后一个命名子节点」
1
(父节点 (子节点) .)
  • 场景 3:锚点放在两个子节点之间,仅匹配「相邻同级节点」
1
(父节点 节点A . 节点B)

谓词(Predicates)和指令(Directives)

你可以在查询模式的任意位置添加谓词 S-表达式,以此为匹配规则自定义附加元数据与判断条件。格式规范如下:

  • # 开头、? 结尾
  • 表达式内部可传入任意数量 @捕获名普通字符串 作为参数
  • 作用:对已匹配到的节点做二次过滤,不满足条件则舍弃当前匹配结果。

Tree-sitter 支持以下几类谓词:

  • 等值系列:eq? 家族,用于对捕获节点的文本内容做精确字符串比对
1
2
3
4
5
6
(
(pair
key: (property_identifier) @key-name
value: (identifier) @value-name)
(#eq? @key-name @value-name)
)
  • 正则系列:match? 家族,使用正则表达式匹配节点文本
  • 多值匹配断言:any-of?:判断捕获节点的文本是否等于给定字符串列表中的任意一个,适用于批量匹配多个固定关键字、内置变量等场景
  • 属性断言:is? / is-not?,用于判断捕获节点是否具备特定内置属性

指令(Directives)与谓词作用相似,都可以为查询模式附加元数据与处理逻辑,核心区别:

  • 断言:以 ? 结尾,作用是条件过滤(不满足则放弃匹配);
  • 指令:以 ! 结尾,作用是附加操作/元数据(不改变匹配结果,仅做后置处理)

Tree-sitter CLI 默认支持以下三类指令

  • set!:为当前匹配规则绑定键值对格式的自定义元数据,后续代码可读取该元数据,常用于语法注入、语法分类等场景。
  • select-adjacent!:用于过滤捕获内容,仅保留与另一个指定捕获节点相邻的节点
  • strip!:基于正则表达式移除捕获节点中的指定文本,不会改变语法树结构,仅修改最终提取的文本内容

Query 实战

接下来我们仍然通过一个实际例子来加深对 Query 的理解。示例代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package main

import (
"fmt"
"strings"

sitter "github.com/smacker/go-tree-sitter"
"github.com/smacker/go-tree-sitter/javascript"
)

func main() {
sourceCode := `class UserService {
constructor(apiUrl) {
this.apiUrl = apiUrl;
this.token = null;
}

async login(username, password) {
const response = await fetch(this.apiUrl + "/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password })
});
this.token = response.token;
return this.token;
}

getUser(id) {
return fetch(this.apiUrl + "/users/" + id, {
headers: { Authorization: "Bearer " + this.token }
});
}
}
`

parser := sitter.NewParser()
parser.SetLanguage(javascript.GetLanguage())

tree := parser.Parse(nil, []byte(sourceCode))

root := tree.RootNode()

fmt.Println("=== JavaScript Query 示例 ===")
fmt.Println()
fmt.Println("--- 查找所有类声明 ---")
runQuery(root, []byte(sourceCode), []byte(`(class_declaration name: (identifier) @class_name)`), javascript.GetLanguage())

fmt.Println()
fmt.Println("--- 查找所有方法定义 ---")
runQuery(root, []byte(sourceCode), []byte(`(method_definition name: (property_identifier) @method_name) @method`), javascript.GetLanguage())

fmt.Println()
fmt.Println("--- 查找所有 async 方法 ---")
runQuery(root, []byte(sourceCode), []byte(`(method_definition "async" name: (property_identifier) @async_method_name)`), javascript.GetLanguage())

fmt.Println()
fmt.Println("--- 查找所有 fetch 调用 ---")
runQuery(root, []byte(sourceCode), []byte(`(call_expression function: (identifier) @func_name (#eq? @func_name "fetch")) @fetch_call`), javascript.GetLanguage())

fmt.Println()
fmt.Println("--- 查找所有成员属性访问 (this.xxx) ---")
runQuery(root, []byte(sourceCode), []byte(`(member_expression object: (this) @this_obj property: (property_identifier) @prop_name) @this_access`), javascript.GetLanguage())

tree.Close()
parser.Close()
}

func runQuery(root *sitter.Node, source []byte, queryStr []byte, lang *sitter.Language) {
q, err := sitter.NewQuery(queryStr, lang)
if err != nil {
fmt.Printf(" 查询错误: %s\n", err)
return
}
defer q.Close()

qc := sitter.NewQueryCursor()
defer qc.Close()
qc.Exec(q, root)

for {
m, ok := qc.NextMatch()
if !ok {
break
}

captures := make(map[string]string)
for _, c := range m.Captures {
name := q.CaptureNameForId(c.Index)
captures[name] = c.Node.Content(source)
}

var parts []string
for name, content := range captures {
parts = append(parts, fmt.Sprintf("%s=\"%s\"", name, truncate(content, 50)))
}
fmt.Printf(" 匹配 %d: %s\n", m.PatternIndex, strings.Join(parts, ", "))
}
}

func truncate(s string, maxLen int) string {
s = strings.ReplaceAll(s, "\n", "\\n")
if len(s) > maxLen {
return s[:maxLen] + "..."
}
return s
}

程序运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
=== JavaScript Query 示例 ===

--- 查找所有类声明 ---
匹配 0: class_name="UserService"

--- 查找所有方法定义 ---
匹配 0: method="constructor(apiUrl) {\n this.apiUrl = apiUrl;\n...", method_name="constructor"
匹配 0: method="async login(username, password) {\n const respo...", method_name="login"
匹配 0: method="getUser(id) {\n return fetch(this.apiUrl + "/us...", method_name="getUser"

--- 查找所有 async 方法 ---
匹配 0: async_method_name="login"

--- 查找所有 fetch 调用 ---
匹配 0: func_name="fetch", fetch_call="fetch(this.apiUrl + "/login", {\n method: "PO..."
匹配 0: fetch_call="fetch(this.apiUrl + "/users/" + id, {\n heade...", func_name="fetch"

--- 查找所有成员属性访问 (this.xxx) ---
匹配 0: this_access="this.apiUrl", this_obj="this", prop_name="apiUrl"
匹配 0: this_obj="this", prop_name="token", this_access="this.token"
匹配 0: this_access="this.apiUrl", this_obj="this", prop_name="apiUrl"
匹配 0: this_access="this.token", this_obj="this", prop_name="token"
匹配 0: this_access="this.token", this_obj="this", prop_name="token"
匹配 0: this_access="this.apiUrl", this_obj="this", prop_name="apiUrl"
匹配 0: this_access="this.token", this_obj="this", prop_name="token"

小结

这篇文章我们学习了 tree-sitter 的基本用法,Bearer 项目基于 go-tree-sitter 构建了完整的 SAST 引擎,学习 tree-sitter 的基本用法,可以帮助我们后续理解 Bearer 的实现原理打下基础。