From bd5a6826a4054266c7d8f93801f0812c0bca7304 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Thu, 14 May 2026 23:20:37 -0600 Subject: [PATCH 1/2] fix(julia): port abstract-def / macro-def / signature-call WASM bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WASM Julia extractor diverged from the native Rust extractor in three ways that no existing WASM fixture exercised: - handleAbstractDef: `findChild(node, 'identifier')` only looks at direct children of `abstract_definition`, but tree-sitter-julia nests the identifier inside `type_head`. Result: no abstract type was ever recorded. Fall back to `findBaseName(typeHead)` like the native code. - handleMacroDef: `findChild(node, 'identifier')` resolves to the body's first identifier rather than the macro name (e.g. `macro mymac(x) x end` recorded `@x` instead of `@mymac`). Unwrap via `signatureCall` to reach the call_expression name. - handleCall: the guard `parent.type === 'function_definition'` never matched — the signature's call_expression is parented by `signature`, whose own parent is the function/macro definition. Result: every long-form `function greet(...) ... end` recorded `greet` as both a definition and a call. Match the native walk: skip when parent is `signature` and grandparent is `function_definition` or `macro_definition`. Adds WASM tests mirroring the native cases: extracts_abstract_type, extracts_parameterized_abstract_type_base_name, extracts_macro_def, and does_not_record_function_signature_as_call. Closes #1126 --- src/extractors/julia.ts | 35 +++++++++++++++++++++++++++------ tests/parsers/julia.test.ts | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/extractors/julia.ts b/src/extractors/julia.ts index 804b357f..805ec5f4 100644 --- a/src/extractors/julia.ts +++ b/src/extractors/julia.ts @@ -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; + } ctx.definitions.push({ name: nameNode.text, @@ -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', @@ -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; diff --git a/tests/parsers/julia.test.ts b/tests/parsers/julia.test.ts index 5a95f5b0..b42f4bb0 100644 --- a/tests/parsers/julia.test.ts +++ b/tests/parsers/julia.test.ts @@ -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, From fa33f739eb027683a1c0745e1e5a5ce8fca7ba07 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 15 May 2026 02:12:52 -0600 Subject: [PATCH 2/2] fix(julia): align abstract-def name resolution with struct-def (#1130) --- src/extractors/julia.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/extractors/julia.ts b/src/extractors/julia.ts index af21a49d..7667ec95 100644 --- a/src/extractors/julia.ts +++ b/src/extractors/julia.ts @@ -285,14 +285,12 @@ function handleAbstractDef(node: TreeSitterNode, ctx: ExtractorOutput): void { // 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; - } + // 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({ name: nameNode.text,