首页
/ CodeGraph Ruby 内核移植:语法升级、bug-for-bug 保真与零差异门禁的全记录

CodeGraph Ruby 内核移植:语法升级、bug-for-bug 保真与零差异门禁的全记录

2026-09-06 12:53:59作者:邵娇湘

CodeGraph 的提取内核正在把原本运行在 wasm 上的 TreeSitter 逐语言迁移到原生 Rust walker,docs/design/ruby-kernel-port-checklist.md 就是 Ruby 这一站(代号 R7b)的完整移植检查单:从语法(grammar)单独升级的选型与 SHA 验证,到每一个 TS 侧钩子怪癖的 bug-for-bug 复现清单,再到三重门禁(standalone bump gate、torture fixture、全仓 parity 扫描)的通过标准。这篇指南以该检查单为主体骨架展开,结合仓库中已落地的 walker codegraph-kernel/src/ruby.rs、TS 侧配置 src/extraction/languages/ruby.ts 与测试 tests/kernel-ruby-parity.test.ts,讲清"一个 bug-for-bug 移植到底要保真到什么粒度"。读完后你能掌握:如何为一次内核化改造做语法升级的独立隔离验证、如何用探针把新旧语法的形态差异分类到 hunk 级、以及如何用 torture fixture + 全仓 dump diff 把"行为完全一致"钉成可回归的测试不变量。

移植的最终状态:全部门禁通过

检查单头部即给出结论:Status: PORT COMPLETE (2026-07-20) —— walker 落地于 codegraph-kernel/src/ruby.rs,所有门禁通过:

  • 语法单独升级门禁通过:sinatra/jekyll 的 dump 在旧/新语法下字节级一致,rails 恰好只有那一处被分类的 &.!= hunk;
  • parity 扫描 0-diff:sinatra 147/147、jekyll 164/164、rails 3452/3452,0 deferrals(共 3,763 个文件);
  • full-init dump 门禁字节级一致 ×3;
  • kernel-ruby-parity 测试套件通过;
  • DEFAULT_ROUTED += ruby

仓库中可以逐条核对这些结论:src/extraction/kernel/index.tsruby 已进入 DEFAULT_ROUTED 集合,注释记录了 R7b (2026-07-20) 的 3,763 文件 byte-parity 与 0 deferrals;codegraph-kernel/Cargo.toml 中精确 pin 了 tree-sitter-ruby = "=0.23.1"

调研基线(survey basis)是:每一个 .rb/.rake 文件会触及的 TS 侧分支,带 f1ca991(调研时 HEAD)的 file:line 锚点。所有关于语法形态的论断都不是假设,而是**对旧 tree-sitter-wasms 构建和新 v0.23.1 构建各做一次探针(probe)**后得出的。检查单明确要求与 rust-kernel-migration-plan.md(§0a 配方、§5 门禁)及两份格式先例(rust-lang-kernel-port-checklist.mdccpp-kernel-port-checklist.md)配套阅读。阻塞性发现:无 —— 语法升级在所有提取器相关构造上是形态中立的(两个已分类的 delta,一个无影响、一个精度为正),三个门禁仓库上两臂错误率均为 0.00%,且 codegraph-kernel/src/python.rs 是一个结构非常接近的 walker 骨架。

阶段一:语法升级(Grammar prep)—— 必须最先做、独立合入

旧版语法从哪里来

Ruby 不在 VENDORED_WASM_LANGSsrc/extraction/grammars.ts 第 291-304 行)里 —— 生产环境加载的是 node_modules/tree-sitter-wasms/out/tree-sitter-ruby.wasm,由 tree-sitter-ruby ^0.20.1 构建(tree-sitter-wasms 0.1.13 的 devDependency,2024 年 2 月时代的产物)。升级目标:

  • Crate:tree-sitter-ruby = "=0.23.1"(crates.io 最高版本,2024-11-11 发布)= git tag v0.23.1 = commit 71bd32fb7607035768799732addba884a37a6210(同时是当时的 master)。crate tarball 与 tag 做了 sha256 交叉验证,ruby 带外部 scanner,所以两个生成物都验:
    • src/parser.c 的 sha256 为 4ce468358b6f4e25a35c8cf6bc0eaf60665bc22d602f8c939323c2347255cd15
    • src/scanner.c 的 sha256 为 e7a6196d6e78bf4c6728502e924c867dee5d851c6253e43fcdb8ba169009bc58

