GitNexus PDG Query 完全指南:用 cdg_query/REACHING_DEF 的 control-flows 两层依赖回答"什么守护了什么、值流向了哪里"
【免费下载链接】GitNexusGitNexus: The Zero-Server Code Intelligence Engine - GitNexus is a client-side knowledge graph creator that runs entirely in your browser. Drop in a git repository (Github, Gitlab, Azure, Local) or ZIP file, and get an interactive knowledge graph with a built in Graph RAG Agent. Perfect for code exploration项目地址: https://gitcode.com/GitHub_Trending/gi/GitNexus
本指南以 GitNexus 技能文档 gitnexus-pdg-query.md 为核心,系统讲解pdg_query这个 MCP 工具的完整使用面:它背后的分层程序依赖图(CFG / REACHING_DEF / CDG)、controls与flows两种查询模式、guard-clause 发现方法与正确的 Cypher 写法,以及"必须锚定 + 必须限流"等足以影响查询结果正确性的关键陷阱。读完你将能解释任何一条pdg_query结果(包括空结果),并能读懂、扩展它的底层读取链路(_pdgQueryImpl/ CDG / REACHING_DEF)。
一句话背景:pdg_query是"控制/数据依赖"视角的只读探针
GitNexus 把每个仓库索引成一张以 LadybugDB 持久化的代码关系图。除了面向全局的impact()遍历和面向污点流的explain,它还暴露了一个以基本块(BasicBlock)为粒度、完全限定在单个函数内的依赖查询工具:pdg_query。它只回答两类问题:
- "这条语句在什么条件下才会执行?"(guard 谓词 / 控制依赖,读 CDG 边);
- "这个变量在函数内部流向了哪里?"(def→use 数据依赖,读 REACHING_DEF 边)。
它不是通用图搜索,也不是污点分析——pdg_query是explain(污点消费方)的"控制/数据依赖对偶",二者共享同一套持久化基座(见 tools.ts 中pdg_query的工具描述:"The control/data analog of explain")。
先决条件:PDG 层是--pdg的 opt-in 层
pdg_query运行的这张图与污点分析(taint)共用同一张图,但所有依赖层都是可选的,由gitnexus analyze --pdg显式开启:
L1 CFG 每函数的 basic blocks + 控制流边 L2 REACHING_DEF GEN/KILL 风格的 def→use 数据依赖(纯求解器) L5 CDG Ferrante 控制依赖(基于 post-dominators)在默认的gitnexus analyze(不带--pdg)下,仓库索引产物与开启 PDG 的产物字节级一致——意思是默认运行不会记录上述任何一层。在 CLI 配置上,--pdg是一个布尔开关(见 analyze-config.ts 与 analyze-options.ts),由run-analyze消费并最终把每函数的依赖上限写入RepoMeta.pdg元数据戳(见下文"no PDG layer"探测)。
因此使用pdg_query前,先确认仓库是用 PDG 层索引的:
gitnexus analyze --repo <path> --pdg否则工具不会报错,而是返回一个携带说明性 note 的空结果(详见下文"空结果语义")。
三种边的存储形态:同一个 CodeRelation 表,靠type区分
从底层存储看,CFG / REACHING_DEF / CDG 三类边不是三张不同的表,而是同一张CodeRelation关系表中的行,靠type列区分。实现证据在 emit.ts:
CFG边(第 408–409 行附近写入);REACHING_DEF边(第 613–617 行附近,type: 'REACHING_DEF');CDG边(第 768–772 行附近,type: 'CDG')。
这三类边全部是BasicBlock → BasicBlock方向的边,并且图上不存在Function → BasicBlock边。这一点直接影响如何把"某个函数"解析成"一组基本块",也是后续所有对齐/行号陷阱的根源。
pdg_query的两种模式与参数契约
工具定义与 JSON Schema 位于 tools.ts 中pdg_query条目,参数契约如下:
| 参数 | 必填 | 说明 |
|---|---|---|
mode | 是 | 'controls'(CDG)或'flows'(REACHING_DEF),枚举校验 |
target | 是 | 文件路径(支持后缀匹配)或符号/函数名(按context()同款规则解析);pdg_query没有无锚定模式 |
variable | 否 | 仅flows模式有效:把 REACHING_DEF 结果过滤到某一个绑定名 |
limit | 否 | 返回边数上限,默认 50、最大 200(源码常量见 tools.tsPDG_QUERY_DEFAULT_LIMIT/PDG_QUERY_MAX_LIMIT);越界返回参数错误而非静默截断 |
repo | 否 | 仓库名或路径,单仓库可省略 |
该工具被列入只读工具策略(见 read-only-policy.ts),是典型的"看代码不写代码"探针。
controls 模式:什么在守护什么
{ "mode": "controls", "target": "src/workers/scheduler.ts" }对锚定的函数,返回每条控制依赖边:controller(控制谓词所在行)、dependent(被控块的起始行与文本)、以及边方向label——'T'表示谓词的true/taken 分支,'F'表示false/fall-through 分支。当目标依赖块是一个早退(early return / throw / continue / break)块时,额外标记guard: true。
flows 模式:变量在函数内流向哪里
{ "mode": "flows", "target": "validateUser", "variable": "input" }返回 REACHING_DEF 的 def→use 边:每条边给出variable、def(定义行)与use(使用行 + 使用处文本)。传variable可以把结果收敛到单个绑定,不传则返回该函数内的全部 def→use 边。
注意variable过滤在底层走的是 REACHING_DEF 边的reason字段匹配,而不是对变量做名字解析——所以最好传与索引中一致的源码级标识符。
守卫子句发现与"修正版"Cypher
技能文档特别强调了一个易踩的坑:早期的 RFC 草案(RFC #567 §2)中形如[:CDG {label:'F'}]的 Cypher并不能按原样运行。因为前面说过,CDG 边是CodeRelation表type属性取值为'CDG'的行,而分支方向存在reason字段里,不存在label列。要在原生cypher里手动找 guard 子句,应使用修正版:
MATCH (pred:BasicBlock)-[r:CodeRelation {type: 'CDG'}]->(dep:BasicBlock) WHERE dep.text STARTS WITH 'return' OR dep.text STARTS WITH 'throw' RETURN pred.startLine, r.reason AS branch, dep.startLine, dep.textr.reason给出的就是谓词到达该早退点所走的分支方向。关键教训是:不要硬编码某一种方向。以if (!ok) return;为例,return 语句走的是谓词的true 臂('T'),而被保护的主体则走false 臂('F')——polarity 取决于守卫怎么写,所以按方向过滤守卫子句一定会漏。
如果走pdg_query工具本身,则不需要手写这条 Cypher:controls模式会自动把进入return/throw/continue/break依赖块的边标上guard: true(对应底层实现见_pdgQueryImpl中的守卫正则/^\s*(return|throw|continue|break)\b/,源码位置在 local-backend.ts 的 controls 结果组装段)。
底层执行流程:从参数到结果的完整链路
理解_pdgQueryImpl的代码顺序(local-backend.ts 从约 4925 行开始),就能理解工具为什么是"有界且诚实"的:
- 模式校验:虽然 JSON Schema 里有 enum,但后端仍强制检查
mode,防止不合法的值穿透到查询组装。 - limit 校验:必须是
[1, 200]的整数,否则返回参数错误。limit会被直接插值进 Cypher(LadybugDB 不支持参数化LIMIT),因此先做白名单校验是安全边界。 - 元数据探针(no-layer 短路):调用共享的
pdgStampForMode,检查RepoMeta.pdg上的每函数上限戳(CDG 看maxCdgEdgesPerFunction,REACHING_DEF 看maxReachingDefEdgesPerFunction)。该戳由 run-analyze.ts 在--pdg索引时写入,结构定义在 repo-meta.ts。若戳明确缺失(false),直接返回"no PDG layer"note,不做一次数据库扫描——这是廉价探测的关键。 - 锚定解析:调用共享的
resolveBlockAnchor(见下文),把target变成一组基本块的匹配条件。target必填,所以这里要么命中、要么返回 not-found / ambiguous 早退分支。 - 构造匹配子句并双查询:
MATCH (a:BasicBlock)-[r:CodeRelation]->(b:BasicBlock) WHERE r.type = '<CDG|REACHING_DEF>' AND <anchorClause>[ AND reason过滤],并行执行分页查询(带LIMIT)与COUNT(*)总数查询。edgeType是硬编码字面量,target/variable只走绑定参数,没有 Cypher 字符串拼接注入面。 - 空结果兜底:如果总数是 0 且元数据不可读(
undefined),再做一次单条边类型探测区分"该锚点确实无边"与"整个层不存在",据此选择"状态未知"note。 - 结果组装:
controls模式输出 controller/dependent/label(即reason的分支方向)+ 可选guard: true;flows模式用decodeReachingDefReason把编码过的 reason 解码成纯变量名,输出def/use。
返回结构统一带anchor、total,当total > results.length时置truncated: true,提示结果页被裁剪。
flows 模式背后的 REACHING_DEF reason 编码(FU-B-2)
flows结果里的variable不是现成的列,而是从边reason字段解码出来的。编码器在 reaching-def-reason-codec.ts:早期形态就是裸变量名(legacy),新形态是带注解的"<name>|1:<defLine>:<useLine>[;<defLine>:<useLine>...]"——名字在最前、|分隔、1:是 codec 版本,后面跟;分隔的 def/use 行号对。解码是"永不抛错、退化为裸名"的防御式设计:遇到非字符串、无注解的裸名或畸形注解,都返回{ name, pairs: [] },消费方退回到基本块粒度。
这解释了工具实现中的一个细节:variable过滤条件写的是r.reason = $variable OR r.reason STARTS WITH $variablePrefix(前缀为"<name>|")。由于源码标识符不可能含|,<name>|前缀是精确的,不会误配更长的同名前缀,从而同时兼容 legacy 裸名与 FU-B-2 注解两种形态。
锚定原理:没有 Function→BasicBlock 边,怎么把"函数"解析成基本块
这是整个pdg_query最容易出 bug 的地方,技能文档称之为"最承重的陷阱"之一。由于图中没有Function → BasicBlock边,代码通过两块信息重建 join:
- id 前缀匹配:BasicBlock 节点 id 形如
BasicBlock:<filePath>:<fnStartLine>:<fnCol>:<blockIdx>(文件路径中可能含:,所以解析时从右侧切分)。 - 行号窗口匹配:基本块
startLine是1-based,而符号(Symbol)节点的startLine/endLine是0-based。因此锚定时上下界都 +1,把窗口移动成[symStart+1, symEnd+1]:- 上界
+1:把落在函数最后一行的 guard / def / use 保留在窗口内; - 下界
+1:把紧贴函数上一行的相邻函数的块排除在外。
- 上界
这段逻辑就是 local-backend.ts 的resolveBlockAnchor(约 4497 行起),它被explain、pdg_query、impact三个工具共享。三种锚定形态:
- 看起来像文件路径:
target走文件锚定——a.id STARTS WITH 'BasicBlock:<file>:'(精确)或a.filePath = target(完整路径)或a.filePath ENDS WITH '/<file>'(后缀匹配); - 符号名命中:先
resolveSymbolCandidates,有可用 span 就用"id 前缀 + 1-based 行窗口"; - 符号无可用 span:降级为只按文件级 id 前缀过滤(实现里有注释说明这一降级是刻意的)。
同名的符号会返回ambiguous候选列表(含totalCandidates与candidatesTruncated提示),找不到则返回not-found。同一行内塞多个函数、嵌套函数等极端排布下,锚定只能"粗略对齐"——这是文档明确承认的边界。
那些承重的 Gotchas(决定结果正确性的细节)
技能文档把下面几条列为"load-bearing",读结果前务必逐条对照:
- 必须锚定 + 必须 LIMIT 限流。LadybugDB没有关系属性(rel-property)索引,一条不带锚点的
[:CDG*]/[:REACHING_DEF*]路径扫描是无界的。pdg_query强制要求target并对分页限流;如果你直接裸调cypher,必须自己用文件 id 前缀或符号 span 做锚定。锚点本身就是"界"——这是架构选择,不是疏漏。 - BasicBlock ↔ 符号的 join 是"重建"的:没有
Function→BasicBlock边,全靠 id 前缀 + 行号窗口(含上面说的 1-based / 0-based+1偏移)。 - 没有 PDG 层 ⇒ 返回 note,而不是错误。工具返回
{ results: [], note: "no PDG layer — run gitnexus analyze --pdg …" },靠对RepoMeta.pdg上限戳的廉价探测判定,绝不做全库扫描。元数据不可读 + 锚定结果为空时,返回的是措辞更审慎的"状态未知"note——因为一个"全线性函数、天然无边"的仓库,其边缘形态与"缺层"在数据上无法区分,实现因此拒绝断言(可参见_pdgQueryImpl中的设计注释)。 - CDG 标签在 M5/M6 是二值的。所有
switch的 case 臂都被记为'T',每个 case 内部更细的条件分支尚未区分——所以别指望用controls区分同一个 switch 内不同 case 的谓词条件。 - 仅限函数内(intra-procedural)。跨函数流动属于污点分析
explain的地盘,不要用pdg_query推导跨函数调用链。
"Mirror, don't fork":为什么_pdgQueryImpl应该只复用不重写
_pdgQueryImpl在结构上是_explainImpl的前半段镜像:WAL 包装、元数据 no-layer 探测、limit 校验、resolveSymbolCandidates锚定解析等全部共享,只是把读的边从TAINTED换成CDG/REACHING_DEF,并且完全没有污点那一套路径编解码与跨函数TAINT_PATH机制。注释中明确要求:扩展或审查这条读取路径时,复用这些共享 helper(pdgStampForMode、resolveBlockAnchor、decodeReachingDefReason、fnLineOf等),不要重新实现。
顺着这条镜像关系还能找到它更广的上下文:
explain(local-backend.ts 约 4591 行)消费同一批 CDG/REACHING_DEF 层的兄弟数据——污点 TAINTED 边,做 source→sink 查询,两者都要求--pdg索引;impact的mode: "pdg"(见 pdg-impact.ts,以及 eval-server.ts 对mode:'pdg'的处理)把 CDG + REACHING_DEF 进一步聚合成语句级affectedStatements切片;无层/降级结果会给出 "UNKNOWN risk" note,而不会静默给出空 blast radius;- CLI 的
--mode pdg --line <N>(见 index.ts 帮助文本)是这条能力的命令行走道。
实战复盘:一条完整的排查路径
把上面所有机制串起来,一次典型的"这条语句被什么守护"排查长这样:
- 用
gitnexus analyze --repo <path> --pdg建索引(确认RepoMeta.pdg两个上限戳写入); - 对代码里好奇的语句所在函数调用
pdg_query({ mode: "controls", target: "<函数名或文件路径>" }); - 如果空结果:先看
note是否为no PDG layer…——是则说明没开--pdg;若 note 是"状态未知"且仓库确实是--pdg索引,那大概率是锚点本身无边(函数全线性、无分支),这属于"edge-free 层"的正常表现; - 若结果里出现
guard: true的边且 dependent 是return/throw,它就对应源码里某个 early-return 守卫;对照controller.line与label('T'/'F')即可还原"谓词走哪个臂到达早退"; - 想知道某个变量在函数内的去向,换
pdg_query({ mode: "flows", target, variable }),对照每条def.line → use.line的配对; - 跨函数的流动不要在这里找,改用
explain(taint)或impact --mode pdg(语句级切片)。
参考源码位置速查
- 技能文档本身:gitnexus-pdg-query.md
pdg_queryMCP 工具定义与 limit 常量:tools.ts_pdgQueryImpl/pdgQuery/resolveBlockAnchor核心实现:local-backend.ts- CFG / REACHING_DEF / CDG 三类边的落盘写入:emit.ts
- REACHING_DEF reason 编解码(FU-B-2 注解):reaching-def-reason-codec.ts
- PDG 元数据戳写入与结构:run-analyze.ts、repo-meta.ts
--pdg开关配置:analyze-config.ts、analyze-options.tsmode:"pdg"的语句级影响切片:pdg-impact.ts、eval-server.ts
【免费下载链接】GitNexusGitNexus: The Zero-Server Code Intelligence Engine - GitNexus is a client-side knowledge graph creator that runs entirely in your browser. Drop in a git repository (Github, Gitlab, Azure, Local) or ZIP file, and get an interactive knowledge graph with a built in Graph RAG Agent. Perfect for code exploration项目地址: https://gitcode.com/GitHub_Trending/gi/GitNexus
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考