Skip to content

Commit f06a213

Browse files
authored
fix(gleam): extract parameters for external functions (#1127)
* 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 * 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.
1 parent 031ab81 commit f06a213

3 files changed

Lines changed: 64 additions & 1 deletion

File tree

crates/codegraph-core/src/extractors/gleam.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ fn handle_external_function(node: &Node, source: &[u8], symbols: &mut FileSymbol
6161
None => return,
6262
};
6363

64+
let params = extract_params(node, source);
65+
6466
symbols.definitions.push(Definition {
6567
name: node_text(&name_node, source).to_string(),
6668
kind: "function".to_string(),
@@ -69,7 +71,7 @@ fn handle_external_function(node: &Node, source: &[u8], symbols: &mut FileSymbol
6971
decorators: None,
7072
complexity: None,
7173
cfg: None,
72-
children: None,
74+
children: opt_children(params),
7375
});
7476
}
7577

@@ -443,4 +445,39 @@ mod tests {
443445
.expect("expected constant");
444446
assert_eq!(c.kind, "variable");
445447
}
448+
449+
#[test]
450+
fn extracts_external_function_with_named_parameters() {
451+
let code = "pub external fn parse(input: String, base: Int) -> Int = \"erlang_mod\" \"parse\"\n";
452+
let s = parse_gleam(code);
453+
let parse_fn = s
454+
.definitions
455+
.iter()
456+
.find(|d| d.name == "parse")
457+
.expect("expected external function `parse`");
458+
assert_eq!(parse_fn.kind, "function");
459+
let children = parse_fn
460+
.children
461+
.as_ref()
462+
.expect("expected external function parameters as children");
463+
let names: Vec<&str> = children.iter().map(|c| c.name.as_str()).collect();
464+
assert!(names.contains(&"input"), "missing `input` param, got {names:?}");
465+
assert!(names.contains(&"base"), "missing `base` param, got {names:?}");
466+
assert!(children.iter().all(|c| c.kind == "parameter"));
467+
}
468+
469+
#[test]
470+
fn external_function_without_param_names_has_no_children() {
471+
// External function with type-only parameters (no names) — the tree-sitter
472+
// grammar still produces parameter nodes, but they lack a `name` field, so
473+
// `extract_params` returns an empty Vec and `children` is None.
474+
let code = "pub external fn random(Int, String) -> Int = \"rand\" \"uniform\"\n";
475+
let s = parse_gleam(code);
476+
let random_fn = s
477+
.definitions
478+
.iter()
479+
.find(|d| d.name == "random")
480+
.expect("expected external function `random`");
481+
assert!(random_fn.children.is_none());
482+
}
446483
}

src/extractors/gleam.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,15 @@ function handleExternalFunction(node: TreeSitterNode, ctx: ExtractorOutput): voi
8585
const nameNode = node.childForFieldName('name') || findChild(node, 'identifier');
8686
if (!nameNode) return;
8787

88+
const params = extractParams(node);
89+
8890
ctx.definitions.push({
8991
name: nameNode.text,
9092
kind: 'function',
9193
line: node.startPosition.row + 1,
9294
endLine: nodeEndLine(node),
9395
visibility: isPublic(node) ? 'public' : 'private',
96+
children: params.length > 0 ? params : undefined,
9497
});
9598
}
9699

tests/parsers/gleam.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,27 @@ import gleam/string`);
4545
}`);
4646
expect(symbols.calls.length).toBeGreaterThanOrEqual(1);
4747
});
48+
49+
it('extracts external function parameters as children', () => {
50+
const symbols = parseGleam(
51+
`pub external fn parse(input: String, base: Int) -> Int = "erlang_mod" "parse"`,
52+
);
53+
const parseFn = symbols.definitions.find((d) => d.name === 'parse');
54+
expect(parseFn).toBeDefined();
55+
expect(parseFn?.kind).toBe('function');
56+
expect(parseFn?.children).toBeDefined();
57+
const names = parseFn?.children?.map((c) => c.name) ?? [];
58+
expect(names).toContain('input');
59+
expect(names).toContain('base');
60+
expect(parseFn?.children?.every((c) => c.kind === 'parameter')).toBe(true);
61+
});
62+
63+
it('omits children for external functions with type-only parameters', () => {
64+
// Type-only params: parameter nodes exist in the tree but lack a `name` field,
65+
// so extractParams returns an empty list and `children` is omitted.
66+
const symbols = parseGleam(`pub external fn random(Int, String) -> Int = "rand" "uniform"`);
67+
const randomFn = symbols.definitions.find((d) => d.name === 'random');
68+
expect(randomFn).toBeDefined();
69+
expect(randomFn?.children).toBeUndefined();
70+
});
4871
});

0 commit comments

Comments
 (0)