diff --git a/examples/rendering/lib/sector_layout.dart b/examples/rendering/lib/sector_layout.dart index 87ea7fba382..9644dc186a6 100644 --- a/examples/rendering/lib/sector_layout.dart +++ b/examples/rendering/lib/sector_layout.dart @@ -79,16 +79,16 @@ abstract class RenderSector extends RenderObject { } SectorConstraints get constraints => super.constraints; - bool debugDoesMeetConstraints() { + void debugAssertDoesMeetConstraints() { assert(constraints != null); assert(deltaRadius != null); assert(deltaRadius < double.INFINITY); assert(deltaTheta != null); assert(deltaTheta < double.INFINITY); - return constraints.minDeltaRadius <= deltaRadius && - deltaRadius <= math.max(constraints.minDeltaRadius, constraints.maxDeltaRadius) && - constraints.minDeltaTheta <= deltaTheta && - deltaTheta <= math.max(constraints.minDeltaTheta, constraints.maxDeltaTheta); + assert(constraints.minDeltaRadius <= deltaRadius); + assert(deltaRadius <= math.max(constraints.minDeltaRadius, constraints.maxDeltaRadius)); + assert(constraints.minDeltaTheta <= deltaTheta); + assert(deltaTheta <= math.max(constraints.minDeltaTheta, constraints.maxDeltaTheta)); } void performResize() { // default behaviour for subclasses that have sizedByParent = true diff --git a/packages/flutter/lib/src/rendering/block.dart b/packages/flutter/lib/src/rendering/block.dart index 0f196c4dc42..e3c3ad6c23f 100644 --- a/packages/flutter/lib/src/rendering/block.dart +++ b/packages/flutter/lib/src/rendering/block.dart @@ -350,8 +350,9 @@ class RenderBlockViewport extends RenderBlockBase { double result; if (intrinsicCallback == null) { assert(() { - 'RenderBlockViewport does not support returning intrinsic dimensions if the relevant callbacks have not been specified.'; - return RenderObject.debugInDebugDoesMeetConstraints; + if (!RenderObject.debugCheckingIntrinsics) + throw new UnsupportedError('$runtimeType does not support returning intrinsic dimensions if the relevant callbacks have not been specified.'); + return true; }); return constrainer(0.0); } diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 9933f5ff2a0..8a98e46071a 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -98,7 +98,7 @@ class BoxConstraints extends Constraints { /// Returns new box constraints that are smaller by the given edge dimensions. BoxConstraints deflate(EdgeDims edges) { assert(edges != null); - assert(isNormalized); + assert(debugAssertIsNormalized); double horizontal = edges.left + edges.right; double vertical = edges.top + edges.bottom; double deflatedMinWidth = math.max(0.0, minWidth - horizontal); @@ -113,7 +113,7 @@ class BoxConstraints extends Constraints { /// Returns new box constraints that remove the minimum width and height requirements. BoxConstraints loosen() { - assert(isNormalized); + assert(debugAssertIsNormalized); return new BoxConstraints( minWidth: 0.0, maxWidth: maxWidth, @@ -154,14 +154,14 @@ class BoxConstraints extends Constraints { /// Returns the width that both satisfies the constraints and is as close as /// possible to the given width. double constrainWidth([double width = double.INFINITY]) { - assert(isNormalized); + assert(debugAssertIsNormalized); return width.clamp(minWidth, maxWidth); } /// Returns the height that both satisfies the constraints and is as close as /// possible to the given height. double constrainHeight([double height = double.INFINITY]) { - assert(isNormalized); + assert(debugAssertIsNormalized); return height.clamp(minHeight, maxHeight); } @@ -192,9 +192,15 @@ class BoxConstraints extends Constraints { /// Whether there is exactly one size that satifies the constraints. bool get isTight => hasTightWidth && hasTightHeight; + /// Whether there is an upper bound on the maximum width. + bool get hasBoundedWidth => maxWidth < double.INFINITY; + + /// Whether there is an upper bound on the maximum height. + bool get hasBoundedHeight => maxHeight < double.INFINITY; + /// Whether the given size satisfies the constraints. bool isSatisfiedBy(Size size) { - assert(isNormalized); + assert(debugAssertIsNormalized); return (minWidth <= size.width) && (size.width <= maxWidth) && (minHeight <= size.height) && (size.height <= maxHeight); } @@ -245,8 +251,8 @@ class BoxConstraints extends Constraints { return b * t; if (b == null) return a * (1.0 - t); - assert(a.isNormalized); - assert(b.isNormalized); + assert(a.debugAssertIsNormalized); + assert(b.debugAssertIsNormalized); return new BoxConstraints( minWidth: ui.lerpDouble(a.minWidth, b.minWidth, t), maxWidth: ui.lerpDouble(a.maxWidth, b.maxWidth, t), @@ -255,8 +261,34 @@ class BoxConstraints extends Constraints { ); } + /// Returns whether the object's constraints are normalized. + /// Constraints are normalised if the minimums are less than or + /// equal to the corresponding maximums. + /// + /// For example, a BoxConstraints object with a minWidth of 100.0 + /// and a maxWidth of 90.0 is not normalized. + /// + /// Most of the APIs on BoxConstraints expect the constraints to be + /// normalized and have undefined behavior when they are not. In + /// checked mode, many of these APIs will assert if the constraints + /// are not normalized. bool get isNormalized => minWidth <= maxWidth && minHeight <= maxHeight; + /// Same as [isNormalized] but, in checked mode, throws an exception + /// if isNormalized is false. + bool get debugAssertIsNormalized { + assert(() { + if (maxWidth < minWidth && maxHeight < minHeight) + throw new RenderingError('BoxConstraints has both width and height constraints non-normalized.\n$this'); + if (maxWidth < minWidth) + throw new RenderingError('BoxConstraints has non-normalized width constraints.\n$this'); + if (maxHeight < minHeight) + throw new RenderingError('BoxConstraints has non-normalized height constraints.\n$this'); + return isNormalized; + }); + return isNormalized; + } + BoxConstraints normalize() { return new BoxConstraints( minWidth: minWidth, @@ -267,13 +299,13 @@ class BoxConstraints extends Constraints { } bool operator ==(dynamic other) { - assert(isNormalized); + assert(debugAssertIsNormalized); if (identical(this, other)) return true; if (other is! BoxConstraints) return false; final BoxConstraints typedOther = other; - assert(typedOther.isNormalized); + assert(typedOther.debugAssertIsNormalized); return minWidth == typedOther.minWidth && maxWidth == typedOther.maxWidth && minHeight == typedOther.minHeight && @@ -281,7 +313,7 @@ class BoxConstraints extends Constraints { } int get hashCode { - assert(isNormalized); + assert(debugAssertIsNormalized); return hashValues(minWidth, maxWidth, minHeight, maxHeight); } @@ -362,7 +394,7 @@ abstract class RenderBox extends RenderObject { /// /// Override in subclasses that implement [performLayout]. double getMinIntrinsicWidth(BoxConstraints constraints) { - assert(constraints.isNormalized); + assert(constraints.debugAssertIsNormalized); return constraints.constrainWidth(0.0); } @@ -371,7 +403,7 @@ abstract class RenderBox extends RenderObject { /// /// Override in subclasses that implement [performLayout]. double getMaxIntrinsicWidth(BoxConstraints constraints) { - assert(constraints.isNormalized); + assert(constraints.debugAssertIsNormalized); return constraints.constrainWidth(0.0); } @@ -380,7 +412,7 @@ abstract class RenderBox extends RenderObject { /// /// Override in subclasses that implement [performLayout]. double getMinIntrinsicHeight(BoxConstraints constraints) { - assert(constraints.isNormalized); + assert(constraints.debugAssertIsNormalized); return constraints.constrainHeight(0.0); } @@ -393,7 +425,7 @@ abstract class RenderBox extends RenderObject { /// /// Override in subclasses that implement [performLayout]. double getMaxIntrinsicHeight(BoxConstraints constraints) { - assert(constraints.isNormalized); + assert(constraints.debugAssertIsNormalized); return constraints.constrainHeight(0.0); } @@ -447,7 +479,7 @@ abstract class RenderBox extends RenderObject { _size = new _DebugSize(_size, this, debugCanParentUseSize); return true; }); - assert(debugDoesMeetConstraints()); + assert(() { debugAssertDoesMeetConstraints(); return true; }); } Rect get semanticBounds => Point.origin & size; @@ -532,32 +564,91 @@ abstract class RenderBox extends RenderObject { /// The box constraints most recently received from the parent. BoxConstraints get constraints => super.constraints; - bool debugDoesMeetConstraints() { - assert(!RenderObject.debugInDebugDoesMeetConstraints); - RenderObject.debugInDebugDoesMeetConstraints = true; + void debugAssertDoesMeetConstraints() { assert(constraints != null); - // verify that the size is not infinite assert(_size != null); - assert(() { - 'See https://flutter.io/layout/#unbounded-constraints'; - return !_size.isInfinite; - }); + // verify that the size is not infinite + if (_size.isInfinite) { + StringBuffer information = new StringBuffer(); + if (!constraints.hasBoundedWidth) { + RenderBox node = this; + while (!node.constraints.hasBoundedWidth && node.parent is RenderBox) + node = node.parent; + information.writeln('The nearest ancestor providing an unbounded width constraint is:'); + information.writeln(' $node'); + List settings = []; + node.debugDescribeSettings(settings); + for (String line in settings) + information.writeln(' $line'); + } + if (!constraints.hasBoundedHeight) { + RenderBox node = this; + while (!node.constraints.hasBoundedHeight && node.parent is RenderBox) + node = node.parent; + information.writeln('The nearest ancestor providing an unbounded height constraint is:'); + information.writeln(' $node'); + List settings = []; + node.debugDescribeSettings(settings); + for (String line in settings) + information.writeln(' $line'); + } + throw new RenderingError( + '$runtimeType object was given an infinite size during layout.\n' + 'This probably means that it is a render object that tries to be\n' + 'as big as possible, but it was put inside another render object\n' + 'that allows its children to pick their own size.\n' + '$information' + 'See https://flutter.io/layout/ for more information.' + ); + } // verify that the size is within the constraints - bool result = constraints.isSatisfiedBy(_size); - if (!result) - debugPrint("${this.runtimeType} does not meet its constraints. Constraints: $constraints, size: $_size"); + if (!constraints.isSatisfiedBy(_size)) { + throw new RenderingError( + '$runtimeType does not meet its constraints.\n' + 'Constraints: $constraints\n' + 'Size: $_size\n' + 'If you are not writing your own RenderBox subclass, then this is not\n' + 'your fault. Contact support: https://github.com/flutter/flutter/issues/new' + ); + } // verify that the intrinsics are also within the constraints + assert(!RenderObject.debugCheckingIntrinsics); + RenderObject.debugCheckingIntrinsics = true; double intrinsic; + StringBuffer failures = new StringBuffer(); + int failureCount = 0; intrinsic = getMinIntrinsicWidth(constraints); - assert(intrinsic == constraints.constrainWidth(intrinsic)); + if (intrinsic != constraints.constrainWidth(intrinsic)) { + failures.writeln(' * getMinIntrinsicWidth() -- returned: w=$intrinsic'); + failureCount += 1; + } intrinsic = getMaxIntrinsicWidth(constraints); - assert(intrinsic == constraints.constrainWidth(intrinsic)); + if (intrinsic != constraints.constrainWidth(intrinsic)) { + failures.writeln(' * getMaxIntrinsicWidth() -- returned: w=$intrinsic'); + failureCount += 1; + } intrinsic = getMinIntrinsicHeight(constraints); - assert(intrinsic == constraints.constrainHeight(intrinsic)); + if (intrinsic != constraints.constrainHeight(intrinsic)) { + failures.writeln(' * getMinIntrinsicHeight() -- returned: h=$intrinsic'); + failureCount += 1; + } intrinsic = getMaxIntrinsicHeight(constraints); - assert(intrinsic == constraints.constrainHeight(intrinsic)); - RenderObject.debugInDebugDoesMeetConstraints = false; - return result; + if (intrinsic != constraints.constrainHeight(intrinsic)) { + failures.writeln(' * getMaxIntrinsicHeight() -- returned: h=$intrinsic'); + failureCount += 1; + } + RenderObject.debugCheckingIntrinsics = false; + if (failures.isNotEmpty) { + assert(failureCount > 0); + throw new RenderingError( + 'The intrinsic dimension methods of the $runtimeType class returned values that violate the given constraints.\n' + 'The constraints were: $constraints\n' + 'The following method${failureCount > 1 ? "s" : ""} returned values outside of those constraints:\n' + '$failures' + 'If you are not writing your own RenderBox subclass, then this is not\n' + 'your fault. Contact support: https://github.com/flutter/flutter/issues/new' + ); + } } void markNeedsLayout() { diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index ac92bc47f23..6264ef71b8c 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -724,52 +724,45 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { void visitChildren(RenderObjectVisitor visitor) { } dynamic debugOwner; + static int _debugPrintedExceptionCount = 0; void _debugReportException(String method, dynamic exception, StackTrace stack) { try { if (debugRenderingExceptionHandler != null) { debugRenderingExceptionHandler(this, method, exception, stack); } else { - debugPrint('-- EXCEPTION CAUGHT BY RENDERING LIBRARY -------------------------------'); - debugPrint('The following exception was raised during $method():'); - debugPrint('$exception'); - debugPrint('The following RenderObject was being processed when the exception was fired:\n${this}'); - if (debugOwner != null) - debugPrint('This RenderObject had the following owner:\n$debugOwner'); - int depth = 0; - List descendants = []; - const int maxDepth = 5; - void visitor(RenderObject child) { - descendants.add('${" " * depth}$child'); - depth += 1; - if (depth < maxDepth) - child.visitChildren(visitor); - depth -= 1; - } - visitChildren(visitor); - if (descendants.length > 1) { - debugPrint('This RenderObject had the following descendants (showing up to depth $maxDepth):'); - } else if (descendants.length == 1) { - debugPrint('This RenderObject had the following child:'); - } else { - debugPrint('This RenderObject has no descendants.'); - } - descendants.forEach(debugPrint); - assert(() { - if (debugInDebugDoesMeetConstraints) { - debugPrint('This exception was thrown while debugDoesMeetConstraints() was running.'); - debugPrint('debugDoesMeetConstraints() verifies that some invariants are not being'); - debugPrint('violated. For example, it verifies that RenderBox objects are sized in'); - debugPrint('a manner consistent with the constraints provided, and, in addition, that'); - debugPrint('the getMinIntrinsicWidth(), getMaxIntrinsicWidth(), etc, functions all'); - debugPrint('return consistent values within the same constraints.'); - debugPrint('If you are not writing your own RenderObject subclass, then this is not'); - debugPrint('your fault. Contact support: https://github.com/flutter/flutter/issues/new'); + _debugPrintedExceptionCount += 1; + if (_debugPrintedExceptionCount == 1) { + debugPrint('-- EXCEPTION CAUGHT BY RENDERING LIBRARY -------------------------------'); + debugPrint('The following exception was raised during $method():'); + debugPrint('$exception'); + debugPrint('The following RenderObject was being processed when the exception was fired:\n${this}'); + if (debugOwner != null) + debugPrint('This RenderObject had the following owner:\n$debugOwner'); + int depth = 0; + List descendants = []; + const int maxDepth = 5; + void visitor(RenderObject child) { + descendants.add('${" " * depth}$child'); + depth += 1; + if (depth < maxDepth) + child.visitChildren(visitor); + depth -= 1; } - return true; - }); - debugPrint('Stack trace:'); - debugPrint('$stack'); - debugPrint('------------------------------------------------------------------------'); + visitChildren(visitor); + if (descendants.length > 1) { + debugPrint('This RenderObject had the following descendants (showing up to depth $maxDepth):'); + } else if (descendants.length == 1) { + debugPrint('This RenderObject had the following child:'); + } else { + debugPrint('This RenderObject has no descendants.'); + } + descendants.forEach(debugPrint); + debugPrint('Stack trace:'); + debugPrint('$stack'); + debugPrint('------------------------------------------------------------------------'); + } else { + debugPrint('Another exception was raised: ${exception.toString().split("\n")[0]}'); + } } } catch (exception) { debugPrint('(exception during exception handler: $exception)'); @@ -809,15 +802,23 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { Constraints _constraints; /// The layout constraints most recently supplied by the parent. Constraints get constraints => _constraints; - /// Override this function in a subclass to verify that your state matches the constraints object. - bool debugDoesMeetConstraints(); - /// When true, debugDoesMeetConstraints() is currently executing. + /// Verify that the object's constraints are being met. Override + /// this function in a subclass to verify that your state matches + /// the constraints object. This function is only called in checked + /// mode. If the constraints are not met, it should assert or throw + /// an exception. + void debugAssertDoesMeetConstraints(); + + /// When true, debugAssertDoesMeetConstraints() is currently + /// executing asserts for verifying the consistent behaviour of + /// intrinsic dimensions methods. /// - /// This should be set by implementations of debugDoesMeetConstraints() so that - /// tests can selectively ignore custom layout callbacks. It should not be set - /// outside of debugDoesMeetConstraints() implementations and should not be used - /// for purposes other than tests. - static bool debugInDebugDoesMeetConstraints = false; + /// This should only be set by debugAssertDoesMeetConstraints() + /// implementations. It is used by tests to selectively ignore + /// custom layout callbacks. It should not be set outside of + /// debugAssertDoesMeetConstraints(), and should not be checked in + /// release mode (where it will always be false). + static bool debugCheckingIntrinsics = false; bool debugAncestorsAlreadyMarkedNeedsLayout() { if (_relayoutSubtreeRoot == null) return true; // we haven't yet done layout even once, so there's nothing for us to do @@ -1022,7 +1023,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { assert(() { _debugDoingThisResize = true; return true; }); try { performResize(); - assert(debugDoesMeetConstraints()); + assert(() { debugAssertDoesMeetConstraints(); return true; }); } catch (e, stack) { _debugReportException('performResize', e, stack); } @@ -1038,7 +1039,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { try { performLayout(); markNeedsSemanticsUpdate(); - assert(debugDoesMeetConstraints()); + assert(() { debugAssertDoesMeetConstraints(); return true; }); } catch (e, stack) { _debugReportException('performLayout', e, stack); } @@ -2023,3 +2024,10 @@ abstract class ContainerRenderObjectMixin message; +} diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart index 50ab9352be6..abdcbf47654 100644 --- a/packages/flutter/lib/src/rendering/view.dart +++ b/packages/flutter/lib/src/rendering/view.dart @@ -78,7 +78,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin // We never call layout() on this class, so this should never get // checked. (This class is laid out using scheduleInitialLayout().) - bool debugDoesMeetConstraints() { assert(false); return false; } + void debugAssertDoesMeetConstraints() { assert(false); } void performResize() { assert(false); diff --git a/packages/flutter/lib/src/widgets/mixed_viewport.dart b/packages/flutter/lib/src/widgets/mixed_viewport.dart index 75f1a521b5e..fcac2afe897 100644 --- a/packages/flutter/lib/src/widgets/mixed_viewport.dart +++ b/packages/flutter/lib/src/widgets/mixed_viewport.dart @@ -155,10 +155,14 @@ class _MixedViewportElement extends RenderObjectElement { double _noIntrinsicExtent(BoxConstraints constraints) { assert(() { - 'MixedViewport does not support returning intrinsic dimensions. ' + - 'Calculating the intrinsic dimensions would require walking the entire child list, ' + - 'which defeats the entire point of having a lazily-built list of children.'; - return RenderObject.debugInDebugDoesMeetConstraints; + if (!RenderObject.debugCheckingIntrinsics) { + throw new UnsupportedError( + 'MixedViewport does not support returning intrinsic dimensions.\n' + 'Calculating the intrinsic dimensions would require walking the entire child list,\n' + 'which defeats the entire point of having a lazily-built list of children.' + ); + } + return true; }); return null; } diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 70921b61702..52630baa806 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -442,13 +442,14 @@ class ScrollableViewportState extends ScrollableState { class Block extends StatelessComponent { Block({ Key key, - this.children, + this.children: const [], this.padding, this.initialScrollOffset, this.scrollDirection: Axis.vertical, this.onScroll, this.scrollableKey }) : super(key: key) { + assert(children != null); assert(!children.any((Widget child) => child == null)); } diff --git a/packages/flutter/test/widget/custom_multi_child_layout_test.dart b/packages/flutter/test/widget/custom_multi_child_layout_test.dart index 863317e76ca..6145a77cce6 100644 --- a/packages/flutter/test/widget/custom_multi_child_layout_test.dart +++ b/packages/flutter/test/widget/custom_multi_child_layout_test.dart @@ -11,7 +11,7 @@ class TestMultiChildLayoutDelegate extends MultiChildLayoutDelegate { BoxConstraints getSizeConstraints; Size getSize(BoxConstraints constraints) { - if (!RenderObject.debugInDebugDoesMeetConstraints) + if (!RenderObject.debugCheckingIntrinsics) getSizeConstraints = constraints; return new Size(200.0, 300.0); } @@ -23,7 +23,7 @@ class TestMultiChildLayoutDelegate extends MultiChildLayoutDelegate { bool performLayoutIsChild; void performLayout(Size size, BoxConstraints constraints) { - assert(!RenderObject.debugInDebugDoesMeetConstraints); + assert(!RenderObject.debugCheckingIntrinsics); expect(() { performLayoutSize = size; performLayoutConstraints = constraints; @@ -36,7 +36,7 @@ class TestMultiChildLayoutDelegate extends MultiChildLayoutDelegate { bool shouldRelayoutCalled = false; bool shouldRelayoutValue = false; bool shouldRelayout(_) { - assert(!RenderObject.debugInDebugDoesMeetConstraints); + assert(!RenderObject.debugCheckingIntrinsics); shouldRelayoutCalled = true; return shouldRelayoutValue; } diff --git a/packages/flutter/test/widget/custom_one_child_layout_test.dart b/packages/flutter/test/widget/custom_one_child_layout_test.dart index 72b894c309b..ecf7506c3e0 100644 --- a/packages/flutter/test/widget/custom_one_child_layout_test.dart +++ b/packages/flutter/test/widget/custom_one_child_layout_test.dart @@ -14,13 +14,13 @@ class TestOneChildLayoutDelegate extends OneChildLayoutDelegate { Size childSizeFromGetPositionForChild; Size getSize(BoxConstraints constraints) { - if (!RenderObject.debugInDebugDoesMeetConstraints) + if (!RenderObject.debugCheckingIntrinsics) constraintsFromGetSize = constraints; return new Size(200.0, 300.0); } BoxConstraints getConstraintsForChild(BoxConstraints constraints) { - assert(!RenderObject.debugInDebugDoesMeetConstraints); + assert(!RenderObject.debugCheckingIntrinsics); constraintsFromGetConstraintsForChild = constraints; return new BoxConstraints( minWidth: 100.0, @@ -31,7 +31,7 @@ class TestOneChildLayoutDelegate extends OneChildLayoutDelegate { } Offset getPositionForChild(Size size, Size childSize) { - assert(!RenderObject.debugInDebugDoesMeetConstraints); + assert(!RenderObject.debugCheckingIntrinsics); sizeFromGetPositionForChild = size; childSizeFromGetPositionForChild = childSize; return Offset.zero; @@ -40,7 +40,7 @@ class TestOneChildLayoutDelegate extends OneChildLayoutDelegate { bool shouldRelayoutCalled = false; bool shouldRelayoutValue = false; bool shouldRelayout(_) { - assert(!RenderObject.debugInDebugDoesMeetConstraints); + assert(!RenderObject.debugCheckingIntrinsics); shouldRelayoutCalled = true; return shouldRelayoutValue; }