Skip to content

Commit 963f23e

Browse files
computeDryLayout access size bad (#164663)
Asserts if `RenderBox.size` is accessed in `computeDryLayout` Also changes `x is RenderObject` to `x != null` when x's static type is `RenderObject?`. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent a3f63a7 commit 963f23e

File tree

7 files changed

+140
-105
lines changed

7 files changed

+140
-105
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,14 +242,16 @@ class RenderAnimatedSize extends RenderAligningShiftedBox {
242242
return _sizeTween.evaluate(_animation);
243243
}
244244

245+
late Size _currentSize;
246+
245247
@override
246248
void performLayout() {
247249
_lastValue = _controller.value;
248250
_hasVisualOverflow = false;
249251
final BoxConstraints constraints = this.constraints;
250252
if (child == null || constraints.isTight) {
251253
_controller.stop();
252-
size = _sizeTween.begin = _sizeTween.end = constraints.smallest;
254+
size = _currentSize = _sizeTween.begin = _sizeTween.end = constraints.smallest;
253255
_state = RenderAnimatedSizeState.start;
254256
child?.layout(constraints);
255257
return;
@@ -268,7 +270,7 @@ class RenderAnimatedSize extends RenderAligningShiftedBox {
268270
_layoutUnstable();
269271
}
270272

271-
size = constraints.constrain(_animatedSize!);
273+
size = _currentSize = constraints.constrain(_animatedSize!);
272274
alignChild();
273275

274276
if (size.width < _sizeTween.end!.width || size.height < _sizeTween.end!.height) {
@@ -292,7 +294,7 @@ class RenderAnimatedSize extends RenderAligningShiftedBox {
292294
return constraints.constrain(childSize);
293295
case RenderAnimatedSizeState.stable:
294296
if (_sizeTween.end != childSize) {
295-
return constraints.constrain(size);
297+
return constraints.constrain(_currentSize);
296298
} else if (_controller.value == _controller.upperBound) {
297299
return constraints.constrain(childSize);
298300
}

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2262,7 +2262,6 @@ abstract class RenderBox extends RenderObject {
22622262
!doingRegularLayout ||
22632263
debugDoingThisResize ||
22642264
debugDoingThisLayout ||
2265-
_computingThisDryLayout ||
22662265
RenderObject.debugActiveLayout == parent && size._canBeUsedByParent;
22672266
assert(
22682267
sizeAccessAllowed,
@@ -2273,16 +2272,29 @@ abstract class RenderBox extends RenderObject {
22732272
'trying to access a child\'s size, pass "parentUsesSize: true" to '
22742273
"that child's layout() in ${objectRuntimeType(this, 'RenderBox')}.performLayout.",
22752274
);
2275+
final RenderBox? renderBoxDoingDryLayout =
2276+
_computingThisDryLayout
2277+
? this
2278+
: (parent is RenderBox && parent._computingThisDryLayout ? parent : null);
2279+
2280+
assert(
2281+
renderBoxDoingDryLayout == null,
2282+
'RenderBox.size accessed in '
2283+
'${objectRuntimeType(renderBoxDoingDryLayout, 'RenderBox')}.computeDryLayout. '
2284+
"The computeDryLayout method must not access the RenderBox's own size, or the size of its child, "
2285+
"because it's established in performLayout or performResize using different BoxConstraints.",
2286+
);
2287+
22762288
final RenderBox? renderBoxDoingDryBaseline =
22772289
_computingThisDryBaseline
22782290
? this
22792291
: (parent is RenderBox && parent._computingThisDryBaseline ? parent : null);
22802292
assert(
22812293
renderBoxDoingDryBaseline == null,
2294+
22822295
'RenderBox.size accessed in '
2283-
'${objectRuntimeType(renderBoxDoingDryBaseline, 'RenderBox')}.computeDryBaseline.'
2284-
'The computeDryBaseline method must not access '
2285-
'${renderBoxDoingDryBaseline == this ? "the RenderBox's own size" : "the size of its child"},'
2296+
'${objectRuntimeType(renderBoxDoingDryBaseline, 'RenderBox')}.computeDryBaseline. '
2297+
"The computeDryBaseline method must not access the RenderBox's own size, or the size of its child, "
22862298
"because it's established in performLayout or performResize using different BoxConstraints.",
22872299
);
22882300
assert(size == _size);
@@ -2740,7 +2752,7 @@ abstract class RenderBox extends RenderObject {
27402752
} finally {
27412753
RenderObject.debugCheckingIntrinsics = false;
27422754
}
2743-
if (_debugDryLayoutCalculationValid && dryLayoutSize != size) {
2755+
if (_debugDryLayoutCalculationValid && dryLayoutSize != _size) {
27442756
throw FlutterError.fromParts(<DiagnosticsNode>[
27452757
ErrorSummary(
27462758
'The size given to the ${objectRuntimeType(this, 'RenderBox')} class differs from the size computed by computeDryLayout.',

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

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2594,7 +2594,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
25942594
void scheduleInitialLayout() {
25952595
assert(!_debugDisposed);
25962596
assert(attached);
2597-
assert(parent is! RenderObject);
2597+
assert(parent == null);
25982598
assert(!owner!._debugDoingLayout);
25992599
assert(_relayoutBoundary == null);
26002600
_relayoutBoundary = this;
@@ -2712,7 +2712,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
27122712
assert(!_debugDoingThisResize);
27132713
assert(!_debugDoingThisLayout);
27142714
final bool isRelayoutBoundary =
2715-
!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject;
2715+
!parentUsesSize || sizedByParent || constraints.isTight || parent == null;
27162716
final RenderObject relayoutBoundary = isRelayoutBoundary ? this : parent!._relayoutBoundary!;
27172717
assert(() {
27182718
_debugCanParentUseSize = parentUsesSize;
@@ -3077,8 +3077,8 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
30773077
return;
30783078
}
30793079
_needsCompositingBitsUpdate = true;
3080-
if (parent is RenderObject) {
3081-
final RenderObject parent = this.parent!;
3080+
final RenderObject? parent = this.parent;
3081+
if (parent != null) {
30823082
if (parent._needsCompositingBitsUpdate) {
30833083
return;
30843084
}
@@ -3089,9 +3089,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
30893089
}
30903090
}
30913091
// parent is fine (or there isn't one), but we are dirty
3092-
if (owner != null) {
3093-
owner!._nodesNeedingCompositingBitsUpdate.add(this);
3094-
}
3092+
owner?._nodesNeedingCompositingBitsUpdate.add(this);
30953093
}
30963094

30973095
late bool _needsCompositing; // initialized in the constructor
@@ -3299,7 +3297,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
32993297
assert(_layerHandle.layer != null);
33003298
assert(!_layerHandle.layer!.attached);
33013299
RenderObject? node = parent;
3302-
while (node is RenderObject) {
3300+
while (node != null) {
33033301
if (node.isRepaintBoundary) {
33043302
if (node._layerHandle.layer == null) {
33053303
// Looks like the subtree here has never been painted. Let it handle itself.
@@ -3324,7 +3322,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
33243322
void scheduleInitialPaint(ContainerLayer rootLayer) {
33253323
assert(rootLayer.attached);
33263324
assert(attached);
3327-
assert(parent is! RenderObject);
3325+
assert(parent == null);
33283326
assert(!owner!._debugDoingPaint);
33293327
assert(isRepaintBoundary);
33303328
assert(_layerHandle.layer == null);
@@ -3342,7 +3340,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
33423340
assert(!_debugDisposed);
33433341
assert(rootLayer.attached);
33443342
assert(attached);
3345-
assert(parent is! RenderObject);
3343+
assert(parent == null);
33463344
assert(!owner!._debugDoingPaint);
33473345
assert(isRepaintBoundary);
33483346
assert(_layerHandle.layer != null); // use scheduleInitialPaint the first time
@@ -3391,8 +3389,8 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
33913389
}
33923390
assert(() {
33933391
if (_needsCompositingBitsUpdate) {
3394-
if (parent is RenderObject) {
3395-
final RenderObject parent = this.parent!;
3392+
final RenderObject? parent = this.parent;
3393+
if (parent != null) {
33963394
bool visitedByParent = false;
33973395
parent.visitChildren((RenderObject child) {
33983396
if (child == this) {
@@ -3669,7 +3667,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
36693667
void scheduleInitialSemantics() {
36703668
assert(!_debugDisposed);
36713669
assert(attached);
3672-
assert(parent is! RenderObject);
3670+
assert(parent == null);
36733671
assert(!owner!._debugDoingSemantics);
36743672
assert(_semantics.parentDataDirty || !_semantics.built);
36753673
assert(owner!._semanticsOwner != null);
@@ -4005,14 +4003,12 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
40054003
Duration duration = Duration.zero,
40064004
Curve curve = Curves.ease,
40074005
}) {
4008-
if (parent is RenderObject) {
4009-
parent!.showOnScreen(
4010-
descendant: descendant ?? this,
4011-
rect: rect,
4012-
duration: duration,
4013-
curve: curve,
4014-
);
4015-
}
4006+
parent?.showOnScreen(
4007+
descendant: descendant ?? this,
4008+
rect: rect,
4009+
duration: duration,
4010+
curve: curve,
4011+
);
40164012
}
40174013

40184014
/// Adds a debug representation of a [RenderObject] optimized for including in

packages/flutter/test/material/list_tile_test.dart

Lines changed: 56 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2605,15 +2605,12 @@ void main() {
26052605
expect(trailingOffset.dy - tileOffset.dy, topPosition);
26062606
});
26072607

2608-
testWidgets('Leading/Trailing exceeding list tile width throws exception', (
2609-
WidgetTester tester,
2610-
) async {
2611-
List<dynamic> exceptions = <dynamic>[];
2612-
FlutterExceptionHandler? oldHandler = FlutterError.onError;
2613-
FlutterError.onError = (FlutterErrorDetails details) {
2614-
exceptions.add(details.exception);
2615-
};
2608+
group('Leading/Trailing exceeding list tile width throws exception', () {
2609+
final List<Object> exceptions = <Object>[];
2610+
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
2611+
tearDown(exceptions.clear);
26162612

2613+
void onError(FlutterErrorDetails details) => exceptions.add(details.exception);
26172614
Widget buildListTile({Widget? leading, Widget? trailing}) {
26182615
return MaterialApp(
26192616
home: Material(
@@ -2624,61 +2621,59 @@ void main() {
26242621
);
26252622
}
26262623

2627-
// Test a trailing widget that exceeds the list tile width.
2628-
// 16 (content padding) + 61 (leading width) + 24 (content padding) = 101.
2629-
// List tile width is 100 as a result, an exception should be thrown.
2630-
await tester.pumpWidget(buildListTile(leading: const SizedBox(width: 61)));
2624+
testWidgets('leading', (WidgetTester tester) async {
2625+
// Test a leading widget that exceeds the list tile width.
2626+
// 16 (content padding) + 61 (leading width) + 24 (content padding) = 101.
2627+
// List tile width is 100 as a result, an exception should be thrown.
2628+
FlutterError.onError = onError;
2629+
await tester.pumpWidget(buildListTile(leading: const SizedBox(width: 61)));
2630+
FlutterError.onError = oldHandler;
26312631

2632-
FlutterError.onError = oldHandler;
2633-
expect(exceptions.first.runtimeType, FlutterError);
2634-
FlutterError error = exceptions.first as FlutterError;
2635-
expect(error.diagnostics.length, 3);
2636-
expect(
2637-
error.diagnostics[0].toStringDeep(),
2638-
'Leading widget consumes the entire tile width (including\nListTile.contentPadding).\n',
2639-
);
2640-
expect(
2641-
error.diagnostics[1].toStringDeep(),
2642-
'Either resize the tile width so that the leading widget plus any\n'
2643-
'content padding do not exceed the tile width, or use a sized\n'
2644-
'widget, or consider replacing ListTile with a custom widget.\n',
2645-
);
2646-
expect(
2647-
error.diagnostics[2].toStringDeep(),
2648-
'See also:\n'
2649-
'https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4\n',
2650-
);
2632+
final FlutterError error = exceptions.first as FlutterError;
2633+
expect(error.diagnostics.length, 3);
2634+
expect(
2635+
error.diagnostics[0].toStringDeep(),
2636+
'Leading widget consumes the entire tile width (including\nListTile.contentPadding).\n',
2637+
);
2638+
expect(
2639+
error.diagnostics[1].toStringDeep(),
2640+
'Either resize the tile width so that the leading widget plus any\n'
2641+
'content padding do not exceed the tile width, or use a sized\n'
2642+
'widget, or consider replacing ListTile with a custom widget.\n',
2643+
);
2644+
expect(
2645+
error.diagnostics[2].toStringDeep(),
2646+
'See also:\n'
2647+
'https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4\n',
2648+
);
2649+
});
26512650

2652-
exceptions = <dynamic>[];
2653-
oldHandler = FlutterError.onError;
2654-
FlutterError.onError = (FlutterErrorDetails details) {
2655-
exceptions.add(details.exception);
2656-
};
2657-
2658-
// Test a trailing widget that exceeds the list tile width.
2659-
// 16 (content padding) + 61 (trailing width) + 24 (content padding) = 101.
2660-
// List tile width is 100 as a result, an exception should be thrown.
2661-
await tester.pumpWidget(buildListTile(trailing: const SizedBox(width: 61)));
2662-
2663-
FlutterError.onError = oldHandler;
2664-
expect(exceptions.first.runtimeType, FlutterError);
2665-
error = exceptions.first as FlutterError;
2666-
expect(error.diagnostics.length, 3);
2667-
expect(
2668-
error.diagnostics[0].toStringDeep(),
2669-
'Trailing widget consumes the entire tile width (including\nListTile.contentPadding).\n',
2670-
);
2671-
expect(
2672-
error.diagnostics[1].toStringDeep(),
2673-
'Either resize the tile width so that the trailing widget plus any\n'
2674-
'content padding do not exceed the tile width, or use a sized\n'
2675-
'widget, or consider replacing ListTile with a custom widget.\n',
2676-
);
2677-
expect(
2678-
error.diagnostics[2].toStringDeep(),
2679-
'See also:\n'
2680-
'https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4\n',
2681-
);
2651+
testWidgets('trailing', (WidgetTester tester) async {
2652+
// Test a trailing widget that exceeds the list tile width.
2653+
// 16 (content padding) + 61 (trailing width) + 24 (content padding) = 101.
2654+
// List tile width is 100 as a result, an exception should be thrown.
2655+
FlutterError.onError = onError;
2656+
await tester.pumpWidget(buildListTile(trailing: const SizedBox(width: 61)));
2657+
FlutterError.onError = oldHandler;
2658+
2659+
final FlutterError error = exceptions.first as FlutterError;
2660+
expect(error.diagnostics.length, 3);
2661+
expect(
2662+
error.diagnostics[0].toStringDeep(),
2663+
'Trailing widget consumes the entire tile width (including\nListTile.contentPadding).\n',
2664+
);
2665+
expect(
2666+
error.diagnostics[1].toStringDeep(),
2667+
'Either resize the tile width so that the trailing widget plus any\n'
2668+
'content padding do not exceed the tile width, or use a sized\n'
2669+
'widget, or consider replacing ListTile with a custom widget.\n',
2670+
);
2671+
expect(
2672+
error.diagnostics[2].toStringDeep(),
2673+
'See also:\n'
2674+
'https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4\n',
2675+
);
2676+
});
26822677
});
26832678

26842679
group('Material 2', () {

packages/flutter/test/rendering/box_test.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ class BadBaselineRenderBox extends RenderBox {
4848
}
4949
}
5050

51+
class InvalidSizeAccessInDryLayoutBox extends RenderBox {
52+
@override
53+
Size computeDryLayout(covariant BoxConstraints constraints) {
54+
return constraints.constrain(hasSize ? size : Size.infinite);
55+
}
56+
57+
@override
58+
void performLayout() {
59+
size = getDryLayout(constraints);
60+
}
61+
}
62+
5163
void main() {
5264
TestRenderingFlutterBinding.ensureInitialized();
5365

@@ -231,6 +243,31 @@ void main() {
231243
}
232244
});
233245

246+
test('Invalid size access error message', () {
247+
final InvalidSizeAccessInDryLayoutBox testBox = InvalidSizeAccessInDryLayoutBox();
248+
249+
late FlutterErrorDetails errorDetails;
250+
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
251+
FlutterError.onError = (FlutterErrorDetails details) {
252+
errorDetails = details;
253+
};
254+
try {
255+
testBox.layout(const BoxConstraints.tightFor(width: 100.0, height: 100.0));
256+
} finally {
257+
FlutterError.onError = oldHandler;
258+
}
259+
260+
expect(
261+
errorDetails.toString().replaceAll('\n', ' '),
262+
contains(
263+
'RenderBox.size accessed in '
264+
'InvalidSizeAccessInDryLayoutBox.computeDryLayout. '
265+
"The computeDryLayout method must not access the RenderBox's own size, or the size of its child, "
266+
"because it's established in performLayout or performResize using different BoxConstraints.",
267+
),
268+
);
269+
});
270+
234271
test('Flex and padding', () {
235272
final RenderBox size = RenderConstrainedBox(
236273
additionalConstraints: const BoxConstraints().tighten(height: 100.0),

0 commit comments

Comments
 (0)