From 61abb4c16cb3a772f164a74ad62add3307716a1c Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Thu, 14 May 2026 17:28:36 -0600 Subject: [PATCH] fix(groovy): dispatch juxt_function_call in both engines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Groovy command-style calls (`foo bar(x)`, Gradle DSL `task someTask { ... }`, `apply plugin: 'java'`, etc.) parse as `juxt_function_call` nodes, which neither the JS source-of-truth extractor nor the Rust port was dispatching — so these call edges were silently dropped from the call graph in both engines. The juxt node has a `name` field with the same shape as `method_invocation`, so adding it to the existing dispatch in `walkGroovyNode` / `match_groovy_node` is sufficient — the existing call handler picks up the callee without further special-casing. Closes #1108 --- .../codegraph-core/src/extractors/groovy.rs | 28 +++++++++++++++---- src/extractors/groovy.ts | 1 + tests/parsers/groovy.test.ts | 16 +++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/crates/codegraph-core/src/extractors/groovy.rs b/crates/codegraph-core/src/extractors/groovy.rs index 3e0523fb0..9ef620216 100644 --- a/crates/codegraph-core/src/extractors/groovy.rs +++ b/crates/codegraph-core/src/extractors/groovy.rs @@ -23,9 +23,11 @@ use tree_sitter::{Node, Tree}; /// is only matched as a callee sub-node inside `handle_call_expr` when examining /// the `function`/`method` field of a call. /// -/// Note: `juxt_function_call` (Groovy command-style calls like `foo bar(x)`) -/// is not dispatched here — the JS extractor also omits it. Tracked in #1108 -/// for adding support to both engines. +/// `juxt_function_call` (Groovy command-style calls like `foo bar(x)` or the +/// Gradle DSL `task someTask { ... }`) is dispatched through `handle_call_expr`: +/// the grammar gives the juxt node a `name` field with the same shape as +/// `method_invocation`, so the existing handler picks up the callee without +/// special-casing. pub struct GroovyExtractor; impl SymbolExtractor for GroovyExtractor { @@ -59,9 +61,8 @@ fn match_groovy_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep "constructor_declaration" | "constructor_definition" => handle_constructor_decl(node, source, symbols), "function_definition" | "function_declaration" => handle_function_decl(node, source, symbols), "import_declaration" | "import_statement" => handle_import_decl(node, source, symbols), - "method_invocation" | "method_call" | "call_expression" | "function_call" => { - handle_call_expr(node, source, symbols) - } + "method_invocation" | "method_call" | "call_expression" | "function_call" + | "juxt_function_call" => handle_call_expr(node, source, symbols), "object_creation_expression" => handle_object_creation(node, source, symbols), _ => {} } @@ -499,6 +500,21 @@ mod tests { assert!(names.contains(&"GREEN")); } + #[test] + fn extracts_command_style_juxt_calls() { + // Gradle DSL pattern: `task`, `apply`, and `println` are invoked + // command-style without parens. The grammar emits these as + // `juxt_function_call` nodes; missing dispatch silently drops them + // from the call graph. + let s = parse_groovy( + "apply plugin: 'java'\ntask someTask {\n doLast {\n println \"hello\"\n }\n}", + ); + let names: Vec<&str> = s.calls.iter().map(|c| c.name.as_str()).collect(); + assert!(names.contains(&"apply"), "missing `apply` juxt call: {:?}", names); + assert!(names.contains(&"task"), "missing `task` juxt call: {:?}", names); + assert!(names.contains(&"println"), "missing `println` juxt call: {:?}", names); + } + #[test] fn extracts_superclass_and_interfaces() { let s = parse_groovy("class Sub extends Base implements I1, I2 {}"); diff --git a/src/extractors/groovy.ts b/src/extractors/groovy.ts index bfb62be15..64b695940 100644 --- a/src/extractors/groovy.ts +++ b/src/extractors/groovy.ts @@ -68,6 +68,7 @@ function walkGroovyNode(node: TreeSitterNode, ctx: ExtractorOutput): void { case 'method_invocation': case 'call_expression': case 'function_call': + case 'juxt_function_call': handleGroovyCallExpr(node, ctx); break; case 'object_creation_expression': diff --git a/tests/parsers/groovy.test.ts b/tests/parsers/groovy.test.ts index 3b03e3d79..60c86919a 100644 --- a/tests/parsers/groovy.test.ts +++ b/tests/parsers/groovy.test.ts @@ -58,4 +58,20 @@ describe('Groovy parser', () => { expect.objectContaining({ name: 'Color', kind: 'enum' }), ); }); + + it('extracts command-style (juxt) function calls', () => { + // Gradle DSL pattern: `task` and `apply` are invoked command-style without + // parens. The grammar emits these as `juxt_function_call` nodes; missing + // dispatch silently drops them from the call graph. + const symbols = parseGroovy(`apply plugin: 'java' +task someTask { + doLast { + println "hello" + } +}`); + const callNames = symbols.calls.map((c) => c.name); + expect(callNames).toContain('apply'); + expect(callNames).toContain('task'); + expect(callNames).toContain('println'); + }); });