@@ -145,7 +145,10 @@ export function extractSymbolsAndCalls(
145145 if ( langKey === "lua" ) {
146146 return extractFromLua ( source , relativePath , language , moduleSymbol ) ;
147147 }
148- // Dart, Svelte, Vue and others fall through to the regex fallback.
148+ if ( langKey === "dart" ) {
149+ return extractFromDart ( source , relativePath , language , moduleSymbol ) ;
150+ }
151+ // Svelte, Vue and others fall through to the regex fallback.
149152 return extractFromRegex ( source , relativePath , language , moduleSymbol ) ;
150153 } catch ( err ) {
151154 if ( ! symbolExtractionWarned . has ( langKey ) ) {
@@ -276,6 +279,216 @@ function extractFromLua(
276279 return { symbols, rawCalls } ;
277280}
278281
282+ // ── Dart (type-first signatures, sibling signature/body pairs, selector calls) ──
283+
284+ /**
285+ * Dart previously fell through to the regex fallback, which cannot match
286+ * type-first signatures (`void foo()`, `Future<int> baz() async`), so
287+ * classes, methods, and call sites were invisible to the symbol graph.
288+ * This walks the ast-grep Dart tree instead. Grammar quirks handled here:
289+ * class/mixin/enum/extension nodes span their bodies, but a function is a
290+ * `function_signature` followed by a SIBLING `function_body`, so scope
291+ * ranges are stitched from each pair; plain constructors live inside a
292+ * generic `declaration` wrapper; and there is no call_expression kind, so
293+ * calls are recovered from `argument_part` nodes (callee = the preceding
294+ * identifier or selector chain, or the `cascade_selector` for `..` calls).
295+ */
296+ function extractFromDart (
297+ source : string ,
298+ file : string ,
299+ language : string ,
300+ moduleSym : SymbolNode ,
301+ ) : ExtractedSymbols {
302+ const root = parse ( "dart" as unknown as Lang , source ) . root ( ) ;
303+ const symbols : SymbolNode [ ] = [ moduleSym ] ;
304+ const scopes : ScopeFrame [ ] = [ ] ;
305+
306+ // biome-ignore lint/suspicious/noExplicitAny: ast-grep node type leaks through
307+ const kidsOf = ( n : any ) : any [ ] => {
308+ try {
309+ return n . children ( ) ;
310+ } catch {
311+ return [ ] ;
312+ }
313+ } ;
314+ // biome-ignore lint/suspicious/noExplicitAny: ast-grep node type leaks through
315+ const childOfKind = ( n : any , kind : string ) : any | null =>
316+ // biome-ignore lint/suspicious/noExplicitAny: ast-grep node type leaks through
317+ kidsOf ( n ) . find ( ( c : any ) => c . kind ( ) === kind ) ?? null ;
318+ // Direct identifier children only — the name slot. Type annotations are
319+ // `type_identifier`/`void_type` and parameter names are nested deeper, so
320+ // they never appear here.
321+ // biome-ignore lint/suspicious/noExplicitAny: ast-grep node type leaks through
322+ const idChildren = ( n : any ) : any [ ] =>
323+ // biome-ignore lint/suspicious/noExplicitAny: ast-grep node type leaks through
324+ kidsOf ( n ) . filter ( ( c : any ) => c . kind ( ) === "identifier" ) ;
325+
326+ const addSym = (
327+ name : string ,
328+ qualifiedName : string ,
329+ kind : SymbolKind ,
330+ startLine : number ,
331+ endLine : number ,
332+ ) : void => {
333+ const sym : SymbolNode = {
334+ id : makeId ( file , qualifiedName , startLine ) ,
335+ name,
336+ qualifiedName,
337+ kind,
338+ file,
339+ line : startLine ,
340+ endLine,
341+ language,
342+ } ;
343+ symbols . push ( sym ) ;
344+ scopes . push ( { name : qualifiedName , startLine, endLine, symbolId : sym . id } ) ;
345+ } ;
346+
347+ // biome-ignore lint/suspicious/noExplicitAny: ast-grep node type leaks through
348+ const lineOf = ( n : any ) : number => n . range ( ) . start . line + 1 ;
349+ // biome-ignore lint/suspicious/noExplicitAny: ast-grep node type leaks through
350+ const endLineOf = ( n : any ) : number => n . range ( ) . end . line + 1 ;
351+
352+ /**
353+ * Emit the member symbols of a class-like body. Members come in ordered
354+ * sibling pairs: a `method_signature` (wrapping function/getter/setter/
355+ * factory signatures) or a `declaration` (fields and plain constructors),
356+ * optionally followed by its `function_body`.
357+ */
358+ // biome-ignore lint/suspicious/noExplicitAny: ast-grep node type leaks through
359+ const walkMembers = ( bodyNode : any , owner : string ) : void => {
360+ const members = kidsOf ( bodyNode ) ;
361+ for ( let i = 0 ; i < members . length ; i ++ ) {
362+ const member = members [ i ] ;
363+ const memberKind = member . kind ( ) ;
364+ const next = members [ i + 1 ] ;
365+ const scopeEnd = next && next . kind ( ) === "function_body" ? endLineOf ( next ) : endLineOf ( member ) ;
366+
367+ if ( memberKind === "method_signature" ) {
368+ const inner = kidsOf ( member ) [ 0 ] ;
369+ if ( ! inner ) continue ;
370+ const innerKind = inner . kind ( ) ;
371+ if ( innerKind === "factory_constructor_signature" ) {
372+ const ids = idChildren ( inner ) ;
373+ if ( ids . length === 0 ) continue ;
374+ // biome-ignore lint/suspicious/noExplicitAny: ast-grep node type leaks through
375+ const qn = ids . map ( ( c : any ) => c . text ( ) ) . join ( "." ) ;
376+ addSym ( ids [ ids . length - 1 ] . text ( ) , qn , "constructor" , lineOf ( member ) , scopeEnd ) ;
377+ } else if (
378+ innerKind === "function_signature" ||
379+ innerKind === "getter_signature" ||
380+ innerKind === "setter_signature"
381+ ) {
382+ const ids = idChildren ( inner ) ;
383+ if ( ids . length === 0 ) continue ;
384+ const name = ids [ ids . length - 1 ] . text ( ) ;
385+ addSym ( name , `${ owner } .${ name } ` , "method" , lineOf ( member ) , scopeEnd ) ;
386+ }
387+ } else if ( memberKind === "declaration" ) {
388+ // Plain (possibly named) constructors: `Foo(this.c);` / `Foo.named(...)`.
389+ // Field declarations have no constructor_signature child and are skipped.
390+ const ctor = childOfKind ( member , "constructor_signature" ) ;
391+ if ( ! ctor ) continue ;
392+ const ids = idChildren ( ctor ) ;
393+ if ( ids . length === 0 ) continue ;
394+ // biome-ignore lint/suspicious/noExplicitAny: ast-grep node type leaks through
395+ const qn = ids . map ( ( c : any ) => c . text ( ) ) . join ( "." ) ;
396+ addSym ( ids [ ids . length - 1 ] . text ( ) , qn , "constructor" , lineOf ( member ) , scopeEnd ) ;
397+ }
398+ }
399+ } ;
400+
401+ // ── Top-level declarations (ordered walk so signature/body pairs line up) ──
402+ // Dart 3.3 `extension type` is NOT handled: the vendored grammar
403+ // (@ast-grep/lang-dart 0.0.7) predates the syntax and parses it to ERROR
404+ // nodes (no extension_type_declaration kind exists), so such declarations
405+ // degrade to "not extracted" while the rest of the file extracts normally.
406+ // Revisit when the upstream grammar adds the kind.
407+ const topLevel = kidsOf ( root ) ;
408+ for ( let i = 0 ; i < topLevel . length ; i ++ ) {
409+ const node = topLevel [ i ] ;
410+ const nodeKind = node . kind ( ) ;
411+
412+ if ( nodeKind === "class_definition" || nodeKind === "mixin_declaration" || nodeKind === "extension_declaration" ) {
413+ const nameNode = childOfKind ( node , "identifier" ) ;
414+ if ( ! nameNode ) continue ;
415+ const name = nameNode . text ( ) ;
416+ const kind : SymbolKind = nodeKind === "mixin_declaration" ? "trait" : "class" ;
417+ addSym ( name , name , kind , lineOf ( node ) , endLineOf ( node ) ) ;
418+ const body = childOfKind ( node , "class_body" ) ?? childOfKind ( node , "extension_body" ) ;
419+ if ( body ) walkMembers ( body , name ) ;
420+ } else if ( nodeKind === "enum_declaration" ) {
421+ const nameNode = childOfKind ( node , "identifier" ) ;
422+ if ( nameNode ) addSym ( nameNode . text ( ) , nameNode . text ( ) , "enum" , lineOf ( node ) , endLineOf ( node ) ) ;
423+ } else if ( nodeKind === "type_alias" ) {
424+ const nameNode = childOfKind ( node , "type_identifier" ) ;
425+ if ( nameNode ) addSym ( nameNode . text ( ) , nameNode . text ( ) , "interface" , lineOf ( node ) , endLineOf ( node ) ) ;
426+ } else if ( nodeKind === "function_signature" || nodeKind === "getter_signature" || nodeKind === "setter_signature" ) {
427+ const ids = idChildren ( node ) ;
428+ if ( ids . length === 0 ) continue ;
429+ const name = ids [ ids . length - 1 ] . text ( ) ;
430+ const next = topLevel [ i + 1 ] ;
431+ const scopeEnd = next && next . kind ( ) === "function_body" ? endLineOf ( next ) : endLineOf ( node ) ;
432+ addSym ( name , name , "function" , lineOf ( node ) , scopeEnd ) ;
433+ }
434+ }
435+
436+ // ── Calls — every invocation wraps an `argument_part` node ──────────────
437+ const rawCalls : ExtractedSymbols [ "rawCalls" ] = [ ] ;
438+ for ( const ap of safeFindAll ( root , "argument_part" ) ) {
439+ const holder = ap . parent ( ) ;
440+ if ( ! holder ) continue ;
441+ const holderKind = holder . kind ( ) ;
442+ let callee : string | null = null ;
443+
444+ if ( holderKind === "cascade_section" ) {
445+ // `obj..method(args)` — the callee lives in the cascade_selector.
446+ const cs = childOfKind ( holder , "cascade_selector" ) ;
447+ const id = cs ? childOfKind ( cs , "identifier" ) : null ;
448+ callee = id ? id . text ( ) : null ;
449+ } else if ( holderKind === "selector" ) {
450+ // `name(args)` / `expr.name(args)` — the callee is the previous
451+ // sibling: a bare identifier, or a selector whose trailing identifier
452+ // is the method name (`f.bar(…)`, `mat.runApp(…)`, `Foo.create(…)`).
453+ const parent = holder . parent ( ) ;
454+ if ( ! parent ) continue ;
455+ const siblings = kidsOf ( parent ) ;
456+ const hr = holder . range ( ) ;
457+ const idx = siblings . findIndex (
458+ // biome-ignore lint/suspicious/noExplicitAny: ast-grep node type leaks through
459+ ( c : any ) => {
460+ if ( c . kind ( ) !== "selector" ) return false ;
461+ const r = c . range ( ) ;
462+ return (
463+ r . start . line === hr . start . line &&
464+ r . start . column === hr . start . column &&
465+ r . end . line === hr . end . line &&
466+ r . end . column === hr . end . column
467+ ) ;
468+ } ,
469+ ) ;
470+ if ( idx <= 0 ) continue ;
471+ const prev = siblings [ idx - 1 ] ;
472+ if ( prev . kind ( ) === "identifier" ) {
473+ callee = prev . text ( ) ;
474+ } else if ( prev . kind ( ) === "selector" ) {
475+ const ids = safeFindAll ( prev , "identifier" ) ;
476+ callee = ids . length > 0 ? ids [ ids . length - 1 ] . text ( ) : null ;
477+ }
478+ }
479+
480+ if ( ! callee ) continue ;
481+ const line = ap . range ( ) . start . line + 1 ;
482+ rawCalls . push ( {
483+ callerId : findCallerId ( scopes , line , moduleSym . id ) ,
484+ calleeName : callee ,
485+ callSite : { file, line } ,
486+ } ) ;
487+ }
488+
489+ return { symbols, rawCalls } ;
490+ }
491+
279492// ── JS / TS / TSX ────────────────────────────────────────────────────────
280493
281494function extractFromTsLike (
0 commit comments