Return `null` from `RootInfo.package` for non-`package:` root libraries (#10033)
Avoid returning invalid package data from service manager.
This is just a simple fix where we return null of a library URI is not a `package:` URI.
Fixes https://github.com/flutter/devtools/issues/6734
diff --git a/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart b/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart
index 62c0bb9..99eb263 100644
--- a/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart
+++ b/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart
@@ -21,6 +21,7 @@
import '../panes/diff/controller/diff_pane_controller.dart';
import '../panes/profile/profile_pane_controller.dart';
import '../panes/tracing/tracing_pane_controller.dart';
+import '../shared/primitives/memory_utils.dart';
import 'offline_data/offline_data.dart';
/// Screen controller for the Memory screen.
@@ -129,7 +130,7 @@
chart = MemoryChartPaneController(data: offlineData?.chart ?? ChartData());
final rootPackage = isConnected
- ? serviceConnection.serviceManager.rootInfoNow().package!
+ ? serviceConnection.serviceManager.rootInfoNow().rootPackagePrefix!
: null;
diff =
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_diff.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_diff.dart
index cfc008b..c46a531 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_diff.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_diff.dart
@@ -12,6 +12,7 @@
import '../../../../../shared/primitives/utils.dart';
import '../../../../../shared/table/table.dart';
import '../../../../../shared/table/table_data.dart';
+import '../../../shared/primitives/memory_utils.dart';
import '../../../shared/primitives/simple_elements.dart';
import '../../../shared/widgets/class_filter.dart';
import '../../../shared/widgets/shared_memory_widgets.dart';
@@ -53,7 +54,9 @@
theClass: data.className,
showCopyButton: isRowSelected,
copyGaItem: gac.MemoryEvents.diffClassDiffCopy.name,
- rootPackage: serviceConnection.serviceManager.rootInfoNow().package,
+ rootPackage: serviceConnection.serviceManager
+ .rootInfoNow()
+ .rootPackagePrefix,
);
}
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_single.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_single.dart
index 460ebf9..3e5b022 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_single.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_single.dart
@@ -12,6 +12,7 @@
import '../../../../../shared/primitives/utils.dart';
import '../../../../../shared/table/table.dart';
import '../../../../../shared/table/table_data.dart';
+import '../../../shared/primitives/memory_utils.dart';
import '../../../shared/primitives/simple_elements.dart';
import '../../../shared/widgets/class_filter.dart';
import '../../../shared/widgets/shared_memory_widgets.dart';
@@ -50,7 +51,9 @@
theClass: data.className,
showCopyButton: isRowSelected,
copyGaItem: gac.MemoryEvents.diffClassSingleCopy.name,
- rootPackage: serviceConnection.serviceManager.rootInfoNow().package,
+ rootPackage: serviceConnection.serviceManager
+ .rootInfoNow()
+ .rootPackagePrefix,
);
}
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart
index 0465b9f..fd274c0 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart
@@ -18,6 +18,7 @@
import '../../../../shared/table/table_data.dart';
import '../../../../shared/ui/common_widgets.dart';
import '../../shared/heap/class_filter.dart';
+import '../../shared/primitives/memory_utils.dart';
import '../../shared/primitives/simple_elements.dart';
import '../../shared/widgets/class_filter.dart';
import '../../shared/widgets/shared_memory_widgets.dart';
@@ -146,7 +147,9 @@
theClass: data.heapClass,
showCopyButton: isRowSelected,
copyGaItem: gac.MemoryEvents.diffClassSingleCopy.name,
- rootPackage: serviceConnection.serviceManager.rootInfoNow().package,
+ rootPackage: serviceConnection.serviceManager
+ .rootInfoNow()
+ .rootPackagePrefix,
);
}
diff --git a/packages/devtools_app/lib/src/screens/memory/shared/heap/sampler.dart b/packages/devtools_app/lib/src/screens/memory/shared/heap/sampler.dart
index 36b72c1..f322719 100644
--- a/packages/devtools_app/lib/src/screens/memory/shared/heap/sampler.dart
+++ b/packages/devtools_app/lib/src/screens/memory/shared/heap/sampler.dart
@@ -13,6 +13,7 @@
import '../../../../shared/memory/heap_data.dart';
import '../../../../shared/memory/heap_object.dart';
import '../../../../shared/utils/vm_utils.dart';
+import '../primitives/memory_utils.dart';
class _HeapObjects {
_HeapObjects(this.objects, this.heap);
@@ -106,7 +107,7 @@
bool get isEvalEnabled =>
heapClass.classType(
- serviceConnection.serviceManager.rootInfoNow().package,
+ serviceConnection.serviceManager.rootInfoNow().rootPackagePrefix,
) !=
ClassType.runtime;
diff --git a/packages/devtools_app/lib/src/screens/memory/shared/primitives/memory_utils.dart b/packages/devtools_app/lib/src/screens/memory/shared/primitives/memory_utils.dart
index b14327f..f3ebf7c 100644
--- a/packages/devtools_app/lib/src/screens/memory/shared/primitives/memory_utils.dart
+++ b/packages/devtools_app/lib/src/screens/memory/shared/primitives/memory_utils.dart
@@ -2,10 +2,43 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+/// @docImport '../../../../shared/memory/class_name.dart';
+library;
+
+import 'package:devtools_app_shared/service.dart' show RootInfo;
import 'package:vm_service/vm_service.dart';
import '../../../../shared/globals.dart';
+/// Memory screen specific information derived from the [RootInfo] of the
+/// connected app.
+extension RootInfoMemoryExtension on RootInfo {
+ /// The prefix of the URIs of the libraries that belong to the root package of
+ /// the connected app, or `null` if the root library is unknown.
+ ///
+ /// The Memory screen passes this prefix as `rootPackage` to
+ /// [HeapClassName.classType], which classifies a class as
+ /// [ClassType.rootPackage] (a "project" class) if the URI of its library
+ /// starts with the prefix.
+ ///
+ /// For the root library `package:my_app/main.dart`, the prefix is
+ /// `package:my_app`.
+ ///
+ /// Unlike [RootInfo.package], the prefix is not `null` when the root library
+ /// is not a `package:` URI. For example, the root library of a Dart CLI app
+ /// started with `dart run` is a `file:` URI like `file:///app/bin/main.dart`,
+ /// and the prefix is `file:`. Without a prefix, the classes defined in the
+ /// files of such an app would be classified as [ClassType.runtime], which
+ /// disables evaluating them in the live app.
+ String? get rootPackagePrefix {
+ final library = this.library;
+ if (library == null) return null;
+
+ final slashIndex = library.indexOf('/');
+ return slashIndex == -1 ? library : library.substring(0, slashIndex);
+ }
+}
+
Future<HeapSnapshotGraph?> snapshotMemoryInSelectedIsolate() async {
final isolate =
serviceConnection.serviceManager.isolateManager.selectedIsolate.value;
diff --git a/packages/devtools_app/test/screens/memory/framework/memory_controller_test.dart b/packages/devtools_app/test/screens/memory/framework/memory_controller_test.dart
index e0282d6..528cbdb 100644
--- a/packages/devtools_app/test/screens/memory/framework/memory_controller_test.dart
+++ b/packages/devtools_app/test/screens/memory/framework/memory_controller_test.dart
@@ -7,6 +7,7 @@
import 'package:devtools_app/devtools_app.dart';
import 'package:devtools_app/src/screens/memory/shared/heap/class_filter.dart';
+import 'package:devtools_app_shared/service.dart' show RootInfo;
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_test/devtools_test.dart';
import 'package:devtools_test/helpers.dart';
@@ -177,4 +178,26 @@
expect(scene.controller.diff.core.snapshots.value.length, 1);
});
});
+
+ group('root package', () {
+ for (final (rootLibrary, expectedRootPackage) in const [
+ ('package:my_app/main.dart', 'package:my_app'),
+ // The root library of a Dart CLI app started with `dart run` is a `file:`
+ // URI, not a `package:` URI.
+ ('file:///app/bin/main.dart', 'file:'),
+ ]) {
+ testWidgets(
+ 'is $expectedRootPackage when the root library is $rootLibrary',
+ (WidgetTester tester) async {
+ scene = MemoryDefaultScene();
+ await scene.setUp(
+ classList: classList,
+ rootInfo: RootInfo(rootLibrary),
+ );
+
+ expect(scene.controller.trace!.rootPackage, expectedRootPackage);
+ },
+ );
+ }
+ });
}
diff --git a/packages/devtools_app/test/screens/memory/shared/primitives/memory_utils_test.dart b/packages/devtools_app/test/screens/memory/shared/primitives/memory_utils_test.dart
new file mode 100644
index 0000000..b6dd9f0
--- /dev/null
+++ b/packages/devtools_app/test/screens/memory/shared/primitives/memory_utils_test.dart
@@ -0,0 +1,58 @@
+// Copyright 2026 The Flutter Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+
+import 'package:devtools_app/src/screens/memory/shared/primitives/memory_utils.dart';
+import 'package:devtools_app/src/shared/memory/class_name.dart';
+import 'package:devtools_app_shared/service.dart' show RootInfo;
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ group('RootInfoMemoryExtension.rootPackagePrefix', () {
+ for (final (rootLibrary, expectedPrefix) in const [
+ ('package:my_app/main.dart', 'package:my_app'),
+ ('package:my_app/src/nested/file.dart', 'package:my_app'),
+ ('package:my_app', 'package:my_app'),
+ // The root library of a Dart CLI app started with `dart run`.
+ ('file:///app/bin/main.dart', 'file:'),
+ ('org-dartlang-app:///lib/main.dart', 'org-dartlang-app:'),
+ (null, null),
+ ]) {
+ test('is $expectedPrefix for $rootLibrary', () {
+ expect(RootInfo(rootLibrary).rootPackagePrefix, expectedPrefix);
+ });
+ }
+
+ test('classifies classes of a file: root library as project classes', () {
+ const rootLibrary = 'file:///app/bin/main.dart';
+ final heapClass = HeapClassName(
+ library: rootLibrary,
+ className: 'MyClass',
+ );
+
+ expect(
+ heapClass.classType(RootInfo(rootLibrary).rootPackagePrefix),
+ ClassType.rootPackage,
+ );
+ });
+
+ test('keeps classes of other libraries out of the project', () {
+ final rootPrefix = RootInfo('package:my_app/main.dart').rootPackagePrefix;
+
+ expect(
+ HeapClassName(
+ library: 'package:my_app/src/model.dart',
+ className: 'Model',
+ ).classType(rootPrefix),
+ ClassType.rootPackage,
+ );
+ expect(
+ HeapClassName(
+ library: 'package:other_package/other.dart',
+ className: 'Other',
+ ).classType(rootPrefix),
+ ClassType.dependency,
+ );
+ });
+ });
+}
diff --git a/packages/devtools_app/test/test_infra/scenes/memory/default.dart b/packages/devtools_app/test/test_infra/scenes/memory/default.dart
index 58cf958..95a0246 100644
--- a/packages/devtools_app/test/test_infra/scenes/memory/default.dart
+++ b/packages/devtools_app/test/test_infra/scenes/memory/default.dart
@@ -7,6 +7,7 @@
import 'package:devtools_app/src/screens/memory/panes/diff/controller/diff_pane_controller.dart';
import 'package:devtools_app/src/screens/memory/panes/profile/profile_pane_controller.dart';
import 'package:devtools_app/src/screens/memory/shared/heap/class_filter.dart';
+import 'package:devtools_app_shared/service.dart' show RootInfo;
import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_shared/devtools_shared.dart';
@@ -97,9 +98,11 @@
///
/// [classList] will be returned by VmService.getClassList.
/// [heapProviders] will be used to for heap snapshotting.
+ /// [rootInfo] will be returned by ServiceManager.rootInfoNow.
Future<void> setUp({
ClassList? classList,
List<HeapProvider>? heapProviders,
+ RootInfo? rootInfo,
}) async {
heapProviders = heapProviders ?? MemoryDefaultSceneHeaps.all;
@@ -129,6 +132,7 @@
allocationData: allocationJson,
classList: classList,
),
+ rootInfo: rootInfo,
);
final app = fakeServiceConnection.serviceManager.connectedApp!;
mockConnectedApp(app, isProfileBuild: true);
diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md
index 4965452..683bc2b 100644
--- a/packages/devtools_app_shared/CHANGELOG.md
+++ b/packages/devtools_app_shared/CHANGELOG.md
@@ -10,6 +10,9 @@
* Safely handle RPC errors and unexpected exceptions when calling service extensions in `ServiceExtensionManager`.
* The minimum Dart SDK version is bumped to 3.11.0.
* The minimum Flutter SDK version is bumped to 3.41.0.
+* Fix `RootInfo.package` to be `null` when the root library is not a `package:` URI,
+ such as the `file:` URI of a Dart CLI app started with `dart run`. Previously it
+ was the scheme of the URI, such as `file:`, which is not a package.
* Updates `devtools_shared` constraint to `^14.0.1`.
## 0.5.1
diff --git a/packages/devtools_app_shared/lib/src/service/isolate_state.dart b/packages/devtools_app_shared/lib/src/service/isolate_state.dart
index d08e05e..4f7a107 100644
--- a/packages/devtools_app_shared/lib/src/service/isolate_state.dart
+++ b/packages/devtools_app_shared/lib/src/service/isolate_state.dart
@@ -69,14 +69,29 @@
}
}
+/// Information about the root library of an isolate of the connected app.
+///
+/// The root library is the library that contains the `main` function of the
+/// isolate. See also [Isolate.rootLib].
class RootInfo {
RootInfo(this.library) : package = _libraryToPackage(library);
+ /// The URI of the root library, or `null` if it is unknown.
+ ///
+ /// This is not necessarily a `package:` URI. For example, the root library of
+ /// a Dart CLI app started with `dart run` is a `file:` URI like
+ /// `file:///app/bin/main.dart`.
final String? library;
+
+ /// The package that contains the root library, in the form `package:name`, or
+ /// `null` if it is unknown.
+ ///
+ /// This is `null` if [library] is `null` or is not a `package:` URI (for
+ /// example, a `file:` URI), since the name of the package is not known then.
final String? package;
static String? _libraryToPackage(String? library) {
- if (library == null) return null;
+ if (library == null || !library.startsWith('package:')) return null;
final slashIndex = library.indexOf('/');
if (slashIndex == -1) return library;
return library.substring(0, slashIndex);
diff --git a/packages/devtools_app_shared/test/service/isolate_state_test.dart b/packages/devtools_app_shared/test/service/isolate_state_test.dart
new file mode 100644
index 0000000..1fcbe63
--- /dev/null
+++ b/packages/devtools_app_shared/test/service/isolate_state_test.dart
@@ -0,0 +1,45 @@
+// Copyright 2026 The Flutter Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+
+import 'package:devtools_app_shared/src/service/isolate_state.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ group('RootInfo', () {
+ test('handles package URIs', () {
+ final info = RootInfo('package:my_package/main.dart');
+ expect(info.library, 'package:my_package/main.dart');
+ expect(info.package, 'package:my_package');
+ });
+
+ test('handles package URIs without path', () {
+ final info = RootInfo('package:my_package');
+ expect(info.library, 'package:my_package');
+ expect(info.package, 'package:my_package');
+ });
+
+ test('handles null library', () {
+ final info = RootInfo(null);
+ expect(info.library, isNull);
+ expect(info.package, isNull);
+ });
+
+ test('handles file URIs (non-package)', () {
+ final info = RootInfo(
+ 'file:///Users/viktor/Projects/tests/gui_1/gui_1_server/bin/main.dart',
+ );
+ expect(
+ info.library,
+ 'file:///Users/viktor/Projects/tests/gui_1/gui_1_server/bin/main.dart',
+ );
+ expect(info.package, isNull);
+ });
+
+ test('handles dart: URIs (non-package)', () {
+ final info = RootInfo('dart:core');
+ expect(info.library, 'dart:core');
+ expect(info.package, isNull);
+ });
+ });
+}
diff --git a/packages/devtools_test/lib/src/mocks/fake_service_manager.dart b/packages/devtools_test/lib/src/mocks/fake_service_manager.dart
index 5b948b5..bb2b821 100644
--- a/packages/devtools_test/lib/src/mocks/fake_service_manager.dart
+++ b/packages/devtools_test/lib/src/mocks/fake_service_manager.dart
@@ -28,6 +28,7 @@
List<String> availableServices = const [],
List<String> availableLibraries = const [],
String? rootLibrary,
+ RootInfo? rootInfo,
}) {
_serviceManager = FakeServiceManager(
service: service,
@@ -36,6 +37,7 @@
availableLibraries: availableLibraries,
availableServices: availableServices,
rootLibrary: rootLibrary,
+ rootInfo: rootInfo,
);
for (final screen in ScreenMetaData.values) {
final screenId = screen.id;
@@ -92,9 +94,11 @@
this.onVmServiceOpened,
Map<String, Response>? serviceExtensionResponses,
String? rootLibrary,
+ RootInfo? rootInfo,
bool hasConnection = true,
}) : serviceExtensionResponses =
serviceExtensionResponses ?? _defaultServiceExtensionResponses,
+ _rootInfo = rootInfo ?? RootInfo('package:myPackage/myPackage.dart'),
_isolateManager = FakeIsolateManager(rootLibrary: rootLibrary) {
this.service = service ?? createFakeService();
serviceUri = this.service!.wsUri;
@@ -179,8 +183,15 @@
@override
ConnectedApp? connectedApp = MockConnectedApp();
+ /// The [RootInfo] returned by [rootInfoNow].
+ ///
+ /// This is independent of the `rootLibrary` constructor argument, which only
+ /// configures the root library of the fake main isolate. Some tests depend on
+ /// this defaulting to a fixed `package:` root library.
+ final RootInfo _rootInfo;
+
@override
- RootInfo rootInfoNow() => RootInfo('package:myPackage/myPackage.dart');
+ RootInfo rootInfoNow() => _rootInfo;
@override
Future<RootInfo?> tryToDetectMainRootInfo() => Future.value(rootInfoNow());