Handbooks
Reference

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 one

Full tier

Hand-written adapters: type-driven call resolution, inherited members, per-attribute state tracking, statement spans.

LanguageExtensionsRegistry keyTypes indexed
Python.pypythonclass
TypeScript (and JavaScript).ts .tsx .js .jsx .mjs .cjstypescriptclass interface enum alias
Go.gogostruct interface alias other
Rust.rsruststruct enum trait alias other
Java.javajavaclass interface enum record other
C#.cscsharpclass interface struct record enum other
C/C++.c .h .cpp .cc .cxx .c++ .hpp .hh .hxxcppclass struct enum alias other
Ruby.rb .rake .gemspecrubyclass other
PHP.php .phtmlphpclass interface enum trait
Swift.swiftswiftclass struct enum interface alias
Dart.dartdartclass enum trait alias other
Solidity.solsolidityclass interface struct enum other
Shell.sh .bashshell

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.

LanguageExtensionsRegistry key
Kotlin.kt .ktskotlin
Scala.scala .scscala
Zig.zigzig
Objective-C.mobjc
OCaml.mlocaml

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-patches

If 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=611
jq '.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

Adding 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.

On this page