第 4 章 · 03 codeindex 与 tree-sitter 代码语义索引


文档摘要

第 4 章 · 03 codeindex 与 tree-sitter 代码语义索引 本节摘要:本节精读 codeindex 工具。codeindex 是 Reasonix 的"轻量内置代码符号索引",提供两种动作: (列出文件/目录下的符号)与 (按名字找符号定义候选)。它最值得讲的设计是分语言解析策略:Go 文件用标准库 原生 AST(零依赖、最准);JavaScript/TypeScript(含 TSX)/Python/Rust 用 tree-sitter(纯 cgo 绑定,可选编译);其它语言退化为文本解析。

第 4 章 · 03 codeindex 与 tree-sitter 代码语义索引

本节摘要:本节精读 codeindex 工具。codeindex 是 Reasonix 的"轻量内置代码符号索引",提供两种动作:outline(列出文件/目录下的符号)与 search(按名字找符号定义候选)。它最值得讲的设计是分语言解析策略:Go 文件用标准库 go/parser 原生 AST(零依赖、最准);JavaScript/TypeScript(含 TSX)/Python/Rust 用 tree-sitter(纯 cgo 绑定,可选编译);其它语言退化为文本解析。本节讲清这套分层解析、tree-sitter 的增量解析/错误恢复/多语言统一 API 三大原理、@func.name/@func.symbol 这种 query capture 语法、语义搜索 vs 文本 grep 的根本区别,以及"为何选 tree-sitter"。

内容来源:原项目源码 internal/tool/builtin/codeindex.gointernal/tool/builtin/codeindex_treesitter.go(含五语言 query)、internal/tool/builtin/codeindex_treesitter_stub.godocs/TOOL_CONTRACT.md

⚠️ 注意:tree-sitter 是 cgo 绑定,通过 //go:build treesitter && cgo 构建标签控制。默认编译不带 tree-sitter(满足 SPEC §1.2 CGO_ENABLED=0 单静态二进制);需要时用 -tags treesitter 编译。默认构建下,codeindex 对 JS/TS/Python/Rust 退化为文本解析,只有 Go 仍走原生 AST。

学习目标

阅读完本节,你应当能够:

  1. 说清 codeindex 的两种动作(outline/search)与适用场景。
  2. 描述 Go/JS/TS/Python/Rust/其它语言各自的解析策略。
  3. 解释 tree-sitter 的增量解析、错误恢复、多语言统一 API 三大原理。
  4. 读懂 @func.name/@func.symbol 这种 query capture 语法。
  5. 区分"语义搜索"(codeindex)与"文本 grep"(grep 工具)。
  6. 解释 tree-sitter 为何走可选 cgo 构建标签。

一、codeindex 工具定位

code_index 工具的 Description 写得很谦虚(internal/tool/builtin/codeindex.go:24-25):

Lightweight built-in code symbol index. Prefer lsp_* for language semantics and installed code graph MCP tools for call graph, impact, and architecture relationships; use this as the local fallback for file outlines and symbol definition candidates, then verify with read_file or grep.

要点:

  • 定位:轻量内置代码符号索引,本地兜底。
  • 推荐优先级:LSP(lsp_*)> 代码图 MCP 工具 > codeindex > read_file/grep。
  • 核心动作:文件大纲 + 符号定义候选搜索。
  • 使用建议:codeindex 找到候选后,用 read_filegrep 验证。

不是全功能 LSP,不提供调用图/影响分析/架构关系——那是 LSP 与专门 MCP 工具的活。codeindex 解决"这个文件有哪些函数/类""某个函数名定义在哪"这类基础符号问题,且无需外部 LSP server。

Schema 两个字段(codeindex.go:31-34):

{"action":"outline|search", "path":".", "query":"符号名", "kind":"func|method|class|..."}

outline 列出 path 下所有符号;search 按 query 子串找定义候选,可选用 kind 过滤。

二、分语言解析策略

codeindex.go:188-197 是解析路由:

if filepath.Ext(path) == ".go" { return c.parseGo(path) } if treeSymbols, ok, err := c.parseTreeSitter(path); ok && err == nil { textSymbols, _ := c.parseText(path) return mergeCodeSymbols(treeSymbols, textSymbols), nil } return c.parseText(path)

三种策略:

  1. Go:parseGo——用标准库 go/parser + go/ast(codeindex.go:17-19 的 import)。零外部依赖,解析最准(Go 语法固定,标准库 AST 是权威)。
  2. JS/JSX/TS/TSX/Python/Rust:parseTreeSitter——用 tree-sitter。若编译未带 tree-sitter(stub 返回 false),回退到文本解析。
  3. 其它语言(.java/.kt/.cs/.c/.cc/.cpp/.h/.hpp 等,见 codeindex.go:366):parseText——纯正则文本解析,精度较低。

