Language support
18 languages across two analysis tiers — which extensions each claims, what the generic tier gives up, and the two caveats worth knowing before you hit them.
--lang auto (the default) detects and merges every language in one pass. You almost
never need to name one.
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 tier
Hand-written adapters: type-driven call resolution, inherited members, per-attribute state tracking, statement spans.
| Language | Extensions | Registry key | Types indexed |
|---|---|---|---|
| Python | .py | python | class |
| TypeScript (and 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 | — |
The last column is AdapterCapabilities.typeKinds — what each adapter actually finds, not
what it aspires to. Shell has no type declarations at all, so — there is a positive claim
rather than a gap. Constants, variables and macros are indexed in no language.
JavaScript has no separate adapter
It is covered by the TypeScript one. There is nothing to pick — .js, .jsx, .mjs and .cjs are claimed
automatically.
One C-family adapter, not two
The cpp grammar handles C as well; the c grammar does not handle C++ at all. So one adapter claims both,
and --lang cpp is correct for a pure-C project.
Generic tier
One config-driven engine, one declarative spec per language. File and function inventory is exact; call relations are best-effort.
| Language | Extensions | Registry key |
|---|---|---|
| Kotlin | .kt .kts | kotlin |
| Scala | .scala .sc | scala |
| Zig | .zig | zig |
| Objective-C | .m | objc |
| OCaml | .ml | ocaml |
A handbook whose analysis mixes tiers says so in its overview. See Analysis fidelity for exactly what degrades.
Two caveats, stated before you hit them
Swift aborts the process on V8 ≥ 13
The bundled Swift grammar causes a fatal out-of-memory abort once V8 tiers the WASM module up — measured fatal 5 times out of 5 on Node 24, fine on Node 21, and unique to that one grammar among nineteen.
The adapter refuses at discovery on such a runtime and names the remedy rather than taking your whole run down:
node --liftoff-only $(which handbook) analyze --source ~/code/ios-app --work work/ios--liftoff-only skips the tier-up compilation that triggers it. The repo's own test suite
passes the same flag for exactly this reason.
Shell scripts containing case are skipped
The pinned bash grammar's external scanner imports env.isalpha, which
web-tree-sitter@0.25.10's dynamic linker does not provide, so parse() throws on any
case statement — and leaves the parser poisoned, so the adapter discards it and moves on.
This is bigger than it sounds
case is ubiquitous in shell. Measured on nvm: all
6 files and all 122 functions were skipped. Shell is listed as full tier because the
adapter is full tier, but treat shell coverage as partial until that grammar is
fixed upstream.
The scan log says so explicitly, and names the reason:
[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, …Every one of those files is also named individually in phase1/scan-coverage.json with
reason: "unparsable", and kept out of the graph's scannedFiles — so they are a
visible gap rather than a silent one.
What is skipped everywhere
Every adapter honours one shared skip list:
.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-patchesIf your build output lives somewhere else, point --source at the real source root rather
than the repository root.
When one file could belong to two languages
Discovery runs adapters in a fixed order and the first one to claim a file keeps it;
extension matching prefers the longest match. In practice this only matters for .h
(claimed by the C/C++ adapter) and .m (Objective-C).
Reading what actually got scanned
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 allAdding a language
Adding a generic-tier language is a declarative spec, not a parser — and needs no new dependency, because the grammars already ship. See Adding a language.