diff --git a/src/extractors/julia.ts b/src/extractors/julia.ts index c81b4b71..7667ec95 100644 --- a/src/extractors/julia.ts +++ b/src/extractors/julia.ts @@ -282,7 +282,14 @@ function handleStructDef(node: TreeSitterNode, ctx: ExtractorOutput): void { } function handleAbstractDef(node: TreeSitterNode, ctx: ExtractorOutput): void { - const nameNode = node.childForFieldName('name') || findChild(node, 'identifier'); + // 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. + // Mirror handleStructDef and skip rather than emit a garbled name when no + // base identifier can be located. + const typeHead = findChild(node, 'type_head'); + if (!typeHead) return; + const nameNode = findBaseName(typeHead); if (!nameNode) return; ctx.definitions.push({ @@ -298,10 +305,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', @@ -360,8 +374,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; diff --git a/tests/parsers/julia.test.ts b/tests/parsers/julia.test.ts index bbef7504..dd85280f 100644 --- a/tests/parsers/julia.test.ts +++ b/tests/parsers/julia.test.ts @@ -113,6 +113,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,