From e402b87b73d30503c1915d72963191aca2a63227 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 (cherry picked from commit 2b965f23fc1470c678ed8cbdb9d6ef3e4a90fad0) --- 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 a066e5f3d..883954c2d 100644 --- a/graphify/extractors/dart.py +++ b/graphify/extractors/dart.py @@ -539,17 +539,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 e3b01ce2d..d54f4b6b6 100644 --- a/tests/test_dart.py +++ b/tests/test_dart.py @@ -737,5 +737,46 @@ class TestDart(unittest.TestCase): for nid in defined: self.assertFalse(nid.startswith(part_stem + "_"), nid) + 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()