代码图谱构建与解析

本页讲图谱的两个 pass:Build(收集声明节点)与 addEdges(解析关系边),以及承重原语 Resolve(跟随引用到真实声明)。源码 packages/ttsc/internal/graph/build.goedges.goresolve.gograph.go

两 pass 构建

Buildbuild.go:16)先遍历每个用户源文件 collectDeclarations 建节点,再 addEdges 解析关系。driver.SourceFiles 已去掉声明文件、且 program 从不编译依赖的 .ts,所以 Build emit 的每个节点都是 workspace 源(External=false);external 边界 leaf 只作为边的解析目标进图(见下)。

pass 1:收集声明

collectStatementsbuild.go:57)按节点 kind 建节点:

AST kind节点 kind额外
FunctionDeclarationfunction
ClassDeclarationclass+ collectMembers(方法节点)
InterfaceDeclarationinterface+ collectMembers
TypeAliasDeclarationtype
EnumDeclarationenum
VariableStatementvariable每个 binding 一个
ModuleDeclaration(namespace)—(容器)递归 body

namespace 自身是分组容器,不建节点;它的成员是各自的声明,递归进 body 收集(build.go:74)。成员 ID 从符号构建(符号已在父链里带 enclosing namespace),所以这里记的节点与稍后解析的边目标无需 walk 线程化 namespace 名就能一致。

方法节点(NodeMethodgraph.go:13)的 ID 是 class 限定的(path#Class.method:method),让解析出的方法调用落到 build pass 记的同一节点。

pass 2:解析边

addEdgesedges.go:15)对每个源文件再遍历一次,收集五类:

  • markExports:经 checker 的导出表解析哪些节点是模块导出表面的一部分——re-export(export { Foo } from)或 barrel(export *)都算,不只 inline export modifier。这是公共 API 投影过滤的信号。
  • collectHeritage:class/interface 的 extends/implements 基类,经 checker 解析(解包 barrel)。
  • collectCalls:value-call 边(调用、new T()<Component/> JSX、tagged-template)。
  • collectTypeRefs:type-ref 边(参数、返回、属性、别名提到的具名类型)。
  • collectDecorators:装饰器(语法捕获,dump-only 元数据,让消费者解释 @Controller/@Get 约定)。

边去重

addEdgeAtedges.go:35)按 wire kind(不是内部 kind)键控去重:from\x00to\x00wireEdgeKind(kind, origin)。这样同一目标的两种用法(一个 call 和一个 new、同一基类的 extendsimplements)都保留,而同一形式的重复用法塌成一条边。seen map 让 O(1) 查找,N 条边 O(N) 构建而非 O(N²)(graph.go:113)。

Resolve:承重原语

Resolveresolve.go:43)跟随一个引用到 checker 绑定的真实声明:

func Resolve(checker *shimchecker.Checker, ref *shimast.Node) *Target {
  symbol := checker.GetSymbolAtLocation(ref)
  if symbol == nil { return nil }
  if symbol.Flags&shimast.SymbolFlagsAlias != 0 {
    if aliased := shimchecker.Checker_getAliasedSymbol(checker, symbol); aliased != nil {
      symbol = aliased
    }
  }
  // ... 分类声明住哪 (external?)
}

两步关键:

  1. alias 解包:符号带 SymbolFlagsAlias 时(经 barrel re-export)调 Checker_getAliasedSymbol 解包到真正声明的兄弟源。停在 GetSymbolAtLocation 会落到本地 import alias、在 index 文件切断边、塌回 tree-sitter 质量。
  2. external 分类:声明文件是 .d.ts 或路径含 /node_modules/External=trueresolve.go:59)。checker 因 preserveSymlinks 默认 false 解析 symlink 到 realpath,所以 pnpm workspace:* 兄弟解析到真实源、不被误判为 external(resolve.go:69 注释)。

Targetresolve.go:30)返回解析符号、声明源文件、是否 external、以及位置。返回 nil 表示 checker 绑不了符号(数字字面量、标点 token、未解析名)。

external leaf 的物化

当一条边的目标住在 node_modules 或 .d.ts,graph 物化一个 external 边界 leaf 节点作为边目标,但把依赖的内部拉进图(edges.go:14 注释)。这让图的边界停在外部类型上——你能看到"用了 lodash 的某函数"这条边,但不会把 lodash 内部铺进图。

SourceTexts:evidence 输入

SourceTextsbuild.go:31)把每个源文件映射到文本——NewDump 把节点/边的 byte span 转成 line/column 的 evidence 输入。这就是 MCP 工具能给出 sourceSpan 锚点的来源。

不变量

  • 节点 ID 位置无关(path#name:kind),编辑不 re-key。
  • 边去重按 wire kind,区分形式但塌重复。
  • workspace 节点 External=false;external 只作边目标 leaf 进图。
  • Resolve 解包 alias 链,barrel re-export 落到真实声明。

失败模式

情形行为
引用绑不到符号Resolve 返回 nil,无边
符号无声明(intrinsic/合成)declarationFile 返回 nil
循环 / 自引用边去重 + 跳过自边(collectCalls 逻辑)
symlink 兄弟(pnpm)realpath 解析,不误判 external

接下来