语言支持
18 种语言,横跨两个分析层级——各层级分别认领哪些扩展名、generic 层级放弃了什么,以及在你撞上之前值得先知道的两个注意事项。
--lang auto(默认值)会在一趟扫描中检测并合并每一种语言。你几乎从来不需要指定其中某一种。
handbook analyze --source ~/code/polyglot-repo --work work/poly # one pass, all languages
handbook analyze --source ~/code/repo --work work/x --lang python # or pin onefull 层级
手写的适配器:类型驱动的调用解析、继承成员、按属性的状态跟踪、语句范围。
| 语言 | 扩展名 | 注册表键 | 索引的类型 |
|---|---|---|---|
| Python | .py | python | class |
| TypeScript (以及 JavaScript) | .ts .tsx .js .jsx .mjs .cjs | typescript | class interface enum alias |
| Go | .go | go | struct interface alias other |
| Rust | .rs | rust | struct enum trait alias other |
| Java | .java | java | class interface enum record other |
| C# | .cs | csharp | class interface struct record enum other |
| C/C++ | .c .h .cpp .cc .cxx .c++ .hpp .hh .hxx | cpp | class struct enum alias other |
| Ruby | .rb .rake .gemspec | ruby | class other |
| PHP | .php .phtml | php | class interface enum trait |
| Swift | .swift | swift | class struct enum interface alias |
| Dart | .dart | dart | class enum trait alias other |
| Solidity | .sol | solidity | class interface struct enum other |
| Shell | .sh .bash | shell | — |
最后一列就是 AdapterCapabilities.typeKinds——每个 adapter 实际能找到的,不是它想找到的。
Shell 根本没有类型声明,所以那里的 — 是一个肯定的声明而不是缺口。
常量、变量和宏在任何语言里都不被索引。
JavaScript 没有单独的适配器
它由 TypeScript 那个适配器覆盖。这里没有什么好选的——.js、.jsx、.mjs 和 .cjs 都会被自动 认领。
C 系只有一个适配器,不是两个
cpp 语法同样能处理 C;而 c 语法根本处理不了 C++。所以由一个适配器同时认领两者,对纯 C 项目来说 --lang cpp 才是正确的写法。
generic 层级
一套配置驱动的引擎,每种语言一份声明式规格。文件和函数清单是精确的;调用关系是尽力而为。
| 语言 | 扩展名 | 注册表键 |
|---|---|---|
| Kotlin | .kt .kts | kotlin |
| Scala | .scala .sc | scala |
| Zig | .zig | zig |
| Objective-C | .m | objc |
| OCaml | .ml | ocaml |
分析结果混用了层级的手册,会在它的总览里说明这一点。究竟有哪些能力会退化,参见 分析保真度。
两个注意事项,在你撞上之前先讲清楚
在 V8 ≥ 13 上,Swift 会让进程直接中止
随包发布的 Swift 语法,会在 V8 把 WASM 模块升到更高编译层之后触发一次致命的内存耗尽中止——在 Node 24 上实测 5 次中 5 次致命,在 Node 21 上一切正常,而且在十九种语法里只有它是这样。
在这样的运行时上,适配器会在发现阶段就直接拒绝,并点明补救办法,而不是把你整趟运行一起拖垮:
node --liftoff-only $(which handbook) analyze --source ~/code/ios-app --work work/ios--liftoff-only 会跳过触发它的那次升层编译。本仓库自己的测试套件传同一个标志,正是出于同一个原因。
含 case 的 shell 脚本会被跳过
固定版本的 bash 语法,其外部扫描器会导入 env.isalpha,而 web-tree-sitter@0.25.10 的动态链接器
并不提供这个符号,于是 parse() 一遇到 case 语句就抛错——而且会让解析器就此中毒,所以适配器会
丢弃它并继续往下走。
这件事比听上去要严重
case 在 shell 里无处不在。在 nvm 上实测:全部 6 个文件、
全部 122 个函数都被跳过了。Shell 之所以列在 full 层级,是因为适配器本身是 full 层级的,但在那个
语法于上游修好之前,请把 shell 的覆盖率当作部分覆盖。
扫描日志会明确说明这一点,并点出原因:
[scan] shell: 5 file(s) the grammar could not parse — the pinned bash grammar throws on
`case`, which most real scripts use; their functions are absent from this graph, not
merely unresolved — install.sh, nvm.sh, …这些文件每一个还会在 phase1/scan-coverage.json 里被单独点名,reason: "unparsable",并且被挡在
图的 scannedFiles 之外——所以它们是一个看得见的缺口,而不是一个无声的缺口。
到处都会被跳过的东西
每个适配器都遵守同一份共享的跳过清单:
.git .hg .svn node_modules vendor target build dist out
__pycache__ .mypy_cache .pytest_cache .ruff_cache .tox
venv .venv env .env site-packages .idea .vscode .handbook-patches如果你的构建产物放在别的地方,请把 --source 指向真正的源码根目录,而不是仓库根目录。
当一个文件可能同属两种语言
发现阶段会按固定顺序运行各个适配器,第一个认领某个文件的适配器就把它留下;扩展名匹配优先取最长
匹配。实际当中,这只对 .h(由 C/C++ 适配器认领)和 .m(Objective-C)有影响。
查看实际扫描到了什么
handbook analyze --source ~/code/repo --work work/x -v
# [scan] auto root=/Users/me/code/repo
# [scan] typescript: 284 files
# [scan] python: 96 files
# [scan] shell: 12 files
# [build] functions=3187 kept=9042 dropped=611jq '.metadata.languages | keys' work/x/phase1/graph.json # which tiers are in play
jq '.metadata.byCategory' work/x/phase1/dropped-calls.json # what could not be resolved
jq '.metadata.byReason, .files' work/x/phase1/scan-coverage.json # what could not be parsed at all新增一门语言
新增一门 generic 层级的语言,写的是一份声明式规格,而不是一个解析器——而且不需要任何新依赖, 因为那些语法已经随包发布了。参见新增一门语言。