@@ -356,3 +356,187 @@ export const extractSignature = (
356356export const getBodyDelimiter = ( language : Language ) : string => {
357357 return BODY_DELIMITERS [ language ]
358358}
359+
360+ /**
361+ * Node types that represent import source/path by language
362+ */
363+ const IMPORT_SOURCE_NODE_TYPES : readonly string [ ] = [
364+ 'string' ,
365+ 'string_literal' ,
366+ 'interpreted_string_literal' , // Go
367+ 'source' , // Some grammars use this field name
368+ ]
369+
370+ /**
371+ * Extract the import source path from an import AST node
372+ *
373+ * Works for all supported languages by looking at the AST structure:
374+ * - JS/TS: import { foo } from 'source' -> string child
375+ * - Python: from source import foo -> 'module_name' field or dotted_name
376+ * - Rust: use crate::module::item -> scoped_identifier or path
377+ * - Go: import "source" -> interpreted_string_literal
378+ * - Java: import package.Class -> scoped_identifier
379+ *
380+ * @param node - The import AST node
381+ * @param language - The programming language
382+ * @returns The import source path, or null if not found
383+ */
384+ export const extractImportSource = (
385+ node : SyntaxNode ,
386+ language : Language ,
387+ ) : string | null => {
388+ // Try the 'source' field first (common in many grammars)
389+ const sourceField = node . childForFieldName ( 'source' )
390+ if ( sourceField ) {
391+ return stripQuotes ( sourceField . text )
392+ }
393+
394+ // Language-specific extraction
395+ switch ( language ) {
396+ case 'typescript' :
397+ case 'javascript' : {
398+ // Look for string literal child (the 'from "..."' part)
399+ for ( const child of node . children ) {
400+ if ( child . type === 'string' ) {
401+ return stripQuotes ( child . text )
402+ }
403+ }
404+ break
405+ }
406+
407+ case 'python' : {
408+ // For 'from X import Y', look for module_name field or dotted_name
409+ const moduleNameField = node . childForFieldName ( 'module_name' )
410+ if ( moduleNameField ) {
411+ return moduleNameField . text
412+ }
413+ // For 'import X' style
414+ const nameField = node . childForFieldName ( 'name' )
415+ if ( nameField ) {
416+ return nameField . text
417+ }
418+ // Fallback: look for dotted_name
419+ for ( const child of node . children ) {
420+ if ( child . type === 'dotted_name' ) {
421+ return child . text
422+ }
423+ }
424+ break
425+ }
426+
427+ case 'rust' : {
428+ // For 'use path::to::item', extract the path
429+ // Look for scoped_identifier, use_wildcard, use_list, or identifier
430+ const argumentField = node . childForFieldName ( 'argument' )
431+ if ( argumentField ) {
432+ // Get the path part (everything except the last segment if it's a use_list)
433+ return extractRustUsePath ( argumentField )
434+ }
435+ // Fallback: look for children that could be paths
436+ for ( const child of node . children ) {
437+ if (
438+ child . type === 'scoped_identifier' ||
439+ child . type === 'identifier' ||
440+ child . type === 'use_wildcard'
441+ ) {
442+ return extractRustUsePath ( child )
443+ }
444+ }
445+ break
446+ }
447+
448+ case 'go' : {
449+ // For 'import "path"', look for import_spec or interpreted_string_literal
450+ for ( const child of node . children ) {
451+ // Single import: import "fmt" -> has import_spec child
452+ if ( child . type === 'import_spec' ) {
453+ const pathNode = child . childForFieldName ( 'path' )
454+ if ( pathNode ) {
455+ return stripQuotes ( pathNode . text )
456+ }
457+ // Fallback: look for string literal in import_spec
458+ for ( const specChild of child . children ) {
459+ if ( specChild . type === 'interpreted_string_literal' ) {
460+ return stripQuotes ( specChild . text )
461+ }
462+ }
463+ }
464+ // Direct string literal (some Go grammars)
465+ if ( child . type === 'interpreted_string_literal' ) {
466+ return stripQuotes ( child . text )
467+ }
468+ // For import blocks: import ( "fmt" "os" )
469+ if ( child . type === 'import_spec_list' ) {
470+ for ( const spec of child . children ) {
471+ if ( spec . type === 'import_spec' ) {
472+ const pathNode = spec . childForFieldName ( 'path' )
473+ if ( pathNode ) {
474+ return stripQuotes ( pathNode . text )
475+ }
476+ }
477+ }
478+ }
479+ }
480+ break
481+ }
482+
483+ case 'java' : {
484+ // For 'import package.Class', look for scoped_identifier
485+ for ( const child of node . children ) {
486+ if ( child . type === 'scoped_identifier' ) {
487+ return child . text
488+ }
489+ }
490+ break
491+ }
492+ }
493+
494+ // Fallback: look for any string-like child
495+ for ( const child of node . children ) {
496+ if ( IMPORT_SOURCE_NODE_TYPES . includes ( child . type ) ) {
497+ return stripQuotes ( child . text )
498+ }
499+ }
500+
501+ return null
502+ }
503+
504+ /**
505+ * Extract the path from a Rust use declaration
506+ * For 'std::collections::HashMap', returns 'std::collections::HashMap'
507+ * For 'std::collections::{HashMap, HashSet}', returns 'std::collections'
508+ */
509+ const extractRustUsePath = ( node : SyntaxNode ) : string => {
510+ // If it's a use_list (e.g., {HashMap, HashSet}), get the parent path
511+ if ( node . type === 'use_list' ) {
512+ return ''
513+ }
514+
515+ // For scoped_identifier, check if the last part is a use_list
516+ if ( node . type === 'scoped_identifier' ) {
517+ const lastChild = node . children [ node . children . length - 1 ]
518+ if ( lastChild ?. type === 'use_list' ) {
519+ // Return everything except the use_list
520+ const pathChild = node . childForFieldName ( 'path' )
521+ if ( pathChild ) {
522+ return pathChild . text
523+ }
524+ }
525+ }
526+
527+ return node . text
528+ }
529+
530+ /**
531+ * Strip surrounding quotes from a string
532+ */
533+ const stripQuotes = ( str : string ) : string => {
534+ if (
535+ ( str . startsWith ( '"' ) && str . endsWith ( '"' ) ) ||
536+ ( str . startsWith ( "'" ) && str . endsWith ( "'" ) ) ||
537+ ( str . startsWith ( '`' ) && str . endsWith ( '`' ) )
538+ ) {
539+ return str . slice ( 1 , - 1 )
540+ }
541+ return str
542+ }
0 commit comments