tree-sitter 路径还会合并文本解析结果(mergeCodeSymbols(treeSymbols, textSymbols)),用文本解析补 tree-sitter 可能漏掉的符号——双保险。

为什么 Go 单独走原生 AST?三个原因:

  • 零依赖:Go 标准库自带,不引入 cgo,默认编译可用。
  • 最准:Go 语法固定,标准库 AST 是语言权威,不会有 tree-sitter grammar 滞后问题。
  • 工具链友好:Reasonix 自己是 Go 项目,开发者最熟悉 Go AST,维护成本低。

三、tree-sitter:五语言语法树

internal/tool/builtin/codeindex_treesitter.go:1-17 是构建标签与 import:

//go:build treesitter && cgo package builtin import ( "github.com/tree-sitter/go-tree-sitter" tree_sitter_javascript "github.com/tree-sitter/tree-sitter-javascript/bindings/go" tree_sitter_python "github.com/tree-sitter/tree-sitter-python/bindings/go" tree_sitter_rust "github.com/tree-sitter/tree-sitter-rust/bindings/go" tree_sitter_typescript "github.com/tree-sitter/tree-sitter-typescript/bindings/go" )

注意第一行 //go:build treesitter && cgo——只有同时满足 treesitter 标签和 cgo 启用才编译。对应的 stub(codeindex_treesitter_stub.go:1-3):

//go:build !treesitter || !cgo package builtin func (c codeIndex) parseTreeSitter(path string) ([]codeSymbol, bool, error) { return nil, false, nil }

stub 版直接返回 false(不可用)。所以默认 CGO_ENABLED=0 构建里,JS/TS/Python/Rust 退化为文本解析;只有显式 -tags treesitter 且开启 cgo 才启用 tree-sitter。这是 SPEC §1.2(单静态二进制)与 §1.6(演进不过度工程)的平衡——核心二进制保持纯 Go,需要语义索引的用户可自选编译。

codeIndexTreeSitterSpecForExt(codeindex_treesitter.go:111-125)按扩展名选 language 与 query:

扩展名 language query
.js/.jsx tree_sitter_javascript.Language treeSitterJavaScriptQuery
.ts tree_sitter_typescript.LanguageTypescript treeSitterTypeScriptQuery
.tsx tree_sitter_typescript.LanguageTSX treeSitterTypeScriptQuery
.py tree_sitter_python.Language treeSitterPythonQuery
.rs tree_sitter_rust.Language treeSitterRustQuery

注意 Go 不在表里(Go 走原生 AST)。

四、tree-sitter query capture 语法

tree-sitter 用一种声明式 query 语言描述"我要从 AST 里抓什么"。codeindex_treesitter.go:172- 给出 JavaScript query:

