-
Notifications
You must be signed in to change notification settings - Fork 27.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Prevent uncontrolled crash in Release mode when ParentDataWidget is used incorrectly #155091
Prevent uncontrolled crash in Release mode when ParentDataWidget is used incorrectly #155091
Conversation
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption, contact "@test-exemption-reviewer" in the #hackers channel in Discord (don't just cc them here, they won't see it!). If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. The test exemption team is a small volunteer group, so all reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group. |
|
||
final dynamic exception = tester.takeException(); | ||
|
||
expect(exception, isFlutterError); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prevent the use of dynamic
here. (If you really want a type, use Object?
instead)
expect(exception, isFlutterError); | |
expect(tester.takeException(), isFlutterError); |
|
||
final dynamic exception = tester.takeException(); | ||
|
||
expect(exception, isFlutterError); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should also check if the error message is correct, using the having()
matcher.
@@ -1574,6 +1574,23 @@ void main() { | |||
expect(RawKeyboard.instance.keyEventHandler, same(rawKeyEventHandler)); | |||
}); | |||
|
|||
testWidgets('Incorrect use of ParentDataWidget shows a useful error message', (WidgetTester tester) async { | |||
await tester.pumpWidget( | |||
Container( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Use SizedBox
instead of Container
.
throw FlutterError.fromParts(<DiagnosticsNode>[ | ||
ErrorSummary('Incorrect use of ParentDataWidget.'), | ||
ErrorSummary('Incorrect use of ParentDataWidget. The widget `${ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the formatting be improved by using a triple quotes string?
I.e.
'''
Incorrect use of ParentDataWidget.
The widget `${f.runtimeType} must be a direct child
of a `Row`, `Column`, or `Flex` widget.'
'''
throw FlutterError.fromParts(<DiagnosticsNode>[ | ||
ErrorSummary('Incorrect use of ParentDataWidget.'), | ||
ErrorSummary('Incorrect use of ParentDataWidget. The widget `${ | ||
parentDataWidget.runtimeType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling runtimeType.toString()
in release mode is expensive. Consider using the objectRuntimeType()
helper, although that one also uses an assert.
See https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/foundation/object.dart
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found a toStringShort
helper function within this file which calls objectRuntimeType
- don't call expensive `.runtimeType` - improve formatting - make test more specific
@navaronbracke thanks for your review, I have resolved your comments and updated the branch. |
SizedBox( | ||
key: const Key('container'), | ||
child: Flexible( // Flexible should not be used directly in a Container or other inappropriate parents | ||
child: Container(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still a Container
. This should probably also be a SizedBox()
. See https://dart.dev/tools/linter-rules/avoid_unnecessary_containers
You might also need to add const
to the first SizedBox widget.
try { | ||
String errorMessage = "Incorrect use of ParentDataWidget.\n"; | ||
|
||
if (parentDataWidget.toStringShort() == 'Positioned') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we special case the Positioned/Stack case here? There might be an unknown amount of possibilities that lead to the "Incorrect use of ParentDataWidget" error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember when I was new to Flutter, the Incorrect use of ParentDataWidget.
error was very confusing to me, so I wanted to have a more descriptive error message as well as to fix the CI tests. Also when I was looking around the tests, I couldn't find any other violations leading to this error - but that's not to say there won't be a new way of violating it in future.
I'll add specific cases only for Positioned
, Flexible
, and Expanded
, leaving the generic error if it doesn't match.
Co-authored-by: Navaron Bracke <brackenavaron@gmail.com>
String errorMessage = 'Incorrect use of ParentDataWidget.\n'; | ||
|
||
if (parentDataWidgetType == 'Positioned') { | ||
errorMessage += 'The widget `Positioned` must be a descendent of a `Stack` widget.\n'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The wording in these two messages is different? One mentions descendant
(there is a typo in that word, too)
and the other mentions the word child
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll update the spelling there. So with Positioned
it can be anywhere in the Stack
(i.e. it does not have to be a direct child). For a Flexible
or Expanded
, it needs to be a direct child, hence the differing wording.
@@ -1574,6 +1574,32 @@ void main() { | |||
expect(RawKeyboard.instance.keyEventHandler, same(rawKeyEventHandler)); | |||
}); | |||
|
|||
testWidgets('Incorrect use of ParentDataWidget shows a useful error message', (WidgetTester tester) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need an additional test for the Stack/Positioned case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So there are already tests for this which I've found in parent_test_data.dart
, which already covers the case that I've tested in this PR. I've edited the tests in parent_test_data
to work with the new error messages and I now think the test I have made is redundant, and actually would not have failed before I made the change in framework.dart
, so I think I need a better test, what do you think?
I can't currently think of a way to test release-mode code though - which is what this PR addresses, so I'm going to look into this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original code I modified relied on code in an assert
to control the flow, and hence running into nasty exceptions only in release mode. I have made a contrived example below demonstrating the issue in the original code:
num returnDivisionCalculation() {
num numerator = 1;
num denominator = 0;
bool attemptCalculation = true;
assert(() {
if (denominator == 0) {
attemptCalculation = false;
try {
throw "Specific division error";
} catch {
// log the error and continue
}
}
}());
if (attemptCalculation) { // Always true in release mode.
return numerator / denominator;
}
return 0;
}
In debug mode, the code in assert
will be run, causing safe and controlled execution of the code, but in release mode an unexpected exception will be thrown.
I don't think it's possible to write a failing test that will succeed after my change and I will ask for an exemption on Discord. I have edited the tests to update the new error messages for misuse of these widgets (of which I am happy to revert, just thought I might improve them while here)
@navaronbracke do you think that there is a way to test this?
if (parentDataWidgetType == 'Positioned') { | ||
errorMessage += 'The widget `Positioned` must be a descendant of a `Stack` widget.\n'; | ||
} else if (parentDataWidgetType == 'Flexible' || parentDataWidgetType == 'Expanded') { | ||
errorMessage += 'The widget `$parentDataWidgetType` must be a direct child of a `Row`, `Column`, or `Flex` widget.\n'; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This checking for types is not something we should do. There are many ParentDataWidgets, this error message handling could not possibly account for all of them. It also means we are baking in knowledge of other classes here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, the idea here is to only account for 3 common ones -- it leaves the error message as-is for any other ParentDataWidget to reduce confusion. IMO "Incorrect use of ParentDataWidget" isn't a very clear error message.
Having said this I'm very happy to remove attempted improvements to DX and fix only the original issue!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The right solution is likely to expose a way for ParentDataWidgets to be able to provide their own error message. That way any of them could provide more information.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, makes sense. I think that is beyond the scope of this fix then, would you agree? I will remove changes to the error messages in this PR tonight.
A problem I'm having with this fix is testing. At its core, it's fixing some bad code that is only run in debug mode. I left a comment here, do you know of a way to test this?
- Remove tests - Remove attempt at improving error message
@Piinks I've now simplified this PR, but it's still missing a test. |
(triage) Hi @darajava we need a test to merge this pr. Can you add a test? |
Hi, no I don’t think this is testable since the bug is due to flow control
being determined from with an assert, which only is run on debug mode,
hence the bug only happens on release mode. Happy to add a test if there is
a way to do it!
…On Wed, 2 Oct 2024 at 00:11, chunhtai ***@***.***> wrote:
(triage) Hi @darajava <https://github.com/darajava> we need a test to
merge this pr. Can you add a test?
—
Reply to this email directly, view it on GitHub
<#155091 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABNCOBN73HEEE5YSHHOS64LZZMMX5AVCNFSM6AAAAABODPODHOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDGOBXGE3DINRWGY>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
try { | ||
if (!parentDataWidget.debugIsValidRenderObject(renderObject)) { | ||
applyParentData = false; | ||
if (parentDataWidget.debugIsValidRenderObject(renderObject)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably not named started with debug, and should probably be convert into a private static method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. It affects the control flow, and should not be exclusive to assert mode. But it was written this way ever since the first day, and not only debugIsValidRenderObject
, but also the method that it depends on, debugTypicalAncestorWidgetClass
, are prefixed by debug
, the latter being intended for overriding and is indeed overridden by many classes. We might want to ask Hixie (the first author) whether it was intentional.
Edit: Hixie has added context in #108186 weeks ago.
It would be best to rename debugTypicalAncestorWidgetClass
(which assigns the type used here) as well, but it has been used by many widgets that I doubt if we can. At least we should add a comment to explain that it's now used in release mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't use debugTypicalAncestorWidgetClass in release mode, because it'll force widgets to be included that might otherwise get optimised out. But I think we can still throw in release mode even if the error message isn't as clear. We have lots of precedent for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(debugIsValidRenderObject should be renamed though)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've renamed debugIsValidRenderObject
and made it private. I tried making it static but the generic type made some tests fail and I couldn't figure out why.
Are you saying that incorrect usage of ParentDataWidget should still throw and cause a crash to users in release mode? I'd have imagined that release mode should be as fault-tolerant as possible, and attempt to deliver a good experience even if it doesn't follow Flutter's rules. In my experience using e.g. Flexible
outside a Column
does not cause any visual/usability issues, only a broken tree which is flagged in the logs in debug mode.
A similar case to this that currently does exist is using a Positioned
outside a Stack
. There is no point in using it outside a Stack
, and it is an error, but it does not currently break the release build and gives an error in the console in debug mode.
@darajava in the test I would expect to verify the assertion is triggered. |
@@ -1604,7 +1604,7 @@ abstract class ParentDataWidget<T extends ParentData> extends ProxyWidget { | |||
/// | |||
/// This is called just before [applyParentData] is invoked with the same | |||
/// [RenderObject] provided to that method. | |||
bool debugIsValidRenderObject(RenderObject renderObject) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a public method, making it private is a breaking change. Do we need to rename this here?
// If it's not a valid render object, then don't show the ErrorWidget, or | ||
// throw an exception, just report the error and move on. | ||
// | ||
// This prevents it from crashing in release mode and still reports the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may not prevent it from crashing, depending on what relies on that parent data being applied. For example, if a widget lays out multiple children, and relies on parent data to inform the layout, this change could skip a crash here, just to crash later when it cannot properly lay out the widgets.
I’ve put too many hours into this important but simple fix and can’t afford to put more in unfortunately. If anyone wants to continue please feel free! |
Fixes #108186 and #155051
The following (incorrect) code causes a nasty crash in release mode:
Before:
After:
I have refactored
_updateParentData
to make it more simple and to give a clearer and more specific error message whenFlexible
orExpanded
is misused. In both release and debug builds we now checkparentDataWidget.debugIsValidRenderObject
before callingapplyParentData
. The problem in the previous code was that there was a check happening in anassert
. Therefore, in release/profile modesapplyParentData
was being unconditionally called, leading to an error causing the UI to completely break and become unusable.With this change,
Flexible
can be called now in any component without breaking the UI, and still gets an exception reported in debug mode.I had initially tried to show
ErrorWidget
when the tree is in a broken state, but that leads to more exceptions as the original comment in the code I worked on suggested because adding more widgets to a broken tree leads to more exceptions which is hard to test and poor for the debugging experience, as is mentioned.Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.