GitNexus PDG Query 完全指南:用 cdg_query/REACHING_DEF 的 control-flows 两层依赖回答“什么守护了什么、值流向了哪里“
2026/9/10 19:37:05 网站建设 项目流程

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)、controlsflows两种查询模式、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_queryexplain(污点消费方)的"控制/数据依赖对偶",二者共享同一套持久化基座(见 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没有无锚定模式
variableflows模式有效:把 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 边:每条边给出variabledef(定义行)与use(使用行 + 使用处文本)。传variable可以把结果收敛到单个绑定,不传则返回该函数内的全部 def→use 边。

注意variable过滤在底层走的是 REACHING_DEF 边的reason字段匹配,而不是对变量做名字解析——所以最好传与索引中一致的源码级标识符。

守卫子句发现与"修正版"Cypher

技能文档特别强调了一个易踩的坑:早期的 RFC 草案(RFC #567 §2)中形如[:CDG {label:'F'}]的 Cypher并不能按原样运行。因为前面说过,CDG 边是CodeRelationtype属性取值为'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.text

r.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 行开始),就能理解工具为什么是"有界且诚实"的:

  1. 模式校验:虽然 JSON Schema 里有 enum,但后端仍强制检查mode,防止不合法的值穿透到查询组装。
  2. limit 校验:必须是[1, 200]的整数,否则返回参数错误。limit会被直接插值进 Cypher(LadybugDB 不支持参数化LIMIT),因此先做白名单校验是安全边界。
  3. 元数据探针(no-layer 短路):调用共享的pdgStampForMode,检查RepoMeta.pdg上的每函数上限戳(CDG 看maxCdgEdgesPerFunction,REACHING_DEF 看maxReachingDefEdgesPerFunction)。该戳由 run-analyze.ts 在--pdg索引时写入,结构定义在 repo-meta.ts。若戳明确缺失(false),直接返回"no PDG layer"note,不做一次数据库扫描——这是廉价探测的关键。
  4. 锚定解析:调用共享的resolveBlockAnchor(见下文),把target变成一组基本块的匹配条件。target必填,所以这里要么命中、要么返回 not-found / ambiguous 早退分支。
  5. 构造匹配子句并双查询MATCH (a:BasicBlock)-[r:CodeRelation]->(b:BasicBlock) WHERE r.type = '<CDG|REACHING_DEF>' AND <anchorClause>[ AND reason过滤],并行执行分页查询(带LIMIT)与COUNT(*)总数查询。edgeType是硬编码字面量,target/variable只走绑定参数,没有 Cypher 字符串拼接注入面。
  6. 空结果兜底:如果总数是 0 且元数据不可读(undefined),再做一次单条边类型探测区分"该锚点确实无边"与"整个层不存在",据此选择"状态未知"note。
  7. 结果组装controls模式输出 controller/dependent/label(即reason的分支方向)+ 可选guard: trueflows模式用decodeReachingDefReason把编码过的 reason 解码成纯变量名,输出def/use

返回结构统一带anchortotal,当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:

  1. id 前缀匹配:BasicBlock 节点 id 形如BasicBlock:<filePath>:<fnStartLine>:<fnCol>:<blockIdx>(文件路径中可能含:,所以解析时从右侧切分)。
  2. 行号窗口匹配:基本块startLine1-based,而符号(Symbol)节点的startLine/endLine0-based。因此锚定时上下界都 +1,把窗口移动成[symStart+1, symEnd+1]
    • 上界+1:把落在函数最后一行的 guard / def / use 保留在窗口内;
    • 下界+1:把紧贴函数上一行的相邻函数的块排除在外。

这段逻辑就是 local-backend.ts 的resolveBlockAnchor(约 4497 行起),它被explainpdg_queryimpact三个工具共享。三种锚定形态:

  • 看起来像文件路径target走文件锚定——a.id STARTS WITH 'BasicBlock:<file>:'(精确)或a.filePath = target(完整路径)或a.filePath ENDS WITH '/<file>'(后缀匹配);
  • 符号名命中:先resolveSymbolCandidates,有可用 span 就用"id 前缀 + 1-based 行窗口";
  • 符号无可用 span:降级为只按文件级 id 前缀过滤(实现里有注释说明这一降级是刻意的)。

同名的符号会返回ambiguous候选列表(含totalCandidatescandidatesTruncated提示),找不到则返回not-found。同一行内塞多个函数、嵌套函数等极端排布下,锚定只能"粗略对齐"——这是文档明确承认的边界。

那些承重的 Gotchas(决定结果正确性的细节)

技能文档把下面几条列为"load-bearing",读结果前务必逐条对照:

  1. 必须锚定 + 必须 LIMIT 限流。LadybugDB没有关系属性(rel-property)索引,一条不带锚点的[:CDG*]/[:REACHING_DEF*]路径扫描是无界的。pdg_query强制要求target并对分页限流;如果你直接裸调cypher必须自己用文件 id 前缀或符号 span 做锚定。锚点本身就是"界"——这是架构选择,不是疏漏。
  2. BasicBlock ↔ 符号的 join 是"重建"的:没有Function→BasicBlock边,全靠 id 前缀 + 行号窗口(含上面说的 1-based / 0-based+1偏移)。
  3. 没有 PDG 层 ⇒ 返回 note,而不是错误。工具返回{ results: [], note: "no PDG layer — run gitnexus analyze --pdg …" },靠对RepoMeta.pdg上限戳的廉价探测判定,绝不做全库扫描。元数据不可读 + 锚定结果为空时,返回的是措辞更审慎的"状态未知"note——因为一个"全线性函数、天然无边"的仓库,其边缘形态与"缺层"在数据上无法区分,实现因此拒绝断言(可参见_pdgQueryImpl中的设计注释)。
  4. CDG 标签在 M5/M6 是二值的。所有switch的 case 臂都被记为'T',每个 case 内部更细的条件分支尚未区分——所以别指望用controls区分同一个 switch 内不同 case 的谓词条件。
  5. 仅限函数内(intra-procedural)。跨函数流动属于污点分析explain的地盘,不要用pdg_query推导跨函数调用链。

"Mirror, don't fork":为什么_pdgQueryImpl应该只复用不重写

_pdgQueryImpl在结构上是_explainImpl前半段镜像:WAL 包装、元数据 no-layer 探测、limit 校验、resolveSymbolCandidates锚定解析等全部共享,只是把读的边从TAINTED换成CDG/REACHING_DEF,并且完全没有污点那一套路径编解码与跨函数TAINT_PATH机制。注释中明确要求:扩展或审查这条读取路径时,复用这些共享 helper(pdgStampForModeresolveBlockAnchordecodeReachingDefReasonfnLineOf等),不要重新实现

顺着这条镜像关系还能找到它更广的上下文:

  • explain(local-backend.ts 约 4591 行)消费同一批 CDG/REACHING_DEF 层的兄弟数据——污点 TAINTED 边,做 source→sink 查询,两者都要求--pdg索引;
  • impactmode: "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 帮助文本)是这条能力的命令行走道。

实战复盘:一条完整的排查路径

把上面所有机制串起来,一次典型的"这条语句被什么守护"排查长这样:

  1. gitnexus analyze --repo <path> --pdg建索引(确认RepoMeta.pdg两个上限戳写入);
  2. 对代码里好奇的语句所在函数调用pdg_query({ mode: "controls", target: "<函数名或文件路径>" })
  3. 如果空结果:先看note是否为no PDG layer…——是则说明没开--pdg;若 note 是"状态未知"且仓库确实是--pdg索引,那大概率是锚点本身无边(函数全线性、无分支),这属于"edge-free 层"的正常表现;
  4. 若结果里出现guard: true的边且 dependent 是return/throw,它就对应源码里某个 early-return 守卫;对照controller.linelabel'T'/'F')即可还原"谓词走哪个臂到达早退";
  5. 想知道某个变量在函数内的去向,换pdg_query({ mode: "flows", target, variable }),对照每条def.line → use.line的配对;
  6. 跨函数的流动不要在这里找,改用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.ts
  • mode:"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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询