Commit daf5c312 authored by Hixie's avatar Hixie

Improve exception reporting in Widgets framework

This specifically improves the reporting of exceptions in syncChild(),
and makes the way we've been adding information to toStringName() less
ad-hoc and easier to extend.
parent dfd821e5
...@@ -140,7 +140,6 @@ abstract class GlobalKey extends Key { ...@@ -140,7 +140,6 @@ abstract class GlobalKey extends Key {
if (_syncedKeys.isEmpty && _removedKeys.isEmpty) if (_syncedKeys.isEmpty && _removedKeys.isEmpty)
return; return;
try { try {
for (GlobalKey key in _syncedKeys) { for (GlobalKey key in _syncedKeys) {
Widget widget = _registry[key]; Widget widget = _registry[key];
if (widget != null && _syncListeners.containsKey(key)) { if (widget != null && _syncListeners.containsKey(key)) {
...@@ -149,7 +148,6 @@ abstract class GlobalKey extends Key { ...@@ -149,7 +148,6 @@ abstract class GlobalKey extends Key {
listener(key, widget); listener(key, widget);
} }
} }
for (GlobalKey key in _removedKeys) { for (GlobalKey key in _removedKeys) {
if (!_registry.containsKey(key) && _removeListeners.containsKey(key)) { if (!_registry.containsKey(key) && _removeListeners.containsKey(key)) {
Set<GlobalKeyRemoveListener> localListeners = new Set<GlobalKeyRemoveListener>.from(_removeListeners[key]); Set<GlobalKeyRemoveListener> localListeners = new Set<GlobalKeyRemoveListener>.from(_removeListeners[key]);
...@@ -388,101 +386,113 @@ abstract class Widget { ...@@ -388,101 +386,113 @@ abstract class Widget {
// Returns the child which should be retained as the child of this node. // Returns the child which should be retained as the child of this node.
Widget syncChild(Widget newNode, Widget oldNode, dynamic slot) { Widget syncChild(Widget newNode, Widget oldNode, dynamic slot) {
String debugDetails;
if (newNode == oldNode) {
// TODO(ianh): Simplify next few asserts once the analyzer is cleverer
assert(newNode is! RenderObjectWrapper ||
(newNode is RenderObjectWrapper && newNode._ancestor != null)); // if the child didn't change, it had better be configured
assert(newNode is! StatefulComponent ||
(newNode is StatefulComponent && newNode._isStateInitialized)); // if the child didn't change, it had better be configured
if (newNode != null) {
newNode.setParent(this);
newNode._markAsFromCurrentGeneration();
}
return newNode; // Nothing to do. Subtrees must be identical.
}
assert(() { assert(() {
'You have probably used a single instance of a Widget in two different places in the widget tree. Widgets can only be used in one place at a time.'; // we save this information early because by the time the exception fires we might have changed everything around
return newNode == null || newNode.isFromOldGeneration; debugDetails = " old child: ${oldNode?.toStringName()}\n new child: ${newNode?.toStringName()}";
return true;
}); });
try {
if (oldNode != null && !oldNode.isFromOldGeneration) if (newNode == oldNode) {
oldNode = null; // TODO(ianh): Simplify next few asserts once the analyzer is cleverer
assert(newNode is! RenderObjectWrapper ||
if (newNode == null) { (newNode is RenderObjectWrapper && newNode._ancestor != null)); // if the child didn't change, it had better be configured
// the child in this slot has gone away assert(newNode is! StatefulComponent ||
// remove it if they old one is still here (newNode is StatefulComponent && newNode._isStateInitialized)); // if the child didn't change, it had better be configured
if (oldNode != null) { if (newNode != null) {
assert(oldNode != null); newNode.setParent(this);
assert(oldNode.isFromOldGeneration); newNode._markAsFromCurrentGeneration();
assert(oldNode.mounted); }
oldNode.detachRenderObject(); return newNode; // Nothing to do. Subtrees must be identical.
oldNode.remove();
assert(!oldNode.mounted);
// we don't update the generation of oldNode, because there's
// still a chance it could be reused as-is later in the tree.
} }
return null;
}
if (oldNode != null) { assert(() {
assert(newNode != null); 'You have probably used a single instance of a Widget in two different places in the widget tree. Widgets can only be used in one place at a time.';
assert(newNode.isFromOldGeneration); return newNode == null || newNode.isFromOldGeneration;
assert(oldNode.isFromOldGeneration); });
if (!Widget._canSync(newNode, oldNode)) {
assert(oldNode.mounted); if (oldNode != null && !oldNode.isFromOldGeneration)
// We want to handle the case where there is a removal of zero
// or more widgets. In this case, we should be able to sync
// ourselves with a Widget that is a descendant of the
// oldNode, skipping the nodes in between. Let's try that.
Widget deadNode = oldNode;
Widget candidate = _getCandidateSingleChildFrom(oldNode);
oldNode = null; oldNode = null;
while (candidate != null) { if (newNode == null) {
if (Widget._canSync(newNode, candidate)) { // the child in this slot has gone away
assert(candidate.parent != null); // remove it if they old one is still here
assert(candidate.parent.singleChild == candidate); if (oldNode != null) {
if (candidate.renderObject != deadNode.renderObject) { assert(oldNode != null);
// TODO(ianh): Handle removal across RenderNode boundaries assert(oldNode.isFromOldGeneration);
} else { assert(oldNode.mounted);
candidate.parent.takeChild(); oldNode.detachRenderObject();
oldNode = candidate; oldNode.remove();
} assert(!oldNode.mounted);
break; // we don't update the generation of oldNode, because there's
} // still a chance it could be reused as-is later in the tree.
candidate = _getCandidateSingleChildFrom(candidate);
} }
return null;
// TODO(ianh): Handle insertion, too...
if (oldNode == null)
deadNode.detachRenderObject();
deadNode.remove();
} }
if (oldNode != null) { if (oldNode != null) {
assert(newNode != null);
assert(newNode.isFromOldGeneration); assert(newNode.isFromOldGeneration);
assert(oldNode.isFromOldGeneration); assert(oldNode.isFromOldGeneration);
assert(Widget._canSync(newNode, oldNode)); if (!Widget._canSync(newNode, oldNode)) {
if (oldNode.retainStatefulNodeIfPossible(newNode)) {
assert(oldNode.mounted); assert(oldNode.mounted);
assert(!newNode.mounted); // We want to handle the case where there is a removal of zero
oldNode.setParent(this); // or more widgets. In this case, we should be able to sync
oldNode._sync(newNode, slot); // ourselves with a Widget that is a descendant of the
assert(oldNode.renderObject is RenderObject); // oldNode, skipping the nodes in between. Let's try that.
return oldNode; Widget deadNode = oldNode;
} else { Widget candidate = _getCandidateSingleChildFrom(oldNode);
oldNode.setParent(null); oldNode = null;
while (candidate != null) {
if (Widget._canSync(newNode, candidate)) {
assert(candidate.parent != null);
assert(candidate.parent.singleChild == candidate);
if (candidate.renderObject != deadNode.renderObject) {
// TODO(ianh): Handle removal across RenderNode boundaries
} else {
candidate.parent.takeChild();
oldNode = candidate;
}
break;
}
candidate = _getCandidateSingleChildFrom(candidate);
}
// TODO(ianh): Handle insertion, too...
if (oldNode == null)
deadNode.detachRenderObject();
deadNode.remove();
}
if (oldNode != null) {
assert(newNode.isFromOldGeneration);
assert(oldNode.isFromOldGeneration);
assert(Widget._canSync(newNode, oldNode));
if (oldNode.retainStatefulNodeIfPossible(newNode)) {
assert(oldNode.mounted);
assert(!newNode.mounted);
oldNode.setParent(this);
oldNode._sync(newNode, slot);
assert(oldNode.renderObject is RenderObject);
return oldNode;
} else {
oldNode.setParent(null);
}
} }
} }
}
assert(oldNode == null || (oldNode.mounted == false && oldNode.parent == null)); assert(oldNode == null || (oldNode.mounted == false && oldNode.parent == null));
newNode.setParent(this); newNode.setParent(this);
newNode._sync(oldNode, slot); newNode._sync(oldNode, slot);
assert(newNode.renderObject is RenderObject); assert(newNode.renderObject is RenderObject);
return newNode; return newNode;
} catch (e, stack) {
_debugReportException('syncing children of ${this.toStringName()}\n$debugDetails', e, stack);
return null;
}
} }
String _adjustPrefixWithParentCheck(Widget child, String prefix) { String _adjustPrefixWithParentCheck(Widget child, String prefix) {
...@@ -507,19 +517,25 @@ abstract class Widget { ...@@ -507,19 +517,25 @@ abstract class Widget {
nextPrefix = prefix + ' '; nextPrefix = prefix + ' ';
childrenString += lastChild.toString(nextPrefix, _adjustPrefixWithParentCheck(lastChild, nextStartPrefix)); childrenString += lastChild.toString(nextPrefix, _adjustPrefixWithParentCheck(lastChild, nextStartPrefix));
} }
String suffix = ''; return '$startPrefix${toStringName()}\n$childrenString';
}
String toStringName() {
List<String> details = <String>[];
debugAddDetails(details);
String detailString = details.join('; ');
return '$runtimeType($detailString})';
}
void debugAddDetails(List<String> details) {
if (key != null)
details.add('$key');
details.add('hashCode=$hashCode');
details.add(mounted ? 'mounted' : 'not mounted');
String generationString = '';
if (_generation != _currentGeneration) { if (_generation != _currentGeneration) {
int delta = _generation - _currentGeneration; int delta = _generation - _currentGeneration;
String sign = delta < 0 ? '' : '+'; String sign = delta < 0 ? '' : '+';
suffix = ' gen$sign$delta'; details.add('gen$sign$delta');
} }
return '$startPrefix${toStringName()}$suffix\n$childrenString';
}
String toStringName() {
String keyString = key == null ? '' : '$key; ';
String hashCodeString = 'hashCode=$hashCode';
String mountedString = mounted ? '; mounted' : '; not mounted';
return '$runtimeType($keyString$hashCodeString$mountedString)';
} }
// This function can be safely called when the layout is valid. // This function can be safely called when the layout is valid.
...@@ -902,6 +918,11 @@ abstract class Component extends Widget { ...@@ -902,6 +918,11 @@ abstract class Component extends Widget {
Widget build(); Widget build();
void debugAddDetails(List<String> details) {
super.debugAddDetails(details);
if (_dirty)
details.add('dirty');
}
} }
abstract class StatefulComponent extends Component { abstract class StatefulComponent extends Component {
...@@ -964,10 +985,9 @@ abstract class StatefulComponent extends Component { ...@@ -964,10 +985,9 @@ abstract class StatefulComponent extends Component {
_scheduleBuild(); _scheduleBuild();
} }
String toStringName() { void debugAddDetails(List<String> details) {
if (_isStateInitialized) super.debugAddDetails(details);
return 'Stateful ${super.toStringName()}'; details.add(_isStateInitialized ? 'stateful' : 'stateless');
return 'Stateless ${super.toStringName()}';
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment