@@ -62,6 +62,10 @@ class CallGraphAnalyzer:
6262 def __init__ (self ):
6363 """Initialize the call graph analyzer."""
6464 self .functions : dict [str , Node ] = {}
65+ self ._dart_directives : dict [str , list ] = {}
66+ self ._dart_visible : dict [str , set [str ]] = {}
67+ self ._dart_library_members : dict [str , set [str ]] = {}
68+ self ._dart_unscoped = False
6569 self .call_relationships : list [CallRelationship ] = []
6670 self ._python_project_modules : set = set ()
6771 self ._python_external_import_roots : set = set ()
@@ -85,6 +89,10 @@ def analyze_code_files(self, code_files: list[dict], base_dir: str) -> dict:
8589 code_files = self ._route_contextual_headers (code_files , base_dir )
8690 self ._python_project_modules = self ._collect_python_modules (code_files )
8791 self ._python_external_import_roots = set ()
92+ self ._dart_directives = {}
93+ self ._dart_visible = {}
94+ self ._dart_library_members = {}
95+ self ._dart_unscoped = False
8896
8997 files_analyzed = 0
9098 files_failed = 0
@@ -116,6 +124,9 @@ def analyze_code_files(self, code_files: list[dict], base_dir: str) -> dict:
116124 f"{ files_failed } failed, { len (self .functions )} functions, { len (self .call_relationships )} relationships ({ elapsed_time :.1f} s)"
117125 )
118126
127+ if self ._dart_directives :
128+ self ._build_dart_scopes (base_dir )
129+
119130 logger .debug ("Resolving call relationships" )
120131 self ._resolve_call_relationships ()
121132 self ._deduplicate_relationships ()
@@ -261,6 +272,8 @@ def _analyze_code_file(self, repo_dir: str, file_info: dict):
261272 self ._analyze_scala_file (file_path , content , repo_dir )
262273 elif language == "rust" :
263274 self ._analyze_rust_file (file_path , content , repo_dir )
275+ elif language == "dart" :
276+ self ._analyze_dart_file (file_path , content , repo_dir )
264277 # else:
265278 # logger.warning(
266279 # f"Unsupported language for call graph analysis: {language} for file {file_path}"
@@ -560,6 +573,136 @@ def _analyze_rust_file(self, file_path: str, content: str, repo_dir: str):
560573 except Exception :
561574 logger .exception (f"Failed to analyze Rust file { file_path } " )
562575
576+ def _analyze_dart_file (self , file_path : str , content : str , repo_dir : str ):
577+ """
578+ Analyze Dart file using tree-sitter based analyzer.
579+
580+ Besides components and relationships, records the file's
581+ import/export/part directives for Dart scope-aware resolution.
582+
583+ Args:
584+ file_path: Path to the Dart file
585+ content: File content string
586+ repo_dir: Repository base directory
587+ """
588+ from codewiki .src .be .dependency_analyzer .analyzers .dart import TreeSitterDartAnalyzer
589+
590+ try :
591+ analyzer = TreeSitterDartAnalyzer (str (file_path ), content , repo_path = repo_dir )
592+
593+ for func in analyzer .nodes :
594+ func_id = func .id if func .id else f"{ file_path } :{ func .name } "
595+ self .functions [func_id ] = func
596+
597+ self .call_relationships .extend (analyzer .call_relationships )
598+ self ._dart_directives [analyzer .relative_path ] = analyzer .directives
599+ except Exception :
600+ logger .exception (f"Failed to analyze Dart file { file_path } " )
601+
602+ def _build_dart_scopes (self , base_dir : str ) -> None :
603+ """Resolve recorded Dart directives into per-file visibility scopes."""
604+ from codewiki .src .be .dependency_analyzer .analyzers .dart_imports import (
605+ DartPackageResolver ,
606+ build_scopes ,
607+ )
608+
609+ files = set (self ._dart_directives )
610+ resolver = DartPackageResolver .from_files (base_dir , files )
611+ scopes = build_scopes (self ._dart_directives , resolver , files )
612+ self ._dart_library_members = scopes .library_members
613+ if resolver .packages :
614+ self ._dart_visible = scopes .visible
615+ return
616+ # Without a pubspec.yaml (e.g. only `lib/` was analyzed) `package:`
617+ # imports resolve to nothing, and scoping would drop nearly every
618+ # cross-file edge. Match names across all Dart files instead.
619+ self ._dart_unscoped = True
620+ logger .warning (
621+ "No pubspec.yaml found for the analyzed Dart files; `package:` imports "
622+ "cannot be resolved, so Dart calls are matched by name across Dart files."
623+ )
624+
625+ def _dart_scope_candidates (
626+ self , func_ids : list [str ], scope : set [str ] | None , caller_file : str | None = None
627+ ) -> list [str ]:
628+ from codewiki .src .be .dependency_analyzer .analyzers .dart_imports import (
629+ is_private_dart_name ,
630+ )
631+
632+ own_library = self ._dart_library_members .get (caller_file , set ()) if caller_file else set ()
633+ candidates : list [str ] = []
634+ for func_id in func_ids :
635+ func = self .functions .get (func_id )
636+ if func_id in candidates or func is None :
637+ continue
638+ func_file = Path (func .relative_path ).as_posix ()
639+ if scope is not None and func_file not in scope :
640+ continue
641+ # A `_`-prefixed segment in the target (`_Ticker.tick`) is private
642+ # to its library, whatever the callee's own spelling.
643+ if is_private_dart_name (func .name ) and func_file not in own_library :
644+ continue
645+ candidates .append (func_id )
646+ return candidates
647+
648+ def _resolve_dart_callee (
649+ self , callee : str , caller : Node , lang_indexes : dict | None
650+ ) -> str | None :
651+ """Prefer definitions the caller's library can see (imports, parts,
652+ re-exports); library-private names only resolve inside the library."""
653+ from codewiki .src .be .dependency_analyzer .analyzers .dart_imports import (
654+ is_private_dart_name ,
655+ )
656+
657+ if not lang_indexes or "::" in callee :
658+ return None
659+ caller_file = Path (caller .relative_path ).as_posix ()
660+ scope : set [str ] | None
661+ if is_private_dart_name (callee ):
662+ scope = self ._dart_library_members .get (caller_file , {caller_file })
663+ elif self ._dart_unscoped :
664+ scope = None # any Dart file
665+ else :
666+ scope = self ._dart_visible .get (caller_file )
667+ if not scope :
668+ return None
669+ keys = [callee ]
670+ if "." in callee :
671+ keys .append (callee .split ("." )[0 ])
672+ for key in keys :
673+ # Exact names first: an imported `helper()` must not turn ambiguous
674+ # because some visible class also has a `helper` method.
675+ for index in ("exact" , "simple" ):
676+ candidates = self ._dart_scope_candidates (
677+ lang_indexes [index ].get (key , []), scope , caller_file
678+ )
679+ if len (candidates ) == 1 :
680+ return candidates [0 ]
681+ if candidates :
682+ break
683+
684+ from codewiki .src .be .dependency_analyzer .analyzers .dart_flutter import (
685+ RIVERPOD_NOTIFIER_TYPE ,
686+ RIVERPOD_PROVIDER_TYPE ,
687+ provider_alias_candidates ,
688+ )
689+
690+ # riverpod_generator providers live in ignored *.g.dart files: map
691+ # `fooProvider` to the @riverpod declaration it was generated from.
692+ for alias in provider_alias_candidates (callee ):
693+ candidates = [
694+ func_id
695+ for func_id in self ._dart_scope_candidates (
696+ lang_indexes ["exact" ].get (alias , []), scope , caller_file
697+ )
698+ if self .functions [func_id ].node_type
699+ in (RIVERPOD_PROVIDER_TYPE , RIVERPOD_NOTIFIER_TYPE )
700+ and self .functions [func_id ].name == alias
701+ ]
702+ if len (candidates ) == 1 :
703+ return candidates [0 ]
704+ return None
705+
563706 def _resolve_call_relationships (self ):
564707 """
565708 Resolve function call relationships across all languages.
@@ -590,13 +733,21 @@ def _resolve_call_relationships(self):
590733 relationship
591734 for relationship in self .call_relationships
592735 if relationship .is_resolved
593- or not self ._is_external_callee (
736+ or not self ._is_unresolvable_dart (relationship )
737+ and not self ._is_external_callee (
594738 self ._caller_language (relationship .caller ),
595739 relationship .callee ,
596740 dotted_packages ,
597741 )
598742 ]
599743
744+ def _is_unresolvable_dart (self , relationship : CallRelationship ) -> bool :
745+ """A Dart caller's callee that Dart resolution rejected is external or
746+ invisible; drop it so ast_parser's name-only fallback cannot bind it
747+ to an unrelated component."""
748+ caller = self .functions .get (relationship .caller )
749+ return caller is not None and caller .language == "dart"
750+
600751 def _dotted_project_packages (self ) -> dict [str , set ]:
601752 """Project packages/namespaces, partitioned by language. Java and C#
602753 share the namespace-origin rule: a dotted callee qualified to a package
@@ -637,6 +788,16 @@ def _is_external_callee(
637788 return True
638789 if language == "python" and "." in callee :
639790 return self ._is_external_python_callee (callee )
791+ if language == "dart" :
792+ from codewiki .src .be .dependency_analyzer .analyzers .dart_imports import (
793+ is_private_dart_name ,
794+ )
795+
796+ # A library-private name that did not resolve inside its library
797+ # can never be a component elsewhere; dropping it keeps the
798+ # name-based fallback from binding another library's `_Body`.
799+ if is_private_dart_name (callee ):
800+ return True
640801 return False
641802
642803 def _is_external_python_callee (self , callee : str ) -> bool :
@@ -721,6 +882,10 @@ def _resolve_callee(
721882 caller_language = caller .language if caller else None
722883
723884 lang_indexes = indexes ["by_lang" ].get (caller_language ) if caller_language else None
885+ if caller_language == "dart" and caller is not None :
886+ # Dart resolution is authoritative: it never falls back to name
887+ # guessing in other languages or invisible libraries.
888+ return self ._resolve_dart_callee (relationship .callee , caller , lang_indexes )
724889 if lang_indexes :
725890 match = self ._resolve_callee_in (
726891 relationship , lang_indexes ["exact" ], lang_indexes ["simple" ]
@@ -860,6 +1025,8 @@ def _generate_visualization_data(self) -> dict:
8601025 node_classes .append ("lang-scala" )
8611026 elif file_ext == ".rs" :
8621027 node_classes .append ("lang-rust" )
1028+ elif file_ext == ".dart" :
1029+ node_classes .append ("lang-dart" )
8631030
8641031 cytoscape_elements .append (
8651032 {
0 commit comments