Docs / tier-1-language-support · Edit on GitHub

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:

TierHandlerCrate patternIndexingCFG / PDG / taintCall graphHybrid CPG (Layer F)
Tier 1custom — dedicated LanguagePluginrgbuilder-lang-{id}/Rich symbols + relationsRequiredRequired (Calls at minimum)Required — same bar as Java
Tier 2Generic tree-sitterrgbuilder-lang-{id}/ + config.rsKinds from LanguageConfigOptionalUsually noneNot required
Tier 3Regexrgbuilder-lang-{id}/ + regex patternsPattern-based symbolsNoNoNo

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)

#RequirementWhere
A1Custom LanguagePlugin (not generic Tier 2 only)crates/rgbuilder-lang-{id}/src/plugin.rs
A2Tree-sitter grammar wired (grammar() + parse)Plugin + tree-sitter-{id} crate dep
A3Symbols: functions/methods with name, location, signature, parametersextract_symbols()
A4Symbols: types (class/struct/interface/enum as appropriate)extract_symbols()
A5Relations: Calls between functionsextract_relations() — use rgbuilder_plugin_api::walk_calls or language-specific walker
A6Relations (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.
A7Cyclomatic / cognitive complexity for functionscalculate_complexity()
A8Entry in languages.toml with handler = "custom"Repo root
A9Registered in rgbuilder-languagescrates/rgbuilder-languages/src/lib.rs

Layer B — Analysis profile (CFG pipeline)

#RequirementWhere
B1LanguageAnalysisProfile with cfg_enabled: truecrates/rgbuilder-analysis/src/language_profile.rs
B2tree-sitter-{id} dependency on rgbuilder-analysiscrates/rgbuilder-analysis/Cargo.toml
B3function_kinds match tree-sitter node kinds used in CFG lookuplanguage_profile.rs + cfg_builder.rs
B4CFG builders for control flow: if, loops, return, break/continuecrates/rgbuilder-analysis/src/cfg_builder.rs
B5CFG builders for language-specific control flow (e.g. switch, select, match, try)cfg_builder.rs
B6Definition-use extraction for assignments / declarationscrates/rgbuilder-analysis/src/def_use.rs
B7PDG builds from CFG + source (automatic once CFG + def/use work)crates/rgbuilder-analysis/src/pdg.rs
B8discover --with-cfg / discover --with-cfg --with-security --with-taint includes .ext filesAutomatic via cfg_language_id_from_path in discover_impl.rs

Layer C — Security & interprocedural

#RequirementWhere
C1taint_enabled: true on profilelanguage_profile.rs
C2detect_{lang}_patterns() — sources, sinks, sanitizerscrates/rgbuilder-analysis/src/taint.rs
C3Taint routed via canonical_language_id()TaintAnalyzer::detect_patterns
C4Interprocedural CFG uses correct language (not wrong grammar)interprocedural_cfg.rslanguage_id_from_path
C5Slice CLI resolves language from file pathsrc/cli/context.rslanguage_from_path

Layer D — Dashboard & UX

#RequirementWhere
D1discover --with-cfg --with-security --with-taint writes .rgbuilder/dashboard/ with CFG index populatedcfg_index.json available: true
D2Per-function CFG + dominance render in dashboardManual smoke or Playwright
D3Dataflow / taint tabs show data when flows existPDG + taint archive export
D4Blast radius lists functions with non-zero scores when call graph existsmanifest.json calls_count > 0

Layer E — Tests (required for merge)

#RequirementWhere
E1Plugin unit tests: symbols + at least one Calls relationcrates/rgbuilder-lang-{id}/src/plugin.rs #[cfg(test)]
E2CFG unit tests: branching function + loop cyclecrates/rgbuilder-analysis/src/cfg_builder.rs tests
E3Taint unit/integration test: at least one source→sink pathtests/taint_analysis.rs or tests/{lang}_taint.rs
E4Fixture integration test on a small real repoe.g. tests/go_cfg_analysis.rs
E5Dashboard golden gate: discover --with-cfg --with-security --with-taint + bundle assertionse.g. tests/dashboard_ecommerce_go.rs + tests/dashboard_harness.rs
E6cargo test + cargo clippy clean for touched cratesCI

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.

#RequirementWhereAcceptance
F1Type symbols populate fields[] (name + best-effort type string)extract_symbols()Plugin unit test lists ≥1 field with type when the grammar has types
F2Constructors (or language equivalent) extracted as functions; detectable as ctorPlugin metadatametadata.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
F3Methods expose typed parameters when the grammar has themparameters[].param_typePlugin unit test; dynamic langs may leave None but still extract names
F4CFG/def_use: field-access LHS recorded as member write (obj.field; -> normalized to .)def_use.rs + lang kindsCovered by shared field-access kinds + lang decl kinds (lexical_declaration, etc.)
F5Best-effort local/param types for CFG functionsfield_write_locals.rsmerge_local_types("{id}", …) recovers formals + typed locals (or copy-assign inference for JS)
F6Golden mutation fixture: type T with field write outside ctor → cpg mutations / index query hits it with --exclude-ctorsfield_write::tests::{id}_cfg_captures_field_write_and_queryExactly one non-ctor hit for the typed write
F7Document resolution limits (no reflection, no full inference)This doc + hybrid planHonesty 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)

