Skip to content

Dart: calls to private members across part files are dropped since 1.6 #2466

Open

Description

@libin1528

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 calls edges 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

  1. maxmilian commented on Oct 9, 2026

    @maxmilian
    Contributor

    I'd like to take this. Plan: in isVisibleAcrossFiles, check a Dart private candidate with the existing inSameDartLibrary (library file + its parts) before the PRIVATE_IS_FILE_LOCAL rule, 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 _trace must stay unlinked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions