Skip to content

Commit 966f4cd

Browse files
authored
Merge pull request #130 from majmohar4/main
Added flutter/dart support
2 parents f0975ea + 9d8c538 commit 966f4cd

20 files changed

Lines changed: 2443 additions & 8 deletions

‎CHANGELOG.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ uses [Semantic Versioning](https://semver.org/).
1212
impl/trait methods become components, with trait-impl, field-type,
1313
struct-literal, and call edges. `#[cfg(test)]` modules are skipped, and
1414
`build.rs` is classified as a build artifact.
15+
- **Dart / Flutter analyzer.** Classes, mixins, extensions, enums, top-level
16+
functions and getters, and methods become components, with inheritance,
17+
field-type, instantiation and call edges. `package:`/relative imports,
18+
exports and `part` files decide which definition a name resolves to, and
19+
library-private names never resolve outside their library; names not
20+
visible through imports are dropped rather than guessed. Without a
21+
`pubspec.yaml` in the analyzed tree, `package:` imports cannot be resolved,
22+
so Dart calls fall back to name matching across Dart files (with a
23+
warning). Flutter-aware:
24+
widgets and State classes are typed, instantiations in `build()` become
25+
widget-to-widget edges, `ref.watch`/`ref.read` link widgets to Riverpod
26+
providers (including `@riverpod` codegen), and GoRouter routes link to their
27+
screens. Generated `*.g.dart`/`*.freezed.dart` files and `.dart_tool/` are
28+
ignored, and Dart modules get Flutter-specific documentation guidance.
1529

1630
### Changed
1731

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ Full list in the [CHANGELOG](./CHANGELOG.md).
119119
## Features
120120

121121
**Languages.** Python, Java, JavaScript, TypeScript, C, C++, C#, Kotlin,
122-
PHP, Ruby, Scala, Rust.
122+
PHP, Ruby, Scala, Rust, Dart (Flutter-aware).
123123

124124
**Hierarchical decomposition.** The dependency graph is clustered into a
125125
module tree, recursively, so a 1.4M-line repository gets the same treatment

‎codewiki/cli/main.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def cli(ctx):
2020
2121
AI agents write the documentation from your dependency graph. Supports
2222
Python, Java, JavaScript, TypeScript, C, C++, C#, Kotlin, PHP, Ruby,
23-
Scala, and Rust.
23+
Scala, Rust, and Dart.
2424
"""
2525
# Ensure context object exists
2626
ctx.ensure_object(dict)

‎codewiki/cli/utils/repo_validator.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
".scala", # Scala
3535
".sc", # Scala Scripts
3636
".rs", # Rust
37+
".dart", # Dart
3738
}
3839

3940

@@ -65,7 +66,7 @@ def validate_repository(repo_path: Path) -> tuple[Path, list[tuple[str, int]]]:
6566
raise RepositoryError(
6667
f"No supported code files found in {repo_path}\n\n"
6768
"CodeWiki supports: Python, Java, JavaScript, TypeScript, C, C++, C#, Kotlin, PHP, "
68-
"Ruby, Scala, Rust\n\n"
69+
"Ruby, Scala, Rust, Dart\n\n"
6970
"Please navigate to a code repository or specify a custom directory:\n"
7071
" cd /path/to/your/project\n"
7172
" codewiki generate"

‎codewiki/cli/utils/validation.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ def detect_supported_languages(directory: Path) -> list[tuple[str, int]]:
168168
"Ruby": [".rb"],
169169
"Scala": [".scala", ".sc"],
170170
"Rust": [".rs"],
171+
"Dart": [".dart"],
171172
}
172173

173174
# Directories to exclude from counting

‎codewiki/mcp/tools/analysis.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ def _detect_via_mtime(
229229
".scala",
230230
".sc",
231231
".rs",
232+
".dart",
232233
}
233234

234235
changed: list[str] = []

‎codewiki/src/be/dependency_analyzer/analysis/analysis_service.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ def _filter_supported_languages(self, code_files: list[dict]) -> list[dict]:
340340
Filter code files to only include supported languages.
341341
342342
Supports Python, JavaScript, TypeScript, Java, C#, C, C++, PHP, Ruby, Kotlin,
343-
Scala, and Rust.
343+
Scala, Rust, and Dart.
344344
"""
345345
supported_languages = {
346346
"python",
@@ -356,6 +356,7 @@ def _filter_supported_languages(self, code_files: list[dict]) -> list[dict]:
356356
"rust",
357357
"kotlin",
358358
"scala",
359+
"dart",
359360
}
360361

361362
return [
@@ -379,6 +380,7 @@ def _get_supported_languages(self) -> list[str]:
379380
"kotlin",
380381
"scala",
381382
"rust",
383+
"dart",
382384
]
383385

384386
def _cleanup_repository(self, temp_dir: str):

‎codewiki/src/be/dependency_analyzer/analysis/call_graph_analyzer.py‎

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)