From 852ed779a62c85e376e189a113bd6e689cf389f2 Mon Sep 17 00:00:00 2001 From: sunerpy Date: Wed, 15 Jul 2026 14:53:15 +0800 Subject: [PATCH] feat(extraction): index Terraform/HCL (.tf/.tfvars/.tofu) at the extraction tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the extraction half of upstream colby Terraform support (#1173). Terraform is HCL; the tree-sitter-hcl grammar parses `.tf`, `.tfvars`, and `.tofu`. Those files now index their blocks as qualified symbols — resource and data blocks as `type.name` / `data.type.name`, `module.name`, `var.name`, `output.name`, providers as namespaces, and each `locals` attribute as `local.key` — plus the traversal references (`var.X`, `local.X`, `module.M`, `data.T.N`, `.`) in block bodies, via the tree-sitter-hcl crates.io grammar (no vendored grammar). HCL is block-based: every construct is a `block` distinguished by its first identifier, so TERRAFORM_SPEC carries empty type-sets and a single Language::Terraform-guarded `visit_terraform_node` walker arm (the pattern Nix and GDScript use) dispatches on the block type and emits the qualified symbol and its body references. The arm is gated to Language::Terraform and shape-guarded, so no other language's extraction changes. Adds the Language::Terraform variant, the three extension mappings, the store language-parser arm, and a dedicated terraform golden fixture. Same-file references resolve through the existing name matcher (e.g. `var.region` to its variable, `module.vpc` to the module block); an undeclared target stays an unresolved reference. The module-boundary framework resolver from the upstream commit — the `:`-scoped `module.M:var`/`:output` cross-module wiring, the directory-scoped `.tfvars`-to-variable resolution, and the cross-module impact bridge — is deferred: this port has one concrete FrameworkResolver (Godot), and a second is outside the extraction-tier scope, the same disposition used for the ArkTS ArkUI bridges and the Nix module-system synthesizer. The language enum is a stored TEXT value, not DDL, so the golden .schema is unchanged and the nine existing golden fixtures are byte-identical (no regen); only the new terraform fixture is added. --- Cargo.lock | 31 +- README.md | 6 +- .../fixtures/terraform/main.tf | 30 ++ crates/codegraph-bench/tests/equivalence.rs | 33 ++ crates/codegraph-core/src/types.rs | 9 +- crates/codegraph-extract/Cargo.toml | 1 + crates/codegraph-extract/src/engine.rs | 10 +- crates/codegraph-extract/src/lang/mod.rs | 7 + .../codegraph-extract/src/lang/terraform.rs | 444 ++++++++++++++++++ crates/codegraph-extract/src/walker.rs | 254 ++++++++++ crates/codegraph-store/src/queries.rs | 9 + docs/equivalence.md | 53 +++ docs/languages.md | 61 +-- reference/golden/terraform/colby.db | Bin 0 -> 151552 bytes reference/golden/terraform/edges.json | 143 ++++++ reference/golden/terraform/files.json | 10 + reference/golden/terraform/nodes.json | 222 +++++++++ reference/golden/terraform/refs.json | 12 + reference/golden/terraform/schema.sql | 117 +++++ 19 files changed, 1406 insertions(+), 46 deletions(-) create mode 100644 crates/codegraph-bench/fixtures/terraform/main.tf create mode 100644 crates/codegraph-extract/src/lang/terraform.rs create mode 100644 reference/golden/terraform/colby.db create mode 100644 reference/golden/terraform/edges.json create mode 100644 reference/golden/terraform/files.json create mode 100644 reference/golden/terraform/nodes.json create mode 100644 reference/golden/terraform/refs.json create mode 100644 reference/golden/terraform/schema.sql diff --git a/Cargo.lock b/Cargo.lock index 6c80e93..886f024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -432,7 +432,7 @@ dependencies = [ [[package]] name = "codegraph-bench" -version = "0.35.0" +version = "0.36.0" dependencies = [ "anyhow", "clap", @@ -447,7 +447,7 @@ dependencies = [ [[package]] name = "codegraph-core" -version = "0.35.0" +version = "0.36.0" dependencies = [ "anyhow", "serde", @@ -463,7 +463,7 @@ dependencies = [ [[package]] name = "codegraph-daemon" -version = "0.35.0" +version = "0.36.0" dependencies = [ "anyhow", "codegraph-core", @@ -483,7 +483,7 @@ dependencies = [ [[package]] name = "codegraph-extract" -version = "0.35.0" +version = "0.36.0" dependencies = [ "anyhow", "codegraph-core", @@ -502,6 +502,7 @@ dependencies = [ "tree-sitter-dart", "tree-sitter-gdscript", "tree-sitter-go", + "tree-sitter-hcl", "tree-sitter-html", "tree-sitter-java", "tree-sitter-javascript", @@ -528,7 +529,7 @@ dependencies = [ [[package]] name = "codegraph-graph" -version = "0.35.0" +version = "0.36.0" dependencies = [ "anyhow", "codegraph-core", @@ -542,7 +543,7 @@ dependencies = [ [[package]] name = "codegraph-mcp" -version = "0.35.0" +version = "0.36.0" dependencies = [ "anyhow", "axum", @@ -565,7 +566,7 @@ dependencies = [ [[package]] name = "codegraph-resolve" -version = "0.35.0" +version = "0.36.0" dependencies = [ "anyhow", "codegraph-core", @@ -579,7 +580,7 @@ dependencies = [ [[package]] name = "codegraph-rs" -version = "0.35.0" +version = "0.36.0" dependencies = [ "anyhow", "assert_cmd", @@ -615,7 +616,7 @@ dependencies = [ [[package]] name = "codegraph-store" -version = "0.35.0" +version = "0.36.0" dependencies = [ "anyhow", "codegraph-core", @@ -627,7 +628,7 @@ dependencies = [ [[package]] name = "codegraph-watch" -version = "0.35.0" +version = "0.36.0" dependencies = [ "anyhow", "codegraph-core", @@ -3504,6 +3505,16 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-hcl" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7b2cc3d7121553b84309fab9d11b3ff3d420403eef9ae50f9fd1cd9d9cf012" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-html" version = "0.23.2" diff --git a/README.md b/README.md index e9c2878..6efe80e 100644 --- a/README.md +++ b/README.md @@ -439,7 +439,7 @@ Omit `--install` to print to stdout. Full per-shell install paths and notes: ## What CodeGraph Does (and Doesn't) -**Does:** deterministic code-structure extraction across 35 languages (TypeScript, +**Does:** deterministic code-structure extraction across 36 languages (TypeScript, Python, Go, Rust, Java, C/C++, C#, Vue, Svelte, GDScript, and more — see [`docs/languages.md`](docs/languages.md)), cross-file resolution (including Godot project graphs), graph traversal, FTS5 search, whole-graph export with @@ -454,9 +454,9 @@ beyond the fixed `LANGUAGES` set. ## Supported Languages -CodeGraph supports **35 languages** grouped by extraction depth. Quick overview: +CodeGraph supports **36 languages** grouped by extraction depth. Quick overview: -- **Tier 1 — Full symbol extraction (26):** TypeScript, TSX, JavaScript, JSX, ArkTS, Python, Go, Rust, Java, C, C++, C#, PHP, Ruby, Swift, Kotlin, Dart, Scala, Lua, Luau, Objective-C, R, Solidity, Nix, GDScript, Pascal. +- **Tier 1 — Full symbol extraction (27):** TypeScript, TSX, JavaScript, JSX, ArkTS, Python, Go, Rust, Java, C, C++, C#, PHP, Ruby, Swift, Kotlin, Dart, Scala, Lua, Luau, Objective-C, R, Solidity, Nix, Terraform, GDScript, Pascal. - **Tier 2 — Embedded / template extraction (6):** Vue, Svelte, Astro, Razor/`.cshtml`, Liquid, XML/MyBatis mapper. - **Tier 3 — File-level only (3):** YAML, Twig, Properties. diff --git a/crates/codegraph-bench/fixtures/terraform/main.tf b/crates/codegraph-bench/fixtures/terraform/main.tf new file mode 100644 index 0000000..a8ecacb --- /dev/null +++ b/crates/codegraph-bench/fixtures/terraform/main.tf @@ -0,0 +1,30 @@ +terraform {} + +provider "aws" {} + +variable "region" {} + +locals { + name = "demo" + tagged = var.region +} + +data "aws_ami" "ubuntu" {} + +resource "aws_s3_bucket" "b" { + bucket = var.region + kms_key_id = aws_kms_key.logs.arn +} + +module "vpc" { + source = "./vpc" + region = var.region +} + +output "bucket_id" { + value = aws_s3_bucket.b.id +} + +output "vpc_id" { + value = module.vpc.id +} diff --git a/crates/codegraph-bench/tests/equivalence.rs b/crates/codegraph-bench/tests/equivalence.rs index 0c93da1..efe0227 100644 --- a/crates/codegraph-bench/tests/equivalence.rs +++ b/crates/codegraph-bench/tests/equivalence.rs @@ -179,6 +179,31 @@ fn nix_db_is_self_equivalent_to_nix_golden() { assert_equivalent(&nix_db(), &nix_golden_dir()).unwrap(); } +#[test] +fn generated_golden_matches_committed_terraform_fixture() { + // Guards Terraform/HCL extraction (upstream #1173, extraction slice only): + // the `.tf`/`.tfvars`/`.tofu`->Terraform mapping, block-type dispatch + // (resource/data->Class, module->Module, variable/output->Variable, + // provider->Namespace, locals->Constant per attr) with qualified names, and + // plain attribute-expression traversal refs + // (var.X/local.X/module.M/data.T.N/.)->References with built-ins + // skipped. The module-boundary TerraformResolver, emitModuleWiring's + // :-scoped refs, and the .tfvars var ref are DEFERRED, so no :-scoped ref is + // emitted; the undeclared aws_kms_key.logs stays unresolved. + let tempdir = TestDir::new("generated-golden-terraform"); + write_golden(&terraform_db(), tempdir.path()).unwrap(); + + let expected = load_golden(&terraform_golden_dir()).unwrap(); + let actual = load_golden(tempdir.path()).unwrap(); + + diff_canonical(&expected, &actual, None).unwrap(); +} + +#[test] +fn terraform_db_is_self_equivalent_to_terraform_golden() { + assert_equivalent(&terraform_db(), &terraform_golden_dir()).unwrap(); +} + #[test] fn tier1_node_drift_is_reported() { let expected = load_golden(&mini_golden_dir()).unwrap(); @@ -297,6 +322,14 @@ fn nix_golden_dir() -> PathBuf { workspace_root().join("reference/golden/nix") } +fn terraform_db() -> PathBuf { + workspace_root().join("reference/golden/terraform/colby.db") +} + +fn terraform_golden_dir() -> PathBuf { + workspace_root().join("reference/golden/terraform") +} + struct TestDir { path: PathBuf, } diff --git a/crates/codegraph-core/src/types.rs b/crates/codegraph-core/src/types.rs index 92ea64e..7459c43 100644 --- a/crates/codegraph-core/src/types.rs +++ b/crates/codegraph-core/src/types.rs @@ -49,7 +49,7 @@ pub const EDGE_KIND_STRINGS: [&str; 12] = [ "decorates", ]; -pub const LANGUAGE_STRINGS: [&str; 39] = [ +pub const LANGUAGE_STRINGS: [&str; 40] = [ "typescript", "javascript", "tsx", @@ -80,6 +80,7 @@ pub const LANGUAGE_STRINGS: [&str; 39] = [ "r", "solidity", "nix", + "terraform", "yaml", "twig", "xml", @@ -329,6 +330,8 @@ pub enum Language { Solidity, #[serde(rename = "nix")] Nix, + #[serde(rename = "terraform")] + Terraform, #[serde(rename = "yaml")] Yaml, #[serde(rename = "twig")] @@ -350,7 +353,7 @@ pub enum Language { } impl Language { - pub const ALL: [Self; 39] = [ + pub const ALL: [Self; 40] = [ Self::TypeScript, Self::JavaScript, Self::Tsx, @@ -381,6 +384,7 @@ impl Language { Self::R, Self::Solidity, Self::Nix, + Self::Terraform, Self::Yaml, Self::Twig, Self::Xml, @@ -424,6 +428,7 @@ impl Language { Self::R => "r", Self::Solidity => "solidity", Self::Nix => "nix", + Self::Terraform => "terraform", Self::Yaml => "yaml", Self::Twig => "twig", Self::Xml => "xml", diff --git a/crates/codegraph-extract/Cargo.toml b/crates/codegraph-extract/Cargo.toml index 72fe682..97fb9df 100644 --- a/crates/codegraph-extract/Cargo.toml +++ b/crates/codegraph-extract/Cargo.toml @@ -31,6 +31,7 @@ tree-sitter-css = "0.25.0" tree-sitter-dart = "0.2.0" tree-sitter-go = "0.25.0" tree-sitter-gdscript = "6.1.0" +tree-sitter-hcl = "1.1.0" tree-sitter-html = "0.23.2" tree-sitter-java = "0.23.5" tree-sitter-javascript = "0.25.0" diff --git a/crates/codegraph-extract/src/engine.rs b/crates/codegraph-extract/src/engine.rs index 39069f0..7978be6 100644 --- a/crates/codegraph-extract/src/engine.rs +++ b/crates/codegraph-extract/src/engine.rs @@ -84,6 +84,7 @@ pub fn builtin_language_for_ext(ext: &str) -> Option { "r" => Language::R, "sol" => Language::Solidity, "nix" => Language::Nix, + "tf" | "tfvars" | "tofu" => Language::Terraform, "yml" | "yaml" => Language::Yaml, "twig" => Language::Twig, "xml" => Language::Xml, @@ -872,7 +873,7 @@ mod tests { assert_eq!(detect_language("s.metal"), Language::Cpp); assert_eq!(detect_language("k.cu"), Language::Cpp); assert_eq!(detect_language("k.cuh"), Language::Cpp); - assert_eq!(Language::ALL.len(), 39); + assert_eq!(Language::ALL.len(), 40); } #[test] @@ -890,6 +891,13 @@ mod tests { assert_eq!(detect_language("flake.nix"), Language::Nix); } + #[test] + fn terraform_extensions_map_to_terraform() { + assert_eq!(detect_language("main.tf"), Language::Terraform); + assert_eq!(detect_language("prod.tfvars"), Language::Terraform); + assert_eq!(detect_language("main.tofu"), Language::Terraform); + } + #[test] fn plain_ts_stays_typescript() { assert_eq!(detect_language("m.ts"), Language::TypeScript); diff --git a/crates/codegraph-extract/src/lang/mod.rs b/crates/codegraph-extract/src/lang/mod.rs index 4a2074b..d26bda9 100644 --- a/crates/codegraph-extract/src/lang/mod.rs +++ b/crates/codegraph-extract/src/lang/mod.rs @@ -27,6 +27,7 @@ mod rust; mod scala; mod solidity; mod swift; +mod terraform; mod tsx; mod typescript; @@ -64,6 +65,11 @@ pub use rust::RUST_SPEC; pub use scala::SCALA_SPEC; pub use solidity::SOLIDITY_SPEC; pub use swift::SWIFT_SPEC; +pub use terraform::TERRAFORM_SPEC; +pub(crate) use terraform::{ + TerraformBlockDecl, collect_terraform_references, describe_terraform_block, + read_terraform_block_header, terraform_block_body, +}; pub use tsx::TSX_SPEC; pub use typescript::TYPESCRIPT_SPEC; @@ -94,6 +100,7 @@ pub fn spec_for_language(language: Language) -> Option<&'static dyn LanguageSpec Language::R => Some(&R_SPEC), Language::Solidity => Some(&SOLIDITY_SPEC), Language::Nix => Some(&NIX_SPEC), + Language::Terraform => Some(&TERRAFORM_SPEC), Language::Gdscript => Some(&GDSCRIPT_SPEC), _ => None, } diff --git a/crates/codegraph-extract/src/lang/terraform.rs b/crates/codegraph-extract/src/lang/terraform.rs new file mode 100644 index 0000000..8da5013 --- /dev/null +++ b/crates/codegraph-extract/src/lang/terraform.rs @@ -0,0 +1,444 @@ +//! Terraform / OpenTofu (`.tf` / `.tfvars` / `.tofu`) `LanguageSpec`. +//! +//! Extraction-tier port of `upstream extraction/languages/terraform.ts` (commit +//! `6c24f4b`, #1173 — the extraction slice only). HCL is intentionally generic: +//! ALL Terraform top-level constructs share the AST node kind `block`, +//! distinguished only by the first `identifier` child (the block "type": +//! `resource` / `data` / `module` / `variable` / `output` / `provider` / +//! `locals` / `terraform`). There are no C-family `class`/`struct`/`method`/ +//! `enum`/`interface`/`function` node kinds, so — faithful to upstream's +//! entirely empty `terraformExtractor` config — [`TerraformSpec`] returns `&[]` +//! for every type-set and the extraction is driven entirely by the +//! `Language::Terraform`-guarded `visit_terraform_node` walker extension in +//! [`crate::walker`]. +//! +//! The module-boundary framework RESOLVER (`resolution/frameworks/terraform.ts`), +//! its registration, the directory-scoping resolution gate, and the +//! extraction-side emitters that feed ONLY that resolver (`emitModuleWiring`'s +//! `module.M:file` / `module.M:var.X` / `module.M:output.X` `:`-scoped refs, the +//! `.tfvars` top-level-assignment `var.X` ref, and the `module.M:output.` +//! scoped half of `qualifyReference`) are DEFERRED — the port has exactly one +//! concrete `FrameworkResolver` (`GodotResolver`). +//! +//! The pure AST helper fns below are `pub(crate)` and re-exported through +//! [`crate::lang`] so the walker can reach them as `crate::lang::`. + +use codegraph_core::types::{Language, NodeKind}; +use tree_sitter::{Language as TsLanguage, Node}; + +use crate::spec::LanguageSpec; +use crate::walker::node_text; + +pub struct TerraformSpec; + +pub static TERRAFORM_SPEC: TerraformSpec = TerraformSpec; + +impl LanguageSpec for TerraformSpec { + fn language(&self) -> Language { + Language::Terraform + } + + fn tree_sitter_language(&self) -> TsLanguage { + tree_sitter_hcl::LANGUAGE.into() + } + + fn function_types(&self) -> &'static [&'static str] { + &[] + } + + fn class_types(&self) -> &'static [&'static str] { + &[] + } + + fn method_types(&self) -> &'static [&'static str] { + &[] + } + + fn interface_types(&self) -> &'static [&'static str] { + &[] + } + + fn struct_types(&self) -> &'static [&'static str] { + &[] + } + + fn enum_types(&self) -> &'static [&'static str] { + &[] + } + + fn enum_member_types(&self) -> &'static [&'static str] { + &[] + } + + fn type_alias_types(&self) -> &'static [&'static str] { + &[] + } + + fn import_types(&self) -> &'static [&'static str] { + &[] + } + + fn call_types(&self) -> &'static [&'static str] { + &[] + } + + fn variable_types(&self) -> &'static [&'static str] { + &[] + } + + fn name_field(&self) -> &'static str { + "" + } + + fn body_field(&self) -> &'static str { + "" + } + + fn params_field(&self) -> &'static str { + "" + } + + fn return_field(&self) -> &'static str { + "" + } +} + +/// Built-in reference heads that should NOT be resolved to project nodes. +/// Ports `BUILTIN_HEADS` (terraform.ts:37-43). +const BUILTIN_HEADS: [&str; 5] = ["each", "count", "self", "path", "terraform"]; + +/// Bare strings never treated as references. Ports `BUILTIN_KEYWORDS` +/// (terraform.ts:46). +const BUILTIN_KEYWORDS: [&str; 3] = ["null", "true", "false"]; + +/// A described top-level Terraform block: its `NodeKind`, symbol name, qualified +/// name, and signature. Ports the `BlockDecl` interface (terraform.ts:361-366). +pub(crate) struct TerraformBlockDecl { + pub kind: NodeKind, + pub name: String, + pub qualified_name: String, + pub signature: String, +} + +/// Read a `string_lit` value (its `template_literal` named child text), else +/// `""` (an empty `""` string parses with no `template_literal`). Ports +/// `stringLitValue` (terraform.ts:49-55). +pub(crate) fn terraform_string_lit_value(node: Node<'_>, source: &str) -> String { + node.named_children(&mut node.walk()) + .find(|c| c.kind() == "template_literal") + .map(|literal| node_text(literal, source)) + .unwrap_or_default() +} + +/// Block "type" and its label values. `None` if the block is malformed (first +/// named child is not an `identifier`). Ports `readBlockHeader` +/// (terraform.ts:58-79). +pub(crate) fn read_terraform_block_header( + block: Node<'_>, + source: &str, +) -> Option<(String, Vec)> { + let named: Vec> = block.named_children(&mut block.walk()).collect(); + let first = named.first()?; + if first.kind() != "identifier" { + return None; + } + let block_type = node_text(*first, source); + let mut labels: Vec = Vec::new(); + for child in named.iter().skip(1) { + match child.kind() { + "string_lit" => labels.push(terraform_string_lit_value(*child, source)), + // HCL allows unquoted identifier labels (rare in Terraform, legal). + "identifier" => labels.push(node_text(*child, source)), + _ => break, + } + } + Some((block_type, labels)) +} + +/// The `body` named child of a block. Ports `getBlockBody` +/// (terraform.ts:82-84). +pub(crate) fn terraform_block_body(block: Node<'_>) -> Option> { + block + .named_children(&mut block.walk()) + .find(|c| c.kind() == "body") +} + +/// Map a block type + labels to a symbol declaration. Ports `describeBlock` +/// (terraform.ts:368-435): resource/data → Class, module → Module, +/// variable/output → Variable, provider → Namespace. `None` for unknown types +/// or missing required labels. +pub(crate) fn describe_terraform_block( + block_type: &str, + labels: &[String], +) -> Option { + let first = labels.first(); + let second = labels.get(1); + match block_type { + "resource" => { + let (first, second) = (first?, second?); + Some(TerraformBlockDecl { + kind: NodeKind::Class, + name: format!("{first}.{second}"), + qualified_name: format!("{first}.{second}"), + signature: format!("resource \"{first}\" \"{second}\""), + }) + } + "data" => { + let (first, second) = (first?, second?); + Some(TerraformBlockDecl { + kind: NodeKind::Class, + name: format!("{first}.{second}"), + qualified_name: format!("data.{first}.{second}"), + signature: format!("data \"{first}\" \"{second}\""), + }) + } + "module" => { + let first = first?; + Some(TerraformBlockDecl { + kind: NodeKind::Module, + name: first.clone(), + qualified_name: format!("module.{first}"), + signature: format!("module \"{first}\""), + }) + } + "variable" => { + let first = first?; + Some(TerraformBlockDecl { + kind: NodeKind::Variable, + name: first.clone(), + qualified_name: format!("var.{first}"), + signature: format!("variable \"{first}\""), + }) + } + "output" => { + let first = first?; + Some(TerraformBlockDecl { + kind: NodeKind::Variable, + name: first.clone(), + qualified_name: format!("output.{first}"), + signature: format!("output \"{first}\""), + }) + } + "provider" => { + let first = first?; + Some(TerraformBlockDecl { + kind: NodeKind::Namespace, + name: first.clone(), + qualified_name: format!("provider.{first}"), + signature: format!("provider \"{first}\""), + }) + } + _ => None, + } +} + +/// Turn a reference head + attribute chain into qualified name(s). Ports +/// `qualifyReference` (terraform.ts:158-189) MINUS the DEFERRED +/// `module.M:output.` scoped half — a `module.M.out` chain emits ONLY the +/// plain `module.M`. +fn qualify_terraform_reference(head: &str, attrs: &[String]) -> Vec { + match head { + "var" => attrs + .first() + .map(|a| vec![format!("var.{a}")]) + .unwrap_or_default(), + "local" => attrs + .first() + .map(|a| vec![format!("local.{a}")]) + .unwrap_or_default(), + "module" => { + // Plain `module.M` only; the `module.M:output.` scoped half is + // DEFERRED (it feeds the deferred TerraformResolver). + attrs + .first() + .map(|a| vec![format!("module.{a}")]) + .unwrap_or_default() + } + "data" => match (attrs.first(), attrs.get(1)) { + (Some(t), Some(n)) => vec![format!("data.{t}.{n}")], + _ => Vec::new(), + }, + _ => { + // .[....] — managed resource (e.g. + // aws_s3_bucket.my). Skip a bare head with no dotted chain. + attrs + .first() + .map(|a| vec![format!("{head}.{a}")]) + .unwrap_or_default() + } + } +} + +/// Read a `variable_expr` head + its `get_attr` sibling chain and emit qualified +/// reference(s). Ports `emitRefFromVariableExpr` (terraform.ts:126-155). +fn emit_ref_from_variable_expr( + var_expr: Node<'_>, + source: &str, + out: &mut Vec<(String, u32, u32)>, +) { + let Some(id) = var_expr + .named_children(&mut var_expr.walk()) + .find(|c| c.kind() == "identifier") + else { + return; + }; + let head = node_text(id, source); + if BUILTIN_HEADS.contains(&head.as_str()) || BUILTIN_KEYWORDS.contains(&head.as_str()) { + return; + } + + let mut attrs: Vec = Vec::new(); + let mut cursor = var_expr.next_named_sibling(); + while let Some(node) = cursor { + match node.kind() { + "get_attr" => { + let Some(attr_id) = node + .named_children(&mut node.walk()) + .find(|c| c.kind() == "identifier") + else { + break; + }; + attrs.push(node_text(attr_id, source)); + cursor = node.next_named_sibling(); + } + "index" | "new_index" | "legacy_index" | "splat" | "attr_splat" | "full_splat" => { + // foo[0], foo[*], foo.* — keep walking, add no segment. + cursor = node.next_named_sibling(); + } + _ => break, + } + } + + let line = var_expr.start_position().row as u32 + 1; + let col = var_expr.start_position().column as u32; + for qname in qualify_terraform_reference(&head, &attrs) { + out.push((qname, line, col)); + } +} + +/// BFS an `expression` subtree, emitting a qualified reference for every dotted +/// name whose head is a Terraform reference root. Ports `collectReferences` +/// (terraform.ts:106-121). Returns `(qualified_name, line, col)` tuples in +/// traversal order. +pub(crate) fn collect_terraform_references( + expr: Node<'_>, + source: &str, +) -> Vec<(String, u32, u32)> { + let mut out: Vec<(String, u32, u32)> = Vec::new(); + let mut queue: std::collections::VecDeque> = std::collections::VecDeque::new(); + queue.push_back(expr); + while let Some(node) = queue.pop_front() { + if node.kind() == "variable_expr" { + emit_ref_from_variable_expr(node, source, &mut out); + } + for child in node.named_children(&mut node.walk()) { + queue.push_back(child); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(src: &str) -> tree_sitter::Tree { + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(&tree_sitter_hcl::LANGUAGE.into()) + .unwrap(); + parser.parse(src, None).unwrap() + } + + fn first_of_kind<'t>(node: Node<'t>, kind: &str) -> Option> { + if node.kind() == kind { + return Some(node); + } + for i in 0..node.named_child_count() as u32 { + let child = node.named_child(i)?; + if let Some(found) = first_of_kind(child, kind) { + return Some(found); + } + } + None + } + + #[test] + fn terraform_spec_has_empty_type_sets() { + assert!(TERRAFORM_SPEC.function_types().is_empty()); + assert!(TERRAFORM_SPEC.class_types().is_empty()); + assert!(TERRAFORM_SPEC.method_types().is_empty()); + assert!(TERRAFORM_SPEC.call_types().is_empty()); + assert!(TERRAFORM_SPEC.variable_types().is_empty()); + assert!(TERRAFORM_SPEC.import_types().is_empty()); + assert_eq!(TERRAFORM_SPEC.language(), Language::Terraform); + } + + #[test] + fn describe_terraform_block_resource() { + let decl = describe_terraform_block("resource", &["aws_s3_bucket".into(), "b".into()]) + .expect("resource decl"); + assert_eq!(decl.kind, NodeKind::Class); + assert_eq!(decl.name, "aws_s3_bucket.b"); + assert_eq!(decl.qualified_name, "aws_s3_bucket.b"); + } + + #[test] + fn describe_terraform_block_data() { + let decl = describe_terraform_block("data", &["aws_ami".into(), "ubuntu".into()]) + .expect("data decl"); + assert_eq!(decl.kind, NodeKind::Class); + assert_eq!(decl.qualified_name, "data.aws_ami.ubuntu"); + } + + #[test] + fn describe_terraform_block_module() { + let decl = describe_terraform_block("module", &["vpc".into()]).expect("module decl"); + assert_eq!(decl.kind, NodeKind::Module); + assert_eq!(decl.qualified_name, "module.vpc"); + } + + #[test] + fn describe_terraform_block_variable() { + let decl = describe_terraform_block("variable", &["region".into()]).expect("variable decl"); + assert_eq!(decl.kind, NodeKind::Variable); + assert_eq!(decl.qualified_name, "var.region"); + } + + #[test] + fn describe_terraform_block_provider() { + let decl = describe_terraform_block("provider", &["aws".into()]).expect("provider decl"); + assert_eq!(decl.kind, NodeKind::Namespace); + assert_eq!(decl.qualified_name, "provider.aws"); + } + + #[test] + fn describe_terraform_block_unknown_is_none() { + assert!(describe_terraform_block("terraform", &[]).is_none()); + assert!(describe_terraform_block("resource", &["only_one".into()]).is_none()); + } + + #[test] + fn qualify_terraform_reference_module_output_is_plain() { + // A two-segment module.M.out chain emits ONLY plain module.M (the + // module.M:output.out scoped half is DEFERRED). + assert_eq!( + qualify_terraform_reference("module", &["vpc".into(), "id".into()]), + vec!["module.vpc".to_string()] + ); + } + + #[test] + fn terraform_parses_block() { + let tree = parse("resource \"aws_s3_bucket\" \"b\" { bucket = var.name }"); + let root = tree.root_node(); + let block = first_of_kind(root, "block").expect("a block node"); + let (block_type, labels) = read_terraform_block_header( + block, + "resource \"aws_s3_bucket\" \"b\" { bucket = var.name }", + ) + .expect("header"); + assert_eq!(block_type, "resource"); + assert_eq!(labels, vec!["aws_s3_bucket".to_string(), "b".to_string()]); + } +} diff --git a/crates/codegraph-extract/src/walker.rs b/crates/codegraph-extract/src/walker.rs index 35fe0d6..3e0d61e 100644 --- a/crates/codegraph-extract/src/walker.rs +++ b/crates/codegraph-extract/src/walker.rs @@ -230,10 +230,152 @@ impl<'a, 'tree> TreeSitterWalker<'a, 'tree> { Language::Cpp => self.visit_cpp_node(node), Language::Solidity => self.visit_solidity_node(node), Language::Nix => self.visit_nix_node(node), + Language::Terraform => self.visit_terraform_node(node), _ => false, } } + /// Terraform/HCL extraction (upstream `terraformExtractor.visitNode`, + /// `terraform.ts`, #1173 — the extraction slice only). HCL's AST has no + /// C-family type-set node kinds — EVERY top-level construct is a `block` + /// distinguished only by its first `identifier` child — so `TERRAFORM_SPEC` + /// returns `&[]` for every type-set and this custom visitor (like + /// `visit_nix_node`) is the SOLE owner of the `block` kind. Every other kind + /// returns `false` → generic `visit_named_children` descent (so + /// `config_file`/`body` are walked into automatically, reaching each + /// `block`). Strictly `Language::Terraform`-guarded. + /// + /// Block-type dispatch: `resource`/`data` → Class, `module` → Module, + /// `variable`/`output` → Variable, `provider` → Namespace, `locals` → + /// Constant per attribute; body attribute-expression traversal refs + /// (`var.X`/`local.X`/`module.M`/`data.T.N`/`.`) → + /// `EdgeKind::References` with built-ins skipped. The `emitModuleWiring` + /// `:`-scoped refs and the `.tfvars` var ref are DEFERRED (they feed the + /// deferred `TerraformResolver`). + fn visit_terraform_node(&mut self, node: SyntaxNode<'tree>) -> bool { + if node.kind() != "block" { + // .tfvars top-level-assignment `var.X` ref DEFERRED — no special + // case; let the generic walker descend. + return false; + } + + let Some((block_type, labels)) = + crate::lang::read_terraform_block_header(node, self.source) + else { + return false; + }; + let body = crate::lang::terraform_block_body(node); + + // locals: every body attribute becomes its own Constant. + if block_type == "locals" && labels.is_empty() { + self.visit_terraform_locals(body); + return true; + } + + // terraform { ... } settings block — no symbols, no refs. + if block_type == "terraform" && labels.is_empty() { + return true; + } + + // resource / data / module / variable / output / provider. + let Some(decl) = crate::lang::describe_terraform_block(&block_type, &labels) else { + // Unknown top-level block — generic descent. + return false; + }; + let crate::lang::TerraformBlockDecl { + kind, + name, + qualified_name, + signature, + } = decl; + let is_exported = kind == NodeKind::Variable; + if let Some(created) = self.create_node( + kind, + &name, + node, + NodeExtra { + signature: Some(signature), + is_exported, + qualified_name: Some(qualified_name), + ..NodeExtra::default() + }, + ) { + self.node_stack.push(created.id); + if let Some(body) = body { + self.emit_terraform_references_in_body(body); + } + // emitModuleWiring for `module` blocks DEFERRED. + self.node_stack.pop(); + } + true + } + + /// `locals { k = … }` → one `NodeKind::Constant` per body `attribute` + /// (qualified `local.`), plus traversal refs inside each attr's + /// `expression`. Ports `emitLocals` (terraform.ts:437-524). + fn visit_terraform_locals(&mut self, body: Option>) { + let Some(body) = body else { + return; + }; + for attr in body.named_children(&mut body.walk()) { + if attr.kind() != "attribute" { + continue; + } + let Some(id_node) = attr + .named_children(&mut attr.walk()) + .find(|c| c.kind() == "identifier") + else { + continue; + }; + let name = node_text(id_node, self.source); + if let Some(created) = self.create_node( + NodeKind::Constant, + &name, + attr, + NodeExtra { + signature: Some(format!("local.{name}")), + qualified_name: Some(format!("local.{name}")), + ..NodeExtra::default() + }, + ) { + if let Some(expr) = attr + .named_children(&mut attr.walk()) + .find(|c| c.kind() == "expression") + { + let refs = crate::lang::collect_terraform_references(expr, self.source); + for (qname, _line, _col) in refs { + self.push_ref(&created.id, &qname, EdgeKind::References, expr); + } + } + } + } + } + + /// BFS the block body; when a node's kind is `expression`, run + /// `collect_terraform_references` and push each ref from the current owner, + /// then DON'T descend into that expression (the collector already walked + /// it). Ports `emitReferencesInBody` (terraform.ts:526-550). + fn emit_terraform_references_in_body(&mut self, body: SyntaxNode<'tree>) { + let Some(from_id) = self.node_stack.last().cloned() else { + return; + }; + let mut queue: std::collections::VecDeque> = + std::collections::VecDeque::new(); + queue.push_back(body); + while let Some(node) = queue.pop_front() { + if node.kind() == "expression" { + let refs = crate::lang::collect_terraform_references(node, self.source); + for (qname, _line, _col) in refs { + self.push_ref(&from_id, &qname, EdgeKind::References, node); + } + continue; + } + for child in node.named_children(&mut node.walk()) { + queue.push_back(child); + } + } + } + /// Nix extraction (upstream `nixExtractor.visitNode`, `nix.ts`, #1190 — the /// extraction slice only). Nix's expression AST has no C-family type-set /// node kinds, so `NIX_SPEC` returns `&[]` for every type-set and this @@ -5792,4 +5934,116 @@ public: assert!(has_node(&nodes, NodeKind::Variable, "lib")); assert!(has_node(&nodes, NodeKind::Variable, "pkgs")); } + + // ---- Terraform / HCL (hook-driven; block-type dispatch) ---- + + #[test] + fn terraform_resource_is_class_qualified() { + let (nodes, _refs) = run( + "main.tf", + "resource \"aws_s3_bucket\" \"b\" {}", + Language::Terraform, + ); + let n = node(&nodes, NodeKind::Class, "aws_s3_bucket.b"); + assert_eq!(n.qualified_name, "aws_s3_bucket.b"); + } + + #[test] + fn terraform_variable_is_variable() { + let (nodes, _refs) = run("main.tf", "variable \"region\" {}", Language::Terraform); + let n = node(&nodes, NodeKind::Variable, "region"); + assert_eq!(n.qualified_name, "var.region"); + assert!(n.is_exported); + } + + #[test] + fn terraform_output_is_variable_with_ref() { + let (nodes, refs) = run( + "main.tf", + "output \"arn\" { value = aws_s3_bucket.b.arn }", + Language::Terraform, + ); + let n = node(&nodes, NodeKind::Variable, "arn"); + assert_eq!(n.qualified_name, "output.arn"); + assert!(has_ref(&refs, EdgeKind::References, "aws_s3_bucket.b")); + } + + #[test] + fn terraform_module_is_module_no_scoped_ref() { + let (nodes, refs) = run( + "main.tf", + "module \"vpc\" { source = \"./vpc\" }", + Language::Terraform, + ); + let n = node(&nodes, NodeKind::Module, "vpc"); + assert_eq!(n.qualified_name, "module.vpc"); + assert!( + !refs.iter().any(|r| r.reference_name.contains(':')), + "no :-scoped module wiring ref (emitModuleWiring DEFERRED): {refs:?}" + ); + } + + #[test] + fn terraform_provider_is_namespace() { + let (nodes, _refs) = run("main.tf", "provider \"aws\" {}", Language::Terraform); + let n = node(&nodes, NodeKind::Namespace, "aws"); + assert_eq!(n.qualified_name, "provider.aws"); + } + + #[test] + fn terraform_locals_emit_constants() { + let (nodes, refs) = run( + "main.tf", + "locals {\n name = \"x\"\n region = var.r\n}", + Language::Terraform, + ); + assert_eq!( + node(&nodes, NodeKind::Constant, "name").qualified_name, + "local.name" + ); + assert_eq!( + node(&nodes, NodeKind::Constant, "region").qualified_name, + "local.region" + ); + assert!(has_ref(&refs, EdgeKind::References, "var.r")); + } + + #[test] + fn terraform_body_ref_var() { + let (_nodes, refs) = run( + "main.tf", + "resource \"aws_s3_bucket\" \"b\" { bucket = var.name }", + Language::Terraform, + ); + assert!(has_ref(&refs, EdgeKind::References, "var.name")); + } + + #[test] + fn terraform_module_output_ref_is_plain() { + let (_nodes, refs) = run( + "main.tf", + "output \"vpc_id\" { value = module.vpc.id }", + Language::Terraform, + ); + assert!(has_ref(&refs, EdgeKind::References, "module.vpc")); + assert!( + !refs.iter().any(|r| r.reference_name.contains(':')), + "the module.vpc:output.id scoped half is DEFERRED: {refs:?}" + ); + } + + #[test] + fn terraform_builtins_skipped() { + let (_nodes, refs) = run( + "main.tf", + "resource \"x\" \"y\" {\n a = each.key\n b = count.index\n c = path.module\n}", + Language::Terraform, + ); + assert!( + !refs + .iter() + .any(|r| r.reference_kind == EdgeKind::References), + "built-in heads (each/count/path) emit no ref: {refs:?}" + ); + } } diff --git a/crates/codegraph-store/src/queries.rs b/crates/codegraph-store/src/queries.rs index b3e0c67..0cf3ca3 100644 --- a/crates/codegraph-store/src/queries.rs +++ b/crates/codegraph-store/src/queries.rs @@ -1526,6 +1526,7 @@ fn parse_language(value: String) -> rusqlite::Result { "r" => Language::R, "solidity" => Language::Solidity, "nix" => Language::Nix, + "terraform" => Language::Terraform, "yaml" => Language::Yaml, "twig" => Language::Twig, "xml" => Language::Xml, @@ -2199,6 +2200,14 @@ mod tests { assert_eq!(parse_language("nix".to_string()).unwrap(), Language::Nix); } + #[test] + fn parse_language_accepts_terraform() { + assert_eq!( + parse_language("terraform".to_string()).unwrap(), + Language::Terraform + ); + } + #[test] fn edges_by_target_and_all_getters() { let mut store = store("edges-target"); diff --git a/docs/equivalence.md b/docs/equivalence.md index 5e88a2e..8fc03fe 100644 --- a/docs/equivalence.md +++ b/docs/equivalence.md @@ -504,6 +504,59 @@ The `generated_golden_matches_committed_nix_fixture` and `nix_db_is_self_equivalent_to_nix_golden` tests in `crates/codegraph-bench/tests/equivalence.rs` enforce byte-stability. +### Terraform fixture + +A tenth golden fixture, `reference/golden/terraform/`, guards Terraform/OpenTofu +(HCL) extraction (upstream #1173 / `6c24f4b`, the extraction slice only). +Terraform is a **new `Language::Terraform` variant** backed by a **dedicated +`tree-sitter-hcl` grammar** (`.tf`/`.tfvars`/`.tofu` → `Language::Terraform`). +HCL is intentionally generic — every top-level construct is a `block` +distinguished only by its first `identifier` child — so `TERRAFORM_SPEC` has +all-empty type-sets and extraction is driven by the `Language::Terraform`-guarded +`visit_terraform_node` walker extension. The corpus +(`crates/codegraph-bench/fixtures/terraform/main.tf`) is a single deterministic +file with a `terraform {}` settings block, a `provider "aws"`, a +`variable "region"`, a `locals` block, a `data "aws_ami" "ubuntu"`, a +`resource "aws_s3_bucket" "b"`, a `module "vpc"`, and two `output` blocks. What +it guards: + +- the `.tf` file with `"language": "terraform"`; +- block-type dispatch: `resource`/`data` → `NodeKind::Class` (qualified `T.N` / + `data.T.N`), `module` → `NodeKind::Module` (`module.M`), `variable`/`output` → + `NodeKind::Variable` (`var.V` / `output.O`, `is_exported`), `provider` → + `NodeKind::Namespace` (`provider.P`), `locals` attributes → `NodeKind::Constant` + per attribute (`local.k`); +- plain attribute-expression traversal refs + (`var.X`/`local.X`/`module.M`/`data.T.N`/`.`) → `References`, with + built-in heads (`each`/`count`/`self`/`path`/`terraform`) skipped. + +The plain traversal refs with a unique same-file target resolve via the existing +generic qualified-name matcher: `var.region` ×3 → `variable "region"`, +`aws_s3_bucket.b` → the resource, `module.vpc` → the module (each an EDGE, absent +from `refs.json`). The undeclared `aws_kms_key.logs` stays the sole unresolved +`refs.json` row. The module-boundary `TerraformResolver`, `emitModuleWiring`'s +`:`-scoped refs (`module.M:file`/`:var.X`/`:output.X`), the `.tfvars` +top-level-assignment `var.X` ref, and the `module.M:output.` scoped half of +`qualifyReference` are all **DEFERRED** — the port keeps its single +`GodotResolver` — so no `:`-scoped ref is emitted. Adding the variant is +byte-neutral for `colby.schema.sql` (language is a stored TEXT value, not DDL) +and for the nine existing goldens (none holds a `.tf`/`.tfvars`/`.tofu` file). + +Regenerate reproducibly (identical recipe to the Nix fixture, substituting +`terraform`): + +```bash +rm -rf /tmp/cg-fixture-terraform && cp -r crates/codegraph-bench/fixtures/terraform /tmp/cg-fixture-terraform +cargo build --release -p codegraph-rs +CODEGRAPH_NO_DAEMON=1 CODEGRAPH_NO_WATCH=1 ./target/release/codegraph init /tmp/cg-fixture-terraform +cp /tmp/cg-fixture-terraform/.codegraph/codegraph.db reference/golden/terraform/colby.db +cargo run -p codegraph-bench --bin bench -- --gen-golden reference/golden/terraform/colby.db reference/golden/terraform +``` + +The `generated_golden_matches_committed_terraform_fixture` and +`terraform_db_is_self_equivalent_to_terraform_golden` tests in +`crates/codegraph-bench/tests/equivalence.rs` enforce byte-stability. + ### KNOWN_DIFFS.md format Tier-3 differences are allowlisted by grep-able lines in repo-root diff --git a/docs/languages.md b/docs/languages.md index ab43097..2c30441 100644 --- a/docs/languages.md +++ b/docs/languages.md @@ -4,7 +4,7 @@ CodeGraph extracts code structure deterministically using tree-sitter grammars a embedded extractors. No AI, vectors, or embeddings are involved. The output is byte-stable across runs. -**35 concrete languages** are supported, grouped into three extraction tiers based on what +**36 concrete languages** are supported, grouped into three extraction tiers based on what the extractor produces. > **Note on TypeScript/JavaScript variants:** `typescript` and `tsx`, and `javascript` and @@ -14,40 +14,41 @@ the extractor produces. --- -## Tier 1 — Full symbol extraction (26 languages) +## Tier 1 — Full symbol extraction (27 languages) Tree-sitter parses the file and extracts all symbols (functions, classes, structs, methods, variables, imports, etc.) plus call and dependency edges. This is the richest extraction level. -| Language | Extensions | Extraction | Notes | -| ----------- | ------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| TypeScript | `.ts` `.mts` `.cts` | Full tree-sitter | | -| TSX | `.tsx` | Full tree-sitter | TypeScript grammar, JSX syntax | -| JavaScript | `.js` `.mjs` `.cjs` `.xsjs` `.xsjslib` | Full tree-sitter | | -| JSX | `.jsx` | Full tree-sitter | JavaScript grammar, JSX syntax | -| ArkTS | `.ets` | Full tree-sitter | HarmonyOS / OpenHarmony; `tree-sitter-arkts` grammar. `@Component struct` → struct symbol. ArkUI dynamic-dispatch bridges deferred. Plain `.ts` stays TypeScript | -| Python | `.py` `.pyw` | Full tree-sitter | | -| Go | `.go` | Full tree-sitter | | -| Rust | `.rs` | Full tree-sitter | | -| Java | `.java` | Full tree-sitter | | -| C | `.c` `.h` | Full tree-sitter | `.h` may be promoted to C++ or Objective-C by heuristics | -| C++ | `.cpp` `.cc` `.cxx` `.hpp` `.hxx` | Full tree-sitter | | -| C# | `.cs` | Full tree-sitter | | -| PHP | `.php` `.module` `.install` `.theme` `.inc` | Full tree-sitter | | -| Ruby | `.rb` `.rake` | Full tree-sitter | | -| Swift | `.swift` | Full tree-sitter | | -| Kotlin | `.kt` `.kts` | Full tree-sitter | | -| Dart | `.dart` | Full tree-sitter | | -| Scala | `.scala` `.sc` | Full tree-sitter | | -| Lua | `.lua` | Full tree-sitter | | -| Luau | `.luau` | Full tree-sitter | Roblox Luau dialect | -| Objective-C | `.m` `.mm` | Full tree-sitter | | -| R | `.r` | Full tree-sitter | | -| Solidity | `.sol` | Full tree-sitter | `tree-sitter-solidity` grammar; contracts/libraries/interfaces, structs, enums, modifiers, events, errors; `is`-inheritance → Extends (resolver promotes to Implements for interfaces); emit/revert/modifier-guard call edges | -| Nix | `.nix` | Full tree-sitter | `tree-sitter-nix` grammar; `let`/attrset bindings, curried lambdas, `inherit`; `import`/`callPackage`/`imports`-list file imports; module-system option synthesizer deferred | -| GDScript | `.gd` | Full tree-sitter | Godot scripting; extracts functions, classes, enums, variables, signals, extends, preload. Dynamic dispatch edges (connect/get_node/$/%/call/group) added by the Godot resolver — see [`docs/godot.md`](godot.md) | -| Pascal | `.pas` `.dpr` `.dpk` `.lpr` `.dfm` `.fmx` | Full tree-sitter / custom | `.dfm`/`.fmx` form files use a custom path | +| Language | Extensions | Extraction | Notes | +| ----------- | ------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| TypeScript | `.ts` `.mts` `.cts` | Full tree-sitter | | +| TSX | `.tsx` | Full tree-sitter | TypeScript grammar, JSX syntax | +| JavaScript | `.js` `.mjs` `.cjs` `.xsjs` `.xsjslib` | Full tree-sitter | | +| JSX | `.jsx` | Full tree-sitter | JavaScript grammar, JSX syntax | +| ArkTS | `.ets` | Full tree-sitter | HarmonyOS / OpenHarmony; `tree-sitter-arkts` grammar. `@Component struct` → struct symbol. ArkUI dynamic-dispatch bridges deferred. Plain `.ts` stays TypeScript | +| Python | `.py` `.pyw` | Full tree-sitter | | +| Go | `.go` | Full tree-sitter | | +| Rust | `.rs` | Full tree-sitter | | +| Java | `.java` | Full tree-sitter | | +| C | `.c` `.h` | Full tree-sitter | `.h` may be promoted to C++ or Objective-C by heuristics | +| C++ | `.cpp` `.cc` `.cxx` `.hpp` `.hxx` | Full tree-sitter | | +| C# | `.cs` | Full tree-sitter | | +| PHP | `.php` `.module` `.install` `.theme` `.inc` | Full tree-sitter | | +| Ruby | `.rb` `.rake` | Full tree-sitter | | +| Swift | `.swift` | Full tree-sitter | | +| Kotlin | `.kt` `.kts` | Full tree-sitter | | +| Dart | `.dart` | Full tree-sitter | | +| Scala | `.scala` `.sc` | Full tree-sitter | | +| Lua | `.lua` | Full tree-sitter | | +| Luau | `.luau` | Full tree-sitter | Roblox Luau dialect | +| Objective-C | `.m` `.mm` | Full tree-sitter | | +| R | `.r` | Full tree-sitter | | +| Solidity | `.sol` | Full tree-sitter | `tree-sitter-solidity` grammar; contracts/libraries/interfaces, structs, enums, modifiers, events, errors; `is`-inheritance → Extends (resolver promotes to Implements for interfaces); emit/revert/modifier-guard call edges | +| Nix | `.nix` | Full tree-sitter | `tree-sitter-nix` grammar; `let`/attrset bindings, curried lambdas, `inherit`; `import`/`callPackage`/`imports`-list file imports; module-system option synthesizer deferred | +| Terraform | `.tf` `.tfvars` `.tofu` | Full tree-sitter | `tree-sitter-hcl` grammar (HCL/Terraform/OpenTofu); `resource`/`data`→class, `module`→module, `variable`/`output`→variable, `provider`→namespace, `locals`→constant; qualified names + `var`/`local`/`module`/`data`/resource traversal refs; module-boundary framework resolver deferred | +| GDScript | `.gd` | Full tree-sitter | Godot scripting; extracts functions, classes, enums, variables, signals, extends, preload. Dynamic dispatch edges (connect/get_node/$/%/call/group) added by the Godot resolver — see [`docs/godot.md`](godot.md) | +| Pascal | `.pas` `.dpr` `.dpk` `.lpr` `.dfm` `.fmx` | Full tree-sitter / custom | `.dfm`/`.fmx` form files use a custom path | --- diff --git a/reference/golden/terraform/colby.db b/reference/golden/terraform/colby.db new file mode 100644 index 0000000000000000000000000000000000000000..1415c6536042325cba196aff74eab4f705ff3161 GIT binary patch literal 151552 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 literal 0 HcmV?d00001 diff --git a/reference/golden/terraform/edges.json b/reference/golden/terraform/edges.json new file mode 100644 index 0000000..dc860b9 --- /dev/null +++ b/reference/golden/terraform/edges.json @@ -0,0 +1,143 @@ +[ + { + "col": 10, + "kind": "references", + "line": 25, + "metadata": { + "confidence": 0.95, + "resolvedBy": "qualified-name" + }, + "provenance": null, + "source": "variable:60a98b4d776ea6b99ec109f16565e238", + "target": "class:9ea649776b48847a71f06911daf0119e" + }, + { + "col": 10, + "kind": "references", + "line": 29, + "metadata": { + "confidence": 0.95, + "resolvedBy": "qualified-name" + }, + "provenance": null, + "source": "variable:20d776b47f45d7da835d4d4e82010d17", + "target": "module:6387ce4b696974e8905686ae83527943" + }, + { + "col": 11, + "kind": "references", + "line": 21, + "metadata": { + "confidence": 0.95, + "resolvedBy": "qualified-name" + }, + "provenance": null, + "source": "module:6387ce4b696974e8905686ae83527943", + "target": "variable:161a5352b21594f9a56b14df3143a6c4" + }, + { + "col": 11, + "kind": "references", + "line": 9, + "metadata": { + "confidence": 0.95, + "resolvedBy": "qualified-name" + }, + "provenance": null, + "source": "constant:60a0bf4de8e88ec795bc637f794b133e", + "target": "variable:161a5352b21594f9a56b14df3143a6c4" + }, + { + "col": 15, + "kind": "references", + "line": 15, + "metadata": { + "confidence": 0.95, + "resolvedBy": "qualified-name" + }, + "provenance": null, + "source": "class:9ea649776b48847a71f06911daf0119e", + "target": "variable:161a5352b21594f9a56b14df3143a6c4" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:main.tf", + "target": "class:4dd4b9246c74373ca3146282f84872bd" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:main.tf", + "target": "class:9ea649776b48847a71f06911daf0119e" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:main.tf", + "target": "constant:30d18d17f1263f980b13bb367101b29e" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:main.tf", + "target": "constant:60a0bf4de8e88ec795bc637f794b133e" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:main.tf", + "target": "module:6387ce4b696974e8905686ae83527943" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:main.tf", + "target": "namespace:97d1ebc29d6e37a70a7ade3c6e6441c5" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:main.tf", + "target": "variable:161a5352b21594f9a56b14df3143a6c4" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:main.tf", + "target": "variable:20d776b47f45d7da835d4d4e82010d17" + }, + { + "col": null, + "kind": "contains", + "line": null, + "metadata": null, + "provenance": null, + "source": "file:main.tf", + "target": "variable:60a98b4d776ea6b99ec109f16565e238" + } +] diff --git a/reference/golden/terraform/files.json b/reference/golden/terraform/files.json new file mode 100644 index 0000000..549b157 --- /dev/null +++ b/reference/golden/terraform/files.json @@ -0,0 +1,10 @@ +[ + { + "content_hash": "7df7854f67b585b19f9fb67712ff48434fc209255e89d426d2036200cd4d960c", + "errors": null, + "language": "terraform", + "node_count": 10, + "path": "main.tf", + "size": 387 + } +] diff --git a/reference/golden/terraform/nodes.json b/reference/golden/terraform/nodes.json new file mode 100644 index 0000000..200df10 --- /dev/null +++ b/reference/golden/terraform/nodes.json @@ -0,0 +1,222 @@ +[ + { + "decorators": null, + "docstring": null, + "end_column": 26, + "end_line": 12, + "file_path": "main.tf", + "id": "class:4dd4b9246c74373ca3146282f84872bd", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "terraform", + "name": "aws_ami.ubuntu", + "qualified_name": "data.aws_ami.ubuntu", + "return_type": null, + "signature": "data \"aws_ami\" \"ubuntu\"", + "start_column": 0, + "start_line": 12, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 1, + "end_line": 17, + "file_path": "main.tf", + "id": "class:9ea649776b48847a71f06911daf0119e", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "class", + "language": "terraform", + "name": "aws_s3_bucket.b", + "qualified_name": "aws_s3_bucket.b", + "return_type": null, + "signature": "resource \"aws_s3_bucket\" \"b\"", + "start_column": 0, + "start_line": 14, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 17, + "end_line": 8, + "file_path": "main.tf", + "id": "constant:30d18d17f1263f980b13bb367101b29e", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "constant", + "language": "terraform", + "name": "name", + "qualified_name": "local.name", + "return_type": null, + "signature": "local.name", + "start_column": 2, + "start_line": 8, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 21, + "end_line": 9, + "file_path": "main.tf", + "id": "constant:60a0bf4de8e88ec795bc637f794b133e", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "constant", + "language": "terraform", + "name": "tagged", + "qualified_name": "local.tagged", + "return_type": null, + "signature": "local.tagged", + "start_column": 2, + "start_line": 9, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 0, + "end_line": 31, + "file_path": "main.tf", + "id": "file:main.tf", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "file", + "language": "terraform", + "name": "main.tf", + "qualified_name": "main.tf", + "return_type": null, + "signature": null, + "start_column": 0, + "start_line": 1, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 1, + "end_line": 22, + "file_path": "main.tf", + "id": "module:6387ce4b696974e8905686ae83527943", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "module", + "language": "terraform", + "name": "vpc", + "qualified_name": "module.vpc", + "return_type": null, + "signature": "module \"vpc\"", + "start_column": 0, + "start_line": 19, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 17, + "end_line": 3, + "file_path": "main.tf", + "id": "namespace:97d1ebc29d6e37a70a7ade3c6e6441c5", + "is_abstract": 0, + "is_async": 0, + "is_exported": 0, + "is_static": 0, + "kind": "namespace", + "language": "terraform", + "name": "aws", + "qualified_name": "provider.aws", + "return_type": null, + "signature": "provider \"aws\"", + "start_column": 0, + "start_line": 3, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 20, + "end_line": 5, + "file_path": "main.tf", + "id": "variable:161a5352b21594f9a56b14df3143a6c4", + "is_abstract": 0, + "is_async": 0, + "is_exported": 1, + "is_static": 0, + "kind": "variable", + "language": "terraform", + "name": "region", + "qualified_name": "var.region", + "return_type": null, + "signature": "variable \"region\"", + "start_column": 0, + "start_line": 5, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 1, + "end_line": 30, + "file_path": "main.tf", + "id": "variable:20d776b47f45d7da835d4d4e82010d17", + "is_abstract": 0, + "is_async": 0, + "is_exported": 1, + "is_static": 0, + "kind": "variable", + "language": "terraform", + "name": "vpc_id", + "qualified_name": "output.vpc_id", + "return_type": null, + "signature": "output \"vpc_id\"", + "start_column": 0, + "start_line": 28, + "type_parameters": null, + "visibility": null + }, + { + "decorators": null, + "docstring": null, + "end_column": 1, + "end_line": 26, + "file_path": "main.tf", + "id": "variable:60a98b4d776ea6b99ec109f16565e238", + "is_abstract": 0, + "is_async": 0, + "is_exported": 1, + "is_static": 0, + "kind": "variable", + "language": "terraform", + "name": "bucket_id", + "qualified_name": "output.bucket_id", + "return_type": null, + "signature": "output \"bucket_id\"", + "start_column": 0, + "start_line": 24, + "type_parameters": null, + "visibility": null + } +] diff --git a/reference/golden/terraform/refs.json b/reference/golden/terraform/refs.json new file mode 100644 index 0000000..ec090be --- /dev/null +++ b/reference/golden/terraform/refs.json @@ -0,0 +1,12 @@ +[ + { + "candidates": null, + "col": 15, + "file_path": "main.tf", + "from_node_id": "class:9ea649776b48847a71f06911daf0119e", + "language": "terraform", + "line": 16, + "reference_kind": "references", + "reference_name": "aws_kms_key.logs" + } +] diff --git a/reference/golden/terraform/schema.sql b/reference/golden/terraform/schema.sql new file mode 100644 index 0000000..d36218f --- /dev/null +++ b/reference/golden/terraform/schema.sql @@ -0,0 +1,117 @@ +CREATE TABLE schema_versions ( +version INTEGER PRIMARY KEY, +applied_at INTEGER NOT NULL, +description TEXT +); +CREATE TABLE nodes ( +id TEXT PRIMARY KEY, +kind TEXT NOT NULL, +name TEXT NOT NULL, +qualified_name TEXT NOT NULL, +file_path TEXT NOT NULL, +language TEXT NOT NULL, +start_line INTEGER NOT NULL, +end_line INTEGER NOT NULL, +start_column INTEGER NOT NULL, +end_column INTEGER NOT NULL, +docstring TEXT, +signature TEXT, +visibility TEXT, +is_exported INTEGER DEFAULT 0, +is_async INTEGER DEFAULT 0, +is_static INTEGER DEFAULT 0, +is_abstract INTEGER DEFAULT 0, +decorators TEXT, -- JSON array +type_parameters TEXT, -- JSON array +return_type TEXT, -- normalized return/result type name (e.g. C++ method return, for receiver-type inference) +updated_at INTEGER NOT NULL +); +CREATE TABLE edges ( +id INTEGER PRIMARY KEY AUTOINCREMENT, +source TEXT NOT NULL, +target TEXT NOT NULL, +kind TEXT NOT NULL, +metadata TEXT, -- JSON object +line INTEGER, +col INTEGER, +provenance TEXT DEFAULT NULL, +FOREIGN KEY (source) REFERENCES nodes(id) ON DELETE CASCADE, +FOREIGN KEY (target) REFERENCES nodes(id) ON DELETE CASCADE +); +CREATE TABLE sqlite_sequence(name,seq); +CREATE TABLE files ( +path TEXT PRIMARY KEY, +content_hash TEXT NOT NULL, +language TEXT NOT NULL, +size INTEGER NOT NULL, +modified_at INTEGER NOT NULL, +indexed_at INTEGER NOT NULL, +node_count INTEGER DEFAULT 0, +errors TEXT -- JSON array +); +CREATE TABLE unresolved_refs ( +id INTEGER PRIMARY KEY AUTOINCREMENT, +from_node_id TEXT NOT NULL, +reference_name TEXT NOT NULL, +reference_kind TEXT NOT NULL, +line INTEGER NOT NULL, +col INTEGER NOT NULL, +candidates TEXT, -- JSON array +file_path TEXT NOT NULL DEFAULT '', +language TEXT NOT NULL DEFAULT 'unknown', +reference_subkind TEXT, +FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE +); +CREATE INDEX idx_nodes_kind ON nodes(kind); +CREATE INDEX idx_nodes_name ON nodes(name); +CREATE INDEX idx_nodes_qualified_name ON nodes(qualified_name); +CREATE INDEX idx_nodes_file_path ON nodes(file_path); +CREATE INDEX idx_nodes_language ON nodes(language); +CREATE INDEX idx_nodes_file_line ON nodes(file_path, start_line); +CREATE INDEX idx_nodes_lower_name ON nodes(lower(name)); +CREATE VIRTUAL TABLE nodes_fts USING fts5( +id, +name, +qualified_name, +docstring, +signature, +content='nodes', +content_rowid='rowid' +) +/* nodes_fts(id,name,qualified_name,docstring,signature) */; +CREATE TABLE 'nodes_fts_data'(id INTEGER PRIMARY KEY, block BLOB); +CREATE TABLE 'nodes_fts_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID; +CREATE TABLE 'nodes_fts_docsize'(id INTEGER PRIMARY KEY, sz BLOB); +CREATE TABLE 'nodes_fts_config'(k PRIMARY KEY, v) WITHOUT ROWID; +CREATE TRIGGER nodes_ai AFTER INSERT ON nodes BEGIN +INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring, signature) +VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring, NEW.signature); +END; +CREATE TRIGGER nodes_ad AFTER DELETE ON nodes BEGIN +INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring, signature) +VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring, OLD.signature); +END; +CREATE TRIGGER nodes_au AFTER UPDATE ON nodes BEGIN +INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring, signature) +VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring, OLD.signature); +INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring, signature) +VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring, NEW.signature); +END; +CREATE INDEX idx_edges_kind ON edges(kind); +CREATE INDEX idx_edges_source_kind ON edges(source, kind); +CREATE INDEX idx_edges_target_kind ON edges(target, kind); +CREATE UNIQUE INDEX idx_edges_identity +ON edges(source, target, kind, IFNULL(line, -1), IFNULL(col, -1)); +CREATE INDEX idx_files_language ON files(language); +CREATE INDEX idx_files_modified_at ON files(modified_at); +CREATE INDEX idx_unresolved_from_node ON unresolved_refs(from_node_id); +CREATE INDEX idx_unresolved_name ON unresolved_refs(reference_name); +CREATE INDEX idx_unresolved_file_path ON unresolved_refs(file_path); +CREATE INDEX idx_unresolved_from_name ON unresolved_refs(from_node_id, reference_name); +CREATE INDEX idx_edges_provenance ON edges(provenance); +CREATE TABLE project_metadata ( +key TEXT PRIMARY KEY, +value TEXT NOT NULL, +updated_at INTEGER NOT NULL +); +CREATE TABLE sqlite_stat1(tbl,idx,stat);