-
Notifications
You must be signed in to change notification settings - Fork 1.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
Migration: propagate non-null intent when all downstream nodes have non-null intent. #40509
Comments
@MichaelRFairhurst notes that if we make this change, we'll also have to make additional graph edges to track situations where a variable's value might not be used. Consider: int/*1*/ f(int x) => x;
void g(bool b, int/*?*/ x) {
int/*2*/ foo = f(x);
if (b) {
foo + 1;
}
} There's only one edge leading out from node 2, pointing to "never" (caused by the statement int/*1*/ f(int x) => x;
void g(bool b, int/*?*/ x) {
int/*2*/ foo = f(x!);
if (b) {
foo + 1;
}
} This is bad because it changes behavior in the case where |
There's a problem in the algorithm proposed above: it does the wrong thing for local variables. Consider: void f(int x, bool b1, bool b2, bool b3) {
if (b1) print(x + 1);
if (b2) print(x + 2);
if (b3) print(x + 3);
}
main() {
f(null, false, false, false);
} Analyzing this code causes three edges to be created pointing from the nullability of @MichaelRFairhurst and I believe that we can solve this by classifying nodes into two kinds: "variable-like" nodes and "dispatch-like" nodes. A node that is "dispatch-like" has the property that if a null value flows into it through one of its upstream edges at any point in program execution, that event is guaranteed to be followed by a null value flowing out of it along at least one of its downstream edges; a "variable-like" node does not have this property. We would only do the back propagation of non-null intent proposed above on dispatch-like nodes. Examples of dispatch-like nodes are the nodes associated with return types (since returned values are immediately propagated to their respective call sites) and the nodes associated with callback parameters (since values passed into callbacks are immediately propagated to the callback's implementation). Examples of variable-like nodes are nodes associated with the nullability of ordinary function parameters and local variables. Note that one way of implementing the distinction between variable-like and dispatch-like would be to create an artifical edge pointing from any variable-like node to "always". But this might not provide the best user experience, since the edge would show up in the preview tool. |
Helps with #40509 and #38747 Change-Id: Iac7208ee38dcc6c0b055f9e46c2d755ad0388488 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/143962 Commit-Queue: Samuel Rawlins <srawlins@google.com> Reviewed-by: Paul Berry <paulberry@google.com>
@a14n made a similar suggestion. His example looks like this: class A {
Platform platform;
String get _env => platform.environment['key'];
void m1() {
List<String> v = _env.split('.');
}
void m2() {
int v = _env.length;
}
} The migration tool suggests: class A {
late Platform platform;
String? get _env => platform.environment['key'];
void m1() {
List<String > v = _env!.split('.');
}
void m2() {
int v = _env!.length;
}
} But this would be better: class A {
late Platform platform;
String get _env => platform.environment['key']!;
void m1() {
List<String > v = _env.split('.');
}
void m2() {
int v = _env.length;
}
} |
As of 1c7fe71, the null safety migration tool has been removed from active development and retired. No further work on the tool is planned. If you still need help, or you believe this issue has been closed in error, please feel free to reopen. |
Capturing some discussion between me and @MichaelRFairhurst this morning
Consider the following code:
Currently the migration engine transforms (3) into
g((int? x) => x!)
because ifx
were not null-checked, the function literal wound have typeint? Function(int?)
, and hence couldn't be passed tog
.Semantically, the call sites at (4) and (5) are equivalent to (3), so it seems reasonable that the tool should perform similar migrations to
f1
andf2
, changing (1) toint f1(int? x) => x!;
, and (2) tovar f2 = (int? x) => x!;
. But currently it doesn't. The reasoning process is: in (3) the function literal exists solely so that it can be passed tog
, so no functionality is lost by adding the null check. Whereas the functions declared in (1) and (2) might be used in some other context where their ability to return a null value is important. As a result, migration adds a cast to (4) and (5) that is guaranteed to fail, and the user has to take manual action to address the problem (see #40471).But we can do better. Since the migration tool does whole program analysis, it should know whether the functions declared in (1) and (2) are used in any other context; if they aren't it can safely propagate the null check into them, producing a better migration result.
This requires a small change to the way the nullability graph propagates non-null intent. Today the rule is: given a node A, if there is any node B pointed to by A via a hard edge, and B has non-null intent, then non-null intent is propagated to A. We would keep that rule, and add the additional rule: given a node A, if all nodes B pointed to by A have non-null intent (regardless of the type of edge connecting them), then non-null intent is propagated to A.
Note: in addition to fixing cases involving callback functions like the one above, we expect this design to in general reduce the number of null checks the tool has to introduce, because if it detects that all calls to a function use its return value in a way that requires it to be non-null, it will consolidate the null check into the function itself rather than force all call sites to do the null check.
Note: this algorithm affects not just the behavior of the
f
functions in the example above, but also theg
function. Once all thef
functions have had non-nullable intent propagated through them, all nodes pointed to by the node forcallback
's parameter will have non-null intent, so it will be marked as having non-null intent, and the null check will be pushed back to the explicitnull
, like so:Of course, it's trivial to see that (1) will fail at runtime. We think this is good, since the code is almost certainly in error, and pushing the null check back to (1) makes that trivially apparent to the user.
Note: in order for this to work properly, we need to make sure that when the EdgeBuilder encounters an expression statement, it draws an edge from the type of the expression to
always
. This will ensure that in the following code, the call at (2) prevents us from adding a null check to (1):Note: if a function in the package's public API might return null, but all call sites within the package null-check its result, the migration tool will go ahead and mark the function's return type as non-null (and add null checks within the function itself). This may not be what the user wants. We think this is ok--it is consistent with the principle that the migration tool should bias toward non-nullability as much as it can based on the examples it sees in the source code given to it; if the user wants to allow additional nullability, they should either add a test case or a
/*?*/
hint.Note: this should also improve our migration results for the case where a list is created, populated, and then passed to a function expecting a list of non-null, e.g. in the following code a null check will be added to (1) rather than a cast added to (2):
The text was updated successfully, but these errors were encountered: