






















在软件开发领域,理解和操作代码的结构至关重要。无论是语法高亮、代码导航,还是更高级的静态分析和代码重构,都离不开对代码的精确解析。Tree-sitter 正是为此而生的一个强大的工具,它是一个通用的解析器生成器和增量解析库,能够为各种编程语言构建具体的语法树,并在代码编辑过程中高效地更新这些语法树 。
Tree-sitter 的核心在于其能够以高效且可靠的方式理解代码结构。它主要通过以下几个关键功能实现这一点:
Tree-sitter 语法本质上是一个 grammar.js 文件,这个文件用 JavaScript 描述语言的语法规则。虽然语法规则是用 JavaScript 描述的,但这只是用来生成最终 C 程序的中间描述。编写完 grammar.js 文件之后,使用 tree-sitter generate [OPTIONS] [GRAMMAR_PATH] 命令即可生成对应语言的解析器。
💡
tree-sitter generate 读取当前工作目录中的 grammar.js 文件,并创建一个名为 src/parser.c 的文件,该文件实现了分析器。更改语法后,只需再次运行 tree-sitter generate。1
经过观察,在 src 目录下还有几个固定的文件,比如 grammar.json 等,在官方的文档中并没有找到明确的说明 grammar.json 来源,但是在一些 issue 中可以看到核心开发者的说明:
grammar.jsonis generated withtree-sittergenerate (along with everything insrcminusscanner.c)
所以,执行 tree-sitter generate 命令后,Tree-sitter 工具链会处理 grammar.js 文件,并在 src/ 目录下生成多个文件,包括 grammar.json 以及解析器的 C 代码。

所以,grammar.json 是从 grammar.js 文件自动生成的,生成过程如下:
grammar.js 中定义语言语法规则tree-sitter generate 时,Tree-sitter 工具会:grammar.js 文件src/grammar.json 文件对于 grammar.json ,因为其组织方式是 json,所以通过这个文件来学习对应语言的语法解析规则较于 grammar.js 是容易的。但是对于其存在的具体作用,下面是 AI 搜索给出的答案:
🤖
grammar.json 文件扮演着重要的角色:
grammar.json 的构成一个完整的grammar.json 通常包含一万行左右的规则定义,所以如果要全部理解不同 statement 的语法还是需要学习一段时间的。不过全部学习也不是必要的,因为不同 statement 的语法规则声明方式是类似的,所以可以掌握基本的定义方式,直接搜索需要处理的 statement 即可。下面通过 PHP 语言的 grammar.json 来分析具体的内容。