ABI 注意(与 rust 先例不同)

v0.23.1 check-in 的 parser.c 声明 LANGUAGE_VERSION 14 —— 该 tag 早于 ABI-15 生成器,所以这是一次 grammar 内容升级,ABI 保持 14(web-tree-sitter 与 native tree-sitter 0.25 都接受,最低兼容 13)。因此不要期望 kernel-grammar-parity 出现 ABI 变化,但要断言 same-revision。这一点在落地仓库的 src/extraction/grammars.ts 注释里可以原样看到:R7b (Ruby kernel port prep): tree-sitter-ruby v0.23.1 (71bd32f) … Content bump only — the tag's checked-in parser.c is still ABI 14 (predates the ABI-15 generator)

构建方式:只从 check-in 的 parser.c 构建

永远不要跑 tree-sitter generate,直接使用该 tag 自带的生成物:

git clone https://github.com/tree-sitter/tree-sitter-ruby && cd tree-sitter-ruby
git checkout v0.23.1
npx -y tree-sitter-cli@0.25.10 build --wasm -o tree-sitter-ruby.wasm .

调研产物 sha256 为 4cb5a4b12870876ca864c1e92fe1f5cd47036b2adc083e9306488af88867dbb4(2,106,097 字节)。仓库中该 wasm 已 vendor 至 src/extraction/wasm/tree-sitter-ruby.wasm

暂存计划(Grammar-bump PR:先于任何 walker 存在时)

  1. 把 wasm vendor 到 src/extraction/wasm/tree-sitter-ruby.wasm
  2. VENDORED_WASM_LANGS(grammars.ts:291)中加入 'ruby',附 R7b 注释(沿用 rust 模式:tag + sha-matched 说明);
  3. codegraph-kernel/Cargo.toml 的精确-pin 注释块下 pin tree-sitter-ruby = "=0.23.1"= 精确版本,与 c/cpp/rust 一致 —— crate 与 wasm 必须一起动,否则 kernel-grammar-parity 会失败);
  4. walker 落地时在 langs.rs 注册内核符号 tree_sitter_ruby::LANGUAGE
  5. 全量套件绿 + 独立 bump 门禁(见后文 §Gates)通过后才开始 walker 工作。

形态 delta(OLD→NEW):完整分类,共两处

  1. __END__ 数据拖尾 —— 无影响(INERT)。 OLD 有独立的 __END__ 匿名 token + 从换行之后开始的 uninterpreted 节点;NEW 删掉了 __END__ 的 kind 表条目(353 → 351 kinds,kind id 重编号),uninterpreted 紧跟 __END__ 之后开始(把前导 \n 也含进文本)。提取器没有任何分支触碰 uninterpreted(不在 ruby 的任何类型列表里,递归进也什么都匹配不到;文件的 endLine 来自 source.split('\n')),所以不产生任何输出变化。字段表(32 个 field)完全一致。
  2. Safe-nav 操作符方法调用 —— 行为改变,精度为正。 recv&.!= arg(rails 的 activerecord/lib/active_record/relation/where_clause.rb:62,在 3,763 个真实文件中唯一的命中):OLD 误解析为 assignmentrecv&.! 方法 !,然后 = arg)→ wasm 输出 recv.! 的 calls 引用;NEW 正确解析为 call,method 是 operator 类型的 != → 输出 recv.!= 的 calls 引用,参数按 arguments 走。这是 bump 时点仅 wasm 路径的一次 churn(两臂此后结论一致),在 bump 门禁的 dump diff 中按 rust 的"小型精度为正的边缘 churn"方式分类并接受。

