diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index 35cf0baa80..7c56bed8ec 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -2109,24 +2109,35 @@ mixin WidgetInspectorService { summaryTree: true, subtreeDepth: subtreeDepth, service: this, - addAdditionalPropertiesCallback: (DiagnosticsNode node, InspectorSerializationDelegate delegate) { + addAdditionalPropertiesCallback: + (DiagnosticsNode node, InspectorSerializationDelegate delegate) { final Object? value = node.value; - final RenderObject? renderObject = value is Element ? value.renderObject : null; + final RenderObject? renderObject = + value is Element ? value.renderObject : null; if (renderObject == null) { return const {}; } - final DiagnosticsSerializationDelegate renderObjectSerializationDelegate = delegate.copyWith( + final DiagnosticsSerializationDelegate + renderObjectSerializationDelegate = delegate.copyWith( subtreeDepth: 0, includeProperties: true, expandPropertyValues: false, ); final Map additionalJson = { - 'renderObject': renderObject.toDiagnosticsNode().toJsonMap(renderObjectSerializationDelegate), + // Only include renderObject properties separately if this value is not already the renderObject. + // Only include if we are expanding property values to mitigate the risk of infinite loops if + // RenderObjects have properties that are Element objects. + if (value is! RenderObject && delegate.expandPropertyValues) + 'renderObject': renderObject + .toDiagnosticsNode() + .toJsonMap(renderObjectSerializationDelegate), }; final RenderObject? renderParent = renderObject.parent; - if (renderParent is RenderObject && subtreeDepth > 0) { + if (renderParent != null && + delegate.subtreeDepth > 0 && + delegate.expandPropertyValues) { final Object? parentCreator = renderParent.debugCreator; if (parentCreator is DebugCreator) { additionalJson['parentRenderElement'] = @@ -2147,7 +2158,7 @@ mixin WidgetInspectorService { if (!renderObject.debugNeedsLayout) { // ignore: invalid_use_of_protected_member final Constraints constraints = renderObject.constraints; - final MapconstraintsProperty = { + final Map constraintsProperty = { 'type': constraints.runtimeType.toString(), 'description': constraints.toString(), }; diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index 125f3b83af..29d6b33b36 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -4771,6 +4771,53 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { } expect(error, isNull); }); + + testWidgets( + 'ext.flutter.inspector.getLayoutExplorerNode, on a ToolTip, does not throw StackOverflowError', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/devtools/issues/5946 + const Widget widget = MaterialApp( + home: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Row( + children: [ + Flexible( + child: ColoredBox( + color: Colors.green, + child: Tooltip( + message: 'a', + child: ElevatedButton( + onPressed: null, + child: Text('a'), + ), + ), + ), + ), + ], + ), + ), + ), + ); + await tester.pumpWidget(widget); + + final Element elevatedButton = + tester.element(find.byType(ElevatedButton).first); + service.setSelection(elevatedButton, group); + + final String id = service.toId(elevatedButton, group)!; + + Object? error; + try { + await service.testExtension( + WidgetInspectorServiceExtensions.getLayoutExplorerNode.name, + {'id': id, 'groupName': group, 'subtreeDepth': '1'}, + ); + } catch (e) { + error = e; + } + expect(error, isNull); + }); }); test('ext.flutter.inspector.structuredErrors', () async {