LanguageCtor conventionType strength
Java / C#Real constructors → Type.<init>Strong
C++Name == enclosing class → Type::<init>Strong
GoNewT returning T/*TT.<init> (heuristic)Strong on structs
Rustfn new in impl → Type::<init>Strong on structs
TypeScript / JavaScriptconstructor method → class .<init>TS strong; JS weak (params may be untyped; graph param types / copy inference help)
Python__init__Class.<init>; harvest self.x fieldsAnnotations when present
CNo language ctors; struct fields + typed params requiredStrong on structs

Java extract honesty (java-extract-gaps + java-grammar-remainder + java-gql-remainder-gates):

  • Annotation types are :Annotation nodes (not :Interface). Usages emit AnnotatedWith; no classpath/FQN resolution beyond imports/package best-effort.
  • Records are Class with metadata.is_record; compact ctors and <clinit> / <initblock>N are CFG entry points.
  • Annotation elements are Functions with is_annotation_element; interface constant_declaration becomes fields.
  • Generics/throws are 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$N lookup is file-global (prefer enclosing-method CFG).
  • Anonymous classes use synthetic Outer.$AnonymousN owners.
  • Expression refs: field reads → References; array newInstantiates; .classReferences. 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 (fixture tests/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

LanguageCrate namePackage name on crates.io path
Gorgbuilder-lang-gorgbuilder-lang-go
Javargbuilder-lang-javargbuilder-lang-java
TypeScriptrgbuilder-lang-typescripthyphens, not underscores
  • Directory: crates/rgbuilder-lang-{id}/
  • language_id(): lowercase, no spaces ("go", "csharp", "javascript")
  • Tree-sitter dep: tree-sitter-{grammar} (version pin in crate Cargo.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

  1. Copy an existing custom plugin crate (e.g. rgbuilder-lang-go or rgbuilder-lang-java).
  2. Rename to crates/rgbuilder-lang-{id}/.
  3. 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"
  1. Implement lib.rs:
pub fn register(registry: &mut LanguageRegistry) {
    registry.register_language_plugin(Arc::new(MyPlugin::new().expect("init MyPlugin")));
}
  1. Add to workspace root Cargo.toml:
    • members list
    • [workspace.dependencies] rgbuilder-lang-{id} = { path = "...", version = "0.1.0" }
  2. 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:

MethodPurpose
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:

FeatureLook at
Calls + inheritancecrates/rgbuilder-lang-java/src/plugin.rs
Structs + methodscrates/rgbuilder-lang-go/src/plugin.rs
Classes + type inferencecrates/rgbuilder-lang-python/src/plugin.rs
Traits + functionscrates/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:

  1. Confirm build_cfg_for_function parses via language_profile::parse_source.
  2. Add visit_* handlers for language-specific statement node kinds.
  3. Add is_block_like() kinds if the grammar uses nonstandard block nodes (Go uses statement_list).
  4. 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:

  1. Add detect_mylang_patterns(&mut self).
  2. Register in detect_patterns via canonical_language_id match arm.
  3. 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:

  1. Replace generic plugin with custom plugin.rs (copy from Go/Java).
  2. Change languages.toml handler from "tree_sitter" to "custom".
  3. Implement extract_relations (at least Calls).
  4. Complete Layers B–F checklist (including Layer F).
  5. Keep config.rs only 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-patternWhy
Parse language X in rgbuilder-graph or discover_impl.rsBelongs in rgbuilder-lang-* + rgbuilder-analysis
Hardcode .ext lists in CLIUse language_profile / languages.toml
Tier 1 plugin without Calls relationsBlast 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 testsDashboard shows blocks but regressions go unnoticed
Duplicate grammar only in plugin cratergbuilder-analysis needs its own tree-sitter-* dep for CFG
Skip rgbuilder-languages registrationLanguage won’t ship in default rg-build binary
Full type checker inside the pluginOut of scope — bound resolution only (decl / param / field)

7. PR submission checklist

Copy into your PR description:

  • crates/rgbuilder-lang-{id}/ with LanguagePlugin + tests
  • languages.toml updated (handler = "custom")
  • Workspace Cargo.toml + bundle registration
  • language_profile.rs entry (cfg_enabled, taint_enabled, grammar)
  • cfg_builder.rs + tests for control-flow constructs
  • def_use.rs cases for declarations/assignments including field-access LHS
  • Layer F: fields[], is_constructor + .<init>/::<init>, typed params
  • Layer F: merge_local_types arm in field_write_locals.rs (or documented N/A)
  • Layer F: golden {id}_cfg_captures_field_write_and_query in field_write tests
  • taint.rs detect_{id}_patterns
  • extract_relations emits Calls (and inheritance if applicable)
  • Integration test + dashboard gate (or documented fixture path)
  • discover --with-cfg --with-security --with-taint smoke on fixture repo documented in test
  • No new CDN / online-only dashboard dependencies

8. Current parity snapshot (2026-07)

LanguageTierCallsCFGTaintDashboard gateLayer F (CPG mutations)
Java1 custom✅ + Extends/Implements/AnnotatedWith/Permits/Instantiates✅ (+ compact ctor, <clinit>)✅ richgbuilder golden✅ F1–F6
Go1 custom✅ deep✅ richdashboard_ecommerce_go✅ F1–F6
C#1 customdashboard_ecommerce_csharp✅ F1–F6
C1 customdashboard_ecommerce_c✅ F1/F3–F6 (no native ctors)
C++1 customdashboard_ecommerce_cpp✅ F1–F6
Python1 custom✅ richestdashboard_ecommerce_python✅ F1–F6
Rust1 custom✅ richdashboard_ecommerce_rust✅ F1–F6
JS / TS1 custom✅ richdashboard_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::LanguagePlugin and the reference crates above.
  • For dashboard contract details, see tests/dashboard_harness.rs and docs/dashboard-design.md.