Skip to content

Commit 38f8490

Browse files
authored
Add more structure to errors messages. (flutter#34684)
Breaking change to extremely rarely used ParentDataWidget.debugDescribeInvalidAncestorChain api changing the return type of the method from String to DiagnosticsNode.
1 parent 1b176c5 commit 38f8490

16 files changed

+1586
-492
lines changed

packages/flutter/lib/src/animation/animation_controller.dart

+9-6
Original file line numberDiff line numberDiff line change
@@ -728,12 +728,15 @@ class AnimationController extends Animation<double>
728728
void dispose() {
729729
assert(() {
730730
if (_ticker == null) {
731-
throw FlutterError(
732-
'AnimationController.dispose() called more than once.\n'
733-
'A given $runtimeType cannot be disposed more than once.\n'
734-
'The following $runtimeType object was disposed multiple times:\n'
735-
' $this'
736-
);
731+
throw FlutterError.fromParts(<DiagnosticsNode>[
732+
ErrorSummary('AnimationController.dispose() called more than once.'),
733+
ErrorDescription('A given $runtimeType cannot be disposed more than once.\n'),
734+
DiagnosticsProperty<AnimationController>(
735+
'The following $runtimeType object was disposed multiple times',
736+
this,
737+
style: DiagnosticsTreeStyle.errorProperty,
738+
),
739+
]);
737740
}
738741
return true;
739742
}());

packages/flutter/lib/src/foundation/assertions.dart

+16-3
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,19 @@ class ErrorHint extends _ErrorDiagnostic {
176176
ErrorHint._fromParts(List<Object> messageParts) : super._fromParts(messageParts, level:DiagnosticLevel.hint);
177177
}
178178

179+
/// An [ErrorSpacer] creates an empty [DiagnosticsNode], that can be used to
180+
/// tune the spacing between other [DiagnosticsNode] objects.
181+
class ErrorSpacer extends DiagnosticsProperty<void> {
182+
/// Creates an empty space to insert into a list of [DiagnosticNode] objects
183+
/// typically within a [FlutterError] object.
184+
ErrorSpacer() : super(
185+
'',
186+
null,
187+
description: '',
188+
showName: false,
189+
);
190+
}
191+
179192
/// Class for information provided to [FlutterExceptionHandler] callbacks.
180193
///
181194
/// See [FlutterError.onError].
@@ -407,7 +420,7 @@ class FlutterErrorDetails extends Diagnosticable {
407420
}
408421
}
409422
if (ourFault) {
410-
properties.add(DiagnosticsNode.message(''));
423+
properties.add(ErrorSpacer());
411424
properties.add(ErrorHint(
412425
'Either the assertion indicates an error in the framework itself, or we should '
413426
'provide substantially more information in this error message to help you determine '
@@ -418,11 +431,11 @@ class FlutterErrorDetails extends Diagnosticable {
418431
}
419432
}
420433
if (stack != null) {
421-
properties.add(DiagnosticsNode.message(''));
434+
properties.add(ErrorSpacer());
422435
properties.add(DiagnosticsStackTrace('When the exception was thrown, this was the stack', stack, stackFilter: stackFilter));
423436
}
424437
if (informationCollector != null) {
425-
properties.add(DiagnosticsNode.message(''));
438+
properties.add(ErrorSpacer());
426439
informationCollector().forEach(properties.add);
427440
}
428441
}

packages/flutter/lib/src/material/stepper.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
690690
throw FlutterError(
691691
'Steppers must not be nested. The material specification advises '
692692
'that one should avoid embedding steppers within steppers. '
693-
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage\n'
693+
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage'
694694
);
695695
return true;
696696
}());

packages/flutter/lib/src/rendering/box.dart

+142-120
Large diffs are not rendered by default.

packages/flutter/lib/src/rendering/object.dart

