From 2b965f23fc1470c678ed8cbdb9d6ef3e4a90fad0 Mon Sep 17 00:00:00 2001 From: BrLumen Date: Fri, 2 Oct 2026 23:56:10 +0700 Subject: [PATCH] fix(dart): link every URI of a configurable import/export (#4002) A configurable directive such as export 'service_stub.dart' if (dart.library.io) 'service_io.dart'; was matched only up to its first quoted string, so the `if` alternatives were dropped. In the usual Flutter facade the stub is an empty placeholder and the real implementation lives in the conditional branch, which left the implementation unreachable from the facade and its importers. Parse the whole directive and emit one edge per URI: the default URI keeps its plain edge (unchanged), each alternative gets the same relation tagged context="conditional_uri" with its own source line. A string literal inside the condition (`if (app.flavor == 'prod')`) is part of the URI test and is not mistaken for a URI. Co-Authored-By: Claude Opus 5.5 --- graphify/extractors/dart.py | 33 +++++++++++++++++++---------- tests/test_dart.py | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/graphify/extractors/dart.py b/graphify/extractors/dart.py index a8b8031a3..ee7a43590 100644 --- a/graphify/extractors/dart.py +++ b/graphify/extractors/dart.py @@ -535,17 +535,28 @@ def extract_dart(path: Path) -> dict: line=_line_at(brace_pos + om.start(1))) # 6. Imports and Exports - for m in re.finditer(r"""^\s*import\s+['"]([^'"]+)['"]""", src_clean, re.MULTILINE): - pkg = m.group(1) - tgt_nid = _make_id(pkg) - add_node(tgt_nid, pkg, source_file=None) - add_edge(file_nid, tgt_nid, "imports", line=_line_at(m.start(1))) - - for m in re.finditer(r"""^\s*export\s+['"]([^'"]+)['"]""", src_clean, re.MULTILINE): - pkg = m.group(1) - tgt_nid = _make_id(pkg) - add_node(tgt_nid, pkg, source_file=None) - add_edge(file_nid, tgt_nid, "exports", line=_line_at(m.start(1))) + # A directive may carry configurable URIs that pick a platform-specific + # library: `export 'x_stub.dart' if (dart.library.io) 'x_io.dart';`. The + # default URI keeps its plain edge; every `if (...)` alternative is emitted + # too, tagged context="conditional_uri", because the real implementation + # usually lives there. A string literal inside the condition itself + # (`if (dart.library.io == 'true')`) is part of the test, not a URI. + quoted = r"""(?:'[^']*'|"[^"]*")""" + condition = rf"""if\s*\(\s*[\w.]+\s*(?:==\s*{quoted}\s*)?\)\s*""" + branch_re = re.compile(rf"""{condition}(['"])(?P[^'"]+)\1""") + for kind, relation in (("import", "imports"), ("export", "exports")): + directive_re = re.compile( + rf"""^\s*{kind}\s+(['"])(?P[^'"]+)\1(?P(?:\s*{condition}{quoted})*)""", + re.MULTILINE, + ) + for m in directive_re.finditer(src_clean): + uris: list[tuple[str, int, str | None]] = [(m.group("uri"), m.start("uri"), None)] + for bm in branch_re.finditer(m.group("branches")): + uris.append((bm.group("uri"), m.start("branches") + bm.start("uri"), "conditional_uri")) + for pkg, offset, context in uris: + tgt_nid = _make_id(pkg) + add_node(tgt_nid, pkg, source_file=None) + add_edge(file_nid, tgt_nid, relation, context=context, line=_line_at(offset)) # 7. Generic Invocations / Type Lookups (Universal Dependency Lookup) # Matches any method call with type parameters: methodName() or object.methodName() diff --git a/tests/test_dart.py b/tests/test_dart.py index c40ace25f..c21d569e1 100644 --- a/tests/test_dart.py +++ b/tests/test_dart.py @@ -696,5 +696,46 @@ class TestDart(unittest.TestCase): self.assertEqual(inherits["source_location"], "L3") + def test_conditional_uris_emit_every_branch(self): + """A configurable export/import must link every platform branch, not only + the default URI; a string literal inside the condition is not a URI.""" + code = textwrap.dedent("""\ + import 'conn_stub.dart' + if (dart.library.io) 'conn_io.dart' + if (dart.library.js_interop) 'conn_web.dart'; + import 'flags.dart' if (app.flavor == 'prod') 'flags_prod.dart'; + export 'service_stub.dart' if (dart.library.io) "service_io.dart"; + """) + path = self.temp_path / "facade.dart" + path.write_text(code, encoding="utf-8") + result = extract_dart(path) + file_nid = _make_id(str(path)) + + def targets(relation, context): + return { + e["target"] + for e in result["edges"] + if e["source"] == file_nid + and e["relation"] == relation + and e.get("context") == context + } + + self.assertEqual(targets("imports", None), {_make_id("conn_stub.dart"), _make_id("flags.dart")}) + self.assertEqual( + targets("imports", "conditional_uri"), + {_make_id("conn_io.dart"), _make_id("conn_web.dart"), _make_id("flags_prod.dart")}, + ) + self.assertEqual(targets("exports", None), {_make_id("service_stub.dart")}) + self.assertEqual(targets("exports", "conditional_uri"), {_make_id("service_io.dart")}) + labels = {n["label"] for n in result["nodes"]} + self.assertNotIn("prod", labels) + loc = { + e["target"]: e["source_location"] + for e in result["edges"] + if e["relation"] in ("imports", "exports") + } + self.assertEqual(loc[_make_id("conn_web.dart")], "L3") + self.assertEqual(loc[_make_id("service_io.dart")], "L5") + if __name__ == "__main__": unittest.main()