From 2ae78d5e38371643b99467ce2bde514ee60cf1ee Mon Sep 17 00:00:00 2001 From: Kenzie Davisson Date: Mon, 15 Jun 2026 16:03:41 -0700 Subject: [PATCH 1/4] Enable `dispose-class-fields` and `dispose-fields` lints to resolve memory leaks --- analysis_options.yaml | 2 + packages/devtools_app/lib/src/app.dart | 1 + .../src/extensions/embedded/_view_web.dart | 1 + .../lib/src/framework/notifications_view.dart | 3 ++ .../lib/src/framework/scaffold/scaffold.dart | 1 + .../src/screens/app_size/app_size_screen.dart | 3 ++ .../lib/src/screens/debugger/call_stack.dart | 1 + .../lib/src/screens/debugger/controls.dart | 1 + .../screens/debugger/debugger_controller.dart | 3 ++ .../src/screens/debugger/debugger_screen.dart | 2 + .../deep_link_list_view.dart | 1 + .../deep_links_screen.dart | 1 + .../select_project_view.dart | 1 + .../lib/src/screens/dtd/events.dart | 6 ++- .../lib/src/screens/dtd/services.dart | 8 ++-- .../inspector/inspector_controller.dart | 2 + .../inspector/inspector_tree_controller.dart | 2 + .../inspector/layout_explorer/flex/flex.dart | 6 +++ .../ui/layout_explorer_widget.dart | 3 ++ .../lib/src/screens/logging/_log_details.dart | 6 +++ .../src/screens/logging/logging_screen.dart | 1 + .../screens/memory/framework/screen_body.dart | 1 + .../memory/shared/widgets/class_filter.dart | 7 +++ .../screens/network/network_controller.dart | 5 ++ .../src/screens/network/network_screen.dart | 2 + .../perfetto/_perfetto_controller_web.dart | 1 + .../perfetto/_perfetto_web.dart | 4 +- .../performance/performance_controller.dart | 4 ++ .../performance/performance_screen.dart | 1 + .../performance/tabbed_performance_view.dart | 12 ++--- .../src/screens/profiler/profiler_screen.dart | 1 + .../isolate_statistics_view.dart | 6 +++ .../object_inspector_view.dart | 24 ++++++---- .../process_memory/process_memory_view.dart | 10 ++++ .../service/connected_app/connected_app.dart | 1 + .../service/service_extension_widgets.dart | 13 ++++++ .../lib/src/shared/charts/flame_chart.dart | 2 + .../drag_and_drop/drag_and_drop.dart | 8 +++- .../lib/src/shared/console/console.dart | 6 +++ .../shared/diagnostics/inspector_service.dart | 1 + .../lib/src/shared/editor/editor_client.dart | 9 ++++ .../src/shared/preferences/preferences.dart | 18 +++++--- .../custom_pointer_scroll_view.dart | 6 ++- .../lib/src/shared/table/_tree_table.dart | 3 ++ .../lib/src/shared/table/table.dart | 46 ++++++++++++------- .../lib/src/shared/ui/editable_list.dart | 1 + .../lib/src/shared/ui/vm_flag_widgets.dart | 6 +++ .../property_editor_controller.dart | 1 + .../property_editor_inputs.dart | 8 ++++ .../linked_scroll_controller_test.dart | 7 +++ .../test/shared/table/table_test.dart | 4 +- .../lib/missing_material_error.dart | 6 +++ .../editor_service/simulated_editor.dart | 5 ++ .../standalone_ui/mock_editor_widget.dart | 7 +++ .../property_editor_sidebar.dart | 5 +- .../lib/src/service/dtd_manager.dart | 3 ++ .../lib/src/service/isolate_state.dart | 1 + .../lib/src/feature_examples/dtd_example.dart | 6 +++ .../service_extension_example.dart | 6 +++ .../_simulated_devtools_controller.dart | 1 + pubspec.lock | 20 ++++---- 61 files changed, 271 insertions(+), 62 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index f219cfddeae..f59d8d0ede5 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -186,6 +186,8 @@ dart_code_metrics: - avoid-explicit-type-declaration # - ban-name # TODO(polina-c): add configuration # - binary-expression-operand-order Some nice catches but too many false positives to enable. + - dispose-class-fields + - dispose-fields - double-literal-format # - format-comment TODO(jacobr): enable this one after fixing violations. # TODO(jacobr): enable member-ordering. This catches a bunch of real style diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index d2661868b1b..272f1fc06cd 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -202,6 +202,7 @@ class DevToolsAppState extends State with AutoDisposeMixin { FrameworkCore.dispose(); // Workaround for https://github.com/flutter/flutter/issues/155265. removeTextFieldFocusFixHandler(); + routerDelegate.dispose(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/extensions/embedded/_view_web.dart b/packages/devtools_app/lib/src/extensions/embedded/_view_web.dart index 3135be38703..79017530a1b 100644 --- a/packages/devtools_app/lib/src/extensions/embedded/_view_web.dart +++ b/packages/devtools_app/lib/src/extensions/embedded/_view_web.dart @@ -46,6 +46,7 @@ class _EmbeddedExtensionState extends State @override void dispose() { + _embeddedExtensionController.dispose(); iFrameController.dispose(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/framework/notifications_view.dart b/packages/devtools_app/lib/src/framework/notifications_view.dart index 83c5b69a6d3..fd89c5c1cb9 100644 --- a/packages/devtools_app/lib/src/framework/notifications_view.dart +++ b/packages/devtools_app/lib/src/framework/notifications_view.dart @@ -87,6 +87,8 @@ class _NotificationsState extends State<_Notifications> with AutoDisposeMixin { @override void dispose() { _overlayEntry!.remove(); + _overlayEntry?.dispose(); + _overlayEntry = null; super.dispose(); } @@ -218,6 +220,7 @@ class _NotificationState extends State<_Notification> void dispose() { controller.dispose(); _dismissTimer?.cancel(); + curve.dispose(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart b/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart index 1f2073e384a..c90533e82f9 100644 --- a/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart +++ b/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart @@ -403,6 +403,7 @@ class KeyboardShortcuts extends StatefulWidget { class KeyboardShortcutsState extends State with AutoDisposeMixin { + // ignore: dispose-fields, false positive. Disposed via autoDisposeFocusNode. late final FocusNode _focusNode; @override diff --git a/packages/devtools_app/lib/src/screens/app_size/app_size_screen.dart b/packages/devtools_app/lib/src/screens/app_size/app_size_screen.dart index ff9deef67f9..dbd51f1be03 100644 --- a/packages/devtools_app/lib/src/screens/app_size/app_size_screen.dart +++ b/packages/devtools_app/lib/src/screens/app_size/app_size_screen.dart @@ -86,6 +86,7 @@ class _AppSizeBodyState extends State ); static final tabs = [analysisTab, diffTab]; + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late AppSizeController controller; late final TabController _tabController; @@ -390,6 +391,7 @@ class AnalysisView extends StatefulWidget { } class _AnalysisViewState extends State with AutoDisposeMixin { + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late AppSizeController controller; TreemapNode? analysisRoot; @@ -482,6 +484,7 @@ class DiffView extends StatefulWidget { } class _DiffViewState extends State with AutoDisposeMixin { + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late AppSizeController controller; TreemapNode? diffRoot; diff --git a/packages/devtools_app/lib/src/screens/debugger/call_stack.dart b/packages/devtools_app/lib/src/screens/debugger/call_stack.dart index eaa2d239982..c40f9acb379 100644 --- a/packages/devtools_app/lib/src/screens/debugger/call_stack.dart +++ b/packages/devtools_app/lib/src/screens/debugger/call_stack.dart @@ -25,6 +25,7 @@ class CallStack extends StatefulWidget { class _CallStackState extends State { StackFrameAndSourcePosition? _clickedOnFrame; + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late DebuggerController controller; @override diff --git a/packages/devtools_app/lib/src/screens/debugger/controls.dart b/packages/devtools_app/lib/src/screens/debugger/controls.dart index 7db8774ef63..ee99940efe2 100644 --- a/packages/devtools_app/lib/src/screens/debugger/controls.dart +++ b/packages/devtools_app/lib/src/screens/debugger/controls.dart @@ -30,6 +30,7 @@ class DebuggingControls extends StatefulWidget { class _DebuggingControlsState extends State with AutoDisposeMixin { + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late DebuggerController controller; @override diff --git a/packages/devtools_app/lib/src/screens/debugger/debugger_controller.dart b/packages/devtools_app/lib/src/screens/debugger/debugger_controller.dart index aacc0ec31a1..08f8a54f272 100644 --- a/packages/devtools_app/lib/src/screens/debugger/debugger_controller.dart +++ b/packages/devtools_app/lib/src/screens/debugger/debugger_controller.dart @@ -89,6 +89,8 @@ class DebuggerController extends DevToolsScreenController _selectedBreakpoint.dispose(); _exceptionPauseMode.dispose(); _hasTruncatedFrames.dispose(); + unawaited(_getStackOperation?.cancel()); + _lastService = null; super.dispose(); } @@ -134,6 +136,7 @@ class DebuggerController extends DevToolsScreenController _firstDebuggerScreenLoaded = false; } + // ignore: dispose-class-fields, reference is nulled in dispose(). VmServiceWrapper? _lastService; void _handleConnectionAvailable(VmServiceWrapper service) { diff --git a/packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart b/packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart index 80745175e6f..7f7996ceaff 100644 --- a/packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart +++ b/packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart @@ -94,6 +94,7 @@ class _DebuggerScreenBodyWrapper extends StatefulWidget { class _DebuggerScreenBodyWrapperState extends State<_DebuggerScreenBodyWrapper> with AutoDisposeMixin { + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late DebuggerController controller; late bool _shownFirstScript; @@ -520,6 +521,7 @@ class _FloatingDebuggerControlsState extends State with AutoDisposeMixin { bool get _isPaused => serviceConnection.serviceManager.isMainIsolatePaused; + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late final DebuggerController _controller; late double _controlHeight; late double _controlOpacity; diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart index 75a90841656..7770cb06b00 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart @@ -33,6 +33,7 @@ class DeepLinkListView extends StatefulWidget { } class _DeepLinkListViewState extends State { + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late DeepLinksController controller; @override diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index 07520397476..cbd164cef9b 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -39,6 +39,7 @@ class DeepLinkPage extends StatefulWidget { } class _DeepLinkPageState extends State { + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late DeepLinksController controller; @override diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/project_root_selection/select_project_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/project_root_selection/select_project_view.dart index d022c479d44..34ed319766a 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/project_root_selection/select_project_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/project_root_selection/select_project_view.dart @@ -31,6 +31,7 @@ class SelectProjectView extends StatefulWidget { } class _SelectProjectViewState extends State { + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late DeepLinksController controller; bool _retrievingFlutterProject = false; diff --git a/packages/devtools_app/lib/src/screens/dtd/events.dart b/packages/devtools_app/lib/src/screens/dtd/events.dart index e00539f25f0..89a86f23898 100644 --- a/packages/devtools_app/lib/src/screens/dtd/events.dart +++ b/packages/devtools_app/lib/src/screens/dtd/events.dart @@ -13,7 +13,8 @@ import 'shared.dart'; /// Manages business logic for the [EventsView] widget, which displays /// information about events sent and received over DTD event streams. class EventsController extends FeatureController { - late DartToolingDaemon dtd; + // ignore: dispose-class-fields, reference is nulled in dispose(). This class is not the owner of this object. + DartToolingDaemon? dtd; @visibleForTesting final events = ListValueNotifier([]); @@ -28,7 +29,7 @@ class EventsController extends FeatureController { super.init(); for (final stream in knownDtdStreams) { autoDisposeStreamSubscription( - dtd.onEvent(stream).listen((event) { + dtd!.onEvent(stream).listen((event) { events.add(event); // Schedule a scroll to the bottom after the frame is built. WidgetsBinding.instance.addPostFrameCallback((_) { @@ -48,6 +49,7 @@ class EventsController extends FeatureController { events.dispose(); selectedEvent.dispose(); scrollController.dispose(); + dtd = null; super.dispose(); } } diff --git a/packages/devtools_app/lib/src/screens/dtd/services.dart b/packages/devtools_app/lib/src/screens/dtd/services.dart index a9dc28da97b..1918b967966 100644 --- a/packages/devtools_app/lib/src/screens/dtd/services.dart +++ b/packages/devtools_app/lib/src/screens/dtd/services.dart @@ -14,7 +14,8 @@ import 'dtd_tools_model.dart'; /// information about service methods registered on DTD and provides /// functionality for calling them. class ServicesController extends FeatureController { - late DartToolingDaemon dtd; + // ignore: dispose-class-fields, reference is nulled in dispose(). This class is not the owner of this object. + DartToolingDaemon? dtd; @visibleForTesting final services = ValueNotifier>([]); @@ -32,6 +33,7 @@ class ServicesController extends FeatureController { void dispose() { services.dispose(); selectedService.dispose(); + dtd = null; super.dispose(); } @@ -40,7 +42,7 @@ class ServicesController extends FeatureController { /// Refreshes [services] with the current set of services registered on /// [dtd]. Future refresh() async { - final response = await dtd.getRegisteredServices(); + final response = await dtd!.getRegisteredServices(); services.value = [ ...response.dtdServices.map((value) { // If the DTD service has the form 'service.method', split up the two @@ -153,7 +155,7 @@ class _ServicesViewState extends State { builder: (context, service, child) { return ManuallyCallService( serviceMethod: service, - dtd: widget.controller.dtd, + dtd: widget.controller.dtd!, ); }, ), diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart index e1db0b59c2a..dce3b31f439 100644 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart @@ -201,7 +201,9 @@ class InspectorController extends DisposableController /// for now mainly to minimize risk. static const refreshFramesPerSecond = 5.0; + // ignore: dispose-class-fields, set from the constructor. This class is not the owner of this object. InspectorTreeController inspectorTree; + final FlutterTreeType treeType; late RateLimiter _refreshRateLimiter; diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart index 67c39baebfd..130bec6a5a2 100644 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart @@ -914,6 +914,8 @@ class _InspectorTreeState extends State Rect? _currentAnimateTarget; AnimationController? _constraintDisplayController; + + // ignore: dispose-fields, false positive. Disposed via autoDisposeFocusNode. late FocusNode _focusNode; /// When autoscrolling, the number of rows to pad the target location with. diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart index 3c3cb55e2ec..dd16fdec685 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart @@ -394,6 +394,12 @@ class FlexLayoutExplorerWidgetState ), ); } + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } } class VisualizeFlexChildren extends StatefulWidget { diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/layout_explorer_widget.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/layout_explorer_widget.dart index 1a9840f9ead..9c8bf182944 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/layout_explorer_widget.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/layout_explorer_widget.dart @@ -162,6 +162,9 @@ abstract class LayoutExplorerWidgetState< entranceController.dispose(); changeController.dispose(); _unregisterInspectorControllerService(); + entranceCurve.dispose(); + changeAnimation.dispose(); + rateLimiter.dispose(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/screens/logging/_log_details.dart b/packages/devtools_app/lib/src/screens/logging/_log_details.dart index f277c54873c..19cba6690c1 100644 --- a/packages/devtools_app/lib/src/screens/logging/_log_details.dart +++ b/packages/devtools_app/lib/src/screens/logging/_log_details.dart @@ -43,6 +43,12 @@ class _LogDetailsState extends State unawaited(_computeLogDetails()); } + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + @override void didUpdateWidget(LogDetails oldWidget) { super.didUpdateWidget(oldWidget); diff --git a/packages/devtools_app/lib/src/screens/logging/logging_screen.dart b/packages/devtools_app/lib/src/screens/logging/logging_screen.dart index 79d2c7cfbe2..a6f5e5ed5f8 100644 --- a/packages/devtools_app/lib/src/screens/logging/logging_screen.dart +++ b/packages/devtools_app/lib/src/screens/logging/logging_screen.dart @@ -50,6 +50,7 @@ class LoggingScreenBody extends StatefulWidget { class _LoggingScreenState extends State with AutoDisposeMixin { + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late LoggingController controller; @override diff --git a/packages/devtools_app/lib/src/screens/memory/framework/screen_body.dart b/packages/devtools_app/lib/src/screens/memory/framework/screen_body.dart index d2389f1cf90..dd6709bd0f1 100644 --- a/packages/devtools_app/lib/src/screens/memory/framework/screen_body.dart +++ b/packages/devtools_app/lib/src/screens/memory/framework/screen_body.dart @@ -25,6 +25,7 @@ class ConnectedMemoryBody extends StatefulWidget { class _ConnectedMemoryBodyState extends State with AutoDisposeMixin, SingleTickerProviderStateMixin { + // ignore: dispose-fields, false positive. Disposed via autoDisposeFocusNode. final _focusNode = FocusNode(debugLabel: 'memory'); @override diff --git a/packages/devtools_app/lib/src/screens/memory/shared/widgets/class_filter.dart b/packages/devtools_app/lib/src/screens/memory/shared/widgets/class_filter.dart index afae49d1454..3374785c762 100644 --- a/packages/devtools_app/lib/src/screens/memory/shared/widgets/class_filter.dart +++ b/packages/devtools_app/lib/src/screens/memory/shared/widgets/class_filter.dart @@ -82,6 +82,13 @@ class _ClassFilterDialogState extends State { _loadStateFromFilter(widget.classFilter); } + @override + void dispose() { + _except.dispose(); + _only.dispose(); + super.dispose(); + } + @override void didUpdateWidget(covariant ClassFilterDialog oldWidget) { super.didUpdateWidget(oldWidget); diff --git a/packages/devtools_app/lib/src/screens/network/network_controller.dart b/packages/devtools_app/lib/src/screens/network/network_controller.dart index deecab62c7e..ce963810a47 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -65,6 +65,7 @@ class NetworkController extends DevToolsScreenController @override final screenId = ScreenMetaData.network.id; + // ignore: dispose-class-fields, false positive. List items are disposed in the dispose method. List? _httpRequests; Future exportAsHarFile() async { @@ -203,6 +204,10 @@ class NetworkController extends DevToolsScreenController selectedRequest.dispose(); _recordingNotifier.dispose(); _currentNetworkRequests.dispose(); + for (final r in _httpRequests ?? []) { + r.dispose(); + } + _httpRequests?.clear(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/screens/network/network_screen.dart b/packages/devtools_app/lib/src/screens/network/network_screen.dart index 66843cf09b0..0538dfaa79c 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -131,6 +131,7 @@ class NetworkScreenBody extends StatefulWidget { class _NetworkScreenBodyState extends State with AutoDisposeMixin { + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late NetworkController controller; @override @@ -180,6 +181,7 @@ class _NetworkProfilerControls extends StatefulWidget { class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> with AutoDisposeMixin { + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late NetworkController controller; bool _recording = false; diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart index ed9bc3a6f3e..c032ba10887 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart @@ -191,6 +191,7 @@ class PerfettoControllerImpl extends PerfettoController { await perfettoPostEventStream.close(); processor.dispose(); _activeScrollToTimeRange.dispose(); + activeTrace.dispose(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart index 2d5cafa0c84..22842170d60 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart @@ -34,14 +34,14 @@ class Perfetto extends StatefulWidget { } class _PerfettoState extends State with AutoDisposeMixin { - late final PerfettoControllerImpl _perfettoController; + PerfettoControllerImpl get _perfettoController => + widget.perfettoController as PerfettoControllerImpl; late final _PerfettoViewController _viewController; @override void initState() { super.initState(); - _perfettoController = widget.perfettoController as PerfettoControllerImpl; _viewController = _PerfettoViewController(_perfettoController)..init(); // If [_perfettoController.activeTrace.trace] has a null value, the trace diff --git a/packages/devtools_app/lib/src/screens/performance/performance_controller.dart b/packages/devtools_app/lib/src/screens/performance/performance_controller.dart index 8bed01cc548..e93c590e0ba 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_controller.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_controller.dart @@ -48,12 +48,16 @@ class PerformanceController extends DevToolsScreenController @override final screenId = ScreenMetaData.performance.id; + // ignore: dispose-class-fields, false positive. See `applyToFeatureControllers` in the dispose() method. late final FlutterFramesController flutterFramesController; + // ignore: dispose-class-fields, false positive. See `applyToFeatureControllers` in the dispose() method. late final TimelineEventsController timelineEventsController; + // ignore: dispose-class-fields, false positive. See `applyToFeatureControllers` in the dispose() method. late final RebuildStatsController rebuildStatsController; + // ignore: dispose-class-fields, false positive. See `applyToFeatureControllers` in the dispose() method. late List _featureControllers; // TODO(jacobr): add the recount controller to [_featureControllers] once your diff --git a/packages/devtools_app/lib/src/screens/performance/performance_screen.dart b/packages/devtools_app/lib/src/screens/performance/performance_screen.dart index ac10baf610d..506d4b78a97 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_screen.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_screen.dart @@ -59,6 +59,7 @@ class PerformanceScreenBody extends StatefulWidget { class PerformanceScreenBodyState extends State with AutoDisposeMixin { + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late PerformanceController controller; @override diff --git a/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart b/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart index 05eef54cd1c..68a3589ae91 100644 --- a/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart +++ b/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart @@ -13,7 +13,6 @@ import '../../shared/globals.dart'; import '../../shared/ui/common_widgets.dart'; import '../../shared/ui/tab.dart'; import 'panes/flutter_frames/flutter_frame_model.dart'; -import 'panes/flutter_frames/flutter_frames_controller.dart'; import 'panes/frame_analysis/frame_analysis.dart'; import 'panes/rebuild_stats/rebuild_stats.dart'; import 'panes/timeline_events/timeline_events_view.dart'; @@ -30,22 +29,21 @@ class _TabbedPerformanceViewState extends State with AutoDisposeMixin { static const _gaPrefix = 'performanceTab'; + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late PerformanceController controller; - late FlutterFramesController _flutterFramesController; - FlutterFrame? _selectedFlutterFrame; @override void initState() { super.initState(); controller = screenControllers.lookup(); - _flutterFramesController = controller.flutterFramesController; + final flutterFramesController = controller.flutterFramesController; - _selectedFlutterFrame = _flutterFramesController.selectedFrame.value; - addAutoDisposeListener(_flutterFramesController.selectedFrame, () { + _selectedFlutterFrame = flutterFramesController.selectedFrame.value; + addAutoDisposeListener(flutterFramesController.selectedFrame, () { setState(() { - _selectedFlutterFrame = _flutterFramesController.selectedFrame.value; + _selectedFlutterFrame = flutterFramesController.selectedFrame.value; }); }); } diff --git a/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart b/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart index cf473f55f24..e436b5f3bb6 100644 --- a/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart +++ b/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart @@ -57,6 +57,7 @@ class ProfilerScreenBody extends StatefulWidget { class _ProfilerScreenBodyState extends State with AutoDisposeMixin { + // ignore: dispose-fields, screen controller disposal is handled by the [ScreenControllers] class. late ProfilerScreenController controller; bool recording = false; diff --git a/packages/devtools_app/lib/src/screens/vm_developer/isolate_statistics/isolate_statistics_view.dart b/packages/devtools_app/lib/src/screens/vm_developer/isolate_statistics/isolate_statistics_view.dart index 77bd8d42c2f..5c25fede612 100644 --- a/packages/devtools_app/lib/src/screens/vm_developer/isolate_statistics/isolate_statistics_view.dart +++ b/packages/devtools_app/lib/src/screens/vm_developer/isolate_statistics/isolate_statistics_view.dart @@ -340,6 +340,12 @@ class _IsolatePortsWidgetState extends State { final selectedPort = ValueNotifier(null); + @override + void dispose() { + selectedPort.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final ports = widget.controller.ports; diff --git a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart index 76249c3402e..6d62c9446fe 100644 --- a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart +++ b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart @@ -15,7 +15,6 @@ import '../../debugger/program_explorer_model.dart'; import '../vm_developer_tools_controller.dart'; import '../vm_developer_tools_screen.dart'; import 'class_hierarchy_explorer.dart'; -import 'object_inspector_view_controller.dart'; import 'object_store.dart'; import 'object_viewport.dart'; @@ -39,16 +38,13 @@ class _ObjectInspectorView extends StatefulWidget { class _ObjectInspectorViewState extends State<_ObjectInspectorView> with TickerProviderStateMixin { - late ObjectInspectorViewController controller; - @override void didChangeDependencies() { super.didChangeDependencies(); final vmDeveloperToolsController = screenControllers .lookup(); - controller = vmDeveloperToolsController.objectInspectorViewController; - unawaited(controller.init()); + unawaited(vmDeveloperToolsController.objectInspectorViewController.init()); } @override @@ -58,7 +54,13 @@ class _ObjectInspectorViewState extends State<_ObjectInspectorView> initialFractions: const [0.2, 0.8], children: [ const ObjectInspectorSelector(), - SelectionArea(child: ObjectViewport(controller: controller)), + SelectionArea( + child: ObjectViewport( + controller: screenControllers + .lookup() + .objectInspectorViewController, + ), + ), ], ); } @@ -78,15 +80,13 @@ class ObjectInspectorSelector extends StatefulWidget { class _ObjectInspectorSelectorState extends State { String value = ObjectInspectorSelector.kProgramExplorer; - late ObjectInspectorViewController controller; @override void didChangeDependencies() { super.didChangeDependencies(); final vmDeveloperToolsController = screenControllers .lookup(); - controller = vmDeveloperToolsController.objectInspectorViewController; - unawaited(controller.init()); + unawaited(vmDeveloperToolsController.objectInspectorViewController.init()); } @override @@ -143,6 +143,9 @@ class _ObjectInspectorSelectorState extends State { } Widget _selectedWidget() { + final controller = screenControllers + .lookup() + .objectInspectorViewController; switch (value) { case ObjectInspectorSelector.kProgramExplorer: return ProgramExplorer( @@ -163,6 +166,9 @@ class _ObjectInspectorSelectorState extends State { } void _onNodeSelected(VMServiceObjectNode node) { + final controller = screenControllers + .lookup() + .objectInspectorViewController; final objRef = node.object; final location = node.location; if (objRef != null && diff --git a/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_view.dart b/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_view.dart index 9ae5d1d7554..2fb5ad78131 100644 --- a/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_view.dart +++ b/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_view.dart @@ -87,6 +87,16 @@ class _VMProcessMemoryViewBodyState extends State _initTabController(); } + @override + void dispose() { + if (_tabControllerInitialized) { + _tabController.removeListener(_onTabChanged); + _tabController.dispose(); + } + controller.dispose(); + super.dispose(); + } + @override void didUpdateWidget(VMProcessMemoryViewBody oldWidget) { super.didUpdateWidget(oldWidget); diff --git a/packages/devtools_app/lib/src/service/connected_app/connected_app.dart b/packages/devtools_app/lib/src/service/connected_app/connected_app.dart index 04c25f58c83..f6e64194c71 100644 --- a/packages/devtools_app/lib/src/service/connected_app/connected_app.dart +++ b/packages/devtools_app/lib/src/service/connected_app/connected_app.dart @@ -87,6 +87,7 @@ class AppState extends DisposableController with AutoDisposeControllerMixin { void dispose() { _variables.dispose(); _currentFrame.dispose(); + _dapVariables.dispose(); super.dispose(); } } diff --git a/packages/devtools_app/lib/src/service/service_extension_widgets.dart b/packages/devtools_app/lib/src/service/service_extension_widgets.dart index 7fdd4b4ad9a..10fab338b7a 100644 --- a/packages/devtools_app/lib/src/service/service_extension_widgets.dart +++ b/packages/devtools_app/lib/src/service/service_extension_widgets.dart @@ -534,6 +534,13 @@ class _ServiceExtensionCheckboxState extends State _initExtensionState(); } + @override + void dispose() { + value.dispose(); + extensionAvailable.dispose(); + super.dispose(); + } + @override void _onMainIsolateChanged() => _initExtensionState(); @@ -704,6 +711,12 @@ class _ServiceExtensionCheckboxGroupButtonState _initExtensionState(); } + @override + void dispose() { + _enabled.dispose(); + super.dispose(); + } + @override void _onMainIsolateChanged() => _initExtensionState(); diff --git a/packages/devtools_app/lib/src/shared/charts/flame_chart.dart b/packages/devtools_app/lib/src/shared/charts/flame_chart.dart index 37200e06ffd..bff073a30dd 100644 --- a/packages/devtools_app/lib/src/shared/charts/flame_chart.dart +++ b/packages/devtools_app/lib/src/shared/charts/flame_chart.dart @@ -114,6 +114,7 @@ abstract class FlameChartState< final sections = []; + // ignore: dispose-fields, false positive. Disposed via autoDisposeFocusNode. final focusNode = FocusNode(debugLabel: 'flame-chart'); double? mouseHoverX; @@ -277,6 +278,7 @@ abstract class FlameChartState< void dispose() { zoomController.dispose(); _verticalFlameChartScrollController.dispose(); + _hoveredNodeNotifier.dispose(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/shared/config_specific/drag_and_drop/drag_and_drop.dart b/packages/devtools_app/lib/src/shared/config_specific/drag_and_drop/drag_and_drop.dart index 2b26dd69e65..b9d04a912a9 100644 --- a/packages/devtools_app/lib/src/shared/config_specific/drag_and_drop/drag_and_drop.dart +++ b/packages/devtools_app/lib/src/shared/config_specific/drag_and_drop/drag_and_drop.dart @@ -38,6 +38,8 @@ abstract class DragAndDropManager { @mustCallSuper void dispose() { _dragAndDropStates.clear(); + activeState?.dispose(); + activeState = null; } void registerDragAndDrop(DragAndDropState state) { @@ -139,7 +141,11 @@ class DragAndDropState extends State { @override void dispose() { - _dragAndDropManager?.unregisterDragAndDrop(this); + _dragAndDropManager + ?..unregisterDragAndDrop(this) + ..dispose(); + _dragAndDropManager = null; + _dragging.dispose(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/shared/console/console.dart b/packages/devtools_app/lib/src/shared/console/console.dart index 91ead2a0640..30e9913ae67 100644 --- a/packages/devtools_app/lib/src/shared/console/console.dart +++ b/packages/devtools_app/lib/src/shared/console/console.dart @@ -90,6 +90,12 @@ class _ConsoleOutputState extends State<_ConsoleOutput> _initHelper(); } + @override + void dispose() { + _scroll.dispose(); + super.dispose(); + } + void _onScrollChanged() { // Detect if the user has scrolled up and stop scrolling to the bottom if // they have scrolled up. diff --git a/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart b/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart index 805e83f22ef..2d4e8a794ff 100644 --- a/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart +++ b/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart @@ -245,6 +245,7 @@ class InspectorService extends InspectorServiceBase { void dispose() { _cachedSelectionGroups?.clear(false); _cachedSelectionGroups = null; + _rootDirectories.dispose(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/shared/editor/editor_client.dart b/packages/devtools_app/lib/src/shared/editor/editor_client.dart index ef03c446f47..951790de13d 100644 --- a/packages/devtools_app/lib/src/shared/editor/editor_client.dart +++ b/packages/devtools_app/lib/src/shared/editor/editor_client.dart @@ -120,6 +120,15 @@ class EditorClient extends DisposableController } } + @override + void dispose() { + _editableArgumentsApiIsRegistered.dispose(); + unawaited(_activeLocationChangedController.close()); + unawaited(_eventController.close()); + unawaited(_editorServiceChangedController.close()); + super.dispose(); + } + void _handleServiceRegistration({ required String service, required String method, diff --git a/packages/devtools_app/lib/src/shared/preferences/preferences.dart b/packages/devtools_app/lib/src/shared/preferences/preferences.dart index a15d6d47072..403c4e43319 100644 --- a/packages/devtools_app/lib/src/shared/preferences/preferences.dart +++ b/packages/devtools_app/lib/src/shared/preferences/preferences.dart @@ -282,13 +282,17 @@ class PreferencesController extends DisposableController @override void dispose() { - cpuProfiler.dispose(); - devToolsExtensions.dispose(); - inspector.dispose(); - logging.dispose(); - memory.dispose(); - network.dispose(); - performance.dispose(); + _cpuProfiler.dispose(); + _extensions.dispose(); + _inspector.dispose(); + _logging.dispose(); + _memory.dispose(); + _network.dispose(); + _performance.dispose(); + darkModeEnabled.dispose(); + advancedDeveloperModeEnabled.dispose(); + wasmEnabled.dispose(); + verboseLoggingEnabled.dispose(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/shared/primitives/custom_pointer_scroll_view.dart b/packages/devtools_app/lib/src/shared/primitives/custom_pointer_scroll_view.dart index f6cc1b52de7..3794f1c02be 100644 --- a/packages/devtools_app/lib/src/shared/primitives/custom_pointer_scroll_view.dart +++ b/packages/devtools_app/lib/src/shared/primitives/custom_pointer_scroll_view.dart @@ -387,7 +387,11 @@ class CustomPointerScrollableState extends State @override void dispose() { widget.controller?.detach(position!); - position!.dispose(); + _position!.dispose(); + _hold?.cancel(); + _hold = null; + _drag?.cancel(); + _drag = null; super.dispose(); } diff --git a/packages/devtools_app/lib/src/shared/table/_tree_table.dart b/packages/devtools_app/lib/src/shared/table/_tree_table.dart index 1880f50a34a..22db562d84c 100644 --- a/packages/devtools_app/lib/src/shared/table/_tree_table.dart +++ b/packages/devtools_app/lib/src/shared/table/_tree_table.dart @@ -122,6 +122,8 @@ class TreeTable> extends StatefulWidget { class TreeTableState> extends State> with TickerProviderStateMixin, AutoDisposeMixin { FocusNode? get focusNode => _focusNode; + + // ignore: dispose-fields, false positive. Disposed via autoDisposeFocusNode. late FocusNode _focusNode; TreeTableController get tableController => _tableController!; @@ -165,6 +167,7 @@ class TreeTableState> extends State> @override void dispose() { + _tableController?.dispose(); _tableController = null; super.dispose(); } diff --git a/packages/devtools_app/lib/src/shared/table/table.dart b/packages/devtools_app/lib/src/shared/table/table.dart index 9e01cf2283b..e74164bc349 100644 --- a/packages/devtools_app/lib/src/shared/table/table.dart +++ b/packages/devtools_app/lib/src/shared/table/table.dart @@ -115,9 +115,14 @@ class DevToolsTableState extends State> with AutoDisposeMixin { static const _resizingDebounceDuration = Duration(milliseconds: 200); - late ScrollController scrollController; + @visibleForTesting + // ignore: dispose-fields, reference is nulled in dispose(). This class is not the owner of this object. + ScrollController? verticalScrollController; + + // ignore: dispose-fields, reference is nulled in dispose(). This class is not the owner of this object. + ScrollController? _horizontalScrollbarController; + late ScrollController pinnedScrollController; - late ScrollController _horizontalScrollbarController; late List _data; @@ -142,13 +147,13 @@ class DevToolsTableState extends State> ? widget.tableController.tableUiState.scrollOffset : 0.0; widget.tableController.initScrollController(initialScrollOffset); - scrollController = widget.tableController.verticalScrollController!; + verticalScrollController = widget.tableController.verticalScrollController!; _horizontalScrollbarController = widget.tableController.horizontalScrollController!; if (widget.startScrolledAtBottom) { WidgetsBinding.instance.addPostFrameCallback((_) { - unawaited(scrollController.autoScrollToBottom(jump: true)); + unawaited(verticalScrollController!.autoScrollToBottom(jump: true)); }); } @@ -208,7 +213,7 @@ class DevToolsTableState extends State> final newPos = selectedDisplayRow * defaultRowHeight; - maybeScrollToPosition(scrollController, newPos); + maybeScrollToPosition(verticalScrollController!, newPos); } }); }); @@ -232,13 +237,15 @@ class DevToolsTableState extends State> final index = _data.indexOf(activeSearch); if (index == -1) return; - + final verticalScrollController = this.verticalScrollController!; final y = index * defaultRowHeight; final indexInView = - y > scrollController.offset && - y < scrollController.offset + scrollController.position.extentInside; + y > verticalScrollController.offset && + y < + verticalScrollController.offset + + verticalScrollController.position.extentInside; if (!indexInView) { - await scrollController.animateTo( + await verticalScrollController.animateTo( index * defaultRowHeight, duration: defaultDuration, curve: defaultCurve, @@ -257,6 +264,9 @@ class DevToolsTableState extends State> @override void dispose() { pinnedScrollController.dispose(); + verticalScrollController = null; + _horizontalScrollbarController = null; + _resizingDebouncer.dispose(); super.dispose(); } @@ -388,10 +398,13 @@ class DevToolsTableState extends State> @override Widget build(BuildContext context) { + final verticalScrollController = this.verticalScrollController!; + // If we're at the end already, scroll to expose the new content. if (widget.autoScrollContent) { - if (scrollController.hasClients && scrollController.atScrollBottom) { - unawaited(scrollController.autoScrollToBottom()); + if (verticalScrollController.hasClients && + verticalScrollController.atScrollBottom) { + unawaited(verticalScrollController.autoScrollToBottom()); } } @@ -405,8 +418,9 @@ class DevToolsTableState extends State> final tableUiState = widget.tableController.tableUiState; final sortColumn = widget.tableController.columns[tableUiState.sortColumnIndex]; - if (widget.preserveVerticalScrollPosition && scrollController.hasClients) { - scrollController.jumpTo(tableUiState.scrollOffset); + if (widget.preserveVerticalScrollPosition && + verticalScrollController.hasClients) { + verticalScrollController.jumpTo(tableUiState.scrollOffset); } return LayoutBuilder( @@ -478,7 +492,7 @@ class DevToolsTableState extends State> Expanded( child: Scrollbar( thumbVisibility: true, - controller: scrollController, + controller: verticalScrollController, child: GestureDetector( behavior: HitTestBehavior.translucent, onTapDown: (a) => widget.focusNode?.requestFocus(), @@ -488,13 +502,13 @@ class DevToolsTableState extends State> widget.handleKeyEvent != null ? widget.handleKeyEvent!( event, - scrollController, + verticalScrollController, constraints, ) : KeyEventResult.ignored, focusNode: widget.focusNode, child: ListView.builder( - controller: scrollController, + controller: verticalScrollController, itemCount: _dataRowCount( constraints, showColumnGroupHeader, diff --git a/packages/devtools_app/lib/src/shared/ui/editable_list.dart b/packages/devtools_app/lib/src/shared/ui/editable_list.dart index b72f5866cbf..392950b5803 100644 --- a/packages/devtools_app/lib/src/shared/ui/editable_list.dart +++ b/packages/devtools_app/lib/src/shared/ui/editable_list.dart @@ -80,6 +80,7 @@ class _EditableListState extends State { @override void dispose() { textFieldController.dispose(); + textFieldFocusNode.dispose(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/shared/ui/vm_flag_widgets.dart b/packages/devtools_app/lib/src/shared/ui/vm_flag_widgets.dart index 187ebff1f04..8ddbce44500 100644 --- a/packages/devtools_app/lib/src/shared/ui/vm_flag_widgets.dart +++ b/packages/devtools_app/lib/src/shared/ui/vm_flag_widgets.dart @@ -175,6 +175,12 @@ class _VMFlagsDialogState extends State with AutoDisposeMixin { }); } + @override + void dispose() { + filterController.dispose(); + super.dispose(); + } + void _updateFromController() { flags = (serviceConnection.vmFlagManager.flags.value?.flags ?? []) .map((flag) => _DialogFlag(flag)) diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart index bad9c16e6aa..c8bf0e17a5a 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart @@ -123,6 +123,7 @@ class PropertyEditorController extends DisposableController @override void dispose() { _requestDebouncer.dispose(); + _editableWidgetData.dispose(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_inputs.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_inputs.dart index 037d46088bb..9d01365c63d 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_inputs.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_inputs.dart @@ -206,6 +206,7 @@ class _TextInputState extends State<_TextInput> with _PropertyInputMixin<_TextInput, T>, AutoDisposeMixin { static const _paddingDiffComparedToDropdown = 1.0; + // ignore: dispose-fields, false positive. Disposed via autoDisposeFocusNode. late final FocusNode _focusNode; late final TextEditingController _controller; @@ -224,6 +225,13 @@ class _TextInputState extends State<_TextInput> // Edit property when clicking or tabbing away from input. await _editProperty(); }); + autoDisposeFocusNode(_focusNode); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); } @override diff --git a/packages/devtools_app/test/shared/primitives/linked_scroll_controller_test.dart b/packages/devtools_app/test/shared/primitives/linked_scroll_controller_test.dart index 13f004632ee..2ec8be6348a 100644 --- a/packages/devtools_app/test/shared/primitives/linked_scroll_controller_test.dart +++ b/packages/devtools_app/test/shared/primitives/linked_scroll_controller_test.dart @@ -345,6 +345,13 @@ class TestState extends State { _numbers = _controllers.addAndGet(); } + @override + void dispose() { + _letters.dispose(); + _numbers.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Directionality( diff --git a/packages/devtools_app/test/shared/table/table_test.dart b/packages/devtools_app/test/shared/table/table_test.dart index dd2dcd97446..a8d6c4a6961 100644 --- a/packages/devtools_app/test/shared/table/table_test.dart +++ b/packages/devtools_app/test/shared/table/table_test.dart @@ -1193,7 +1193,7 @@ void main() { final tableState = tester.state( find.byType(DevToolsTable), ); - final scrollController = tableState.scrollController; + final scrollController = tableState.verticalScrollController!; expect(scrollController.offset, 0.0); expect(scrollController.position.maxScrollExtent, greaterThan(0.0)); @@ -1221,7 +1221,7 @@ void main() { final tableState = tester.state( find.byType(DevToolsTable), ); - final scrollController = tableState.scrollController; + final scrollController = tableState.verticalScrollController!; expect(scrollController.offset, isNot(0.0)); expect(scrollController.position.maxScrollExtent, greaterThan(0.0)); diff --git a/packages/devtools_app/test/test_infra/fixtures/flutter_error_app/lib/missing_material_error.dart b/packages/devtools_app/test/test_infra/fixtures/flutter_error_app/lib/missing_material_error.dart index 1c48982d9b5..04110b1e5c5 100644 --- a/packages/devtools_app/test/test_infra/fixtures/flutter_error_app/lib/missing_material_error.dart +++ b/packages/devtools_app/test/test_infra/fixtures/flutter_error_app/lib/missing_material_error.dart @@ -33,6 +33,12 @@ class ExampleWidget extends StatefulWidget { class _ExampleWidgetState extends State { final _controller = TextEditingController(); + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Column( diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/editor_service/simulated_editor.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/editor_service/simulated_editor.dart index c17709e6ad9..a762658e145 100644 --- a/packages/devtools_app/test/test_infra/scenes/standalone_ui/editor_service/simulated_editor.dart +++ b/packages/devtools_app/test/test_infra/scenes/standalone_ui/editor_service/simulated_editor.dart @@ -35,6 +35,11 @@ class SimulatedEditor { return editor; } + void dispose() { + unawaited(_logger.close()); + unawaited(close()); + } + /// The URI of the DTD instance we are connecting/connected to. final Uri _dtdUri; diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/mock_editor_widget.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/mock_editor_widget.dart index 08329bab45c..d13c09055af 100644 --- a/packages/devtools_app/test/test_infra/scenes/standalone_ui/mock_editor_widget.dart +++ b/packages/devtools_app/test/test_infra/scenes/standalone_ui/mock_editor_widget.dart @@ -87,6 +87,13 @@ class _MockEditorWidgetState extends State ); } + @override + void dispose() { + clientLogRing.dispose(); + editorLogRing.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final editorTheme = VsCodeTheme.of(context); diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/property_editor_sidebar.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/property_editor_sidebar.dart index 889c170f248..7f679252897 100644 --- a/packages/devtools_app/test/test_infra/scenes/standalone_ui/property_editor_sidebar.dart +++ b/packages/devtools_app/test/test_infra/scenes/standalone_ui/property_editor_sidebar.dart @@ -67,8 +67,9 @@ class _PropertyEditorState extends State<_PropertyEditorSidebar> { SimulatedEditor? editor; @override - void initState() { - super.initState(); + void dispose() { + unawaited(editor?.close()); + super.dispose(); } @override diff --git a/packages/devtools_app_shared/lib/src/service/dtd_manager.dart b/packages/devtools_app_shared/lib/src/service/dtd_manager.dart index 0035f7ac5e3..83f99705cc0 100644 --- a/packages/devtools_app_shared/lib/src/service/dtd_manager.dart +++ b/packages/devtools_app_shared/lib/src/service/dtd_manager.dart @@ -276,6 +276,9 @@ class DTDManager { await disconnect(); await _currentServiceRegistrationSubscription?.cancel(); await _serviceRegistrationController.close(); + _periodicConnectionCheck?.cancel(); + _periodicConnectionCheck = null; + _connectionState.dispose(); _connection.dispose(); } 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 ae8ec45c3d6..33764766c36 100644 --- a/packages/devtools_app_shared/lib/src/service/isolate_state.dart +++ b/packages/devtools_app_shared/lib/src/service/isolate_state.dart @@ -48,6 +48,7 @@ class IsolateState { null, () => _isolateLoadCompleter = Completer()..complete(null), ); + _isPaused.dispose(); } void handleDebugEvent(String? kind) { diff --git a/packages/devtools_extensions/example/packages_with_extensions/foo/packages/foo_devtools_extension/lib/src/feature_examples/dtd_example.dart b/packages/devtools_extensions/example/packages_with_extensions/foo/packages/foo_devtools_extension/lib/src/feature_examples/dtd_example.dart index 8ab07259f9f..114902214b0 100644 --- a/packages/devtools_extensions/example/packages_with_extensions/foo/packages/foo_devtools_extension/lib/src/feature_examples/dtd_example.dart +++ b/packages/devtools_extensions/example/packages_with_extensions/foo/packages/foo_devtools_extension/lib/src/feature_examples/dtd_example.dart @@ -169,6 +169,12 @@ class _ReadWriteTmpFileState extends State<_ReadWriteTmpFile> { textEditingController = TextEditingController(); } + @override + void dispose() { + textEditingController.dispose(); + super.dispose(); + } + Future _writeFilesAndUpdate() async { await dtdManager.writeFile(tmpFileUri, textEditingController.text); await _readTmpFile(); diff --git a/packages/devtools_extensions/example/packages_with_extensions/foo/packages/foo_devtools_extension/lib/src/feature_examples/service_extension_example.dart b/packages/devtools_extensions/example/packages_with_extensions/foo/packages/foo_devtools_extension/lib/src/feature_examples/service_extension_example.dart index 1ee30629707..f82d6774061 100644 --- a/packages/devtools_extensions/example/packages_with_extensions/foo/packages/foo_devtools_extension/lib/src/feature_examples/service_extension_example.dart +++ b/packages/devtools_extensions/example/packages_with_extensions/foo/packages/foo_devtools_extension/lib/src/feature_examples/service_extension_example.dart @@ -108,6 +108,12 @@ class _TableOfThingsState extends State { unawaited(_refreshThings()); } + @override + void dispose() { + things.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Column( diff --git a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_controller.dart b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_controller.dart index 2853cbb7a1e..024550eb7b1 100644 --- a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_controller.dart +++ b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_controller.dart @@ -48,6 +48,7 @@ class SimulatedDevToolsController extends DisposableController void dispose() { window.removeEventListener('message', _handleMessageListener); _handleMessageListener = null; + messageLogs.dispose(); super.dispose(); } diff --git a/pubspec.lock b/pubspec.lock index 090a02187c1..cc8b58e6e9b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: coverage - sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.15.1" cross_file: dependency: transitive description: @@ -286,10 +286,10 @@ packages: dependency: transitive description: name: file_selector_android - sha256: ff9f9a900e0b0cf546f05abe86c91a280d611005b977c2c46a446436d7a776e3 + sha256: "6a26687fa65cbc28a5345c7ae6f227e89f0b47740978a4c475b1a625da7a331b" url: "https://pub.dev" source: hosted - version: "0.5.2+7" + version: "0.5.2+8" file_selector_ios: dependency: transitive description: @@ -555,10 +555,10 @@ packages: dependency: transitive description: name: meta - sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739 + sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d url: "https://pub.dev" source: hosted - version: "1.18.2" + version: "1.18.3" mime: dependency: transitive description: @@ -911,10 +911,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "8a46fcbcd5b865e87053fc5096101d12f56f3fe352c42aa621e7fec9290054b7" + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 url: "https://pub.dev" source: hosted - version: "6.3.31" + version: "6.3.32" url_launcher_ios: dependency: transitive description: @@ -967,10 +967,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "47a1b32ee755c3fcffa33db52a7258c137f97bdb2209a1075be847809fac4ccf" + sha256: "1d774bbdf6b72a0b12122fc1560c9c2d2a67db5a4a4cc2bd8a5c990ab20e3188" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" vm_service: dependency: transitive description: From 2742870751adfe42fe70a049d91a9af77c3dde7a Mon Sep 17 00:00:00 2001 From: Kenzie Davisson Date: Tue, 16 Jun 2026 09:43:53 -0700 Subject: [PATCH 2/4] review comments --- .../lib/src/framework/notifications_view.dart | 5 ++-- .../lib/src/screens/dtd/events.dart | 7 +++-- .../lib/src/screens/dtd/services.dart | 9 +++---- .../object_inspector_view.dart | 26 ++++++++----------- .../drag_and_drop/drag_and_drop.dart | 7 +++-- .../lib/src/shared/table/table.dart | 18 ++++++------- .../test/shared/table/table_test.dart | 4 +-- 7 files changed, 34 insertions(+), 42 deletions(-) diff --git a/packages/devtools_app/lib/src/framework/notifications_view.dart b/packages/devtools_app/lib/src/framework/notifications_view.dart index fd89c5c1cb9..e3c2331ae27 100644 --- a/packages/devtools_app/lib/src/framework/notifications_view.dart +++ b/packages/devtools_app/lib/src/framework/notifications_view.dart @@ -86,8 +86,9 @@ class _NotificationsState extends State<_Notifications> with AutoDisposeMixin { @override void dispose() { - _overlayEntry!.remove(); - _overlayEntry?.dispose(); + _overlayEntry + ?..remove() + ..dispose(); _overlayEntry = null; super.dispose(); } diff --git a/packages/devtools_app/lib/src/screens/dtd/events.dart b/packages/devtools_app/lib/src/screens/dtd/events.dart index 89a86f23898..fe6944873b0 100644 --- a/packages/devtools_app/lib/src/screens/dtd/events.dart +++ b/packages/devtools_app/lib/src/screens/dtd/events.dart @@ -13,8 +13,8 @@ import 'shared.dart'; /// Manages business logic for the [EventsView] widget, which displays /// information about events sent and received over DTD event streams. class EventsController extends FeatureController { - // ignore: dispose-class-fields, reference is nulled in dispose(). This class is not the owner of this object. - DartToolingDaemon? dtd; + // ignore: dispose-class-fields, this class is not the owner of this object. + late DartToolingDaemon dtd; @visibleForTesting final events = ListValueNotifier([]); @@ -29,7 +29,7 @@ class EventsController extends FeatureController { super.init(); for (final stream in knownDtdStreams) { autoDisposeStreamSubscription( - dtd!.onEvent(stream).listen((event) { + dtd.onEvent(stream).listen((event) { events.add(event); // Schedule a scroll to the bottom after the frame is built. WidgetsBinding.instance.addPostFrameCallback((_) { @@ -49,7 +49,6 @@ class EventsController extends FeatureController { events.dispose(); selectedEvent.dispose(); scrollController.dispose(); - dtd = null; super.dispose(); } } diff --git a/packages/devtools_app/lib/src/screens/dtd/services.dart b/packages/devtools_app/lib/src/screens/dtd/services.dart index 1918b967966..442248b30b0 100644 --- a/packages/devtools_app/lib/src/screens/dtd/services.dart +++ b/packages/devtools_app/lib/src/screens/dtd/services.dart @@ -14,8 +14,8 @@ import 'dtd_tools_model.dart'; /// information about service methods registered on DTD and provides /// functionality for calling them. class ServicesController extends FeatureController { - // ignore: dispose-class-fields, reference is nulled in dispose(). This class is not the owner of this object. - DartToolingDaemon? dtd; + // ignore: dispose-class-fields, this class is not the owner of this object. + late DartToolingDaemon dtd; @visibleForTesting final services = ValueNotifier>([]); @@ -33,7 +33,6 @@ class ServicesController extends FeatureController { void dispose() { services.dispose(); selectedService.dispose(); - dtd = null; super.dispose(); } @@ -42,7 +41,7 @@ class ServicesController extends FeatureController { /// Refreshes [services] with the current set of services registered on /// [dtd]. Future refresh() async { - final response = await dtd!.getRegisteredServices(); + final response = await dtd.getRegisteredServices(); services.value = [ ...response.dtdServices.map((value) { // If the DTD service has the form 'service.method', split up the two @@ -155,7 +154,7 @@ class _ServicesViewState extends State { builder: (context, service, child) { return ManuallyCallService( serviceMethod: service, - dtd: widget.controller.dtd!, + dtd: widget.controller.dtd, ); }, ), diff --git a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart index 6d62c9446fe..7ec9d03f76b 100644 --- a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart +++ b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart @@ -15,6 +15,7 @@ import '../../debugger/program_explorer_model.dart'; import '../vm_developer_tools_controller.dart'; import '../vm_developer_tools_screen.dart'; import 'class_hierarchy_explorer.dart'; +import 'object_inspector_view_controller.dart'; import 'object_store.dart'; import 'object_viewport.dart'; @@ -38,13 +39,16 @@ class _ObjectInspectorView extends StatefulWidget { class _ObjectInspectorViewState extends State<_ObjectInspectorView> with TickerProviderStateMixin { + // ignore: dispose-fields, this class is not the owner of this object. + late ObjectInspectorViewController controller; @override void didChangeDependencies() { super.didChangeDependencies(); final vmDeveloperToolsController = screenControllers .lookup(); - unawaited(vmDeveloperToolsController.objectInspectorViewController.init()); + controller = vmDeveloperToolsController.objectInspectorViewController; + unawaited(controller.init()); } @override @@ -54,13 +58,7 @@ class _ObjectInspectorViewState extends State<_ObjectInspectorView> initialFractions: const [0.2, 0.8], children: [ const ObjectInspectorSelector(), - SelectionArea( - child: ObjectViewport( - controller: screenControllers - .lookup() - .objectInspectorViewController, - ), - ), + SelectionArea(child: ObjectViewport(controller: controller)), ], ); } @@ -81,12 +79,16 @@ class ObjectInspectorSelector extends StatefulWidget { class _ObjectInspectorSelectorState extends State { String value = ObjectInspectorSelector.kProgramExplorer; + // ignore: dispose-fields, this class is not the owner of this object. + late ObjectInspectorViewController controller; + @override void didChangeDependencies() { super.didChangeDependencies(); final vmDeveloperToolsController = screenControllers .lookup(); - unawaited(vmDeveloperToolsController.objectInspectorViewController.init()); + controller = vmDeveloperToolsController.objectInspectorViewController; + unawaited(controller.init()); } @override @@ -143,9 +145,6 @@ class _ObjectInspectorSelectorState extends State { } Widget _selectedWidget() { - final controller = screenControllers - .lookup() - .objectInspectorViewController; switch (value) { case ObjectInspectorSelector.kProgramExplorer: return ProgramExplorer( @@ -166,9 +165,6 @@ class _ObjectInspectorSelectorState extends State { } void _onNodeSelected(VMServiceObjectNode node) { - final controller = screenControllers - .lookup() - .objectInspectorViewController; final objRef = node.object; final location = node.location; if (objRef != null && diff --git a/packages/devtools_app/lib/src/shared/config_specific/drag_and_drop/drag_and_drop.dart b/packages/devtools_app/lib/src/shared/config_specific/drag_and_drop/drag_and_drop.dart index b9d04a912a9..36a823ba993 100644 --- a/packages/devtools_app/lib/src/shared/config_specific/drag_and_drop/drag_and_drop.dart +++ b/packages/devtools_app/lib/src/shared/config_specific/drag_and_drop/drag_and_drop.dart @@ -26,6 +26,7 @@ abstract class DragAndDropManager { final _dragAndDropStates = {}; + // ignore: dispose-class-fields, dispose should not be manually called on a State object. DragAndDropState? activeState; /// The method is abstract, because we want to force descendants to define it. @@ -38,7 +39,6 @@ abstract class DragAndDropManager { @mustCallSuper void dispose() { _dragAndDropStates.clear(); - activeState?.dispose(); activeState = null; } @@ -115,6 +115,7 @@ class DragAndDrop extends StatefulWidget { class DragAndDropState extends State { final _dragging = ValueNotifier(false); + // ignore: dispose-fields, this class is not the owner of this object. DragAndDropManager? _dragAndDropManager; bool _isActive = false; @@ -141,9 +142,7 @@ class DragAndDropState extends State { @override void dispose() { - _dragAndDropManager - ?..unregisterDragAndDrop(this) - ..dispose(); + _dragAndDropManager?.unregisterDragAndDrop(this); _dragAndDropManager = null; _dragging.dispose(); super.dispose(); diff --git a/packages/devtools_app/lib/src/shared/table/table.dart b/packages/devtools_app/lib/src/shared/table/table.dart index e74164bc349..b6197282394 100644 --- a/packages/devtools_app/lib/src/shared/table/table.dart +++ b/packages/devtools_app/lib/src/shared/table/table.dart @@ -116,11 +116,11 @@ class DevToolsTableState extends State> static const _resizingDebounceDuration = Duration(milliseconds: 200); @visibleForTesting - // ignore: dispose-fields, reference is nulled in dispose(). This class is not the owner of this object. - ScrollController? verticalScrollController; + // ignore: dispose-fields, this class is not the owner of this object. + late final ScrollController verticalScrollController; - // ignore: dispose-fields, reference is nulled in dispose(). This class is not the owner of this object. - ScrollController? _horizontalScrollbarController; + // ignore: dispose-fields, this class is not the owner of this object. + late final ScrollController _horizontalScrollbarController; late ScrollController pinnedScrollController; @@ -153,7 +153,7 @@ class DevToolsTableState extends State> if (widget.startScrolledAtBottom) { WidgetsBinding.instance.addPostFrameCallback((_) { - unawaited(verticalScrollController!.autoScrollToBottom(jump: true)); + unawaited(verticalScrollController.autoScrollToBottom(jump: true)); }); } @@ -213,7 +213,7 @@ class DevToolsTableState extends State> final newPos = selectedDisplayRow * defaultRowHeight; - maybeScrollToPosition(verticalScrollController!, newPos); + maybeScrollToPosition(verticalScrollController, newPos); } }); }); @@ -237,7 +237,7 @@ class DevToolsTableState extends State> final index = _data.indexOf(activeSearch); if (index == -1) return; - final verticalScrollController = this.verticalScrollController!; + final verticalScrollController = this.verticalScrollController; final y = index * defaultRowHeight; final indexInView = y > verticalScrollController.offset && @@ -264,8 +264,6 @@ class DevToolsTableState extends State> @override void dispose() { pinnedScrollController.dispose(); - verticalScrollController = null; - _horizontalScrollbarController = null; _resizingDebouncer.dispose(); super.dispose(); } @@ -398,7 +396,7 @@ class DevToolsTableState extends State> @override Widget build(BuildContext context) { - final verticalScrollController = this.verticalScrollController!; + final verticalScrollController = this.verticalScrollController; // If we're at the end already, scroll to expose the new content. if (widget.autoScrollContent) { diff --git a/packages/devtools_app/test/shared/table/table_test.dart b/packages/devtools_app/test/shared/table/table_test.dart index a8d6c4a6961..f977bb60302 100644 --- a/packages/devtools_app/test/shared/table/table_test.dart +++ b/packages/devtools_app/test/shared/table/table_test.dart @@ -1193,7 +1193,7 @@ void main() { final tableState = tester.state( find.byType(DevToolsTable), ); - final scrollController = tableState.verticalScrollController!; + final scrollController = tableState.verticalScrollController; expect(scrollController.offset, 0.0); expect(scrollController.position.maxScrollExtent, greaterThan(0.0)); @@ -1221,7 +1221,7 @@ void main() { final tableState = tester.state( find.byType(DevToolsTable), ); - final scrollController = tableState.verticalScrollController!; + final scrollController = tableState.verticalScrollController; expect(scrollController.offset, isNot(0.0)); expect(scrollController.position.maxScrollExtent, greaterThan(0.0)); From 9f1c3670043f2ed19ec214c012e454c7c65e0a80 Mon Sep 17 00:00:00 2001 From: Kenzie Davisson Date: Tue, 16 Jun 2026 09:44:33 -0700 Subject: [PATCH 3/4] rnotes --- packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 2cd2c928fe0..14d0b6d408a 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -15,7 +15,7 @@ To learn more about DevTools, check out the ## General updates -TODO: Remove this section if there are not any updates. +* Resolve several memory leaks. - [#9857](https://github.com/flutter/devtools/pull/9857) ## Inspector updates From 064cb1b7a390ed0493fc9a9ce3740e9d2223df33 Mon Sep 17 00:00:00 2001 From: Kenzie Davisson Date: Tue, 16 Jun 2026 09:53:58 -0700 Subject: [PATCH 4/4] Fix the release notes workflow --- .github/workflows/release-notes.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-notes.yaml b/.github/workflows/release-notes.yaml index 32511d05926..463f5c29833 100644 --- a/.github/workflows/release-notes.yaml +++ b/.github/workflows/release-notes.yaml @@ -26,10 +26,9 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PULL_NUMBER: ${{steps.get-pull-request-number.outputs.PULL_REQUEST_NUMBER}} run: | - FILES_RESPONSE=$(gh api /repos/$GITHUB_REPOSITORY/pulls/$PULL_NUMBER/files) - echo "FILES_RESPONSE: $FILES_RESPONSE" + FILES_RESPONSE=$(gh api --paginate /repos/$GITHUB_REPOSITORY/pulls/$PULL_NUMBER/files) - HAS_CHANGED_RELEASE_NOTES=$(echo $FILES_RESPONSE | jq '.[].filename' | jq -s '. | any(. == env.CURRENT_RELEASE_FILE_PATH)') + HAS_CHANGED_RELEASE_NOTES=$(echo "$FILES_RESPONSE" | jq -r --arg path "$CURRENT_RELEASE_FILE_PATH" '[.[].filename == $path] | any') echo "HAS_CHANGED_RELEASE_NOTES=$HAS_CHANGED_RELEASE_NOTES" >> $GITHUB_OUTPUT - name: Check Release Preparedness requirements