Unverified Commit 38f84901 authored by Jacob Richman's avatar Jacob Richman Committed by GitHub

Add more structure to errors messages. (#34684)

Breaking change to extremely rarely used ParentDataWidget.debugDescribeInvalidAncestorChain api changing the return type of the method from String to DiagnosticsNode.
parent 1b176c5d
......@@ -728,12 +728,15 @@ class AnimationController extends Animation<double>
void dispose() {
assert(() {
if (_ticker == null) {
throw FlutterError(
'AnimationController.dispose() called more than once.\n'
'A given $runtimeType cannot be disposed more than once.\n'
'The following $runtimeType object was disposed multiple times:\n'
' $this'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('AnimationController.dispose() called more than once.'),
ErrorDescription('A given $runtimeType cannot be disposed more than once.\n'),
DiagnosticsProperty<AnimationController>(
'The following $runtimeType object was disposed multiple times',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
]);
}
return true;
}());
......
......@@ -176,6 +176,19 @@ class ErrorHint extends _ErrorDiagnostic {
ErrorHint._fromParts(List<Object> messageParts) : super._fromParts(messageParts, level:DiagnosticLevel.hint);
}
/// An [ErrorSpacer] creates an empty [DiagnosticsNode], that can be used to
/// tune the spacing between other [DiagnosticsNode] objects.
class ErrorSpacer extends DiagnosticsProperty<void> {
/// Creates an empty space to insert into a list of [DiagnosticNode] objects
/// typically within a [FlutterError] object.
ErrorSpacer() : super(
'',
null,
description: '',
showName: false,
);
}
/// Class for information provided to [FlutterExceptionHandler] callbacks.
///
/// See [FlutterError.onError].
......@@ -407,7 +420,7 @@ class FlutterErrorDetails extends Diagnosticable {
}
}
if (ourFault) {
properties.add(DiagnosticsNode.message(''));
properties.add(ErrorSpacer());
properties.add(ErrorHint(
'Either the assertion indicates an error in the framework itself, or we should '
'provide substantially more information in this error message to help you determine '
......@@ -418,11 +431,11 @@ class FlutterErrorDetails extends Diagnosticable {
}
}
if (stack != null) {
properties.add(DiagnosticsNode.message(''));
properties.add(ErrorSpacer());
properties.add(DiagnosticsStackTrace('When the exception was thrown, this was the stack', stack, stackFilter: stackFilter));
}
if (informationCollector != null) {
properties.add(DiagnosticsNode.message(''));
properties.add(ErrorSpacer());
informationCollector().forEach(properties.add);
}
}
......
......@@ -690,7 +690,7 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
throw FlutterError(
'Steppers must not be nested. The material specification advises '
'that one should avoid embedding steppers within steppers. '
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage\n'
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage'
);
return true;
}());
......
......@@ -1315,20 +1315,22 @@ abstract class RenderBox extends RenderObject {
double getMinIntrinsicWidth(double height) {
assert(() {
if (height == null) {
throw FlutterError(
'The height argument to getMinIntrinsicWidth was null.\n'
'The argument to getMinIntrinsicWidth must not be negative or null. '
'If you do not have a specific height in mind, then pass double.infinity instead.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The height argument to getMinIntrinsicWidth was null.'),
ErrorDescription('The argument to getMinIntrinsicWidth must not be negative or null.'),
ErrorHint('If you do not have a specific height in mind, then pass double.infinity instead.')
]);
}
if (height < 0.0) {
throw FlutterError(
'The height argument to getMinIntrinsicWidth was negative.\n'
'The argument to getMinIntrinsicWidth must not be negative or null. '
'If you perform computations on another height before passing it to '
'getMinIntrinsicWidth, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The height argument to getMinIntrinsicWidth was negative.'),
ErrorDescription('The argument to getMinIntrinsicWidth must not be negative or null.'),
ErrorHint(
'If you perform computations on another height before passing it to '
'getMinIntrinsicWidth, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
),
]);
}
return true;
}());
......@@ -1454,20 +1456,22 @@ abstract class RenderBox extends RenderObject {
double getMaxIntrinsicWidth(double height) {
assert(() {
if (height == null) {
throw FlutterError(
'The height argument to getMaxIntrinsicWidth was null.\n'
'The argument to getMaxIntrinsicWidth must not be negative or null. '
'If you do not have a specific height in mind, then pass double.infinity instead.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The height argument to getMaxIntrinsicWidth was null.'),
ErrorDescription('The argument to getMaxIntrinsicWidth must not be negative or null.'),
ErrorHint('If you do not have a specific height in mind, then pass double.infinity instead.')
]);
}
if (height < 0.0) {
throw FlutterError(
'The height argument to getMaxIntrinsicWidth was negative.\n'
'The argument to getMaxIntrinsicWidth must not be negative or null. '
'If you perform computations on another height before passing it to '
'getMaxIntrinsicWidth, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The height argument to getMaxIntrinsicWidth was negative.'),
ErrorDescription('The argument to getMaxIntrinsicWidth must not be negative or null.'),
ErrorHint(
'If you perform computations on another height before passing it to '
'getMaxIntrinsicWidth, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
)
]);
}
return true;
}());
......@@ -1530,20 +1534,22 @@ abstract class RenderBox extends RenderObject {
double getMinIntrinsicHeight(double width) {
assert(() {
if (width == null) {
throw FlutterError(
'The width argument to getMinIntrinsicHeight was null.\n'
'The argument to getMinIntrinsicHeight must not be negative or null. '
'If you do not have a specific width in mind, then pass double.infinity instead.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The width argument to getMinIntrinsicHeight was null.'),
ErrorDescription('The argument to getMinIntrinsicHeight must not be negative or null.'),
ErrorHint('If you do not have a specific width in mind, then pass double.infinity instead.')
]);
}
if (width < 0.0) {
throw FlutterError(
'The width argument to getMinIntrinsicHeight was negative.\n'
'The argument to getMinIntrinsicHeight must not be negative or null. '
'If you perform computations on another width before passing it to '
'getMinIntrinsicHeight, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The width argument to getMinIntrinsicHeight was negative.'),
ErrorDescription('The argument to getMinIntrinsicHeight must not be negative or null.'),
ErrorHint(
'If you perform computations on another width before passing it to '
'getMinIntrinsicHeight, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
)
]);
}
return true;
}());
......@@ -1603,20 +1609,22 @@ abstract class RenderBox extends RenderObject {
double getMaxIntrinsicHeight(double width) {
assert(() {
if (width == null) {
throw FlutterError(
'The width argument to getMaxIntrinsicHeight was null.\n'
'The argument to getMaxIntrinsicHeight must not be negative or null. '
'If you do not have a specific width in mind, then pass double.infinity instead.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The width argument to getMaxIntrinsicHeight was null.'),
ErrorDescription('The argument to getMaxIntrinsicHeight must not be negative or null.'),
ErrorHint('If you do not have a specific width in mind, then pass double.infinity instead.')
]);
}
if (width < 0.0) {
throw FlutterError(
'The width argument to getMaxIntrinsicHeight was negative.\n'
'The argument to getMaxIntrinsicHeight must not be negative or null. '
'If you perform computations on another width before passing it to '
'getMaxIntrinsicHeight, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The width argument to getMaxIntrinsicHeight was negative.'),
ErrorDescription('The argument to getMaxIntrinsicHeight must not be negative or null.'),
ErrorHint(
'If you perform computations on another width before passing it to '
'getMaxIntrinsicHeight, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
)
]);
}
return true;
}());
......@@ -1753,41 +1761,45 @@ abstract class RenderBox extends RenderObject {
if (value is _DebugSize) {
if (value._owner != this) {
if (value._owner.parent != this) {
throw FlutterError(
'The size property was assigned a size inappropriately.\n'
'The following render object:\n'
' $this\n'
'...was assigned a size obtained from:\n'
' ${value._owner}\n'
'However, this second render object is not, or is no longer, a '
'child of the first, and it is therefore a violation of the '
'RenderBox layout protocol to use that size in the layout of the '
'first render object.\n'
'If the size was obtained at a time where it was valid to read '
'the size (because the second render object above was a child '
'of the first at the time), then it should be adopted using '
'debugAdoptSize at that time.\n'
'If the size comes from a grandchild or a render object from an '
'entirely different part of the render tree, then there is no '
'way to be notified when the size changes and therefore attempts '
'to read that size are almost certainly a source of bugs. A different '
'approach should be used.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The size property was assigned a size inappropriately.'),
describeForError('The following render object'),
value._owner.describeForError('...was assigned a size obtained from'),
ErrorDescription(
'However, this second render object is not, or is no longer, a '
'child of the first, and it is therefore a violation of the '
'RenderBox layout protocol to use that size in the layout of the '
'first render object.'
),
ErrorHint(
'If the size was obtained at a time where it was valid to read '
'the size (because the second render object above was a child '
'of the first at the time), then it should be adopted using '
'debugAdoptSize at that time.'
),
ErrorHint(
'If the size comes from a grandchild or a render object from an '
'entirely different part of the render tree, then there is no '
'way to be notified when the size changes and therefore attempts '
'to read that size are almost certainly a source of bugs. A different '
'approach should be used.'
),
]);
}
if (!value._canBeUsedByParent) {
throw FlutterError(
'A child\'s size was used without setting parentUsesSize.\n'
'The following render object:\n'
' $this\n'
'...was assigned a size obtained from its child:\n'
' ${value._owner}\n'
'However, when the child was laid out, the parentUsesSize argument '
'was not set or set to false. Subsequently this transpired to be '
'inaccurate: the size was nonetheless used by the parent.\n'
'It is important to tell the framework if the size will be used or not '
'as several important performance optimizations can be made if the '
'size will not be used by the parent.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('A child\'s size was used without setting parentUsesSize.'),
describeForError('The following render object'),
value._owner.describeForError('...was assigned a size obtained from its child'),
ErrorDescription(
'However, when the child was laid out, the parentUsesSize argument '
'was not set or set to false. Subsequently this transpired to be '
'inaccurate: the size was nonetheless used by the parent.\n'
'It is important to tell the framework if the size will be used or not '
'as several important performance optimizations can be made if the '
'size will not be used by the parent.'
)
]);
}
}
}
......@@ -2047,12 +2059,14 @@ abstract class RenderBox extends RenderObject {
void performLayout() {
assert(() {
if (!sizedByParent) {
throw FlutterError(
'$runtimeType did not implement performLayout().\n'
'RenderBox subclasses need to either override performLayout() to '
'set a size and lay out any children, or, set sizedByParent to true '
'so that performResize() sizes the render object.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('$runtimeType did not implement performLayout().'),
ErrorHint(
'RenderBox subclasses need to either override performLayout() to '
'set a size and lay out any children, or, set sizedByParent to true '
'so that performResize() sizes the render object.'
)
]);
}
return true;
}());
......@@ -2086,27 +2100,35 @@ abstract class RenderBox extends RenderObject {
assert(() {
if (!hasSize) {
if (debugNeedsLayout) {
throw FlutterError(
'Cannot hit test a render box that has never been laid out.\n'
'The hitTest() method was called on this RenderBox:\n'
' $this\n'
'Unfortunately, this object\'s geometry is not known at this time, '
'probably because it has never been laid out. '
'This means it cannot be accurately hit-tested. If you are trying '
'to perform a hit test during the layout phase itself, make sure '
'you only hit test nodes that have completed layout (e.g. the node\'s '
'children, after their layout() method has been called).'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Cannot hit test a render box that has never been laid out.'),
describeForError('The hitTest() method was called on this RenderBox'),
ErrorDescription(
'Unfortunately, this object\'s geometry is not known at this time, '
'probably because it has never been laid out. '
'This means it cannot be accurately hit-tested.'
),
ErrorHint(
'If you are trying '
'to perform a hit test during the layout phase itself, make sure '
'you only hit test nodes that have completed layout (e.g. the node\'s '
'children, after their layout() method has been called).'
)
]);
}
throw FlutterError(
'Cannot hit test a render box with no size.\n'
'The hitTest() method was called on this RenderBox:\n'
' $this\n'
'Although this node is not marked as needing layout, '
'its size is not set. A RenderBox object must have an '
'explicit size before it can be hit-tested. Make sure '
'that the RenderBox in question sets its size during layout.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Cannot hit test a render box with no size.'),
describeForError('The hitTest() method was called on this RenderBox'),
ErrorDescription(
'Although this node is not marked as needing layout, '
'its size is not set.'
),
ErrorHint(
'A RenderBox object must have an '
'explicit size before it can be hit-tested. Make sure '
'that the RenderBox in question sets its size during layout.'
),
]);
}
return true;
}());
......@@ -2171,19 +2193,19 @@ abstract class RenderBox extends RenderObject {
assert(child.parent == this);
assert(() {
if (child.parentData is! BoxParentData) {
throw FlutterError(
'$runtimeType does not implement applyPaintTransform.\n'
'The following $runtimeType object:\n'
' ${toStringShallow()}\n'
'...did not use a BoxParentData class for the parentData field of the following child:\n'
' ${child.toStringShallow()}\n'
'The $runtimeType class inherits from RenderBox. '
'The default applyPaintTransform implementation provided by RenderBox assumes that the '
'children all use BoxParentData objects for their parentData field. '
'Since $runtimeType does not in fact use that ParentData class for its children, it must '
'provide an implementation of applyPaintTransform that supports the specific ParentData '
'subclass used by its children (which apparently is ${child.parentData.runtimeType}).'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('$runtimeType does not implement applyPaintTransform.'),
describeForError('The following $runtimeType object'),
child.describeForError('...did not use a BoxParentData class for the parentData field of the following child'),
ErrorDescription('The $runtimeType class inherits from RenderBox.'),
ErrorHint(
'The default applyPaintTransform implementation provided by RenderBox assumes that the '
'children all use BoxParentData objects for their parentData field. '
'Since $runtimeType does not in fact use that ParentData class for its children, it must '
'provide an implementation of applyPaintTransform that supports the specific ParentData '
'subclass used by its children (which apparently is ${child.parentData.runtimeType}).'
)
]);
}
return true;
}());
......
......@@ -1194,7 +1194,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
// displaying the truncated children is really useful for command line
// users. Inspector users can see the full tree by clicking on the
// render object so this may not be that useful.
yield describeForError('This RenderObject', style: DiagnosticsTreeStyle.truncateChildren);
yield describeForError('RenderObject', style: DiagnosticsTreeStyle.truncateChildren);
}
));
}
......@@ -2030,14 +2030,17 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
void _paintWithContext(PaintingContext context, Offset offset) {
assert(() {
if (_debugDoingThisPaint) {
throw FlutterError(
'Tried to paint a RenderObject reentrantly.\n'
'The following RenderObject was already being painted when it was '
'painted again:\n'
' ${toStringShallow(joiner: "\n ")}\n'
'Since this typically indicates an infinite recursion, it is '
'disallowed.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Tried to paint a RenderObject reentrantly.'),
describeForError(
'The following RenderObject was already being painted when it was '
'painted again'
),
ErrorDescription(
'Since this typically indicates an infinite recursion, it is '
'disallowed.'
)
]);
}
return true;
}());
......@@ -2052,17 +2055,24 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
return;
assert(() {
if (_needsCompositingBitsUpdate) {
throw FlutterError(
'Tried to paint a RenderObject before its compositing bits were '
'updated.\n'
'The following RenderObject was marked as having dirty compositing '
'bits at the time that it was painted:\n'
' ${toStringShallow(joiner: "\n ")}\n'
'A RenderObject that still has dirty compositing bits cannot be '
'painted because this indicates that the tree has not yet been '
'properly configured for creating the layer tree.\n'
'This usually indicates an error in the Flutter framework itself.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Tried to paint a RenderObject before its compositing bits were '
'updated.'
),
describeForError(
'The following RenderObject was marked as having dirty compositing '
'bits at the time that it was painted',
),
ErrorDescription(
'A RenderObject that still has dirty compositing bits cannot be '
'painted because this indicates that the tree has not yet been '
'properly configured for creating the layer tree.'
),
ErrorHint(
'This usually indicates an error in the Flutter framework itself.'
)
]);
}
return true;
}());
......@@ -2720,21 +2730,31 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject
bool debugValidateChild(RenderObject child) {
assert(() {
if (child is! ChildType) {
throw FlutterError(
'A $runtimeType expected a child of type $ChildType but received a '
'child of type ${child.runtimeType}.\n'
'RenderObjects expect specific types of children because they '
'coordinate with their children during layout and paint. For '
'example, a RenderSliver cannot be the child of a RenderBox because '
'a RenderSliver does not understand the RenderBox layout protocol.\n'
'\n'
'The $runtimeType that expected a $ChildType child was created by:\n'
' $debugCreator\n'
'\n'
'The ${child.runtimeType} that did not match the expected child type '
'was created by:\n'
' ${child.debugCreator}\n'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'A $runtimeType expected a child of type $ChildType but received a '
'child of type ${child.runtimeType}.'
),
ErrorDescription(
'RenderObjects expect specific types of children because they '
'coordinate with their children during layout and paint. For '
'example, a RenderSliver cannot be the child of a RenderBox because '
'a RenderSliver does not understand the RenderBox layout protocol.',
),
ErrorSpacer(),
DiagnosticsProperty<dynamic>(
'The $runtimeType that expected a $ChildType child was created by',
debugCreator,
style: DiagnosticsTreeStyle.errorProperty,
),
ErrorSpacer(),
DiagnosticsProperty<dynamic>(
'The ${child.runtimeType} that did not match the expected child type '
'was created by',
child.debugCreator,
style: DiagnosticsTreeStyle.errorProperty,
)
]);
}
return true;
}());
......@@ -2849,21 +2869,31 @@ mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType
bool debugValidateChild(RenderObject child) {
assert(() {
if (child is! ChildType) {
throw FlutterError(
'A $runtimeType expected a child of type $ChildType but received a '
'child of type ${child.runtimeType}.\n'
'RenderObjects expect specific types of children because they '
'coordinate with their children during layout and paint. For '
'example, a RenderSliver cannot be the child of a RenderBox because '
'a RenderSliver does not understand the RenderBox layout protocol.\n'
'\n'
'The $runtimeType that expected a $ChildType child was created by:\n'
' $debugCreator\n'
'\n'
'The ${child.runtimeType} that did not match the expected child type '
'was created by:\n'
' ${child.debugCreator}\n'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'A $runtimeType expected a child of type $ChildType but received a '
'child of type ${child.runtimeType}.'
),
ErrorDescription(
'RenderObjects expect specific types of children because they '
'coordinate with their children during layout and paint. For '
'example, a RenderSliver cannot be the child of a RenderBox because '
'a RenderSliver does not understand the RenderBox layout protocol.'
),
ErrorSpacer(),
DiagnosticsProperty<dynamic>(
'The $runtimeType that expected a $ChildType child was created by',
debugCreator,
style: DiagnosticsTreeStyle.errorProperty,
),
ErrorSpacer(),
DiagnosticsProperty<dynamic>(
'The ${child.runtimeType} that did not match the expected child type '
'was created by',
child.debugCreator,
style: DiagnosticsTreeStyle.errorProperty,
),
]);
}
return true;
}());
......
......@@ -1282,30 +1282,31 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
assert(!newChildren.any((SemanticsNode child) => child == this));
assert(() {
if (identical(newChildren, _children)) {
final StringBuffer mutationErrors = StringBuffer();
final List<DiagnosticsNode> mutationErrors = <DiagnosticsNode>[];
if (newChildren.length != _debugPreviousSnapshot.length) {
mutationErrors.writeln(
mutationErrors.add(ErrorDescription(
'The list\'s length has changed from ${_debugPreviousSnapshot.length} '
'to ${newChildren.length}.'
);
));
} else {
for (int i = 0; i < newChildren.length; i++) {
if (!identical(newChildren[i], _debugPreviousSnapshot[i])) {
mutationErrors.writeln(
'Child node at position $i was replaced:\n'
'Previous child: ${newChildren[i]}\n'
'New child: ${_debugPreviousSnapshot[i]}\n'
);
if (mutationErrors.isNotEmpty) {
mutationErrors.add(ErrorSpacer());
}
mutationErrors.add(ErrorDescription('Child node at position $i was replaced:'));
mutationErrors.add(newChildren[i].toDiagnosticsNode(name: 'Previous child', style: DiagnosticsTreeStyle.singleLine));
mutationErrors.add(_debugPreviousSnapshot[i].toDiagnosticsNode(name: 'New child', style: DiagnosticsTreeStyle.singleLine));
}
}
}
if (mutationErrors.isNotEmpty) {
throw FlutterError(
'Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.\n'
'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.\n'
'Error details:\n'
'$mutationErrors'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.'),
ErrorHint('Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.'),
ErrorDescription('Error details:'),
...mutationErrors
]);
}
}
assert(!newChildren.any((SemanticsNode node) => node.isMergedIntoParent) || isPartOfNodeMerging);
......
......@@ -164,21 +164,25 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
final String older = _debugReservations[this].toString();
final String newer = parent.toString();
if (older != newer) {
throw FlutterError(
'Multiple widgets used the same GlobalKey.\n'
'The key $this was used by multiple widgets. The parents of those widgets were:\n'
'- $older\n'
'- $newer\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Multiple widgets used the same GlobalKey.'),
ErrorDescription(
'The key $this was used by multiple widgets. The parents of those widgets were:\n'
'- $older\n'
'- $newer\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
)
]);
}
throw FlutterError(
'Multiple widgets used the same GlobalKey.\n'
'The key $this was used by multiple widgets. The parents of those widgets were '
'different widgets that both had the following description:\n'
' $newer\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Multiple widgets used the same GlobalKey.'),
ErrorDescription(
'The key $this was used by multiple widgets. The parents of those widgets were '
'different widgets that both had the following description:\n'
' $parent\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
),
]);
}
_debugReservations[this] = parent;
return true;
......@@ -1098,44 +1102,57 @@ abstract class State<T extends StatefulWidget> extends Diagnosticable {
assert(fn != null);
assert(() {
if (_debugLifecycleState == _StateLifecycle.defunct) {
throw FlutterError(
'setState() called after dispose(): $this\n'
'This error happens if you call setState() on a State object for a widget that '
'no longer appears in the widget tree (e.g., whose parent widget no longer '
'includes the widget in its build). This error can occur when code calls '
'setState() from a timer or an animation callback. The preferred solution is '
'to cancel the timer or stop listening to the animation in the dispose() '
'callback. Another solution is to check the "mounted" property of this '
'object before calling setState() to ensure the object is still in the '
'tree.\n'
'This error might indicate a memory leak if setState() is being called '
'because another object is retaining a reference to this State object '
'after it has been removed from the tree. To avoid memory leaks, '
'consider breaking the reference to this object during dispose().'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('setState() called after dispose(): $this'),
ErrorDescription(
'This error happens if you call setState() on a State object for a widget that '
'no longer appears in the widget tree (e.g., whose parent widget no longer '
'includes the widget in its build). This error can occur when code calls '
'setState() from a timer or an animation callback.'
),
ErrorHint(
'The preferred solution is '
'to cancel the timer or stop listening to the animation in the dispose() '
'callback. Another solution is to check the "mounted" property of this '
'object before calling setState() to ensure the object is still in the '
'tree.'
),
ErrorHint(
'This error might indicate a memory leak if setState() is being called '
'because another object is retaining a reference to this State object '
'after it has been removed from the tree. To avoid memory leaks, '
'consider breaking the reference to this object during dispose().'
),
]);
}
if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
throw FlutterError(
'setState() called in constructor: $this\n'
'This happens when you call setState() on a State object for a widget that '
'hasn\'t been inserted into the widget tree yet. It is not necessary to call '
'setState() in the constructor, since the state is already assumed to be dirty '
'when it is initially created.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('setState() called in constructor: $this'),
ErrorHint(
'This happens when you call setState() on a State object for a widget that '
'hasn\'t been inserted into the widget tree yet. It is not necessary to call '
'setState() in the constructor, since the state is already assumed to be dirty '
'when it is initially created.'
)
]);
}
return true;
}());
final dynamic result = fn() as dynamic;
assert(() {
if (result is Future) {
throw FlutterError(
'setState() callback argument returned a Future.\n'
'The setState() method on $this was called with a closure or method that '
'returned a Future. Maybe it is marked as "async".\n'
'Instead of performing asynchronous work inside a call to setState(), first '
'execute the work (without updating the widget state), and then synchronously '
'update the state inside a call to setState().'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('setState() callback argument returned a Future.'),
ErrorDescription(
'The setState() method on $this was called with a closure or method that '
'returned a Future. Maybe it is marked as "async".'
),
ErrorHint(
'Instead of performing asynchronous work inside a call to setState(), first '
'execute the work (without updating the widget state), and then synchronously '
'update the state inside a call to setState().'
)
]);
}
// We ignore other types of return values so that you can do things like:
// setState(() => x = 3);
......@@ -1433,28 +1450,30 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidge
/// ParentDataWidget subclass. It is called when debugIsValidAncestor()
/// returned false for an ancestor, or when there are extraneous
/// [ParentDataWidget]s in the ancestor chain.
String debugDescribeInvalidAncestorChain({ String description, String ownershipChain, bool foundValidAncestor, Iterable<Widget> badAncestors }) {
Iterable<DiagnosticsNode> debugDescribeInvalidAncestorChain({ String description, DiagnosticsNode ownershipChain, bool foundValidAncestor, Iterable<Widget> badAncestors }) sync* {
assert(T != dynamic);
assert(T != RenderObjectWidget);
String result;
if (!foundValidAncestor) {
result = '$runtimeType widgets must be placed inside $T widgets.\n'
'$description has no $T ancestor at all.\n';
yield ErrorDescription(
'$runtimeType widgets must be placed inside $T widgets.\n'
'$description has no $T ancestor at all.'
);
} else {
assert(badAncestors.isNotEmpty);
result = '$runtimeType widgets must be placed directly inside $T widgets.\n'
'$description has a $T ancestor, but there are other widgets between them:\n';
yield ErrorDescription(
'$runtimeType widgets must be placed directly inside $T widgets.\n'
'$description has a $T ancestor, but there are other widgets between them:'
);
for (Widget ancestor in badAncestors) {
if (ancestor.runtimeType == runtimeType) {
result += '- $ancestor (this is a different $runtimeType than the one with the problem)\n';
yield ErrorDescription('- $ancestor (this is a different $runtimeType than the one with the problem)');
} else {
result += '- $ancestor\n';
yield ErrorDescription('- $ancestor');
}
}
result += 'These widgets cannot come between a $runtimeType and its $T.\n';
yield ErrorDescription('These widgets cannot come between a $runtimeType and its $T.');
}
result += 'The ownership chain for the parent of the offending $runtimeType was:\n $ownershipChain';
return result;
yield ErrorDescription('The ownership chain for the parent of the offending $runtimeType was:\n $ownershipChain');
}
/// Write the data from this widget into the given render object's parent data.
......@@ -2167,16 +2186,18 @@ class BuildOwner {
if (debugPrintScheduleBuildForStacks)
debugPrintStack(label: 'scheduleBuildFor() called for $element${_dirtyElements.contains(element) ? " (ALREADY IN LIST)" : ""}');
if (!element.dirty) {
throw FlutterError(
'scheduleBuildFor() called for a widget that is not marked as dirty.\n'
'The method was called for the following element:\n'
' $element\n'
'This element is not current marked as dirty. Make sure to set the dirty flag before '
'calling scheduleBuildFor().\n'
'If you did not attempt to call scheduleBuildFor() yourself, then this probably '
'indicates a bug in the widgets framework. Please report it: '
'https://github.com/flutter/flutter/issues/new?template=BUG.md'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('scheduleBuildFor() called for a widget that is not marked as dirty.'),
element.describeElement('The method was called for the following element'),
ErrorDescription(
'This element is not current marked as dirty. Make sure to set the dirty flag before '
'calling scheduleBuildFor().'),
ErrorHint(
'If you did not attempt to call scheduleBuildFor() yourself, then this probably '
'indicates a bug in the widgets framework. Please report it:\n'
' https://github.com/flutter/flutter/issues/new?template=BUG.md'
)
]);
}
return true;
}());
......@@ -2185,11 +2206,13 @@ class BuildOwner {
if (debugPrintScheduleBuildForStacks)
debugPrintStack(label: 'BuildOwner.scheduleBuildFor() called; _dirtyElementsNeedsResorting was $_dirtyElementsNeedsResorting (now true); dirty list is: $_dirtyElements');
if (!_debugIsInBuildScope) {
throw FlutterError(
'BuildOwner.scheduleBuildFor() called inappropriately.\n'
'The BuildOwner.scheduleBuildFor() method should only be called while the '
'buildScope() method is actively rebuilding the widget tree.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('BuildOwner.scheduleBuildFor() called inappropriately.'),
ErrorHint(
'The BuildOwner.scheduleBuildFor() method should only be called while the '
'buildScope() method is actively rebuilding the widget tree.'
)
]);
}
return true;
}());
......@@ -2344,12 +2367,11 @@ class BuildOwner {
}
assert(() {
if (_dirtyElements.any((Element element) => element._active && element.dirty)) {
throw FlutterError(
'buildScope missed some dirty elements.\n'
'This probably indicates that the dirty list should have been resorted but was not.\n'
'The list of dirty elements at the end of the buildScope call was:\n'
' $_dirtyElements'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('buildScope missed some dirty elements.'),
ErrorHint('This probably indicates that the dirty list should have been resorted but was not.'),
Element.describeElements('The list of dirty elements at the end of the buildScope call was', _dirtyElements)
]);
}
return true;
}());
......@@ -2459,21 +2481,26 @@ class BuildOwner {
final String they = elementLabels.length == 1 ? 'it' : 'they';
final String think = elementLabels.length == 1 ? 'thinks' : 'think';
final String are = elementLabels.length == 1 ? 'is' : 'are';
throw FlutterError(
'Duplicate GlobalKey$s detected in widget tree.\n'
'The following GlobalKey$s $were specified multiple times in the widget tree. This will lead to '
'parts of the widget tree being truncated unexpectedly, because the second time a key is seen, '
'the previous instance is moved to the new location. The key$s $were:\n'
'- ${keyLabels.join("\n ")}\n'
'This was determined by noticing that after$the widget$s with the above global key$s $were moved '
'out of $their$respective previous parent$s2, $those2 previous parent$s2 never updated during this frame, meaning '
'that $they either did not update at all or updated before the widget$s $were moved, in either case '
'implying that $they still $think that $they should have a child with $those global key$s.\n'
'The specific parent$s2 that did not update after having one or more children forcibly removed '
'due to GlobalKey reparenting $are:\n'
'- ${elementLabels.join("\n ")}\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
);
// TODO(jacobr): make this error more structured to better expose which widgets had problems.
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Duplicate GlobalKey$s detected in widget tree.'),
// TODO(jacobr): refactor this code so the elements are clickable
// in GUI debug tools.
ErrorDescription(
'The following GlobalKey$s $were specified multiple times in the widget tree. This will lead to '
'parts of the widget tree being truncated unexpectedly, because the second time a key is seen, '
'the previous instance is moved to the new location. The key$s $were:\n'
'- ${keyLabels.join("\n ")}\n'
'This was determined by noticing that after$the widget$s with the above global key$s $were moved '
'out of $their$respective previous parent$s2, $those2 previous parent$s2 never updated during this frame, meaning '
'that $they either did not update at all or updated before the widget$s $were moved, in either case '
'implying that $they still $think that $they should have a child with $those global key$s.\n'
'The specific parent$s2 that did not update after having one or more children forcibly removed '
'due to GlobalKey reparenting $are:\n'
'- ${elementLabels.join("\n ")}'
'\nA GlobalKey can only be specified on one widget at a time in the widget tree.'
)
]);
}
}
} finally {
......@@ -2780,13 +2807,15 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
assert(() {
if (owner == null || !owner._debugStateLocked)
return true;
throw FlutterError(
'visitChildElements() called during build.\n'
'The BuildContext.visitChildElements() method can\'t be called during '
'build because the child list is still being updated at that point, '
'so the children might not be constructed yet, or might be old children '
'that are going to be replaced.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('visitChildElements() called during build.'),
ErrorDescription(
'The BuildContext.visitChildElements() method can\'t be called during '
'build because the child list is still being updated at that point, '
'so the children might not be constructed yet, or might be old children '
'that are going to be replaced.'
)
]);
}());
visitChildren(visitor);
}
......@@ -2998,14 +3027,14 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
if (parent != null) {
assert(() {
if (parent == this) {
throw FlutterError(
'A GlobalKey was used multiple times inside one widget\'s child list.\n'
'The offending GlobalKey was: $key\n'
'The parent of the widgets with that key was:\n $parent\n'
'The first child to get instantiated with that key became:\n $element\n'
'The second child that was to be instantiated with that key was:\n $widget\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('A GlobalKey was used multiple times inside one widget\'s child list.'),
DiagnosticsProperty<GlobalKey>('The offending GlobalKey was', key),
parent.describeElement('The parent of the widgets with that key was'),
element.describeElement('The first child to get instantiated with that key became'),
DiagnosticsProperty<Widget>('The second child that was to be instantiated with that key was', widget, style: DiagnosticsTreeStyle.errorProperty),
ErrorDescription('A GlobalKey can only be specified on one widget at a time in the widget tree.')
]);
}
parent.owner._debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans(
parent,
......@@ -3231,108 +3260,117 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
Size get size {
assert(() {
if (_debugLifecycleState != _ElementLifecycle.active) {
throw FlutterError(
'Cannot get size of inactive element.\n'
'In order for an element to have a valid size, the element must be '
'active, which means it is part of the tree. Instead, this element '
'is in the $_debugLifecycleState state.\n'
'The size getter was called for the following element:\n'
' $this\n'
);
// TODO(jacobr): is this a good separation into contract and violation?
// I have added a line of white space.
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Cannot get size of inactive element.'),
ErrorDescription(
'In order for an element to have a valid size, the element must be '
'active, which means it is part of the tree.\n'
'Instead, this element is in the $_debugLifecycleState state.'
),
describeElement('The size getter was called for the following element')
]);
}
if (owner._debugBuilding) {
throw FlutterError(
'Cannot get size during build.\n'
'The size of this render object has not yet been determined because '
'the framework is still in the process of building widgets, which '
'means the render tree for this frame has not yet been determined. '
'The size getter should only be called from paint callbacks or '
'interaction event handlers (e.g. gesture callbacks).\n'
'\n'
'If you need some sizing information during build to decide which '
'widgets to build, consider using a LayoutBuilder widget, which can '
'tell you the layout constraints at a given location in the tree. See '
'<https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html> '
'for more details.\n'
'\n'
'The size getter was called for the following element:\n'
' $this\n'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Cannot get size during build.'),
ErrorDescription(
'The size of this render object has not yet been determined because '
'the framework is still in the process of building widgets, which '
'means the render tree for this frame has not yet been determined. '
'The size getter should only be called from paint callbacks or '
'interaction event handlers (e.g. gesture callbacks).'
),
ErrorSpacer(),
ErrorHint(
'If you need some sizing information during build to decide which '
'widgets to build, consider using a LayoutBuilder widget, which can '
'tell you the layout constraints at a given location in the tree. See '
'<https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html> '
'for more details.'
),
ErrorSpacer(),
describeElement('The size getter was called for the following element')
]);
}
return true;
}());
final RenderObject renderObject = findRenderObject();
assert(() {
if (renderObject == null) {
throw FlutterError(
'Cannot get size without a render object.\n'
'In order for an element to have a valid size, the element must have '
'an associated render object. This element does not have an associated '
'render object, which typically means that the size getter was called '
'too early in the pipeline (e.g., during the build phase) before the '
'framework has created the render tree.\n'
'The size getter was called for the following element:\n'
' $this\n'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Cannot get size without a render object.'),
ErrorHint(
'In order for an element to have a valid size, the element must have '
'an associated render object. This element does not have an associated '
'render object, which typically means that the size getter was called '
'too early in the pipeline (e.g., during the build phase) before the '
'framework has created the render tree.'
),
describeElement('The size getter was called for the following element')
]);
}
if (renderObject is RenderSliver) {
throw FlutterError(
'Cannot get size from a RenderSliver.\n'
'The render object associated with this element is a '
'${renderObject.runtimeType}, which is a subtype of RenderSliver. '
'Slivers do not have a size per se. They have a more elaborate '
'geometry description, which can be accessed by calling '
'findRenderObject and then using the "geometry" getter on the '
'resulting object.\n'
'The size getter was called for the following element:\n'
' $this\n'
'The associated render sliver was:\n'
' ${renderObject.toStringShallow(joiner: "\n ")}'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Cannot get size from a RenderSliver.'),
ErrorHint(
'The render object associated with this element is a '
'${renderObject.runtimeType}, which is a subtype of RenderSliver. '
'Slivers do not have a size per se. They have a more elaborate '
'geometry description, which can be accessed by calling '
'findRenderObject and then using the "geometry" getter on the '
'resulting object.'
),
describeElement('The size getter was called for the following element'),
renderObject.describeForError('The associated render sliver was'),
]);
}
if (renderObject is! RenderBox) {
throw FlutterError(
'Cannot get size from a render object that is not a RenderBox.\n'
'Instead of being a subtype of RenderBox, the render object associated '
'with this element is a ${renderObject.runtimeType}. If this type of '
'render object does have a size, consider calling findRenderObject '
'and extracting its size manually.\n'
'The size getter was called for the following element:\n'
' $this\n'
'The associated render object was:\n'
' ${renderObject.toStringShallow(joiner: "\n ")}'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Cannot get size from a render object that is not a RenderBox.'),
ErrorHint(
'Instead of being a subtype of RenderBox, the render object associated '
'with this element is a ${renderObject.runtimeType}. If this type of '
'render object does have a size, consider calling findRenderObject '
'and extracting its size manually.'
),
describeElement('The size getter was called for the following element'),
renderObject.describeForError('The associated render object was')
]);
}
final RenderBox box = renderObject;
if (!box.hasSize) {
throw FlutterError(
'Cannot get size from a render object that has not been through layout.\n'
'The size of this render object has not yet been determined because '
'this render object has not yet been through layout, which typically '
'means that the size getter was called too early in the pipeline '
'(e.g., during the build phase) before the framework has determined '
'the size and position of the render objects during layout.\n'
'The size getter was called for the following element:\n'
' $this\n'
'The render object from which the size was to be obtained was:\n'
' ${box.toStringShallow(joiner: "\n ")}'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Cannot get size from a render object that has not been through layout.'),
ErrorHint(
'The size of this render object has not yet been determined because '
'this render object has not yet been through layout, which typically '
'means that the size getter was called too early in the pipeline '
'(e.g., during the build phase) before the framework has determined '
'the size and position of the render objects during layout.'
),
describeElement('The size getter was called for the following element'),
box.describeForError('The render object from which the size was to be obtained was')
]);
}
if (box.debugNeedsLayout) {
throw FlutterError(
'Cannot get size from a render object that has been marked dirty for layout.\n'
'The size of this render object is ambiguous because this render object has '
'been modified since it was last laid out, which typically means that the size '
'getter was called too early in the pipeline (e.g., during the build phase) '
'before the framework has determined the size and position of the render '
'objects during layout.\n'
'The size getter was called for the following element:\n'
' $this\n'
'The render object from which the size was to be obtained was:\n'
' ${box.toStringShallow(joiner: "\n ")}\n'
'Consider using debugPrintMarkNeedsLayoutStacks to determine why the render '
'object in question is dirty, if you did not expect this.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Cannot get size from a render object that has been marked dirty for layout.'),
ErrorHint(
'The size of this render object is ambiguous because this render object has '
'been modified since it was last laid out, which typically means that the size '
'getter was called too early in the pipeline (e.g., during the build phase) '
'before the framework has determined the size and position of the render '
'objects during layout.'
),
describeElement('The size getter was called for the following element'),
box.describeForError('The render object from which the size was to be obtained was'),
ErrorHint(
'Consider using debugPrintMarkNeedsLayoutStacks to determine why the render '
'object in question is dirty, if you did not expect this.'
),
]);
}
return true;
}());
......@@ -3348,13 +3386,18 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
bool _debugCheckStateIsActiveForAncestorLookup() {
assert(() {
if (_debugLifecycleState != _ElementLifecycle.active) {
throw FlutterError(
'Looking up a deactivated widget\'s ancestor is unsafe.\n'
'At this point the state of the widget\'s element tree is no longer '
'stable. To safely refer to a widget\'s ancestor in its dispose() method, '
'save a reference to the ancestor by calling inheritFromWidgetOfExactType() '
'in the widget\'s didChangeDependencies() method.\n'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Looking up a deactivated widget\'s ancestor is unsafe.'),
ErrorDescription(
'At this point the state of the widget\'s element tree is no longer '
'stable.'
),
ErrorHint(
'To safely refer to a widget\'s ancestor in its dispose() method, '
'save a reference to the ancestor by calling inheritFromWidgetOfExactType() '
'in the widget\'s didChangeDependencies() method.'
)
]);
}
return true;
}());
......@@ -3468,17 +3511,21 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
bool _debugCheckOwnerBuildTargetExists(String methodName) {
assert(() {
if (owner._debugCurrentBuildTarget == null) {
throw FlutterError(
'$methodName for ${widget.runtimeType} was called at an '
'inappropriate time.\n'
'It may only be called while the widgets are being built. A possible '
'cause of this error is when $methodName is called during '
'one of:\n'
' * network I/O event\n'
' * file I/O event\n'
' * timer\n'
' * microtask (caused by Future.then, async/await, scheduleMicrotask)'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'$methodName for ${widget.runtimeType} was called at an '
'inappropriate time.'
),
ErrorDescription('It may only be called while the widgets are being built.'),
ErrorHint(
'A possible cause of this error is when $methodName is called during '
'one of:\n'
' * network I/O event\n'
' * file I/O event\n'
' * timer\n'
' * microtask (caused by Future.then, async/await, scheduleMicrotask)'
)
]);
}
return true;
}());
......@@ -3603,30 +3650,36 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
if (_debugIsInScope(owner._debugCurrentBuildTarget))
return true;
if (!_debugAllowIgnoredCallsToMarkNeedsBuild) {
throw FlutterError(
'setState() or markNeedsBuild() called during build.\n'
'This ${widget.runtimeType} widget cannot be marked as needing to build because the framework '
'is already in the process of building widgets. A widget can be marked as '
'needing to be built during the build phase only if one of its ancestors '
'is currently building. This exception is allowed because the framework '
'builds parent widgets before children, which means a dirty descendant '
'will always be built. Otherwise, the framework might not visit this '
'widget during this build phase.\n'
'The widget on which setState() or markNeedsBuild() was called was:\n'
' $this\n'
'${owner._debugCurrentBuildTarget == null ? "" : "The widget which was currently being built when the offending call was made was:\n ${owner._debugCurrentBuildTarget}"}'
);
final List<DiagnosticsNode> information = <DiagnosticsNode>[
ErrorSummary('setState() or markNeedsBuild() called during build.'),
ErrorDescription(
'This ${widget.runtimeType} widget cannot be marked as needing to build because the framework '
'is already in the process of building widgets. A widget can be marked as '
'needing to be built during the build phase only if one of its ancestors '
'is currently building. This exception is allowed because the framework '
'builds parent widgets before children, which means a dirty descendant '
'will always be built. Otherwise, the framework might not visit this '
'widget during this build phase.'
),
describeElement(
'The widget on which setState() or markNeedsBuild() was called was',
)
];
if (owner._debugCurrentBuildTarget != null)
information.add(owner._debugCurrentBuildTarget.describeWidget('The widget which was currently being built when the offending call was made was'));
throw FlutterError.fromParts(information);
}
assert(dirty); // can only get here if we're not in scope, but ignored calls are allowed, and our call would somehow be ignored (since we're already dirty)
} else if (owner._debugStateLocked) {
assert(!_debugAllowIgnoredCallsToMarkNeedsBuild);
throw FlutterError(
'setState() or markNeedsBuild() called when widget tree was locked.\n'
'This ${widget.runtimeType} widget cannot be marked as needing to build '
'because the framework is locked.\n'
'The widget on which setState() or markNeedsBuild() was called was:\n'
' $this\n'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('setState() or markNeedsBuild() called when widget tree was locked.'),
ErrorDescription(
'This ${widget.runtimeType} widget cannot be marked as needing to build '
'because the framework is locked.'
),
describeElement('The widget on which setState() or markNeedsBuild() was called was')
]);
}
return true;
}());
......@@ -3937,12 +3990,14 @@ class StatefulElement extends ComponentElement {
super(widget) {
assert(() {
if (!_state._debugTypesAreRight(widget)) {
throw FlutterError(
'StatefulWidget.createState must return a subtype of State<${widget.runtimeType}>\n'
'The createState function for ${widget.runtimeType} returned a state '
'of type ${_state.runtimeType}, which is not a subtype of '
'State<${widget.runtimeType}>, violating the contract for createState.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('StatefulWidget.createState must return a subtype of State<${widget.runtimeType}>'),
ErrorDescription(
'The createState function for ${widget.runtimeType} returned a state '
'of type ${_state.runtimeType}, which is not a subtype of '
'State<${widget.runtimeType}>, violating the contract for createState.'
)
]);
}
return true;
}());
......@@ -3978,12 +4033,14 @@ class StatefulElement extends ComponentElement {
final dynamic debugCheckForReturnedFuture = _state.initState() as dynamic;
assert(() {
if (debugCheckForReturnedFuture is Future) {
throw FlutterError(
'${_state.runtimeType}.initState() returned a Future.\n'
'State.initState() must be a void method without an `async` keyword.\n'
'Rather than awaiting on asynchronous work directly inside of initState,\n'
'call a separate method to do this work without awaiting it.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('${_state.runtimeType}.initState() returned a Future.'),
ErrorDescription('State.initState() must be a void method without an `async` keyword.'),
ErrorHint(
'Rather than awaiting on asynchronous work directly inside of initState, '
'call a separate method to do this work without awaiting it.'
)
]);
}
return true;
}());
......@@ -4011,12 +4068,14 @@ class StatefulElement extends ComponentElement {
final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;
assert(() {
if (debugCheckForReturnedFuture is Future) {
throw FlutterError(
'${_state.runtimeType}.didUpdateWidget() returned a Future.\n'
'State.didUpdateWidget() must be a void method without an `async` keyword.\n'
'Rather than awaiting on asynchronous work directly inside of didUpdateWidget,\n'
'call a separate method to do this work without awaiting it.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('${_state.runtimeType}.didUpdateWidget() returned a Future.'),
ErrorDescription( 'State.didUpdateWidget() must be a void method without an `async` keyword.'),
ErrorHint(
'Rather than awaiting on asynchronous work directly inside of didUpdateWidget, '
'call a separate method to do this work without awaiting it.'
)
]);
}
return true;
}());
......@@ -4049,11 +4108,13 @@ class StatefulElement extends ComponentElement {
assert(() {
if (_state._debugLifecycleState == _StateLifecycle.defunct)
return true;
throw FlutterError(
'${_state.runtimeType}.dispose failed to call super.dispose.\n'
'dispose() implementations must always call their superclass dispose() method, to ensure '
'that all the resources used by the widget are fully released.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('${_state.runtimeType}.dispose failed to call super.dispose.'),
ErrorDescription(
'dispose() implementations must always call their superclass dispose() method, to ensure '
'that all the resources used by the widget are fully released.'
)
]);
}());
_state._element = null;
_state = null;
......@@ -4065,37 +4126,47 @@ class StatefulElement extends ComponentElement {
assert(() {
final Type targetType = ancestor.widget.runtimeType;
if (state._debugLifecycleState == _StateLifecycle.created) {
throw FlutterError(
'inheritFromWidgetOfExactType($targetType) or inheritFromElement() was called before ${_state.runtimeType}.initState() completed.\n'
'When an inherited widget changes, for example if the value of Theme.of() changes, '
'its dependent widgets are rebuilt. If the dependent widget\'s reference to '
'the inherited widget is in a constructor or an initState() method, '
'then the rebuilt dependent widget will not reflect the changes in the '
'inherited widget.\n'
'Typically references to inherited widgets should occur in widget build() methods. Alternatively, '
'initialization based on inherited widgets can be placed in the didChangeDependencies method, which '
'is called after initState and whenever the dependencies change thereafter.'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('inheritFromWidgetOfExactType($targetType) or inheritFromElement() was called before ${_state.runtimeType}.initState() completed.'),
ErrorDescription(
'When an inherited widget changes, for example if the value of Theme.of() changes, '
'its dependent widgets are rebuilt. If the dependent widget\'s reference to '
'the inherited widget is in a constructor or an initState() method, '
'then the rebuilt dependent widget will not reflect the changes in the '
'inherited widget.',
),
ErrorHint(
'Typically references to inherited widgets should occur in widget build() methods. Alternatively, '
'initialization based on inherited widgets can be placed in the didChangeDependencies method, which '
'is called after initState and whenever the dependencies change thereafter.'
)
]);
}
if (state._debugLifecycleState == _StateLifecycle.defunct) {
throw FlutterError(
'inheritFromWidgetOfExactType($targetType) or inheritFromElement() was called after dispose(): $this\n'
'This error happens if you call inheritFromWidgetOfExactType() on the '
'BuildContext for a widget that no longer appears in the widget tree '
'(e.g., whose parent widget no longer includes the widget in its '
'build). This error can occur when code calls '
'inheritFromWidgetOfExactType() from a timer or an animation callback. '
'The preferred solution is to cancel the timer or stop listening to the '
'animation in the dispose() callback. Another solution is to check the '
'"mounted" property of this object before calling '
'inheritFromWidgetOfExactType() to ensure the object is still in the '
'tree.\n'
'This error might indicate a memory leak if '
'inheritFromWidgetOfExactType() is being called because another object '
'is retaining a reference to this State object after it has been '
'removed from the tree. To avoid memory leaks, consider breaking the '
'reference to this object during dispose().'
);
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('inheritFromWidgetOfExactType($targetType) or inheritFromElement() was called after dispose(): $this'),
ErrorDescription(
'This error happens if you call inheritFromWidgetOfExactType() on the '
'BuildContext for a widget that no longer appears in the widget tree '
'(e.g., whose parent widget no longer includes the widget in its '
'build). This error can occur when code calls '
'inheritFromWidgetOfExactType() from a timer or an animation callback.'
),
ErrorHint(
'The preferred solution is to cancel the timer or stop listening to the '
'animation in the dispose() callback. Another solution is to check the '
'"mounted" property of this object before calling '
'inheritFromWidgetOfExactType() to ensure the object is still in the '
'tree.'
),
ErrorHint(
'This error might indicate a memory leak if '
'inheritFromWidgetOfExactType() is being called because another object '
'is retaining a reference to this State object after it has been '
'removed from the tree. To avoid memory leaks, consider breaking the '
'reference to this object during dispose().'
),
]);
}
return true;
}());
......@@ -4192,15 +4263,18 @@ class ParentDataElement<T extends RenderObjectWidget> extends ProxyElement {
}
if (ancestor != null && badAncestors.isEmpty)
return true;
throw FlutterError(
'Incorrect use of ParentDataWidget.\n' +
widget.debugDescribeInvalidAncestorChain(
// TODO(jacobr): switch to describing the invalid parent chain in terms
// of DiagnosticsNode objects when possible.
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Incorrect use of ParentDataWidget.'),
// TODO(jacobr): fix this constructor call to use FlutterErrorBuilder.
...widget.debugDescribeInvalidAncestorChain(
description: '$this',
ownershipChain: parent.debugGetCreatorChain(10),
ownershipChain: ErrorDescription(parent.debugGetCreatorChain(10)),
foundValidAncestor: ancestor != null,
badAncestors: badAncestors,
)
);
),
]);
}());
super.mount(parent, newSlot);
}
......
......@@ -431,7 +431,7 @@ class ClampingScrollPhysics extends ScrollPhysics {
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n'
' $position'
);
}
return true;
......
......@@ -348,7 +348,29 @@ void main() {
expect(controller.repeat, throwsFlutterError);
controller.dispose();
expect(controller.dispose, throwsFlutterError);
FlutterError result;
try {
controller.dispose();
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' AnimationController.dispose() called more than once.\n'
' A given AnimationController cannot be disposed more than once.\n'
' The following AnimationController object was disposed multiple\n'
' times:\n'
' AnimationController#00000(⏮ 0.000; paused; DISPOSED)\n'
),
);
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
result.debugFillProperties(builder);
final DiagnosticsNode controllerProperty = builder.properties.last;
expect(controllerProperty.name, 'The following AnimationController object was disposed multiple times');
expect(controllerProperty.value, controller);
});
test('AnimationController repeat() throws if period is not specified', () {
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -478,6 +479,72 @@ void main() {
expect(find.text('!'), findsOneWidget);
});
testWidgets('Nested stepper error test', (WidgetTester tester) async {
FlutterErrorDetails errorDetails;
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
errorDetails = details;
};
try {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Stepper(
type: StepperType.horizontal,
steps: <Step>[
Step(
title: const Text('Step 2'),
content: Stepper(
type: StepperType.vertical,
steps: const <Step>[
Step(
title: Text('Nested step 1'),
content: Text('A'),
),
Step(
title: Text('Nested step 2'),
content: Text('A'),
),
],
)
),
const Step(
title: Text('Step 1'),
content: Text('A'),
),
],
),
),
),
);
} finally {
FlutterError.onError = oldHandler;
}
expect(errorDetails, isNotNull);
expect(errorDetails.stack, isNotNull);
// Check the ErrorDetails without the stack trace
final List<String> lines = errorDetails.toString().split('\n');
// The lines in the middle of the error message contain the stack trace
// which will change depending on where the test is run.
expect(lines.length, greaterThan(9));
expect(
lines.take(9).join('\n'),
equalsIgnoringHashCodes(
'══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════\n'
'The following assertion was thrown building Stepper(dirty,\n'
'dependencies: [_LocalizationsScope-[GlobalKey#00000]], state:\n'
'_StepperState#00000):\n'
'Steppers must not be nested. The material specification advises\n'
'that one should avoid embedding steppers within steppers.\n'
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage\n'
'\n'
'When the exception was thrown, this was the stack:'
),
);
});
///https://github.com/flutter/flutter/issues/16920
testWidgets('Stepper icons size test', (WidgetTester tester) async {
await tester.pumpWidget(
......
......@@ -9,6 +9,27 @@ import 'package:flutter_test/flutter_test.dart';
import 'rendering_tester.dart';
class MissingPerformLayoutRenderBox extends RenderBox {
void triggerExceptionSettingSizeOutsideOfLayout() {
size = const Size(200, 200);
}
// performLayout is left unimplemented to test the error reported if it is
// missing.
}
class FakeMissingSizeRenderBox extends RenderBox {
@override
void performLayout() {
size = constraints.biggest;
}
@override
bool get hasSize => fakeMissingSize ? false : super.hasSize;
bool fakeMissingSize = false;
}
void main() {
test('should size to render view', () {
final RenderBox root = RenderDecoratedBox(
......@@ -27,6 +48,165 @@ void main() {
expect(root.size.height, equals(600.0));
});
test('performLayout error message', () {
FlutterError result;
try {
MissingPerformLayoutRenderBox().performLayout();
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' MissingPerformLayoutRenderBox did not implement performLayout().\n'
' RenderBox subclasses need to either override performLayout() to\n'
' set a size and lay out any children, or, set sizedByParent to\n'
' true so that performResize() sizes the render object.\n'
)
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'RenderBox subclasses need to either override performLayout() to set a '
'size and lay out any children, or, set sizedByParent to true so that '
'performResize() sizes the render object.'
);
});
test('applyPaintTransform error message', () {
final RenderBox paddingBox = RenderPadding(
padding: const EdgeInsets.all(10.0),
);
final RenderBox root = RenderPadding(
padding: const EdgeInsets.all(10.0),
child: paddingBox,
);
layout(root);
// Trigger the error by overriding the parentData with data that isn't a
// BoxParentData.
paddingBox.parentData = ParentData();
FlutterError result;
try {
root.applyPaintTransform(paddingBox, Matrix4.identity());
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' RenderPadding does not implement applyPaintTransform.\n'
' The following RenderPadding object: RenderPadding#00000 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE:\n'
' parentData: <none>\n'
' constraints: BoxConstraints(w=800.0, h=600.0)\n'
' size: Size(800.0, 600.0)\n'
' padding: EdgeInsets.all(10.0)\n'
' ...did not use a BoxParentData class for the parentData field of the following child:\n'
' RenderPadding#00000 NEEDS-PAINT:\n'
' parentData: <none> (can use size)\n'
' constraints: BoxConstraints(w=780.0, h=580.0)\n'
' size: Size(780.0, 580.0)\n'
' padding: EdgeInsets.all(10.0)\n'
' The RenderPadding class inherits from RenderBox.\n'
' The default applyPaintTransform implementation provided by\n'
' RenderBox assumes that the children all use BoxParentData objects\n'
' for their parentData field. Since RenderPadding does not in fact\n'
' use that ParentData class for its children, it must provide an\n'
' implementation of applyPaintTransform that supports the specific\n'
' ParentData subclass used by its children (which apparently is\n'
' ParentData).\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'The default applyPaintTransform implementation provided by RenderBox '
'assumes that the children all use BoxParentData objects for their '
'parentData field. Since RenderPadding does not in fact use that '
'ParentData class for its children, it must provide an implementation '
'of applyPaintTransform that supports the specific ParentData subclass '
'used by its children (which apparently is ParentData).'
);
});
test('Set size error messages', () {
final RenderBox root = RenderDecoratedBox(
decoration: const BoxDecoration(
color: Color(0xFF00FF00),
),
);
layout(root);
final MissingPerformLayoutRenderBox testBox = MissingPerformLayoutRenderBox();
{
FlutterError result;
try {
testBox.triggerExceptionSettingSizeOutsideOfLayout();
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' RenderBox size setter called incorrectly.\n'
' The size setter was called from outside layout (neither\n'
' performResize() nor performLayout() were being run for this\n'
' object).\n'
' Because this RenderBox has sizedByParent set to false, it must\n'
' set its size in performLayout().\n'
)
);
expect(result.diagnostics.where((DiagnosticsNode node) => node.level == DiagnosticLevel.hint), isEmpty);
}
{
FlutterError result;
try {
testBox.debugAdoptSize(root.size);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The size property was assigned a size inappropriately.\n'
' The following render object: MissingPerformLayoutRenderBox#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED:\n'
' parentData: MISSING\n'
' constraints: MISSING\n'
' size: MISSING\n'
' ...was assigned a size obtained from: RenderDecoratedBox#00000 NEEDS-PAINT:\n'
' parentData: <none>\n'
' constraints: BoxConstraints(w=800.0, h=600.0)\n'
' size: Size(800.0, 600.0)\n'
' decoration: BoxDecoration:\n'
' color: Color(0xff00ff00)\n'
' configuration: ImageConfiguration()\n'
' However, this second render object is not, or is no longer, a\n'
' child of the first, and it is therefore a violation of the\n'
' RenderBox layout protocol to use that size in the layout of the\n'
' first render object.\n'
' If the size was obtained at a time where it was valid to read the\n'
' size (because the second render object above was a child of the\n'
' first at the time), then it should be adopted using\n'
' debugAdoptSize at that time.\n'
' If the size comes from a grandchild or a render object from an\n'
' entirely different part of the render tree, then there is no way\n'
' to be notified when the size changes and therefore attempts to\n'
' read that size are almost certainly a source of bugs. A different\n'
' approach should be used.\n'
)
);
expect(result.diagnostics.where((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).length, 2);
}
});
test('Flex and padding', () {
final RenderBox size = RenderConstrainedBox(
additionalConstraints: const BoxConstraints().tighten(height: 100.0),
......@@ -192,6 +372,230 @@ void main() {
expect(unconstrained.getMaxIntrinsicWidth(100.0), equals(200.0));
});
test ('getMinInstrinsicWidth error handling', () {
final RenderUnconstrainedBox unconstrained = RenderUnconstrainedBox(
textDirection: TextDirection.ltr,
child: RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(width: 200.0),
),
alignment: Alignment.center,
);
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
layout(unconstrained, constraints: viewport);
{
FlutterError result;
try {
unconstrained.getMinIntrinsicWidth(null);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The height argument to getMinIntrinsicWidth was null.\n'
' The argument to getMinIntrinsicWidth must not be negative or\n'
' null.\n'
' If you do not have a specific height in mind, then pass\n'
' double.infinity instead.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you do not have a specific height in mind, then pass double.infinity instead.'
);
}
{
FlutterError result;
try {
unconstrained.getMinIntrinsicWidth(-1);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The height argument to getMinIntrinsicWidth was negative.\n'
' The argument to getMinIntrinsicWidth must not be negative or\n'
' null.\n'
' If you perform computations on another height before passing it\n'
' to getMinIntrinsicWidth, consider using math.max() or\n'
' double.clamp() to force the value into the valid range.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you perform computations on another height before passing it to '
'getMinIntrinsicWidth, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
);
}
{
FlutterError result;
try {
unconstrained.getMinIntrinsicHeight(null);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The width argument to getMinIntrinsicHeight was null.\n'
' The argument to getMinIntrinsicHeight must not be negative or\n'
' null.\n'
' If you do not have a specific width in mind, then pass\n'
' double.infinity instead.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you do not have a specific width in mind, then pass double.infinity instead.'
);
}
{
FlutterError result;
try {
unconstrained.getMinIntrinsicHeight(-1);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The width argument to getMinIntrinsicHeight was negative.\n'
' The argument to getMinIntrinsicHeight must not be negative or\n'
' null.\n'
' If you perform computations on another width before passing it to\n'
' getMinIntrinsicHeight, consider using math.max() or\n'
' double.clamp() to force the value into the valid range.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you perform computations on another width before passing it to '
'getMinIntrinsicHeight, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
);
}
{
FlutterError result;
try {
unconstrained.getMaxIntrinsicWidth(null);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The height argument to getMaxIntrinsicWidth was null.\n'
' The argument to getMaxIntrinsicWidth must not be negative or\n'
' null.\n'
' If you do not have a specific height in mind, then pass\n'
' double.infinity instead.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you do not have a specific height in mind, then pass double.infinity instead.'
);
}
{
FlutterError result;
try {
unconstrained.getMaxIntrinsicWidth(-1);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The height argument to getMaxIntrinsicWidth was negative.\n'
' The argument to getMaxIntrinsicWidth must not be negative or\n'
' null.\n'
' If you perform computations on another height before passing it\n'
' to getMaxIntrinsicWidth, consider using math.max() or\n'
' double.clamp() to force the value into the valid range.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you perform computations on another height before passing it to '
'getMaxIntrinsicWidth, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
);
}
{
FlutterError result;
try {
unconstrained.getMaxIntrinsicHeight(null);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The width argument to getMaxIntrinsicHeight was null.\n'
' The argument to getMaxIntrinsicHeight must not be negative or\n'
' null.\n'
' If you do not have a specific width in mind, then pass\n'
' double.infinity instead.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you do not have a specific width in mind, then pass double.infinity instead.'
);
}
{
FlutterError result;
try {
unconstrained.getMaxIntrinsicHeight(-1);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' The width argument to getMaxIntrinsicHeight was negative.\n'
' The argument to getMaxIntrinsicHeight must not be negative or\n'
' null.\n'
' If you perform computations on another width before passing it to\n'
' getMaxIntrinsicHeight, consider using math.max() or\n'
' double.clamp() to force the value into the valid range.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you perform computations on another width before passing it to '
'getMaxIntrinsicHeight, consider using math.max() or double.clamp() '
'to force the value into the valid range.'
);
}
});
test('UnconstrainedBox.toStringDeep returns useful information', () {
final RenderUnconstrainedBox unconstrained = RenderUnconstrainedBox(
textDirection: TextDirection.ltr,
......@@ -513,6 +917,84 @@ void main() {
expect(positions.single, position + const Offset(20, 30));
positions.clear();
});
test('error message', () {
{
final RenderBox renderObject = RenderConstrainedBox(
additionalConstraints: const BoxConstraints().tighten(height: 100.0),
);
FlutterError result;
try {
final BoxHitTestResult result = BoxHitTestResult();
renderObject.hitTest(result, position: Offset.zero);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' Cannot hit test a render box that has never been laid out.\n'
' The hitTest() method was called on this RenderBox: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED:\n'
' parentData: MISSING\n'
' constraints: MISSING\n'
' size: MISSING\n'
' additionalConstraints: BoxConstraints(0.0<=w<=Infinity, h=100.0)\n'
' Unfortunately, this object\'s geometry is not known at this time,\n'
' probably because it has never been laid out. This means it cannot\n'
' be accurately hit-tested.\n'
' If you are trying to perform a hit test during the layout phase\n'
' itself, make sure you only hit test nodes that have completed\n'
' layout (e.g. the node\'s children, after their layout() method has\n'
' been called).\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'If you are trying to perform a hit test during the layout phase '
'itself, make sure you only hit test nodes that have completed '
'layout (e.g. the node\'s children, after their layout() method has '
'been called).'
);
}
{
FlutterError result;
final FakeMissingSizeRenderBox renderObject = FakeMissingSizeRenderBox();
layout(renderObject);
renderObject.fakeMissingSize = true;
try {
final BoxHitTestResult result = BoxHitTestResult();
renderObject.hitTest(result, position: Offset.zero);
} on FlutterError catch (e) {
result = e;
}
expect(result, isNotNull);
expect(
result.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' Cannot hit test a render box with no size.\n'
' The hitTest() method was called on this RenderBox: FakeMissingSizeRenderBox#00000 NEEDS-PAINT:\n'
' parentData: <none>\n'
' constraints: BoxConstraints(w=800.0, h=600.0)\n'
' size: Size(800.0, 600.0)\n'
' Although this node is not marked as needing layout, its size is\n'
' not set.\n'
' A RenderBox object must have an explicit size before it can be\n'
' hit-tested. Make sure that the RenderBox in question sets its\n'
' size during layout.\n'
),
);
expect(
result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'A RenderBox object must have an explicit size before it can be '
'hit-tested. Make sure that the RenderBox in question sets its '
'size during layout.'
);
}
});
});
}
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/src/binding.dart' show TestWidgetsFlutterBinding;
......@@ -35,6 +36,51 @@ void main() {
renderObject.markNeedsSemanticsUpdate();
expect(renderObject.describeSemanticsConfigurationCallCount, 0);
});
test('ensure errors processing render objects are well formatted', () {
FlutterErrorDetails errorDetails;
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
errorDetails = details;
};
final PipelineOwner owner = PipelineOwner();
final TestThrowingRenderObject renderObject = TestThrowingRenderObject();
try {
renderObject.attach(owner);
renderObject.layout(const BoxConstraints());
} finally {
FlutterError.onError = oldHandler;
}
expect(errorDetails, isNotNull);
expect(errorDetails.stack, isNotNull);
// Check the ErrorDetails without the stack trace
final List<String> lines = errorDetails.toString().split('\n');
// The lines in the middle of the error message contain the stack trace
// which will change depending on where the test is run.
expect(lines.length, greaterThan(8));
expect(
lines.take(4).join('\n'),
equalsIgnoringHashCodes(
'══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══════════════════════\n'
'The following assertion was thrown during performLayout():\n'
'TestThrowingRenderObject does not support performLayout.\n'
)
);
expect(
lines.getRange(lines.length - 8, lines.length).join('\n'),
equalsIgnoringHashCodes(
'\n'
'The following RenderObject was being processed when the exception was fired:\n'
' TestThrowingRenderObject#00000 NEEDS-PAINT:\n'
' parentData: MISSING\n'
' constraints: BoxConstraints(unconstrained)\n'
'This RenderObject has no descendants.\n'
'═════════════════════════════════════════════════════════════════\n'
),
);
});
}
class TestRenderObject extends RenderObject {
......@@ -62,3 +108,22 @@ class TestRenderObject extends RenderObject {
describeSemanticsConfigurationCallCount++;
}
}
class TestThrowingRenderObject extends RenderObject {
@override
void performLayout() {
throw FlutterError('TestThrowingRenderObject does not support performLayout.');
}
@override
void debugAssertDoesMeetConstraints() { }
@override
Rect get paintBounds => null;
@override
void performResize() { }
@override
Rect get semanticBounds => null;
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'rendering_tester.dart';
void main() {
// This test has to be kept separate from object_test.dart because the way
// the rendering_test.dart dependency of this test uses the bindings in not
// compatible with existing tests in object_test.dart.
test('reentrant paint error', () {
FlutterErrorDetails errorDetails;
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
errorDetails = details;
};
final RenderBox root = TestReentrantPaintingErrorRenderBox();
try {
layout(root);
pumpFrame(phase: EnginePhase.paint);
} finally {
FlutterError.onError = oldHandler;
}
expect(errorDetails, isNotNull);
expect(errorDetails.stack, isNotNull);
// Check the ErrorDetails without the stack trace
final List<String> lines = errorDetails.toString().split('\n');
// The lines in the middle of the error message contain the stack trace
// which will change depending on where the test is run.
expect(lines.length, greaterThan(12));
expect(
lines.take(12).join('\n'),
equalsIgnoringHashCodes(
'══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══════════════════════\n'
'The following assertion was thrown during paint():\n'
'Tried to paint a RenderObject reentrantly.\n'
'The following RenderObject was already being painted when it was painted again:\n'
' TestReentrantPaintingErrorRenderBox#00000:\n'
' parentData: <none>\n'
' constraints: BoxConstraints(w=800.0, h=600.0)\n'
' size: Size(100.0, 100.0)\n'
'Since this typically indicates an infinite recursion, it is\n'
'disallowed.\n'
'\n'
'When the exception was thrown, this was the stack:'
),
);
expect(
lines.getRange(lines.length - 8, lines.length).join('\n'),
equalsIgnoringHashCodes(
'The following RenderObject was being processed when the exception was fired:\n'
' TestReentrantPaintingErrorRenderBox#00000:\n'
' parentData: <none>\n'
' constraints: BoxConstraints(w=800.0, h=600.0)\n'
' size: Size(100.0, 100.0)\n'
'This RenderObject has no descendants.\n'
'═════════════════════════════════════════════════════════════════\n'
),
);
});
test('needsCompositingBitsUpdate paint error', () {
FlutterError flutterError;
final RenderBox root = RenderRepaintBoundary(child: RenderSizedBox(const Size(100, 100)));
try {
layout(root);
PaintingContext.repaintCompositedChild(root, debugAlsoPaintedParent: true);
} on FlutterError catch (exception) {
flutterError = exception;
}
expect(flutterError, isNotNull);
// The lines in the middle of the error message contain the stack trace
// which will change depending on where the test is run.
expect(
flutterError.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' Tried to paint a RenderObject before its compositing bits were\n'
' updated.\n'
' The following RenderObject was marked as having dirty compositing bits at the time that it was painted:\n'
' RenderRepaintBoundary#00000 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE:\n'
' needs compositing\n'
' parentData: <none>\n'
' constraints: BoxConstraints(w=800.0, h=600.0)\n'
' layer: OffsetLayer#00000 DETACHED\n'
' size: Size(800.0, 600.0)\n'
' metrics: 0.0% useful (1 bad vs 0 good)\n'
' diagnosis: insufficient data to draw conclusion (less than five\n'
' repaints)\n'
' A RenderObject that still has dirty compositing bits cannot be\n'
' painted because this indicates that the tree has not yet been\n'
' properly configured for creating the layer tree.\n'
' This usually indicates an error in the Flutter framework itself.\n'
),
);
expect(
flutterError.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'This usually indicates an error in the Flutter framework itself.'
);
});
}
class TestReentrantPaintingErrorRenderBox extends RenderBox {
@override
void paint(PaintingContext context, Offset offset) {
// Cause a reentrant painting bug that would show up as a stack overflow if
// it was not for debugging checks in RenderObject.
context.paintChild(this, offset);
}
@override
void performLayout() {
size = const Size(100, 100);
}
}
......@@ -63,6 +63,107 @@ void main() {
expect(node.getSemanticsData().tags, tags);
});
test('mutate existing semantic node list errors', () {
final SemanticsNode node = SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
final SemanticsConfiguration config = SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = true;
final List<SemanticsNode> children = <SemanticsNode>[
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0)
];
node.updateWith(
config: config,
childrenInInversePaintOrder: children
);
children.add( SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(42.0, 42.0, 10.0, 10.0)
);
{
FlutterError error;
try {
node.updateWith(
config: config,
childrenInInversePaintOrder: children
);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.toString(), equalsIgnoringHashCodes(
'Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.\n'
'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.\n'
'Error details:\n'
'The list\'s length has changed from 1 to 2.'
));
expect(
error.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.'
);
}
{
FlutterError error;
final List<SemanticsNode> modifiedChildren = <SemanticsNode>[
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0),
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(10.0, 10.0, 20.0, 20.0)
];
node.updateWith(
config: config,
childrenInInversePaintOrder: modifiedChildren,
);
try {
modifiedChildren[0] = SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(0.0, 0.0, 20.0, 20.0);
modifiedChildren[1] = SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(40.0, 14.0, 20.0, 20.0);
node.updateWith(
config: config,
childrenInInversePaintOrder: modifiedChildren
);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' Failed to replace child semantics nodes because the list of\n'
' `SemanticsNode`s was mutated.\n'
' Instead of mutating the existing list, create a new list\n'
' containing the desired `SemanticsNode`s.\n'
' Error details:\n'
' Child node at position 0 was replaced:\n'
' Previous child: SemanticsNode#6(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(0.0, 0.0, 20.0, 20.0))\n'
' New child: SemanticsNode#4(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(5.0, 5.0, 10.0, 10.0))\n'
'\n'
' Child node at position 1 was replaced:\n'
' Previous child: SemanticsNode#7(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(40.0, 14.0, 20.0, 20.0))\n'
' New child: SemanticsNode#5(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(10.0, 10.0, 20.0, 20.0))\n'
));
expect(
error.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.'
);
// Two previous children and two new children.
expect(error.diagnostics.where((DiagnosticsNode node) => node.value is SemanticsNode).length, 4);
}
});
test('after markNeedsSemanticsUpdate() all render objects between two semantic boundaries are asked for annotations', () {
renderer.pipelineOwner.ensureSemantics();
......
......@@ -76,7 +76,18 @@ void main() {
),
],
));
expect(tester.takeException(), isFlutterError);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Multiple widgets used the same GlobalKey.\n'
'The key [GlobalKey#00000 problematic] was used by multiple widgets. The parents of those widgets were:\n'
'- Container-[<1>]\n'
'- Container-[<2>]\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
),
);
});
testWidgets('GlobalKey duplication 2 - splitting and changing type', (WidgetTester tester) async {
......@@ -111,7 +122,18 @@ void main() {
],
));
expect(tester.takeException(), isFlutterError);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Multiple widgets used the same GlobalKey.\n'
'The key [GlobalKey#00000 problematic] was used by multiple widgets. The parents of those widgets were:\n'
'- Container-[<1>]\n'
'- Container-[<2>]\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
)
);
});
testWidgets('GlobalKey duplication 3 - splitting and changing type', (WidgetTester tester) async {
......@@ -129,7 +151,18 @@ void main() {
Placeholder(key: key),
],
));
expect(tester.takeException(), isFlutterError);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Multiple widgets used the same GlobalKey.\n'
'The key [GlobalKey#00000 problematic] was used by 2 widgets:\n'
' SizedBox-[GlobalKey#00000 problematic]\n'
' Placeholder-[GlobalKey#00000 problematic]\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
)
);
});
testWidgets('GlobalKey duplication 4 - splitting and half changing type', (WidgetTester tester) async {
......@@ -147,7 +180,18 @@ void main() {
Placeholder(key: key),
],
));
expect(tester.takeException(), isFlutterError);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Multiple widgets used the same GlobalKey.\n'
'The key [GlobalKey#00000 problematic] was used by 2 widgets:\n'
' Container-[GlobalKey#00000 problematic]\n'
' Placeholder-[GlobalKey#00000 problematic]\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.'
)
);
});
testWidgets('GlobalKey duplication 5 - splitting and half changing type', (WidgetTester tester) async {
......@@ -314,7 +358,16 @@ void main() {
Container(key: key3),
],
));
expect(tester.takeException(), isFlutterError);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Duplicate keys found.\n'
'If multiple keyed nodes exist as children of another node, they must have unique keys.\n'
'Stack(alignment: AlignmentDirectional.topStart, textDirection: ltr, fit: loose, overflow: clip) has multiple children with key [GlobalKey#00000 problematic].'
),
);
});
testWidgets('GlobalKey duplication 13 - all kinds of badness at once', (WidgetTester tester) async {
......
......@@ -266,7 +266,20 @@ void main() {
],
),
);
expect(tester.takeException(), isFlutterError);
dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Incorrect use of ParentDataWidget.\n'
'Positioned widgets must be placed directly inside Stack widgets.\n'
'Positioned(no depth, left: 7.0, top: 6.0, dirty) has a Stack ancestor, but there are other widgets between them:\n'
'- Positioned(top: 5.0, bottom: 8.0) (this is a different Positioned than the one with the problem)\n'
'These widgets cannot come between a Positioned and its Stack.\n'
'The ownership chain for the parent of the offending Positioned was:\n'
' Positioned ← Stack ← [root]'
),
);
await tester.pumpWidget(Stack(textDirection: TextDirection.ltr));
......@@ -285,7 +298,18 @@ void main() {
),
),
);
expect(tester.takeException(), isFlutterError);
exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Incorrect use of ParentDataWidget.\n'
'Positioned widgets must be placed inside Stack widgets.\n'
'Positioned(no depth, left: 7.0, top: 6.0, dirty) has no Stack ancestor at all.\n'
'The ownership chain for the parent of the offending Positioned was:\n'
' Row ← Container ← [root]'
)
);
await tester.pumpWidget(
Stack(textDirection: TextDirection.ltr)
......@@ -356,7 +380,7 @@ void main() {
await tester.pumpWidget(Row(
children: <Widget>[
Stack(
textDirection: TextDirection.ltr,
textDirection: TextDirection.ltr,
children: <Widget>[
Expanded(
child: Container(),
......@@ -366,6 +390,19 @@ void main() {
],
));
expect(tester.takeException(), isFlutterError);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Incorrect use of ParentDataWidget.\n'
'Expanded widgets must be placed directly inside Flex widgets.\n'
'Expanded(no depth, flex: 1, dirty) has a Flex ancestor, but there are other widgets between them:\n'
'- Stack(alignment: AlignmentDirectional.topStart, textDirection: ltr, fit: loose, overflow: clip)\n'
'These widgets cannot come between a Expanded and its Flex.\n'
'The ownership chain for the parent of the offending Expanded was:\n'
' Stack ← Row ← [root]'
),
);
});
}
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