From c7d5535c1640a375a988a6ee6a7c22539740a8bd Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Thu, 14 May 2026 18:29:25 -0600 Subject: [PATCH 1/2] fix(gleam): extract parameters for external functions Mirror regular `function` behavior so `external_function` definitions expose their parameter list as children in both native and WASM engines. Previously external functions silently dropped parameter children even though the grammar exposes them via the same `parameters` field. Closes #1110 --- crates/codegraph-core/src/extractors/gleam.rs | 37 ++++++++++++++++++- src/extractors/gleam.ts | 3 ++ tests/parsers/gleam.test.ts | 21 +++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/crates/codegraph-core/src/extractors/gleam.rs b/crates/codegraph-core/src/extractors/gleam.rs index 879929c8a..e1dc706f3 100644 --- a/crates/codegraph-core/src/extractors/gleam.rs +++ b/crates/codegraph-core/src/extractors/gleam.rs @@ -61,6 +61,8 @@ fn handle_external_function(node: &Node, source: &[u8], symbols: &mut FileSymbol None => return, }; + let params = extract_params(node, source); + symbols.definitions.push(Definition { name: node_text(&name_node, source).to_string(), kind: "function".to_string(), @@ -69,7 +71,7 @@ fn handle_external_function(node: &Node, source: &[u8], symbols: &mut FileSymbol decorators: None, complexity: None, cfg: None, - children: None, + children: opt_children(params), }); } @@ -443,4 +445,37 @@ mod tests { .expect("expected constant"); assert_eq!(c.kind, "variable"); } + + #[test] + fn extracts_external_function_with_named_parameters() { + let code = "pub external fn parse(input: String, base: Int) -> Int = \"erlang_mod\" \"parse\"\n"; + let s = parse_gleam(code); + let parse_fn = s + .definitions + .iter() + .find(|d| d.name == "parse") + .expect("expected external function `parse`"); + assert_eq!(parse_fn.kind, "function"); + let children = parse_fn + .children + .as_ref() + .expect("expected external function parameters as children"); + let names: Vec<&str> = children.iter().map(|c| c.name.as_str()).collect(); + assert!(names.contains(&"input"), "missing `input` param, got {names:?}"); + assert!(names.contains(&"base"), "missing `base` param, got {names:?}"); + assert!(children.iter().all(|c| c.kind == "parameter")); + } + + #[test] + fn external_function_without_param_names_has_no_children() { + // External function with type-only parameters (no names) — should not emit children. + let code = "pub external fn random() -> Int = \"rand\" \"uniform\"\n"; + let s = parse_gleam(code); + let random_fn = s + .definitions + .iter() + .find(|d| d.name == "random") + .expect("expected external function `random`"); + assert!(random_fn.children.is_none()); + } } diff --git a/src/extractors/gleam.ts b/src/extractors/gleam.ts index a20ff994b..45f8bd2b8 100644 --- a/src/extractors/gleam.ts +++ b/src/extractors/gleam.ts @@ -85,12 +85,15 @@ function handleExternalFunction(node: TreeSitterNode, ctx: ExtractorOutput): voi const nameNode = node.childForFieldName('name') || findChild(node, 'identifier'); if (!nameNode) return; + const params = extractParams(node); + ctx.definitions.push({ name: nameNode.text, kind: 'function', line: node.startPosition.row + 1, endLine: nodeEndLine(node), visibility: isPublic(node) ? 'public' : 'private', + children: params.length > 0 ? params : undefined, }); } diff --git a/tests/parsers/gleam.test.ts b/tests/parsers/gleam.test.ts index c634fe5aa..f43845c7e 100644 --- a/tests/parsers/gleam.test.ts +++ b/tests/parsers/gleam.test.ts @@ -45,4 +45,25 @@ import gleam/string`); }`); expect(symbols.calls.length).toBeGreaterThanOrEqual(1); }); + + it('extracts external function parameters as children', () => { + const symbols = parseGleam( + `pub external fn parse(input: String, base: Int) -> Int = "erlang_mod" "parse"`, + ); + const parseFn = symbols.definitions.find((d) => d.name === 'parse'); + expect(parseFn).toBeDefined(); + expect(parseFn?.kind).toBe('function'); + expect(parseFn?.children).toBeDefined(); + const names = parseFn?.children?.map((c) => c.name) ?? []; + expect(names).toContain('input'); + expect(names).toContain('base'); + expect(parseFn?.children?.every((c) => c.kind === 'parameter')).toBe(true); + }); + + it('omits children for external functions with type-only parameters', () => { + const symbols = parseGleam(`pub external fn random() -> Int = "rand" "uniform"`); + const randomFn = symbols.definitions.find((d) => d.name === 'random'); + expect(randomFn).toBeDefined(); + expect(randomFn?.children).toBeUndefined(); + }); }); From 7dde2c3547a3cf59d97921cfd4597410cc36d02d Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Thu, 14 May 2026 23:14:27 -0600 Subject: [PATCH 2/2] test(gleam): exercise type-only external params with named-less args (#1127) Replace random() (zero parameters) with random(Int, String) (type-only parameters) so the test actually covers the documented edge case: parameter nodes that exist in the tree but lack a name field. The old case asserted the same observable outcome (no children) but exercised the empty-parameter-list path, not the type-only path. Applies to both Rust and JS engines. --- crates/codegraph-core/src/extractors/gleam.rs | 6 ++++-- tests/parsers/gleam.test.ts | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/codegraph-core/src/extractors/gleam.rs b/crates/codegraph-core/src/extractors/gleam.rs index e1dc706f3..16990cdab 100644 --- a/crates/codegraph-core/src/extractors/gleam.rs +++ b/crates/codegraph-core/src/extractors/gleam.rs @@ -468,8 +468,10 @@ mod tests { #[test] fn external_function_without_param_names_has_no_children() { - // External function with type-only parameters (no names) — should not emit children. - let code = "pub external fn random() -> Int = \"rand\" \"uniform\"\n"; + // External function with type-only parameters (no names) — the tree-sitter + // grammar still produces parameter nodes, but they lack a `name` field, so + // `extract_params` returns an empty Vec and `children` is None. + let code = "pub external fn random(Int, String) -> Int = \"rand\" \"uniform\"\n"; let s = parse_gleam(code); let random_fn = s .definitions diff --git a/tests/parsers/gleam.test.ts b/tests/parsers/gleam.test.ts index f43845c7e..2d97e3450 100644 --- a/tests/parsers/gleam.test.ts +++ b/tests/parsers/gleam.test.ts @@ -61,7 +61,9 @@ import gleam/string`); }); it('omits children for external functions with type-only parameters', () => { - const symbols = parseGleam(`pub external fn random() -> Int = "rand" "uniform"`); + // Type-only params: parameter nodes exist in the tree but lack a `name` field, + // so extractParams returns an empty list and `children` is omitted. + const symbols = parseGleam(`pub external fn random(Int, String) -> Int = "rand" "uniform"`); const randomFn = symbols.definitions.find((d) => d.name === 'random'); expect(randomFn).toBeDefined(); expect(randomFn?.children).toBeUndefined();