代码库图谱 · 用 tree-sitter 建一张符号索引

grok 用 tree-sitter 给五种语言建了一张作用域符号图(Bloop 风格),支撑跳转定义、查引用。这一章看它怎么解析、建什么节点与边、以及那个专为省内存设计的字符串驻留器。

作者 David更新于 第 30 篇(共 60 篇)

代码库图谱 · 用 tree-sitter 建一张符号索引

xai-codebase-graph 是一张 Bloop 风格的作用域符号图:用 tree-sitter 解析代码,建出「符号在哪定义、被谁引用」的索引,支撑编辑器级的跳转定义 / 查引用。这一章看它怎么建。

五种语言,靠 tree-sitter 查询

它支持 Rust / TypeScript / JavaScript / Go / Python 五种语言。每种语言一个 TSLanguageConfig,核心是一段 tree-sitter S 表达式查询,约定捕获名:@name.definition.<kind>(定义)、@name.reference.<kind>(引用),外加别名对。<kind>(function/struct/const……)对应命名空间,用于「引用→定义」的命名空间感知匹配。

LanguageRegistry 有个 compute_query_hash()——对所有语言的查询字符串算哈希。查询一改,哈希变,缓存自动失效重建。把「规则」纳入缓存版本是个好习惯:否则你改了 query 却读到旧索引,查出来的结果对不上。

两种图:每文件 vs 跨文件

  • ScopeGraph(每文件):一张 petgraph 图,节点是 Scope/Def/Import/Ref,边是 RefToDef/DefToScope……。插入引用时从最紧作用域往根走(ScopeStack),按名字 + 命名空间匹配定义。这是经典的作用域图解析。
  • ScopeGraphIndex(跨文件,真正的索引):扁平结构,definitions: 符号ID → [(路径ID, 行)]、references: ...、别名、文件元信息。这才是持久化、被查询的东西。

一个性能取舍:索引构建走快路径 extract_symbols_fast——跳过建图,直接返回定义/引用/别名列表(预计算好每个捕获是 def 还是 ref,避免热循环里反复比字符串,“快 2–3 倍”)。完整的 ScopeGraph 不持久化,需要时才重建。

字符串驻留器:为省内存而生

大仓库里符号名、路径重复度极高。StringInterner(interner.rs)是个内存优化的关键:一个字节 arena + offsets: Vec<(u32, u16)> + 用预哈希的 nohash_hasher 做查找,StringId(u32) 代替字符串到处传。定义/引用表里存的都是 StringId,行号用 u32(8 字节/条,而非 16)。7 月版源码的注释把目标写得很直白:把大仓库的索引从 ~1GB 压到 ~100MB(新代码删掉了这句注释,数据结构没变)。

构建流程:并行两阶段

IndexBuilder::build_fast:

  1. 收文件:先试 git2 拿 tracked 文件(快),不行再用 ignore 并行 walk(尊重 .gitignore),按支持的扩展名过滤。
  2. 阶段一(并行):rayon 线程池,par_chunks(100) 逐文件 process_file_fast——线程本地缓存 parser/query、5MB 大小闸、8KB 前缀查二进制、解析、抽符号。
  3. 阶段二(串行合并):归并进单个 ScopeGraphIndex(单一 interner)。按 5000 文件/批控制峰值内存。
  4. compact() 收缩。

持久化用自定义二进制格式(魔数 SGIX)——手写小端读写 interner arena、defs、refs、别名。识别不了魔数就当遗留、回退重建。

和《从 LLM 到 Coding Agent》对照

那本没有代码库图谱——它超出了「最小 agent」的范围。grok 这套(tree-sitter 多语言 + 作用域图 + 驻留器 + 并行构建 + 自定义序列化)是一个独立、完整的符号索引引擎。不过它有个出人意料的真相——它其实不喂给模型。下一章专门讲这件事。


上一篇:Git 集成 · 下一篇:图谱的真实用途

本页目录