Unverified Commit 09a5382f authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Add error messages to `_debugCanPerformMutations` (#105638)

parent c4aaa394
......@@ -1579,34 +1579,115 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
late bool result;
assert(() {
if (_debugDisposed) {
result = false;
return true;
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('A disposed RenderObject was mutated.'),
DiagnosticsProperty<RenderObject>(
'The disposed RenderObject was',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
]);
}
if (owner != null && !owner!.debugDoingLayout) {
final PipelineOwner? owner = this.owner;
// Detached nodes are allowed to mutate and the "can perform mutations"
// check will be performed when they re-attach. This assert is only useful
// during layout.
if (owner == null || !owner.debugDoingLayout) {
result = true;
return true;
}
RenderObject node = this;
while (true) {
if (node._doingThisLayoutWithCallback) {
RenderObject? activeLayoutRoot = this;
while (activeLayoutRoot != null) {
final bool mutationsToDirtySubtreesAllowed = activeLayoutRoot.owner?._debugAllowMutationsToDirtySubtrees ?? false;
final bool doingLayoutWithCallback = activeLayoutRoot._doingThisLayoutWithCallback;
// Mutations on this subtree is allowed when:
// - the subtree is being mutated in a layout callback.
// - a different part of the render tree is doing a layout callback,
// and this subtree is being reparented to that subtree, as a result
// of global key reparenting.
if (doingLayoutWithCallback || mutationsToDirtySubtreesAllowed && activeLayoutRoot._needsLayout) {
result = true;
break;
return true;
}
if (owner != null && owner!._debugAllowMutationsToDirtySubtrees && node._needsLayout) {
result = true;
if (!activeLayoutRoot._debugMutationsLocked) {
final AbstractNode? p = activeLayoutRoot.parent;
activeLayoutRoot = p is RenderObject ? p : null;
} else {
// activeLayoutRoot found.
break;
}
if (node._debugMutationsLocked) {
result = false;
break;
}
if (node.parent is! RenderObject) {
result = true;
break;
final RenderObject debugActiveLayout = RenderObject.debugActiveLayout!;
final String culpritMethodName = debugActiveLayout.debugDoingThisLayout ? 'performLayout' : 'performResize';
final String culpritFullMethodName = '${debugActiveLayout.runtimeType}.$culpritMethodName';
result = false;
if (activeLayoutRoot == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('A $runtimeType was mutated in $culpritFullMethodName.'),
ErrorDescription(
'The RenderObject was mutated when none of its ancestors is actively performing layout.',
),
DiagnosticsProperty<RenderObject>(
'The RenderObject being mutated was',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
DiagnosticsProperty<RenderObject>(
'The RenderObject that was mutating the said $runtimeType was',
debugActiveLayout,
style: DiagnosticsTreeStyle.errorProperty,
),
]);
}
node = node.parent! as RenderObject;
if (activeLayoutRoot == this) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('A $runtimeType was mutated in its own $culpritMethodName implementation.'),
ErrorDescription('A RenderObject must not re-dirty itself while still being laid out.'),
DiagnosticsProperty<RenderObject>(
'The RenderObject being mutated was',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
ErrorHint('Consider using the LayoutBuilder widget to dynamically change a subtree during layout.'),
]);
}
return true;
final ErrorSummary summary = ErrorSummary('A $runtimeType was mutated in $culpritFullMethodName.');
final bool isMutatedByAncestor = activeLayoutRoot == debugActiveLayout;
final String description = isMutatedByAncestor
? 'A RenderObject must not mutate its descendants in its $culpritMethodName method.'
: 'A RenderObject must not mutate another RenderObject from a different render subtree '
'in its $culpritMethodName method.';
throw FlutterError.fromParts(<DiagnosticsNode>[
summary,
ErrorDescription(description),
DiagnosticsProperty<RenderObject>(
'The RenderObject being mutated was',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
DiagnosticsProperty<RenderObject>(
'The ${isMutatedByAncestor ? 'ancestor ' : ''}RenderObject that was mutating the said $runtimeType was',
debugActiveLayout,
style: DiagnosticsTreeStyle.errorProperty,
),
if (!isMutatedByAncestor) DiagnosticsProperty<RenderObject>(
'Their common ancestor was',
activeLayoutRoot,
style: DiagnosticsTreeStyle.errorProperty,
),
ErrorHint(
'Mutating the layout of another RenderObject may cause some RenderObjects in its subtree to be laid out more than once. '
'Consider using the LayoutBuilder widget to dynamically mutate a subtree during layout.'
),
]);
}());
return result;
}
......@@ -1799,6 +1880,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// Only call this if [parent] is not null.
@protected
void markParentNeedsLayout() {
assert(_debugCanPerformMutations);
_needsLayout = true;
assert(this.parent != null);
final RenderObject parent = this.parent! as RenderObject;
......
......@@ -8,9 +8,12 @@ import 'package:flutter_test/flutter_test.dart';
import 'rendering_tester.dart';
class RenderLayoutTestBox extends RenderProxyBox {
RenderLayoutTestBox(this.onLayout);
RenderLayoutTestBox(this.onLayout, {
this.onPerformLayout,
});
final VoidCallback onLayout;
final VoidCallback? onPerformLayout;
@override
void layout(Constraints constraints, { bool parentUsesSize = false }) {
......@@ -27,7 +30,10 @@ class RenderLayoutTestBox extends RenderProxyBox {
bool get sizedByParent => true;
@override
void performLayout() { }
void performLayout() {
child?.layout(constraints, parentUsesSize: true);
onPerformLayout?.call();
}
}
void main() {
......@@ -72,4 +78,130 @@ void main() {
expect(movedChild1, isFalse);
expect(movedChild2, isFalse);
});
group('Throws when illegal mutations are attempted: ', () {
FlutterError catchLayoutError(RenderBox box) {
Object? error;
layout(box, onErrors: () {
error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!.exception;
});
expect(error, isFlutterError);
return error! as FlutterError;
}
test('on disposed render objects', () {
final RenderBox box = RenderLayoutTestBox(() {});
box.dispose();
Object? error;
try {
box.markNeedsLayout();
} catch (e) {
error = e;
}
expect(error, isFlutterError);
expect(
(error! as FlutterError).message,
equalsIgnoringWhitespace(
'A disposed RenderObject was mutated.\n'
'The disposed RenderObject was:\n'
'${box.toStringShort()}'
)
);
});
test('marking itself dirty in performLayout', () {
late RenderBox child1;
final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
block.add(child1 = RenderLayoutTestBox(() {}, onPerformLayout: () { child1.markNeedsLayout(); }));
expect(
catchLayoutError(block).message,
equalsIgnoringWhitespace(
'A RenderLayoutTestBox was mutated in its own performLayout implementation.\n'
'A RenderObject must not re-dirty itself while still being laid out.\n'
'The RenderObject being mutated was:\n'
'${child1.toStringShort()}\n'
'Consider using the LayoutBuilder widget to dynamically change a subtree during layout.'
)
);
});
test('marking a sibling dirty in performLayout', () {
late RenderBox child1, child2;
final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
block.add(child1 = RenderLayoutTestBox(() {}));
block.add(child2 = RenderLayoutTestBox(() {}, onPerformLayout: () { child1.markNeedsLayout(); }));
expect(
catchLayoutError(block).message,
equalsIgnoringWhitespace(
'A RenderLayoutTestBox was mutated in RenderLayoutTestBox.performLayout.\n'
'A RenderObject must not mutate another RenderObject from a different render subtree in its performLayout method.\n'
'The RenderObject being mutated was:\n'
'${child1.toStringShort()}\n'
'The RenderObject that was mutating the said RenderLayoutTestBox was:\n'
'${child2.toStringShort()}\n'
'Their common ancestor was:\n'
'${block.toStringShort()}\n'
'Mutating the layout of another RenderObject may cause some RenderObjects in its subtree to be laid out more than once. Consider using the LayoutBuilder widget to dynamically mutate a subtree during layout.'
)
);
});
test('marking a descendant dirty in performLayout', () {
late RenderBox child1;
final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
block.add(child1 = RenderLayoutTestBox(() {}));
block.add(RenderLayoutTestBox(child1.markNeedsLayout));
expect(
catchLayoutError(block).message,
equalsIgnoringWhitespace(
'A RenderLayoutTestBox was mutated in RenderFlex.performLayout.\n'
'A RenderObject must not mutate its descendants in its performLayout method.\n'
'The RenderObject being mutated was:\n'
'${child1.toStringShort()}\n'
'The ancestor RenderObject that was mutating the said RenderLayoutTestBox was:\n'
'${block.toStringShort()}\n'
'Mutating the layout of another RenderObject may cause some RenderObjects in its subtree to be laid out more than once. Consider using the LayoutBuilder widget to dynamically mutate a subtree during layout.'
),
);
});
test('marking an out-of-band mutation in performLayout', () {
late RenderProxyBox child1, child11, child2, child21;
final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
block.add(child1 = RenderLayoutTestBox(() {}));
block.add(child2 = RenderLayoutTestBox(() {}));
child1.child = child11 = RenderLayoutTestBox(() {});
layout(block);
expect(block.debugNeedsLayout, false);
expect(child1.debugNeedsLayout, false);
expect(child11.debugNeedsLayout, false);
expect(child2.debugNeedsLayout, false);
// Add a new child to child2 which is a relayout boundary.
child2.child = child21 = RenderLayoutTestBox(() {}, onPerformLayout: child11.markNeedsLayout);
FlutterError? error;
pumpFrame(onErrors: () {
error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!.exception as FlutterError;
});
expect(
error?.message,
equalsIgnoringWhitespace(
'A RenderLayoutTestBox was mutated in RenderLayoutTestBox.performLayout.\n'
'The RenderObject was mutated when none of its ancestors is actively performing layout.\n'
'The RenderObject being mutated was:\n'
'${child11.toStringShort()}\n'
'The RenderObject that was mutating the said RenderLayoutTestBox was:\n'
'${child21.toStringShort()}'
),
);
});
});
}
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