From f8b363858fb564749469e93a697d943bc2d13078 Mon Sep 17 00:00:00 2001 From: kelmith Date: Thu, 25 Jun 2026 01:59:51 +0530 Subject: [PATCH 1/3] feat: udpate accross csharp, kotlin, swift and java --- ast/src/lang/registry/cs_resolver.rs | 14 +- ast/src/lang/registry/go_resolver.rs | 171 ++++++++++++++++-- ast/src/lang/registry/golang.rs | 9 +- ast/src/lang/registry/java_resolver.rs | 13 +- ast/src/lang/registry/kotlin_registry.rs | 7 + ast/src/lang/registry/kotlin_resolver.rs | 80 ++++++-- ast/src/lang/registry/php_registry.rs | 7 + ast/src/lang/registry/php_resolver.rs | 69 ++++++- ast/src/lang/registry/py_resolver.rs | 71 +++++++- ast/src/lang/registry/python.rs | 7 + ast/src/lang/registry/rust_registry.rs | 10 +- ast/src/lang/registry/rust_resolver.rs | 116 ++++++++---- ast/src/lang/registry/swift_registry.rs | 7 + ast/src/lang/registry/swift_resolver.rs | 66 +++++-- ast/src/testing/coverage/csharp.rs | 8 +- ast/src/testing/coverage/go.rs | 10 +- ast/src/testing/coverage/java.rs | 8 +- ast/src/testing/coverage/kotlin.rs | 8 +- ast/src/testing/coverage/php.rs | 8 +- ast/src/testing/coverage/rust.rs | 38 ++-- ast/src/testing/coverage/swift.rs | 8 +- ast/src/testing/csharp/MethodChainFactory.cs | 38 ++++ ast/src/testing/go/method_chain.go | 26 +++ .../java/nonweb/MethodChainFactory.java | 39 ++++ .../com/kotlintestapp/MethodChainFactory.kt | 24 +++ ast/src/testing/php/factory_function.php | 30 +++ .../python/module_calls/module_calls.py | 25 +++ ast/src/testing/rust/src/method_chain.rs | 17 ++ .../ModernApp/MethodChainFactory.swift | 25 +++ cli/tests/cli/csharp.rs | 4 +- cli/tests/cli/java.rs | 4 +- cli/tests/cli/kotlin.rs | 4 +- cli/tests/cli/php.rs | 4 +- cli/tests/cli/swift.rs | 4 +- 34 files changed, 834 insertions(+), 145 deletions(-) create mode 100644 ast/src/testing/csharp/MethodChainFactory.cs create mode 100644 ast/src/testing/java/src/main/java/graph/stakgraph/java/nonweb/MethodChainFactory.java create mode 100644 ast/src/testing/kotlin/app/src/main/java/com/kotlintestapp/MethodChainFactory.kt create mode 100644 ast/src/testing/php/factory_function.php create mode 100644 ast/src/testing/swift/ModernApp/Sources/ModernApp/MethodChainFactory.swift diff --git a/ast/src/lang/registry/cs_resolver.rs b/ast/src/lang/registry/cs_resolver.rs index bc72494bb..6643a0a72 100644 --- a/ast/src/lang/registry/cs_resolver.rs +++ b/ast/src/lang/registry/cs_resolver.rs @@ -271,8 +271,18 @@ fn eval_expr_type( source: &[u8], ) -> Option { match node.kind() { - // Bare identifier: local var, parameter, or field seeded from class scope - "identifier" => scope_lookup(scope, node.utf8_text(source).ok()?).map(str::to_string), + // Bare identifier: local var, parameter, or field seeded from class scope. + // Uppercase identifiers not in scope are treated as class names (static receiver). + "identifier" => { + let name = node.utf8_text(source).ok()?; + scope_lookup(scope, name).map(str::to_string).or_else(|| { + if name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) { + Some(name.to_string()) + } else { + None + } + }) + } // `this` keyword → current class type from scope "this" => scope_lookup(scope, "this").map(str::to_string), diff --git a/ast/src/lang/registry/go_resolver.rs b/ast/src/lang/registry/go_resolver.rs index cd29e27fc..645a6a854 100644 --- a/ast/src/lang/registry/go_resolver.rs +++ b/ast/src/lang/registry/go_resolver.rs @@ -193,12 +193,92 @@ fn extract_fn_return_from_node(node: Node, source: &[u8], out: &mut HashMap HashMap<(String, String), String> { + let mut out = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + let src = source.as_bytes(); + let root = tree.root_node(); + for i in 0..root.named_child_count() { + let Some(node) = root.named_child(i) else { + continue; + }; + if node.kind() != "method_declaration" { + continue; + } + extract_method_return_from_node(node, src, &mut out); + } + out +} + +fn extract_method_return_from_node( + node: Node, + source: &[u8], + out: &mut HashMap<(String, String), String>, +) { + // Receiver: (parameter_list (parameter_declaration type: T | *T)) + let Some(recv_list) = node.child_by_field_name("receiver") else { + return; + }; + let Some(pd) = recv_list.named_child(0) else { + return; + }; + if pd.kind() != "parameter_declaration" { + return; + } + let Some(type_node) = pd.child_by_field_name("type") else { + return; + }; + let Some(recv_type) = strip_go_type(type_node, source) else { + return; + }; + + let Some(name_node) = node.child_by_field_name("name") else { + return; + }; + let Ok(method_name) = name_node.utf8_text(source) else { + return; + }; + + let Some(result_node) = node.child_by_field_name("result") else { + return; + }; + let base_type: Option = match result_node.kind() { + "type_identifier" | "pointer_type" | "generic_type" => { + strip_go_type(result_node, source).filter(|t| t != "error") + } + "parameter_list" => (0..result_node.named_child_count()) + .filter_map(|j| result_node.named_child(j)) + .find_map(|child| { + let tn = if child.kind() == "parameter_declaration" { + child.child_by_field_name("type")? + } else { + child + }; + let t = strip_go_type(tn, source)?; + if t == "error" { None } else { Some(t) } + }), + _ => None, + }; + + if let Some(t) = base_type { + out.entry((recv_type, method_name.to_string())).or_insert(t); + } +} + // ── Type evaluator ───────────────────────────────────────────────────────────── fn eval_expr_type( scope: &Scope, struct_fields: &HashMap>, fn_returns: &HashMap, + method_returns: &HashMap<(String, String), String>, node: Node, source: &[u8], ) -> Option { @@ -209,6 +289,7 @@ fn eval_expr_type( scope, struct_fields, fn_returns, + method_returns, node.child_by_field_name("operand")?, source, )?; @@ -220,8 +301,9 @@ fn eval_expr_type( } "unary_expression" => { // &T{} or *ptr → eval the operand - node.named_child(0) - .and_then(|inner| eval_expr_type(scope, struct_fields, fn_returns, inner, source)) + node.named_child(0).and_then(|inner| { + eval_expr_type(scope, struct_fields, fn_returns, method_returns, inner, source) + }) } "composite_literal" => { // SomeType{...} or &SomeType{...} → extract type child @@ -230,11 +312,27 @@ fn eval_expr_type( } "call_expression" => { let func = node.child_by_field_name("function")?; - if func.kind() == "identifier" { - let func_name = func.utf8_text(source).ok()?; - fn_returns.get(func_name).cloned() - } else { - None + match func.kind() { + "identifier" => { + let func_name = func.utf8_text(source).ok()?; + fn_returns.get(func_name).cloned() + } + "selector_expression" => { + let obj_type = eval_expr_type( + scope, + struct_fields, + fn_returns, + method_returns, + func.child_by_field_name("operand")?, + source, + )?; + let method_name = + func.child_by_field_name("field")?.utf8_text(source).ok()?; + method_returns + .get(&(obj_type, method_name.to_string())) + .cloned() + } + _ => None, } } _ => None, @@ -247,6 +345,7 @@ fn resolve_callee( scope: &Scope, struct_fields: &HashMap>, fn_returns: &HashMap, + method_returns: &HashMap<(String, String), String>, pkg_fns: &HashMap>, graph: &G, node: Node, @@ -259,6 +358,7 @@ fn resolve_callee( scope, struct_fields, fn_returns, + method_returns, node.child_by_field_name("operand")?, source, )?; @@ -339,6 +439,7 @@ fn try_bind_short_var( scope: &mut Scope, struct_fields: &HashMap>, fn_returns: &HashMap, + method_returns: &HashMap<(String, String), String>, ) { let Some(left) = node.child_by_field_name("left") else { return; @@ -355,7 +456,7 @@ fn try_bind_short_var( let types: Vec> = (0..right.named_child_count()) .filter_map(|i| right.named_child(i)) - .map(|n| eval_expr_type(scope, struct_fields, fn_returns, n, source)) + .map(|n| eval_expr_type(scope, struct_fields, fn_returns, method_returns, n, source)) .collect(); for (i, name) in names.iter().enumerate() { @@ -376,6 +477,7 @@ fn walk_node( scope: &mut Scope, struct_fields: &HashMap>, fn_returns: &HashMap, + method_returns: &HashMap<(String, String), String>, pkg_fns: &HashMap>, graph: &G, out: &mut HashMap<(usize, usize), NodeKeys>, @@ -388,7 +490,10 @@ fn walk_node( bind_params(params, source, scope); } if let Some(body) = node.child_by_field_name("body") { - walk_node(body, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file); + walk_node( + body, source, scope, struct_fields, fn_returns, method_returns, pkg_fns, + graph, out, file, + ); } scope_pop(scope); } @@ -426,7 +531,10 @@ fn walk_node( bind_params(params, source, scope); } if let Some(body) = node.child_by_field_name("body") { - walk_node(body, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file); + walk_node( + body, source, scope, struct_fields, fn_returns, method_returns, pkg_fns, + graph, out, file, + ); } scope_pop(scope); } @@ -436,22 +544,36 @@ fn walk_node( bind_params(params, source, scope); } if let Some(body) = node.child_by_field_name("body") { - walk_node(body, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file); + walk_node( + body, source, scope, struct_fields, fn_returns, method_returns, pkg_fns, + graph, out, file, + ); } scope_pop(scope); } "short_var_declaration" => { // Bind types before recursing so later calls can see them in scope - try_bind_short_var(node, source, scope, struct_fields, fn_returns); + try_bind_short_var(node, source, scope, struct_fields, fn_returns, method_returns); if let Some(right) = node.child_by_field_name("right") { - walk_node(right, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file); + walk_node( + right, source, scope, struct_fields, fn_returns, method_returns, pkg_fns, + graph, out, file, + ); } } "call_expression" => { if let Some(func_node) = node.child_by_field_name("function") { - if let Some(target) = - resolve_callee(scope, struct_fields, fn_returns, pkg_fns, graph, func_node, source, file) - { + if let Some(target) = resolve_callee( + scope, + struct_fields, + fn_returns, + method_returns, + pkg_fns, + graph, + func_node, + source, + file, + ) { let pos = if func_node.kind() == "selector_expression" { func_node .child_by_field_name("field") @@ -463,17 +585,26 @@ fn walk_node( out.insert((pos.row, pos.column), target); } // Recurse into func_node to find nested calls (e.g. chained method calls) - walk_node(func_node, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file); + walk_node( + func_node, source, scope, struct_fields, fn_returns, method_returns, pkg_fns, + graph, out, file, + ); // Recurse into arguments for nested calls within args if let Some(args) = node.child_by_field_name("arguments") { - walk_node(args, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file); + walk_node( + args, source, scope, struct_fields, fn_returns, method_returns, pkg_fns, + graph, out, file, + ); } } } _ => { for i in 0..node.named_child_count() { if let Some(child) = node.named_child(i) { - walk_node(child, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file); + walk_node( + child, source, scope, struct_fields, fn_returns, method_returns, pkg_fns, + graph, out, file, + ); } } } @@ -489,6 +620,7 @@ pub fn resolve_file_calls( file: &str, struct_fields: &HashMap>, fn_returns: &HashMap, + method_returns: &HashMap<(String, String), String>, pkg_fns: &HashMap>, var_types: &HashMap<(String, String), String>, graph: &G, @@ -519,6 +651,7 @@ pub fn resolve_file_calls( &mut scope, struct_fields, fn_returns, + method_returns, pkg_fns, graph, &mut out, diff --git a/ast/src/lang/registry/golang.rs b/ast/src/lang/registry/golang.rs index 00223a81e..b772f4ade 100644 --- a/ast/src/lang/registry/golang.rs +++ b/ast/src/lang/registry/golang.rs @@ -16,6 +16,7 @@ pub struct GoRegistry { methods: HashMap<(String, String), String>, struct_fields: HashMap>, fn_returns: HashMap, + method_returns: HashMap<(String, String), String>, pkg_fns: HashMap>, resolved: HashMap<(String, usize, usize), NodeKeys>, } @@ -27,6 +28,7 @@ impl GoRegistry { methods: HashMap::new(), struct_fields: HashMap::new(), fn_returns: HashMap::new(), + method_returns: HashMap::new(), pkg_fns: HashMap::new(), resolved: HashMap::new(), }; @@ -64,7 +66,7 @@ impl GoRegistry { } } - // Pass 1.5: extract struct field types from source files + // Pass 1.5: extract struct field types and method return types from source files for (file, source) in filez { if !file.ends_with(".go") { continue; @@ -76,6 +78,10 @@ impl GoRegistry { .or_default() .extend(field_map); } + let method_rets = go_resolver::extract_method_return_types(source); + for (key, ret) in method_rets { + reg.method_returns.entry(key).or_insert(ret); + } } // Build fn_returns: func_name → base_return_type for factory-call typing @@ -96,6 +102,7 @@ impl GoRegistry { file, ®.struct_fields, ®.fn_returns, + ®.method_returns, ®.pkg_fns, ®.var_types, graph, diff --git a/ast/src/lang/registry/java_resolver.rs b/ast/src/lang/registry/java_resolver.rs index 883ac93c3..d6a4a2dc0 100644 --- a/ast/src/lang/registry/java_resolver.rs +++ b/ast/src/lang/registry/java_resolver.rs @@ -274,9 +274,18 @@ fn eval_expr_type( // `this` is its own keyword node in tree-sitter-java (not an identifier) "this" => scope_lookup(scope, "this").map(str::to_string), - // Bare name: local var, parameter, or class field (seeded into scope on class entry) + // Bare name: local var, parameter, class field, or static class reference. + // If scope lookup fails and the name starts with an uppercase letter, treat it + // as a class name used as a static-call receiver (e.g. PersonFactory.create()). "identifier" => { - scope_lookup(scope, node.utf8_text(source).ok()?).map(str::to_string) + let name = node.utf8_text(source).ok()?; + scope_lookup(scope, name).map(str::to_string).or_else(|| { + if name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) { + Some(name.to_string()) + } else { + None + } + }) } // this.gateway or obj.field diff --git a/ast/src/lang/registry/kotlin_registry.rs b/ast/src/lang/registry/kotlin_registry.rs index 7b66c7055..1a7efcf08 100644 --- a/ast/src/lang/registry/kotlin_registry.rs +++ b/ast/src/lang/registry/kotlin_registry.rs @@ -14,6 +14,7 @@ fn parent_dir(file: &str) -> String { pub struct KotlinRegistry { class_fields: HashMap>, method_returns: HashMap<(String, String), String>, + fn_returns: HashMap, dir_fns: HashMap>, resolved: HashMap<(String, usize, usize), NodeKeys>, } @@ -23,6 +24,7 @@ impl KotlinRegistry { let mut reg = KotlinRegistry { class_fields: HashMap::new(), method_returns: HashMap::new(), + fn_returns: HashMap::new(), dir_fns: HashMap::new(), resolved: HashMap::new(), }; @@ -59,6 +61,10 @@ impl KotlinRegistry { for (key, ret) in returns { reg.method_returns.entry(key).or_insert(ret); } + let fns = kotlin_resolver::extract_fn_returns(source); + for (fname, ret) in fns { + reg.fn_returns.entry(fname).or_insert(ret); + } } // Pass 2: pre-resolve all call sites per file. @@ -71,6 +77,7 @@ impl KotlinRegistry { file, ®.class_fields, ®.method_returns, + ®.fn_returns, ®.dir_fns, graph, ) diff --git a/ast/src/lang/registry/kotlin_resolver.rs b/ast/src/lang/registry/kotlin_resolver.rs index eb6578aa7..7711bc3ae 100644 --- a/ast/src/lang/registry/kotlin_resolver.rs +++ b/ast/src/lang/registry/kotlin_resolver.rs @@ -253,6 +253,56 @@ fn walk_for_class_fields( } } +// ── Free-function return type extraction ────────────────────────────────────── + +pub fn extract_fn_returns(source: &str) -> HashMap { + let mut out = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + let src = source.as_bytes(); + let root = tree.root_node(); + for i in 0..root.named_child_count() { + let Some(node) = root.named_child(i) else { + continue; + }; + if node.kind() != "function_declaration" { + continue; + } + let Some(name_node) = (0..node.named_child_count()) + .filter_map(|j| node.named_child(j)) + .find(|n| n.kind() == "simple_identifier") + else { + continue; + }; + let Ok(func_name) = name_node.utf8_text(src) else { + continue; + }; + const SKIP_KINDS: &[&str] = &[ + "modifiers", + "simple_identifier", + "function_value_parameters", + "function_body", + "type_constraints", + "type_parameters", + "type_modifiers", + "receiver_type", + ]; + let ret_type = (0..node.named_child_count()) + .filter_map(|j| node.named_child(j)) + .find(|n| !SKIP_KINDS.contains(&n.kind())) + .and_then(|n| strip_kotlin_type(n, src)) + .filter(|t| !SKIP_TYPES.contains(&t.as_str())); + if let Some(rt) = ret_type { + out.entry(func_name.to_string()).or_insert(rt); + } + } + out +} + // ── Method return type extraction ────────────────────────────────────────────── pub fn extract_method_return_types(source: &str) -> HashMap<(String, String), String> { @@ -372,6 +422,7 @@ fn eval_expr_type( scope: &Scope, class_fields: &HashMap>, method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, node: Node, source: &[u8], ) -> Option { @@ -392,7 +443,7 @@ fn eval_expr_type( .named_child(0) .filter(|n| n.kind() == "simple_identifier") .and_then(|n| n.utf8_text(source).ok())?; - let recv_type = eval_expr_type(scope, class_fields, method_returns, recv, source)?; + let recv_type = eval_expr_type(scope, class_fields, method_returns, fn_returns, recv, source)?; class_fields.get(&recv_type)?.get(prop).cloned() } @@ -400,12 +451,13 @@ fn eval_expr_type( let first = node.named_child(0)?; match first.kind() { // ClassName(...) → constructor call: type = class name + // factoryFn() → free function return type from fn_returns "simple_identifier" => { let name = first.utf8_text(source).ok()?; if name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) { Some(name.to_string()) } else { - None + fn_returns.get(name).cloned() } } // obj.method(...) → return type of method @@ -419,7 +471,7 @@ fn eval_expr_type( .filter(|n| n.kind() == "simple_identifier") .and_then(|n| n.utf8_text(source).ok())?; let recv_type = - eval_expr_type(scope, class_fields, method_returns, recv, source)?; + eval_expr_type(scope, class_fields, method_returns, fn_returns, recv, source)?; method_returns .get(&(recv_type, method_name.to_string())) .cloned() @@ -473,6 +525,7 @@ fn walk_node( scope: &mut Scope, class_fields: &HashMap>, method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, dir_fns: &HashMap>, graph: &G, out: &mut HashMap<(usize, usize), NodeKeys>, @@ -499,7 +552,7 @@ fn walk_node( if let Some(child) = body.named_child(i) { walk_node( child, source, scope, class_fields, method_returns, - dir_fns, graph, out, file, + fn_returns, dir_fns, graph, out, file, ); } } @@ -522,7 +575,7 @@ fn walk_node( { walk_node( body, source, scope, class_fields, method_returns, - dir_fns, graph, out, file, + fn_returns, dir_fns, graph, out, file, ); } scope_pop(scope); @@ -563,7 +616,7 @@ fn walk_node( }); let inferred = init.and_then(|n| { - eval_expr_type(scope, class_fields, method_returns, n, source) + eval_expr_type(scope, class_fields, method_returns, fn_returns, n, source) }); let bound = explicit.or(inferred); @@ -576,11 +629,11 @@ fn walk_node( if let Some(init_node) = init { walk_node( init_node, source, scope, class_fields, method_returns, - dir_fns, graph, out, file, + fn_returns, dir_fns, graph, out, file, ); } } else { - recurse(node, source, scope, class_fields, method_returns, dir_fns, graph, out, file); + recurse(node, source, scope, class_fields, method_returns, fn_returns, dir_fns, graph, out, file); } } @@ -602,7 +655,7 @@ fn walk_node( { if let Ok(method_name) = method_name_node.utf8_text(source) { if let Some(recv_type) = eval_expr_type( - scope, class_fields, method_returns, recv_node, source, + scope, class_fields, method_returns, fn_returns, recv_node, source, ) { if let Some(target) = find_method_in_class(graph, &recv_type, method_name) @@ -637,11 +690,11 @@ fn walk_node( } } // Recurse into all children to process nested calls (args, trailing lambdas, etc.) - recurse(node, source, scope, class_fields, method_returns, dir_fns, graph, out, file); + recurse(node, source, scope, class_fields, method_returns, fn_returns, dir_fns, graph, out, file); } _ => { - recurse(node, source, scope, class_fields, method_returns, dir_fns, graph, out, file); + recurse(node, source, scope, class_fields, method_returns, fn_returns, dir_fns, graph, out, file); } } } @@ -652,6 +705,7 @@ fn recurse( scope: &mut Scope, class_fields: &HashMap>, method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, dir_fns: &HashMap>, graph: &G, out: &mut HashMap<(usize, usize), NodeKeys>, @@ -661,7 +715,7 @@ fn recurse( if let Some(child) = node.named_child(i) { walk_node( child, source, scope, class_fields, method_returns, - dir_fns, graph, out, file, + fn_returns, dir_fns, graph, out, file, ); } } @@ -674,6 +728,7 @@ pub fn resolve_file_calls( file: &str, class_fields: &HashMap>, method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, dir_fns: &HashMap>, graph: &G, ) -> HashMap<(usize, usize), NodeKeys> { @@ -692,6 +747,7 @@ pub fn resolve_file_calls( &mut scope, class_fields, method_returns, + fn_returns, dir_fns, graph, &mut out, diff --git a/ast/src/lang/registry/php_registry.rs b/ast/src/lang/registry/php_registry.rs index b48b36f4e..a7887e87d 100644 --- a/ast/src/lang/registry/php_registry.rs +++ b/ast/src/lang/registry/php_registry.rs @@ -14,6 +14,7 @@ fn parent_dir(file: &str) -> String { pub struct PhpRegistry { class_fields: HashMap>, method_returns: HashMap<(String, String), String>, + fn_returns: HashMap, dir_fns: HashMap>, resolved: HashMap<(String, usize, usize), NodeKeys>, } @@ -23,6 +24,7 @@ impl PhpRegistry { let mut reg = PhpRegistry { class_fields: HashMap::new(), method_returns: HashMap::new(), + fn_returns: HashMap::new(), dir_fns: HashMap::new(), resolved: HashMap::new(), }; @@ -59,6 +61,10 @@ impl PhpRegistry { for (key, ret) in returns { reg.method_returns.entry(key).or_insert(ret); } + let fn_rets = php_resolver::extract_fn_returns(source); + for (key, ret) in fn_rets { + reg.fn_returns.entry(key).or_insert(ret); + } } // Pass 2: pre-resolve all call sites per file. @@ -71,6 +77,7 @@ impl PhpRegistry { file, ®.class_fields, ®.method_returns, + ®.fn_returns, ®.dir_fns, graph, ) diff --git a/ast/src/lang/registry/php_resolver.rs b/ast/src/lang/registry/php_resolver.rs index eabd662ad..ce93372b5 100644 --- a/ast/src/lang/registry/php_resolver.rs +++ b/ast/src/lang/registry/php_resolver.rs @@ -242,6 +242,44 @@ fn recurse_fields( } } +// ── Free-function return type extraction ────────────────────────────────────── + +/// Extract top-level function return types from PHP source. +/// Returns { func_name → base_return_type }. Class methods are excluded. +pub fn extract_fn_returns(source: &str) -> HashMap { + let mut out = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + let src = source.as_bytes(); + let root = tree.root_node(); + for i in 0..root.named_child_count() { + let Some(node) = root.named_child(i) else { + continue; + }; + if node.kind() != "function_definition" { + continue; + } + let Some(name_node) = node.child_by_field_name("name") else { + continue; + }; + let Ok(func_name) = name_node.utf8_text(src) else { + continue; + }; + let Some(ret_wrapper) = node.child_by_field_name("return_type") else { + continue; + }; + let type_node = ret_wrapper.named_child(0).unwrap_or(ret_wrapper); + if let Some(ret_type) = strip_php_type(type_node, src) { + out.entry(func_name.to_string()).or_insert(ret_type); + } + } + out +} + // ── Method return type extraction ────────────────────────────────────────────── pub fn extract_method_return_types(source: &str) -> HashMap<(String, String), String> { @@ -369,6 +407,7 @@ fn eval_expr_type( scope: &Scope, class_fields: &HashMap>, method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, node: Node, src: &[u8], ) -> Option { @@ -385,6 +424,7 @@ fn eval_expr_type( scope, class_fields, method_returns, + fn_returns, node.child_by_field_name("object")?, src, )?; @@ -398,6 +438,7 @@ fn eval_expr_type( scope, class_fields, method_returns, + fn_returns, node.child_by_field_name("object")?, src, )?; @@ -428,6 +469,19 @@ fn eval_expr_type( .cloned() } + // free_function() — return type via fn_returns + "function_call_expression" => { + let func_node = node + .child_by_field_name("function") + .or_else(|| node.named_child(0))?; + if func_node.kind() == "name" { + let func_name = func_node.utf8_text(src).ok()?; + fn_returns.get(func_name).cloned() + } else { + None + } + } + _ => None, } } @@ -464,6 +518,7 @@ fn walk_node( scope: &mut Scope, class_fields: &HashMap>, method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, dir_fns: &HashMap>, graph: &G, out: &mut HashMap<(usize, usize), NodeKeys>, @@ -493,6 +548,7 @@ fn walk_node( scope, class_fields, method_returns, + fn_returns, dir_fns, graph, out, @@ -516,6 +572,7 @@ fn walk_node( scope, class_fields, method_returns, + fn_returns, dir_fns, graph, out, @@ -533,7 +590,7 @@ fn walk_node( if left.kind() == "variable_name" { if let Ok(var_name) = left.utf8_text(src) { if let Some(t) = - eval_expr_type(scope, class_fields, method_returns, right, src) + eval_expr_type(scope, class_fields, method_returns, fn_returns, right, src) { scope_bind(scope, var_name, &t); } @@ -545,6 +602,7 @@ fn walk_node( scope, class_fields, method_returns, + fn_returns, dir_fns, graph, out, @@ -559,7 +617,7 @@ fn walk_node( let name_node = node.child_by_field_name("name"); if let (Some(obj_node), Some(name_node)) = (obj_node, name_node) { let obj_type = - eval_expr_type(scope, class_fields, method_returns, obj_node, src); + eval_expr_type(scope, class_fields, method_returns, fn_returns, obj_node, src); let method_name = name_node.utf8_text(src).ok(); if let (Some(obj_type), Some(method_name)) = (obj_type, method_name) { let pos = { @@ -578,6 +636,7 @@ fn walk_node( scope, class_fields, method_returns, + fn_returns, dir_fns, graph, out, @@ -614,6 +673,7 @@ fn walk_node( scope, class_fields, method_returns, + fn_returns, dir_fns, graph, out, @@ -631,6 +691,7 @@ fn walk_node( scope, class_fields, method_returns, + fn_returns, dir_fns, graph, out, @@ -667,6 +728,7 @@ fn walk_node( scope, class_fields, method_returns, + fn_returns, dir_fns, graph, out, @@ -684,6 +746,7 @@ fn walk_node( scope, class_fields, method_returns, + fn_returns, dir_fns, graph, out, @@ -702,6 +765,7 @@ pub fn resolve_file_calls( file: &str, class_fields: &HashMap>, method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, dir_fns: &HashMap>, graph: &G, ) -> HashMap<(usize, usize), NodeKeys> { @@ -720,6 +784,7 @@ pub fn resolve_file_calls( &mut scope, class_fields, method_returns, + fn_returns, dir_fns, graph, &mut out, diff --git a/ast/src/lang/registry/py_resolver.rs b/ast/src/lang/registry/py_resolver.rs index 55a0d9451..500c00a71 100644 --- a/ast/src/lang/registry/py_resolver.rs +++ b/ast/src/lang/registry/py_resolver.rs @@ -311,11 +311,56 @@ pub fn extract_fn_returns(source: &str) -> HashMap { out } +pub fn extract_method_return_types(source: &str) -> HashMap<(String, String), String> { + let mut out = HashMap::new(); + let Some(mut parser) = make_parser() else { return out }; + let Some(tree) = parser.parse(source, None) else { return out }; + let src = source.as_bytes(); + let root = tree.root_node(); + + for i in 0..root.named_child_count() { + let Some(stmt) = root.named_child(i) else { continue }; + let class_node = if stmt.kind() == "decorated_definition" { + stmt.child_by_field_name("definition").unwrap_or(stmt) + } else { + stmt + }; + if class_node.kind() != "class_definition" { + continue; + } + let Some(name_node) = class_node.child_by_field_name("name") else { continue }; + let Ok(class_name) = name_node.utf8_text(src) else { continue }; + let Some(body) = class_node.child_by_field_name("body") else { continue }; + for j in 0..body.named_child_count() { + let Some(member) = body.named_child(j) else { continue }; + let fn_node = if member.kind() == "decorated_definition" { + member.child_by_field_name("definition").unwrap_or(member) + } else { + member + }; + if fn_node.kind() != "function_definition" { + continue; + } + let Some(fn_name_node) = fn_node.child_by_field_name("name") else { continue }; + let Ok(fn_name) = fn_name_node.utf8_text(src) else { continue }; + if fn_name == "__init__" { + continue; + } + let Some(ret_node) = fn_node.child_by_field_name("return_type") else { continue }; + if let Some(t) = extract_type_annotation(ret_node, src) { + out.entry((class_name.to_string(), fn_name.to_string())).or_insert(t); + } + } + } + out +} + // ── expression type evaluator ───────────────────────────────────────────────── fn eval_expr_type( scope: &Scope, class_fields: &HashMap>, + method_returns: &HashMap<(String, String), String>, fn_returns: &HashMap, node: Node, source: &[u8], @@ -326,6 +371,7 @@ fn eval_expr_type( let obj_type = eval_expr_type( scope, class_fields, + method_returns, fn_returns, node.child_by_field_name("object")?, source, @@ -340,6 +386,11 @@ fn eval_expr_type( fn_returns .get(func_name) .map(|(ret_type, def_file)| format!("{}@{}", ret_type, def_file)) + } else if func.kind() == "attribute" { + let obj_node = func.child_by_field_name("object")?; + let obj_type = eval_expr_type(scope, class_fields, method_returns, fn_returns, obj_node, source)?; + let method_name = func.child_by_field_name("attribute")?.utf8_text(source).ok()?; + method_returns.get(&(base_type(&obj_type).to_string(), method_name.to_string())).cloned() } else { None } @@ -347,7 +398,7 @@ fn eval_expr_type( "await" => { for i in 0..node.named_child_count() { if let Some(child) = node.named_child(i) { - if let Some(t) = eval_expr_type(scope, class_fields, fn_returns, child, source) + if let Some(t) = eval_expr_type(scope, class_fields, method_returns, fn_returns, child, source) { return Some(t); } @@ -364,6 +415,7 @@ fn eval_expr_type( fn resolve_callee( scope: &Scope, class_fields: &HashMap>, + method_returns: &HashMap<(String, String), String>, fn_returns: &HashMap, graph: &G, node: Node, @@ -393,6 +445,7 @@ fn resolve_callee( let raw_type = eval_expr_type( scope, class_fields, + method_returns, fn_returns, node.child_by_field_name("object")?, source, @@ -448,6 +501,7 @@ fn try_bind_assignment( source: &[u8], scope: &mut Scope, class_fields: &HashMap>, + method_returns: &HashMap<(String, String), String>, fn_returns: &HashMap, ) { if node.kind() != "assignment" { @@ -481,7 +535,7 @@ fn try_bind_assignment( } // x = expr — infer via eval - if let Some(t) = eval_expr_type(scope, class_fields, fn_returns, right, source) { + if let Some(t) = eval_expr_type(scope, class_fields, method_returns, fn_returns, right, source) { scope_bind(scope, name, &t); } } @@ -515,6 +569,7 @@ fn walk_node( scope: &mut Scope, enclosing_class: Option<&str>, class_fields: &HashMap>, + method_returns: &HashMap<(String, String), String>, fn_returns: &HashMap, graph: &G, out: &mut HashMap<(usize, usize), NodeKeys>, @@ -536,6 +591,7 @@ fn walk_node( scope, class_name.as_deref(), class_fields, + method_returns, fn_returns, graph, out, @@ -554,6 +610,7 @@ fn walk_node( scope, enclosing_class, class_fields, + method_returns, fn_returns, graph, out, @@ -587,6 +644,7 @@ fn walk_node( scope, enclosing_class, class_fields, + method_returns, fn_returns, graph, out, @@ -599,7 +657,7 @@ fn walk_node( "call" => { if let Some(func_node) = node.child_by_field_name("function") { if let Some(target) = - resolve_callee(scope, class_fields, fn_returns, graph, func_node, source, file, import_sources) + resolve_callee(scope, class_fields, method_returns, fn_returns, graph, func_node, source, file, import_sources) { // Key by the attribute identifier position (the method name) let pos = func_node @@ -616,6 +674,7 @@ fn walk_node( scope, enclosing_class, class_fields, + method_returns, fn_returns, graph, out, @@ -628,7 +687,7 @@ fn walk_node( "expression_statement" => { if let Some(inner) = node.named_child(0) { if inner.kind() == "assignment" { - try_bind_assignment(inner, source, scope, class_fields, fn_returns); + try_bind_assignment(inner, source, scope, class_fields, method_returns, fn_returns); } // Still walk for nested calls walk_node( @@ -637,6 +696,7 @@ fn walk_node( scope, enclosing_class, class_fields, + method_returns, fn_returns, graph, out, @@ -654,6 +714,7 @@ fn walk_node( scope, enclosing_class, class_fields, + method_returns, fn_returns, graph, out, @@ -691,6 +752,7 @@ pub fn resolve_file_calls( source: &str, file: &str, class_fields: &HashMap>, + method_returns: &HashMap<(String, String), String>, fn_returns: &HashMap, import_sources: &HashMap<(String, String), String>, var_types: &HashMap<(String, String), String>, @@ -719,6 +781,7 @@ pub fn resolve_file_calls( &mut scope, None, class_fields, + method_returns, fn_returns, graph, &mut out, diff --git a/ast/src/lang/registry/python.rs b/ast/src/lang/registry/python.rs index 5e5da238b..009fc87f9 100644 --- a/ast/src/lang/registry/python.rs +++ b/ast/src/lang/registry/python.rs @@ -9,6 +9,7 @@ pub struct PythonRegistry { methods: HashMap<(String, String), String>, pub(super) import_sources: HashMap<(String, String), String>, pub(super) class_fields: HashMap>, + method_returns: HashMap<(String, String), String>, resolved: HashMap<(String, usize, usize), NodeKeys>, } @@ -19,6 +20,7 @@ impl PythonRegistry { methods: HashMap::new(), import_sources: HashMap::new(), class_fields: HashMap::new(), + method_returns: HashMap::new(), resolved: HashMap::new(), }; @@ -84,6 +86,10 @@ impl PythonRegistry { .entry((file.clone(), var_name)) .or_insert(type_name); } + let method_rets = py_resolver::extract_method_return_types(source); + for (key, ret) in method_rets { + reg.method_returns.entry(key).or_insert(ret); + } } // Build fn_returns: func_name → (return_type, defining_file) @@ -106,6 +112,7 @@ impl PythonRegistry { source, file, ®.class_fields, + ®.method_returns, &fn_returns, ®.import_sources, ®.var_types, diff --git a/ast/src/lang/registry/rust_registry.rs b/ast/src/lang/registry/rust_registry.rs index c53304e48..eb021c475 100644 --- a/ast/src/lang/registry/rust_registry.rs +++ b/ast/src/lang/registry/rust_registry.rs @@ -16,6 +16,8 @@ pub struct RustRegistry { methods_idx: HashMap<(String, String), String>, /// struct_name → { field_name → base_type } struct_fields: HashMap>, + /// func_name → base_return_type for top-level free functions + fn_returns: HashMap, /// dir → { fn_name → NodeKeys } for free functions in the same crate directory pkg_fns: HashMap>, /// (file, row, col) → pre-resolved target NodeKeys @@ -27,6 +29,7 @@ impl RustRegistry { let mut reg = RustRegistry { methods_idx: HashMap::new(), struct_fields: HashMap::new(), + fn_returns: HashMap::new(), pkg_fns: HashMap::new(), resolved: HashMap::new(), }; @@ -57,7 +60,7 @@ impl RustRegistry { } } - // Pass 1.5: extract struct field types from source. + // Pass 1.5: extract struct field types and free-function return types from source. for (file, source) in filez { if !file.ends_with(".rs") { continue; @@ -69,6 +72,10 @@ impl RustRegistry { .or_default() .extend(field_map); } + let fns = rust_resolver::extract_fn_returns(source); + for (fname, ret) in fns { + reg.fn_returns.entry(fname).or_insert(ret); + } } // Pass 2: pre-resolve all call sites per file. @@ -80,6 +87,7 @@ impl RustRegistry { source, file, ®.struct_fields, + ®.fn_returns, ®.pkg_fns, graph, ) diff --git a/ast/src/lang/registry/rust_resolver.rs b/ast/src/lang/registry/rust_resolver.rs index d4bea2df9..29319b0b4 100644 --- a/ast/src/lang/registry/rust_resolver.rs +++ b/ast/src/lang/registry/rust_resolver.rs @@ -115,6 +115,44 @@ fn walk_field_declaration_list( } } +/// Extract `func_name → base_return_type` for top-level free functions. +/// Skips impl-block methods, generic/primitive return types, and functions +/// returning Self/Result/Option (too polymorphic to dispatch on). +pub fn extract_fn_returns(source: &str) -> HashMap { + let mut out = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + let src = source.as_bytes(); + let root = tree.root_node(); + for i in 0..root.named_child_count() { + let Some(node) = root.named_child(i) else { + continue; + }; + if node.kind() != "function_item" { + continue; + } + let Some(name_node) = node.child_by_field_name("name") else { + continue; + }; + let Ok(fname) = name_node.utf8_text(src) else { + continue; + }; + let Some(ret_node) = node.child_by_field_name("return_type") else { + continue; + }; + if let Some(ret_type) = strip_rust_type(ret_node, src) { + if !matches!(ret_type.as_str(), "Self" | "Result" | "Option") { + out.entry(fname.to_string()).or_insert(ret_type); + } + } + } + out +} + /// Extract `struct_name → { field_name → base_type }` from a Rust source file. /// Enum variants and tuple struct fields are skipped. pub fn extract_struct_fields(source: &str) -> HashMap> { @@ -162,6 +200,7 @@ pub fn extract_struct_fields(source: &str) -> HashMap>, + fn_returns: &HashMap, node: Node, source: &[u8], ) -> Option { @@ -176,6 +215,7 @@ fn eval_expr_type( let obj_type = eval_expr_type( scope, struct_fields, + fn_returns, node.child_by_field_name("value")?, source, )?; @@ -187,32 +227,39 @@ fn eval_expr_type( } // Type::new() → "Type" (constructor heuristic) + // free_fn() → look up in fn_returns "call_expression" => { let func = node.child_by_field_name("function")?; - if func.kind() == "scoped_identifier" { - let path = func.child_by_field_name("path")?; - let name = func.child_by_field_name("name")?; - let name_text = name.utf8_text(source).ok()?; - if name_text == "new" || name_text.starts_with("new_") { - // path could be identifier or scoped_identifier; take rightmost name - let path_name = match path.kind() { - "identifier" => path.utf8_text(source).ok()?.to_string(), - "scoped_identifier" => path - .child_by_field_name("name") - .and_then(|n| n.utf8_text(source).ok()) - .map(str::to_string)?, - _ => return None, - }; - return Some(path_name); + match func.kind() { + "scoped_identifier" => { + let path = func.child_by_field_name("path")?; + let name = func.child_by_field_name("name")?; + let name_text = name.utf8_text(source).ok()?; + if name_text == "new" || name_text.starts_with("new_") { + let path_name = match path.kind() { + "identifier" => path.utf8_text(source).ok()?.to_string(), + "scoped_identifier" => path + .child_by_field_name("name") + .and_then(|n| n.utf8_text(source).ok()) + .map(str::to_string)?, + _ => return None, + }; + return Some(path_name); + } + None + } + "identifier" => { + let func_name = func.utf8_text(source).ok()?; + fn_returns.get(func_name).cloned() } + _ => None, } - None } // &expr or *expr — strip the operator "reference_expression" | "unary_expression" => node .named_child(0) - .and_then(|inner| eval_expr_type(scope, struct_fields, inner, source)), + .and_then(|inner| eval_expr_type(scope, struct_fields, fn_returns, inner, source)), // Foo { ... } struct literal → type name "struct_expression" => { @@ -223,7 +270,7 @@ fn eval_expr_type( // (expr) — transparent "parenthesized_expression" => node .named_child(0) - .and_then(|inner| eval_expr_type(scope, struct_fields, inner, source)), + .and_then(|inner| eval_expr_type(scope, struct_fields, fn_returns, inner, source)), // expr as Type — use the cast type "type_cast_expression" => node @@ -233,7 +280,7 @@ fn eval_expr_type( // expr.await — recurse; Future → we don't track T, but try the inner expr "await_expression" => node .child_by_field_name("value") - .and_then(|inner| eval_expr_type(scope, struct_fields, inner, source)), + .and_then(|inner| eval_expr_type(scope, struct_fields, fn_returns, inner, source)), _ => None, } @@ -273,6 +320,7 @@ fn position_of_callee(func_node: Node) -> (usize, usize) { fn resolve_callee( scope: &Scope, struct_fields: &HashMap>, + fn_returns: &HashMap, pkg_fns: &HashMap>, graph: &G, node: Node, @@ -289,6 +337,7 @@ fn resolve_callee( let obj_type = eval_expr_type( scope, struct_fields, + fn_returns, node.child_by_field_name("value")?, source, )?; @@ -391,6 +440,7 @@ fn walk_node( source: &[u8], scope: &mut Scope, struct_fields: &HashMap>, + fn_returns: &HashMap, pkg_fns: &HashMap>, graph: &G, out: &mut HashMap<(usize, usize), NodeKeys>, @@ -412,6 +462,7 @@ fn walk_node( source, scope, struct_fields, + fn_returns, pkg_fns, graph, out, @@ -430,7 +481,7 @@ fn walk_node( bind_params(params, source, scope, impl_type); } if let Some(body) = node.child_by_field_name("body") { - walk_node(body, source, scope, struct_fields, pkg_fns, graph, out, file, None); + walk_node(body, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file, None); } scope_pop(scope); } @@ -439,7 +490,6 @@ fn walk_node( "closure_expression" => { scope_push(scope); if let Some(params) = node.child_by_field_name("parameters") { - // Closure params use "closure_parameter" kind, which may or may not have a type. for i in 0..params.named_child_count() { if let Some(p) = params.named_child(i) { if p.kind() == "closure_parameter" { @@ -460,22 +510,20 @@ fn walk_node( } } if let Some(body) = node.child_by_field_name("body") { - walk_node(body, source, scope, struct_fields, pkg_fns, graph, out, file, None); + walk_node(body, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file, None); } scope_pop(scope); } - // let x: Type = value OR let x = Type::new() + // let x: Type = value OR let x = free_fn() / Type::new() "let_declaration" => { - // Determine the type: explicit annotation wins, then constructor inference. let type_name = if let Some(type_node) = node.child_by_field_name("type") { strip_rust_type(type_node, source) } else { node.child_by_field_name("value") - .and_then(|v| eval_expr_type(scope, struct_fields, v, source)) + .and_then(|v| eval_expr_type(scope, struct_fields, fn_returns, v, source)) }; - // Bind the pattern identifier (simple ident only; skip tuple/struct patterns). if let Some(pat) = node.child_by_field_name("pattern") { if pat.kind() == "identifier" { if let (Ok(name), Some(t)) = (pat.utf8_text(source), &type_name) { @@ -486,9 +534,8 @@ fn walk_node( } } - // Recurse into the value expression to capture nested call edges. if let Some(val) = node.child_by_field_name("value") { - walk_node(val, source, scope, struct_fields, pkg_fns, graph, out, file, impl_type); + walk_node(val, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file, impl_type); } } @@ -496,19 +543,17 @@ fn walk_node( "call_expression" => { if let Some(func_node) = node.child_by_field_name("function") { if let Some(target) = - resolve_callee(scope, struct_fields, pkg_fns, graph, func_node, source, file) + resolve_callee(scope, struct_fields, fn_returns, pkg_fns, graph, func_node, source, file) { let pos = position_of_callee(func_node); out.entry(pos).or_insert(target); } - // Recurse into the function node (handles chained receivers). walk_node( - func_node, source, scope, struct_fields, pkg_fns, graph, out, file, impl_type, + func_node, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file, impl_type, ); } - // Recurse into arguments for nested calls. if let Some(args) = node.child_by_field_name("arguments") { - walk_node(args, source, scope, struct_fields, pkg_fns, graph, out, file, impl_type); + walk_node(args, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file, impl_type); } } @@ -516,7 +561,7 @@ fn walk_node( for i in 0..node.named_child_count() { if let Some(child) = node.named_child(i) { walk_node( - child, source, scope, struct_fields, pkg_fns, graph, out, file, impl_type, + child, source, scope, struct_fields, fn_returns, pkg_fns, graph, out, file, impl_type, ); } } @@ -532,6 +577,7 @@ pub fn resolve_file_calls( source: &str, file: &str, struct_fields: &HashMap>, + fn_returns: &HashMap, pkg_fns: &HashMap>, graph: &G, ) -> HashMap<(usize, usize), NodeKeys> { @@ -544,7 +590,6 @@ pub fn resolve_file_calls( }; let src = source.as_bytes(); - // Root scope starts empty — Rust doesn't have cross-file package-level var scope. let mut scope: Scope = vec![HashMap::new()]; walk_node( @@ -552,6 +597,7 @@ pub fn resolve_file_calls( src, &mut scope, struct_fields, + fn_returns, pkg_fns, graph, &mut out, diff --git a/ast/src/lang/registry/swift_registry.rs b/ast/src/lang/registry/swift_registry.rs index 0e96cf335..c9d319939 100644 --- a/ast/src/lang/registry/swift_registry.rs +++ b/ast/src/lang/registry/swift_registry.rs @@ -14,6 +14,7 @@ fn parent_dir(file: &str) -> String { pub struct SwiftRegistry { class_fields: HashMap>, method_returns: HashMap<(String, String), String>, + fn_returns: HashMap, dir_fns: HashMap>, resolved: HashMap<(String, usize, usize), NodeKeys>, } @@ -23,6 +24,7 @@ impl SwiftRegistry { let mut reg = SwiftRegistry { class_fields: HashMap::new(), method_returns: HashMap::new(), + fn_returns: HashMap::new(), dir_fns: HashMap::new(), resolved: HashMap::new(), }; @@ -59,6 +61,10 @@ impl SwiftRegistry { for (key, ret) in returns { reg.method_returns.entry(key).or_insert(ret); } + let fns = swift_resolver::extract_fn_returns(source); + for (fname, ret) in fns { + reg.fn_returns.entry(fname).or_insert(ret); + } } // Pass 2: pre-resolve all call sites per file. @@ -71,6 +77,7 @@ impl SwiftRegistry { file, ®.class_fields, ®.method_returns, + ®.fn_returns, ®.dir_fns, graph, ) diff --git a/ast/src/lang/registry/swift_resolver.rs b/ast/src/lang/registry/swift_resolver.rs index f1d33377e..018734432 100644 --- a/ast/src/lang/registry/swift_resolver.rs +++ b/ast/src/lang/registry/swift_resolver.rs @@ -214,6 +214,40 @@ fn recurse_class_fields( // ── Method return type extraction ────────────────────────────────────────────── +pub fn extract_fn_returns(source: &str) -> HashMap { + let mut out = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + let src = source.as_bytes(); + let root = tree.root_node(); + for i in 0..root.named_child_count() { + let Some(node) = root.named_child(i) else { + continue; + }; + if node.kind() != "function_declaration" { + continue; + } + let Some(name_node) = node.child_by_field_name("name") else { + continue; + }; + let Ok(func_name) = name_node.utf8_text(src) else { + continue; + }; + let ret_type = node + .child_by_field_name("return_type") + .and_then(|n| strip_swift_type(n, src)) + .filter(|t| !SKIP_TYPES.contains(&t.as_str())); + if let Some(rt) = ret_type { + out.entry(func_name.to_string()).or_insert(rt); + } + } + out +} + pub fn extract_method_return_types(source: &str) -> HashMap<(String, String), String> { let mut out = HashMap::new(); let Some(mut parser) = make_parser() else { @@ -315,6 +349,7 @@ fn eval_expr_type( scope: &Scope, class_fields: &HashMap>, method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, node: Node, source: &[u8], ) -> Option { @@ -334,7 +369,7 @@ fn eval_expr_type( .child_by_field_name("suffix") .filter(|n| n.kind() == "simple_identifier") .and_then(|n| n.utf8_text(source).ok())?; - let recv_type = eval_expr_type(scope, class_fields, method_returns, recv, source)?; + let recv_type = eval_expr_type(scope, class_fields, method_returns, fn_returns, recv, source)?; class_fields.get(&recv_type)?.get(prop).cloned() } @@ -342,12 +377,13 @@ fn eval_expr_type( let first = node.named_child(0)?; match first.kind() { // ClassName(...) → constructor: type = class name + // factoryFn() → free function return type from fn_returns "simple_identifier" => { let name = first.utf8_text(source).ok()?; if name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) { Some(name.to_string()) } else { - None + fn_returns.get(name).cloned() } } // obj.method(...) → look up return type @@ -359,7 +395,7 @@ fn eval_expr_type( .filter(|n| n.kind() == "simple_identifier") .and_then(|n| n.utf8_text(source).ok())?; let recv_type = - eval_expr_type(scope, class_fields, method_returns, recv, source)?; + eval_expr_type(scope, class_fields, method_returns, fn_returns, recv, source)?; method_returns .get(&(recv_type, method_name.to_string())) .cloned() @@ -372,7 +408,7 @@ fn eval_expr_type( "try_expression" | "await_expression" => node .child_by_field_name("expr") .or_else(|| (0..node.named_child_count()).filter_map(|i| node.named_child(i)).find(|n| !matches!(n.kind(), "try_operator" | "await"))) - .and_then(|n| eval_expr_type(scope, class_fields, method_returns, n, source)), + .and_then(|n| eval_expr_type(scope, class_fields, method_returns, fn_returns, n, source)), _ => None, } @@ -415,6 +451,7 @@ fn walk_node( scope: &mut Scope, class_fields: &HashMap>, method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, dir_fns: &HashMap>, graph: &G, out: &mut HashMap<(usize, usize), NodeKeys>, @@ -440,7 +477,7 @@ fn walk_node( if let Some(child) = body.named_child(i) { walk_node( child, source, scope, class_fields, method_returns, - dir_fns, graph, out, file, + fn_returns, dir_fns, graph, out, file, ); } } @@ -455,7 +492,7 @@ fn walk_node( if let Some(body) = node.child_by_field_name("body") { walk_node( body, source, scope, class_fields, method_returns, - dir_fns, graph, out, file, + fn_returns, dir_fns, graph, out, file, ); } scope_pop(scope); @@ -466,7 +503,7 @@ fn walk_node( scope_push(scope); recurse( node, source, scope, class_fields, method_returns, - dir_fns, graph, out, file, + fn_returns, dir_fns, graph, out, file, ); scope_pop(scope); } @@ -495,7 +532,7 @@ fn walk_node( }); let inferred = init_node.and_then(|n| { - eval_expr_type(scope, class_fields, method_returns, n, source) + eval_expr_type(scope, class_fields, method_returns, fn_returns, n, source) }); let bound = type_ann.or(inferred); @@ -508,7 +545,7 @@ fn walk_node( if let Some(init) = init_node { walk_node( init, source, scope, class_fields, method_returns, - dir_fns, graph, out, file, + fn_returns, dir_fns, graph, out, file, ); } } @@ -529,7 +566,7 @@ fn walk_node( if let Some(method_name_node) = method_name_node { if let Ok(method_name) = method_name_node.utf8_text(source) { let recv_type = eval_expr_type( - scope, class_fields, method_returns, recv_node, source, + scope, class_fields, method_returns, fn_returns, recv_node, source, ); if let Some(recv_type) = recv_type { if let Some(target) = @@ -567,14 +604,14 @@ fn walk_node( // Recurse into all children (args, trailing closures, etc.) recurse( node, source, scope, class_fields, method_returns, - dir_fns, graph, out, file, + fn_returns, dir_fns, graph, out, file, ); } _ => { recurse( node, source, scope, class_fields, method_returns, - dir_fns, graph, out, file, + fn_returns, dir_fns, graph, out, file, ); } } @@ -586,6 +623,7 @@ fn recurse( scope: &mut Scope, class_fields: &HashMap>, method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, dir_fns: &HashMap>, graph: &G, out: &mut HashMap<(usize, usize), NodeKeys>, @@ -595,7 +633,7 @@ fn recurse( if let Some(child) = node.named_child(i) { walk_node( child, source, scope, class_fields, method_returns, - dir_fns, graph, out, file, + fn_returns, dir_fns, graph, out, file, ); } } @@ -608,6 +646,7 @@ pub fn resolve_file_calls( file: &str, class_fields: &HashMap>, method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, dir_fns: &HashMap>, graph: &G, ) -> HashMap<(usize, usize), NodeKeys> { @@ -626,6 +665,7 @@ pub fn resolve_file_calls( &mut scope, class_fields, method_returns, + fn_returns, dir_fns, graph, &mut out, diff --git a/ast/src/testing/coverage/csharp.rs b/ast/src/testing/coverage/csharp.rs index 53c2e550d..5ad52dfe4 100644 --- a/ast/src/testing/coverage/csharp.rs +++ b/ast/src/testing/coverage/csharp.rs @@ -71,7 +71,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(endpoints.len(), 81); let functions = graph.find_nodes_by_type(NodeType::Function); - assert_eq!(functions.len(), 362); + assert_eq!(functions.len(), 366); let unit_tests = graph.find_nodes_by_type(NodeType::UnitTest); assert_eq!(unit_tests.len(), 31); @@ -83,7 +83,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(e2e_tests.len(), 0); let classes = graph.find_nodes_by_type(NodeType::Class); - assert_eq!(classes.len(), 164); + assert_eq!(classes.len(), 167); let data_models = graph.find_nodes_by_type(NodeType::DataModel); assert_eq!(data_models.len(), 19); @@ -108,10 +108,10 @@ async fn test_btreemap_test_to_function_edges() -> Result<()> { let graph = repo.build_graph_inner::().await?; let calls_edges = graph.count_edges_of_type(EdgeType::Calls); - assert_eq!(calls_edges, 300); + assert_eq!(calls_edges, 302); let contains_edges = graph.count_edges_of_type(EdgeType::Contains); - assert_eq!(contains_edges, 1523); + assert_eq!(contains_edges, 1531); let handler_edges = graph.count_edges_of_type(EdgeType::Handler); assert_eq!(handler_edges, 81); diff --git a/ast/src/testing/coverage/go.rs b/ast/src/testing/coverage/go.rs index 43a3bfb07..9bad9b7e4 100644 --- a/ast/src/testing/coverage/go.rs +++ b/ast/src/testing/coverage/go.rs @@ -71,7 +71,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(endpoints.len(), 5); let functions = graph.find_nodes_by_type(NodeType::Function); - assert_eq!(functions.len(), 37); + assert_eq!(functions.len(), 40); let unit_tests = graph.find_nodes_by_type(NodeType::UnitTest); assert_eq!(unit_tests.len(), 2); @@ -83,10 +83,10 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(e2e_tests.len(), 2); let classes = graph.find_nodes_by_type(NodeType::Class); - assert_eq!(classes.len(), 10); + assert_eq!(classes.len(), 11); let data_models = graph.find_nodes_by_type(NodeType::DataModel); - assert_eq!(data_models.len(), 14); + assert_eq!(data_models.len(), 15); let traits = graph.find_nodes_by_type(NodeType::Trait); assert_eq!(traits.len(), 1); @@ -108,10 +108,10 @@ async fn test_btreemap_test_to_function_edges() -> Result<()> { let graph = repo.build_graph_inner::().await?; let calls_edges = graph.count_edges_of_type(EdgeType::Calls); - assert_eq!(calls_edges, 19); + assert_eq!(calls_edges, 22); let contains_edges = graph.count_edges_of_type(EdgeType::Contains); - assert_eq!(contains_edges, 128); + assert_eq!(contains_edges, 136); let handler_edges = graph.count_edges_of_type(EdgeType::Handler); assert_eq!(handler_edges, 5); diff --git a/ast/src/testing/coverage/java.rs b/ast/src/testing/coverage/java.rs index 3f4edc801..423e1b93f 100644 --- a/ast/src/testing/coverage/java.rs +++ b/ast/src/testing/coverage/java.rs @@ -71,7 +71,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(endpoints.len(), 11); let functions = graph.find_nodes_by_type(NodeType::Function); - assert_eq!(functions.len(), 52); + assert_eq!(functions.len(), 56); let unit_tests = graph.find_nodes_by_type(NodeType::UnitTest); assert_eq!(unit_tests.len(), 1); @@ -83,7 +83,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(e2e_tests.len(), 0); let classes = graph.find_nodes_by_type(NodeType::Class); - assert_eq!(classes.len(), 18); + assert_eq!(classes.len(), 21); let data_models = graph.find_nodes_by_type(NodeType::DataModel); assert_eq!(data_models.len(), 2); @@ -108,10 +108,10 @@ async fn test_btreemap_test_to_function_edges() -> Result<()> { let graph = repo.build_graph_inner::().await?; let calls_edges = graph.count_edges_of_type(EdgeType::Calls); - assert_eq!(calls_edges, 35); + assert_eq!(calls_edges, 37); let contains_edges = graph.count_edges_of_type(EdgeType::Contains); - assert_eq!(contains_edges, 247); + assert_eq!(contains_edges, 259); let handler_edges = graph.count_edges_of_type(EdgeType::Handler); assert_eq!(handler_edges, 11); diff --git a/ast/src/testing/coverage/kotlin.rs b/ast/src/testing/coverage/kotlin.rs index 2272edcf9..df3181b85 100644 --- a/ast/src/testing/coverage/kotlin.rs +++ b/ast/src/testing/coverage/kotlin.rs @@ -71,7 +71,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(endpoints.len(), 0); let functions = graph.find_nodes_by_type(NodeType::Function); - assert_eq!(functions.len(), 27); + assert_eq!(functions.len(), 30); let unit_tests = graph.find_nodes_by_type(NodeType::UnitTest); assert_eq!(unit_tests.len(), 3); @@ -83,7 +83,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(e2e_tests.len(), 0); let classes = graph.find_nodes_by_type(NodeType::Class); - assert_eq!(classes.len(), 16); + assert_eq!(classes.len(), 17); let data_models = graph.find_nodes_by_type(NodeType::DataModel); assert_eq!(data_models.len(), 9); @@ -108,10 +108,10 @@ async fn test_btreemap_test_to_function_edges() -> Result<()> { let graph = repo.build_graph_inner::().await?; let calls_edges = graph.count_edges_of_type(EdgeType::Calls); - assert_eq!(calls_edges, 28); + assert_eq!(calls_edges, 30); let contains_edges = graph.count_edges_of_type(EdgeType::Contains); - assert_eq!(contains_edges, 219); + assert_eq!(contains_edges, 225); let handler_edges = graph.count_edges_of_type(EdgeType::Handler); assert_eq!(handler_edges, 0); diff --git a/ast/src/testing/coverage/php.rs b/ast/src/testing/coverage/php.rs index 15b3c3596..0ef5a9b50 100644 --- a/ast/src/testing/coverage/php.rs +++ b/ast/src/testing/coverage/php.rs @@ -71,7 +71,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(endpoints.len(), 41); let functions = graph.find_nodes_by_type(NodeType::Function); - assert_eq!(functions.len(), 22); + assert_eq!(functions.len(), 25); let unit_tests = graph.find_nodes_by_type(NodeType::UnitTest); assert_eq!(unit_tests.len(), 5); @@ -83,7 +83,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(e2e_tests.len(), 0); let classes = graph.find_nodes_by_type(NodeType::Class); - assert_eq!(classes.len(), 9); + assert_eq!(classes.len(), 10); let data_models = graph.find_nodes_by_type(NodeType::DataModel); assert_eq!(data_models.len(), 0); @@ -108,10 +108,10 @@ async fn test_btreemap_test_to_function_edges() -> Result<()> { let graph = repo.build_graph_inner::().await?; let calls_edges = graph.count_edges_of_type(EdgeType::Calls); - assert_eq!(calls_edges, 22); + assert_eq!(calls_edges, 25); let contains_edges = graph.count_edges_of_type(EdgeType::Contains); - assert_eq!(contains_edges, 106); + assert_eq!(contains_edges, 112); let handler_edges = graph.count_edges_of_type(EdgeType::Handler); assert_eq!(handler_edges, 13); diff --git a/ast/src/testing/coverage/rust.rs b/ast/src/testing/coverage/rust.rs index c3a13eabc..5a4e3a131 100644 --- a/ast/src/testing/coverage/rust.rs +++ b/ast/src/testing/coverage/rust.rs @@ -78,8 +78,8 @@ async fn test_rust_coverage() -> Result<()> { let functions = graph.find_nodes_by_type(NodeType::Function); assert_eq!( functions.len(), - 102, - "Expected 102 functions after test improvements" + 104, + "Expected 104 functions after test improvements" ); let unit_tests = graph.find_nodes_by_type(NodeType::UnitTest); @@ -92,7 +92,7 @@ async fn test_rust_coverage() -> Result<()> { assert_eq!(e2e_tests.len(), 8, "Expected 8 e2e tests"); let calls_edges = graph.count_edges_of_type(EdgeType::Calls); - assert_eq!(calls_edges, 122, "Expected 122 Calls edges"); + assert_eq!(calls_edges, 125, "Expected 125 Calls edges"); let unit_test_to_function_edges = graph.find_nodes_with_edge_type(NodeType::UnitTest, NodeType::Function, EdgeType::Calls); @@ -139,8 +139,8 @@ async fn test_rust_graph_upload() -> Result<()> { let graph_ops = setup_rust_graph().await?; let (nodes, edges) = graph_ops.get_graph_size().await?; - assert_eq!(nodes, 321, "Graph should have 321 nodes after upload"); - assert_eq!(edges, 524, "Graph should have 524 edges after upload"); + assert_eq!(nodes, 323, "Graph should have 323 nodes after upload"); + assert_eq!(edges, 530, "Graph should have 530 edges after upload"); Ok(()) } @@ -159,15 +159,15 @@ async fn test_coverage_default_params() -> Result<()> { ); let unit = coverage.unit_tests.expect("Should have unit_tests"); - assert_eq!(unit.total, 49, "Should have 49 unit test targets"); + assert_eq!(unit.total, 51, "Should have 51 unit test targets"); assert_eq!(unit.total_tests, 43, "Should have 43 unit tests"); let integration = coverage .integration_tests .expect("Should have integration_tests"); assert_eq!( - integration.total, 49, - "Should have 49 integration test targets" + integration.total, 51, + "Should have 51 integration test targets" ); assert_eq!( integration.total_tests, 17, @@ -215,8 +215,8 @@ async fn test_coverage_with_ignore_dirs() -> Result<()> { let unit = filtered.unit_tests.expect("Should have unit_tests"); assert_eq!( - unit.total, 43, - "Should have 43 targets after ignoring routes" + unit.total, 45, + "Should have 45 targets after ignoring routes" ); Ok(()) @@ -297,7 +297,7 @@ async fn test_nodes_function_type() -> Result<()> { ) .await?; - assert_eq!(count, 49, "Should have 49 unique Function nodes"); + assert_eq!(count, 51, "Should have 51 unique Function nodes"); for (node_type, _, _, _, _, _, _, _, _) in &results { assert_eq!( @@ -525,7 +525,7 @@ async fn test_nodes_multi_type() -> Result<()> { ) .await?; - assert_eq!(count, 70, "Should have 49 Functions + 21 Endpoints = 70"); + assert_eq!(count, 72, "Should have 51 Functions + 21 Endpoints = 72"); let has_function = results .iter() @@ -591,7 +591,7 @@ async fn test_nodes_pagination_default() -> Result<()> { ) .await?; - assert_eq!(count, 49, "Total count should be 49"); + assert_eq!(count, 51, "Total count should be 51"); assert_eq!(results.len(), 10, "Should return 10 items per page"); Ok(()) @@ -678,7 +678,7 @@ async fn test_nodes_pagination_large_offset() -> Result<()> { ) .await?; - assert_eq!(count, 49, "Total count should still be accurate"); + assert_eq!(count, 51, "Total count should still be accurate"); assert!( results.is_empty(), "Should return empty for offset beyond data" @@ -708,8 +708,8 @@ async fn test_nodes_pagination_max_limit() -> Result<()> { ) .await?; - assert_eq!(count, 49, "Total count should be 49"); - assert_eq!(results.len(), 49, "Should return all 49 functions"); + assert_eq!(count, 51, "Total count should be 51"); + assert_eq!(results.len(), 51, "Should return all 51 functions"); Ok(()) } @@ -949,7 +949,7 @@ async fn test_nodes_repo_filter() -> Result<()> { ) .await?; - assert_eq!(count, 49, "Should find 49 functions in Rust repo"); + assert_eq!(count, 51, "Should find 51 functions in Rust repo"); for (_, node_data, _, _, _, _, _, _, _) in &results { assert!( @@ -1121,8 +1121,8 @@ async fn test_nodes_is_muted() -> Result<()> { ) .await?; - assert_eq!(unmuted_count, 49, "Unmuted count should be 49"); - assert_eq!(all_count, 49, "All count should be 49"); + assert_eq!(unmuted_count, 51, "Unmuted count should be 51"); + assert_eq!(all_count, 51, "All count should be 51"); assert_eq!( unmuted_count, all_count, "Unmuted should equal all when no nodes muted" diff --git a/ast/src/testing/coverage/swift.rs b/ast/src/testing/coverage/swift.rs index a6fee1fce..115e93add 100644 --- a/ast/src/testing/coverage/swift.rs +++ b/ast/src/testing/coverage/swift.rs @@ -71,7 +71,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(endpoints.len(), 0); let functions = graph.find_nodes_by_type(NodeType::Function); - assert_eq!(functions.len(), 36); + assert_eq!(functions.len(), 39); let unit_tests = graph.find_nodes_by_type(NodeType::UnitTest); assert_eq!(unit_tests.len(), 5); @@ -83,7 +83,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(e2e_tests.len(), 0); let classes = graph.find_nodes_by_type(NodeType::Class); - assert_eq!(classes.len(), 26); + assert_eq!(classes.len(), 27); let data_models = graph.find_nodes_by_type(NodeType::DataModel); assert_eq!(data_models.len(), 1); @@ -108,10 +108,10 @@ async fn test_btreemap_test_to_function_edges() -> Result<()> { let graph = repo.build_graph_inner::().await?; let calls_edges = graph.count_edges_of_type(EdgeType::Calls); - assert_eq!(calls_edges, 24); + assert_eq!(calls_edges, 26); let contains_edges = graph.count_edges_of_type(EdgeType::Contains); - assert_eq!(contains_edges, 168); + assert_eq!(contains_edges, 173); let handler_edges = graph.count_edges_of_type(EdgeType::Handler); assert_eq!(handler_edges, 0); diff --git a/ast/src/testing/csharp/MethodChainFactory.cs b/ast/src/testing/csharp/MethodChainFactory.cs new file mode 100644 index 000000000..bd0095b3b --- /dev/null +++ b/ast/src/testing/csharp/MethodChainFactory.cs @@ -0,0 +1,38 @@ +// Tests static-factory call resolution via the uppercase-identifier fallback. +// +// WidgetFactory.Create() is a static method returning Widget. The resolver +// must recognise "WidgetFactory" as a class-name receiver (not a local var) +// so it can look up the method in the graph. Then w.Render() resolves +// Widget::Render via find_method_in_class. +// +// @ast node: Class "Widget" +// @ast node: Class "WidgetClient" +// @ast node: Class "WidgetFactory" +// @ast node: Function "Create" +// @ast node: Function "Render" +// @ast node: Function "WidgetClient" +// @ast node: Function "Run" +// @ast edge: Calls -> Function "Create" "MethodChainFactory.cs" +// @ast edge: Calls -> Function "Render" "MethodChainFactory.cs" +namespace MethodChain; + +class Widget +{ + public void Render() { } +} + +class WidgetFactory +{ + public static Widget Create() { return new Widget(); } +} + +class WidgetClient +{ + public WidgetClient() { } + + public void Run() + { + Widget w = WidgetFactory.Create(); + w.Render(); + } +} diff --git a/ast/src/testing/go/method_chain.go b/ast/src/testing/go/method_chain.go index 7144f217a..10dcc2771 100644 --- a/ast/src/testing/go/method_chain.go +++ b/ast/src/testing/go/method_chain.go @@ -5,7 +5,11 @@ package main // @ast node: DataModel "ServiceHandler" // @ast node: Class "ItemStore" // @ast edge: Operand -> Function "fetchItems" "method_chain.go" +// @ast edge: Operand -> Function "getRecord" "method_chain.go" // @ast node: DataModel "ItemStore" +// @ast node: Class "Record" +// @ast edge: Operand -> Function "process" "method_chain.go" +// @ast node: DataModel "Record" // @ast node: Function "fetchItems" // @ast node: Function "processItems" // @ast edge: Calls -> Function "fetchItems" "method_chain.go" @@ -15,6 +19,12 @@ package main // @ast node: Function "runFactory" // @ast edge: Calls -> Function "newItemStore" "method_chain.go" // @ast edge: Calls -> Function "fetchItems" "method_chain.go" +// @ast node: Function "getRecord" +// @ast node: Function "process" +// @ast node: Function "runMethodChain" +// @ast edge: Calls -> Function "newItemStore" "method_chain.go" +// @ast edge: Calls -> Function "getRecord" "method_chain.go" +// @ast edge: Calls -> Function "process" "method_chain.go" type ServiceHandler struct { store *ItemStore @@ -48,3 +58,19 @@ func runFactory() []string { store := newItemStore() return store.fetchItems() } + +// Record is a result type returned by a method. +type Record struct{} + +func (r *Record) process() string { return "done" } + +// getRecord is a method on ItemStore returning *Record; its return type seeds method_returns. +func (s *ItemStore) getRecord() *Record { return &Record{} } + +// runMethodChain exercises the method_returns path: store.getRecord() types the result +// as Record, then record.process() resolves via the method_returns lookup. +func runMethodChain() string { + store := newItemStore() + record := store.getRecord() + return record.process() +} diff --git a/ast/src/testing/java/src/main/java/graph/stakgraph/java/nonweb/MethodChainFactory.java b/ast/src/testing/java/src/main/java/graph/stakgraph/java/nonweb/MethodChainFactory.java new file mode 100644 index 000000000..f4b2e26d7 --- /dev/null +++ b/ast/src/testing/java/src/main/java/graph/stakgraph/java/nonweb/MethodChainFactory.java @@ -0,0 +1,39 @@ +// Tests static-factory call resolution. +// +// WidgetFactory.create() is a static method returning Widget. The resolver +// must recognise "WidgetFactory" as a class-name receiver (not a local var) +// so it can look up (WidgetFactory, create) in method_returns → Widget. +// Then w.render() resolves Widget::render via class_fields dispatch. +// +// @ast node: Class "Widget" +// @ast node: Function "render" +// @ast node: Class "WidgetFactory" +// @ast node: Function "create" +// @ast node: Class "WidgetClient" +// @ast node: Function "WidgetClient" +// @ast node: Function "run" +// @ast edge: Calls -> Function "create" "MethodChainFactory.java" +// @ast edge: Calls -> Function "render" "MethodChainFactory.java" +// @ast node: Instance "w" +// @ast edge: Of -> Class "Widget" "MethodChainFactory.java" +// @ast node: Var "w" +package graph.stakgraph.java.nonweb; + +class Widget { + public void render() {} +} + +class WidgetFactory { + public static Widget create() { + return new Widget(); + } +} + +class WidgetClient { + public WidgetClient() {} + + public void run() { + Widget w = WidgetFactory.create(); + w.render(); + } +} diff --git a/ast/src/testing/kotlin/app/src/main/java/com/kotlintestapp/MethodChainFactory.kt b/ast/src/testing/kotlin/app/src/main/java/com/kotlintestapp/MethodChainFactory.kt new file mode 100644 index 000000000..2ac8861df --- /dev/null +++ b/ast/src/testing/kotlin/app/src/main/java/com/kotlintestapp/MethodChainFactory.kt @@ -0,0 +1,24 @@ +package com.kotlintestapp +// Tests free-function return-type seeding (fn_returns) for Kotlin. +// makeWidget() returns Widget; fn_returns seeds the return type so that +// `val w = makeWidget()` binds w → Widget, enabling w.render() to +// resolve to Widget::render via find_method_in_class. +// +// @ast node: Class "Widget" +// @ast node: Function "render" +// @ast node: Function "makeWidget" +// @ast node: Function "factoryPipeline" +// @ast edge: Calls -> Function "makeWidget" "MethodChainFactory.kt" +// @ast edge: Calls -> Function "render" "MethodChainFactory.kt" +// @ast node: Import "import-imports-srctestingkotlinappsrcmainjavacomkotlintestappmethodchainfactorykt-0" + +class Widget { + fun render() {} +} + +fun makeWidget(): Widget = Widget() + +fun factoryPipeline() { + val w = makeWidget() + w.render() +} diff --git a/ast/src/testing/php/factory_function.php b/ast/src/testing/php/factory_function.php new file mode 100644 index 000000000..ea98bcbb7 --- /dev/null +++ b/ast/src/testing/php/factory_function.php @@ -0,0 +1,30 @@ + Function "render" "factory_function.php" +// @ast node: Function "render" +// @ast node: Function "createWidget" +// @ast node: Function "useWidget" +// @ast edge: Calls -> Function "createWidget" "factory_function.php" +// @ast edge: Calls -> Function "render" "factory_function.php" +// @ast node: Var "$w" + +namespace App; + +class Widget +{ + public function render(): string + { + return 'rendered'; + } +} + +function createWidget(): Widget +{ + return new Widget(); +} + +function useWidget(): string +{ + $w = createWidget(); + return $w->render(); +} diff --git a/ast/src/testing/python/module_calls/module_calls.py b/ast/src/testing/python/module_calls/module_calls.py index bd6bc094c..df05e18b0 100644 --- a/ast/src/testing/python/module_calls/module_calls.py +++ b/ast/src/testing/python/module_calls/module_calls.py @@ -1,3 +1,28 @@ +# @ast node: Class "Record" +# @ast edge: Operand -> Function "process" "module_calls.py" +class Record: + # @ast node: Function "process" + def process(self) -> str: + return "done" + + +# @ast node: Class "Store" +# @ast edge: Operand -> Function "get_record" "module_calls.py" +class Store: + # @ast node: Function "get_record" + def get_record(self) -> Record: + return Record() + + +# @ast node: Function "run_method_chain" +# @ast edge: Calls -> Function "get_record" "module_calls.py" +# @ast edge: Calls -> Function "process" "module_calls.py" +def run_method_chain() -> str: + store = Store() + record = store.get_record() + return record.process() + + # @ast node: Function "fetch_data" def fetch_data(url: str) -> dict: return {"url": url} diff --git a/ast/src/testing/rust/src/method_chain.rs b/ast/src/testing/rust/src/method_chain.rs index 5ffec1393..b3b266ed4 100644 --- a/ast/src/testing/rust/src/method_chain.rs +++ b/ast/src/testing/rust/src/method_chain.rs @@ -30,6 +30,10 @@ use std::sync::Arc; // @ast node: Function "pipeline" // @ast edge: Calls -> Function "new" "method_chain.rs" [operand=Repo] // @ast edge: Calls -> Function "total" "method_chain.rs" +// @ast node: Function "make_data_store" +// @ast node: Function "factory_pipeline" +// @ast edge: Calls -> Function "make_data_store" "method_chain.rs" +// @ast edge: Calls -> Function "count" "method_chain.rs" [operand=DataStore] /// Leaf type — Pool::count is the first "count" defined in this file. pub struct Pool; @@ -101,3 +105,16 @@ pub fn pipeline() -> usize { let repo = Repo::new(); repo.total() } + +/// Factory function: return type seeds fn_returns so that callers can type-bind +/// the result variable without an explicit type annotation. +pub fn make_data_store() -> DataStore { + DataStore::new() +} + +/// Tests the fn_returns path: let store = make_data_store() binds store → DataStore, +/// then store.count() resolves to DataStore::count via struct_fields dispatch. +pub fn factory_pipeline() -> usize { + let store = make_data_store(); + store.count() +} diff --git a/ast/src/testing/swift/ModernApp/Sources/ModernApp/MethodChainFactory.swift b/ast/src/testing/swift/ModernApp/Sources/ModernApp/MethodChainFactory.swift new file mode 100644 index 000000000..363b7bca5 --- /dev/null +++ b/ast/src/testing/swift/ModernApp/Sources/ModernApp/MethodChainFactory.swift @@ -0,0 +1,25 @@ +// Tests free-function return-type seeding (fn_returns) for Swift. +// makeWidget() is a top-level function returning Widget. The resolver +// seeds fn_returns["makeWidget"] = "Widget" so that +// `let w = makeWidget()` binds w → Widget, enabling w.render() to +// resolve to Widget::render via find_method_in_class. +// +// @ast node: Class "Widget" +// @ast node: Function "render" +// @ast node: Function "makeWidget" +// @ast node: Function "factoryPipeline" +// @ast edge: Calls -> Function "makeWidget" "MethodChainFactory.swift" +// @ast edge: Calls -> Function "render" "MethodChainFactory.swift" + +class Widget { + func render() {} +} + +func makeWidget() -> Widget { + Widget() +} + +func factoryPipeline() { + let w = makeWidget() + w.render() +} diff --git a/cli/tests/cli/csharp.rs b/cli/tests/cli/csharp.rs index ff04353bc..b3e48137e 100644 --- a/cli/tests/cli/csharp.rs +++ b/cli/tests/cli/csharp.rs @@ -48,8 +48,8 @@ fn parse_stats_csharp_dir() { assert_eq!(out.exit_code, 0, "stderr: {}", out.stderr); assert!(out.stdout.contains("Endpoint 81"), "stdout: {}", out.stdout); - assert!(out.stdout.contains("Class 164"), "stdout: {}", out.stdout); - assert!(out.stdout.contains("Function 362"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Class 167"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Function 366"), "stdout: {}", out.stdout); } // ── search ──────────────────────────────────────────────────────────────────── diff --git a/cli/tests/cli/java.rs b/cli/tests/cli/java.rs index 88abdcb32..be68c4f3d 100644 --- a/cli/tests/cli/java.rs +++ b/cli/tests/cli/java.rs @@ -40,8 +40,8 @@ fn parse_stats_java_dir() { let out = run_stakgraph(&["--stats", &dir]); assert_eq!(out.exit_code, 0, "stderr: {}", out.stderr); - assert!(out.stdout.contains("Class 13"), "stdout: {}", out.stdout); - assert!(out.stdout.contains("Import 17"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Class 16"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Import 18"), "stdout: {}", out.stdout); } // ── search ──────────────────────────────────────────────────────────────────── diff --git a/cli/tests/cli/kotlin.rs b/cli/tests/cli/kotlin.rs index 59ef20fc0..fd2875627 100644 --- a/cli/tests/cli/kotlin.rs +++ b/cli/tests/cli/kotlin.rs @@ -43,8 +43,8 @@ fn parse_stats_kotlin_dir() { let out = run_stakgraph(&["--stats", &dir]); assert_eq!(out.exit_code, 0, "stderr: {}", out.stderr); - assert!(out.stdout.contains("Class 16"), "stdout: {}", out.stdout); - assert!(out.stdout.contains("Function 27"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Class 17"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Function 30"), "stdout: {}", out.stdout); assert!(out.stdout.contains("Request 5"), "stdout: {}", out.stdout); assert!(out.stdout.contains("UnitTest 3"), "stdout: {}", out.stdout); } diff --git a/cli/tests/cli/php.rs b/cli/tests/cli/php.rs index be15b41c6..cf9c55774 100644 --- a/cli/tests/cli/php.rs +++ b/cli/tests/cli/php.rs @@ -51,8 +51,8 @@ fn parse_stats_php_dir() { assert_eq!(out.exit_code, 0, "stderr: {}", out.stderr); assert!(out.stdout.contains("Endpoint 41"), "stdout: {}", out.stdout); - assert!(out.stdout.contains("Class 9"), "stdout: {}", out.stdout); - assert!(out.stdout.contains("Function 22"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Class 10"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Function 25"), "stdout: {}", out.stdout); } // ── search ──────────────────────────────────────────────────────────────────── diff --git a/cli/tests/cli/swift.rs b/cli/tests/cli/swift.rs index 55fb53238..980ebed7d 100644 --- a/cli/tests/cli/swift.rs +++ b/cli/tests/cli/swift.rs @@ -39,8 +39,8 @@ fn parse_stats_swift_dir() { let out = run_stakgraph(&["--stats", &dir]); assert_eq!(out.exit_code, 0, "stderr: {}", out.stderr); - assert!(out.stdout.contains("Class 26"), "stdout: {}", out.stdout); - assert!(out.stdout.contains("Function 36"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Class 27"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Function 39"), "stdout: {}", out.stdout); assert!(out.stdout.contains("UnitTest 5"), "stdout: {}", out.stdout); } From 4e44d4343ffc2964b17924ee68d02eb7e5e0c6b3 Mon Sep 17 00:00:00 2001 From: kelmith Date: Sat, 27 Jun 2026 04:06:46 +0530 Subject: [PATCH 2/3] feat: update pending call resolution types --- ast/src/lang/registry/csharp_registry.rs | 55 ++++++++++++++++++------ ast/src/lang/registry/kotlin_registry.rs | 55 ++++++++++++++++++------ ast/src/lang/registry/php_registry.rs | 55 ++++++++++++++++++------ ast/src/lang/registry/swift_registry.rs | 55 ++++++++++++++++++------ 4 files changed, 168 insertions(+), 52 deletions(-) diff --git a/ast/src/lang/registry/csharp_registry.rs b/ast/src/lang/registry/csharp_registry.rs index 1bad25aad..3aa003dab 100644 --- a/ast/src/lang/registry/csharp_registry.rs +++ b/ast/src/lang/registry/csharp_registry.rs @@ -12,6 +12,8 @@ fn parent_dir(file: &str) -> String { } pub struct CSharpRegistry { + var_types: HashMap<(String, String), String>, + methods: HashMap<(String, String), String>, class_fields: HashMap>, method_returns: HashMap<(String, String), String>, dir_fns: HashMap>, @@ -21,26 +23,42 @@ pub struct CSharpRegistry { impl CSharpRegistry { pub fn new(graph: &impl Graph, filez: &[(String, String)]) -> Self { let mut reg = CSharpRegistry { + var_types: HashMap::new(), + methods: HashMap::new(), class_fields: HashMap::new(), method_returns: HashMap::new(), dir_fns: HashMap::new(), resolved: HashMap::new(), }; - // Pass 1: index Function nodes for same-directory bare-name fallback. + // Pass 1: index graph nodes — functions, methods, and typed variables. for (node_type, node_data) in graph.iter_all_nodes() { if !node_data.file.ends_with(".cs") { continue; } - if *node_type != NodeType::Function { - continue; + match node_type { + NodeType::Function => { + let dir = parent_dir(&node_data.file); + reg.dir_fns + .entry(dir) + .or_default() + .entry(node_data.name.clone()) + .or_insert_with(|| NodeKeys::from(node_data)); + if let Some(operand) = node_data.meta.get("operand") { + reg.methods.insert( + (operand.clone(), node_data.name.clone()), + node_data.file.clone(), + ); + } + } + NodeType::Var | NodeType::Instance => { + if let Some(dt) = &node_data.data_type { + reg.var_types + .insert((node_data.file.clone(), node_data.name.clone()), dt.clone()); + } + } + _ => {} } - let dir = parent_dir(&node_data.file); - reg.dir_fns - .entry(dir) - .or_default() - .entry(node_data.name.clone()) - .or_insert_with(|| NodeKeys::from(node_data)); } // Pass 1.5: extract class field types and method return types from source. @@ -85,12 +103,23 @@ impl CSharpRegistry { } impl Registry for CSharpRegistry { - fn resolve_type(&self, _file: &str, _var_name: &str) -> Option<&str> { - None + fn resolve_type(&self, file: &str, var_name: &str) -> Option<&str> { + self.var_types + .get(&(file.to_string(), var_name.to_string())) + .map(|s| s.as_str()) + } + + fn resolve_method(&self, type_name: &str, method_name: &str) -> Option<&str> { + self.methods + .get(&(type_name.to_string(), method_name.to_string())) + .map(|s| s.as_str()) } - fn resolve_method(&self, _type_name: &str, _method_name: &str) -> Option<&str> { - None + fn resolve_field(&self, type_name: &str, field_name: &str) -> Option<&str> { + self.class_fields + .get(type_name)? + .get(field_name) + .map(|s| s.as_str()) } fn resolve_call_at(&self, file: &str, row: usize, col: usize) -> Option { diff --git a/ast/src/lang/registry/kotlin_registry.rs b/ast/src/lang/registry/kotlin_registry.rs index 1a7efcf08..2d6df8f64 100644 --- a/ast/src/lang/registry/kotlin_registry.rs +++ b/ast/src/lang/registry/kotlin_registry.rs @@ -12,6 +12,8 @@ fn parent_dir(file: &str) -> String { } pub struct KotlinRegistry { + var_types: HashMap<(String, String), String>, + methods: HashMap<(String, String), String>, class_fields: HashMap>, method_returns: HashMap<(String, String), String>, fn_returns: HashMap, @@ -22,6 +24,8 @@ pub struct KotlinRegistry { impl KotlinRegistry { pub fn new(graph: &impl Graph, filez: &[(String, String)]) -> Self { let mut reg = KotlinRegistry { + var_types: HashMap::new(), + methods: HashMap::new(), class_fields: HashMap::new(), method_returns: HashMap::new(), fn_returns: HashMap::new(), @@ -29,20 +33,34 @@ impl KotlinRegistry { resolved: HashMap::new(), }; - // Pass 1: index Function nodes for same-directory bare-name fallback. + // Pass 1: index graph nodes — functions, methods, and typed variables. for (node_type, node_data) in graph.iter_all_nodes() { if !node_data.file.ends_with(".kt") { continue; } - if *node_type != NodeType::Function { - continue; + match node_type { + NodeType::Function => { + let dir = parent_dir(&node_data.file); + reg.dir_fns + .entry(dir) + .or_default() + .entry(node_data.name.clone()) + .or_insert_with(|| NodeKeys::from(node_data)); + if let Some(operand) = node_data.meta.get("operand") { + reg.methods.insert( + (operand.clone(), node_data.name.clone()), + node_data.file.clone(), + ); + } + } + NodeType::Var | NodeType::Instance => { + if let Some(dt) = &node_data.data_type { + reg.var_types + .insert((node_data.file.clone(), node_data.name.clone()), dt.clone()); + } + } + _ => {} } - let dir = parent_dir(&node_data.file); - reg.dir_fns - .entry(dir) - .or_default() - .entry(node_data.name.clone()) - .or_insert_with(|| NodeKeys::from(node_data)); } // Pass 1.5: extract class field types and method return types from source. @@ -92,12 +110,23 @@ impl KotlinRegistry { } impl Registry for KotlinRegistry { - fn resolve_type(&self, _file: &str, _var_name: &str) -> Option<&str> { - None + fn resolve_type(&self, file: &str, var_name: &str) -> Option<&str> { + self.var_types + .get(&(file.to_string(), var_name.to_string())) + .map(|s| s.as_str()) + } + + fn resolve_method(&self, type_name: &str, method_name: &str) -> Option<&str> { + self.methods + .get(&(type_name.to_string(), method_name.to_string())) + .map(|s| s.as_str()) } - fn resolve_method(&self, _type_name: &str, _method_name: &str) -> Option<&str> { - None + fn resolve_field(&self, type_name: &str, field_name: &str) -> Option<&str> { + self.class_fields + .get(type_name)? + .get(field_name) + .map(|s| s.as_str()) } fn resolve_call_at(&self, file: &str, row: usize, col: usize) -> Option { diff --git a/ast/src/lang/registry/php_registry.rs b/ast/src/lang/registry/php_registry.rs index a7887e87d..da3c645fd 100644 --- a/ast/src/lang/registry/php_registry.rs +++ b/ast/src/lang/registry/php_registry.rs @@ -12,6 +12,8 @@ fn parent_dir(file: &str) -> String { } pub struct PhpRegistry { + var_types: HashMap<(String, String), String>, + methods: HashMap<(String, String), String>, class_fields: HashMap>, method_returns: HashMap<(String, String), String>, fn_returns: HashMap, @@ -22,6 +24,8 @@ pub struct PhpRegistry { impl PhpRegistry { pub fn new(graph: &impl Graph, filez: &[(String, String)]) -> Self { let mut reg = PhpRegistry { + var_types: HashMap::new(), + methods: HashMap::new(), class_fields: HashMap::new(), method_returns: HashMap::new(), fn_returns: HashMap::new(), @@ -29,20 +33,34 @@ impl PhpRegistry { resolved: HashMap::new(), }; - // Pass 1: index Function nodes by directory for bare-name fallback. + // Pass 1: index graph nodes — functions, methods, and typed variables. for (node_type, node_data) in graph.iter_all_nodes() { if !node_data.file.ends_with(".php") { continue; } - if *node_type != NodeType::Function { - continue; + match node_type { + NodeType::Function => { + let dir = parent_dir(&node_data.file); + reg.dir_fns + .entry(dir) + .or_default() + .entry(node_data.name.clone()) + .or_insert_with(|| NodeKeys::from(node_data)); + if let Some(operand) = node_data.meta.get("operand") { + reg.methods.insert( + (operand.clone(), node_data.name.clone()), + node_data.file.clone(), + ); + } + } + NodeType::Var | NodeType::Instance => { + if let Some(dt) = &node_data.data_type { + reg.var_types + .insert((node_data.file.clone(), node_data.name.clone()), dt.clone()); + } + } + _ => {} } - let dir = parent_dir(&node_data.file); - reg.dir_fns - .entry(dir) - .or_default() - .entry(node_data.name.clone()) - .or_insert_with(|| NodeKeys::from(node_data)); } // Pass 1.5: extract class field types and method return types. @@ -92,12 +110,23 @@ impl PhpRegistry { } impl Registry for PhpRegistry { - fn resolve_type(&self, _file: &str, _var_name: &str) -> Option<&str> { - None + fn resolve_type(&self, file: &str, var_name: &str) -> Option<&str> { + self.var_types + .get(&(file.to_string(), var_name.to_string())) + .map(|s| s.as_str()) + } + + fn resolve_method(&self, type_name: &str, method_name: &str) -> Option<&str> { + self.methods + .get(&(type_name.to_string(), method_name.to_string())) + .map(|s| s.as_str()) } - fn resolve_method(&self, _type_name: &str, _method_name: &str) -> Option<&str> { - None + fn resolve_field(&self, type_name: &str, field_name: &str) -> Option<&str> { + self.class_fields + .get(type_name)? + .get(field_name) + .map(|s| s.as_str()) } fn resolve_call_at(&self, file: &str, row: usize, col: usize) -> Option { diff --git a/ast/src/lang/registry/swift_registry.rs b/ast/src/lang/registry/swift_registry.rs index c9d319939..3dc6b737a 100644 --- a/ast/src/lang/registry/swift_registry.rs +++ b/ast/src/lang/registry/swift_registry.rs @@ -12,6 +12,8 @@ fn parent_dir(file: &str) -> String { } pub struct SwiftRegistry { + var_types: HashMap<(String, String), String>, + methods: HashMap<(String, String), String>, class_fields: HashMap>, method_returns: HashMap<(String, String), String>, fn_returns: HashMap, @@ -22,6 +24,8 @@ pub struct SwiftRegistry { impl SwiftRegistry { pub fn new(graph: &impl Graph, filez: &[(String, String)]) -> Self { let mut reg = SwiftRegistry { + var_types: HashMap::new(), + methods: HashMap::new(), class_fields: HashMap::new(), method_returns: HashMap::new(), fn_returns: HashMap::new(), @@ -29,20 +33,34 @@ impl SwiftRegistry { resolved: HashMap::new(), }; - // Pass 1: index Function nodes for same-directory bare-name fallback. + // Pass 1: index graph nodes — functions, methods, and typed variables. for (node_type, node_data) in graph.iter_all_nodes() { if !node_data.file.ends_with(".swift") { continue; } - if *node_type != NodeType::Function { - continue; + match node_type { + NodeType::Function => { + let dir = parent_dir(&node_data.file); + reg.dir_fns + .entry(dir) + .or_default() + .entry(node_data.name.clone()) + .or_insert_with(|| NodeKeys::from(node_data)); + if let Some(operand) = node_data.meta.get("operand") { + reg.methods.insert( + (operand.clone(), node_data.name.clone()), + node_data.file.clone(), + ); + } + } + NodeType::Var | NodeType::Instance => { + if let Some(dt) = &node_data.data_type { + reg.var_types + .insert((node_data.file.clone(), node_data.name.clone()), dt.clone()); + } + } + _ => {} } - let dir = parent_dir(&node_data.file); - reg.dir_fns - .entry(dir) - .or_default() - .entry(node_data.name.clone()) - .or_insert_with(|| NodeKeys::from(node_data)); } // Pass 1.5: extract class fields and method return types from source. @@ -92,12 +110,23 @@ impl SwiftRegistry { } impl Registry for SwiftRegistry { - fn resolve_type(&self, _file: &str, _var_name: &str) -> Option<&str> { - None + fn resolve_type(&self, file: &str, var_name: &str) -> Option<&str> { + self.var_types + .get(&(file.to_string(), var_name.to_string())) + .map(|s| s.as_str()) + } + + fn resolve_method(&self, type_name: &str, method_name: &str) -> Option<&str> { + self.methods + .get(&(type_name.to_string(), method_name.to_string())) + .map(|s| s.as_str()) } - fn resolve_method(&self, _type_name: &str, _method_name: &str) -> Option<&str> { - None + fn resolve_field(&self, type_name: &str, field_name: &str) -> Option<&str> { + self.class_fields + .get(type_name)? + .get(field_name) + .map(|s| s.as_str()) } fn resolve_call_at(&self, file: &str, row: usize, col: usize) -> Option { From 3f1858af9ed081319208936982ed0c006b1cb9dd Mon Sep 17 00:00:00 2001 From: kelmith Date: Sat, 27 Jun 2026 07:43:05 +0530 Subject: [PATCH 3/3] feat: cpp and c inventory for hybrid lsp --- ast/src/lang/registry/c_registry.rs | 117 +++ ast/src/lang/registry/c_resolver.rs | 473 ++++++++++++ ast/src/lang/registry/cpp_registry.rs | 150 ++++ ast/src/lang/registry/cpp_resolver.rs | 706 ++++++++++++++++++ ast/src/lang/registry/mod.rs | 6 + .../method_chain_factory.c | 26 + ast/src/testing/coverage/c.rs | 8 +- ast/src/testing/coverage/cpp.rs | 8 +- .../cpp/web_api/MethodChainFactory.cpp | 27 + cli/tests/cli/c.rs | 4 +- cli/tests/cli/cpp.rs | 4 +- 11 files changed, 1517 insertions(+), 12 deletions(-) create mode 100644 ast/src/lang/registry/c_registry.rs create mode 100644 ast/src/lang/registry/c_resolver.rs create mode 100644 ast/src/lang/registry/cpp_registry.rs create mode 100644 ast/src/lang/registry/cpp_resolver.rs create mode 100644 ast/src/testing/c/systems-programming/method_chain_factory.c create mode 100644 ast/src/testing/cpp/web_api/MethodChainFactory.cpp diff --git a/ast/src/lang/registry/c_registry.rs b/ast/src/lang/registry/c_registry.rs new file mode 100644 index 000000000..94d8f2837 --- /dev/null +++ b/ast/src/lang/registry/c_registry.rs @@ -0,0 +1,117 @@ +use super::{c_resolver, Registry}; +use crate::lang::asg::NodeKeys; +use crate::lang::graphs::{Graph, NodeType}; +use std::collections::HashMap; +use std::path::Path; + +fn parent_dir(file: &str) -> String { + Path::new(file) + .parent() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default() +} + +pub struct CRegistry { + var_types: HashMap<(String, String), String>, + struct_fields: HashMap>, + fn_returns: HashMap, + dir_fns: HashMap>, + resolved: HashMap<(String, usize, usize), NodeKeys>, +} + +impl CRegistry { + pub fn new(graph: &impl Graph, filez: &[(String, String)]) -> Self { + let mut reg = CRegistry { + var_types: HashMap::new(), + struct_fields: HashMap::new(), + fn_returns: HashMap::new(), + dir_fns: HashMap::new(), + resolved: HashMap::new(), + }; + + for (node_type, node_data) in graph.iter_all_nodes() { + if !(node_data.file.ends_with(".c") || node_data.file.ends_with(".h")) { + continue; + } + match node_type { + NodeType::Function => { + let dir = parent_dir(&node_data.file); + reg.dir_fns + .entry(dir) + .or_default() + .entry(node_data.name.clone()) + .or_insert_with(|| NodeKeys::from(node_data)); + } + NodeType::Var | NodeType::Instance => { + if let Some(dt) = &node_data.data_type { + reg.var_types + .insert((node_data.file.clone(), node_data.name.clone()), dt.clone()); + } + } + _ => {} + } + } + + for (file, source) in filez { + if !(file.ends_with(".c") || file.ends_with(".h")) { + continue; + } + let fields = c_resolver::extract_struct_fields(source); + for (struct_name, field_map) in fields { + reg.struct_fields + .entry(struct_name) + .or_default() + .extend(field_map); + } + let fn_rets = c_resolver::extract_fn_returns(source); + for (key, ret) in fn_rets { + reg.fn_returns.entry(key).or_insert(ret); + } + } + + let all_resolved: Vec<((String, usize, usize), NodeKeys)> = filez + .iter() + .filter(|(f, _)| f.ends_with(".c") || f.ends_with(".h")) + .flat_map(|(file, source)| { + c_resolver::resolve_file_calls( + source, + file, + ®.struct_fields, + ®.fn_returns, + ®.dir_fns, + graph, + ) + .into_iter() + .map(|((row, col), nk)| ((file.clone(), row, col), nk)) + }) + .collect(); + reg.resolved.extend(all_resolved); + + reg + } +} + +impl Registry for CRegistry { + fn resolve_type(&self, file: &str, var_name: &str) -> Option<&str> { + self.var_types + .get(&(file.to_string(), var_name.to_string())) + .map(|s| s.as_str()) + } + + fn resolve_method(&self, _type_name: &str, _method_name: &str) -> Option<&str> { + None + } + + fn resolve_field(&self, type_name: &str, field_name: &str) -> Option<&str> { + self.struct_fields + .get(type_name)? + .get(field_name) + .map(|s| s.as_str()) + } + + fn resolve_call_at(&self, file: &str, row: usize, col: usize) -> Option { + self.resolved + .get(&(file.to_string(), row, col)) + .cloned() + } +} diff --git a/ast/src/lang/registry/c_resolver.rs b/ast/src/lang/registry/c_resolver.rs new file mode 100644 index 000000000..7bf266a5e --- /dev/null +++ b/ast/src/lang/registry/c_resolver.rs @@ -0,0 +1,473 @@ +use crate::lang::asg::NodeKeys; +use crate::lang::graphs::Graph; +use std::collections::HashMap; +use std::path::Path; +use tree_sitter::{Node, Parser}; + +use super::scope::{scope_bind, scope_lookup, scope_pop, scope_push, Scope}; + +fn make_parser() -> Option { + let mut parser = Parser::new(); + let lang: tree_sitter::Language = tree_sitter_c::LANGUAGE.into(); + parser.set_language(&lang).ok()?; + Some(parser) +} + +fn parent_dir(file: &str) -> String { + Path::new(file) + .parent() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default() +} + +fn strip_c_type(node: Node, source: &[u8]) -> Option { + match node.kind() { + "type_identifier" => node.utf8_text(source).ok().map(str::to_string), + + "struct_specifier" => { + node.child_by_field_name("name") + .and_then(|n| n.utf8_text(source).ok().map(str::to_string)) + } + + "primitive_type" | "sized_type_specifier" => None, + + _ => None, + } +} + +// ── Struct field extraction ────────────────────────────────────────────────── + +pub fn extract_struct_fields(source: &str) -> HashMap> { + let mut out: HashMap> = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + walk_for_struct_fields(tree.root_node(), source.as_bytes(), &mut out); + out +} + +fn walk_for_struct_fields( + node: Node, + source: &[u8], + out: &mut HashMap>, +) { + match node.kind() { + "struct_specifier" => { + if let Some(name_node) = node.child_by_field_name("name") { + if let Ok(struct_name) = name_node.utf8_text(source) { + let mut fields = HashMap::new(); + if let Some(body) = node.child_by_field_name("body") { + for i in 0..body.named_child_count() { + let Some(child) = body.named_child(i) else { + continue; + }; + if child.kind() != "field_declaration" { + continue; + } + let type_node = child.child_by_field_name("type"); + let field_type = type_node.and_then(|t| strip_c_type(t, source)); + let Some(ref ft) = field_type else { + continue; + }; + if let Some(declarator) = child.child_by_field_name("declarator") { + if let Some(name) = extract_declarator_name(declarator, source) { + fields.insert(name, ft.clone()); + } + } + } + } + if !fields.is_empty() { + out.entry(struct_name.to_string()) + .or_default() + .extend(fields); + } + } + } + } + "type_definition" => { + // typedef struct { ... } TypeName; + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + if child.kind() == "struct_specifier" { + let mut fields = HashMap::new(); + if let Some(body) = child.child_by_field_name("body") { + for j in 0..body.named_child_count() { + let Some(field_decl) = body.named_child(j) else { + continue; + }; + if field_decl.kind() != "field_declaration" { + continue; + } + let type_node = field_decl.child_by_field_name("type"); + let field_type = type_node.and_then(|t| strip_c_type(t, source)); + let Some(ref ft) = field_type else { + continue; + }; + if let Some(declarator) = + field_decl.child_by_field_name("declarator") + { + if let Some(name) = + extract_declarator_name(declarator, source) + { + fields.insert(name, ft.clone()); + } + } + } + } + if !fields.is_empty() { + // use the typedef name (last type_identifier child of type_definition) + if let Some(typedef_name) = node + .child_by_field_name("declarator") + .and_then(|d| extract_declarator_name(d, source)) + { + out.entry(typedef_name) + .or_default() + .extend(fields.clone()); + } + // also store under struct name if it has one + if let Some(name_node) = child.child_by_field_name("name") { + if let Ok(name) = name_node.utf8_text(source) { + out.entry(name.to_string()) + .or_default() + .extend(fields); + } + } + } + } + } + } + } + _ => { + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + walk_for_struct_fields(child, source, out); + } + } + } + } +} + +fn extract_declarator_name(node: Node, source: &[u8]) -> Option { + match node.kind() { + "field_identifier" | "identifier" | "type_identifier" => { + node.utf8_text(source).ok().map(str::to_string) + } + "pointer_declarator" => node + .child_by_field_name("declarator") + .and_then(|d| extract_declarator_name(d, source)), + _ => None, + } +} + +// ── Function return type extraction ────────────────────────────────────────── + +pub fn extract_fn_returns(source: &str) -> HashMap { + let mut out = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + walk_for_fn_returns(tree.root_node(), source.as_bytes(), &mut out); + out +} + +fn walk_for_fn_returns(node: Node, source: &[u8], out: &mut HashMap) { + match node.kind() { + "function_definition" | "declaration" => { + if let Some(ret_node) = node.child_by_field_name("type") { + if let Some(ret_type) = strip_c_type(ret_node, source) { + if let Some(declarator) = node.child_by_field_name("declarator") { + if let Some(name) = extract_fn_decl_name(declarator, source) { + out.entry(name).or_insert(ret_type); + } + } + } + } + } + _ => { + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + walk_for_fn_returns(child, source, out); + } + } + } + } +} + +fn extract_fn_decl_name(declarator: Node, source: &[u8]) -> Option { + match declarator.kind() { + "function_declarator" => { + let name_node = declarator.child_by_field_name("declarator")?; + extract_fn_decl_name(name_node, source) + } + "pointer_declarator" => { + let inner = declarator.child_by_field_name("declarator")?; + extract_fn_decl_name(inner, source) + } + "identifier" => declarator.utf8_text(source).ok().map(str::to_string), + _ => None, + } +} + +// ── Type evaluator ─────────────────────────────────────────────────────────── + +fn eval_expr_type( + scope: &Scope, + struct_fields: &HashMap>, + fn_returns: &HashMap, + node: Node, + source: &[u8], +) -> Option { + match node.kind() { + "identifier" => { + let name = node.utf8_text(source).ok()?; + scope_lookup(scope, name).map(str::to_string) + } + + "field_expression" => { + let obj = node.child_by_field_name("argument")?; + let obj_type = eval_expr_type(scope, struct_fields, fn_returns, obj, source)?; + let field = node.child_by_field_name("field")?.utf8_text(source).ok()?; + struct_fields.get(obj_type.as_str())?.get(field).cloned() + } + + "call_expression" => { + let func = node.child_by_field_name("function")?; + if func.kind() == "identifier" { + let name = func.utf8_text(source).ok()?; + fn_returns.get(name).cloned() + } else { + None + } + } + + "cast_expression" => { + let type_node = node.child_by_field_name("type")?; + strip_c_type(type_node, source) + } + + _ => None, + } +} + +// ── Parameter binding ──────────────────────────────────────────────────────── + +fn bind_parameters(params: Node, source: &[u8], scope: &mut Scope) { + for i in 0..params.named_child_count() { + let Some(param) = params.named_child(i) else { + continue; + }; + if param.kind() != "parameter_declaration" { + continue; + } + let Some(type_node) = param.child_by_field_name("type") else { + continue; + }; + let Some(type_name) = strip_c_type(type_node, source) else { + continue; + }; + if let Some(decl) = param.child_by_field_name("declarator") { + if let Some(name) = extract_declarator_name(decl, source) { + scope_bind(scope, &name, &type_name); + } + } + } +} + +// ── AST walker ─────────────────────────────────────────────────────────────── + +fn walk_node( + node: Node, + source: &[u8], + scope: &mut Scope, + struct_fields: &HashMap>, + fn_returns: &HashMap, + dir_fns: &HashMap>, + graph: &G, + out: &mut HashMap<(usize, usize), NodeKeys>, + file: &str, +) { + match node.kind() { + "function_definition" => { + scope_push(scope); + if let Some(declarator) = node.child_by_field_name("declarator") { + if declarator.kind() == "function_declarator" { + if let Some(params) = declarator.child_by_field_name("parameters") { + bind_parameters(params, source, scope); + } + } + } + if let Some(body) = node.child_by_field_name("body") { + walk_node( + body, source, scope, struct_fields, fn_returns, dir_fns, graph, out, file, + ); + } + scope_pop(scope); + } + + "declaration" => { + let type_node = node.child_by_field_name("type"); + let explicit_type = type_node.and_then(|t| strip_c_type(t, source)); + + if let Some(declarator) = node.child_by_field_name("declarator") { + bind_and_walk_declarator( + declarator, + &explicit_type, + source, + scope, + struct_fields, + fn_returns, + dir_fns, + graph, + out, + file, + ); + } + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + if child.kind() == "init_declarator" { + bind_and_walk_declarator( + child, + &explicit_type, + source, + scope, + struct_fields, + fn_returns, + dir_fns, + graph, + out, + file, + ); + } + } + } + } + + "call_expression" => { + if let Some(func_node) = node.child_by_field_name("function") { + if func_node.kind() == "identifier" { + if let Ok(fn_name) = func_node.utf8_text(source) { + let dir = parent_dir(file); + let pos = { + let p = func_node.start_position(); + (p.row, p.column) + }; + if let Some(nk) = dir_fns + .get(&dir) + .and_then(|m| m.get(fn_name)) + { + out.entry(pos).or_insert(nk.clone()); + } + } + } + } + if let Some(args) = node.child_by_field_name("arguments") { + walk_node( + args, source, scope, struct_fields, fn_returns, dir_fns, graph, out, file, + ); + } + } + + _ => { + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + walk_node( + child, source, scope, struct_fields, fn_returns, dir_fns, graph, out, file, + ); + } + } + } + } +} + +fn bind_and_walk_declarator( + node: Node, + explicit_type: &Option, + source: &[u8], + scope: &mut Scope, + struct_fields: &HashMap>, + fn_returns: &HashMap, + dir_fns: &HashMap>, + graph: &G, + out: &mut HashMap<(usize, usize), NodeKeys>, + file: &str, +) { + match node.kind() { + "init_declarator" => { + let decl_node = node.child_by_field_name("declarator"); + let value_node = node.child_by_field_name("value"); + + if let Some(decl) = decl_node { + let name = extract_declarator_name(decl, source); + let bound = if let Some(ref t) = explicit_type { + Some(t.clone()) + } else { + value_node.and_then(|v| { + eval_expr_type(scope, struct_fields, fn_returns, v, source) + }) + }; + if let (Some(name), Some(t)) = (name, bound) { + scope_bind(scope, &name, &t); + } + } + + if let Some(value) = value_node { + walk_node( + value, source, scope, struct_fields, fn_returns, dir_fns, graph, out, file, + ); + } + } + "pointer_declarator" => { + if let Some(inner) = node.child_by_field_name("declarator") { + let name = extract_declarator_name(inner, source); + if let (Some(name), Some(ref t)) = (name, explicit_type) { + scope_bind(scope, &name, t); + } + } + } + "identifier" | "field_identifier" => { + if let (Ok(name), Some(ref t)) = (node.utf8_text(source), explicit_type) { + scope_bind(scope, name, t); + } + } + _ => {} + } +} + +// ── Public entry point ─────────────────────────────────────────────────────── + +pub fn resolve_file_calls( + source: &str, + file: &str, + struct_fields: &HashMap>, + fn_returns: &HashMap, + dir_fns: &HashMap>, + graph: &G, +) -> HashMap<(usize, usize), NodeKeys> { + let mut out = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + let mut scope: Scope = vec![HashMap::new()]; + + walk_node( + tree.root_node(), + source.as_bytes(), + &mut scope, + struct_fields, + fn_returns, + dir_fns, + graph, + &mut out, + file, + ); + out +} diff --git a/ast/src/lang/registry/cpp_registry.rs b/ast/src/lang/registry/cpp_registry.rs new file mode 100644 index 000000000..903edaf5d --- /dev/null +++ b/ast/src/lang/registry/cpp_registry.rs @@ -0,0 +1,150 @@ +use super::{cpp_resolver, Registry}; +use crate::lang::asg::NodeKeys; +use crate::lang::graphs::{Graph, NodeType}; +use std::collections::HashMap; +use std::path::Path; + +fn parent_dir(file: &str) -> String { + Path::new(file) + .parent() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default() +} + +pub struct CppRegistry { + var_types: HashMap<(String, String), String>, + methods: HashMap<(String, String), String>, + class_fields: HashMap>, + method_returns: HashMap<(String, String), String>, + fn_returns: HashMap, + dir_fns: HashMap>, + resolved: HashMap<(String, usize, usize), NodeKeys>, +} + +impl CppRegistry { + pub fn new(graph: &impl Graph, filez: &[(String, String)]) -> Self { + let mut reg = CppRegistry { + var_types: HashMap::new(), + methods: HashMap::new(), + class_fields: HashMap::new(), + method_returns: HashMap::new(), + fn_returns: HashMap::new(), + dir_fns: HashMap::new(), + resolved: HashMap::new(), + }; + + for (node_type, node_data) in graph.iter_all_nodes() { + if !(node_data.file.ends_with(".cpp") + || node_data.file.ends_with(".cc") + || node_data.file.ends_with(".cxx") + || node_data.file.ends_with(".h") + || node_data.file.ends_with(".hpp")) + { + continue; + } + match node_type { + NodeType::Function => { + let dir = parent_dir(&node_data.file); + reg.dir_fns + .entry(dir) + .or_default() + .entry(node_data.name.clone()) + .or_insert_with(|| NodeKeys::from(node_data)); + if let Some(operand) = node_data.meta.get("operand") { + reg.methods.insert( + (operand.clone(), node_data.name.clone()), + node_data.file.clone(), + ); + } + } + NodeType::Var | NodeType::Instance => { + if let Some(dt) = &node_data.data_type { + reg.var_types + .insert((node_data.file.clone(), node_data.name.clone()), dt.clone()); + } + } + _ => {} + } + } + + for (file, source) in filez { + if !(file.ends_with(".cpp") + || file.ends_with(".cc") + || file.ends_with(".cxx") + || file.ends_with(".h") + || file.ends_with(".hpp")) + { + continue; + } + let fields = cpp_resolver::extract_class_fields(source); + for (class_name, field_map) in fields { + reg.class_fields + .entry(class_name) + .or_default() + .extend(field_map); + } + let returns = cpp_resolver::extract_method_return_types(source); + for (key, ret) in returns { + reg.method_returns.entry(key).or_insert(ret); + } + let fn_rets = cpp_resolver::extract_fn_returns(source); + for (key, ret) in fn_rets { + reg.fn_returns.entry(key).or_insert(ret); + } + } + + let all_resolved: Vec<((String, usize, usize), NodeKeys)> = filez + .iter() + .filter(|(f, _)| { + f.ends_with(".cpp") + || f.ends_with(".cc") + || f.ends_with(".cxx") + || f.ends_with(".h") + || f.ends_with(".hpp") + }) + .flat_map(|(file, source)| { + cpp_resolver::resolve_file_calls( + source, + file, + ®.class_fields, + ®.method_returns, + ®.fn_returns, + ®.dir_fns, + graph, + ) + .into_iter() + .map(|((row, col), nk)| ((file.clone(), row, col), nk)) + }) + .collect(); + reg.resolved.extend(all_resolved); + + reg + } +} + +impl Registry for CppRegistry { + fn resolve_type(&self, file: &str, var_name: &str) -> Option<&str> { + self.var_types + .get(&(file.to_string(), var_name.to_string())) + .map(|s| s.as_str()) + } + + fn resolve_method(&self, type_name: &str, method_name: &str) -> Option<&str> { + self.methods + .get(&(type_name.to_string(), method_name.to_string())) + .map(|s| s.as_str()) + } + + fn resolve_field(&self, type_name: &str, field_name: &str) -> Option<&str> { + self.class_fields + .get(type_name)? + .get(field_name) + .map(|s| s.as_str()) + } + + fn resolve_call_at(&self, file: &str, row: usize, col: usize) -> Option { + self.resolved + .get(&(file.to_string(), row, col)) + .cloned() + } +} diff --git a/ast/src/lang/registry/cpp_resolver.rs b/ast/src/lang/registry/cpp_resolver.rs new file mode 100644 index 000000000..2ceacb796 --- /dev/null +++ b/ast/src/lang/registry/cpp_resolver.rs @@ -0,0 +1,706 @@ +use crate::lang::asg::NodeKeys; +use crate::lang::graphs::{Graph, NodeType}; +use std::collections::HashMap; +use std::path::Path; +use tree_sitter::{Node, Parser}; + +use super::scope::{scope_bind, scope_lookup, scope_pop, scope_push, Scope}; + +fn make_parser() -> Option { + let mut parser = Parser::new(); + let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into(); + parser.set_language(&lang).ok()?; + Some(parser) +} + +fn parent_dir(file: &str) -> String { + Path::new(file) + .parent() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default() +} + +fn strip_cpp_type(node: Node, source: &[u8]) -> Option { + match node.kind() { + "type_identifier" => node.utf8_text(source).ok().map(str::to_string), + + "qualified_identifier" | "scoped_type_identifier" => { + let name = node.child_by_field_name("name")?; + strip_cpp_type(name, source) + } + + "template_type" => { + let name = node.child_by_field_name("name")?; + let name_text = name.utf8_text(source).ok()?; + if matches!( + name_text, + "unique_ptr" | "shared_ptr" | "optional" | "vector" | "future" + ) { + let args = node.child_by_field_name("arguments")?; + let first = args.named_child(0)?; + strip_cpp_type(first, source) + } else { + Some(name_text.to_string()) + } + } + + "primitive_type" | "sized_type_specifier" | "auto" => None, + + _ => None, + } +} + +// ── Class field extraction ─────────────────────────────────────────────────── + +pub fn extract_class_fields(source: &str) -> HashMap> { + let mut out: HashMap> = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + walk_for_class_fields(tree.root_node(), source.as_bytes(), &mut out); + out +} + +fn walk_for_class_fields( + node: Node, + source: &[u8], + out: &mut HashMap>, +) { + match node.kind() { + "class_specifier" | "struct_specifier" => { + if let Some(name_node) = node.child_by_field_name("name") { + if let Ok(class_name) = name_node.utf8_text(source) { + let mut fields = HashMap::new(); + if let Some(body) = node.child_by_field_name("body") { + extract_fields_from_body(body, source, &mut fields); + for i in 0..body.named_child_count() { + if let Some(child) = body.named_child(i) { + walk_for_class_fields(child, source, out); + } + } + } + if !fields.is_empty() { + out.entry(class_name.to_string()) + .or_default() + .extend(fields); + } + } + } + } + _ => { + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + walk_for_class_fields(child, source, out); + } + } + } + } +} + +fn extract_fields_from_body(body: Node, source: &[u8], fields: &mut HashMap) { + for i in 0..body.named_child_count() { + let Some(child) = body.named_child(i) else { + continue; + }; + if child.kind() != "field_declaration" { + continue; + } + let type_node = child.child_by_field_name("type"); + let field_type = type_node.and_then(|t| strip_cpp_type(t, source)); + let Some(ref ft) = field_type else { + continue; + }; + if let Some(declarator) = child.child_by_field_name("declarator") { + if let Some(name) = extract_declarator_name(declarator, source) { + fields.insert(name, ft.clone()); + } + } + for j in 0..child.named_child_count() { + if let Some(decl) = child.named_child(j) { + if decl.kind() == "field_identifier" || decl.kind() == "identifier" { + if let Ok(name) = decl.utf8_text(source) { + fields.entry(name.to_string()).or_insert(ft.clone()); + } + } + } + } + } +} + +fn extract_declarator_name(node: Node, source: &[u8]) -> Option { + match node.kind() { + "field_identifier" | "identifier" => node.utf8_text(source).ok().map(str::to_string), + "pointer_declarator" | "reference_declarator" => { + node.child_by_field_name("declarator") + .and_then(|d| extract_declarator_name(d, source)) + } + _ => None, + } +} + +// ── Method return type extraction ──────────────────────────────────────────── + +pub fn extract_method_return_types(source: &str) -> HashMap<(String, String), String> { + let mut out = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + walk_for_method_returns(tree.root_node(), source.as_bytes(), None, &mut out); + out +} + +fn walk_for_method_returns( + node: Node, + source: &[u8], + current_class: Option<&str>, + out: &mut HashMap<(String, String), String>, +) { + match node.kind() { + "class_specifier" | "struct_specifier" => { + if let Some(name_node) = node.child_by_field_name("name") { + if let Ok(class_name) = name_node.utf8_text(source) { + if let Some(body) = node.child_by_field_name("body") { + for i in 0..body.named_child_count() { + if let Some(child) = body.named_child(i) { + walk_for_method_returns(child, source, Some(class_name), out); + } + } + } + } + } + } + "function_definition" | "declaration" => { + if let Some(ret_node) = node.child_by_field_name("type") { + if let Some(ret_type) = strip_cpp_type(ret_node, source) { + if let Some(declarator) = node.child_by_field_name("declarator") { + if let Some((class, method)) = + extract_qualified_method_name(declarator, source) + { + out.entry((class, method)).or_insert(ret_type.clone()); + } else if let Some(class_name) = current_class { + if let Some(name) = extract_simple_fn_name(declarator, source) { + out.entry((class_name.to_string(), name)) + .or_insert(ret_type); + } + } + } + } + } + } + _ => { + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + walk_for_method_returns(child, source, current_class, out); + } + } + } + } +} + +fn extract_qualified_method_name(declarator: Node, source: &[u8]) -> Option<(String, String)> { + match declarator.kind() { + "function_declarator" => { + let name_node = declarator.child_by_field_name("declarator")?; + extract_qualified_method_name(name_node, source) + } + "qualified_identifier" => { + let scope_node = declarator.child_by_field_name("scope")?; + let name_node = declarator.child_by_field_name("name")?; + let class = scope_node.utf8_text(source).ok()?; + let method = name_node.utf8_text(source).ok()?; + if method.starts_with('~') { + return None; + } + Some((class.to_string(), method.to_string())) + } + _ => None, + } +} + +fn extract_simple_fn_name(declarator: Node, source: &[u8]) -> Option { + match declarator.kind() { + "function_declarator" => { + let name_node = declarator.child_by_field_name("declarator")?; + extract_simple_fn_name(name_node, source) + } + "identifier" | "field_identifier" => declarator.utf8_text(source).ok().map(str::to_string), + _ => None, + } +} + +// ── Free function return type extraction ───────────────────────────────────── + +pub fn extract_fn_returns(source: &str) -> HashMap { + let mut out = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + walk_for_fn_returns(tree.root_node(), source.as_bytes(), false, &mut out); + out +} + +fn walk_for_fn_returns(node: Node, source: &[u8], in_class: bool, out: &mut HashMap) { + match node.kind() { + "class_specifier" | "struct_specifier" => { + if let Some(body) = node.child_by_field_name("body") { + for i in 0..body.named_child_count() { + if let Some(child) = body.named_child(i) { + walk_for_fn_returns(child, source, true, out); + } + } + } + } + "function_definition" | "declaration" if !in_class => { + if let Some(ret_node) = node.child_by_field_name("type") { + if let Some(ret_type) = strip_cpp_type(ret_node, source) { + if let Some(declarator) = node.child_by_field_name("declarator") { + if extract_qualified_method_name(declarator, source).is_none() { + if let Some(name) = extract_simple_fn_name(declarator, source) { + out.entry(name).or_insert(ret_type); + } + } + } + } + } + } + _ => { + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + walk_for_fn_returns(child, source, in_class, out); + } + } + } + } +} + +// ── Method lookup ──────────────────────────────────────────────────────────── + +fn find_method_in_class( + graph: &G, + class_name: &str, + method_name: &str, +) -> Option { + if let Some(n) = graph + .find_nodes_by_name(NodeType::Function, method_name) + .into_iter() + .find(|n| n.meta.get("operand").map(|s| s.as_str()) == Some(class_name)) + { + return Some(NodeKeys::from(&n)); + } + None +} + +// ── Type evaluator ─────────────────────────────────────────────────────────── + +fn eval_expr_type( + scope: &Scope, + class_fields: &HashMap>, + method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, + node: Node, + source: &[u8], +) -> Option { + match node.kind() { + "identifier" => { + let name = node.utf8_text(source).ok()?; + scope_lookup(scope, name).map(str::to_string).or_else(|| { + if name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) { + Some(name.to_string()) + } else { + None + } + }) + } + + "this" => scope_lookup(scope, "this").map(str::to_string), + + "field_expression" => { + let obj = node.child_by_field_name("argument")?; + let obj_type = + eval_expr_type(scope, class_fields, method_returns, fn_returns, obj, source)?; + let field = node.child_by_field_name("field")?.utf8_text(source).ok()?; + class_fields + .get(obj_type.as_str())? + .get(field) + .cloned() + } + + "new_expression" => { + let type_node = node.child_by_field_name("type")?; + strip_cpp_type(type_node, source) + } + + "call_expression" => { + let func = node.child_by_field_name("function")?; + if func.kind() == "field_expression" { + let obj = func.child_by_field_name("argument")?; + let obj_type = eval_expr_type( + scope, + class_fields, + method_returns, + fn_returns, + obj, + source, + )?; + let method = func.child_by_field_name("field")?.utf8_text(source).ok()?; + method_returns + .get(&(obj_type, method.to_string())) + .cloned() + } else if func.kind() == "identifier" { + let name = func.utf8_text(source).ok()?; + fn_returns.get(name).cloned() + } else if func.kind() == "qualified_identifier" { + let name = func.child_by_field_name("name")?; + let name_text = name.utf8_text(source).ok()?; + let scope_node = func.child_by_field_name("scope")?; + let class_text = scope_node.utf8_text(source).ok()?; + method_returns + .get(&(class_text.to_string(), name_text.to_string())) + .cloned() + } else { + None + } + } + + _ => None, + } +} + +// ── Parameter binding ──────────────────────────────────────────────────────── + +fn bind_parameters(params: Node, source: &[u8], scope: &mut Scope) { + for i in 0..params.named_child_count() { + let Some(param) = params.named_child(i) else { + continue; + }; + if param.kind() != "parameter_declaration" { + continue; + } + let Some(type_node) = param.child_by_field_name("type") else { + continue; + }; + let Some(type_name) = strip_cpp_type(type_node, source) else { + continue; + }; + if let Some(decl) = param.child_by_field_name("declarator") { + if let Some(name) = extract_declarator_name(decl, source) { + scope_bind(scope, &name, &type_name); + } + } + } +} + +// ── AST walker ─────────────────────────────────────────────────────────────── + +fn walk_node( + node: Node, + source: &[u8], + scope: &mut Scope, + class_fields: &HashMap>, + method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, + dir_fns: &HashMap>, + graph: &G, + out: &mut HashMap<(usize, usize), NodeKeys>, + file: &str, +) { + match node.kind() { + "class_specifier" | "struct_specifier" => { + let class_name = node + .child_by_field_name("name") + .and_then(|n| n.utf8_text(source).ok().map(str::to_string)); + + scope_push(scope); + if let Some(ref name) = class_name { + scope_bind(scope, "this", name); + if let Some(fields) = class_fields.get(name.as_str()) { + for (field_name, field_type) in fields { + scope_bind(scope, field_name, field_type); + } + } + } + if let Some(body) = node.child_by_field_name("body") { + for i in 0..body.named_child_count() { + if let Some(child) = body.named_child(i) { + walk_node( + child, + source, + scope, + class_fields, + method_returns, + fn_returns, + dir_fns, + graph, + out, + file, + ); + } + } + } + scope_pop(scope); + } + + "function_definition" => { + scope_push(scope); + if let Some(declarator) = node.child_by_field_name("declarator") { + if declarator.kind() == "function_declarator" { + if let Some(params) = declarator.child_by_field_name("parameters") { + bind_parameters(params, source, scope); + } + } + } + if let Some(body) = node.child_by_field_name("body") { + walk_node( + body, source, scope, class_fields, method_returns, fn_returns, dir_fns, graph, + out, file, + ); + } + scope_pop(scope); + } + + "declaration" => { + let type_node = node.child_by_field_name("type"); + let explicit_type = type_node.and_then(|t| strip_cpp_type(t, source)); + + if let Some(declarator) = node.child_by_field_name("declarator") { + bind_and_walk_declarator( + declarator, + &explicit_type, + source, + scope, + class_fields, + method_returns, + fn_returns, + dir_fns, + graph, + out, + file, + ); + } + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + if child.kind() == "init_declarator" { + bind_and_walk_declarator( + child, + &explicit_type, + source, + scope, + class_fields, + method_returns, + fn_returns, + dir_fns, + graph, + out, + file, + ); + } + } + } + } + + "call_expression" => { + if let Some(func_node) = node.child_by_field_name("function") { + let resolved = if func_node.kind() == "field_expression" { + let obj_node = func_node.child_by_field_name("argument"); + let name_node = func_node.child_by_field_name("field"); + if let (Some(obj_node), Some(name_node)) = (obj_node, name_node) { + let obj_type = eval_expr_type( + scope, + class_fields, + method_returns, + fn_returns, + obj_node, + source, + ); + let method_name = name_node.utf8_text(source).ok(); + if let (Some(obj_type), Some(method_name)) = (obj_type, method_name) { + let pos = { + let p = name_node.start_position(); + (p.row, p.column) + }; + find_method_in_class(graph, &obj_type, method_name) + .map(|t| (pos, t)) + } else { + None + } + } else { + None + } + } else if func_node.kind() == "identifier" { + if let Ok(fn_name) = func_node.utf8_text(source) { + let dir = parent_dir(file); + let pos = { + let p = func_node.start_position(); + (p.row, p.column) + }; + dir_fns + .get(&dir) + .and_then(|m| m.get(fn_name)) + .map(|nk| (pos, nk.clone())) + } else { + None + } + } else if func_node.kind() == "qualified_identifier" { + let name_node = func_node.child_by_field_name("name"); + let scope_node = func_node.child_by_field_name("scope"); + if let (Some(scope_node), Some(name_node)) = (scope_node, name_node) { + let class = scope_node.utf8_text(source).ok(); + let method = name_node.utf8_text(source).ok(); + if let (Some(class), Some(method)) = (class, method) { + let pos = { + let p = name_node.start_position(); + (p.row, p.column) + }; + find_method_in_class(graph, class, method).map(|t| (pos, t)) + } else { + None + } + } else { + None + } + } else { + None + }; + + if let Some((pos, target)) = resolved { + out.entry(pos).or_insert(target); + } + } + if let Some(args) = node.child_by_field_name("arguments") { + walk_node( + args, source, scope, class_fields, method_returns, fn_returns, dir_fns, graph, + out, file, + ); + } + } + + _ => { + recurse( + node, source, scope, class_fields, method_returns, fn_returns, dir_fns, graph, out, + file, + ); + } + } +} + +fn bind_and_walk_declarator( + node: Node, + explicit_type: &Option, + source: &[u8], + scope: &mut Scope, + class_fields: &HashMap>, + method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, + dir_fns: &HashMap>, + graph: &G, + out: &mut HashMap<(usize, usize), NodeKeys>, + file: &str, +) { + match node.kind() { + "init_declarator" => { + let decl_node = node.child_by_field_name("declarator"); + let value_node = node.child_by_field_name("value"); + + if let Some(decl) = decl_node { + let name = extract_declarator_name(decl, source); + let bound = if let Some(ref t) = explicit_type { + Some(t.clone()) + } else { + value_node.and_then(|v| { + eval_expr_type(scope, class_fields, method_returns, fn_returns, v, source) + }) + }; + if let (Some(name), Some(t)) = (name, bound) { + scope_bind(scope, &name, &t); + } + } + + if let Some(value) = value_node { + walk_node( + value, source, scope, class_fields, method_returns, fn_returns, dir_fns, graph, + out, file, + ); + } + } + "pointer_declarator" | "reference_declarator" => { + if let Some(inner) = node.child_by_field_name("declarator") { + let name = extract_declarator_name(inner, source); + if let (Some(name), Some(ref t)) = (name, explicit_type) { + scope_bind(scope, &name, t); + } + } + } + "identifier" | "field_identifier" => { + if let (Ok(name), Some(ref t)) = (node.utf8_text(source), explicit_type) { + scope_bind(scope, name, t); + } + } + _ => {} + } +} + +fn recurse( + node: Node, + source: &[u8], + scope: &mut Scope, + class_fields: &HashMap>, + method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, + dir_fns: &HashMap>, + graph: &G, + out: &mut HashMap<(usize, usize), NodeKeys>, + file: &str, +) { + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + walk_node( + child, source, scope, class_fields, method_returns, fn_returns, dir_fns, graph, + out, file, + ); + } + } +} + +// ── Public entry point ─────────────────────────────────────────────────────── + +pub fn resolve_file_calls( + source: &str, + file: &str, + class_fields: &HashMap>, + method_returns: &HashMap<(String, String), String>, + fn_returns: &HashMap, + dir_fns: &HashMap>, + graph: &G, +) -> HashMap<(usize, usize), NodeKeys> { + let mut out = HashMap::new(); + let Some(mut parser) = make_parser() else { + return out; + }; + let Some(tree) = parser.parse(source, None) else { + return out; + }; + let mut scope: Scope = vec![HashMap::new()]; + + walk_node( + tree.root_node(), + source.as_bytes(), + &mut scope, + class_fields, + method_returns, + fn_returns, + dir_fns, + graph, + &mut out, + file, + ); + out +} diff --git a/ast/src/lang/registry/mod.rs b/ast/src/lang/registry/mod.rs index 56bf82a43..06d09395d 100644 --- a/ast/src/lang/registry/mod.rs +++ b/ast/src/lang/registry/mod.rs @@ -1,3 +1,7 @@ +pub mod c_registry; +pub mod c_resolver; +pub mod cpp_registry; +pub mod cpp_resolver; pub mod cs_resolver; pub mod csharp_registry; pub mod scope; @@ -49,6 +53,8 @@ pub fn build( Language::Kotlin => Some(Box::new(kotlin_registry::KotlinRegistry::new(graph, filez))), Language::Swift => Some(Box::new(swift_registry::SwiftRegistry::new(graph, filez))), Language::Php => Some(Box::new(php_registry::PhpRegistry::new(graph, filez))), + Language::C => Some(Box::new(c_registry::CRegistry::new(graph, filez))), + Language::Cpp => Some(Box::new(cpp_registry::CppRegistry::new(graph, filez))), _ => None, } } diff --git a/ast/src/testing/c/systems-programming/method_chain_factory.c b/ast/src/testing/c/systems-programming/method_chain_factory.c new file mode 100644 index 000000000..242451c1a --- /dev/null +++ b/ast/src/testing/c/systems-programming/method_chain_factory.c @@ -0,0 +1,26 @@ +// Tests free-function call resolution via the C registry. +// +// create_pool() and use_pool() are free functions. The resolver +// seeds dir_fns so that the call to create_pool() inside use_pool() +// resolves via directory-scoped function lookup. +// +// @ast node: Class "SimplePool" +// @ast node: Function "create_pool" +// @ast node: Function "use_pool" +// @ast edge: Calls -> Function "create_pool" "method_chain_factory.c" +#include + +typedef struct { + int size; +} SimplePool; + +SimplePool* create_pool(int size) { + SimplePool* p = (SimplePool*)malloc(sizeof(SimplePool)); + p->size = size; + return p; +} + +void use_pool() { + SimplePool* pool = create_pool(64); + pool->size = 128; +} diff --git a/ast/src/testing/coverage/c.rs b/ast/src/testing/coverage/c.rs index 22edb6526..31eb8bf48 100644 --- a/ast/src/testing/coverage/c.rs +++ b/ast/src/testing/coverage/c.rs @@ -71,7 +71,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(endpoints.len(), 4); let functions = graph.find_nodes_by_type(NodeType::Function); - assert_eq!(functions.len(), 46); + assert_eq!(functions.len(), 48); let unit_tests = graph.find_nodes_by_type(NodeType::UnitTest); assert_eq!(unit_tests.len(), 21); @@ -83,7 +83,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(e2e_tests.len(), 0); let classes = graph.find_nodes_by_type(NodeType::Class); - assert_eq!(classes.len(), 23); + assert_eq!(classes.len(), 24); let data_models = graph.find_nodes_by_type(NodeType::DataModel); assert_eq!(data_models.len(), 5); @@ -108,10 +108,10 @@ async fn test_btreemap_test_to_function_edges() -> Result<()> { let graph = repo.build_graph_inner::().await?; let calls_edges = graph.count_edges_of_type(EdgeType::Calls); - assert_eq!(calls_edges, 61); + assert_eq!(calls_edges, 64); let contains_edges = graph.count_edges_of_type(EdgeType::Contains); - assert_eq!(contains_edges, 221); + assert_eq!(contains_edges, 226); let handler_edges = graph.count_edges_of_type(EdgeType::Handler); assert_eq!(handler_edges, 4); diff --git a/ast/src/testing/coverage/cpp.rs b/ast/src/testing/coverage/cpp.rs index edd006181..029be0a16 100644 --- a/ast/src/testing/coverage/cpp.rs +++ b/ast/src/testing/coverage/cpp.rs @@ -71,7 +71,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(endpoints.len(), 3); let functions = graph.find_nodes_by_type(NodeType::Function); - assert_eq!(functions.len(), 110); + assert_eq!(functions.len(), 113); let unit_tests = graph.find_nodes_by_type(NodeType::UnitTest); assert_eq!(unit_tests.len(), 8); @@ -83,7 +83,7 @@ async fn test_btreemap_graph_structure() -> Result<()> { assert_eq!(e2e_tests.len(), 0); let classes = graph.find_nodes_by_type(NodeType::Class); - assert_eq!(classes.len(), 1); + assert_eq!(classes.len(), 2); let data_models = graph.find_nodes_by_type(NodeType::DataModel); assert_eq!(data_models.len(), 1); @@ -108,10 +108,10 @@ async fn test_btreemap_test_to_function_edges() -> Result<()> { let graph = repo.build_graph_inner::().await?; let calls_edges = graph.count_edges_of_type(EdgeType::Calls); - assert_eq!(calls_edges, 13); + assert_eq!(calls_edges, 15); let contains_edges = graph.count_edges_of_type(EdgeType::Contains); - assert_eq!(contains_edges, 226); + assert_eq!(contains_edges, 232); let handler_edges = graph.count_edges_of_type(EdgeType::Handler); assert_eq!(handler_edges, 3); diff --git a/ast/src/testing/cpp/web_api/MethodChainFactory.cpp b/ast/src/testing/cpp/web_api/MethodChainFactory.cpp new file mode 100644 index 000000000..439ee1c4d --- /dev/null +++ b/ast/src/testing/cpp/web_api/MethodChainFactory.cpp @@ -0,0 +1,27 @@ +// Tests method chain resolution via the C++ registry. +// +// makeWidget() is a free function returning Widget. The resolver +// seeds fn_returns so that `Widget w = makeWidget()` binds w → Widget, +// enabling w.render() to resolve to Widget::render via +// find_method_in_class. +// +// @ast node: Class "Widget" +// @ast node: Function "render" +// @ast node: Function "makeWidget" +// @ast node: Function "factoryPipeline" +// @ast edge: Calls -> Function "makeWidget" "MethodChainFactory.cpp" +// @ast edge: Calls -> Function "render" "MethodChainFactory.cpp" + +class Widget { +public: + void render() {} +}; + +Widget makeWidget() { + return Widget(); +} + +void factoryPipeline() { + Widget w = makeWidget(); + w.render(); +} diff --git a/cli/tests/cli/c.rs b/cli/tests/cli/c.rs index 71c62afcc..0a451c7a8 100644 --- a/cli/tests/cli/c.rs +++ b/cli/tests/cli/c.rs @@ -43,9 +43,9 @@ fn parse_stats_c_dir() { let out = run_stakgraph(&["--stats", &dir]); assert_eq!(out.exit_code, 0, "stderr: {}", out.stderr); - assert!(out.stdout.contains("Class 23"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Class 24"), "stdout: {}", out.stdout); assert!(out.stdout.contains("Endpoint 4"), "stdout: {}", out.stdout); - assert!(out.stdout.contains("Function 46"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Function 48"), "stdout: {}", out.stdout); } // ── search ──────────────────────────────────────────────────────────────────── diff --git a/cli/tests/cli/cpp.rs b/cli/tests/cli/cpp.rs index 165f239fd..54a30418f 100644 --- a/cli/tests/cli/cpp.rs +++ b/cli/tests/cli/cpp.rs @@ -38,8 +38,8 @@ fn parse_stats_cpp_dir() { assert_eq!(out.exit_code, 0, "stderr: {}", out.stderr); assert!(out.stdout.contains("Endpoint 3"), "stdout: {}", out.stdout); - assert!(out.stdout.contains("Function 110"), "stdout: {}", out.stdout); - assert!(out.stdout.contains("Class 1"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Function 113"), "stdout: {}", out.stdout); + assert!(out.stdout.contains("Class 2"), "stdout: {}", out.stdout); } // ── search ────────────────────────────────────────────────────────────────────