Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit c626460

Browse files
authored
Make _focusDebug not interpolate in debug mode (#119680)
* Make _focusDebug not interpolate in debug mode * Add test * Revert undesired change * Fix test to fail before too * Remove accidental skips * Switch to using a generating closure for arguments. * Remove a word
1 parent 66b2ca6 commit c626460

File tree

2 files changed

+107
-28
lines changed

2 files changed

+107
-28
lines changed

packages/flutter/lib/src/widgets/focus_manager.dart

+50-28
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,38 @@ import 'framework.dart';
2222
/// be logged.
2323
bool debugFocusChanges = false;
2424

25-
bool _focusDebug(String message, [Iterable<String>? details]) {
26-
if (debugFocusChanges) {
27-
debugPrint('FOCUS: $message');
28-
if (details != null && details.isNotEmpty) {
29-
for (final String detail in details) {
30-
debugPrint(' $detail');
31-
}
25+
// When using _focusDebug, always call it like so:
26+
//
27+
// assert(_focusDebug(() => 'Blah $foo'));
28+
//
29+
// It needs to be inside the assert in order to be removed in release mode, and
30+
// it needs to use a closure to generate the string in order to avoid string
31+
// interpolation when debugFocusChanges is false.
32+
//
33+
// It will throw a StateError if you try to call it when the app is in release
34+
// mode.
35+
bool _focusDebug(
36+
String Function() messageFunc, [
37+
Iterable<Object> Function()? detailsFunc,
38+
]) {
39+
if (kReleaseMode) {
40+
throw StateError(
41+
'_focusDebug was called in Release mode. It should always be wrapped in '
42+
'an assert. Always call _focusDebug like so:\n'
43+
r" assert(_focusDebug(() => 'Blah $foo'));"
44+
);
45+
}
46+
if (!debugFocusChanges) {
47+
return true;
48+
}
49+
debugPrint('FOCUS: ${messageFunc()}');
50+
final Iterable<Object> details = detailsFunc?.call() ?? const <Object>[];
51+
if (details.isNotEmpty) {
52+
for (final Object detail in details) {
53+
debugPrint(' $detail');
3254
}
3355
}
34-
// Return true so that it can be easily used inside of an assert.
56+
// Return true so that it can be used inside of an assert.
3557
return true;
3658
}
3759

@@ -118,10 +140,10 @@ class _Autofocus {
118140
&& scope.focusedChild == null
119141
&& autofocusNode.ancestors.contains(scope);
120142
if (shouldApply) {
121-
assert(_focusDebug('Applying autofocus: $autofocusNode'));
143+
assert(_focusDebug(() => 'Applying autofocus: $autofocusNode'));
122144
autofocusNode._doRequestFocus(findFirstFocus: true);
123145
} else {
124-
assert(_focusDebug('Autofocus request discarded for node: $autofocusNode.'));
146+
assert(_focusDebug(() => 'Autofocus request discarded for node: $autofocusNode.'));
125147
}
126148
}
127149
}
@@ -169,7 +191,7 @@ class FocusAttachment {
169191
///
170192
/// Calling [FocusNode.dispose] will also automatically detach the node.
171193
void detach() {
172-
assert(_focusDebug('Detaching node:', <String>[_node.toString(), 'With enclosing scope ${_node.enclosingScope}']));
194+
assert(_focusDebug(() => 'Detaching node:', () => <Object>[_node, 'With enclosing scope ${_node.enclosingScope}']));
173195
if (isAttached) {
174196
if (_node.hasPrimaryFocus || (_node._manager != null && _node._manager!._markedForFocus == _node)) {
175197
_node.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
@@ -877,7 +899,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
877899
scope._doRequestFocus(findFirstFocus: true);
878900
break;
879901
}
880-
assert(_focusDebug('Unfocused node:', <String>['primary focus was $this', 'next focus will be ${_manager?._markedForFocus}']));
902+
assert(_focusDebug(() => 'Unfocused node:', () => <Object>['primary focus was $this', 'next focus will be ${_manager?._markedForFocus}']));
881903
}
882904

883905
/// Removes the keyboard token from this focus node if it has one.
@@ -1063,7 +1085,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
10631085
// Note that this is overridden in FocusScopeNode.
10641086
void _doRequestFocus({required bool findFirstFocus}) {
10651087
if (!canRequestFocus) {
1066-
assert(_focusDebug('Node NOT requesting focus because canRequestFocus is false: $this'));
1088+
assert(_focusDebug(() => 'Node NOT requesting focus because canRequestFocus is false: $this'));
10671089
return;
10681090
}
10691091
// If the node isn't part of the tree, then we just defer the focus request
@@ -1078,7 +1100,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
10781100
return;
10791101
}
10801102
_hasKeyboardToken = true;
1081-
assert(_focusDebug('Node requesting focus: $this'));
1103+
assert(_focusDebug(() => 'Node requesting focus: $this'));
10821104
_markNextFocus(this);
10831105
}
10841106

@@ -1109,7 +1131,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
11091131
FocusNode scopeFocus = this;
11101132
for (final FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) {
11111133
assert(scopeFocus != ancestor, 'Somehow made a loop by setting focusedChild to its scope.');
1112-
assert(_focusDebug('Setting $scopeFocus as focused child for scope:', <String>[ancestor.toString()]));
1134+
assert(_focusDebug(() => 'Setting $scopeFocus as focused child for scope:', () => <Object>[ancestor]));
11131135
// Remove it anywhere in the focused child history.
11141136
ancestor._focusedChildren.remove(scopeFocus);
11151137
// Add it to the end of the list, which is also the top of the queue: The
@@ -1276,7 +1298,7 @@ class FocusScopeNode extends FocusNode {
12761298
/// tree, the given scope must be a descendant of this scope.
12771299
void setFirstFocus(FocusScopeNode scope) {
12781300
assert(scope != this, 'Unexpected self-reference in setFirstFocus.');
1279-
assert(_focusDebug('Setting scope as first focus in $this to node:', <String>[scope.toString()]));
1301+
assert(_focusDebug(() => 'Setting scope as first focus in $this to node:', () => <Object>[scope]));
12801302
if (scope._parent == null) {
12811303
_reparent(scope);
12821304
}
@@ -1306,7 +1328,7 @@ class FocusScopeNode extends FocusNode {
13061328
}
13071329

13081330
assert(_manager != null);
1309-
assert(_focusDebug('Autofocus scheduled for $node: scope $this'));
1331+
assert(_focusDebug(() => 'Autofocus scheduled for $node: scope $this'));
13101332
_manager?._pendingAutofocuses.add(_Autofocus(scope: this, autofocusNode: node));
13111333
_manager?._markNeedsUpdate();
13121334
}
@@ -1542,7 +1564,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
15421564
void _markDetached(FocusNode node) {
15431565
// The node has been removed from the tree, so it no longer needs to be
15441566
// notified of changes.
1545-
assert(_focusDebug('Node was detached: $node'));
1567+
assert(_focusDebug(() => 'Node was detached: $node'));
15461568
if (_primaryFocus == node) {
15471569
_primaryFocus = null;
15481570
}
@@ -1551,7 +1573,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
15511573

15521574
void _markPropertiesChanged(FocusNode node) {
15531575
_markNeedsUpdate();
1554-
assert(_focusDebug('Properties changed for node $node.'));
1576+
assert(_focusDebug(() => 'Properties changed for node $node.'));
15551577
_dirtyNodes.add(node);
15561578
}
15571579

@@ -1575,7 +1597,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
15751597
// Request that an update be scheduled, optionally requesting focus for the
15761598
// given newFocus node.
15771599
void _markNeedsUpdate() {
1578-
assert(_focusDebug('Scheduling update, current focus is $_primaryFocus, next focus will be $_markedForFocus'));
1600+
assert(_focusDebug(() => 'Scheduling update, current focus is $_primaryFocus, next focus will be $_markedForFocus'));
15791601
if (_haveScheduledUpdate) {
15801602
return;
15811603
}
@@ -1597,7 +1619,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
15971619
// then revert to the root scope.
15981620
_markedForFocus = rootScope;
15991621
}
1600-
assert(_focusDebug('Refreshing focus state. Next focus will be $_markedForFocus'));
1622+
assert(_focusDebug(() => 'Refreshing focus state. Next focus will be $_markedForFocus'));
16011623
// A node has requested to be the next focus, and isn't already the primary
16021624
// focus.
16031625
if (_markedForFocus != null && _markedForFocus != _primaryFocus) {
@@ -1613,7 +1635,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
16131635
}
16141636
assert(_markedForFocus == null);
16151637
if (previousFocus != _primaryFocus) {
1616-
assert(_focusDebug('Updating focus from $previousFocus to $_primaryFocus'));
1638+
assert(_focusDebug(() => 'Updating focus from $previousFocus to $_primaryFocus'));
16171639
if (previousFocus != null) {
16181640
_dirtyNodes.add(previousFocus);
16191641
}
@@ -1624,7 +1646,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
16241646
for (final FocusNode node in _dirtyNodes) {
16251647
node._notify();
16261648
}
1627-
assert(_focusDebug('Notified ${_dirtyNodes.length} dirty nodes:', _dirtyNodes.toList().map<String>((FocusNode node) => node.toString())));
1649+
assert(_focusDebug(() => 'Notified ${_dirtyNodes.length} dirty nodes:', () => _dirtyNodes));
16281650
_dirtyNodes.clear();
16291651
if (previousFocus != _primaryFocus) {
16301652
notifyListeners();
@@ -1766,9 +1788,9 @@ class _HighlightModeManager {
17661788
_lastInteractionWasTouch = false;
17671789
updateMode();
17681790

1769-
assert(_focusDebug('Received key event $message'));
1791+
assert(_focusDebug(() => 'Received key event $message'));
17701792
if (FocusManager.instance.primaryFocus == null) {
1771-
assert(_focusDebug('No primary focus for key event, ignored: $message'));
1793+
assert(_focusDebug(() => 'No primary focus for key event, ignored: $message'));
17721794
return false;
17731795
}
17741796

@@ -1794,11 +1816,11 @@ class _HighlightModeManager {
17941816
case KeyEventResult.ignored:
17951817
continue;
17961818
case KeyEventResult.handled:
1797-
assert(_focusDebug('Node $node handled key event $message.'));
1819+
assert(_focusDebug(() => 'Node $node handled key event $message.'));
17981820
handled = true;
17991821
break;
18001822
case KeyEventResult.skipRemainingHandlers:
1801-
assert(_focusDebug('Node $node stopped key event propagation: $message.'));
1823+
assert(_focusDebug(() => 'Node $node stopped key event propagation: $message.'));
18021824
handled = false;
18031825
break;
18041826
}
@@ -1808,7 +1830,7 @@ class _HighlightModeManager {
18081830
break;
18091831
}
18101832
if (!handled) {
1811-
assert(_focusDebug('Key event not handled by anyone: $message.'));
1833+
assert(_focusDebug(() => 'Key event not handled by anyone: $message.'));
18121834
}
18131835
return handled;
18141836
}

packages/flutter/test/widgets/focus_manager_test.dart

+57
Original file line numberDiff line numberDiff line change
@@ -1700,4 +1700,61 @@ void main() {
17001700
expect(messagesStr, contains('FOCUS: Notified 2 dirty nodes'));
17011701
expect(messagesStr, contains(RegExp(r'FOCUS: Scheduling update, current focus is null, next focus will be FocusScopeNode#.*parent1')));
17021702
});
1703+
1704+
testWidgets("doesn't call toString on a focus node when debugFocusChanges is false", (WidgetTester tester) async {
1705+
final bool oldDebugFocusChanges = debugFocusChanges;
1706+
final DebugPrintCallback oldDebugPrint = debugPrint;
1707+
final StringBuffer messages = StringBuffer();
1708+
debugPrint = (String? message, {int? wrapWidth}) {
1709+
messages.writeln(message ?? '');
1710+
};
1711+
Future<void> testDebugFocusChanges() async {
1712+
final BuildContext context = await setupWidget(tester);
1713+
final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
1714+
final FocusAttachment parent1Attachment = parent1.attach(context);
1715+
final FocusNode child1 = debugFocusChanges ? FocusNode(debugLabel: 'child1') : _LoggingTestFocusNode(debugLabel: 'child1');
1716+
final FocusAttachment child1Attachment = child1.attach(context);
1717+
parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
1718+
child1Attachment.reparent(parent: parent1);
1719+
1720+
child1.requestFocus();
1721+
await tester.pump();
1722+
child1.dispose();
1723+
parent1.dispose();
1724+
await tester.pump();
1725+
}
1726+
try {
1727+
debugFocusChanges = false;
1728+
await testDebugFocusChanges();
1729+
expect(messages, isEmpty);
1730+
expect(tester.takeException(), isNull);
1731+
debugFocusChanges = true;
1732+
await testDebugFocusChanges();
1733+
expect(messages.toString(), contains('FOCUS: Notified 3 dirty nodes:'));
1734+
expect(tester.takeException(), isNull);
1735+
} finally {
1736+
debugFocusChanges = oldDebugFocusChanges;
1737+
debugPrint = oldDebugPrint;
1738+
}
1739+
});
1740+
}
1741+
1742+
class _LoggingTestFocusNode extends FocusNode {
1743+
_LoggingTestFocusNode({super.debugLabel});
1744+
1745+
@override
1746+
String toString({
1747+
DiagnosticLevel minLevel = DiagnosticLevel.debug,
1748+
}) {
1749+
throw StateError("Shouldn't call toString here");
1750+
}
1751+
1752+
@override
1753+
String toStringDeep({
1754+
String prefixLineOne = '',
1755+
String? prefixOtherLines,
1756+
DiagnosticLevel minLevel = DiagnosticLevel.debug,
1757+
}) {
1758+
throw StateError("Shouldn't call toStringDeep here");
1759+
}
17031760
}

0 commit comments

Comments
 (0)