+80-50
Original file line numberDiff line numberDiff line change
@@ -1194,7 +1194,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
11941194
// displaying the truncated children is really useful for command line
11951195
// users. Inspector users can see the full tree by clicking on the
11961196
// render object so this may not be that useful.
1197-
yield describeForError('This RenderObject', style: DiagnosticsTreeStyle.truncateChildren);
1197+
yield describeForError('RenderObject', style: DiagnosticsTreeStyle.truncateChildren);
11981198
}
11991199
));
12001200
}
@@ -2030,14 +2030,17 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
20302030
void _paintWithContext(PaintingContext context, Offset offset) {
20312031
assert(() {
20322032
if (_debugDoingThisPaint) {
2033-
throw FlutterError(
2034-
'Tried to paint a RenderObject reentrantly.\n'
2035-
'The following RenderObject was already being painted when it was '
2036-
'painted again:\n'
2037-
' ${toStringShallow(joiner: "\n ")}\n'
2038-
'Since this typically indicates an infinite recursion, it is '
2039-
'disallowed.'
2040-
);
2033+
throw FlutterError.fromParts(<DiagnosticsNode>[
2034+
ErrorSummary('Tried to paint a RenderObject reentrantly.'),
2035+
describeForError(
2036+
'The following RenderObject was already being painted when it was '
2037+
'painted again'
2038+
),
2039+
ErrorDescription(
2040+
'Since this typically indicates an infinite recursion, it is '
2041+
'disallowed.'
2042+
)
2043+
]);
20412044
}
20422045
return true;
20432046
}());
@@ -2052,17 +2055,24 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
20522055
return;
20532056
assert(() {
20542057
if (_needsCompositingBitsUpdate) {
2055-
throw FlutterError(
2056-
'Tried to paint a RenderObject before its compositing bits were '
2057-
'updated.\n'
2058-
'The following RenderObject was marked as having dirty compositing '
2059-
'bits at the time that it was painted:\n'
2060-
' ${toStringShallow(joiner: "\n ")}\n'
2061-
'A RenderObject that still has dirty compositing bits cannot be '
2062-
'painted because this indicates that the tree has not yet been '
2063-
'properly configured for creating the layer tree.\n'
2064-
'This usually indicates an error in the Flutter framework itself.'
2065-
);
2058+
throw FlutterError.fromParts(<DiagnosticsNode>[
2059+
ErrorSummary(
2060+
'Tried to paint a RenderObject before its compositing bits were '
2061+
'updated.'
2062+
),
2063+
describeForError(
2064+
'The following RenderObject was marked as having dirty compositing '
2065+
'bits at the time that it was painted',
2066+
),
2067+
ErrorDescription(
2068+
'A RenderObject that still has dirty compositing bits cannot be '
2069+
'painted because this indicates that the tree has not yet been '
2070+
'properly configured for creating the layer tree.'
2071+
),
2072+
ErrorHint(
2073+
'This usually indicates an error in the Flutter framework itself.'
2074+
)
2075+
]);
20662076
}
20672077
return true;
20682078
}());
@@ -2720,21 +2730,31 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject
27202730
bool debugValidateChild(RenderObject child) {
27212731
assert(() {
27222732
if (child is! ChildType) {
2723-
throw FlutterError(
2724-
'A $runtimeType expected a child of type $ChildType but received a '
2725-
'child of type ${child.runtimeType}.\n'
2726-
'RenderObjects expect specific types of children because they '
2727-
'coordinate with their children during layout and paint. For '
2728-
'example, a RenderSliver cannot be the child of a RenderBox because '
2729-
'a RenderSliver does not understand the RenderBox layout protocol.\n'
2730-
'\n'
2731-
'The $runtimeType that expected a $ChildType child was created by:\n'
2732-
' $debugCreator\n'
2733-
'\n'
2734-
'The ${child.runtimeType} that did not match the expected child type '
2735-
'was created by:\n'
2736-
' ${child.debugCreator}\n'
2737-
);
2733+
throw FlutterError.fromParts(<DiagnosticsNode>[
2734+
ErrorSummary(
2735+
'A $runtimeType expected a child of type $ChildType but received a '
2736+
'child of type ${child.runtimeType}.'
2737+
),
2738+
ErrorDescription(
2739+
'RenderObjects expect specific types of children because they '
2740+
'coordinate with their children during layout and paint. For '
2741+
'example, a RenderSliver cannot be the child of a RenderBox because '
2742+
'a RenderSliver does not understand the RenderBox layout protocol.',
2743+
),
2744+
ErrorSpacer(),
2745+
DiagnosticsProperty<dynamic>(
2746+
'The $runtimeType that expected a $ChildType child was created by',
2747+
debugCreator,
2748+
style: DiagnosticsTreeStyle.errorProperty,
2749+
),
2750+
ErrorSpacer(),
2751+
DiagnosticsProperty<dynamic>(
2752+
'The ${child.runtimeType} that did not match the expected child type '
2753+
'was created by',
2754+
child.debugCreator,
2755+
style: DiagnosticsTreeStyle.errorProperty,
2756+
)
2757+
]);
27382758
}
27392759
return true;
27402760
}());
@@ -2849,21 +2869,31 @@ mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType
28492869
bool debugValidateChild(RenderObject child) {
28502870
assert(() {
28512871
if (child is! ChildType) {
2852-
throw FlutterError(
2853-
'A $runtimeType expected a child of type $ChildType but received a '
2854-
'child of type ${child.runtimeType}.\n'
2855-
'RenderObjects expect specific types of children because they '
2856-
'coordinate with their children during layout and paint. For '
2857-
'example, a RenderSliver cannot be the child of a RenderBox because '
2858-
'a RenderSliver does not understand the RenderBox layout protocol.\n'
2859-
'\n'
2860-
'The $runtimeType that expected a $ChildType child was created by:\n'
2861-
' $debugCreator\n'
2862-
'\n'
2863-
'The ${child.runtimeType} that did not match the expected child type '
2864-
'was created by:\n'
2865-
' ${child.debugCreator}\n'
2866-
);
2872+
throw FlutterError.fromParts(<DiagnosticsNode>[
2873+
ErrorSummary(
2874+
'A $runtimeType expected a child of type $ChildType but received a '
2875+
'child of type ${child.runtimeType}.'
2876+
),
2877+
ErrorDescription(
2878+
'RenderObjects expect specific types of children because they '
2879+
'coordinate with their children during layout and paint. For '
2880+
'example, a RenderSliver cannot be the child of a RenderBox because '
2881+
'a RenderSliver does not understand the RenderBox layout protocol.'
2882+
),
2883+
ErrorSpacer(),
2884+
DiagnosticsProperty<dynamic>(
2885+
'The $runtimeType that expected a $ChildType child was created by',
2886+
debugCreator,
2887+
style: DiagnosticsTreeStyle.errorProperty,
2888+
),
2889+
ErrorSpacer(),
2890+
DiagnosticsProperty<dynamic>(
2891+
'The ${child.runtimeType} that did not match the expected child type '
2892+
'was created by',
2893+
child.debugCreator,
2894+
style: DiagnosticsTreeStyle.errorProperty,
2895+
),
2896+
]);
28672897
}
28682898
return true;
28692899
}());

packages/flutter/lib/src/semantics/semantics.dart

+15-14
Original file line numberDiff line numberDiff line change
@@ -1282,30 +1282,31 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
12821282
assert(!newChildren.any((SemanticsNode child) => child == this));
12831283
assert(() {
12841284
if (identical(newChildren, _children)) {
1285-
final StringBuffer mutationErrors = StringBuffer();
1285+
final List<DiagnosticsNode> mutationErrors = <DiagnosticsNode>[];
12861286
if (newChildren.length != _debugPreviousSnapshot.length) {
1287-
mutationErrors.writeln(
1287+
mutationErrors.add(ErrorDescription(
12881288
'The list\'s length has changed from ${_debugPreviousSnapshot.length} '
12891289
'to ${newChildren.length}.'
1290-
);
1290+
));
12911291
} else {
12921292
for (int i = 0; i < newChildren.length; i++) {
12931293
if (!identical(newChildren[i], _debugPreviousSnapshot[i])) {
1294-
mutationErrors.writeln(
1295-
'Child node at position $i was replaced:\n'
1296-
'Previous child: ${newChildren[i]}\n'
1297-
'New child: ${_debugPreviousSnapshot[i]}\n'
1298-
);
1294+
if (mutationErrors.isNotEmpty) {
1295+
mutationErrors.add(ErrorSpacer());
1296+
}
1297+
mutationErrors.add(ErrorDescription('Child node at position $i was replaced:'));
1298+
mutationErrors.add(newChildren[i].toDiagnosticsNode(name: 'Previous child', style: DiagnosticsTreeStyle.singleLine));
1299+
mutationErrors.add(_debugPreviousSnapshot[i].toDiagnosticsNode(name: 'New child', style: DiagnosticsTreeStyle.singleLine));
12991300
}
13001301
}
13011302
}
13021303
if (mutationErrors.isNotEmpty) {
1303-
throw FlutterError(
1304-
'Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.\n'
1305-
'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.\n'
1306-
'Error details:\n'
1307-
'$mutationErrors'
1308-
);
1304+
throw FlutterError.fromParts(<DiagnosticsNode>[
1305+
ErrorSummary('Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.'),
1306+
ErrorHint('Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.'),
1307+
ErrorDescription('Error details:'),
1308+
...mutationErrors
1309+
]);
13091310
}
13101311
}
13111312
assert(!newChildren.any((SemanticsNode node) => node.isMergedIntoParent) || isPartOfNodeMerging);

0 commit comments

Comments
 (0)