Tier 1 language support — contributor requirements
This document defines what fully supported means for a programming language in rgBuilder, and the concrete steps to add or promote a language to that level.
Audience: contributors adding a new Tier 1 language or bringing a language to parity with the current bar (Java-shaped Layer F + Layers A–E).
Related docs: Code_structure.md (crate layout), dashboard-design.md (dashboard bundle), user-guide.md (discover / serve), hybrid-cpg-plan.md (CPG).
1. Language tiers (quick reference)
rgBuilder uses a hybrid tiering model:
| Tier | Handler | Crate pattern | Indexing | CFG / PDG / taint | Call graph | Hybrid CPG (Layer F) |
|---|---|---|---|---|---|---|
| Tier 1 | custom — dedicated LanguagePlugin | rgbuilder-lang-{id}/ | Rich symbols + relations | Required | Required (Calls at minimum) | Required — same bar as Java |
| Tier 2 | Generic tree-sitter | rgbuilder-lang-{id}/ + config.rs | Kinds from LanguageConfig | Optional | Usually none | Not required |
| Tier 3 | Regex | rgbuilder-lang-{id}/ + regex patterns | Pattern-based symbols | No | No | No |
Tier 1 custom plugins today: Rust, Python, TypeScript, JavaScript, Go, Java, C#, C, C++ — see languages.toml (handler = "custom").
Fully supported means the language passes all of Layers A–F with automated tests. Layer F (hybrid CPG: fields, constructors, typed params, field-write mutations) is not optional for Tier 1 — Java is the reference implementation, not a special case.
Honesty limits still apply (no full points-to, no reflection, dynamic languages may emit more Unresolved receivers) — but plugins must ship the same shapes and golden mutation fixture as Java.
2. Capability checklist — “fully supported”
A language is fully supported when all rows are ✅ and backed by automated tests.
Layer A — Graph plugin (indexing)
| # | Requirement | Where |
|---|---|---|
| A1 | Custom LanguagePlugin (not generic Tier 2 only) | crates/rgbuilder-lang-{id}/src/plugin.rs |
| A2 | Tree-sitter grammar wired (grammar() + parse) | Plugin + tree-sitter-{id} crate dep |
| A3 | Symbols: functions/methods with name, location, signature, parameters | extract_symbols() |
| A4 | Symbols: types (class/struct/interface/enum as appropriate) | extract_symbols() |
| A5 | Relations: Calls between functions | extract_relations() — use rgbuilder_plugin_api::walk_calls or language-specific walker |
| A6 | Relations (OOP / composition): Extends / Implements (or language equivalent: Go struct embed → Extends, method-set satisfaction → Implements; Rust trait impls when extracted) | Required for Tier 1 — not optional. Document honesty limits (no full points-to) separately. |
| A7 | Cyclomatic / cognitive complexity for functions | calculate_complexity() |
| A8 | Entry in languages.toml with handler = "custom" | Repo root |
| A9 | Registered in rgbuilder-languages | crates/rgbuilder-languages/src/lib.rs |
Layer B — Analysis profile (CFG pipeline)
| # | Requirement | Where |
|---|---|---|
| B1 | LanguageAnalysisProfile with cfg_enabled: true | crates/rgbuilder-analysis/src/language_profile.rs |
| B2 | tree-sitter-{id} dependency on rgbuilder-analysis | crates/rgbuilder-analysis/Cargo.toml |
| B3 | function_kinds match tree-sitter node kinds used in CFG lookup | language_profile.rs + cfg_builder.rs |
| B4 | CFG builders for control flow: if, loops, return, break/continue | crates/rgbuilder-analysis/src/cfg_builder.rs |
| B5 | CFG builders for language-specific control flow (e.g. switch, select, match, try) | cfg_builder.rs |
| B6 | Definition-use extraction for assignments / declarations | crates/rgbuilder-analysis/src/def_use.rs |
| B7 | PDG builds from CFG + source (automatic once CFG + def/use work) | crates/rgbuilder-analysis/src/pdg.rs |
| B8 | discover --with-cfg / discover --with-cfg --with-security --with-taint includes .ext files | Automatic via cfg_language_id_from_path in discover_impl.rs |
Layer C — Security & interprocedural
| # | Requirement | Where |
|---|---|---|
| C1 | taint_enabled: true on profile | language_profile.rs |
| C2 | detect_{lang}_patterns() — sources, sinks, sanitizers | crates/rgbuilder-analysis/src/taint.rs |
| C3 | Taint routed via canonical_language_id() | TaintAnalyzer::detect_patterns |
| C4 | Interprocedural CFG uses correct language (not wrong grammar) | interprocedural_cfg.rs → language_id_from_path |
| C5 | Slice CLI resolves language from file path | src/cli/context.rs → language_from_path |
Layer D — Dashboard & UX
| # | Requirement | Where |
|---|---|---|
| D1 | discover --with-cfg --with-security --with-taint writes .rgbuilder/dashboard/ with CFG index populated | cfg_index.json available: true |
| D2 | Per-function CFG + dominance render in dashboard | Manual smoke or Playwright |
| D3 | Dataflow / taint tabs show data when flows exist | PDG + taint archive export |
| D4 | Blast radius lists functions with non-zero scores when call graph exists | manifest.json calls_count > 0 |
Layer E — Tests (required for merge)
| # | Requirement | Where |
|---|---|---|
| E1 | Plugin unit tests: symbols + at least one Calls relation | crates/rgbuilder-lang-{id}/src/plugin.rs #[cfg(test)] |
| E2 | CFG unit tests: branching function + loop cycle | crates/rgbuilder-analysis/src/cfg_builder.rs tests |
| E3 | Taint unit/integration test: at least one source→sink path | tests/taint_analysis.rs or tests/{lang}_taint.rs |
| E4 | Fixture integration test on a small real repo | e.g. tests/go_cfg_analysis.rs |
| E5 | Dashboard golden gate: discover --with-cfg --with-security --with-taint + bundle assertions | e.g. tests/dashboard_ecommerce_go.rs + tests/dashboard_harness.rs |
| E6 | cargo test + cargo clippy clean for touched crates | CI |
Layer F — CPG readiness (hybrid CPG) — required for Tier 1
Required for high-quality cpg mutations / typed field writes. Reference: Java (rgbuilder-lang-java) + field_write golden tests. See hybrid-cpg-plan.md.
| # | Requirement | Where | Acceptance |
|---|---|---|---|
| F1 | Type symbols populate fields[] (name + best-effort type string) | extract_symbols() | Plugin unit test lists ≥1 field with type when the grammar has types |
| F2 | Constructors (or language equivalent) extracted as functions; detectable as ctor | Plugin metadata | metadata.is_constructor: true and qualified name ending in .<init> or ::<init> (Java/C#/TS/JS/Python/New* Go / Rust new / C++ same-name ctor). Languages without ctors (C): document limit; golden test may mark an init helper as ctor in the graph node |
| F3 | Methods expose typed parameters when the grammar has them | parameters[].param_type | Plugin unit test; dynamic langs may leave None but still extract names |
| F4 | CFG/def_use: field-access LHS recorded as member write (obj.field; -> normalized to .) | def_use.rs + lang kinds | Covered by shared field-access kinds + lang decl kinds (lexical_declaration, etc.) |
| F5 | Best-effort local/param types for CFG functions | field_write_locals.rs | merge_local_types("{id}", …) recovers formals + typed locals (or copy-assign inference for JS) |
| F6 | Golden mutation fixture: type T with field write outside ctor → cpg mutations / index query hits it with --exclude-ctors | field_write::tests::{id}_cfg_captures_field_write_and_query | Exactly one non-ctor hit for the typed write |
| F7 | Document resolution limits (no reflection, no full inference) | This doc + hybrid plan | Honesty note in PR / language section |
Graph extract must populate Symbol.fields on type symbols (F1). Graph materialization of those fields as Variable nodes under the owning type (Contains) happens only when discover runs with --with-cfg (CPG path) — default discover keeps fields on symbols without emitting per-field graph nodes (rgbuilder-extraction graph builder). Empty fields on the symbol is not enough for Tier 1 plugins.
Non-negotiable: a new Tier 1 language that skips Layer F is not mergeable as Tier 1. Ship it as Tier 2 until F1–F6 land.
Layer F language notes (honesty)
| Language | Ctor convention | Type strength |
|---|---|---|
| Java / C# | Real constructors → Type.<init> | Strong |
| C++ | Name == enclosing class → Type::<init> | Strong |
| Go | NewT returning T/*T → T.<init> (heuristic) | Strong on structs |
| Rust | fn new in impl → Type::<init> | Strong on structs |
| TypeScript / JavaScript | constructor method → class .<init> | TS strong; JS weak (params may be untyped; graph param types / copy inference help) |
| Python | __init__ → Class.<init>; harvest self.x fields | Annotations when present |
| C | No language ctors; struct fields + typed params required | Strong on structs |
Java extract honesty (java-extract-gaps + java-grammar-remainder + java-gql-remainder-gates):
- Annotation types are
:Annotationnodes (not:Interface). Usages emitAnnotatedWith; no classpath/FQN resolution beyond imports/package best-effort. - Records are
Classwithmetadata.is_record; compact ctors and<clinit>/<initblock>Nare CFG entry points. - Annotation elements are Functions with
is_annotation_element; interfaceconstant_declarationbecomes fields. - Generics/
throwsare symbol metadata (type_params,throws); not TypeParameter nodes. GQL JSON projects allowlisted properties (type_params,throws,is_lambda,is_external_stub, …). - Lambdas are synthetic Functions (
$lambda$N,is_lambda); direct CFG$lambda$Nlookup is file-global (prefer enclosing-method CFG). - Anonymous classes use synthetic
Outer.$AnonymousNowners. - Expression refs: field reads →
References; arraynew→Instantiates;.class→References. No full points-to. - Type-use annotations (
annotated_type) and declaration-site parameter annotations attach to the owning method/constructor (parameter encodings are not graph nodes). Field type-use attaches to the field symbol. - Unresolved Instantiates / DependsOn / Uses / AnnotatedWith / References / Calls targets become deduplicated external stub nodes (
is_external_stub, file<external>) so GQL edges survive; stubs are placeholders, not a JDK model. - Pattern-matching (
record_pattern/type_pattern) not first-class symbols. - No full reflection / retention-policy analysis.
- GQL gates:
cargo test --test java_langfeatures(fixturetests/fixtures/java/langfeatures).
3. Repository layout
languages.toml # Metadata source of truth (handler, extensions, kinds)
crates/
rgbuilder-plugin-api/ # LanguagePlugin trait, Symbol, Relation, call_extraction
rgbuilder-lang-runtime/ # Generic Tier 2 TreeSitterLanguagePlugin
rgbuilder-lang-{id}/ # One crate per language (YOU ADD THIS)
Cargo.toml
src/
lib.rs # pub fn register(registry: &mut LanguageRegistry)
plugin.rs # Tier 1: custom LanguagePlugin impl
config.rs # Tier 2 only: static LanguageConfig
rgbuilder-analysis/
src/
language_profile.rs # CFG/taint gating registry
cfg_builder.rs # Per-language CFG visitors
def_use.rs # Per-language def/use AST cases
field_write.rs # Mutation index (Layer F)
field_write_locals.rs # Per-language local/param type recovery (F5)
taint.rs # Per-language taint patterns
rgbuilder-languages/ # Wire register() into default binary
tests/
{lang}_cfg_analysis.rs # Fixture CFG tests
{lang}_taint.rs # Taint + calls integration
dashboard_{fixture}.rs # discover --with-cfg --with-security --with-taint dashboard gate
Crate naming rules
| Language | Crate name | Package name on crates.io path |
|---|---|---|
| Go | rgbuilder-lang-go | rgbuilder-lang-go |
| Java | rgbuilder-lang-java | rgbuilder-lang-java |
| TypeScript | rgbuilder-lang-typescript | hyphens, not underscores |
- Directory:
crates/rgbuilder-lang-{id}/ language_id(): lowercase, no spaces ("go","csharp","javascript")- Tree-sitter dep:
tree-sitter-{grammar}(version pin in crateCargo.toml)
4. Step-by-step: add a new Tier 1 language
Use Kotlin → Tier 1 or C# → Tier 1 as a mental template; use Java / Go as code references.
Step 1 — Scaffold the plugin crate
- Copy an existing custom plugin crate (e.g.
rgbuilder-lang-goorrgbuilder-lang-java). - Rename to
crates/rgbuilder-lang-{id}/. - Update
Cargo.toml:
[package]
name = "rgbuilder-lang-{id}"
description = "rgBuilder language plugin: {id}"
[dependencies]
rgbuilder-plugin-api = { workspace = true }
rgbuilder-registry = { workspace = true }
rgbuilder-plugin-helpers = { workspace = true }
tree-sitter = { workspace = true }
tree-sitter-{grammar} = "0.xx"
serde_json = "1"
- Implement
lib.rs:
pub fn register(registry: &mut LanguageRegistry) {
registry.register_language_plugin(Arc::new(MyPlugin::new().expect("init MyPlugin")));
}
- Add to workspace root
Cargo.toml:memberslist[workspace.dependencies] rgbuilder-lang-{id} = { path = "...", version = "0.1.0" }
- Register in
crates/rgbuilder-languages/src/lib.rs.
Step 2 — languages.toml
Add a [languages.{id}] section:
[languages.{id}]
handler = "custom"
plugin = "MyPlugin"
module = "crate::languages::builtin::{id}" # legacy doc path; crate is standalone
crate = "tree-sitter-{grammar}"
extensions = ["ext"]
aliases = ["alias"]
function_kinds = ["function_declaration"] # must match tree-sitter
class_kinds = ["class_declaration"]
import_kinds = ["import_declaration"]
enable_complexity = true
enable_type_inference = false # true only if plugin infers param types
Run scripts/generate_lang_configs.py if you maintain Tier 2 config.rs files in parallel (not needed for pure custom Tier 1).
Step 3 — Implement LanguagePlugin
Required methods — see crates/rgbuilder-plugin-api/src/lib.rs:
| Method | Purpose |
|---|---|
language_id() | Canonical id string |
file_extensions() | &["go"], &["java"], etc. |
grammar() | Some(tree_sitter_*::LANGUAGE.into()) |
extract_symbols() | Walk AST; emit Symbol list |
extract_relations() | Emit Relation with RelationType::Calls (minimum) |
calculate_complexity() | Optional but expected for Tier 1 |
Calls extraction: prefer shared helper:
use rgbuilder_plugin_api::{walk_calls, GO_CALL_KINDS}; // or define LANG_CALL_KINDS
walk_calls(tree.root_node(), source, file_path, symbols, CALL_KINDS, "mylang", &mut relations);
Add language-specific call node kinds to call_extraction.rs if needed (e.g. method_invocation for Java uses a custom walker today).
Reference implementations:
| Feature | Look at |
|---|---|
| Calls + inheritance | crates/rgbuilder-lang-java/src/plugin.rs |
| Structs + methods | crates/rgbuilder-lang-go/src/plugin.rs |
| Classes + type inference | crates/rgbuilder-lang-python/src/plugin.rs |
| Traits + functions | crates/rgbuilder-lang-rust/src/plugin.rs |
Step 4 — Wire the analysis profile
Edit crates/rgbuilder-analysis/src/language_profile.rs:
LanguageAnalysisProfile {
id: "mylang",
aliases: &["ml"],
extensions: &["ml"],
function_kinds: &["function_declaration"],
cfg_enabled: true,
taint_enabled: true,
},
Add grammar loader in grammar_for():
"mylang" => Ok(tree_sitter_mylang::LANGUAGE.into()),
Add tree-sitter-mylang to crates/rgbuilder-analysis/Cargo.toml.
Export new helpers from crates/rgbuilder-analysis/src/lib.rs if public API additions are needed.
Step 5 — CFG builder
In cfg_builder.rs:
- Confirm
build_cfg_for_functionparses vialanguage_profile::parse_source. - Add
visit_*handlers for language-specific statement node kinds. - Add
is_block_like()kinds if the grammar uses nonstandard block nodes (Go usesstatement_list). - Add unit tests
test_{lang}_if_cfg,test_{lang}_loop_has_cycle, plus switch/try if applicable.
Tip: Dump the AST with a small tree_sitter script or cfg_builder test when mapping kinds — do not guess field names (body vs consequence).
Step 6 — Def-use and PDG
In def_use.rs, add match arms for the language’s assignment/declaration node kinds (see Go short_var_declaration, range_clause).
PDG construction is shared; no separate file unless control-dependency edge cases need work.
Step 7 — Taint patterns
In taint.rs:
- Add
detect_mylang_patterns(&mut self). - Register in
detect_patternsviacanonical_language_idmatch arm. - Cover at minimum: HTTP/input sources, SQL/shell sinks, common sanitizers for that ecosystem.
Keep patterns statement-text based for now (consistent with existing code); type-aware taint is optional (with_type_inference).
Step 8 — Tests
Minimum test matrix:
crates/rgbuilder-lang-{id}/src/plugin.rs # unit: symbols, calls
crates/rgbuilder-analysis/src/cfg_builder.rs # unit: CFG shape
crates/rgbuilder-analysis/src/language_profile.rs # unit: path → id
tests/{id}_cfg_analysis.rs # integration: real fixture file
tests/{id}_taint.rs # taint + calls smoke
tests/dashboard_{fixture}.rs # discover --with-cfg --with-security --with-taint + manifest/cfg_index
Reuse tests/dashboard_harness.rs helpers (run_discover_all, assert_dashboard_bundle_all_analysis).
Provide a small fixture repo under rgbuilder-tests/ or document RGBUILDER_{LANG}_REPO env override.
Step 9 — Manual validation
cargo build --release
./scripts/build-dashboard.sh && cargo build --release # if dashboard dist changed
rg-build discover --with-cfg --with-security --with-taint -r /path/to/fixture-repo -l {id} -v
rg-build serve -r /path/to/fixture-repo --host 127.0.0.1 --port 8080
# Open http://127.0.0.1:8080 — check Graph, CFG, Dataflow, Taint, Blast Radius tabs
Confirm manifest.json shows calls_count > 0 and cfg_index.json has "available": true.
5. Promoting Tier 2 → Tier 1
Many languages exist as generic tree-sitter plugins (TreeSitterLanguagePlugin::from_config). To promote:
- Replace generic plugin with custom
plugin.rs(copy from Go/Java). - Change
languages.tomlhandlerfrom"tree_sitter"to"custom". - Implement
extract_relations(at leastCalls). - Complete Layers B–F checklist (including Layer F).
- Keep
config.rsonly if scripts still generate it; otherwise delete to avoid dual sources of truth.
Do not add CFG support only in cfg_builder.rs without a language_profile entry — discover will skip the language.
6. What not to do
| Anti-pattern | Why |
|---|---|
Parse language X in rgbuilder-graph or discover_impl.rs | Belongs in rgbuilder-lang-* + rgbuilder-analysis |
Hardcode .ext lists in CLI | Use language_profile / languages.toml |
Tier 1 plugin without Calls relations | Blast radius and call graph stay empty |
| Tier 1 without Layer F (fields / ctors / mutation golden) | cpg mutations is Java-only quality; not Tier 1 |
| CFG enabled without tests | Dashboard shows blocks but regressions go unnoticed |
| Duplicate grammar only in plugin crate | rgbuilder-analysis needs its own tree-sitter-* dep for CFG |
Skip rgbuilder-languages registration | Language won’t ship in default rg-build binary |
| Full type checker inside the plugin | Out of scope — bound resolution only (decl / param / field) |
7. PR submission checklist
Copy into your PR description:
-
crates/rgbuilder-lang-{id}/withLanguagePlugin+ tests -
languages.tomlupdated (handler = "custom") - Workspace
Cargo.toml+ bundle registration -
language_profile.rsentry (cfg_enabled,taint_enabled, grammar) -
cfg_builder.rs+ tests for control-flow constructs -
def_use.rscases for declarations/assignments including field-access LHS - Layer F:
fields[],is_constructor+.<init>/::<init>, typed params - Layer F:
merge_local_typesarm infield_write_locals.rs(or documented N/A) - Layer F: golden
{id}_cfg_captures_field_write_and_queryinfield_writetests -
taint.rsdetect_{id}_patterns -
extract_relationsemitsCalls(and inheritance if applicable) - Integration test + dashboard gate (or documented fixture path)
-
discover --with-cfg --with-security --with-taintsmoke on fixture repo documented in test - No new CDN / online-only dashboard dependencies
8. Current parity snapshot (2026-07)
| Language | Tier | Calls | CFG | Taint | Dashboard gate | Layer F (CPG mutations) |
|---|---|---|---|---|---|---|
| Java | 1 custom | ✅ + Extends/Implements/AnnotatedWith/Permits/Instantiates | ✅ (+ compact ctor, <clinit>) | ✅ rich | gbuilder golden | ✅ F1–F6 |
| Go | 1 custom | ✅ | ✅ deep | ✅ rich | dashboard_ecommerce_go | ✅ F1–F6 |
| C# | 1 custom | ✅ | ✅ | ✅ | dashboard_ecommerce_csharp | ✅ F1–F6 |
| C | 1 custom | ✅ | ✅ | ✅ | dashboard_ecommerce_c | ✅ F1/F3–F6 (no native ctors) |
| C++ | 1 custom | ✅ | ✅ | ✅ | dashboard_ecommerce_cpp | ✅ F1–F6 |
| Python | 1 custom | ✅ | ✅ | ✅ richest | dashboard_ecommerce_python | ✅ F1–F6 |
| Rust | 1 custom | ✅ | ✅ | ✅ rich | dashboard_ecommerce_rust | ✅ F1–F6 |
| JS / TS | 1 custom | ✅ | ✅ | ✅ rich | dashboard_ecommerce_javascript, dashboard_ecommerce_typescript | ✅ F1–F6 (JS weaker types) |
Layer F golden coverage lives in crates/rgbuilder-analysis/src/field_write.rs (*_cfg_captures_field_write_and_query). Update this table when promoting a language or when F tests regress.
9. Getting help
- Open a Language Support Request issue before large work.
- Point questions at
rgbuilder-plugin-api::LanguagePluginand the reference crates above. - For dashboard contract details, see
tests/dashboard_harness.rsanddocs/dashboard-design.md.