diff --git a/crates/codegraph-core/src/extractors/gleam.rs b/crates/codegraph-core/src/extractors/gleam.rs index 879929c8..16990cda 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,39 @@ 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) — 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 + .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 a20ff994..45f8bd2b 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 c634fe5a..2d97e345 100644 --- a/tests/parsers/gleam.test.ts +++ b/tests/parsers/gleam.test.ts @@ -45,4 +45,27 @@ 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', () => { + // 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(); + }); });