grammar.json 的结构和组织方式是通过 $schema 字段指向的 URL 规定的,JSON Schema 文件描述了 Tree-sitter 语法的结构和规则,下面是一个 grammar.json 应该包含的顶层键以及对应内容的类型:
$schema:string,表示 schema 的 URL。name:string,必须匹配正则表达式 ^[a-zA-Z_]\\w*,即以字母或下划线开头,后面可以跟字母、数字或下划线。inherits:string,同样需要匹配正则表达式 ^[a-zA-Z_]\\w*。rules:object,其键需要匹配正则表达式 ^[a-zA-Z_]\\w*$,值引用了 #/definitions/rule。extras:array,元素是 rule,且不允许重复。precedences:array,每个元素是另一个数组,包含唯一的字符串或 symbol-rule。reserved:object,键是规则名称,值是 rule 的数组。externals:array,元素是 rule。inline:array,元素是字符串,表示规则名称。conflicts:array,每个元素是一个字符串数组,表示可能冲突的规则名称。word:string,需要匹配正则表达式 ^[a-zA-Z_]\\w*。supertypes:array,元素是字符串,表示规则名称。整个 grammar.json 的核心内容便是 rules 字段,它定义了具体的语法核心规则,每个子键值对定义一个语法规则:
program、statement、for_statement、expression)。这些名称会成为抽象语法树(AST)中的节点类型,当 tree-sitter 解析代码时会生成这些节点。可以将它们视为语言中不同的语法结构。每个规则定义(rules 字典中的值)是一个 JSON 对象,并且必须包含一个 "type" 属性。这个 "type" 属性决定了正在定义的语法规则的类型,以及 tree-sitter 应该如何解析它。grammar.schema.json 中的 definitions 定义了全部类型的规则,它们是语法规则的基础构件:
blank-ruleCHOICE 规则中,使语法的某些部分可选。"type": "BLANK"。在 simple_parameter 中,参数的 type 是可选的,由 BLANK 选项表示。
string-rule "value""value": 需要精确匹配的字符串。"type": "STRING""value": 字符串值。在 _semicolon (分号,主要作用是标识语句的结束)规则中,";" 表示它会精确匹配 PHP 代码中的分号字符。
pattern-rule"value""value": 正则表达式模式。"flags""flags": 正则表达式标志(例如 "i" 表示不区分大小写匹配)"type": "PATTERN""value": 正则表达式。php_tag 规则使用正则表达式匹配 PHP 起始标签的各种变体(如 <?php、<?PHP、<?=、<?)。
symbol-rulerules 字段中定义的另一个规则的引用。它用于从简单规则构建更复杂的规则。"name""name": "name" 的值是另一个规则的名称,此规则引用该名称。"type": "SYMBOL""name": 符号名称。在此示例中,program 规则使用 SYMBOL 引用了 text 规则。在解析时,tree-sitter 会查看 text 规则的定义以理解 text 的组成。
seq-rule"members""members": 一个规则定义数组(可以是 SYMBOL、STRING、PATTERN 或其他规则类型)。这个数组中的每个元素都是一个子规则的定义。SEQ 规则要求输入文本必须按照 "members" 数组中子规则定义的顺序依次匹配。"type": "SEQ""members": 一个 rule 的数组。function_static_declaration 规则的 "type" 是 "SEQ",它定义了 PHP 中函数内部 static 变量声明的语法结构。
SEQ 的第一个成员确保了每个函数内的静态变量声明必须以 static 关键字开始。SEQ 的第二个成员(本身也是一个 SEQ)处理了静态变量声明的列表。它强制至少要有一个 static_variable_declaration,并且允许通过逗号分隔声明多个变量。这种嵌套的 SEQ 结构展示了如何用 SEQ 组合更复杂的序列模式。SEQ 的第三个成员确保了整个 static 变量声明语句以分号结束,符合 PHP 语句的语法规则。在这个例子中,第二行 static $count = 0; 和第三行 static $name = "example", $value; 都匹配 function_static_declaration 规则。
static $count = 0; 匹配 function_static_declaration,因为:static 关键字开始 (members[0])。static_variable_declaration ($count = 0) (members[1] 的内部 SEQ 的 members[0])。; 结束 (members[2])。static $name = "example", $value; 也匹配 function_static_declaration,因为:static 关键字开始 (members[0])。static_variable_declaration ($name = "example") (members[1] 的内部 SEQ 的 members[0])。, 和第二个 static_variable_declaration ($value), 这部分通过 members[1] 的内部 SEQ 的 members[1] (即 REPEAT 规则) 来处理。; 结束 (members[2])。choice-rule"members""members": 一个规则定义数组。"type": "CHOICE""members": 一个 rule 的数组。statement 规则可以是列出的任何语句类型(空语句、复合语句、if 语句等)。
alias-rule"content"、"value"、"named""content":需要重命名的规则。"value":希望在 AST 中为节点命名的新名称。"named":布尔值,决定了通过别名创建的节点是命名节点(named node)还是匿名节点(anonymous node),通常关键字为 false,命名节点为 true。"type": "ALIAS""value": 别名值。"named": 是否命名规则。"content": 引用的规则。💡
Tree-sitter 的语法树中存在两种主要的节点类型:
1. 命名节点 (named: true)
2. 匿名节点 (named: false)
区别:
named 字段决定了别名节点是作为语法树中的独立实体存在(named: true),还是仅作为父节点的一部分存在(named: false)。理解这一区别对于正确设计语法、编写查询以及处理语法树至关重要。
final_modifier 规则将关键字 "final" 别名为 AST 中名为 "final" 的节点。_class_const_declaration 规则将 _class_const_declaration 规则别名为 AST 中的 const_declaration 节点。
repeat-rule & repeat1-rule"REPEAT":匹配 content 规则零次或多次。"REPEAT1":匹配 content 规则一次或多次。"content""content": 要重复的规则定义。"type": "REPEAT" || "type": "REPEAT1""content": 引用的规则。在 program 规则中,在 php_tag 之后可以有零次或多次 statement。text 规则被定义为其内容至少重复一次。
token-ruletoken 规则的核心功能是将一个复杂的规则整体视为单一词法单元。它指示解析器将匹配的内容作为一个原子单元处理,而不是分解为更小的部分。当使用 token 方法时,我们创建了一个单一的文本标记。token 方法只接受终结规则(terminal rules),因此不能引用其他规则。
"content""content":定义标记的规则"type": 可以是 "TOKEN" 或 "IMMEDIATE_TOKEN"。"content": 引用的规则。integer 规则是一个 TOKEN,它可以是几种模式之一(十进制、八进制、十六进制、二进制)。
field-rule"name"、"content""name":希望赋予子节点的名称(例如 "condition"、"body"、"name")。"content":子节点的规则定义。"name": 字段名称。"type": "FIELD""content": 引用的规则。在 if_statement 规则中,parenthesized_expression 被命名为 "condition",紧随其后的 statement 被命名为 "body",使其在 AST 中清楚地表示每个部分的含义。
prec-rule 和优先级规则(PREC_LEFT、PREC_RIGHT、PREC_DYNAMIC)"PREC":设置一个通用的优先级级别。"PREC_LEFT":设置左结合性和优先级。"PREC_RIGHT":设置右结合性和优先级。"PREC_DYNAMIC":动态优先级,通常用于更复杂的情况。"value" 和 "content""value":优先级级别(数字越大优先级越高)。"content":优先级适用的规则。"type": 可以是 "PREC"、"PREC_LEFT"、"PREC_RIGHT" 或 "PREC_DYNAMIC"。"value": 优先级值,可以是整数或字符串。"content": 引用的规则。在 unary_op_expression 中,PREC_LEFT 使得像 -5 中的 - 这种一元运算符具有左结合性,优先级为 19。sequence_expression 中的 PREC 使用一个较低的值,使逗号运算符的优先级非常低。
下面通过 if_statement 的规则定义进行具体的示例讲解。
if_statement 定义可以看到 if_statement 规则的 "type" 是 "SEQ",这意味着一个 PHP 的 if 语句必须按照 "members" 数组中定义的顺序出现。接下来逐个分析 "members" 数组中的元素:
members[0]:ALIAS 规则,它将一个 PATTERN 规则别名为 "if"。PATTERN 规则匹配字符串 "if"(value: "if"),并且 flags: "i" 表示匹配是大小写不敏感的,所以可以匹配 if, If, IF 等。named: false 表示这个节点在抽象语法树中不会被明确命名,而是作为其父节点的匿名子节点。value: "if" 表示在 AST 中,这个节点会被标记为 "if" 类型。if_statement 必须以关键字 if 开头。members[1]:FIELD 规则,它给子规则命名为 "condition"。content 是一个 SYMBOL 规则,引用了另一个规则 "parenthesized_expression"。if 关键字之后,必须紧跟着一个用圆括号包裹的表达式,这个表达式会被解析为 "parenthesized_expression" 规则,并且在 AST 中被标记为 "condition" 字段,表示 if 语句的条件。members[2]:CHOICE 规则,表示在条件表达式之后,if_statement 可以有两种不同的结构。members 数组包含两个 SEQ 规则,分别对应了两种 if 语句的语法形式:让我们进一步看 第一个 SEQ 分支(使用花括号的代码块):
members[0]: FIELD 规则,命名为 "body",content 是 SYMBOL 规则 "statement"。members[1]: REPEAT 规则,表示重复零次或多次。content 是 FIELD 规则,命名为 "alternative",content 是 SYMBOL 规则 "else_if_clause"。members[2]: CHOICE 规则,表示选择。 members 数组包含两个规则:💡
SEQ 规则在 if_statement 中起着至关重要的作用,它定义了 if 语句的语法结构和各个组成部分的顺序:
SEQ 确保了 if 语句必须按照 if 关键字 -> 条件表达式 -> 主体代码块 (-> elseif 分支 -> else 分支) 的顺序出现。如果顺序不对,例如条件表达式在 if 关键字之前,解析器就会报错。SEQ 规则的结构,结合 FIELD 规则的命名,tree-sitter 可以正确地构建 if_statement 的 AST 节点,并且明确地标记出条件、主体、elseif 和 else 分支等各个部分,方便后续的语法分析和代码处理。这个 PHP 代码会成功匹配 if_statement 规则,因为它的结构符合 SEQ 规则定义的顺序:
if 关键字开始 (members[0]).($condition) (members[1]).{} 包裹的代码块 (members[2] 的第一个 SEQ 分支的 members[0]).elseif 分支 (members[2] 的第一个 SEQ 分支的 members[1]).else 分支 (members[2] 的第一个 SEQ 分支的 members[2]).py-tree-sitter(基础库)适用于需要直接控制和自定义语法的情况:
关键点:
Tree-sitter 语言的实现也提供预编译的二进制程序。(不知道 Python 以外的其他语言是否试用)
py-tree-sitter-languages优点:
tree-sitter-language-pack特点:
一般的解析步骤:
主要区别:
库名称 | 是否需要编译 | 语法更新方式 | 支持的语言数量 |
| 是 | 手动更新 | 任意语言 |
| 否 | 定期更新 | 50+ |
| 否 | 频繁更新 | 100+ |
TS-Visualizer 是一个支持本地和网页端的 AST 解析和可视化工具项目,通过 Tree-Sitter 的 WASM 支持在浏览器中解析源码,并使用 tree-sitter-langs 提供的语言解析器编译完成。

学习 Tree-sitter 最好的方法是实践,找一段简短的代码,解析出 AST,然后可以通过可视化查看 AST 中的节点及其字段中的具体内容,通过代码尝试便利某个节点的 children 等,看的多了就慢慢熟悉整个结构了~
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。