新しい言語のサポート
How to add a language to big-code-analysis, for contributors who have a tree-sitter grammar crate for it. The work has three parts:
- Generate the kind enum from the grammar.
- Wire the grammar into the workspace.
- Implement the traits and tests for every supported metric.
文法の生成
As a prerequisite, a tree-sitter grammar crate for the language must exist that matches the tree-sitter runtime version used in this project.
The kind enums are generated by a crate in this repository called enums. The following steps register the grammar crate there and generate the enum file this project matches on when it evaluates metrics.
- 言語固有の
tree-sitterクレートをenumsクレートに追加します。このとき、依存関係がルートのbig-code-analysisのCargo.tomlで使用されているのと同じバージョンに=X.Y.Zでピン留めされていることを確認してください。たとえば Rust サポートの場合、/enums/Cargo.toml には次の行があります:tree-sitter-rust = "=0.24.2"。 - Append the language to the
mk_langs!list in /enums/src/languages.rs. Keeping with Rust as the example, the line isRust,: one variant name per line, which becomes the name of the Rust enum that will be generated. Add a// -> <crate>comment when the backing crate is not the obvioustree_sitter_<name>. - Add a case to the match in the
mk_get_languagemacro rule in /enums/src/macros.rs, which binds each variant to its grammar crate. The current convention uses theLANGUAGEconstant exposed by modern grammar crates: for Rust that line isLang::Rust => tree_sitter_rust::LANGUAGE.into(). - Run the /recreate-grammars.sh script, which runs the
enumscrate to generate the enum for the new language.
This produces a new enum file for the language in /big-code-analysis-ast/src/languages/. See /big-code-analysis-ast/src/languages/language_rust.rs as an example of the generated enum.
新しい文法を big-code-analysis に追加する
- Add the language specific
tree-sittercrate to thebig-code-analysisworkspace, with the same=X.Y.Zpin as theenumscrate uses. Since #1376 this is three edits across two manifests. In the root Cargo.toml[workspace.dependencies], add the exact grammar pin (for Rust,tree-sitter-rust = "=0.24.2"). In /big-code-analysis-ast/Cargo.toml, add the optional workspace dependency (tree-sitter-rust = { workspace = true, optional = true }) and its feature (rust = ["dep:tree-sitter-rust"]), then include that feature inall-languages. In the root[features], add the forwarding feature (rust = ["big-code-analysis-ast/rust"]) and include it inall-languages. - Add the new language module to /big-code-analysis-ast/src/languages/mod.rs:
#![allow(unused)]
fn main() {
pub mod language_rust;
pub use language_rust::*;
}
- Add a definition of the language to the arguments of the
mk_langs!macro in /big-code-analysis-ast/src/langs.rs.
#![allow(unused)]
fn main() {
// 1) このバリアントの文法を有効にする Cargo フィーチャ名
// 2) enum の名前
// 3) 言語の説明
// 4) 表示名
// 5) 実装対象となる空の構造体名
// 6) パーサー名
// 7) Language を取得するために呼び出す tree-sitter 関数
// 8) ファイル拡張子
// 9) emacs モード
// 10) ピン留めされた文法クレートのバージョン(ワークスペースの Cargo.toml にある
// `=X.Y.Z` ピンを反映。両者の一致はドリフトテストで検証されます)
(
"rust",
Rust,
"The `Rust` language",
"rust",
RustCode,
RustParser,
tree_sitter_rust,
[rs],
["rust"],
"0.24.2"
)
}
トレイトとテストの実装
文法の配線は最初のステップにすぎません。新しい <Lang>Code 型は、AST の基盤処理と、ワークスペースが定義するすべてのメトリクストレイトも実装しなければなりません。
Checkerin a new per-language file under /big-code-analysis-ast/src/checker/ (checker/<lang>.rs, registered with amod <lang>;line inchecker.rs): comment, function, closure, call, string-literal, andelse-ifpredicates over the grammar’skind_ids, most of which forward to the language’s role table (below).Getterin a new per-language file under /big-code-analysis-ast/src/getter/ (getter/<lang>.rs, registered ingetter.rs):get_space_kindplus the Halstead operator/operand classification table.Alteratorin /big-code-analysis-ast/src/alterator.rs: analteratebody that flattens the kinds the language’sStringLitrow lists to one verbatim leaf, so the AST dump agrees withfind string. Every grammar carries one; only thePreprocandCcommenthelper variants, whose dump is not a user surface, keep the bare default.- A role table in /big-code-analysis-ast/src/roles/ (
roles/<lang>.rs, onerole_table!per language), declaring for every role either its rows,absent "why", orpending Rnn.make role-census,make role-coverageandmake role-bypass(all inmake pre-commit) fail on a language with no table, a kind id that is neither claimed by a row, excluded with a reason, nor baselined, or a claimed kind restated as classification in a dispatch file without a// role-bypass: allow(<Role>)exemption. - All thirteen metric traits:
Abc,Cognitive,Cyclomatic,Exit(thenexitsmetric),Halstead,Loc,Mi,NArgs,Nom,Npa,Npm,Tokens,Wmc. Register twelve of them via theimplement_metric_trait!macro invocation in each module under /src/metrics/ to start with default (no-op) bodies, then replace with real impls for the metrics that have meaningful semantics for the language.Halsteadis the exception: every grammar is listed inimpl_halstead_forwarding!insrc/metrics/halstead.rs, which routes the metric through the language’sGetter, so there is no per-language Halstead body to write.
エイリアス化された文法バリアントの監査
Tree-sitter grammars frequently emit several distinct kind_ids that map to the same node.kind() string (Identifier / Identifier2 / Identifier3 in Go, InvocationExpression / InvocationExpression2 in C#, QuotedContent through QuotedContent20 in Elixir). Every match node.kind_id() arm that touches an aliasable rule must either list every numbered variant or compare on the string node.kind() instead. Missing an alias silently drops nodes from the metric. See the add-lang skill for the mechanical audit procedure, and .claude/rules/grammar-dispatch.md plus lessons 2, 19 and 34 in docs/development/lessons_learned.md for the failure modes.
テスト
Add per-language tests under each src/metrics/*.rs test module; aim for parity with the existing Rust coverage across the metric files. Every insta assert_json_snapshot! call MUST be anchored: either with an inline expected block, a positive assert_eq! on the headline integer accessor above it, or an explanatory // expected: comment. make snapshot-anchors (run as part of make pre-commit) enforces this against .snapshot-anchor-baseline.txt. Every test that names the new parser also needs #[cfg(feature = "<lang>")], since the *Parser alias compiles in every build and panics in one without the grammar; make check-test-lang-gates (also in make pre-commit) derives the required gate, and ./utils/check-test-lang-gates.py --fix writes it (the Makefile target forwards no arguments).
エンドツーエンドのワークフロー
For an opinionated, end-to-end recipe (including the alias audit, test layout, snapshot anchoring, and code-quality post-passes) see the project’s add-lang Claude Code skill. It is the canonical workflow used by recent language additions (Elixir, PHP, C#, Bash, Go).