From 5d4b1d5c501240528ac8ce4cc764826a4856c9a9 Mon Sep 17 00:00:00 2001 From: Max Hsu Date: Fri, 9 Oct 2026 16:47:06 +0800 Subject: [PATCH] fix(dart): a private member is visible across its library's part files (#2466) Dart privacy is per library, but isVisibleAcrossFiles treated a Dart _name as file-local, so calls between a library file and its part files were dropped. Check private Dart candidates with inSameDartLibrary first. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WuRWooMLGXysyySZsUMNVH --- CHANGELOG.md | 1 + __tests__/dart-part-directives.test.ts | 80 ++++++++++++++++++++++++++ src/resolution/name-matcher.ts | 6 +- 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15f9099acd..411e9fc977 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `codegraph_explore` and `codegraph context` now find your project's busiest file, and favor the code around it, even when twenty or more test or generated files are busier still, such as a package of protobuf stubs. Before, the search looked only at the 20 busiest files and then set the tests and generated ones aside, so it could find no file at all, or judge one far ahead of the next when it was not. `getTopRouteFile()` in the library counts every file's routes the same way. Thanks @inth3shadows for the report. (#2578) - In JavaScript and TypeScript, a call written as the body of an `if`, `while`, `for` or `do` without braces, like `if (e.key === "Escape") setOpen(false);`, no longer links to a same-named property or method in another file when the name is the file's own, such as a `setOpen` from `useState` (#2585). - `codegraph sync` no longer fails with "FOREIGN KEY constraint failed" in a project whose code lazy-loads a workspace package, like Supabase Studio's `dynamic(() => import('icons').then((mod) => mod.EdgeFunctions))` or vben's `() => import('@vben/layouts').then((m) => m.IFrameView)`. Before, every sync after an edit in such a project rolled back, so the index stopped following your changes until a full re-index. Thanks @bompus for the report and @inth3shadows. (#2565) +- In Dart, a call to a private (`_`-prefixed) member declared in another file of the same library — the library file or one of its `part` files — is linked again. Since 1.6 these calls were dropped, so a library split into `part` files lost most calls between its own files from callers and impact. A private member of another library is still not linked. Thanks @libin1528 for the report. (#2466) - In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty. - In Delphi, a routine with an attribute on one of its parameters, like `const [ref] AValue: Integer` or DelphiMVCFramework's `const [MVCFromBody] Person: TPerson`, is now indexed with its calls, and so is every class member declared after it. Before, the routine was lost or its calls were credited to the file, the parameter showed up as a field of the class, and the members after it were missing. Thanks @stalexteam for the report. (#2545) - In Delphi, a `for … in` loop that declares its variable inline, like `for var lItem in AList do` or `for var lHandle: THandle in lHandles do`, now has its calls indexed, and a call on a typed loop variable reaches that type's method. Before, the whole loop was skipped, so a routine calling a method only from such a loop was missing from that method's callers. Thanks @stalexteam for the report. (#2546) diff --git a/__tests__/dart-part-directives.test.ts b/__tests__/dart-part-directives.test.ts index 66429f7f27..b243c12d81 100644 --- a/__tests__/dart-part-directives.test.ts +++ b/__tests__/dart-part-directives.test.ts @@ -214,3 +214,83 @@ void main() {} expect(part.dependents).toEqual(['lib/src/framework.dart']); }); }); + +/** + * Dart privacy is per library: a `_name` declared in one file of a library is + * visible in its `part` files and the other way round, but not in another + * library that imports it (#2466). + */ +describe('Dart private members across part files', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-dart-part-private-')); + writeTree(root, { + 'pubspec.yaml': 'name: app\n', + '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); + String hello() => _local(); + 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); + return 'Hello, $name'; + } +} +`, + 'lib/greeter_shout.dart': `part of 'greeter.dart'; + +mixin _Shouting on _Formatting { + String shout(String name) => _format(name).toUpperCase(); +} +`, + 'lib/src/_greeter_whisper.dart': `part of '../greeter.dart'; + +mixin _Whispering on _Formatting { + String whisper(String name) => _format(name).toLowerCase(); +} +`, + // Another library: the greeter's privates are not its to call. + 'lib/caller.dart': `import 'greeter.dart'; + +void outsider() => _trace('x'); +`, + }); + cg = await CodeGraph.init(root, { index: true }); + }); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + function callEdges(): string[] { + const files = ['lib/greeter.dart', 'lib/greeter_format.dart', 'lib/greeter_shout.dart', 'lib/src/_greeter_whisper.dart', 'lib/caller.dart']; + const ids = files.flatMap((f) => cg.getNodesInFile(f).map((n) => n.id)); + return cg.getOutgoingEdgesFrom(ids, ['calls']) + .map((e) => `${cg.getNode(e.source)!.qualifiedName} -> ${cg.getNode(e.target)!.qualifiedName}`) + .sort(); + } + + it('links a call to a private member declared in another file of the same library', () => { + // `outsider` in lib/caller.dart gets no edge: _trace is private to the greeter library. + expect(callEdges()).toEqual([ + 'Greeter::greet -> _Formatting::_format', + 'Greeter::hello -> Greeter::_local', + '_Formatting::_format -> _trace', + '_Shouting::shout -> _Formatting::_format', + '_Whispering::whisper -> _Formatting::_format', + ]); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index c434635e1f..2d13edd632 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -2150,7 +2150,9 @@ function luaModuleFile(name: string, decl: Node, context: ResolutionContext): st /** * Languages in which `visibility: 'private'` on a definition means no other * FILE can name it: a Kotlin `private fun` is file- or class-local, and the - * same holds for Java, C#, Swift, Scala, Dart and PHP members. + * same holds for Java, C#, Swift, Scala, Dart and PHP members. A Dart + * `_name` is the exception's exception: it is private to its library, which + * spans that library's `part` files, and is handled before this set is consulted. */ const PRIVATE_IS_FILE_LOCAL = new Set(['kotlin', 'java', 'csharp', 'swift', 'scala', 'dart', 'php']); @@ -3496,6 +3498,8 @@ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, contex const owner = rustModuleDir(candidate.filePath); return ref.filePath.startsWith(owner + '/'); } + // Dart privacy is per library: a library's `part` files share its `_names`. + if (lang === 'dart' && candidate.visibility === 'private') return inSameDartLibrary(ref.filePath, candidate.filePath, context); if (PRIVATE_IS_FILE_LOCAL.has(lang)) return candidate.visibility !== 'private'; // An R test file runs in an environment of its own (testthat): its top-level // `c <- ggplot(…)` is not what the package's 2,843 `c(…)` calls mean. The