@@ -42,20 +42,47 @@ func (s Status) Token() string {
4242}
4343
4444// 遍历时跳过的目录名(版本控制 / 依赖 / 构建产物 / 缓存 / deepx 自身数据)。
45- // 注意:按目录"名"匹配,只列不易和源码目录重名的;以 "." 开头的目录在遍历时统一跳过,不必在此重复列。
45+ // 注意:1) 按目录"名"匹配,只列不易和源码目录重名的;以 "." 开头的目录统一跳过,不必在此重复列。
46+ // 2. **键必须全小写** —— shouldSkipDir 用 strings.ToLower(name) 查找以做到大小写不敏感(issue #115)。
4647var skipDirs = map [string ]bool {
4748 ".git" : true , ".hg" : true , ".svn" : true ,
4849 "node_modules" : true , "vendor" : true , ".deepx" : true ,
4950 "dist" : true , "build" : true , "target" : true , ".next" : true ,
5051 // 各语言依赖 / 缓存 / 构建产物
51- "bower_components" : true , "Pods " : true , "__pycache__" : true ,
52+ "bower_components" : true , "pods " : true , "__pycache__" : true ,
5253 "venv" : true , "site-packages" : true , ".nuxt" : true ,
5354 "out" : true , "obj" : true , "coverage" : true , "__snapshots__" : true ,
5455}
5556
57+ // shouldSkipDir 判断遍历时是否跳过该目录:命中 skipDirs(**大小写不敏感**)或以 "." 开头。
58+ // 大小写不敏感是关键(issue #115):Windows/macOS 文件系统大小写不敏感,目录可能叫 Vendor /
59+ // Node_Modules / VENDOR 等;若按原大小写精确匹配,这些依赖目录会漏跳被索引,大仓直接 OOM。
60+ func shouldSkipDir (name string ) bool {
61+ return skipDirs [strings .ToLower (name )] || strings .HasPrefix (name , "." )
62+ }
63+
5664// 单文件大小上限:超过视为生成 / 压缩产物,跳过,避免拖慢索引。
5765const maxFileSize = 1 << 20 // 1 MiB
5866
67+ // 单行字节上限(issue #115):任一行超过即视为压缩 / 打包 / 生成 / 数据文件,解析前跳过。
68+ // tree-sitter 的 GLR 在这类超长行上会内存爆炸,且其超时杀不掉已在 native 解析的 goroutine
69+ // (遗弃的 runaway 会持续吃内存直到 OOM)→ 只能从源头不喂这类文件。用 var 便于测试调小。
70+ var maxLineBytes = 40 << 10 // 40 KiB
71+
72+ // hasOverlongLine 报告 src 是否含超过 max 字节的行(O(n) 扫一遍,不分配)。
73+ func hasOverlongLine (src []byte , max int ) bool {
74+ start := 0
75+ for i , b := range src {
76+ if b == '\n' {
77+ if i - start > max {
78+ return true
79+ }
80+ start = i + 1
81+ }
82+ }
83+ return len (src )- start > max
84+ }
85+
5986// 整次构建的扫描预算:任一触顶即停,图谱标记为降级(StatusDegraded)。
6087// 这是与"根目录是什么"无关的硬兜底 —— 即使 gate 漏判、或撞上病态大仓,
6188// 也保证构建有界:不会再 CPU 焊死 / 内存无限上涨 / 永不结束。
6693 maxIndexDuration = 60 * time .Second // 单次遍历耗时上限
6794)
6895
96+ // 精确 go/types pass 的体量门控(issue #115):go/packages.Load("./...") 带 NeedSyntax|
97+ // NeedTypesInfo 会把整个 module + 依赖树的语法树/类型信息一次性全 load 进内存,无内置上限,
98+ // 大项目下可吃几十 GB 直接 OOM。快图有 512MiB 预算约束,精确 pass 也必须有对应的体量上限:
99+ // 模块自身 Go 源码超过任一阈值,就**跳过精确 pass**、保留已建好的有界快图(Go 调用边退化为
100+ // 语法近似,功能可用)。用 var 便于测试调小。
101+ var (
102+ maxPreciseGoFiles = 2000 // 模块内被精确解析的 .go 文件数上限
103+ maxPreciseGoBytes = int64 (32 << 20 ) // 模块内 .go 源码总字节上限(32 MiB)
104+ )
105+
69106// projectMarkers:出现在根目录顶层即认为"这是一个项目根",可放心自动预热。
70107// 正向识别比黑名单健壮 —— home 顶层通常没有这些,真实项目几乎都有。
71108var projectMarkers = []string {
@@ -156,9 +193,7 @@ type Index struct {
156193
157194 gPrecise bool // 当前 ix.g 是否已是精确图
158195 goSig string // ix.g 精确图对应的 Go 文件签名
159- cachedPrecise []Edge // 上次算出的精确 Go 调用边(按 cachedSig 缓存)
160- cachedSig string
161- preciseRunning bool // 后台精确解析是否在跑(避免并发重复)
196+ preciseRunning bool // 后台精确解析是否在跑(避免并发重复)
162197
163198 rebuildMu sync.Mutex // 保护 rebuildTimer
164199 rebuildTimer * time.Timer // Invalidate 后的防抖重建定时器(连续编辑只在静默后重建一次)
@@ -194,8 +229,8 @@ func NewIndex(root string) *Index {
194229}
195230
196231// Disabled 报告图谱是否因危险根被禁用;Reason 返回禁用 / 不自动预热的原因。
197- func (ix * Index ) Disabled () bool { return ix .forbidden }
198- func (ix * Index ) Reason () string { return ix .reason }
232+ func (ix * Index ) Disabled () bool { return ix .forbidden }
233+ func (ix * Index ) Reason () string { return ix .reason }
199234
200235// Prewarm 后台预热:开机只建"快图"(语法层,便宜),不阻塞调用方。
201236// 刻意不在此跑精确解析(go/packages 较重、可能联网)—— 那留到模型真正调用 CodeGraph 时
@@ -215,7 +250,7 @@ func (ix *Index) Prewarm() {
215250 go func () {
216251 ix .mu .Lock ()
217252 if ix .g == nil {
218- g , degraded , err := ix .assemble ( nil , false )
253+ g , degraded , err := buildViaSubprocess ( ix .root , "quick" )
219254 if err != nil {
220255 ix .mu .Unlock ()
221256 ix .setStatus (StatusIdle )
@@ -245,7 +280,7 @@ func (ix *Index) Graph() (*Graph, error) {
245280 ix .maybePrecise ()
246281 return g , nil
247282 }
248- g , degraded , err := ix .assemble ( nil , false ) // 快图:语法近似
283+ g , degraded , err := buildViaSubprocess ( ix .root , "quick" ) // 快图:语法近似
249284 if err != nil {
250285 ix .mu .Unlock ()
251286 ix .setStatus (StatusIdle )
@@ -267,7 +302,7 @@ func (ix *Index) Reindex() (int, error) {
267302 return 0 , fmt .Errorf ("代码图谱已禁用:%s" , ix .reason )
268303 }
269304 ix .mu .Lock ()
270- g , degraded , err := ix .assemble ( nil , false )
305+ g , degraded , err := buildViaSubprocess ( ix .root , "quick" )
271306 if err != nil {
272307 ix .mu .Unlock ()
273308 return 0 , err
@@ -319,7 +354,7 @@ func (ix *Index) scheduleRebuild() {
319354 ix .setStatus (StatusReady )
320355 return
321356 }
322- g , degraded , err := ix .assemble ( nil , false )
357+ g , degraded , err := buildViaSubprocess ( ix .root , "quick" )
323358 if err != nil {
324359 ix .mu .Unlock ()
325360 return // 建失败:保持"更新",留待下次编辑或查询再试
@@ -340,6 +375,11 @@ func (ix *Index) maybePrecise() {
340375 if ix .forbidden {
341376 return // 危险根:绝不跑 go/types(会扫整棵依赖树)
342377 }
378+ // 体量门控(issue #115):Go 源码过大就跳过精确 pass,避免 go/packages 把整棵依赖树
379+ // load 进内存导致 OOM。保留已建好的有界快图即可。
380+ if files , bytes := ix .goCorpusStats (); files == 0 || files > maxPreciseGoFiles || bytes > maxPreciseGoBytes {
381+ return
382+ }
343383 sig := ix .goSignature ()
344384 if sig == "" {
345385 return // 没有 Go 文件,免跑
@@ -350,29 +390,18 @@ func (ix *Index) maybePrecise() {
350390 return
351391 }
352392 ix .preciseRunning = true
353- reuse := ix .cachedSig == sig && ix .cachedPrecise != nil
354- cached := ix .cachedPrecise
355393 ix .mu .Unlock ()
356394
357395 go func () {
358- edges , ok := cached , reuse
359- if ! ok {
360- edges , ok = goPreciseCallEdges (ix .root ) // 慢:类型检查依赖树
361- }
362- if ok {
363- if g2 , _ , err := ix .assemble (edges , true ); err == nil {
364- ix .mu .Lock ()
365- ix .g = g2
366- ix .gPrecise = true
367- ix .goSig = sig
368- ix .cachedPrecise = edges
369- ix .cachedSig = sig
370- ix .preciseRunning = false
371- ix .mu .Unlock ()
372- return
373- }
374- }
396+ // 精确解析(goPreciseCallEdges + 重新 assemble)整体在受内存限额的子进程里跑,
397+ // 成功才原子换上精确图;失败 / 降级则保持当前快图。
398+ g2 , _ , err := buildViaSubprocess (ix .root , "precise" )
375399 ix .mu .Lock ()
400+ if err == nil && g2 != nil {
401+ ix .g = g2
402+ ix .gPrecise = true
403+ ix .goSig = sig
404+ }
376405 ix .preciseRunning = false
377406 ix .mu .Unlock ()
378407 }()
@@ -399,7 +428,7 @@ func (ix *Index) assemble(preciseGoCalls []Edge, usePrecise bool) (_ *Graph, deg
399428 }
400429 name := d .Name ()
401430 if d .IsDir () {
402- if path != ix .root && ( skipDirs [ name ] || strings . HasPrefix ( name , "." ) ) {
431+ if path != ix .root && shouldSkipDir ( name ) {
403432 return filepath .SkipDir
404433 }
405434 return nil
@@ -418,6 +447,12 @@ func (ix *Index) assemble(preciseGoCalls []Edge, usePrecise bool) (_ *Graph, deg
418447 }
419448 files ++
420449 bytes += int64 (len (src ))
450+ // 超长行 = 压缩 / 打包 / 生成 / 数据文件:tree-sitter 的 GLR 解析对这类输入会内存爆炸
451+ // (issue #115),且其超时杀不掉已在 native 解析的 goroutine,遗弃的 runaway 会持续吃内存。
452+ // 这类文件对代码图谱也几乎无价值 —— 解析前直接跳过,从源头不喂炸弹。
453+ if hasOverlongLine (src , maxLineBytes ) {
454+ return nil
455+ }
421456 rel , relErr := filepath .Rel (ix .root , path )
422457 if relErr != nil {
423458 rel = path
@@ -460,6 +495,32 @@ func (ix *Index) assemble(preciseGoCalls []Edge, usePrecise bool) (_ *Graph, deg
460495 return g , degraded , nil
461496}
462497
498+ // goCorpusStats 统计 workspace 内(跳过 skipDirs/隐藏目录)的 .go 文件数与总字节,只 stat 不读。
499+ // 用于精确 pass 的体量门控(issue #115)。skip 规则与 assemble/goSignature 一致,
500+ // 因此不数 vendor/依赖/.git,约等于「module 自身会被 ./... 精确解析的 Go 源码量」。
501+ func (ix * Index ) goCorpusStats () (files int , bytes int64 ) {
502+ _ = filepath .WalkDir (ix .root , func (path string , d fs.DirEntry , err error ) error {
503+ if err != nil {
504+ return nil
505+ }
506+ if d .IsDir () {
507+ name := d .Name ()
508+ if path != ix .root && shouldSkipDir (name ) {
509+ return filepath .SkipDir
510+ }
511+ return nil
512+ }
513+ if strings .HasSuffix (path , ".go" ) {
514+ if info , e := d .Info (); e == nil {
515+ files ++
516+ bytes += info .Size ()
517+ }
518+ }
519+ return nil
520+ })
521+ return files , bytes
522+ }
523+
463524// goSignature 扫 workspace 里所有 .go 文件的大小+修改时间,拼成签名;Go 没变签名就不变。
464525// 只 stat 不读内容,便宜。
465526func (ix * Index ) goSignature () string {
@@ -470,7 +531,7 @@ func (ix *Index) goSignature() string {
470531 }
471532 if d .IsDir () {
472533 name := d .Name ()
473- if path != ix .root && ( skipDirs [ name ] || strings . HasPrefix ( name , "." ) ) {
534+ if path != ix .root && shouldSkipDir ( name ) {
474535 return filepath .SkipDir
475536 }
476537 return nil
0 commit comments