Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion crates/codegraph-core/src/extractors/gleam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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),
});
}

Expand Down Expand Up @@ -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());
}
}
3 changes: 3 additions & 0 deletions src/extractors/gleam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}

Expand Down
23 changes: 23 additions & 0 deletions tests/parsers/gleam.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Comment on lines +63 to +70
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test description doesn't match the code under test

The test is named 'omits children for external functions with type-only parameters' and its Rust counterpart has the comment // External function with type-only parameters (no names), yet both use random() — a function with zero parameters, not one with anonymous type-only params (e.g. random(Int, String)). These are structurally different tree nodes: an empty parameter list has no children at all, whereas random(Int, String) produces parameter nodes that simply lack a name field. The PR description explicitly calls out "Type-only external params (no name field) are correctly omitted", but that code path is never exercised by either test suite.

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7dde2c3 — both the JS and Rust tests now use random(Int, String) (type-only parameters with no name field) instead of random() (zero parameters), so the assertion that children is undefined actually exercises the documented edge case: parameter nodes that exist in the tree but get filtered out by extractParams/extract_params because they lack a name field.

});
Loading