Skip to content

Commit 54e7d7b

Browse files
committed
dart: fall back to Dart-only name matching when no pubspec.yaml is found
package: URIs map to repository files through pubspec.yaml. When none is in the analyzed tree (e.g. only lib/ was analyzed), every import resolved to nothing while callers were still treated as scoped, so almost every cross-file edge was silently dropped. Keep library-private scoping, but match public names across all Dart files in that case and log a warning. Dart resolution stays authoritative either way, so there is still no cross-language guessing.
1 parent 3be8974 commit 54e7d7b

3 files changed

Lines changed: 53 additions & 24 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ uses [Semantic Versioning](https://semver.org/).
1717
field-type, instantiation and call edges. `package:`/relative imports,
1818
exports and `part` files decide which definition a name resolves to, and
1919
library-private names never resolve outside their library; names not
20-
visible through imports are dropped rather than guessed. Flutter-aware:
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:
2124
widgets and State classes are typed, instantiations in `build()` become
2225
widget-to-widget edges, `ref.watch`/`ref.read` link widgets to Riverpod
2326
providers (including `@riverpod` codegen), and GoRouter routes link to their

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

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def __init__(self):
6565
self._dart_directives: dict[str, list] = {}
6666
self._dart_visible: dict[str, set[str]] = {}
6767
self._dart_library_members: dict[str, set[str]] = {}
68+
self._dart_unscoped = False
6869
self.call_relationships: list[CallRelationship] = []
6970
self._python_project_modules: set = set()
7071
self._python_external_import_roots: set = set()
@@ -91,6 +92,7 @@ def analyze_code_files(self, code_files: list[dict], base_dir: str) -> dict:
9192
self._dart_directives = {}
9293
self._dart_visible = {}
9394
self._dart_library_members = {}
95+
self._dart_unscoped = False
9496

9597
files_analyzed = 0
9698
files_failed = 0
@@ -607,14 +609,21 @@ def _build_dart_scopes(self, base_dir: str) -> None:
607609
files = set(self._dart_directives)
608610
resolver = DartPackageResolver.from_files(base_dir, files)
609611
scopes = build_scopes(self._dart_directives, resolver, files)
610-
self._dart_visible = scopes.visible
611612
self._dart_library_members = scopes.library_members
612-
613-
def _has_dart_scope(self, caller: Node) -> bool:
614-
return Path(caller.relative_path).as_posix() in self._dart_visible
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+
)
615624

616625
def _dart_scope_candidates(
617-
self, func_ids: list[str], scope: set[str], caller_file: str | None = None
626+
self, func_ids: list[str], scope: set[str] | None, caller_file: str | None = None
618627
) -> list[str]:
619628
from codewiki.src.be.dependency_analyzer.analyzers.dart_imports import (
620629
is_private_dart_name,
@@ -627,7 +636,7 @@ def _dart_scope_candidates(
627636
if func_id in candidates or func is None:
628637
continue
629638
func_file = Path(func.relative_path).as_posix()
630-
if func_file not in scope:
639+
if scope is not None and func_file not in scope:
631640
continue
632641
# A `_`-prefixed segment in the target (`_Ticker.tick`) is private
633642
# to its library, whatever the callee's own spelling.
@@ -648,12 +657,15 @@ def _resolve_dart_callee(
648657
if not lang_indexes or "::" in callee:
649658
return None
650659
caller_file = Path(caller.relative_path).as_posix()
660+
scope: set[str] | None
651661
if is_private_dart_name(callee):
652662
scope = self._dart_library_members.get(caller_file, {caller_file})
663+
elif self._dart_unscoped:
664+
scope = None # any Dart file
653665
else:
654666
scope = self._dart_visible.get(caller_file)
655-
if not scope:
656-
return None
667+
if not scope:
668+
return None
657669
keys = [callee]
658670
if "." in callee:
659671
keys.append(callee.split(".")[0])
@@ -730,11 +742,11 @@ def _resolve_call_relationships(self):
730742
]
731743

732744
def _is_unresolvable_dart(self, relationship: CallRelationship) -> bool:
733-
"""A scoped Dart caller's callee that import-aware resolution rejected
734-
is external or invisible; drop it so ast_parser's name-only fallback
735-
cannot bind it to an unrelated component."""
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."""
736748
caller = self.functions.get(relationship.caller)
737-
return caller is not None and caller.language == "dart" and self._has_dart_scope(caller)
749+
return caller is not None and caller.language == "dart"
738750

739751
def _dotted_project_packages(self) -> dict[str, set]:
740752
"""Project packages/namespaces, partitioned by language. Java and C#
@@ -871,17 +883,9 @@ def _resolve_callee(
871883

872884
lang_indexes = indexes["by_lang"].get(caller_language) if caller_language else None
873885
if caller_language == "dart" and caller is not None:
874-
from codewiki.src.be.dependency_analyzer.analyzers.dart_imports import (
875-
is_private_dart_name,
876-
)
877-
878-
match = self._resolve_dart_callee(relationship.callee, caller, lang_indexes)
879-
if match:
880-
return match
881-
# Dart resolution is import-driven: a caller with a scope never
882-
# falls back to name guessing in other languages or libraries.
883-
if is_private_dart_name(relationship.callee) or self._has_dart_scope(caller):
884-
return 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)
885889
if lang_indexes:
886890
match = self._resolve_callee_in(
887891
relationship, lang_indexes["exact"], lang_indexes["simple"]

‎tests/test_dart_analyzer.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,28 @@ def test_top_level_function_wins_over_same_named_method(tmp_path: Path) -> None:
505505
assert components["lib/main.dart::go"].depends_on == {"lib/util.dart::helper"}
506506

507507

508+
def test_missing_pubspec_falls_back_to_dart_name_matching(tmp_path: Path, caplog) -> None:
509+
files = {
510+
"lib/util.dart": "class Engine {\n void start() {}\n}\n",
511+
"lib/main.dart": (
512+
"import 'package:app/util.dart';\n"
513+
"class Car {\n final Engine e = Engine();\n void go() { e.start(); Size(1, 2); }\n}\n"
514+
),
515+
"windows/runner/win32_window.h": "struct Size { int w; int h; };\n",
516+
}
517+
for relpath, source in files.items():
518+
path = tmp_path / relpath
519+
path.parent.mkdir(parents=True, exist_ok=True)
520+
path.write_text(source, encoding="utf-8")
521+
with caplog.at_level("WARNING"):
522+
components = DependencyParser(str(tmp_path)).parse_repository()
523+
assert "No pubspec.yaml" in caplog.text
524+
assert "lib/util.dart::Engine" in components["lib/main.dart::Car"].depends_on
525+
deps = components["lib/main.dart::Car.go"].depends_on
526+
assert "lib/util.dart::Engine.start" in deps
527+
assert not {d for d in deps if d.startswith("windows/")}
528+
529+
508530
def test_barrel_reexport_selects_exported_definition(tmp_path: Path) -> None:
509531
_write_repo(
510532
tmp_path,

0 commit comments

Comments
 (0)