const treeSitterJavaScriptQuery = ` (function_declaration name: (identifier) @func.name) @func.symbol (generator_function_declaration ...

解读:

  • function_declaration 是 AST 节点类型(JS 函数声明)。
  • name: (identifier) @func.name——把函数声明里的 name 字段(一个 identifier 节点)捕获为 func.name(kind=func,role=name)。
  • @func.symbol——整个 function_declaration 节点捕获为 func.symbol(kind=func,role=symbol)。

splitCodeIndexTreeSitterCapture(codeindex_treesitter.go:128-138)解析 capture 名字:

func splitCodeIndexTreeSitterCapture(name string) (kind, role string, ok bool) { before, after, found := strings.Cut(name, ".") if !found || before == "" || after == "" { return "", "", false } if after != "name" && after != "symbol" { return "", "", false } return before, after, true }

capture 名是 <kind>.<role> 格式,kind 是符号类型(func/method/class/...),role 是 name(名字节点)或 symbol(符号节点本身)。一个完整符号需要 name + symbol 配对:name 节点提供符号名字,symbol 节点提供位置/父节点/签名。codeindex_treesitter.go:56-99 的主循环把配对的 capture 组装成 codeSymbol{Name, Kind, File, Line, Parent, Signature}

treeSitterParentName(codeindex_treesitter.go:140-154)专门处理 method 的父类——沿 AST 向上找最近的 class_declaration/class,取其 name 作为 method 的 Parent。这是 tree-sitter 灵活性的体现:语义关系(方法属于哪个类)通过 AST 结构推断,而非硬编码。

五、tree-sitter 三大原理

为什么选 tree-sitter?三大原理:

1. 增量解析(incremental parsing)。tree-sitter 能在源码改动后,只重新解析受影响子树,复用旧树的大部分。这在编辑器场景极其重要(每次按键只更新一小块)。Reasonix 的 codeindex 虽然每次全量解析,但 tree-sitter 的设计天然支持未来扩展成增量索引。

2. 错误恢复(error recovery)。tree-sitter 对语法错误的源码也能产出部分 AST——它在错误点插入 ERROR 节点,继续解析后续。这意味着 codeindex 能处理半成品代码、有语法错误的文件,而不是一错全错。这对真实代码库(常有正在编辑的半成品文件)至关重要。

3. 多语言统一 API。tree-sitter 为每种语言提供 grammar,C 侧 API 统一。Go 绑定 github.com/tree-sitter/go-tree-sitter 提供统一的 Parser/Tree/Node/Query/QueryCursor。codeindex 用同一套代码逻辑处理 JS/TS/Python/Rust,只是 language 与 query 不同——codeindex_treesitter.go:24-104parseTreeSitter 对所有语言通用。

解析主流程(codeindex_treesitter.go:33-48):

language := sitter.NewLanguage(spec.language()) parser := sitter.NewParser() defer parser.Close() if err := parser.SetLanguage(language); err != nil { ... } tree := parser.Parse(source, nil) defer tree.Close() query, qerr := sitter.NewQuery(language, spec.query) defer query.Close()

注意 parser.Parse(source, nil) 的第二个参数是 oldTree——传 nil 全量解析,传旧树增量解析。cursor.SetTimeoutMicros(50_000) 设 50ms 超时防止病态 query 卡死,cursor.DidExceedMatchLimit() 检查是否超限——又是"演进不过度工程"的防护。

六、语义搜索 vs 文本 grep

codeindex(语义)与 grep 工具(文本)的根本区别:

维度 codeindex(语义) grep(文本)
单位 符号(函数/类/变量定义) 文本行
理解 理解代码结构(AST) 只匹配字符
精度 高(只返回真正的符号定义) 低(注释/字符串里的同名文本也匹配)
速度 慢(要解析 AST) 快(ripgrep 直接扫)
适用 "找 FooBar 这个函数定义在哪" "找所有引用 FooBar 的行"

举例:搜 parseGo,grep 会返回所有出现 parseGo 的行(定义、调用、注释、字符串),codeindex 只返回函数定义那行。反过来,找"哪些文件调用了 parseGo"是 grep 的活,codeindex 做不到(那需要调用图,是 LSP/MCP 的活)。

Reasonix 的工具分工:codeindex 找定义,grep 找引用,read_file 读内容,bash 跑命令。TOOL_CONTRACT.md 里 bash 的描述明确(docs/TOOL_CONTRACT.md bash 行):

To search/read/list/edit/move files, prefer the dedicated tools (grep, read_file, ls, glob, edit_file, move_file) over shell grep/cat/ls/find/sed/mv — they behave identically on every OS. For symbol search or architecture questions, prefer LSP/read tools and targeted grep before shell commands.

工具各司其职,优先用专用工具而非 shell——因为专用工具跨平台一致、可被权限/沙箱精细管控、输出形状可预测(对 prefix cache 友好)。

💡 契约要点:codeindex 体现了 Reasonix"演进不过度工程"的哲学。它不追求成为全功能 LSP,只做"本地兜底的符号索引",复杂场景让位给 LSP/MCP。tree-sitter 走可选 cgo,核心二进制保持纯 Go。每个决策都有明确的边界与退路。

本节要点回顾

  1. codeindex 定位:轻量内置符号索引,本地兜底;优先级 LSP > 代码图 MCP > codeindex > read_file/grep;两个动作 outline/search
  2. 分语言解析:Go 走标准库 go/parser(零依赖最准);JS/JSX/TS/TSX/Python/Rust 走 tree-sitter;其它语言退化为文本解析;tree-sitter 结果与文本结果合并。
  3. tree-sitter 可选编译://go:build treesitter && cgo 标签;默认 CGO_ENABLED=0 不带,JS/TS/Python/Rust 退化为文本;-tags treesitter 启用。
  4. query capture 语法:@<kind>.<role>(role=name|symbol);name 提供名字,symbol 提供位置/父节点/签名;method 父类沿 AST 向上找 class。
  5. tree-sitter 三大原理:增量解析(parser.Parse(source, oldTree))、错误恢复(部分 AST)、多语言统一 API;50ms 超时 + match limit 防护。
  6. 语义 vs 文本:codeindex 找符号定义(理解结构),grep 找文本行(匹配字符);工具各司其职,优先专用工具而非 shell,跨平台一致且对 prefix cache 友好。

至此第 4 章结束。Provider 与 Tool 双接口、两层注册表、内置工具的实现细节(bash shell 解析、codeindex tree-sitter)都已讲清。下一章我们进入 Agent——internal/agent 的 Session 生命周期、harness loop 主循环、permission Policy 决策、checkpoint 检查点回放,看这些接口与工具如何被组装成一个能跑的 coding agent。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U