Repository navigation
Dart: calls to private members across part files are dropped since 1.6 #2466
Description
Summary
Since 1.6, a Dart call to a private (_-prefixed) member defined in another file of the same library — the library file and its part files — no longer produces a calls edge. Dart privacy is per library, not per file, so these calls are legal and common: any library split into part files calls its own private helpers across those files. They disappear from callers / impact.
On a ~1,400-file Flutter app that splits its large libraries into part files, cross-file calls edges onto Dart private members went from 552 on 1.5.0 to 9 on 1.6.2. The overall edge count only dropped ~4% (1.6 also removed many wrong name-matched edges), so the loss is easy to miss.
Reproduction
// lib/greeter.dart
part 'greeter_format.dart';
part 'greeter_shout.dart';
part 'src/_greeter_whisper.dart';
class Greeter with _Formatting, _Shouting, _Whispering {
String greet(String name) => _format(name); // library file -> private member in a part
String hello() => _local(); // same-file control
String _local() => 'hi';
}
void _trace(String s) => print(s);// lib/greeter_format.dart
part of 'greeter.dart';
mixin _Formatting {
String _format(String name) {
_trace(name); // part -> private top-level function in the library file
return 'Hello, $name';
}
}// lib/greeter_shout.dart
part of 'greeter.dart';
mixin _Shouting on _Formatting {
String shout(String name) => _format(name).toUpperCase(); // part -> sibling part
}// lib/src/_greeter_whisper.dart
part of '../greeter.dart';
mixin _Whispering on _Formatting {
String whisper(String name) => _format(name).toLowerCase(); // part in a subdirectory -> sibling part
}npx @colbymchenry/codegraph@1.6.2 init -y .
sqlite3 .codegraph/codegraph.db "SELECT s.qualified_name, t.qualified_name FROM edges e
JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target WHERE e.kind = 'calls'"| Build | calls edges |
|---|---|
| 1.6.2 | 1 of 5: only the same-file control Greeter::hello -> Greeter::_local |
| 1.6.2 + library-scoped privacy (below) | 5 of 5 |
Root cause
isVisibleAcrossFiles (src/resolution/name-matcher.ts) puts dart in PRIVATE_IS_FILE_LOCAL:
if (PRIVATE_IS_FILE_LOCAL.has(lang)) return candidate.visibility !== 'private';and the Dart extractor marks every _-prefixed name private (src/extraction/languages/dart.ts). That holds for Kotlin / Java / C#, but in Dart a private name is visible across the whole library: the defining file plus every file its part directives name. It is the same shape as #2164 (the file-local rule dropping legal calls), here for Dart.
Still present on main @ b635dd467 (line 2805).
Suggested fix
main already has inSameDartLibrary (src/resolution/dart-libraries.ts, from #2391), so this may be a single line ahead of the PRIVATE_IS_FILE_LOCAL check:
if (lang === 'dart' && candidate.visibility === 'private')
return inSameDartLibrary(ref.filePath, candidate.filePath, context);I have not built main. I verified the equivalent change on 1.6.2: since 1.6.2 has no inSameDartLibrary, the check there resolves each file's part of target and compares libraries. Results:
- The reproduction above: 5 of 5 edges.
- The 1,400-file app: cross-file private
callsedges went from 9 to 482. Compared with stock 1.6.2, 477 edges were added and 4 removed. A sample of the added edges were all real calls between parts of the same library; the 4 removed were wrong matches across two test files.
Activity
maxmilian commentedon Oct 9, 2026
I'd like to take this. Plan: in
isVisibleAcrossFiles, check a Dartprivatecandidate with the existinginSameDartLibrary(library file + its parts) before thePRIVATE_IS_FILE_LOCALrule, so privacy is library-scoped instead of file-scoped. Regression test is your four-file repro in__tests__/dart-part-directives.test.ts(1 of 5 edges on main, 5 of 5 with the fix) plus a file in another library whose call to the greeter's_tracemust stay unlinked.