其余探针全部在 OLD/NEW 间字节级一致:完整 torture 探针(modules/mixins/visibility/inline-def/calls/blocks/heredocs/hooks —— 848 行 CST dump 相同)、现代语法探针(endless methods def f(x) = ….../&/*/** 转发、case/in 模式匹配、右向赋值、hash 简写、%-literals、操作符方法定义 —— 全部一致,含完全一致的报错行为)、CRLF 变体(两臂都无错误,形态一致,node-type 序列与 LF 相同)。

错误率与 deferral 基线

两臂、全部 ≤1MiB 的 .rb/.rake 文件:sinatra 0/147(0.00%/0.00%)、jekyll 0/164、rails 0/3452 —— 零分歧。Ruby 属于 ts/java/py/go 这一类语言:预期 ~0% deferral,默认 --max-deferral 0.1 有巨大余量;一次 ruby 扫描出现两位数 deferral 就是 walker 坏了(没有 c/cpp 那种 0.5 豁免)。

探针脚本与输出存放在调研 scratchpad(svy-ruby/):shape-probe-ruby.cjs(OLD vs NEW 的 CST dumper)、table-compare.cjs(kind/field 表 + 错误定位)、error-sweep.cjs(逐仓库 has_error + 完整 CST sexp 对比)、extract-probe.cjs(跑真实的 dist 提取器 —— 它的 extract-{dblcap,vis,req,vref,misc,reqedge}.txt dump 是本文档通篇引用的 pinned ground truth,同时充当 walker 测试的预期值),以及 probe.rb/edge.rb/setter.rb/datatrailer.rb/probe-crlf.rb 等 fixture 与 shape-{OLD,NEW}*.txtkinds-{OLD,NEW}.txt dump。scratch 目录是一次性的 —— 丢了就从本文档重新推导。

阶段二:五个架构决策

  1. 没有 preParse。 rubyExtractor 没有 preParse 钩子 —— 路由点的 preParsedSource(kernel/index.ts:82)是 no-op;两臂都解析原始字节,没有需要 hoist 的东西。
  2. Rails 应用走 DECODED 路径,门禁仓库不走。 railsResolverresolution/frameworks/ruby.ts 第 11 行,languages: ['ruby'],注册于 frameworks/index.ts:52)带一个 extract() 钩子,而 parse-worker.ts:93-99 会把任何"有适用框架 extract()"的语言强制到 decoded 的 extractFromSource 路径。但 detect()(ruby.ts:22-39)要求仓库根目录存在包含单引号 'rails' 的 Gemfile、config/application.rbapp/controllers/application_controller.rbconfig/routes.rb —— sinatra/jekyll/rails 框架仓库本身一个都不触发(已验证),所以三个 parity 仓库走的都是 raw buffer 传输。因此:不要从 Rails 应用的性能运行中得出"raw 路径坏了"的结论,也不要从门禁仓库得出"decode 路径没被测过"的结论 —— torture fixture 套件在测试里覆盖了 decode。
  3. 框架提取器无需移植。 它是对原始源码的正则(ruby.ts:109-190),在任意外臂之后都在 extractFromSource(tree-sitter.ts:6736-6758)内部以相同方式运行,合并 route 节点 + controller#action 引用。其输入契约见下文 Frameworks 一节。
  4. 一个 walker 模块。 建议 codegraph-kernel/src/ruby.rs(这次没有 crate-语言命名冲突),在 langs.rs 注册(LANGUAGES + grammar_for + tree_sitter_ruby::LANGUAGE);逐文件 has_error()defer:,与其他所有 walker 相同。python.rs 是最接近的骨架 —— 共享形态:无花括号、def 风格、模块级 assignment → 恒为 variable(无 isConst)、类内函数 → method、不是 TYPE_ANNOTATION 语言、完整 value-ref 机制、从根带 scope 栈遍历。Ruby 与它有六处分歧,后文逐一展开:(a) visitNode 钩子(modules + mixins)对每个节点最先运行;(b) importTypes: ['call'] 吞掉每个顶层 call;(c) extractCall 里 ruby 专属分支(receiver/method 字段、.new → instantiates、常量 receiver 引用);(d) extractBareCall(语句级标识符);(e) sibling-scan 的 getVisibility;(f) 空 idTypes + call/simple_symbol 特例的 fn-ref spec。
  5. .rb/.rakeruby(detectLanguage,grammars.ts:103-104),无内容嗅探、无方言。无扩展名的 Gemfile/Rakefile 解析为 unknownlastIndexOf('.') === -1),永远不是 ruby。MAX_FILE_SIZE(1 MiB,extraction/index.ts:132)、vendor/ 跳过(Bundler,extraction/index.ts:168)与生成文件检测都在编排器/TS 侧,两臂共享。method_call(见下文提取器配置)在语法里不存在 —— 已探针确认:两种构建里都没有这个节点 kind。

阶段三:提取器配置与四个钩子(languages/ruby.ts,共 147 行,必须通读)

类型表(src/extraction/languages/ruby.ts 开头几行即可核对):

字段 取值 备注
functionTypes [method]
classTypes [class]
methodTypes [method, singleton_method]
interfaceTypes [] module 由钩子处理
structTypes / enumTypes / typeAliasTypes []
importTypes [call] 承重怪癖,见 visitNode 调度
callTypes [call, method_call] DEAD:语法里没有 method_call,为 parity 保留
variableTypes [assignment]
nameField / bodyField / paramsField name / body / parameters params 未被使用 —— 没有 getSignature

没有 enumMemberTypes、propertyTypes、fieldTypes、packageTypes。

钩子一:visitNode(ruby.ts:19-76)—— 对每个节点先于调度阶梯运行

在调度阶梯(tree-sitter.ts:943-953)之前执行,两项职责:

1. Mixins:receiver 字段的 call,且其 method 字段文本是 include/extend/prepend → 对 arguments 字段(?? namedChildren.find(type==='argument_list'))中每个类型为 constantscope_resolution 的 namedChild,推一个 implements unresolved 引用 {fromNodeId: nodeStack 栈顶, referenceName: 参数完整文本(Foo::Bar 原样), line/column: call 节点起点(同一调用的所有参数同行同列)} —— 然后 return true(已处理;该 call 永远到不了阶梯)。必须守住的门禁:nodeStack 非空 找到了 args 节点,否则 fall through 未处理。extend self → 参数类型为 self,跳过(无引用)但仍算 handled。带 receiver 的形式 Foo.include Bar 有 receiver → 钩子拒绝 → 该 call 死在 extractImport(什么都不输出)。

2. Modules: 类型为 module 且有 name 字段 → 创建一个 module 节点(name = name 字段文本 —— module A::B 就是 A::B 原样;无 docstring、无 visibility、无 extras —— ctx.createNode 不带任何 extra),push 其 id,对 body 字段的每个 namedChild 调 ctx.visitNode,pop,return true。无 name 字段 → false(fall through:children 裸递归;合法 ruby 中不可能发生)。当钩子处理了某节点,调度器会对其运行 scanFnRefSubtree(tree-sitter.ts:951)—— 仅捕获,depth>0 时在嵌套 functionTypes(method)处停下,但在 class/module 节点停,深度 ≤12。

怪癖,经验性钉死(dist 探针 extract-dblcap.txt):钩子处理的 module 会对 fn-ref 容器"多重捕获"。 钩子内部 ctx.visitNode 走一遍,在真实作用域捕获一次;随后钩子后的 scan 用"扫描时刻的栈顶"(module 已 pop)再捕获同一批容器 —— 嵌套 module 还会复合放大(每一层钩子 return 都触发对其整棵子树的另一次 scan)。module A { module B { class C < Base { before_action :hooked … } } } 的 ground truth 是 三个 function_ref "this.hooked" 引用 —— 分别来自 class:C(内层走查)、module:A(对 B 的 scan,此刻 A 仍在栈上)、file(对 A 的 scan),严格按这个候选顺序,且全部 flush(this. 前缀候选跳过门禁;flush 去重按 (fromNodeId,name),不同作用域全部存活)。类体中不在任何 module 内的容器只捕获一次(文件级的裸 class C 不受钩子处理)。必须精确复现这种多重捕获 —— Rails(module Admin; class XController; before_action …)无处不在地命中它。Mixin 处理的 call 同样会被 scan(其 argument_list 产不出东西 —— constant/self 不是候选形态)。

落地实现里可以直接对照 codegraph-kernel/src/ruby.rs 顶部注释:它开宗明义列出了这份"故意保留的承重怪癖"清单 —— importTypes: ['call'] 漏斗、钩子 module 的多重捕获、sibling-scan visibility 三件套、brace-block 不可见、value-ref DFS 逆序 —— 与检查单一一对应。

钩子二:extractBareCall(ruby.ts:77-105)—— 只从 visitFunctionBody 调用

针对"不是 callTypes/instantiation"的节点(tree-sitter.ts:5159-5173)。节点类型必须是 identifier;父类型必须在 BLOCK_PARENTS = {body_statement, then, else, do, begin, rescue, ensure, when} 中;名字不在 SKIP = {true, false, nil, self, super, FILE, LINE, dir} 中;首字符不是 ASCII A-Z(charCodeAt(0) 在 [65,90] —— 仅 ASCII:Unicode 大写开头的标识符不会被跳过);否则返回名字 → 调度器输出一个 calls 引用 {caller = 栈顶, name, line = identifier 的 startRow+1, col = startColumn}。

已探针确认的推论:def/do_block 体内的语句级 reset → 引用;brace-block 体是 block_body —— 不在集合里 → 5.times { beep } 对 beep 什么都不输出,而 do…end 体(body_statement)会;modifier 形式(cleanup unless done?compute rescue nil)父节点是 unless_modifier/rescue_modifier → 什么都不输出;三元分支(父 conditional)→ 无;interpolation 内裸标识符 → 无;begin/rescue(经其 then 体)/else/ensure/when(then)/while(体 do) 的语句标识符 → 引用。注意 ?/! 结尾的零参调用(done?block_given?)解析为 call(只有 method 字段)—— 走 extractCall 而不是本钩子,因此在任何位置(包括条件中)都会得到引用。

钩子三:getVisibility(ruby.ts:106-122)—— sibling 扫描

从 def 节点沿 previousNamedSibling 链走(无上限,穿过多轮不匹配的兄弟);第一个类型为 callmethod 字段文本为 private/protected/public 的兄弟说了算;否则 'public'。已探针确认的怪癖,全部保留

  • 一行裸的 private/protected/public 解析为 identifier 而非 call不可见 —— 其后方法保持 'public';
  • private :greet / private def x; end 是 method 为 private 的 call → 其后所有 def(任意距离)都变 'private',与 ruby 真实语义无关(private :sym 实际是 arg-scoped);
  • private def foo 内部的 def 位于该 call 的 argument_list 中、没有前驱 named sibling → 'public'
  • 适用于 extractFunctionextractMethodextractClassprivate :x 之后的 class 也会变 'private')与顶层 def(扫描在 program 层运行)。extractInterface/module 钩子不计算 visibility。

钩子四:extractImport(ruby.ts:123-146)—— 每个进入 importTypes 分支的 call

signature = 整个 call 的 source.substring(startIndex, endIndex).trim()(UTF-16 substring)。门禁:第一个类型为 identifier 的 namedChild(通常是 method 字段子节点;对带 receiver 的 foo.require 会先找到 receiver foo 而拒绝;对 Kernel.require "x",receiver 是 constant,find 会到达 method 标识符 require当作 require 处理 —— 保留此怪癖),文本必须恰为 require|require_relative,否则 null。然后:第一个 argument_list namedChild → 其第一个 string namedChild → 其第一个 string_content namedChild → moduleName = 内容文本;任何一环缺失 → null(require :sym → 无)。已探针:%q() string 节点 → require %q(pct/lib) 是完整 require(import 节点 + 引用);插值路径只取第一个 string_content —— require "interp/#{x}" → moduleName interp/ → 引用 interp/ + interp/.rb(垃圾但确定 —— 保留)。返回 {moduleName, signature},无 handledRefs → 通用路径同样触发(见下文 extractImport 漏斗)。

不存在的钩子 —— walker 严禁做这些事

preParseresolveNamerecoverMangledNameisMisparsedFunctionisConstisStaticisExportedisAsyncgetSignaturegetReturnTypegetReceiverTyperesolveBodyclassifyClassNodeclassifyMethodNodeextractPropertyNameinterfaceKindextraClassNodeTypespackageTypes/extractPackageextractModifierssynthesizeMembersskipBodilessClassmethodsAreTopLevel

推论:每个 ruby 函数/方法节点的 signature/isAsync/isStatic/returnType/isExported 都是 undefined(file 节点 isExported:false);没有 isConst 意味着每个 assignment 提取为 kind 'variable',绝不是 'constant' —— 包括 MAX = 3(value-ref 定位仍然有效:kind variable 是目标);attr_accessor/attr_reader/attr_writer 什么都不合成(无 synthesizeMembers —— 它们就是类体里的普通 call,完全无输出);没有参数节点,没有 decorates 引用(extractDecoratorsFor,tree-sitter.ts:4897 会运行,但 ruby 没有 decorator/annotation/marker_annotation 节点 kind,反向扫描在第一个非装饰器兄弟处停止 —— 永远是 no-op)。

阶段四:tree-sitter.ts 调度阶梯 —— 每个 ruby 节点命中什么

以下表格的锚点以 f1ca991 为准(调度阶梯在 936-1303 行):

节点 分支 行为
每个节点 visitNode 钩子最先(943) mixin call + module 在此处理;handled → scanFnRefSubtree + 停
每个节点 maybeCaptureFnRefs(990) 对 RUBY_SPEC 的调度键 argument_list / pair 同样在 visitNode 上下文中触发 —— 这就是类体钩子 DSL 符号的捕获方式
method functionTypes:994 在 class-like 内(isInsideClassLikeNode:1486 —— module 也算,1498)且在 methodTypes → extractMethod:1737;否则 extractFunction:1517。skipChildren
singleton_methoddef self.x / def Foo.x / def obj.x 不是 functionTypes → methodTypes:1027 无 classifyMethodNode → extractMethod。顶层时:非 class-like、无 methodsAreTopLevel、无 receiver 钩子 → 门禁 1747 送入 extractFunction → 顶层 def self.x 是名为 x 的普通 function 节点。object 字段处处被忽略 —— def self.xdef x 在图中不可区分(isStatic undefined)
class classTypes:1005 无 classifyClassNode → extractClass:1679(kind 'class')
singleton_classclass << self 无分支 递归 —— 其 body 里 body_statement 的 def 提取为外层类的 method,与实例方法不可区分(已探针)
assignment(顶层/class/module) variableTypes:1098 门禁:!isInsideClassLikeNode() || isClassScopeConstantAssignment(1508:type==='assignment' 且(left 字段 ?? namedChild(0)).type === 'constant')。文件作用域:identifier 与 constant LHS 都提取。class/module 作用域:仅 constant LHS。然后 extractVariable:2538 + scanFnRefSubtree(1110)+ skipChildren —— RHS 永不遍历:顶层/class 级 X = Foo.new 不产生任何 instantiates/calls
operator_assignment+=||= 无分支 不是 assignment → 任何作用域都不提取;递归(children 在非 body 作用域也不产出)
call(顶层/class/module 体) importTypes:1209 —— 绝不是 callTypes extractImport:3170。require/require_relative → import 节点 + 引用;其余所有 call → 无(钩子 null → 3350 处 if (this.extractor.extractImport) return;)。skipChildren 保持 false → children 会被访问:嵌套 call 同样落到这里(无输出),argument_list 获得 fn-ref 捕获。这把 attr_accessorhas_manydefine_method连同其 do_block 体)、get '/x' do…end 路由块、裸 DSL call 全部杀掉 —— 在非 body 作用域对提取完全不可见,例外只有 fn-ref 候选 + rails 正则提取器
call(方法/函数体内) visitFunctionBody:5143 extractCall:3684 → ruby 分支 3905-3960
alias / alias_method 无分支 / class 作用域的 call 两者都不输出(已探针:alias 子节点是 name/alias identifier 字段,父节点不在 BLOCK_PARENTS)
comment 无分支 只被 docstring 的 sibling 扫描消费
顶层 if/unless/case/while/begin 无分支 经 visitNode 递归 —— 所以顶层 if 里的 require 仍然到达 extractImport(全 visitNode 递归);但其中的 call/标识符无输出
uninterpreted__END__ 数据) 无分支 无(见语法准备一节)

对 ruby 不适用(可尽早廉价验证的 early-out):interfaceTypes/structTypes/enumTypes/typeAliasTypes/propertyTypes/fieldTypes 分支;swift property 分支(1121);TS re-export/vue-store export_statement 分支(1219/1235);INSTANTIATION_KINDS(354-361 —— 没有 ruby 节点 kind;ruby 的 .new 在 extractCall 中处理,所以 extractInstantiation:4610 对 ruby 不可达);impl_item(1274);property_signature/method_signature(1282,门禁在 TYPE_ANNOTATION_LANGUAGES,排除 ruby,5752-5754);extractFilePackage:1397(无 packageTypes → 无 namespace 节点);namespacePrefix 恒为空(cpp 专属)。

节点创建、ID 与限定名

  • createNode(1308):id = generateNodeId(filePath, kind, name, startRow+1) = `${kind}:${sha256(filePath:kind:name:line).hex.slice(0,32)}`(tree-sitter-helpers.ts:18-30)。FILE 节点 id 是字面量 file:${filePath}(509)。去重/自检比较的是 ID 字符串node_ids vec 模式)。
  • resolveBody 的 endLine 扩展(1329)是 no-op(无钩子);节点 endLine = node.endPosition.row+1(对 method,tree-sitter 的 method 节点本身已跨 def…end)。
  • 每个创建的节点都有来自 nodeStack 栈顶的 contains 边(1363);每次 create 都调 captureValueRefScope(1374)。
  • qualifiedName = nodeStack 名字以 :: 连接(buildQualifiedName:1447,namespacePrefix 空)—— module Outer; module Inner; class DeepOuter::Inner::Deep,方法 Outer::Inner::Deep::greet。紧凑形式 class A::B::C / module A::B 保留完整 scope_resolution 文本作为节点 NAME(extractName → nameField → getNodeText),所以其下嵌套类的 QN 形如 A::B::C::m —— 名字段内的 :: 原样组合。无 receiver-QN 路径(没有 getReceiverType)。
  • File 节点:kind file,name 为 basename,qualifiedName = filePath,endLine = source.split('\n').length,isExported false。

extractFunction / extractMethod / extractClass(1517 / 1737 / 1679)

  • extractFunction(顶层 method,以及被 1747 门禁弹回的 singleton_method):无 receiver 钩子(1522 跳过);名字经 nameField name(identifier;?/!/= 结尾的 def 名 —— done?save!value= —— 名字保留后缀)。<anonymous> 路径(1549)不会触发(语法强制要求 name)。无 misparse 钩子。节点 extras:docstring(getPrecedingDocstring)、signature undefined、visibility(sibling 扫描)、isExported/isAsync/isStatic/returnType undefined。extractTypeAnnotations → no-op(ruby ∉ TYPE_ANNOTATION_LANGUAGES:5752)。extractDecoratorsFor → no-op。push,沿 body 字段(body_statement)经 visitFunctionBody 走,pop。parameters(method_parameters)永不遍历 —— 默认参数里的调用 def f(x = compute()) 什么都不输出。
  • extractMethod(class/module 内的 def + class-like 内的 singleton_method):receiverType undefined(1742);门禁 1747 经 class-like 通过;object-literal 父节点检查(1751)不会匹配 ruby(object/object_expression 是 TS 节点 kind)。extras 同 function;无 receiver QN(1790),无 owner-contains 回退(1799 —— 需要 receiverType)。extractEnumMembers/interface 永不触发。
  • extractClass:resolvedBody = body 字段;无 skipBodilessClass → 无体的 class TopDoc; end(body 字段缺失 —— 已探针)仍然铸造 class 节点;body 遍历目标回退到 class 节点自身(1714)→ namedChildren = [name-constant, superclass?] 逐个 visitNode(无输出 —— 但注意 superclass 子树被再次访问,无害)。extras:docstring、visibility(sibling 扫描)、isExported undefined。extractInheritance(1704,见下);extractCsharpPrimaryCtorParamRefs/extractDecoratorsFor 为 no-op;synthesizeMembers 缺失(1727)。然后 class 入栈后走 body children。
  • 方法体内的嵌套 def:visitFunctionBody:5245 —— method 属 functionTypes,named → extractFunction → 此刻不是 class-like(栈顶是外层 METHOD 节点)→ extractFunction 路径;其内部调度是直接的(无门禁)→ 嵌在 def 里的 def 提取为被外层方法包含的 function。body 内的 class:命中 5255 → extractClass(被方法包含)。body 内的 module 节点被 visitForCallsAndStructure 匹配(钩子在那里不运行)—— visitFunctionBody 从不调用提取器的 visitNode 钩子,所以方法体内定义的 module 不铸造任何 module 节点;其 children 递归(5277),其 def 命中 5245 → 归到外层方法的 function。body 内的 include call 同理:走 extractCall(callTypes)→ 名为 includecalls 引用 —— 不是 implements 引用。两个怪癖都保留。

extractImport 漏斗(3170):所有顶层 call 的归口

钩子只在"带 string 的 require/require_relative"时返回 {moduleName, signature}(见上)。之后:

  1. import 节点:createNode('import', moduleName, node, {signature}) —— id 来自 kind import,name = moduleName(jsonsidekiq/fetch../foo/bar)。
  2. 通用 imports 引用(3183-3194,钩子不设 handledRefs):{fromNodeId: 栈顶(file/class/module/何处均可), referenceName: moduleName 原样, line: call startRow+1, column: call startColumn}。
  3. emitRubyRequireRefs(3231-3234 → 3532-3560):自行重新推导 method 名 + string 内容(namedChildren.find identifier / argument_list / string / string_content —— 同样的形态);req = 内容 .trim()require_relative → refPath = path.posix.normalize(dirname(filePath) + '/' + req)(dirname 用 as-indexed filePath 的 lastIndexOf('/') —— posix 语义);require → refPath = req 不变。然后:refPath 中没有 / → 返回(裸 gem/stdlib require 不产生文件引用);末尾追加 .rb(除非已 endsWith('.rb'));推 {fromNodeId, referenceName: refPath, referenceKind:'imports', call 的 line/col}。所以 require "sidekiq/fetch" 在 import 节点之后输出两个 imports 引用(sidekiq/fetch + sidekiq/fetch.rb)—— 输出顺序:节点、通用引用、require 引用。
  4. 钩子 null(每个非 require call):穿过 python/go/php 的多 import 分支落到 3350 的 return —— 什么都不输出,children 仍被阶梯访问(import 分支 skipChildren 为 false)。

body 内的 require 永不走到这里(visitFunctionBody 把 call 路由到 extractCall)—— def 内的 require "x" 只输出一个裸 calls 引用 require;body 内 Kernel.require "some/lib" → calls Kernel.require + references Kernel(已探针)。只有 visitNode 上下文的 require(顶层、类体、module 体、顶层 if/begin 内)才创建 import 节点。

extractCall(3684):ruby 分支(3905-3960),永远提前返回

仅从 visitFunctionBody:5143 到达。门禁:language==='ruby' && (type 'call' || 'method_call') —— 位于 LITERAL_RECEIVER_TYPES / 通用 field_expression 机制之前,所以 ruby 永远不运行后者。步骤:

  1. methodName = method 字段文本;空/缺失 → 什么都不输出返回(操作符/元素引用形态;注意 element_reference 是自有的节点 kind,本来就到不了这里)。
  2. receiver 字段 → 一个 calls 引用 {name: methodName, line: call startRow+1, col: call startColumn}。覆盖无括号命令、putsdone? 式零参调用、lambda/proc(block 体随后递归)、body 内 require、body 内 include(就是普通 calls 引用!)。
  3. receiverName = 完整 receiver 文本(getNodeText —— 原样,sigil 与换行都算:@name@@cv$g"literal"5[1, 2]chained.first_calla.b(x))。
  4. methodName === 'new':className = receiverName 在最后一个 :: 之后(slice(lastIndexOf('::')+2));若 /^[A-Z]/instantiates 引用 {name: className} 位于 call 位置,返回。Widget.new → instantiates WidgetNS::Widget.newWidget不带限定名!);lower.new → 不大写 → 落到第 5 步 → calls 引用 lower.new
  5. SKIP_RECEIVERS = {self, super} 按文本(ruby 专属集合 —— 比通用的 {self,this,cls,super,parent,static} 更小):命中 → 裸 methodName;否则 `${receiverName}.${methodName}`。safe navigation &. 以普通 . 连接(deep&.safe_calldeep.safe_call);链保留原始 receiver 文本(chained.first_call.second_callWidget.create(x).saveWidget.create(x).save —— 参数文本不规范化为 (),与 java/php 的链编码不同);字面量 receiver 被过滤("literal".upcase"literal".upcase5.times5.times —— 不可解析的噪声,保留)。
  6. receiver 节点类型恰为 constant(且未被 skip)→ 额外 references 引用 {name: receiverName, line/col: receiver 的位置} —— Klass.static_call 输出 calls Klass.static_call + references Klass;对 VALUE 常量同样触发(RETRY_MAX.times {…} → calls RETRY_MAX.times + references RETRY_MAX —— 已钉死)。scope_resolution receiver(Foo::Bar.baz得到 references 引用(类型 ≠ constant)。保留。
  7. return —— 内层 receiver call 之后也会被访问(body walker 在 5277 递归 children —— extractCall 不消费其子树),所以 chained.first_call.second_call 同时输出 chained.first_call.second_callchained.first_call;参数 string 里的插值调用
登录后查看全文
热门项目推荐
相关项目推荐