Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f5ee3de
chore: remove dead exports and un-export internal constant
carlos-alm Mar 17, 2026
f236b55
Merge remote-tracking branch 'origin/main' into worktree-titan-recon
carlos-alm Mar 17, 2026
17cdcb0
refactor: extract shared findNodes utility from cfg and dataflow feat…
carlos-alm Mar 17, 2026
a09740d
fix: replace empty catch blocks in db connection and migrations
carlos-alm Mar 17, 2026
b691fcc
fix: replace empty catch blocks in domain analysis layer
carlos-alm Mar 17, 2026
dadb383
fix: replace empty catch blocks in parser.js
carlos-alm Mar 17, 2026
22d94f4
fix: replace empty catch blocks in features layer
carlos-alm Mar 17, 2026
3b36534
refactor: decompose extractSymbolsWalk into per-category handlers
carlos-alm Mar 17, 2026
e1d7ee0
refactor: decompose extractPythonSymbols into per-category handlers
carlos-alm Mar 17, 2026
3a656bb
refactor: decompose extractJavaSymbols into per-category handlers
carlos-alm Mar 17, 2026
bf5b986
refactor: decompose remaining language extractors
carlos-alm Mar 17, 2026
eafdf19
refactor: decompose AST analysis visitors and engine into focused hel…
carlos-alm Mar 17, 2026
46a95ae
refactor: decompose domain builder stages into focused helpers
carlos-alm Mar 17, 2026
0a3fbc7
refactor: decompose domain analysis functions into focused helpers
carlos-alm Mar 17, 2026
b2f89f1
refactor: decompose buildComplexityMetrics
carlos-alm Mar 17, 2026
cb82258
refactor: decompose buildStructure into traversal, cohesion, and clas…
carlos-alm Mar 17, 2026
54b0067
refactor: decompose buildCFGData and buildDataflowEdges
carlos-alm Mar 17, 2026
7030e7f
refactor: decompose sequenceData into BFS and message construction
carlos-alm Mar 17, 2026
b4d8a0d
refactor: decompose explain() into section renderers
carlos-alm Mar 17, 2026
ae805d5
refactor: decompose stats() into section printers
carlos-alm Mar 17, 2026
3aa2e4b
fix: address quality issues in features (boundaries, communities, tri…
carlos-alm Mar 17, 2026
246fc21
fix: split data fetching from formatting in presentation queries
carlos-alm Mar 17, 2026
3d60159
fix: extract subcommand dispatch in check, triage CLI and MCP server
carlos-alm Mar 17, 2026
fc721f3
fix: move startMCPServer JSDoc to correct function location
carlos-alm Mar 17, 2026
22ae887
fix: reorder imports in MCP server for lint compliance
carlos-alm Mar 17, 2026
e6e712d
Merge remote-tracking branch 'origin/main' into fix/review-493
carlos-alm Mar 17, 2026
a21840f
chore: release v3.2.0
carlos-alm Mar 17, 2026
6a838be
Merge branch 'main' into release/3.2.0
carlos-alm Mar 17, 2026
155dcc7
merge main into release/3.2.0
carlos-alm Mar 17, 2026
d4f9490
fix: add missing changelog entries for #498 and #493, restore libc fi…
carlos-alm Mar 17, 2026
66f6d16
Merge branch 'release/3.2.0' of https://github.com/optave/codegraph i…
carlos-alm Mar 17, 2026
fe45c55
feat: add type inference for all typed languages (WASM + native)
carlos-alm Mar 18, 2026
de21be4
Merge branch 'main' into feat/type-inference-all-langs
carlos-alm Mar 18, 2026
9cdb931
fix: check parameter name not type name for self/cls filter in Python…
carlos-alm Mar 18, 2026
1c96146
refactor: remove redundant variable typeMap extraction in walk path (…
carlos-alm Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,18 +486,20 @@ codegraph registry remove <name> # Unregister

## 🌐 Language Support

| Language | Extensions | Coverage |
|---|---|---|
| ![JavaScript](https://img.shields.io/badge/-JavaScript-F7DF1E?style=flat-square&logo=javascript&logoColor=black) | `.js`, `.jsx`, `.mjs`, `.cjs` | Full — functions, classes, imports, call sites, dataflow |
| ![TypeScript](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) | `.ts`, `.tsx` | Full — interfaces, type aliases, `.d.ts`, dataflow |
| ![Python](https://img.shields.io/badge/-Python-3776AB?style=flat-square&logo=python&logoColor=white) | `.py` | Functions, classes, methods, imports, decorators, dataflow |
| ![Go](https://img.shields.io/badge/-Go-00ADD8?style=flat-square&logo=go&logoColor=white) | `.go` | Functions, methods, structs, interfaces, imports, call sites, dataflow |
| ![Rust](https://img.shields.io/badge/-Rust-000000?style=flat-square&logo=rust&logoColor=white) | `.rs` | Functions, methods, structs, traits, `use` imports, call sites, dataflow |
| ![Java](https://img.shields.io/badge/-Java-ED8B00?style=flat-square&logo=openjdk&logoColor=white) | `.java` | Classes, methods, constructors, interfaces, imports, call sites, dataflow |
| ![C#](https://img.shields.io/badge/-C%23-512BD4?style=flat-square&logo=dotnet&logoColor=white) | `.cs` | Classes, structs, records, interfaces, enums, methods, constructors, using directives, invocations, dataflow |
| ![PHP](https://img.shields.io/badge/-PHP-777BB4?style=flat-square&logo=php&logoColor=white) | `.php` | Functions, classes, interfaces, traits, enums, methods, namespace use, calls, dataflow |
| ![Ruby](https://img.shields.io/badge/-Ruby-CC342D?style=flat-square&logo=ruby&logoColor=white) | `.rb` | Classes, modules, methods, singleton methods, require/require_relative, include/extend, dataflow |
| ![Terraform](https://img.shields.io/badge/-Terraform-844FBA?style=flat-square&logo=terraform&logoColor=white) | `.tf`, `.hcl` | Resource, data, variable, module, output blocks |
| Language | Extensions | Symbols Extracted | Type Inference | Parity |
|---|---|---|:---:|:---:|
| ![JavaScript](https://img.shields.io/badge/-JavaScript-F7DF1E?style=flat-square&logo=javascript&logoColor=black) | `.js`, `.jsx`, `.mjs`, `.cjs` | functions, classes, methods, imports, exports, call sites, constants, dataflow | ✅ | ✅ |
| ![TypeScript](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) | `.ts`, `.tsx` | functions, classes, interfaces, type aliases, methods, imports, exports, call sites, dataflow | ✅ | ✅ |
| ![Python](https://img.shields.io/badge/-Python-3776AB?style=flat-square&logo=python&logoColor=white) | `.py` | functions, classes, methods, imports, decorators, constants, call sites, dataflow | ✅ | ✅ |
| ![Go](https://img.shields.io/badge/-Go-00ADD8?style=flat-square&logo=go&logoColor=white) | `.go` | functions, methods, structs, interfaces, constants, imports, call sites, dataflow | ✅ | ✅ |
| ![Rust](https://img.shields.io/badge/-Rust-000000?style=flat-square&logo=rust&logoColor=white) | `.rs` | functions, methods, structs, enums, traits, constants, `use` imports, call sites, dataflow | ✅ | ✅ |
| ![Java](https://img.shields.io/badge/-Java-ED8B00?style=flat-square&logo=openjdk&logoColor=white) | `.java` | classes, methods, constructors, interfaces, enums, imports, call sites, dataflow | ✅ | ✅ |
| ![C#](https://img.shields.io/badge/-C%23-512BD4?style=flat-square&logo=dotnet&logoColor=white) | `.cs` | classes, structs, records, interfaces, enums, methods, constructors, properties, using directives, call sites, dataflow | ✅ | ✅ |
| ![PHP](https://img.shields.io/badge/-PHP-777BB4?style=flat-square&logo=php&logoColor=white) | `.php` | functions, classes, interfaces, traits, enums, methods, namespace use, call sites, dataflow | ✅ | ✅ |
| ![Ruby](https://img.shields.io/badge/-Ruby-CC342D?style=flat-square&logo=ruby&logoColor=white) | `.rb` | classes, modules, methods, singleton methods, require/require_relative, include/extend, dataflow | — | ✅ |
| ![Terraform](https://img.shields.io/badge/-Terraform-844FBA?style=flat-square&logo=terraform&logoColor=white) | `.tf`, `.hcl` | resource, data, variable, module, output blocks | — | ✅ |

> **Type Inference** extracts a per-file type map from annotations (`const x: Router`, `MyType x`, `x: MyType`) and `new` expressions, enabling the edge resolver to connect `x.method()` → `Type.method()`. **Parity** = WASM and native Rust engines produce identical output.

## ⚙️ How It Works

Expand Down
46 changes: 41 additions & 5 deletions crates/codegraph-core/src/edge_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ pub struct DefInfo {
pub end_line: Option<u32>,
}

#[napi(object)]
pub struct TypeMapInput {
pub name: String,
#[napi(js_name = "typeName")]
pub type_name: String,
}

#[napi(object)]
pub struct FileEdgeInput {
pub file: String,
Expand All @@ -53,6 +60,8 @@ pub struct FileEdgeInput {
#[napi(js_name = "importedNames")]
pub imported_names: Vec<ImportedName>,
pub classes: Vec<ClassInfo>,
#[napi(js_name = "typeMap")]
pub type_map: Vec<TypeMapInput>,
}

#[napi(object)]
Expand Down Expand Up @@ -108,6 +117,13 @@ pub fn build_call_edges(
.map(|im| (im.name.as_str(), im.file.as_str()))
.collect();

// Build type map (variable name → declared type name)
let type_map: HashMap<&str, &str> = file_input
.type_map
.iter()
.map(|tm| (tm.name.as_str(), tm.type_name.as_str()))
.collect();

// Build def → node ID map for caller resolution (match by name+kind+file+line)
let file_nodes: Vec<&NodeInfo> = all_nodes.iter().filter(|n| n.file == *rel_path).collect();

Expand Down Expand Up @@ -204,10 +220,25 @@ pub fn build_call_edges(

if !method_candidates.is_empty() {
targets = method_candidates;
} else if call.receiver.is_none()
} else if let Some(ref receiver) = call.receiver {
// Type-aware resolution: translate variable receiver to declared type
if let Some(type_name) = type_map.get(receiver.as_str()) {
let qualified = format!("{}.{}", type_name, call.name);
let typed: Vec<&NodeInfo> = nodes_by_name
.get(qualified.as_str())
.map(|v| v.iter().filter(|n| n.kind == "method").copied().collect())
.unwrap_or_default();
if !typed.is_empty() {
targets = typed;
}
}
}

if targets.is_empty()
&& (call.receiver.is_none()
|| call.receiver.as_deref() == Some("this")
|| call.receiver.as_deref() == Some("self")
|| call.receiver.as_deref() == Some("super")
|| call.receiver.as_deref() == Some("super"))
{
// Scoped fallback — same-dir or parent-dir only
targets = nodes_by_name
Expand Down Expand Up @@ -263,15 +294,19 @@ pub fn build_call_edges(
&& receiver != "self"
&& receiver != "super"
{
// Resolve variable to its declared type via typeMap
let effective_receiver = type_map.get(receiver.as_str()).copied().unwrap_or(receiver.as_str());
let type_resolved = effective_receiver != receiver.as_str();

let samefile = nodes_by_name_and_file
.get(&(receiver.as_str(), rel_path.as_str()))
.get(&(effective_receiver, rel_path.as_str()))
.cloned()
.unwrap_or_default();
let candidates = if !samefile.is_empty() {
samefile
} else {
nodes_by_name
.get(receiver.as_str())
.get(effective_receiver)
.cloned()
.unwrap_or_default()
};
Expand All @@ -286,11 +321,12 @@ pub fn build_call_edges(
(1u64 << 63) | ((caller_id as u64) << 32) | (recv_target.id as u64);
if !seen_edges.contains(&recv_key) {
seen_edges.insert(recv_key);
let confidence = if type_resolved { 0.9 } else { 0.7 };
edges.push(ComputedEdge {
source_id: caller_id,
target_id: recv_target.id,
kind: "receiver".to_string(),
confidence: 0.7,
confidence,
dynamic: 0,
});
}
Expand Down
70 changes: 70 additions & 0 deletions crates/codegraph-core/src/extractors/csharp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ impl SymbolExtractor for CSharpExtractor {
let mut symbols = FileSymbols::new(file_path.to_string());
walk_node(&tree.root_node(), source, &mut symbols);
walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &CSHARP_AST_CONFIG);
extract_csharp_type_map(&tree.root_node(), source, &mut symbols);
symbols
}
}
Expand Down Expand Up @@ -469,3 +470,72 @@ fn extract_csharp_base_types(
}
}
}

// ── Type map extraction ─────────────────────────────────────────────────────

fn extract_csharp_type_name<'a>(type_node: &Node<'a>, source: &'a [u8]) -> Option<&'a str> {
match type_node.kind() {
"identifier" | "qualified_name" => Some(node_text(type_node, source)),
"predefined_type" => None, // skip int, string, etc.
"generic_name" => type_node.child(0).map(|n| node_text(&n, source)),
"nullable_type" => {
type_node.child(0).and_then(|inner| extract_csharp_type_name(&inner, source))
}
_ => None,
}
}

fn extract_csharp_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
extract_csharp_type_map_depth(node, source, symbols, 0);
}

fn extract_csharp_type_map_depth(node: &Node, source: &[u8], symbols: &mut FileSymbols, depth: usize) {
if depth >= MAX_WALK_DEPTH {
return;
}
match node.kind() {
"variable_declaration" => {
let type_node = node.child_by_field_name("type").or_else(|| node.child(0));
if let Some(type_node) = type_node {
if type_node.kind() != "var_keyword" && type_node.kind() != "implicit_type" {
if let Some(type_name) = extract_csharp_type_name(&type_node, source) {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "variable_declarator" {
let name_node = child.child_by_field_name("name")
.or_else(|| child.child(0));
if let Some(name_node) = name_node {
if name_node.kind() == "identifier" {
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
});
}
}
}
}
}
}
}
}
}
"parameter" => {
if let Some(type_node) = node.child_by_field_name("type") {
if let Some(type_name) = extract_csharp_type_name(&type_node, source) {
if let Some(name_node) = node.child_by_field_name("name") {
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
});
}
}
}
}
_ => {}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
extract_csharp_type_map_depth(&child, source, symbols, depth + 1);
}
}
}
68 changes: 68 additions & 0 deletions crates/codegraph-core/src/extractors/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ impl SymbolExtractor for GoExtractor {
let mut symbols = FileSymbols::new(file_path.to_string());
walk_node(&tree.root_node(), source, &mut symbols);
walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &GO_AST_CONFIG);
extract_go_type_map(&tree.root_node(), source, &mut symbols);
symbols
}
}
Expand Down Expand Up @@ -310,6 +311,73 @@ fn extract_go_import_spec(spec: &Node, source: &[u8], symbols: &mut FileSymbols)
}
}

// ── Type map extraction ─────────────────────────────────────────────────────

fn extract_go_type_name<'a>(type_node: &Node<'a>, source: &'a [u8]) -> Option<&'a str> {
match type_node.kind() {
"type_identifier" | "identifier" | "qualified_type" => Some(node_text(type_node, source)),
"pointer_type" => {
// *MyType → MyType
for i in 0..type_node.child_count() {
if let Some(child) = type_node.child(i) {
if child.kind() == "type_identifier" || child.kind() == "identifier" {
return Some(node_text(&child, source));
}
}
}
None
}
"generic_type" => type_node.child(0).map(|n| node_text(&n, source)),
_ => None,
}
}

fn extract_go_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
extract_go_type_map_depth(node, source, symbols, 0);
}

fn extract_go_type_map_depth(node: &Node, source: &[u8], symbols: &mut FileSymbols, depth: usize) {
if depth >= MAX_WALK_DEPTH {
return;
}
match node.kind() {
"var_spec" => {
if let Some(type_node) = node.child_by_field_name("type") {
if let Some(type_name) = extract_go_type_name(&type_node, source) {
if let Some(name_node) = node.child_by_field_name("name") {
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
});
}
}
}
}
"parameter_declaration" => {
if let Some(type_node) = node.child_by_field_name("type") {
if let Some(type_name) = extract_go_type_name(&type_node, source) {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "identifier" {
symbols.type_map.push(TypeMapEntry {
name: node_text(&child, source).to_string(),
type_name: type_name.to_string(),
});
}
}
}
}
}
}
_ => {}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
extract_go_type_map_depth(&child, source, symbols, depth + 1);
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
59 changes: 59 additions & 0 deletions crates/codegraph-core/src/extractors/java.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,69 @@ impl SymbolExtractor for JavaExtractor {
let mut symbols = FileSymbols::new(file_path.to_string());
walk_node(&tree.root_node(), source, &mut symbols);
walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &JAVA_AST_CONFIG);
extract_java_type_map(&tree.root_node(), source, &mut symbols);
symbols
}
}

// ── Type inference helpers ──────────────────────────────────────────────────

fn extract_java_type_name<'a>(type_node: &Node<'a>, source: &'a [u8]) -> Option<&'a str> {
if type_node.kind() == "generic_type" {
type_node.child(0).map(|n| node_text(&n, source))
} else {
Some(node_text(type_node, source))
}
}

fn extract_java_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
extract_java_type_map_depth(node, source, symbols, 0);
}

fn extract_java_type_map_depth(node: &Node, source: &[u8], symbols: &mut FileSymbols, depth: usize) {
if depth >= MAX_WALK_DEPTH {
return;
}
match node.kind() {
"local_variable_declaration" => {
if let Some(type_node) = node.child_by_field_name("type") {
if let Some(type_name) = extract_java_type_name(&type_node, source) {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "variable_declarator" {
if let Some(name_node) = child.child_by_field_name("name") {
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
});
}
}
}
}
}
}
}
"formal_parameter" => {
if let Some(type_node) = node.child_by_field_name("type") {
if let Some(type_name) = extract_java_type_name(&type_node, source) {
if let Some(name_node) = node.child_by_field_name("name") {
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
});
}
}
}
}
_ => {}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
extract_java_type_map_depth(&child, source, symbols, depth + 1);
}
}
}

fn find_java_parent_class<'a>(node: &Node<'a>, source: &[u8]) -> Option<String> {
let mut current = node.parent();
while let Some(parent) = current {
Expand Down
Loading
Loading