crates/mehen-<lang>/. The analyzer:
- Pins its own parser (a language-specific parser like Ruff / Oxc / Mago / Prism /
ra_ap_syntax, or a tree-sitter grammar). - Owns metric interpretation for that language’s syntax — what counts as a decision, an operator, a method, a comment, etc.
- Returns
LanguageAnalysis(owned,Send + 'static) somehen-enginecan analyze files in parallel and never holds onto parser arenas.
grammar.rs kind enum, generated from the
pinned grammar’s node-kind table.
Choosing a parser
The first decision is the parser. mehen prefers a language-specific parser when one exists with mature Rust bindings and rich AST/semantic coverage; tree-sitter is the default fallback when no such parser is available.| Parser style | Examples in mehen | When to pick |
|---|---|---|
| Language-specific | Ruff (Python), Oxc (TS/JS), Mago (PHP), Prism (Ruby), ra_ap_syntax (Rust), pulldown-cmark (Markdown) | A maintained Rust crate with a typed AST, semantic model, or rich grammar coverage exists. |
| ANTLR | Kotlin (official Kotlin spec grammar), Java (grammars-v4) | A high-quality, maintained ANTLR v4 grammar exists but no Rust-native parser does. Generated via ophi-dev/antlr-rust-runtime; the CST is far more semantically named than tree-sitter’s. |
| Tree-sitter | Go, C, PowerShell | No mature language-specific or ANTLR parser is available, or tree-sitter’s grammar quality is the best fit. |
Adding a language-specific parser
Add the parser to the workspace
Add the parser crate(s) to
crates/mehen-<lang>/Cargo.toml. Pin an exact version (or a git
revision tagged for the release) so mehen’s behavior is reproducible.Implement the analyzer
In
crates/mehen-<lang>/src/lib.rs:- Define
<Lang>Analyzerand implementmehen_core::LanguageAnalyzerfor it. - Walk the parser’s typed AST and emit metrics through
mehen_metrics::{State, MetricTreeBuilder, …}and the per-metric helpers. - Make sure the parser’s arena, source buffer, and parser state do not escape the
analyzecall —LanguageAnalysismust beSend + 'static.
Register the analyzer
Add it to
mehen-engine’s registry (crates/mehen-engine/src/registry.rs) so
Language::<YourLang> dispatches to it.Adding an ANTLR-backed language
ANTLR is a first-class backend (AnalysisBackend::Antlr), used when a high-quality ANTLR v4
grammar exists but no Rust-native parser does. The shared crate mehen-antlr provides the runtime
re-export, token-span conversion, recovered-error diagnostics, and hidden-channel comment (CLOC)
extraction. The Rust lexer/parser modules are generated offline
from a vendored .g4 grammar and checked in — a normal cargo build never needs Java or the
ANTLR jar.
Vendor the grammar
Copy the ANTLR
.g4 files (lexer, parser, and any imported grammars like UnicodeClasses)
into crates/mehen-<lang>/grammar/. Record the upstream source, commit, and toolchain versions
in a PROVENANCE.md so regeneration is reproducible (see crates/mehen-kotlin/grammar/).Register the codegen target
Add an
AntlrTarget to xtask/src/antlr.rs::TARGETS (slug, crate dir, grammar dir, lexer/parser
filenames).Generate the parser modules
crates/mehen-<lang>/src/generated/. The generated files carry their own
lint and rustfmt::skip attributes, so the owning analyzer can include them with plain
pub(crate) mod ...; declarations unless another crate needs direct access.Add the runtime dependency
Depend on
mehen-antlr (the shared helpers) and antlr4_runtime (the generated modules
reference the runtime by its real crate name). Both reach the same workspace-pinned version, so
the pin stays in one place.Implement the analyzer + walker
Parse via the generated
<Lang>Lexer/<Lang>Parser, then walk the ParseTree with your own
recursive Visitor (like mehen-rust/mehen-ruby). ANTLR rule contexts have no parent
pointer, so thread any parent-dependent context (e.g. else-if detection) top-down. Match
rule_index() against the generated RULE_* constants and terminal token_type() against the
token constants. Comments are on a hidden channel (absent from the tree) — recover CLOC from the
token stream via mehen_antlr::comment_rows.Adding a tree-sitter-backed language
Prerequisite: atree-sitter-<lang> crate compatible with the tree-sitter version pinned in the
workspace (Cargo.toml [workspace.dependencies]).
Pin the grammar
Two files must stay in sync — both reference the grammar at compile time:
xtask/Cargo.toml— the kind-enum generator imports the grammar at codegen time.crates/mehen-<lang>/Cargo.toml— the analyzer imports the grammar at runtime to drivetree_sitter::Parser.
Cargo.toml [workspace.dependencies]) can be
referenced as { workspace = true } from both places. Inline-pinned grammars must be kept in
lockstep manually.Implement the analyzer
Same pattern as a language-specific parser, but use the generated
crate::grammar::<Lang> enum
for kind-id matching — it deduplicates positional kinds and exposes mnemonic identifiers
(PLUS, EQ_EQ, etc.).Bumping a pinned grammar
When dependabot bumps atree-sitter-<lang> version (or you do it manually):
- Update both
xtask/Cargo.tomlandcrates/mehen-<lang>/Cargo.tomlto the new version. Theregenerate-grammarsworkflow does this automatically for inline-pinned grammars. - Run
cargo xtask tree-sitter generate --alland commit the regeneratedgrammar.rsfiles. - CI’s
cargo xtask tree-sitter check-generatedwill fail until the regenerated files are committed.
Validation
See also
- Update grammars — bumping pinned tree-sitter versions.
- Implement LoC — example of a metric trait implementation.