diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 6ad4a1104..7a89b7e0b 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -49,7 +49,7 @@ jobs: - name: Run Automated AI Review id: automated-review - uses: anthropics/claude-code-action@51705da45eecce209d4700538bf8377d5b5fc695 + uses: anthropics/claude-code-action@d5726de019ec4498aa667642bc3a80fca83aa102 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} model: claude-sonnet-4-6 @@ -208,7 +208,7 @@ jobs: - name: Run Interactive AI Assistant id: interactive-claude - uses: anthropics/claude-code-action@51705da45eecce209d4700538bf8377d5b5fc695 + uses: anthropics/claude-code-action@d5726de019ec4498aa667642bc3a80fca83aa102 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} model: claude-sonnet-4-6 diff --git a/crates/codegraph-core/src/domain/graph/builder/stages/collect_files.rs b/crates/codegraph-core/src/domain/graph/builder/stages/collect_files.rs index cf127d92d..59a9e561d 100644 --- a/crates/codegraph-core/src/domain/graph/builder/stages/collect_files.rs +++ b/crates/codegraph-core/src/domain/graph/builder/stages/collect_files.rs @@ -28,6 +28,9 @@ const DEFAULT_IGNORE_DIRS: &[&str] = &[ "venv", "env", ".env", + // Rust workspace convention — contains only Rust source and NAPI-RS generated + // binding artifacts (index.js / index.d.ts) that produce false complexity readings. + "crates", ]; /// All supported file extensions (mirrors the JS `EXTENSIONS` set). diff --git a/crates/codegraph-core/src/extractors/c.rs b/crates/codegraph-core/src/extractors/c.rs index 08a41d47f..0fc5310b0 100644 --- a/crates/codegraph-core/src/extractors/c.rs +++ b/crates/codegraph-core/src/extractors/c.rs @@ -294,62 +294,15 @@ fn match_c_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: u "call_expression" => { if let Some(fn_node) = node.child_by_field_name("function") { - let call_line = start_line(node); match fn_node.kind() { "identifier" => { - let fn_name = node_text(&fn_node, source); - // dlsym(handle, "symbol") — dynamic symbol loading - if fn_name == "dlsym" || fn_name == "dlvsym" { - // Get second arg (index 1) — the symbol name - let args = node.child_by_field_name("arguments") - .or_else(|| find_child(node, "argument_list")); - let mut arg_idx = 0usize; - let mut second_arg: Option = None; - if let Some(args) = args { - for i in 0..args.child_count() { - if let Some(child) = args.child(i) { - match child.kind() { - "(" | ")" | "," => continue, - "string_literal" | "string_content" if arg_idx == 1 => { - second_arg = Some(node_text(&child, source) - .replace(&['"', '\''][..], "")); - break; - } - _ => { arg_idx += 1; } - } - } - } - } - match second_arg { - Some(sym) if !sym.is_empty() => { - symbols.calls.push(Call { - name: sym.clone(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("reflection".to_string()), - key_expr: Some(sym), - ..Default::default() - }); - } - _ => { - symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("unresolved-dynamic".to_string()), - ..Default::default() - }); - } - } - } else { - symbols.calls.push(Call { - name: fn_name.to_string(), - line: call_line, - dynamic: None, - receiver: None, - ..Default::default() - }); - } + symbols.calls.push(Call { + name: node_text(&fn_node, source).to_string(), + line: start_line(node), + dynamic: None, + receiver: None, + ..Default::default() + }); } "field_expression" => { let name = named_child_text(&fn_node, "field", source) @@ -359,34 +312,21 @@ fn match_c_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: u .map(|s| s.to_string()); symbols.calls.push(Call { name, - line: call_line, + line: start_line(node), dynamic: None, receiver, ..Default::default() }); } - // (*fp)(args) — function pointer call; unresolvable - "parenthesized_expression" | "pointer_expression" => { + _ => { symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("unresolved-dynamic".to_string()), + name: node_text(&fn_node, source).to_string(), + line: start_line(node), + dynamic: None, + receiver: None, ..Default::default() }); } - _ => { - let name = node_text(&fn_node, source); - if !name.is_empty() { - symbols.calls.push(Call { - name: name.to_string(), - line: call_line, - dynamic: None, - receiver: None, - ..Default::default() - }); - } - } } } } diff --git a/crates/codegraph-core/src/extractors/cpp.rs b/crates/codegraph-core/src/extractors/cpp.rs index c3534d789..cc693c807 100644 --- a/crates/codegraph-core/src/extractors/cpp.rs +++ b/crates/codegraph-core/src/extractors/cpp.rs @@ -323,57 +323,9 @@ fn handle_cpp_preproc_include(node: &Node, source: &[u8], symbols: &mut FileSymb fn handle_cpp_call_expression(node: &Node, source: &[u8], symbols: &mut FileSymbols) { if let Some(fn_node) = node.child_by_field_name("function") { - let call_line = start_line(node); match fn_node.kind() { "identifier" | "qualified_identifier" | "scoped_identifier" => { - let fn_name = node_text(&fn_node, source); - // dlsym(handle, "symbol") — dynamic symbol loading via C ABI. - // String-literal second arg: resolves as reflection (C symbols are unmangled, name-match works). - // Variable second arg: flagged as unresolved-dynamic. - if fn_name == "dlsym" || fn_name == "dlvsym" { - let args = node.child_by_field_name("arguments") - .or_else(|| find_child(node, "argument_list")); - let mut arg_idx = 0usize; - let mut second_arg: Option = None; - if let Some(args) = args { - for i in 0..args.child_count() { - if let Some(child) = args.child(i) { - match child.kind() { - "(" | ")" | "," => continue, - "string_literal" | "string_content" if arg_idx == 1 => { - second_arg = Some(node_text(&child, source) - .replace(&['"', '\''][..], "")); - break; - } - _ => { arg_idx += 1; } - } - } - } - } - match second_arg { - Some(sym) if !sym.is_empty() => { - symbols.calls.push(Call { - name: sym.clone(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("reflection".to_string()), - key_expr: Some(sym), - ..Default::default() - }); - } - _ => { - symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("unresolved-dynamic".to_string()), - ..Default::default() - }); - } - } - } else { - push_simple_call(symbols, node, fn_name.to_string()); - } + push_simple_call(symbols, node, node_text(&fn_node, source).to_string()); } "field_expression" => { let name = named_child_text(&fn_node, "field", source) @@ -383,16 +335,6 @@ fn handle_cpp_call_expression(node: &Node, source: &[u8], symbols: &mut FileSymb .map(|s| s.to_string()); push_call(symbols, node, name, receiver, None); } - // (*fp)(args) — function pointer call through dereference; unresolvable statically - "parenthesized_expression" | "pointer_expression" => { - symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("unresolved-dynamic".to_string()), - ..Default::default() - }); - } _ => { push_simple_call(symbols, node, node_text(&fn_node, source).to_string()); } diff --git a/crates/codegraph-core/src/extractors/go.rs b/crates/codegraph-core/src/extractors/go.rs index 2235d32e6..794ba200e 100644 --- a/crates/codegraph-core/src/extractors/go.rs +++ b/crates/codegraph-core/src/extractors/go.rs @@ -195,34 +195,13 @@ fn handle_import_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { } } -/// Get the Nth non-punctuation argument from a Go call_expression. -fn get_go_arg(node: &Node, index: usize, source: &[u8]) -> Option<(String, String)> { - let args = node.child_by_field_name("arguments")?; - let mut count = 0usize; - for i in 0..args.child_count() { - let child = args.child(i)?; - match child.kind() { - "(" | ")" | "," => continue, - kind => { - if count == index { - return Some((node_text(&child, source).to_string(), kind.to_string())); - } - count += 1; - } - } - } - None -} - fn handle_call_expr(node: &Node, source: &[u8], symbols: &mut FileSymbols) { let Some(fn_node) = node.child_by_field_name("function") else { return }; - let call_line = start_line(node); - match fn_node.kind() { "identifier" => { symbols.calls.push(Call { name: node_text(&fn_node, source).to_string(), - line: call_line, + line: start_line(node), dynamic: None, receiver: None, ..Default::default() @@ -230,48 +209,11 @@ fn handle_call_expr(node: &Node, source: &[u8], symbols: &mut FileSymbols) { } "selector_expression" => { if let Some(field) = fn_node.child_by_field_name("field") { - let field_name = node_text(&field, source); let receiver = named_child_text(&fn_node, "operand", source) .map(|s| s.to_string()); - - // v.MethodByName("name") — reflect-based dynamic dispatch - if field_name == "MethodByName" { - if let Some((arg_text, arg_kind)) = get_go_arg(node, 0, source) { - match arg_kind.as_str() { - "interpreted_string_literal" | "raw_string_literal" => { - let method = arg_text.replace(&['"', '`'][..], ""); - if !method.is_empty() { - symbols.calls.push(Call { - name: method, - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("reflection".to_string()), - key_expr: Some(arg_text), - receiver, - ..Default::default() - }); - return; - } - } - _ => { - symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("computed-key".to_string()), - key_expr: Some(arg_text), - receiver, - ..Default::default() - }); - return; - } - } - } - } - symbols.calls.push(Call { - name: field_name.to_string(), - line: call_line, + name: node_text(&field, source).to_string(), + line: start_line(node), dynamic: None, receiver, ..Default::default() diff --git a/crates/codegraph-core/src/extractors/php.rs b/crates/codegraph-core/src/extractors/php.rs index 078e34ddd..1feb71818 100644 --- a/crates/codegraph-core/src/extractors/php.rs +++ b/crates/codegraph-core/src/extractors/php.rs @@ -228,88 +228,14 @@ fn handle_namespace_use(node: &Node, source: &[u8], symbols: &mut FileSymbols) { } } -/// Get the first argument expression from a PHP argument_list. -fn get_first_php_arg_text(node: &Node, source: &[u8]) -> Option<(String, String)> { - // Returns (text, kind) of the first non-punctuation argument - let args = node.child_by_field_name("arguments") - .or_else(|| find_child(node, "argument_list"))?; - for i in 0..args.child_count() { - let child = args.child(i)?; - match child.kind() { - "(" | ")" | "," => continue, - kind => { - // Descend into 'argument' wrapper if present - let actual = if kind == "argument" { - child.child(0).unwrap_or(child) - } else { - child - }; - return Some((node_text(&actual, source).to_string(), actual.kind().to_string())); - } - } - } - None -} - fn handle_function_call(node: &Node, source: &[u8], symbols: &mut FileSymbols) { let fn_node = node.child_by_field_name("function").or_else(|| node.child(0)); let Some(fn_node) = fn_node else { return }; - let call_line = start_line(node); - match fn_node.kind() { - // $fn() — variable callable; unresolvable - "variable_name" => { - symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("unresolved-dynamic".to_string()), - ..Default::default() - }); - } "name" | "identifier" => { - let fn_name = node_text(&fn_node, source); - // call_user_func($callable, args) — extract first arg as actual target - if fn_name == "call_user_func" || fn_name == "call_user_func_array" { - if let Some((arg_text, arg_kind)) = get_first_php_arg_text(node, source) { - match arg_kind.as_str() { - "variable_name" => { - symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("unresolved-dynamic".to_string()), - key_expr: Some(arg_text), - ..Default::default() - }); - } - "string" | "encapsed_string" => { - let literal = arg_text.replace(['\'', '"'], ""); - symbols.calls.push(Call { - name: literal, - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("reflection".to_string()), - key_expr: Some(arg_text), - ..Default::default() - }); - } - _ => { - symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("unresolved-dynamic".to_string()), - ..Default::default() - }); - } - } - return; - } - } symbols.calls.push(Call { - name: fn_name.to_string(), - line: call_line, + name: node_text(&fn_node, source).to_string(), + line: start_line(node), dynamic: None, receiver: None, ..Default::default() @@ -320,7 +246,7 @@ fn handle_function_call(node: &Node, source: &[u8], symbols: &mut FileSymbols) { let last = text.split('\\').last().unwrap_or(""); symbols.calls.push(Call { name: last.to_string(), - line: call_line, + line: start_line(node), dynamic: None, receiver: None, ..Default::default() @@ -332,26 +258,11 @@ fn handle_function_call(node: &Node, source: &[u8], symbols: &mut FileSymbols) { fn handle_member_call(node: &Node, source: &[u8], symbols: &mut FileSymbols) { if let Some(name) = node.child_by_field_name("name") { - let receiver = named_child_text(node, "object", source).map(|s| s.to_string()); - let call_line = start_line(node); - - // $obj->$m() — variable method name; unresolvable statically - if name.kind() == "variable_name" { - symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("unresolved-dynamic".to_string()), - key_expr: Some(node_text(&name, source).to_string()), - receiver, - ..Default::default() - }); - return; - } - + let receiver = named_child_text(node, "object", source) + .map(|s| s.to_string()); symbols.calls.push(Call { name: node_text(&name, source).to_string(), - line: call_line, + line: start_line(node), dynamic: None, receiver, ..Default::default() diff --git a/crates/codegraph-core/src/extractors/python.rs b/crates/codegraph-core/src/extractors/python.rs index efbfafe02..b0f46cee8 100644 --- a/crates/codegraph-core/src/extractors/python.rs +++ b/crates/codegraph-core/src/extractors/python.rs @@ -112,137 +112,27 @@ fn handle_expr_stmt(node: &Node, source: &[u8], symbols: &mut FileSymbols) { }); } -/// Iterate non-punctuation arguments in a Python argument_list node. -fn iter_py_args(node: &Node, source: &[u8]) -> Vec<(String, String)> { - // Returns vec of (text, kind) for each real argument - let args = node.child_by_field_name("arguments") - .or_else(|| find_child(node, "argument_list")); - let mut result = Vec::new(); - if let Some(args) = args { - for i in 0..args.child_count() { - let Some(child) = args.child(i) else { continue }; - match child.kind() { - "(" | ")" | "," | "*" | "**" => continue, - kind => { - result.push((node_text(&child, source).to_string(), kind.to_string())); - } - } - } - } - result -} - fn handle_call(node: &Node, source: &[u8], symbols: &mut FileSymbols) { let Some(fn_node) = node.child_by_field_name("function") else { return }; - let call_line = start_line(node); - - match fn_node.kind() { - "identifier" => { - let name = node_text(&fn_node, source); - match name { - // eval(code) / exec(code) — dynamic code execution; flag - "eval" | "exec" => { - symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("eval".to_string()), - ..Default::default() - }); - } - // getattr(obj, 'method') — resolvable if literal string - "getattr" => { - let args = iter_py_args(node, source); - let receiver = args.first().map(|(t, _)| t.clone()); - match args.get(1) { - Some((text, kind)) if kind == "string" || kind == "concatenated_string" => { - let attr = text.replace(&['"', '\''][..], ""); - if !attr.is_empty() && !attr.contains('{') { - symbols.calls.push(Call { - name: attr.clone(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("reflection".to_string()), - key_expr: Some(text.clone()), - receiver, - ..Default::default() - }); - return; - } - } - Some((text, _)) => { - // Variable key — computed-key - symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("computed-key".to_string()), - key_expr: Some(text.clone()), - receiver, - ..Default::default() - }); - return; - } - None => { - symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("unresolved-dynamic".to_string()), - ..Default::default() - }); - return; - } - } - } - _ => { - symbols.calls.push(Call { - name: name.to_string(), - line: call_line, - dynamic: None, - receiver: None, - ..Default::default() - }); - } - } - } + let (call_name, receiver) = match fn_node.kind() { + "identifier" => (Some(node_text(&fn_node, source).to_string()), None), "attribute" => { - let call_name = named_child_text(&fn_node, "attribute", source) + let name = named_child_text(&fn_node, "attribute", source) .map(|s| s.to_string()); - let receiver = named_child_text(&fn_node, "object", source) + let recv = named_child_text(&fn_node, "object", source) .map(|s| s.to_string()); - - // functools.partial(fn, ...) — extract first arg as reflection target - if call_name.as_deref() == Some("partial") - && receiver.as_deref().map(|r| r == "functools" || r.ends_with("functools")).unwrap_or(false) - { - let args = iter_py_args(node, source); - if let Some((text, kind)) = args.first() { - if kind == "identifier" { - symbols.calls.push(Call { - name: text.clone(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("reflection".to_string()), - receiver: Some("functools.partial".to_string()), - ..Default::default() - }); - return; - } - } - } - - if let Some(name) = call_name { - symbols.calls.push(Call { - name, - line: call_line, - dynamic: None, - receiver, - ..Default::default() - }); - } + (name, recv) } - _ => {} + _ => (None, None), + }; + if let Some(name) = call_name { + symbols.calls.push(Call { + name, + line: start_line(node), + dynamic: None, + receiver, + ..Default::default() + }); } } diff --git a/crates/codegraph-core/src/extractors/ruby.rs b/crates/codegraph-core/src/extractors/ruby.rs index daec795a6..61bac796b 100644 --- a/crates/codegraph-core/src/extractors/ruby.rs +++ b/crates/codegraph-core/src/extractors/ruby.rs @@ -136,106 +136,25 @@ fn handle_singleton_method(node: &Node, source: &[u8], symbols: &mut FileSymbols }); } -/// Get the first non-punctuation argument from a Ruby call node's argument list. -/// Tries `child_by_field_name("arguments")` first, then falls back to `argument_list`. -fn get_first_ruby_arg<'a>(node: &Node<'a>) -> Option> { - let args = node.child_by_field_name("arguments") - .or_else(|| find_child(node, "argument_list"))?; - for i in 0..args.child_count() { - let child = args.child(i)?; - match child.kind() { - "(" | ")" | "," => continue, - _ => return Some(child), - } - } - None -} - fn handle_call(node: &Node, source: &[u8], symbols: &mut FileSymbols) { let Some(method_node) = node.child_by_field_name("method") else { return }; let method_text = node_text(&method_node, source); - let call_line = start_line(node); if method_text == "require" || method_text == "require_relative" { handle_require_call(node, source, symbols); - return; - } - if method_text == "include" || method_text == "extend" || method_text == "prepend" { + } else if method_text == "include" || method_text == "extend" || method_text == "prepend" { handle_mixin_call(node, source, symbols); - return; - } - - // send / public_send / __send__ — dynamic dispatch; first arg is the method name - if method_text == "send" || method_text == "public_send" || method_text == "__send__" { - let receiver = named_child_text(node, "receiver", source).map(|s| s.to_string()); - if let Some(first_arg) = get_first_ruby_arg(node) { - match first_arg.kind() { - "simple_symbol" | "symbol" => { - let sym = node_text(&first_arg, source); - let method_name = sym.trim_start_matches(':').replace(['\'', '"'], ""); - if !method_name.is_empty() { - symbols.calls.push(Call { - name: method_name, - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("reflection".to_string()), - key_expr: Some(sym.to_string()), - receiver, - ..Default::default() - }); - return; - } - } - "string" | "string_content" => { - let raw = node_text(&first_arg, source); - let method_name = raw.replace(['\'', '"'], ""); - if !method_name.is_empty() { - symbols.calls.push(Call { - name: method_name, - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("reflection".to_string()), - key_expr: Some(raw.to_string()), - receiver, - ..Default::default() - }); - return; - } - } - _ => { - let key = node_text(&first_arg, source).to_string(); - symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("computed-key".to_string()), - key_expr: Some(key), - receiver, - ..Default::default() - }); - return; - } - } - } - // No first arg — unresolved + } else { + let receiver = named_child_text(node, "receiver", source) + .map(|s| s.to_string()); symbols.calls.push(Call { - name: "".to_string(), - line: call_line, - dynamic: Some(true), - dynamic_kind: Some("unresolved-dynamic".to_string()), + name: method_text.to_string(), + line: start_line(node), + dynamic: None, + receiver, ..Default::default() }); - return; } - - let receiver = named_child_text(node, "receiver", source).map(|s| s.to_string()); - symbols.calls.push(Call { - name: method_text.to_string(), - line: call_line, - dynamic: None, - receiver, - ..Default::default() - }); } fn handle_require_call(node: &Node, source: &[u8], symbols: &mut FileSymbols) { diff --git a/src/db/repository/build-stmts.ts b/src/db/repository/build-stmts.ts index 4ba717ff5..05d62943e 100644 --- a/src/db/repository/build-stmts.ts +++ b/src/db/repository/build-stmts.ts @@ -6,6 +6,7 @@ interface PurgeStmts { cfgBlocks: SqliteStatement | null; dataflow: SqliteStatement | null; dataflowByVertex: SqliteStatement | null; + dataflowByCallEdge: SqliteStatement | null; dataflowSummary: SqliteStatement | null; dataflowVertices: SqliteStatement | null; complexity: SqliteStatement | null; @@ -51,6 +52,16 @@ function preparePurgeStmts(db: BetterSqlite3Database): PurgeStmts { `DELETE FROM dataflow WHERE source_vertex IN (SELECT id FROM dataflow_vertices WHERE func_id IN (SELECT id FROM nodes WHERE file = ?)) OR target_vertex IN (SELECT id FROM dataflow_vertices WHERE func_id IN (SELECT id FROM nodes WHERE file = ?))`, ), + // Delete dataflow rows whose call_edge_id references a calls edge that + // touches the deleted file (source or target). These rows are not caught by + // the source_id/target_id or vertex-based deletions above when the dataflow + // row's own nodes live in other files. Must run before the edges delete to + // avoid SQLITE_CONSTRAINT_FOREIGNKEY: dataflow.call_edge_id REFERENCES edges(id). + dataflowByCallEdge: tryPrepare( + `DELETE FROM dataflow WHERE call_edge_id IN + (SELECT id FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) + OR target_id IN (SELECT id FROM nodes WHERE file = @f))`, + ), dataflowSummary: tryPrepare( 'DELETE FROM dataflow_summary WHERE func_id IN (SELECT id FROM nodes WHERE file = ?)', ), @@ -94,6 +105,9 @@ function runPurge(stmts: PurgeStmts, file: string, opts: PurgeOpts = {}): void { stmts.cfgBlocks?.run(file); stmts.dataflow?.run(file, file); stmts.dataflowByVertex?.run(file, file); + // Clear dataflow rows keyed by call_edge_id before deleting edges so the FK + // (dataflow.call_edge_id REFERENCES edges(id)) does not block the edge purge. + stmts.dataflowByCallEdge?.run({ f: file }); stmts.dataflowSummary?.run(file); stmts.dataflowVertices?.run(file); stmts.complexity?.run(file); diff --git a/src/domain/analysis/roles.ts b/src/domain/analysis/roles.ts index f225e090f..15ca183d1 100644 --- a/src/domain/analysis/roles.ts +++ b/src/domain/analysis/roles.ts @@ -20,7 +20,6 @@ export function dynamicCallsData(customDbPath: string): DynamicCallCount[] { `SELECT dynamic_kind, COUNT(*) AS count FROM edges WHERE dynamic_kind IS NOT NULL - AND confidence = 0 GROUP BY dynamic_kind ORDER BY count DESC`, ) diff --git a/src/domain/graph/builder/stages/native-orchestrator.ts b/src/domain/graph/builder/stages/native-orchestrator.ts index c16ecb45e..5b04ba39b 100644 --- a/src/domain/graph/builder/stages/native-orchestrator.ts +++ b/src/domain/graph/builder/stages/native-orchestrator.ts @@ -2116,12 +2116,25 @@ export async function tryNativeOrchestrator( // exec may not exist on very old addon versions — safe to ignore } - const resultJson = ctx.nativeDb.buildGraph( - ctx.rootDir, - JSON.stringify(ctx.config), - JSON.stringify(ctx.aliases), - JSON.stringify(ctx.opts), - ); + let resultJson: string; + try { + resultJson = ctx.nativeDb.buildGraph( + ctx.rootDir, + JSON.stringify(ctx.config), + JSON.stringify(ctx.aliases), + JSON.stringify(ctx.opts), + ); + } finally { + // Restore FK enforcement so any subsequent writes to this connection + // (gap-repair, structure patch) retain FK protection — even if buildGraph() + // throws. + try { + ctx.nativeDb.exec('PRAGMA foreign_keys = ON'); + } catch { + // safe to ignore on very old addon versions + } + } + const result = JSON.parse(resultJson) as NativeOrchestratorResult; if (result.earlyExit) { diff --git a/src/extractors/c.ts b/src/extractors/c.ts index e014b6d14..b9c15a9b2 100644 --- a/src/extractors/c.ts +++ b/src/extractors/c.ts @@ -1,4 +1,10 @@ -import type { ExtractorOutput, SubDeclaration, TreeSitterNode, TreeSitterTree } from '../types.js'; +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; import { findChild, nodeEndLine } from './helpers.js'; /** @@ -136,81 +142,19 @@ function handleCInclude(node: TreeSitterNode, ctx: ExtractorOutput): void { }); } -/** Get the Nth non-punctuation argument from a C call_expression. */ -function getCArg(node: TreeSitterNode, index: number): TreeSitterNode | null { - const args = node.childForFieldName('arguments'); - if (!args) return null; - let count = 0; - for (let i = 0; i < args.childCount; i++) { - const child = args.child(i); - if (!child) continue; - const t = child.type; - if (t === '(' || t === ')' || t === ',' || t === 'comment') continue; - if (count === index) return child; - count++; - } - return null; -} - function handleCCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void { const funcNode = node.childForFieldName('function'); if (!funcNode) return; - const callLine = node.startPosition.row + 1; - + const call: Call = { name: '', line: node.startPosition.row + 1 }; if (funcNode.type === 'field_expression') { const field = funcNode.childForFieldName('field'); const argument = funcNode.childForFieldName('argument'); - if (field) { - ctx.calls.push({ - name: field.text, - line: callLine, - ...(argument ? { receiver: argument.text } : {}), - }); - } - return; - } - - // (*fp)(args) — function pointer call through dereference; unresolvable statically - if (funcNode.type === 'parenthesized_expression' || funcNode.type === 'pointer_expression') { - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'unresolved-dynamic', - }); - return; - } - - const fnName = funcNode.text; - - // dlsym(handle, "symbol") — dynamic symbol loading - if (fnName === 'dlsym' || fnName === 'dlvsym') { - const nameArg = getCArg(node, 1); // second arg is the symbol name - if (nameArg && (nameArg.type === 'string_literal' || nameArg.type === 'string_content')) { - const sym = nameArg.text.replace(/['"]/g, ''); - if (sym) { - ctx.calls.push({ - name: sym, - line: callLine, - dynamic: true, - dynamicKind: 'reflection', - keyExpr: nameArg.text, - }); - return; - } - } - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'unresolved-dynamic', - }); - return; - } - - if (fnName) { - ctx.calls.push({ name: fnName, line: callLine }); + if (field) call.name = field.text; + if (argument) call.receiver = argument.text; + } else { + call.name = funcNode.text; } + if (call.name) ctx.calls.push(call); } // ── Child extraction helpers ──────────────────────────────────────────────── diff --git a/src/extractors/cpp.ts b/src/extractors/cpp.ts index e113a3d24..4c76a7298 100644 --- a/src/extractors/cpp.ts +++ b/src/extractors/cpp.ts @@ -1,4 +1,10 @@ -import type { ExtractorOutput, SubDeclaration, TreeSitterNode, TreeSitterTree } from '../types.js'; +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; import { extractModifierVisibility, findChild, @@ -193,22 +199,6 @@ function handleCppTypedef(node: TreeSitterNode, ctx: ExtractorOutput): void { }); } -/** Get the nth non-punctuation argument from a call_expression argument list. */ -function getCppArg(node: TreeSitterNode, index: number): TreeSitterNode | null { - const args = node.childForFieldName('arguments'); - if (!args) return null; - let count = 0; - for (let i = 0; i < args.childCount; i++) { - const child = args.child(i); - if (!child) continue; - const t = child.type; - if (t === '(' || t === ')' || t === ',' || t === 'comment') continue; - if (count === index) return child; - count++; - } - return null; -} - function handleCppInclude(node: TreeSitterNode, ctx: ExtractorOutput): void { const pathNode = node.childForFieldName('path'); if (!pathNode) return; @@ -260,61 +250,16 @@ function handleCppDeclaration(node: TreeSitterNode, ctx: ExtractorOutput): void function handleCppCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void { const funcNode = node.childForFieldName('function'); if (!funcNode) return; - const callLine = node.startPosition.row + 1; - + const call: Call = { name: '', line: node.startPosition.row + 1 }; if (funcNode.type === 'field_expression') { const field = funcNode.childForFieldName('field'); const argument = funcNode.childForFieldName('argument'); - if (field) { - ctx.calls.push({ - name: field.text, - line: callLine, - ...(argument ? { receiver: argument.text } : {}), - }); - } - return; + if (field) call.name = field.text; + if (argument) call.receiver = argument.text; + } else { + call.name = funcNode.text; } - - // (*fp)(args) — function pointer dereference call; unresolvable statically - if (funcNode.type === 'parenthesized_expression' || funcNode.type === 'pointer_expression') { - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'unresolved-dynamic', - }); - return; - } - - const fnName = funcNode.text; - // dlsym(handle, "symbol") — dynamic symbol loading via C ABI. - // String-literal argument: resolves as reflection (extern "C" symbols are not mangled). - // Variable argument: flagged as unresolved-dynamic. - if (fnName === 'dlsym' || fnName === 'dlvsym') { - const nameArg = getCppArg(node, 1); // second arg is the symbol name - if (nameArg && (nameArg.type === 'string_literal' || nameArg.type === 'string_content')) { - const sym = nameArg.text.replace(/['"]/g, ''); - if (sym) { - ctx.calls.push({ - name: sym, - line: callLine, - dynamic: true, - dynamicKind: 'reflection', - keyExpr: nameArg.text, - }); - return; - } - } - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'unresolved-dynamic', - }); - return; - } - - if (fnName) ctx.calls.push({ name: fnName, line: callLine }); + if (call.name) ctx.calls.push(call); } // ── Utility helpers ───────────────────────────────────────────────────────── diff --git a/src/extractors/csharp.ts b/src/extractors/csharp.ts index 96767c4f6..850bb8a34 100644 --- a/src/extractors/csharp.ts +++ b/src/extractors/csharp.ts @@ -1,4 +1,5 @@ import type { + Call, ClassRelation, ExtractorOutput, SubDeclaration, @@ -226,89 +227,22 @@ function handleCsUsingDirective(node: TreeSitterNode, ctx: ExtractorOutput): voi }); } -/** Get the first string-literal argument text from a C# invocation node. */ -function getCsFirstStringArg(node: TreeSitterNode): string | null { - const args = node.childForFieldName('argument_list') || findChild(node, 'argument_list'); - if (!args) return null; - for (let i = 0; i < args.childCount; i++) { - const child = args.child(i); - if (!child) continue; - const t = child.type; - if (t === '(' || t === ')' || t === ',') continue; - // argument node may wrap the literal - const target = t === 'argument' ? (child.child(0) ?? child) : child; - if (target?.type === 'string_literal' || target?.type === 'verbatim_string_literal') { - return target.text.replace(/^["@]+|"$/g, ''); - } - break; - } - return null; -} - function handleCsInvocationExpr(node: TreeSitterNode, ctx: ExtractorOutput): void { const fn = node.childForFieldName('function') || node.child(0); if (!fn) return; - const callLine = node.startPosition.row + 1; - if (fn.type === 'identifier') { - ctx.calls.push({ name: fn.text, line: callLine }); - return; - } - - if (fn.type === 'member_access_expression') { + ctx.calls.push({ name: fn.text, line: node.startPosition.row + 1 }); + } else if (fn.type === 'member_access_expression') { const name = fn.childForFieldName('name'); - if (!name) return; - const methodName = name.text; - const expr = fn.childForFieldName('expression'); - const receiver = expr?.text; - - // method.Invoke(target, args) — runtime reflection; target unknown - if (methodName === 'Invoke') { - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'unresolved-dynamic', - receiver, - }); - return; + if (name) { + const expr = fn.childForFieldName('expression'); + const call: Call = { name: name.text, line: node.startPosition.row + 1 }; + if (expr) call.receiver = expr.text; + ctx.calls.push(call); } - - // type.GetMethod("name") / GetRuntimeMethod("name") — resolvable if literal - if ( - methodName === 'GetMethod' || - methodName === 'GetRuntimeMethod' || - methodName === 'GetDeclaredMethod' - ) { - const literal = getCsFirstStringArg(node); - if (literal) { - ctx.calls.push({ - name: literal, - line: callLine, - dynamic: true, - dynamicKind: 'reflection', - keyExpr: literal, - receiver, - }); - } else { - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'computed-key', - receiver, - }); - } - return; - } - - ctx.calls.push({ name: methodName, line: callLine, ...(receiver ? { receiver } : {}) }); - return; - } - - if (fn.type === 'generic_name' || fn.type === 'member_binding_expression') { + } else if (fn.type === 'generic_name' || fn.type === 'member_binding_expression') { const name = fn.childForFieldName('name') || fn.child(0); - if (name) ctx.calls.push({ name: name.text, line: callLine }); + if (name) ctx.calls.push({ name: name.text, line: node.startPosition.row + 1 }); } } diff --git a/src/extractors/elixir.ts b/src/extractors/elixir.ts index 313f5bf52..b1ad19b8b 100644 --- a/src/extractors/elixir.ts +++ b/src/extractors/elixir.ts @@ -80,50 +80,6 @@ function handleElixirCall( case 'alias': handleElixirImport(node, ctx, keyword); return; - case 'apply': { - // apply(module, :function, args) — Elixir dynamic dispatch - const applyArgs = node.childForFieldName('arguments'); - if (applyArgs) { - let argIdx = 0; - let secondArg: TreeSitterNode | null = null; - for (let i = 0; i < applyArgs.childCount; i++) { - const child = applyArgs.child(i); - if (!child) continue; - const t = child.type; - if (t === '(' || t === ')' || t === ',') continue; - if (argIdx === 1) { - secondArg = child; - break; - } - argIdx++; - } - if (secondArg) { - // :atom — strip leading colon to get function name - if (secondArg.type === 'atom' || secondArg.type === 'atom_literal') { - const fnName = secondArg.text.replace(/^:/, ''); - ctx.calls.push({ - name: fnName, - line: node.startPosition.row + 1, - dynamic: true, - dynamicKind: 'reflection', - keyExpr: secondArg.text, - }); - return; - } - // Variable function name — computed-key - ctx.calls.push({ - name: '', - line: node.startPosition.row + 1, - dynamic: true, - dynamicKind: 'computed-key', - keyExpr: secondArg.text, - }); - return; - } - } - ctx.calls.push({ name: 'apply', line: node.startPosition.row + 1 }); - return; - } default: // Regular function call ctx.calls.push({ name: keyword, line: node.startPosition.row + 1 }); diff --git a/src/extractors/go.ts b/src/extractors/go.ts index 710166980..21549134a 100644 --- a/src/extractors/go.ts +++ b/src/extractors/go.ts @@ -226,73 +226,19 @@ function handleGoConstDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { } } -/** Get the Nth non-punctuation argument from a Go call_expression node. */ -function getGoArg(node: TreeSitterNode, index: number): TreeSitterNode | null { - const args = node.childForFieldName('arguments'); - if (!args) return null; - let count = 0; - for (let i = 0; i < args.childCount; i++) { - const child = args.child(i); - if (!child) continue; - const t = child.type; - if (t === '(' || t === ')' || t === ',') continue; - if (count === index) return child; - count++; - } - return null; -} - function handleGoCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void { const fn = node.childForFieldName('function'); if (!fn) return; - const callLine = node.startPosition.row + 1; - if (fn.type === 'identifier') { - ctx.calls.push({ name: fn.text, line: callLine }); - return; - } - - if (fn.type === 'selector_expression') { + ctx.calls.push({ name: fn.text, line: node.startPosition.row + 1 }); + } else if (fn.type === 'selector_expression') { const field = fn.childForFieldName('field'); - if (!field) return; - const fieldName = field.text; - const operand = fn.childForFieldName('operand'); - - // v.MethodByName("name") — reflect-based dynamic dispatch - if (fieldName === 'MethodByName') { - const nameArg = getGoArg(node, 0); - if ( - nameArg && - (nameArg.type === 'interpreted_string_literal' || nameArg.type === 'raw_string_literal') - ) { - const methodName = nameArg.text.replace(/["`]/g, ''); - if (methodName) { - ctx.calls.push({ - name: methodName, - line: callLine, - dynamic: true, - dynamicKind: 'reflection', - keyExpr: nameArg.text, - receiver: operand?.text, - }); - return; - } - } - // Variable name arg — computed-key - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'computed-key', - keyExpr: nameArg?.text, - receiver: operand?.text, - }); - return; + if (field) { + const operand = fn.childForFieldName('operand'); + const call: Call = { name: field.text, line: node.startPosition.row + 1 }; + if (operand) call.receiver = operand.text; + ctx.calls.push(call); } - - const call: Call = { name: fieldName, line: callLine }; - if (operand) call.receiver = operand.text; - ctx.calls.push(call); } } diff --git a/src/extractors/lua.ts b/src/extractors/lua.ts index 072f68a8a..85c99b827 100644 --- a/src/extractors/lua.ts +++ b/src/extractors/lua.ts @@ -132,20 +132,6 @@ function handleLuaFunctionCall(node: TreeSitterNode, ctx: ExtractorOutput): void const nameNode = node.childForFieldName('name'); if (!nameNode) return; - // load(chunk) / loadstring(chunk) — dynamic code execution; always undecidable - if ( - nameNode.type === 'identifier' && - (nameNode.text === 'load' || nameNode.text === 'loadstring' || nameNode.text === 'dofile') - ) { - ctx.calls.push({ - name: '', - line: node.startPosition.row + 1, - dynamic: true, - dynamicKind: 'eval', - }); - return; - } - // Check for require() as import if (nameNode.type === 'identifier' && nameNode.text === 'require') { const args = node.childForFieldName('arguments'); @@ -175,37 +161,6 @@ function handleLuaFunctionCall(node: TreeSitterNode, ctx: ExtractorOutput): void const field = nameNode.childForFieldName('field'); if (field) call.name = field.text; if (table) call.receiver = table.text; - } else if (nameNode.type === 'bracket_index_expression') { - // t[k]() — bracket-index function call; key may be variable. - // AST: bracket_index_expression → [table_node, '[', key_expr, ']'] - // childForFieldName('key') is not defined for this node type in tree-sitter-lua, - // so we locate the key by scanning past the '[', ']', and the table node (by id). - const table = nameNode.childForFieldName('table'); - const tableId = table?.id; - let key: TreeSitterNode | null = null; - for (let i = 0; i < nameNode.childCount; i++) { - const ch = nameNode.child(i); - if (!ch) continue; - // Skip punctuation and the table node (compare by node id) - if (ch.type === '[' || ch.type === ']' || ch.id === tableId) continue; - key = ch; - break; - } - if (key && (key.type === 'string' || key.type === 'string_literal')) { - call.name = key.text.replace(/['"]/g, ''); - call.receiver = table?.text; - } else { - // Variable key — flagged as computed-key - ctx.calls.push({ - name: '', - line: call.line, - dynamic: true, - dynamicKind: 'computed-key', - keyExpr: key?.text, - receiver: table?.text, - }); - return; - } } else { call.name = nameNode.text; } diff --git a/src/extractors/php.ts b/src/extractors/php.ts index d868c5b99..b43c2489d 100644 --- a/src/extractors/php.ts +++ b/src/extractors/php.ts @@ -301,86 +301,14 @@ function handlePhpNamespaceUse(node: TreeSitterNode, ctx: ExtractorOutput): void } } -/** Extract first real argument from a PHP argument_list node. - * Descends into 'argument' wrapper nodes used by some PHP grammar variants. */ -function getFirstPhpArg(node: TreeSitterNode): TreeSitterNode | null { - const args = node.childForFieldName('arguments') || findChild(node, 'argument_list'); - if (!args) return null; - for (let i = 0; i < args.childCount; i++) { - const child = args.child(i); - if (!child) continue; - const t = child.type; - if (t === '(' || t === ')' || t === ',') continue; - // Descend into 'argument' wrapper if present - if (t === 'argument') return child.child(0) ?? child; - return child; - } - return null; -} - function handlePhpFuncCall(node: TreeSitterNode, ctx: ExtractorOutput): void { const fn = node.childForFieldName('function') || node.child(0); if (!fn) return; - const callLine = node.startPosition.row + 1; - - // $fn() — variable function call; target cannot be resolved statically. - // Use keyExpr (not receiver) to capture the callable variable name for diagnostics. - if (fn.type === 'variable_name') { - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'unresolved-dynamic', - keyExpr: fn.text, - }); - return; - } - if (fn.type === 'name' || fn.type === 'identifier') { - const fnName = fn.text; - - // call_user_func($callable, args) — extract first arg as actual target - if (fnName === 'call_user_func' || fnName === 'call_user_func_array') { - const firstArg = getFirstPhpArg(node); - if (firstArg) { - const t = firstArg.type; - if (t === 'variable_name') { - // Variable callable — unresolved - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'unresolved-dynamic', - keyExpr: firstArg.text, - }); - } else if (t === 'string' || t === 'encapsed_string') { - const literal = firstArg.text.replace(/['"]/g, ''); - ctx.calls.push({ - name: literal, - line: callLine, - dynamic: true, - dynamicKind: 'reflection', - keyExpr: firstArg.text, - }); - } else { - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'unresolved-dynamic', - }); - } - return; - } - } - - ctx.calls.push({ name: fnName, line: callLine }); - return; - } - - if (fn.type === 'qualified_name') { + ctx.calls.push({ name: fn.text, line: node.startPosition.row + 1 }); + } else if (fn.type === 'qualified_name') { const parts = fn.text.split('\\'); - ctx.calls.push({ name: parts[parts.length - 1] ?? fn.text, line: callLine }); + ctx.calls.push({ name: parts[parts.length - 1] ?? fn.text, line: node.startPosition.row + 1 }); } } @@ -388,22 +316,7 @@ function handlePhpMemberCall(node: TreeSitterNode, ctx: ExtractorOutput): void { const name = node.childForFieldName('name'); if (!name) return; const obj = node.childForFieldName('object'); - const callLine = node.startPosition.row + 1; - - // $obj->$m() — variable method name; unresolvable statically - if (name.type === 'variable_name') { - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'unresolved-dynamic', - receiver: obj?.text, - keyExpr: name.text, - }); - return; - } - - const call: Call = { name: name.text, line: callLine }; + const call: Call = { name: name.text, line: node.startPosition.row + 1 }; if (obj) call.receiver = obj.text; ctx.calls.push(call); } diff --git a/src/extractors/python.ts b/src/extractors/python.ts index d0b222490..2cd547a1e 100644 --- a/src/extractors/python.ts +++ b/src/extractors/python.ts @@ -1,4 +1,10 @@ -import type { ExtractorOutput, SubDeclaration, TreeSitterNode, TreeSitterTree } from '../types.js'; +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; import { findChild, findParentNode, @@ -154,113 +160,22 @@ function handlePyClassDef(node: TreeSitterNode, ctx: ExtractorOutput): void { } } -/** Iterate non-punctuation arguments in a Python argument_list node. */ -function* iterPyArgs(node: TreeSitterNode): Generator { - const args = node.childForFieldName('arguments') || findChild(node, 'argument_list'); - if (!args) return; - for (let i = 0; i < args.childCount; i++) { - const child = args.child(i); - if (!child) continue; - const t = child.type; - if (t === '(' || t === ')' || t === ',' || t === '*' || t === '**') continue; - yield child; - } -} - function handlePyCall(node: TreeSitterNode, ctx: ExtractorOutput): void { const fn = node.childForFieldName('function'); if (!fn) return; - const callLine = node.startPosition.row + 1; - - // ── Identifier calls (e.g. getattr, eval, exec) ───────────────────────── - if (fn.type === 'identifier') { - const name = fn.text; - - // eval(code) / exec(code) — dynamic code execution; always undecidable - if (name === 'eval' || name === 'exec') { - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'eval', - }); - return; - } - - // getattr(obj, 'method') — resolvable if name is a literal; computed-key or sink if variable - if (name === 'getattr') { - const argIter = iterPyArgs(node); - const firstArg = argIter.next().value as TreeSitterNode | undefined; - const secondArg = argIter.next().value as TreeSitterNode | undefined; - const receiver = firstArg?.type === 'identifier' ? firstArg.text : undefined; - if (secondArg) { - const st = secondArg.type; - if (st === 'string' || st === 'concatenated_string') { - const attrName = secondArg.text.replace(/^['"]{1,3}|['"]{1,3}$/g, ''); - if (attrName && !attrName.includes('{')) { - ctx.calls.push({ - name: attrName, - line: callLine, - dynamic: true, - dynamicKind: 'reflection', - keyExpr: secondArg.text, - receiver, - }); - return; - } - } - // Variable or f-string key — computed-key - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'computed-key', - keyExpr: secondArg.text, - receiver, - }); - return; - } - // getattr with no second arg — unresolved - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'unresolved-dynamic', - }); - return; - } - - // Normal identifier call - ctx.calls.push({ name, line: callLine }); - return; - } - - // ── Attribute calls (e.g. obj.method(), functools.partial(fn)) ────────── - if (fn.type === 'attribute') { + let callName: string | null = null; + let receiver: string | undefined; + if (fn.type === 'identifier') callName = fn.text; + else if (fn.type === 'attribute') { const attr = fn.childForFieldName('attribute'); + if (attr) callName = attr.text; const obj = fn.childForFieldName('object'); - if (!attr) return; - const callName = attr.text; - const receiver = obj?.text; - - // functools.partial(fn, *args) — extract fn as reflection target - if (callName === 'partial' && (receiver === 'functools' || receiver?.endsWith('functools'))) { - const argIter = iterPyArgs(node); - const firstArg = argIter.next().value as TreeSitterNode | undefined; - if (firstArg?.type === 'identifier') { - ctx.calls.push({ - name: firstArg.text, - line: callLine, - dynamic: true, - dynamicKind: 'reflection', - receiver: 'functools.partial', - }); - return; - } - } - - ctx.calls.push({ name: callName, line: callLine, ...(receiver ? { receiver } : {}) }); - return; + if (obj) receiver = obj.text; + } + if (callName) { + const call: Call = { name: callName, line: node.startPosition.row + 1 }; + if (receiver) call.receiver = receiver; + ctx.calls.push(call); } } diff --git a/src/extractors/ruby.ts b/src/extractors/ruby.ts index 88accb7f7..60abce7d9 100644 --- a/src/extractors/ruby.ts +++ b/src/extractors/ruby.ts @@ -151,95 +151,23 @@ function handleRubyAssignment(node: TreeSitterNode, ctx: ExtractorOutput): void } } -/** Extract first real argument node from a Ruby call node's argument list. */ -function getFirstRubyArg(node: TreeSitterNode): TreeSitterNode | null { - const args = node.childForFieldName('arguments') || findChild(node, 'argument_list'); - if (!args) return null; - for (let i = 0; i < args.childCount; i++) { - const child = args.child(i); - if (!child) continue; - const t = child.type; - if (t === '(' || t === ')' || t === ',' || t === 'splat_argument') continue; - return child; - } - return null; -} - function handleRubyCall(node: TreeSitterNode, ctx: ExtractorOutput): void { const methodNode = node.childForFieldName('method'); if (!methodNode) return; - const methodName = methodNode.text; - const callLine = node.startPosition.row + 1; - - if (methodName === 'require' || methodName === 'require_relative') { + if (methodNode.text === 'require' || methodNode.text === 'require_relative') { handleRubyRequire(node, ctx); - return; - } - if (methodName === 'include' || methodName === 'extend' || methodName === 'prepend') { + } else if ( + methodNode.text === 'include' || + methodNode.text === 'extend' || + methodNode.text === 'prepend' + ) { handleRubyModuleInclusion(node, methodNode, ctx); - return; - } - - // send / public_send / __send__ — dynamic dispatch; first arg is the method name - if (methodName === 'send' || methodName === 'public_send' || methodName === '__send__') { + } else { const recv = node.childForFieldName('receiver'); - const firstArg = getFirstRubyArg(node); - if (firstArg) { - const t = firstArg.type; - if (t === 'simple_symbol' || t === 'symbol') { - // :method_name — strip leading colon and optional quotes - const symName = firstArg.text.replace(/^:/, '').replace(/['"]/g, ''); - if (symName) { - ctx.calls.push({ - name: symName, - line: callLine, - dynamic: true, - dynamicKind: 'reflection', - keyExpr: firstArg.text, - receiver: recv?.text, - }); - return; - } - } - if (t === 'string' || t === 'string_content') { - const strName = firstArg.text.replace(/['"]/g, ''); - if (strName) { - ctx.calls.push({ - name: strName, - line: callLine, - dynamic: true, - dynamicKind: 'reflection', - keyExpr: firstArg.text, - receiver: recv?.text, - }); - return; - } - } - // Variable method name — computed-key - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'computed-key', - keyExpr: firstArg.text, - receiver: recv?.text, - }); - return; - } - // No first arg — unresolved - ctx.calls.push({ - name: '', - line: callLine, - dynamic: true, - dynamicKind: 'unresolved-dynamic', - }); - return; + const call: Call = { name: methodNode.text, line: node.startPosition.row + 1 }; + if (recv) call.receiver = recv.text; + ctx.calls.push(call); } - - const recv = node.childForFieldName('receiver'); - const call: Call = { name: methodName, line: callLine }; - if (recv) call.receiver = recv.text; - ctx.calls.push(call); } function handleRubyRequire(node: TreeSitterNode, ctx: ExtractorOutput): void { diff --git a/src/extractors/swift.ts b/src/extractors/swift.ts index ddaa67d0b..9b2f6e90f 100644 --- a/src/extractors/swift.ts +++ b/src/extractors/swift.ts @@ -278,78 +278,7 @@ function handleSwiftCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): } else { call.name = funcNode.text; } - - if (!call.name) return; - - // performSelector — ObjC-style dynamic dispatch; selector not statically knowable - if (call.name === 'performSelector') { - ctx.calls.push({ - name: '', - line: call.line, - dynamic: true, - dynamicKind: 'unresolved-dynamic', - receiver: call.receiver, - }); - return; - } - - // NSSelectorFromString("name") — selector from literal string. - // Only inspect the direct first argument to the call (not the whole subtree) to avoid - // false positives from nested calls like NSSelectorFromString(generateSelector("greet")). - // - // Swift AST: call_expression → [simple_identifier, call_suffix] - // call_suffix → [value_arguments] - // value_arguments → ['(', value_argument, ')'] - // value_argument → [line_string_literal] - // line_string_literal → ['"', line_str_text, '"'] - if (call.name === 'NSSelectorFromString') { - let literal: string | null = null; - // Resolve value_arguments: call_expression has a call_suffix child that wraps value_arguments. - const callSuffix = findChild(node, 'call_suffix'); - const valueArgs = - node.childForFieldName('arguments') ?? - (callSuffix ? (findChild(callSuffix, 'value_arguments') ?? callSuffix) : null) ?? - findChild(node, 'value_arguments'); - if (valueArgs) { - for (let i = 0; i < valueArgs.childCount; i++) { - const arg = valueArgs.child(i); - if (!arg) continue; - const t = arg.type; - if (t === '(' || t === ')' || t === ',') continue; - // value_argument wraps the actual value expression - const valueNode = - t === 'value_argument' - ? (arg.childForFieldName('value') ?? arg.child(arg.childCount - 1)) - : arg; - if (!valueNode) break; - const vt = valueNode.type; - if (vt === 'line_string_literal' || vt === 'string_literal') { - // Look for line_str_text (Swift) or string_content (other languages) - for (let j = 0; j < valueNode.childCount; j++) { - const ch = valueNode.child(j); - if (ch?.type === 'line_str_text' || ch?.type === 'string_content') { - literal = ch.text; - break; - } - } - if (!literal) literal = valueNode.text.replace(/^["']|["']$/g, ''); - break; - } - // First non-string non-punctuation argument — computed, not a literal selector - break; - } - } - ctx.calls.push({ - name: literal ?? '', - line: call.line, - dynamic: true, - dynamicKind: literal ? 'reflection' : 'unresolved-dynamic', - keyExpr: literal ?? undefined, - }); - return; - } - - ctx.calls.push(call); + if (call.name) ctx.calls.push(call); } /** diff --git a/src/features/dataflow.ts b/src/features/dataflow.ts index 3c57e5194..e04894792 100644 --- a/src/features/dataflow.ts +++ b/src/features/dataflow.ts @@ -473,7 +473,7 @@ function buildDataflowVerticesAndEdges( * * All callers (buildDataflowVerticesFromMap, buildDataflowEdges, * buildDataflowP4ForNative) manage their own outer transaction and call this - * directly to avoid nesting, which better-sqlite3 does not support. + * directly to avoid nested transactions, which better-sqlite3 does not support. */ function runInterproceduralStitch( db: BetterSqlite3Database, diff --git a/src/presentation/queries-cli/overview.ts b/src/presentation/queries-cli/overview.ts index a85c2fa2d..2b9889e8f 100644 --- a/src/presentation/queries-cli/overview.ts +++ b/src/presentation/queries-cli/overview.ts @@ -221,56 +221,15 @@ function printQuality(data: StatsData): void { } } -// Dead sub-role display metadata: human-readable label and whether the category is actionable. -const DEAD_SUB_ROLE_META: Record = { - 'dead-leaf': { label: 'dead-leaf (params/props/constants)', actionable: false }, - 'dead-unresolved': { label: 'dead-unresolved (resolution gaps)', actionable: false }, - 'dead-ffi': { label: 'dead-ffi (native/FFI boundary)', actionable: false }, - 'dead-entry': { label: 'dead-entry (CLI/MCP entry points)', actionable: false }, - 'dead-callable': { label: 'dead-callable (actionable)', actionable: true }, -}; - function printRoles(data: StatsData): void { - if (!data.roles || Object.keys(data.roles).length === 0) return; - - // The roles object may contain both a synthetic 'dead' aggregate key and - // individual 'dead-*' sub-role keys (on databases built after sub-roles were - // introduced). Exclude the aggregate only when sub-role keys are present to - // avoid double-counting; on older databases without any 'dead-*' keys, keep - // 'dead' so its count is not silently dropped. - const hasDeadSubRoles = Object.keys(data.roles).some((k) => k.startsWith('dead-')); - const nonAggregateEntries = Object.entries(data.roles).filter(([k]) => - hasDeadSubRoles ? k !== 'dead' : true, - ) as [string, number][]; - const total = nonAggregateEntries.reduce((a, [, v]) => a + v, 0); - console.log(`\nRoles: ${total} classified symbols`); - - // Split into dead sub-roles and all other roles. - const deadEntries = nonAggregateEntries.filter(([k]) => k.startsWith('dead-') || k === 'dead'); - const otherEntries = nonAggregateEntries - .filter(([k]) => !k.startsWith('dead-') && k !== 'dead') - .sort((a, b) => b[1] - a[1]); - - if (otherEntries.length > 0) { - printCountGrid(otherEntries, 18); - } - - if (deadEntries.length > 0) { - const deadTotal = deadEntries.reduce((a, [, v]) => a + v, 0); - console.log(`\n Dead symbols: ${deadTotal}`); - // Sort in-place: non-actionable first (largest first within group), actionable last. - deadEntries.sort((a, b) => { - const aActionable = DEAD_SUB_ROLE_META[a[0]]?.actionable ?? false; - const bActionable = DEAD_SUB_ROLE_META[b[0]]?.actionable ?? false; - if (aActionable !== bActionable) return aActionable ? 1 : -1; - return b[1] - a[1]; - }); - for (const [role, count] of deadEntries) { - const meta = DEAD_SUB_ROLE_META[role]; - const label = meta ? meta.label : role; - const actionableTag = meta?.actionable ? ' ← actionable' : ''; - console.log(` ${label.padEnd(38)} ${String(count).padStart(6)}${actionableTag}`); - } + if (data.roles && Object.keys(data.roles).length > 0) { + const total = Object.values(data.roles).reduce((a, b) => a + b, 0); + console.log(`\nRoles: ${total} classified symbols`); + const roleEntries = Object.entries(data.roles).sort((a, b) => b[1] - a[1]) as [ + string, + number, + ][]; + printCountGrid(roleEntries, 18); } } diff --git a/src/types.ts b/src/types.ts index f2a08d177..fa2890b6b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -484,7 +484,7 @@ export interface LOCMetrics { export type DynamicKind = | 'computed-literal' // obj["foo"]() — resolvable; already emitted as normal edge | 'computed-key' // obj[k]() — potentially resolvable via pts; else flagged - | 'reflection' // .call/.apply/.bind / Reflect.* — resolved if target reachable; silently dropped if not (RES-3) + | 'reflection' // .call/.apply/.bind / Reflect.* — usually resolved; not flagged (drops silently if unresolved) | 'eval' // eval() / new Function() — undecidable; always flagged | 'unresolved-dynamic'; // any other detected dynamic pattern; flagged diff --git a/tests/benchmarks/resolution/fixtures/dynamic-c/dispatch.c b/tests/benchmarks/resolution/fixtures/dynamic-c/dispatch.c deleted file mode 100644 index 86a338c9b..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-c/dispatch.c +++ /dev/null @@ -1,25 +0,0 @@ -/* Fixture: C dynamic dispatch patterns - * (*fp)(args) → flagged as unresolved-dynamic (function pointer; target unknown) - * dlsym(handle, "symbol") → resolved as reflection (string literal matches symbol in DB) - */ -#include -#include - -void greet(const char *name) { - printf("Hello, %s\n", name); -} - -void farewell(const char *name) { - printf("Goodbye, %s\n", name); -} - -/* (*fp)(args) — function pointer dereference; unresolvable statically */ -void runFunctionPointer(void (*fp)(const char *)) { - (*fp)("world"); -} - -/* dlsym(handle, "greet") — string literal resolves as reflection; fn pointer call flagged */ -void runDlsym(void *handle) { - void (*fn)(const char *) = dlsym(handle, "greet"); - if (fn) fn("world"); -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-c/expected-edges.json b/tests/benchmarks/resolution/fixtures/dynamic-c/expected-edges.json deleted file mode 100644 index 56fb2d3d5..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-c/expected-edges.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "../../expected-edges.schema.json", - "language": "c", - "description": "Phase 5 C fixture: (*fp)() flagged unresolved-dynamic; dlsym('greet') resolved as reflection.", - "edges": [ - { - "source": { "name": "runDlsym", "file": "dispatch.c" }, - "target": { "name": "greet", "file": "dispatch.c" }, - "kind": "calls", - "mode": "dynamic", - "notes": "dlsym(handle, 'greet') — reflection kind, resolves to greet() in same unit" - } - ] -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-csharp/Reflection.cs b/tests/benchmarks/resolution/fixtures/dynamic-csharp/Reflection.cs deleted file mode 100644 index ef665cefe..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-csharp/Reflection.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Phase 6: C# reflection patterns -// GetMethod("name") → reflection kind -// method.Invoke() → unresolved-dynamic -using System.Reflection; - -public class Reflection { - public static string Greet(string name) => $"Hello, {name}"; - - // type.GetMethod("Greet") — reflection kind, resolvable in theory - public static MethodInfo RunGetMethod(Type type) { - return type.GetMethod("Greet"); - } - - // method.Invoke(target, args) — unresolved-dynamic - public static object RunInvoke(MethodInfo method, object target) { - return method.Invoke(target, new object[] { "world" }); - } -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-csharp/expected-edges.json b/tests/benchmarks/resolution/fixtures/dynamic-csharp/expected-edges.json deleted file mode 100644 index 4752c5073..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-csharp/expected-edges.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "../../expected-edges.schema.json", - "language": "csharp", - "description": "Phase 6 C# fixture: GetMethod resolved as reflection; Invoke flagged. Methods class-qualified in DB → 0% recall until RES-3.", - "edges": [ - { - "source": { "name": "Reflection.RunGetMethod", "file": "Reflection.cs" }, - "target": { "name": "Reflection.Greet", "file": "Reflection.cs" }, - "kind": "calls", - "mode": "dynamic", - "notes": "type.GetMethod('Greet') — reflection kind; 0% recall because DB uses class-qualified name" - } - ] -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-elixir/dispatch.ex b/tests/benchmarks/resolution/fixtures/dynamic-elixir/dispatch.ex deleted file mode 100644 index 12cd948ee..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-elixir/dispatch.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule DynamicDispatch do - # Fixture: Elixir dynamic dispatch via apply/3 - # apply(module, :function, args) → reflection kind - # apply(module, variable, args) → computed-key - - def greet(name), do: "Hello, #{name}" - def farewell(name), do: "Goodbye, #{name}" - - # apply(DynamicDispatch, :greet, ["world"]) — reflection kind - def run_apply_atom do - apply(DynamicDispatch, :greet, ["world"]) - end - - # apply(module, fn_name, args) — computed-key kind - def run_apply_variable(fn_name) do - apply(DynamicDispatch, fn_name, ["world"]) - end -end diff --git a/tests/benchmarks/resolution/fixtures/dynamic-elixir/expected-edges.json b/tests/benchmarks/resolution/fixtures/dynamic-elixir/expected-edges.json deleted file mode 100644 index c010e25e1..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-elixir/expected-edges.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "../../expected-edges.schema.json", - "language": "elixir", - "description": "Phase 6 Elixir fixture: apply(module, :fn, args) detected; module-qualified names → 0% recall until RES-3.", - "edges": [ - { - "source": { "name": "DynamicDispatch.run_apply_atom", "file": "dispatch.ex" }, - "target": { "name": "DynamicDispatch.greet", "file": "dispatch.ex" }, - "kind": "calls", - "mode": "dynamic", - "notes": "apply(DynamicDispatch, :greet, args) — reflection kind; 0% recall (module-qualified DB names)" - } - ] -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-go/expected-edges.json b/tests/benchmarks/resolution/fixtures/dynamic-go/expected-edges.json deleted file mode 100644 index caecb3001..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-go/expected-edges.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "../../expected-edges.schema.json", - "language": "go", - "description": "Phase 5 Go fixture: reflect.Value.MethodByName resolved; variable name flagged.", - "edges": [ - { - "source": { "name": "runMethodByNameLiteral", "file": "reflect.go" }, - "target": { "name": "Greet", "file": "reflect.go" }, - "kind": "calls", - "mode": "dynamic", - "notes": "v.MethodByName('Greet') — reflection kind, resolves to top-level Greet()" - } - ] -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-go/reflect.go b/tests/benchmarks/resolution/fixtures/dynamic-go/reflect.go deleted file mode 100644 index e32518cc8..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-go/reflect.go +++ /dev/null @@ -1,24 +0,0 @@ -// Fixture: Go reflection patterns -// MethodByName("name") → resolved as reflection kind -// MethodByName(variable) → flagged as computed-key -package main - -import "reflect" - -func Greet(name string) string { - return "Hello, " + name -} - -func Farewell(name string) string { - return "Goodbye, " + name -} - -// v.MethodByName("Greet") — reflection kind, resolved to Greet() -func runMethodByNameLiteral(v reflect.Value) reflect.Value { - return v.MethodByName("Greet") -} - -// v.MethodByName(name) — computed-key kind, flagged as sink edge -func runMethodByNameVariable(v reflect.Value, name string) reflect.Value { - return v.MethodByName(name) -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-lua/dispatch.lua b/tests/benchmarks/resolution/fixtures/dynamic-lua/dispatch.lua deleted file mode 100644 index 9884236f7..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-lua/dispatch.lua +++ /dev/null @@ -1,24 +0,0 @@ --- Phase 6: Lua dynamic dispatch patterns --- t["method"]() → computed-literal, resolved --- load(code) → eval kind, flagged --- t[k]() → computed-key kind, flagged - -local function greet(name) - return "Hello, " .. name -end - --- t["greet"]() — computed-literal with string key; resolves to greet() -local function run_literal_dispatch(t) - t["greet"]("world") -end - --- load(code) — dynamic code execution; always flagged -local function run_load(code) - local fn = load(code) - if fn then fn() end -end - --- t[k]() — variable key; flagged as computed-key -local function run_variable_dispatch(t, k) - t[k]("world") -end diff --git a/tests/benchmarks/resolution/fixtures/dynamic-lua/expected-edges.json b/tests/benchmarks/resolution/fixtures/dynamic-lua/expected-edges.json deleted file mode 100644 index 71a0da820..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-lua/expected-edges.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "../../expected-edges.schema.json", - "language": "lua", - "description": "Phase 6 Lua fixture: t['greet']() resolved; load(code) and t[k]() flagged.", - "edges": [ - { - "source": { "name": "run_literal_dispatch", "file": "dispatch.lua" }, - "target": { "name": "greet", "file": "dispatch.lua" }, - "kind": "calls", - "mode": "dynamic", - "notes": "t['greet']() — string-literal bracket index, resolves to local greet()" - } - ] -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-php/dispatch.php b/tests/benchmarks/resolution/fixtures/dynamic-php/dispatch.php deleted file mode 100644 index 4ef991120..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-php/dispatch.php +++ /dev/null @@ -1,28 +0,0 @@ -$m() → flagged as unresolved-dynamic -// $fn() → flagged as unresolved-dynamic - -function greet(string $name): string { - return "Hello, {$name}"; -} - -function farewell(string $name): string { - return "Goodbye, {$name}"; -} - -// call_user_func('greet', ...) — resolved to top-level greet() -function runCallUserFuncLiteral(): string { - return call_user_func('greet', 'world'); -} - -// $obj->$m() — variable method name; unresolved-dynamic -function runVariableMethod(object $obj, string $m): mixed { - return $obj->$m('world'); -} - -// $fn() — variable callable; unresolved-dynamic -function runVariableCallable(callable $fn): mixed { - return $fn('world'); -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-php/expected-edges.json b/tests/benchmarks/resolution/fixtures/dynamic-php/expected-edges.json deleted file mode 100644 index 990bc81d8..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-php/expected-edges.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "../../expected-edges.schema.json", - "language": "php", - "description": "Phase 4 PHP fixture: call_user_func with string literal resolved; variable calls flagged.", - "edges": [ - { - "source": { "name": "runCallUserFuncLiteral", "file": "dispatch.php" }, - "target": { "name": "greet", "file": "dispatch.php" }, - "kind": "calls", - "mode": "dynamic", - "notes": "call_user_func('greet', ...) — reflection kind, resolves to top-level greet()" - } - ] -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-python/dispatch.py b/tests/benchmarks/resolution/fixtures/dynamic-python/dispatch.py deleted file mode 100644 index 0d2b6fef8..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-python/dispatch.py +++ /dev/null @@ -1,37 +0,0 @@ -# Fixture: Python dynamic dispatch patterns -# getattr(obj, 'method') → resolved as reflection kind -# eval/exec → flagged as eval kind -# getattr(obj, variable) → flagged as computed-key - - -def greet(name: str) -> str: - return f"Hello, {name}" - - -def farewell(name: str) -> str: - return f"Goodbye, {name}" - - -def run_getattr_literal(obj: object) -> object: - # getattr(obj, 'greet') — reflection kind, resolved to greet() - return getattr(obj, 'greet') - - -def run_getattr_farewell(obj: object) -> object: - # getattr(obj, 'farewell') — reflection kind, resolved to farewell() - return getattr(obj, 'farewell') - - -def run_getattr_variable(obj: object, method_name: str) -> object: - # getattr(obj, method_name) — computed-key kind, flagged as sink edge - return getattr(obj, method_name) - - -def run_eval(code: str) -> object: - # eval(code) — eval kind, always flagged - return eval(code) # noqa: S307 - - -def run_exec(code: str) -> None: - # exec(code) — eval kind, always flagged - exec(code) # noqa: S102 diff --git a/tests/benchmarks/resolution/fixtures/dynamic-python/expected-edges.json b/tests/benchmarks/resolution/fixtures/dynamic-python/expected-edges.json deleted file mode 100644 index 7798676aa..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-python/expected-edges.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "../../expected-edges.schema.json", - "language": "python", - "description": "Phase 3 Python fixture: getattr with literal name resolved; eval/exec/variable-getattr flagged as sink edges.", - "edges": [ - { - "source": { "name": "run_getattr_literal", "file": "dispatch.py" }, - "target": { "name": "greet", "file": "dispatch.py" }, - "kind": "calls", - "mode": "dynamic", - "notes": "getattr(obj, 'greet') — reflection kind, resolves to greet()" - }, - { - "source": { "name": "run_getattr_farewell", "file": "dispatch.py" }, - "target": { "name": "farewell", "file": "dispatch.py" }, - "kind": "calls", - "mode": "dynamic", - "notes": "getattr(obj, 'farewell') — reflection kind, resolves to farewell()" - } - ] -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-ruby/dispatch.rb b/tests/benchmarks/resolution/fixtures/dynamic-ruby/dispatch.rb deleted file mode 100644 index d922095ae..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-ruby/dispatch.rb +++ /dev/null @@ -1,26 +0,0 @@ -# Fixture: Ruby dynamic dispatch patterns -# send(:method) → resolved as reflection kind -# send(variable) → flagged as computed-key - -def greet(name) - "Hello, #{name}" -end - -def farewell(name) - "Goodbye, #{name}" -end - -# obj.send(:greet) — reflection kind, resolved to greet() -def run_send_symbol(obj) - obj.send(:greet, 'world') -end - -# obj.public_send(:farewell) — reflection kind, resolved to farewell() -def run_public_send(obj) - obj.public_send(:farewell, 'world') -end - -# obj.send(method_name) — computed-key kind, flagged as sink edge -def run_send_variable(obj, method_name) - obj.send(method_name, 'world') -end diff --git a/tests/benchmarks/resolution/fixtures/dynamic-ruby/expected-edges.json b/tests/benchmarks/resolution/fixtures/dynamic-ruby/expected-edges.json deleted file mode 100644 index f10404433..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-ruby/expected-edges.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "../../expected-edges.schema.json", - "language": "ruby", - "description": "Phase 4 Ruby fixture: send(:method) resolved as reflection; send(variable) flagged.", - "edges": [ - { - "source": { "name": "run_send_symbol", "file": "dispatch.rb" }, - "target": { "name": "greet", "file": "dispatch.rb" }, - "kind": "calls", - "mode": "dynamic", - "notes": "obj.send(:greet) — reflection kind, resolves to top-level greet()" - }, - { - "source": { "name": "run_public_send", "file": "dispatch.rb" }, - "target": { "name": "farewell", "file": "dispatch.rb" }, - "kind": "calls", - "mode": "dynamic", - "notes": "obj.public_send(:farewell) — reflection kind, resolves to top-level farewell()" - } - ] -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-swift/dispatch.swift b/tests/benchmarks/resolution/fixtures/dynamic-swift/dispatch.swift deleted file mode 100644 index 62e752673..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-swift/dispatch.swift +++ /dev/null @@ -1,22 +0,0 @@ -// Phase 6: Swift dynamic dispatch patterns -// NSSelectorFromString("name") → reflection kind -// performSelector → flagged as unresolved-dynamic -import Foundation - -func greet(_ name: String) -> String { - return "Hello, \(name)" -} - -func farewell(_ name: String) -> String { - return "Goodbye, \(name)" -} - -// NSSelectorFromString("greet") — reflection kind, resolves to top-level greet() -func runNSSelectorFromString() -> Selector { - return NSSelectorFromString("greet") -} - -// NSSelectorFromString with farewell -func runNSSelectorFarewell() -> Selector { - return NSSelectorFromString("farewell") -} diff --git a/tests/benchmarks/resolution/fixtures/dynamic-swift/expected-edges.json b/tests/benchmarks/resolution/fixtures/dynamic-swift/expected-edges.json deleted file mode 100644 index 297a40123..000000000 --- a/tests/benchmarks/resolution/fixtures/dynamic-swift/expected-edges.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "../../expected-edges.schema.json", - "language": "swift", - "description": "Phase 6 Swift fixture: NSSelectorFromString resolved as reflection (top-level fns unqualified).", - "edges": [ - { - "source": { "name": "runNSSelectorFromString", "file": "dispatch.swift" }, - "target": { "name": "greet", "file": "dispatch.swift" }, - "kind": "calls", - "mode": "dynamic", - "notes": "NSSelectorFromString('greet') — reflection kind, resolves to top-level greet()" - }, - { - "source": { "name": "runNSSelectorFarewell", "file": "dispatch.swift" }, - "target": { "name": "farewell", "file": "dispatch.swift" }, - "kind": "calls", - "mode": "dynamic", - "notes": "NSSelectorFromString('farewell') — reflection kind, resolves to top-level farewell()" - } - ] -} diff --git a/tests/benchmarks/resolution/resolution-benchmark.test.ts b/tests/benchmarks/resolution/resolution-benchmark.test.ts index d85e4c9ec..79934481e 100644 --- a/tests/benchmarks/resolution/resolution-benchmark.test.ts +++ b/tests/benchmarks/resolution/resolution-benchmark.test.ts @@ -142,19 +142,6 @@ const THRESHOLDS: Record = { // dynamic-typescript: Phase 1 fixture — Reflect.apply/construct/get + TS decorators. // 3 expected edges (Reflect.apply → greet, Reflect.construct → UserService, Reflect.get → greet). 'dynamic-typescript': { precision: 1.0, recall: 0.75 }, - // dynamic-python: Phase 3 fixture — getattr('name') resolved; eval/exec/variable-getattr flagged. - 'dynamic-python': { precision: 1.0, recall: 0.75 }, - // Phase 4 scripting fixtures: Ruby send/public_send + PHP call_user_func/variable calls. - 'dynamic-ruby': { precision: 1.0, recall: 1.0 }, - 'dynamic-php': { precision: 1.0, recall: 1.0 }, - // Phase 6: Long-tail languages — C#, Swift, Elixir, Lua. - 'dynamic-csharp': { precision: 0.0, recall: 0.0 }, - 'dynamic-swift': { precision: 1.0, recall: 1.0 }, - 'dynamic-elixir': { precision: 0.0, recall: 0.0 }, - 'dynamic-lua': { precision: 1.0, recall: 1.0 }, - // Phase 5: Go + C/C++ dynamic dispatch patterns. - 'dynamic-go': { precision: 1.0, recall: 1.0 }, - 'dynamic-c': { precision: 1.0, recall: 1.0 }, // Phase 2 JVM fixtures — reflection and dynamic dispatch patterns. // Java/Scala/Groovy: detection works but method names are class-qualified in the DB, // so lookup-by-name fails for getMethod("foo") → 0% recall until RES-3 adds type-aware lookup. diff --git a/tests/engines/dynamic-call-ffi.test.ts b/tests/engines/dynamic-call-ffi.test.ts index c3431b0b4..ab31ff0d5 100644 --- a/tests/engines/dynamic-call-ffi.test.ts +++ b/tests/engines/dynamic-call-ffi.test.ts @@ -8,16 +8,7 @@ */ import { beforeAll, describe, expect, it } from 'vitest'; -import { - createParsers, - extractCSymbols, - extractGoSymbols, - extractPHPSymbols, - extractPythonSymbols, - extractRubySymbols, - extractSymbols, - getParser, -} from '../../src/domain/parser.js'; +import { createParsers, extractSymbols, getParser } from '../../src/domain/parser.js'; let parsers: Awaited>; @@ -194,251 +185,3 @@ describe('Phase 1: TypeScript decorator detection', () => { expect(c?.dynamicKind).toBe('reflection'); }); }); - -describe('Phase 3: Python dynamic dispatch detection', () => { - let parsers: Awaited>; - - beforeAll(async () => { - parsers = await createParsers(); - }, 30_000); - - function parsePy(code: string) { - const parser = getParser(parsers, 'test.py'); - if (!parser) throw new Error('Python parser not available'); - const tree = parser.parse(code); - return extractPythonSymbols(tree, 'test.py'); - } - - it('eval(code) tags as eval kind', () => { - const out = parsePy(` -def test(code): - eval(code) -`); - const c = out.calls.find((c) => c.name === ''); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('eval'); - expect(c?.dynamic).toBe(true); - }); - - it('exec(code) tags as eval kind', () => { - const out = parsePy(` -def test(code): - exec(code) -`); - const c = out.calls.find((c) => c.name === ''); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('eval'); - }); - - it("getattr(obj, 'method') with string literal tags as reflection", () => { - const out = parsePy(` -def test(obj): - getattr(obj, 'greet') -`); - const c = out.calls.find((c) => c.name === 'greet'); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('reflection'); - expect(c?.keyExpr).toContain('greet'); - expect(c?.dynamic).toBe(true); - }); - - it('getattr(obj, variable) with identifier key tags as computed-key', () => { - const out = parsePy(` -def test(obj, method_name): - getattr(obj, method_name) -`); - const c = out.calls.find((c) => c.name === ''); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('computed-key'); - expect(c?.keyExpr).toBe('method_name'); - }); - - it('functools.partial(fn, ...) extracts fn as reflection kind', () => { - const out = parsePy(` -import functools -def test(): - functools.partial(greet, 'world') -`); - const c = out.calls.find((c) => c.name === 'greet'); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('reflection'); - }); - - it('does not tag normal calls dynamically', () => { - const out = parsePy(` -def test(): - greet('world') -`); - const c = out.calls.find((c) => c.name === 'greet'); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBeUndefined(); - expect(c?.dynamic).toBeUndefined(); - }); -}); - -describe('Phase 4: Ruby dynamic dispatch detection', () => { - let parsers: Awaited>; - - beforeAll(async () => { - parsers = await createParsers(); - }, 30_000); - - function parseRb(code: string) { - const parser = getParser(parsers, 'test.rb'); - if (!parser) throw new Error('Ruby parser not available'); - const tree = parser.parse(code); - return extractRubySymbols(tree, 'test.rb'); - } - - it('obj.send(:greet) with symbol literal tags as reflection kind', () => { - const out = parseRb(` -def test(obj) - obj.send(:greet, 'world') -end -`); - const c = out.calls.find((c) => c.name === 'greet'); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('reflection'); - expect(c?.dynamic).toBe(true); - expect(c?.keyExpr).toContain('greet'); - }); - - it('public_send(:farewell) tags as reflection kind', () => { - const out = parseRb(` -def test(obj) - obj.public_send(:farewell, 'world') -end -`); - const c = out.calls.find((c) => c.name === 'farewell'); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('reflection'); - }); - - it('send(variable) with identifier key tags as computed-key', () => { - const out = parseRb(` -def test(obj, method_name) - obj.send(method_name, 'world') -end -`); - const c = out.calls.find((c) => c.name === ''); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('computed-key'); - expect(c?.keyExpr).toBe('method_name'); - }); -}); - -describe('Phase 4: PHP dynamic dispatch detection', () => { - let parsers: Awaited>; - - beforeAll(async () => { - parsers = await createParsers(); - }, 30_000); - - function parsePHP(code: string) { - const parser = getParser(parsers, 'test.php'); - if (!parser) throw new Error('PHP parser not available'); - const tree = parser.parse(code); - return extractPHPSymbols(tree, 'test.php'); - } - - it('$obj->$m() variable method name tags as unresolved-dynamic', () => { - const out = parsePHP(`$m('arg'); -} -`); - const c = out.calls.find((c) => c.name === ''); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('unresolved-dynamic'); - expect(c?.dynamic).toBe(true); - }); - - it('call_user_func($fn, ...) extracts fn as unresolved-dynamic', () => { - const out = parsePHP(` c.name === ''); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('unresolved-dynamic'); - expect(c?.dynamic).toBe(true); - }); - - it('$fn() variable function call tags as unresolved-dynamic', () => { - const out = parsePHP(` c.name === ''); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('unresolved-dynamic'); - }); -}); - -describe('Phase 5: Go MethodByName detection', () => { - let parsers: Awaited>; - beforeAll(async () => { - parsers = await createParsers(); - }, 30_000); - - function parseGo(code: string) { - const parser = getParser(parsers, 'test.go'); - if (!parser) throw new Error('Go parser not available'); - return extractGoSymbols(parser.parse(code), 'test.go'); - } - - it('v.MethodByName("Greet") extracts Greet as reflection kind', () => { - const out = parseGo(`package main -func test(v interface{}) { v.MethodByName("Greet") } -`); - const c = out.calls.find((c) => c.name === 'Greet'); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('reflection'); - expect(c?.dynamic).toBe(true); - }); - - it('v.MethodByName(name) with variable extracts as computed-key', () => { - const out = parseGo(`package main -func test(v interface{}, name string) { v.MethodByName(name) } -`); - const c = out.calls.find((c) => c.name === ''); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('computed-key'); - }); -}); - -describe('Phase 5: C function pointer / dlsym detection', () => { - let parsers: Awaited>; - beforeAll(async () => { - parsers = await createParsers(); - }, 30_000); - - function parseC(code: string) { - const parser = getParser(parsers, 'test.c'); - if (!parser) throw new Error('C parser not available'); - return extractCSymbols(parser.parse(code), 'test.c'); - } - - it('(*fp)(args) function pointer call tags as unresolved-dynamic', () => { - const out = parseC(` -void test(void (*fp)(int)) { (*fp)(42); } -`); - const c = out.calls.find((c) => c.name === ''); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('unresolved-dynamic'); - expect(c?.dynamic).toBe(true); - }); - - it('dlsym(handle, "greet") extracts greet as reflection kind', () => { - const out = parseC(` -#include -void test(void *handle) { dlsym(handle, "greet"); } -`); - const c = out.calls.find((c) => c.name === 'greet'); - expect(c).toBeDefined(); - expect(c?.dynamicKind).toBe('reflection'); - expect(c?.keyExpr).toContain('greet'); - }); -});