提交前确认 / Preflight
版本 / Version
deepx 0.2.106
安装方式 / Install method
直接下载 release 二进制手动运行 / downloaded release binary, ran directly
操作系统 / OS
Windows
环境细节 / Environment detail
No response
问题描述 / What happened
现象
对大型 Rust 文件执行 TagTree 时,输出异常稀少——definition tag 只剩文件里第一个定义,其余(函数/结构体/枚举/impl)全部丢失,reference tag 也为 0。
实测对象:CodeWhale 项目 crates/cli/src/lib.rs(8739 行,317 KiB,无超长行、无生成标记):
| 输入 |
期望 |
v0.19.1 实际 |
| 整文件 |
数百个 definition |
1 个(ProviderArg @32,文件第一个定义) |
| 截断到 797 行 |
正常 |
21 个 ✅ |
截断到 798 行(加一行闭合 },) |
正常 |
1 个 ❌ |
| 706-799 行片段单独解析 |
≥2 个(start_lane/emit_control_receipt) |
0 个 ❌ |
| 最小片段(pub fn + struct + impl) |
3 个 |
3 个 ✅(基线正常) |
定位:与文件体量无关,是特定语法树形态触发
1. 排除项(均实测排除)
| 候选原因 |
结论 |
| 单文件解析超时 |
❌ 排除:解析仅 467ms(< 1s 超时);截断法同样触发 |
| 文件 >1 MiB |
❌ 排除:317 KiB < 1 MiB |
| 超长行(>40 KiB) |
❌ 排除:最长行 243 字符 |
| 自动生成源码(>128 KiB + 生成标记) |
❌ 排除:无生成标记 |
| 文件数/字节预算截断 |
❌ 排除:706-799 段单独解析也触发 |
2. 按行截断的符号数(二分定位,全部同一文件前缀)
| 截断行 |
definition 数 |
| 50 |
0 |
| 200 |
4 |
| 500 |
10 |
| 600 |
16 |
| 700 |
19 |
| 795 |
21 |
| 796 |
21 |
| 797 |
21 |
| 798 |
1 ← 翻转点 |
| 800 / 1000 / 2000 / 4000 / 8739 |
1 |
翻转点:797→798 行。797 行是 None => println!("{}", serde_json::to_string_pretty(receipt)?),,798 行是闭合嵌套 match 的 },。
最小触发构造(已验证,来自 lib.rs 779-800 行)
if json {
match receipt.operation {
codewhale_lane::ControlOperation::LaneList => {
println!("{}", serde_json::to_string_pretty(&receipt.lane_records)?);
}
codewhale_lane::ControlOperation::LaneStatus => match receipt.lane_records.first() {
Some(record) => println!("{}", serde_json::to_string_pretty(record)?),
None if receipt.is_error() => {}
None => println!("{}", serde_json::to_string_pretty(receipt)?),
}, // ← 加上这一行闭合内层 match 后触发
_ => println!("{}", serde_json::to_string_pretty(receipt)?),
}
}
结构特征(三者同时出现时触发,单独/两两未验证):
- 嵌套 match(match 分支内再 match)
- match guard(
None if receipt.is_error() => {})
? 操作符在 println! 宏参数内
注意:该片段单独解析(不带前文)输出 0 个 definition,但截断到 guard 前一行输出正常——证明与代码总量无关,是完整语法树形态(闭合嵌套 match)触发。
复现步骤 / Steps to reproduce
复现步骤
- 准备含上述构造的 Rust 源码(≥798 行,或用触发片段)
tree := NewParser(lang).Parse(src)
tags := NewTagger(lang, ResolveTagsQuery(entry)).TagTree(tree)
- 统计
strings.HasPrefix(tag.Kind, "definition.") 数量
截断法(稳定复现):对触发文件按行截断重跑,观察"闭合嵌套 match 的括号行"加入前后,definition 数从 N 骤变为 1。
期望行为
TagTree 应输出源码中全部定义的 tag,不因嵌套 match / guard / 宏内 ? 等合法语法而丢失任何 definition。
附加验证(修复对照)
| 版本 |
706-799 段 |
整文件 |
run_cli 定义 |
| v0.19.1 |
0 |
1 |
❌ 缺失 |
| v0.48.1 |
1 |
301 |
✅ 可抽出 |
升级到 v0.48.1 后症状消失(未向 v0.48.1 提交最小独立复现,但同一文件、同一调用链输出正常)。若维护者需要最小独立复现文件,可基于上述结构构造并配合截断法确认。
附录 A:复现测试代码(内联,可直接运行)
以下为 deepx codegraph 包内的复现测试(Go)。触发段内联在测试里,无需任何外部文件/路径,go test 直接运行。触发段为 CodeWhale crates/cli/src/lib.rs 706-799 行原文(start_lane 完整函数 + emit_control_receipt 截断部分);tree-sitter 语法解析不校验符号引用,可直接作为输入。注释中反引号已改为单引号(Go raw string 限制,不影响语法解析)。
package codegraph
import (
"testing"
)
// TestMinimalRepro:复现 gotreesitter v0.19.1 TagTree 对 Rust 符号缺失。
func TestMinimalRepro(t *testing.T) {
trigger := `fn start_lane(request: LaneStartRequest) -> Result<()> {
use codewhale_lane::{
LaneRegistry, LaneStartSpec, RuntimeBackendKind, WorktreeProvision, resolve_backend,
};
let LaneStartRequest {
workflow, fleet, issue, goal, runtime, worktree_repo, branch,
worktree_path, worktree_ttl_secs, command, environment, cwd,
} = request;
let kind = RuntimeBackendKind::parse(&runtime)?;
let reg = LaneRegistry::open_default()?;
let mut record = reg.create_pending(workflow, fleet, issue, goal, kind, worktree_ttl_secs)?;
let worktree = match (worktree_repo, branch) {
(Some(repo_root), Some(branch_name)) => {
let path = worktree_path
.unwrap_or_else(|| repo_root.join(".codewhale").join("lanes").join(&record.id));
Some(WorktreeProvision { repo_root, branch: branch_name, path, base_ref: None })
}
(None, None) => None,
_ => bail!("--worktree-repo and --branch must be provided together"),
};
let cmd = if command.is_empty() {
vec!["sh".into(), "-c".into(), format!("echo lane {} started", record.id)]
} else {
command
};
let spec = LaneStartSpec {
command: cmd, cwd, environment,
log_proxy: (kind == RuntimeBackendKind::Tmux)
.then(std::env::current_exe)
.transpose()
.context("resolve current Codewhale executable for tmux log proxy")?,
worktree,
};
let backend = resolve_backend(kind);
backend.start(®, &mut record, &spec)?;
println!("started {}", record.id);
println!("status: {}", record.status.as_str());
println!("runtime: {}", record.runtime.as_str());
println!("log: {}", record.log_path.display());
if let Some(attach) = backend.attach_command(&record) {
println!("attach: {attach}");
}
Ok(())
}
/// Print one shared control receipt on the CLI surface.
///
/// The CLI does not format Lane control results itself: it renders the same
/// [codewhale_lane::ControlReceipt] the slash command and hotbar render, so
/// the three surfaces cannot drift in what they report (#1888).
fn emit_control_receipt(receipt: &codewhale_lane::ControlReceipt, json: bool) -> Result<()> {
if json {
// v0.9.2 compatibility: 'lane list --json' has always emitted an array
// of 'LaneRecord', and 'lane status --json' a single one. Scripts
// select '.[].id', '.worktree_path', '.log_path' off that shape.
match receipt.operation {
codewhale_lane::ControlOperation::LaneList => {
println!("{}", serde_json::to_string_pretty(&receipt.lane_records)?);
}
codewhale_lane::ControlOperation::LaneStatus => match receipt.lane_records.first() {
Some(record) => println!("{}", serde_json::to_string_pretty(record)?),
None if receipt.is_error() => {}
None => println!("{}", serde_json::to_string_pretty(receipt)?),
},
_ => println!("{}", serde_json::to_string_pretty(receipt)?),
}
} else if receipt.is_error() {
eprintln!("{}", receipt.render());
} else {
println!("{}", receipt.render());
}
}
`
p := parserFor("x.rs") // rust parser;NewParser → Parse → NewTagger → TagTree
if p == nil {
t.Fatal("rust parser 未注册")
}
res, perr := p.Parse("x.rs", []byte(trigger))
t.Logf("trigger segment symbols=%d err=%v", len(res.Symbols), perr)
if len(res.Symbols) == 0 {
t.Log("✅ bug 复现:触发段 0 个 definition")
} else {
t.Logf("❌ 未触发:期望 0,got %d", len(res.Symbols))
}
}
说明:p.Parse 内部即 NewParser(lang) → SetTimeoutMicros → Parse(src) → NewTagger(lang, ResolveTagsQuery(entry)) → TagTree(tree),随后按 tag.Kind 前缀 definition. 收集符号;parserFor("x.rs") 是扩展名 → parser 的路由。
报错信息 / 日志 / Error output / logs
## 附录 B:执行结果
### v0.19.1(复现)
=== RUN TestMinimalRepro
repro_test.go:119: trigger segment symbols=0 err=<nil>
repro_test.go:124: ✅ bug 复现:触发段 0 个 definition
--- PASS: TestMinimalRepro (0.04s)
PASS
ok deepx/codegraph 1.469s
补充:整文件(8739 行)`TagTree` 原始输出(不经任何过滤):
total tags=1 defs=1 refs=0
#0 kind=definition.type name="ProviderArg"
### v0.48.1(对照,症状消失)
=== RUN TestMinimalRepro
repro_test.go:126: ❌ 未触发:期望 0,got 2
--- PASS: TestMinimalRepro (0.04s)
PASS
ok deepx/codegraph 1.420s
提交前确认 / Preflight
版本 / Version
deepx 0.2.106
安装方式 / Install method
直接下载 release 二进制手动运行 / downloaded release binary, ran directly
操作系统 / OS
Windows
环境细节 / Environment detail
No response
问题描述 / What happened
现象
对大型 Rust 文件执行
TagTree时,输出异常稀少——definitiontag 只剩文件里第一个定义,其余(函数/结构体/枚举/impl)全部丢失,referencetag 也为 0。实测对象:CodeWhale 项目
crates/cli/src/lib.rs(8739 行,317 KiB,无超长行、无生成标记):ProviderArg@32,文件第一个定义)},)start_lane/emit_control_receipt)定位:与文件体量无关,是特定语法树形态触发
1. 排除项(均实测排除)
2. 按行截断的符号数(二分定位,全部同一文件前缀)
翻转点:797→798 行。797 行是
None => println!("{}", serde_json::to_string_pretty(receipt)?),,798 行是闭合嵌套 match 的},。最小触发构造(已验证,来自 lib.rs 779-800 行)
结构特征(三者同时出现时触发,单独/两两未验证):
None if receipt.is_error() => {})?操作符在println!宏参数内注意:该片段单独解析(不带前文)输出 0 个 definition,但截断到 guard 前一行输出正常——证明与代码总量无关,是完整语法树形态(闭合嵌套 match)触发。
复现步骤 / Steps to reproduce
复现步骤
tree := NewParser(lang).Parse(src)tags := NewTagger(lang, ResolveTagsQuery(entry)).TagTree(tree)strings.HasPrefix(tag.Kind, "definition.")数量截断法(稳定复现):对触发文件按行截断重跑,观察"闭合嵌套 match 的括号行"加入前后,definition 数从 N 骤变为 1。
期望行为
TagTree应输出源码中全部定义的 tag,不因嵌套 match / guard / 宏内?等合法语法而丢失任何 definition。附加验证(修复对照)
run_cli定义升级到 v0.48.1 后症状消失(未向 v0.48.1 提交最小独立复现,但同一文件、同一调用链输出正常)。若维护者需要最小独立复现文件,可基于上述结构构造并配合截断法确认。
附录 A:复现测试代码(内联,可直接运行)
以下为 deepx codegraph 包内的复现测试(Go)。触发段内联在测试里,无需任何外部文件/路径,
go test直接运行。触发段为 CodeWhalecrates/cli/src/lib.rs706-799 行原文(start_lane 完整函数 + emit_control_receipt 截断部分);tree-sitter 语法解析不校验符号引用,可直接作为输入。注释中反引号已改为单引号(Go raw string 限制,不影响语法解析)。说明:
p.Parse内部即NewParser(lang)→SetTimeoutMicros→Parse(src)→NewTagger(lang, ResolveTagsQuery(entry))→TagTree(tree),随后按tag.Kind前缀definition.收集符号;parserFor("x.rs")是扩展名 → parser 的路由。报错信息 / 日志 / Error output / logs