Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
35 changes: 29 additions & 6 deletions src/extractors/julia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,17 @@ function handleStructDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
}

function handleAbstractDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
const nameNode = node.childForFieldName('name') || findChild(node, 'identifier');
if (!nameNode) return;
// abstract_definition: `abstract type` type_head `end`
// The identifier is nested inside `type_head` — possibly wrapped in a
// `Name <: Super` binary_expression or a `Name{T,...}` parameterized form.
// Skip rather than emit a garbled name when no base identifier can be located.
let nameNode = node.childForFieldName('name') || findChild(node, 'identifier');
if (!nameNode) {
const typeHead = findChild(node, 'type_head');
if (!typeHead) return;
nameNode = findBaseName(typeHead);
if (!nameNode) return;
}
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 The legacy childForFieldName('name') || findChild(node, 'identifier') path is kept as the primary lookup before the type_head fallback. handleStructDef (which handles the identical type_head grammar shape) skips this step entirely and goes straight to findChild(node, 'type_head') + findBaseName. If a grammar revision ever makes childForFieldName('name') return the full type_head node rather than null, nameNode.text would emit the raw AbstractVector{T} <: AbstractArray{T,1} string and the parameterized-abstract-type test would silently break. Aligning with handleStructDef removes that fragility.

Suggested change
let nameNode = node.childForFieldName('name') || findChild(node, 'identifier');
if (!nameNode) {
const typeHead = findChild(node, 'type_head');
if (!typeHead) return;
nameNode = findBaseName(typeHead);
if (!nameNode) return;
}
const typeHead = findChild(node, 'type_head');
if (!typeHead) return;
const nameNode = findBaseName(typeHead);
if (!nameNode) return;

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 — dropped the legacy childForFieldName('name') lookup and aligned with handleStructDef's type_head + findBaseName pattern.


ctx.definitions.push({
name: nameNode.text,
Expand All @@ -285,10 +294,17 @@ function handleMacroDef(
ctx: ExtractorOutput,
currentModule: string | null,
): void {
const nameNode = node.childForFieldName('name') || findChild(node, 'identifier');
// macro_definition: `macro` signature/call_expression body `end`.
// The name lives in the same shape as a function signature — unwrap via
// signatureCall so we don't pick up an identifier from the body (e.g.
// `macro mymac(x) x end` would otherwise resolve to `@x`).
const callSig = signatureCall(node);
const nameNode =
callSig?.child(0) ?? node.childForFieldName('name') ?? findChild(node, 'identifier');
if (!nameNode) return;

const name = currentModule ? `${currentModule}.@${nameNode.text}` : `@${nameNode.text}`;
const base = nameNode.text;
const name = currentModule ? `${currentModule}.@${base}` : `@${base}`;
ctx.definitions.push({
name,
kind: 'function',
Expand Down Expand Up @@ -347,8 +363,15 @@ function handleImport(node: TreeSitterNode, ctx: ExtractorOutput): void {
function handleCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
// Don't record if parent is assignment LHS (that's a function definition)
if (node.parent?.type === 'assignment' && node === node.parent.child(0)) return;
// Don't record if parent is function_definition (that's a signature)
if (node.parent?.type === 'function_definition') return;
// Skip when this call is the signature of a function/macro definition.
// tree-sitter-julia wraps the signature in a `signature` node whose parent
// is `function_definition` or `macro_definition`. Body calls (e.g.
// `println(name)` inside `function greet ... end`) appear as descendants of
// the body, not as direct children of `signature`, so they are unaffected.
if (node.parent?.type === 'signature') {
const grand = node.parent.parent;
if (grand?.type === 'function_definition' || grand?.type === 'macro_definition') return;
}

const funcNode = node.child(0);
if (!funcNode) return;
Expand Down
39 changes: 39 additions & 0 deletions tests/parsers/julia.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,45 @@ end`);
expect(names).not.toContain('Foo.Base.show');
});

it('extracts abstract type', () => {
const symbols = parseJulia(`abstract type AbstractShape end`);
const abs = symbols.definitions.find((d) => d.name === 'AbstractShape');
expect(abs).toBeDefined();
expect(abs).toMatchObject({ kind: 'type' });
});

it('extracts parameterized abstract type base name', () => {
// Parameterized generics with a supertype must record only the base
// identifier — never the raw `Name{T} <: Super{T,1}` text.
const symbols = parseJulia(`abstract type AbstractVector{T} <: AbstractArray{T,1} end`);
const names = symbols.definitions.map((d) => d.name);
expect(names).toContain('AbstractVector');
expect(names.every((n) => !n.includes('{') && !n.includes('<'))).toBe(true);
});

it('extracts macro definitions with correct name', () => {
// `findChild(node, 'identifier')` would resolve to the body's `x` here,
// recording the macro as `@x` instead of `@mymac`.
const symbols = parseJulia(`macro mymac(x)
x
end`);
const names = symbols.definitions.map((d) => d.name);
expect(names).toContain('@mymac');
expect(names).not.toContain('@x');
});

it('does not record function signature as call', () => {
// The signature's `call_expression` lives inside a `signature` node — a
// naive `parent.type === 'function_definition'` guard misses it and
// records `greet` as both a definition and a call.
const symbols = parseJulia(`function greet(name)
println(name)
end`);
const callNames = symbols.calls.map((c) => c.name);
expect(callNames).not.toContain('greet');
expect(callNames).toContain('println');
});

it('selected_import handles qualified module', () => {
// `import Foo.Bar: baz` — module is a scoped_identifier. The import
// must record `Foo.Bar` as the source and `baz` as the imported name,
Expand Down
Loading