@@ -10,6 +10,7 @@ import type { LanguageExtractor } from '../tree-sitter-types';
1010 * call_expression → `function foo(args...) end`
1111 * typed_expression → `function foo(x)::T end` (return type annotation on sig)
1212 * where_expression → `function foo(x::T) where T end`
13+ * field_expression → `function Base.getindex(x) end` (qualified name)
1314 */
1415function extractFunctionName ( signatureNode : SyntaxNode , source : string ) : string | null {
1516 // Unwrap the tree-sitter 'signature' wrapper node
@@ -18,7 +19,7 @@ function extractFunctionName(signatureNode: SyntaxNode, source: string): string
1819 if ( inner ) return extractFunctionName ( inner , source ) ;
1920 return getNodeText ( signatureNode , source ) ;
2021 }
21- if ( signatureNode . type === 'identifier' ) {
22+ if ( signatureNode . type === 'identifier' || signatureNode . type === 'field_expression' ) {
2223 return getNodeText ( signatureNode , source ) ;
2324 }
2425 if ( signatureNode . type === 'call_expression' ) {
@@ -83,7 +84,7 @@ function extractFunctionSignature(signatureNode: SyntaxNode, source: string): st
8384
8485/**
8586 * Extract the name from a Julia type_head node (used in struct/abstract definitions).
86- * type_head can be: identifier, call_expression (for parametric types ), binary_expression
87+ * type_head can be: identifier, parametrized_type_expression (Foo{T} ), binary_expression
8788 * (for subtype declarations like `Foo <: Bar`), etc.
8889 */
8990function extractTypeName ( typeHeadNode : SyntaxNode , source : string ) : string | null {
@@ -114,6 +115,51 @@ function extractTypeName(typeHeadNode: SyntaxNode, source: string): string | nul
114115 return getNodeText ( typeHeadNode , source ) ;
115116}
116117
118+ /**
119+ * Extract field name from a struct field node.
120+ * identifier → plain untyped field: `label`
121+ * typed_expression → typed field: `x::Float64`
122+ * assignment → field with default: `x::Int = 1` or `flag = false`
123+ */
124+ function extractFieldName ( node : SyntaxNode ) : string | null {
125+ if ( node . type === 'identifier' ) return node . text ;
126+ if ( node . type === 'typed_expression' ) return node . firstNamedChild ?. text ?? null ;
127+ if ( node . type === 'assignment' ) {
128+ const lhs = node . firstNamedChild ;
129+ if ( lhs ?. type === 'typed_expression' ) return lhs . firstNamedChild ?. text ?? null ;
130+ if ( lhs ?. type === 'identifier' ) return lhs . text ;
131+ }
132+ return null ;
133+ }
134+
135+ /**
136+ * Extract field type annotation from a struct field node.
137+ */
138+ function extractFieldType ( node : SyntaxNode , source : string ) : string | undefined {
139+ if ( node . type === 'typed_expression' ) {
140+ const typeNode = node . namedChild ( 1 ) ;
141+ return typeNode ? getNodeText ( typeNode , source ) . trim ( ) : undefined ;
142+ }
143+ if ( node . type === 'assignment' ) {
144+ const lhs = node . firstNamedChild ;
145+ if ( lhs ?. type === 'typed_expression' ) {
146+ const typeNode = lhs . namedChild ( 1 ) ;
147+ return typeNode ? getNodeText ( typeNode , source ) . trim ( ) : undefined ;
148+ }
149+ }
150+ return undefined ;
151+ }
152+
153+ /**
154+ * True when node is a direct struct body field (not the type_head).
155+ */
156+ function isStructField ( node : SyntaxNode ) : boolean {
157+ const parent = node . parent ;
158+ if ( ! parent ) return false ;
159+ // Fields live inside the block body of a struct_definition
160+ return parent . type === 'block' && parent . parent ?. type === 'struct_definition' ;
161+ }
162+
117163export const juliaExtractor : LanguageExtractor = {
118164 functionTypes : [ 'function_definition' , 'macro_definition' ] ,
119165 classTypes : [ ] ,
@@ -138,11 +184,9 @@ export const juliaExtractor: LanguageExtractor = {
138184 */
139185 getName : ( node , source ) => {
140186 if ( node . type === 'function_definition' || node . type === 'macro_definition' ) {
141- // signature is always the second named child after the 'function'/'macro' keyword
142187 for ( let i = 0 ; i < node . namedChildCount ; i ++ ) {
143188 const child = node . namedChild ( i ) ;
144189 if ( ! child ) continue ;
145- // The signature node is the first non-keyword named child
146190 if ( child . type !== 'block' ) {
147191 return extractFunctionName ( child , source ) ;
148192 }
@@ -151,7 +195,6 @@ export const juliaExtractor: LanguageExtractor = {
151195 }
152196
153197 if ( node . type === 'struct_definition' ) {
154- // Find type_head child
155198 for ( let i = 0 ; i < node . namedChildCount ; i ++ ) {
156199 const child = node . namedChild ( i ) ;
157200 if ( ! child ) continue ;
@@ -163,14 +206,13 @@ export const juliaExtractor: LanguageExtractor = {
163206 }
164207
165208 if ( node . type === 'abstract_definition' ) {
166- // abstract type <type_head> end — first named child is type_head
167209 const typeHead = node . namedChild ( 0 ) ;
168210 if ( typeHead ) return extractTypeName ( typeHead , source ) ;
169211 return null ;
170212 }
171213
172214 if ( node . type === 'module_definition' ) {
173- const nameNode = node . childForFieldName ( 'name' ) ;
215+ const nameNode = node . childForFieldName ( 'name' ) ?? node . namedChild ( 0 ) ;
174216 if ( nameNode ) return getNodeText ( nameNode , source ) ;
175217 return null ;
176218 }
@@ -203,15 +245,131 @@ export const juliaExtractor: LanguageExtractor = {
203245 return null ;
204246 } ,
205247
248+ /**
249+ * Custom visitor to handle:
250+ * 1. Short-form function definitions: `add(x, y) = x + y`
251+ * 2. `include("file.jl")` as relative file import
252+ * 3. Struct field declarations inside struct bodies
253+ * 4. `module_definition` as a namespace node
254+ */
255+ visitNode : ( node , ctx ) => {
256+ const source = ctx . source ;
257+
258+ // ── Struct fields ──────────────────────────────────────────────────────────
259+ // Extract typed and untyped fields from struct bodies.
260+ // `typed_expression` (x::Float64) and bare `identifier` (label) as direct
261+ // children of a struct block; `assignment` handles default values (@with_kw).
262+ if (
263+ ( node . type === 'typed_expression' || node . type === 'identifier' || node . type === 'assignment' ) &&
264+ isStructField ( node )
265+ ) {
266+ const fieldName = extractFieldName ( node ) ;
267+ if ( fieldName ) {
268+ const fieldType = extractFieldType ( node , source ) ;
269+ const sig = fieldType ? `${ fieldName } ::${ fieldType } ` : fieldName ;
270+ ctx . createNode ( 'field' , fieldName , node , { signature : sig } ) ;
271+ }
272+ return true ;
273+ }
274+
275+ // ── Short-form function definitions ────────────────────────────────────────
276+ // `add(x, y) = x + y` → LHS is call_expression
277+ // `f(x::T) where T = x` → LHS is where_expression wrapping call_expression
278+ if ( node . type === 'assignment' ) {
279+ const lhs = node . namedChild ( 0 ) ;
280+
281+ // Unwrap where_expression: `f(x::T) where T = ...`
282+ let callExpr = lhs ;
283+ let whereClause = '' ;
284+ if ( callExpr ?. type === 'where_expression' ) {
285+ const whereType = callExpr . namedChild ( 1 ) ;
286+ if ( whereType ) whereClause = ' where ' + getNodeText ( whereType , source ) ;
287+ callExpr = callExpr . namedChild ( 0 ) ?? null ;
288+ }
289+
290+ if ( callExpr ?. type === 'call_expression' ) {
291+ const nameNode = callExpr . namedChild ( 0 ) ;
292+ const funcName = nameNode ? getNodeText ( nameNode , source ) : null ;
293+ if ( ! funcName ) return false ; // malformed — let default dispatch walk children
294+ const argsNode = callExpr . namedChild ( 1 ) ;
295+ const sig = argsNode ? getNodeText ( argsNode , source ) + whereClause : undefined ;
296+ ctx . createNode ( 'function' , funcName , node , { signature : sig } ) ;
297+ // Visit RHS for calls
298+ const rhs = node . namedChild ( node . namedChildCount - 1 ) ;
299+ if ( rhs && rhs !== lhs ) ctx . visitNode ( rhs ) ;
300+ return true ;
301+ }
302+ // Plain assignment at top level (x = 42) — not extracted, but don't re-dispatch
303+ // its children as function/import/call candidates (they'll be visited anyway via
304+ // the default child-walk below returning false).
305+ return false ;
306+ }
307+
308+ // ── include("file.jl") as relative file import ─────────────────────────────
309+ // Julia uses include() for relative file composition, not import/using.
310+ if ( node . type === 'call_expression' ) {
311+ const callee = node . namedChild ( 0 ) ;
312+ if ( callee ?. type === 'identifier' && callee . text === 'include' ) {
313+ const args = node . namedChild ( 1 ) ;
314+ const strLit = args ?. namedChildren . find ( ( n ) => n . type === 'string_literal' ) ;
315+ const content = strLit ?. namedChildren . find ( ( n ) => n . type === 'content' ) ;
316+ const filePath = content ?. text ?. trim ( ) ;
317+ if ( filePath && filePath . length < 512 && ! filePath . includes ( '\0' ) ) {
318+ // Use the basename without extension as the module name (matches how
319+ // the file will be indexed). Emit an `imports` reference so the resolver
320+ // can wire up cross-file edges via suffix matching.
321+ const baseName = filePath . replace ( / \. j l $ / i, '' ) . replace ( / .* [ \\ / ] / , '' ) ;
322+ ctx . createNode ( 'import' , baseName , node , {
323+ signature : `include("${ filePath } ")` ,
324+ } ) ;
325+ const parentId = ctx . nodeStack . length > 0
326+ ? ctx . nodeStack [ ctx . nodeStack . length - 1 ]
327+ : undefined ;
328+ if ( parentId ) {
329+ ctx . addUnresolvedReference ( {
330+ fromNodeId : parentId ,
331+ referenceName : baseName ,
332+ referenceKind : 'imports' ,
333+ line : node . startPosition . row + 1 ,
334+ column : node . startPosition . column ,
335+ } ) ;
336+ }
337+ return true ;
338+ }
339+ }
340+ // Not include() — fall through to default call extraction
341+ return false ;
342+ }
343+
344+ // ── module_definition as namespace ─────────────────────────────────────────
345+ // Extract `module Foo ... end` as a 'module' kind (maps to NodeKind 'namespace').
346+ if ( node . type === 'module_definition' ) {
347+ const nameNode = node . childForFieldName ( 'name' ) ?? node . namedChild ( 0 ) ;
348+ if ( ! nameNode ) return false ;
349+ const modName = getNodeText ( nameNode , source ) ;
350+ const modNode = ctx . createNode ( 'namespace' , modName , node , { } ) ;
351+ if ( modNode ) {
352+ ctx . pushScope ( modNode . id ) ;
353+ // Visit all children inside the module body
354+ for ( let i = 0 ; i < node . namedChildCount ; i ++ ) {
355+ const child = node . namedChild ( i ) ;
356+ if ( child && child !== nameNode ) ctx . visitNode ( child ) ;
357+ }
358+ ctx . popScope ( ) ;
359+ }
360+ return true ;
361+ }
362+
363+ return false ;
364+ } ,
365+
206366 extractImport : ( node , source ) => {
207367 const importText = source . substring ( node . startIndex , node . endIndex ) . trim ( ) ;
208368
209- // Extract the module name from `import Foo` / `import Foo.Bar` / `using Foo`
210- // The first named child is typically an identifier or import_path or selected_import
211369 const firstChild = node . namedChild ( 0 ) ;
212370 if ( ! firstChild ) return { moduleName : importText , signature : importText } ;
213371
214- // selected_import: `using Foo: bar, baz` → module is `Foo`
372+ // selected_import: `using Foo: bar, baz` or `import Foo: bar` → module is `Foo`
215373 if ( firstChild . type === 'selected_import' ) {
216374 const pathNode = firstChild . namedChild ( 0 ) ;
217375 if ( pathNode ) {
0 commit comments