-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Analyzer does not promote in closures (WAI), but no warning/hint #32285
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
Comments
I see a similar issue when |
Is that really the right repro? That code works for me with bleeding edge analyzer. What version of the analyzer? |
|
I don't see that error with that code, and that analyzer version. |
I'll try and write a tiny reproduction case internally and share with you. |
I shared an internal case. This is the code that fails for me: import 'dart:async';
// Created ~roughly from 'class ApplicationRef' from AngularDart.
class ApplicationRefWithFutureOr {
run<R>(FutureOr<R> callback()) {
FutureOr<R> result;
var completer = new Completer<R>();
Zone.current.run(() {
try {
result = callback();
if (result is Future<R>) {
// error: The method 'then' isn't defined for the class 'FutureOr<R>'.
result.then((ref) {
completer.complete(ref);
});
}
} catch (e) {
rethrow;
}
});
return result is Future ? completer.future : result;
}
}
class ApplicationRefWithDynamic {
run<R>(FutureOr<R> callback()) {
dynamic result;
var completer = new Completer<R>();
Zone.current.run(() {
try {
result = callback();
if (result is Future<R>) {
// No error, but note in IDEs that 'result' is inferred as dynamic.
result.then((ref) {
completer.complete(ref);
});
}
} catch (e) {
rethrow;
}
});
return result is Future ? completer.future : result;
}
}
class ApplicationRefWithObject {
run<R>(FutureOr<R> callback()) {
Object result;
var completer = new Completer<R>();
Zone.current.run(() {
try {
result = callback();
if (result is Future<R>) {
// error: The method 'then' isn't defined for the class 'Object'.
result.then((ref) {
completer.complete(ref);
});
}
} catch (e) {
rethrow;
}
});
return result is Future ? completer.future : result;
}
} |
Ok, in that code, class ApplicationRefWithFutureOr {
run<R>(FutureOr<R> callback()) {
FutureOr<R> result;
var completer = new Completer<R>();
Zone.current.run(() {
try {
FutureOr<R> localResult = callback();
result = localResult;
if (localResult is Future<R>) {
// error: The method 'then' isn't defined for the class 'FutureOr<R>'.
localResult.then((ref) {
completer.complete(ref);
});
}
} catch (e) {
rethrow;
}
});
return result is Future ? completer.future : result;
}
} |
Oh, well, I guess... Why? Should the analyzer help here? |
In general, because closures can hide mutations to variables that can invalidate promotion. The code below calls void test(FutureOr<int> callback()) {
var result;
var a;
a = () {
if (result == null) {
result = callback();
if (result is Future<int>) {
// error: The method 'then' isn't defined for the class 'FutureOr<R>'.
a();
result.then((value) => null);
}
}
result = "hello";
};
a();
}
void main() {
test(() => new Future.value(3));
} |
I'd love for analyzer to be able to help. Making the type system's inference and promotion more visible (less mysterious) is a very desirable goal. I can think of a couple of ways to do that in this case, but both have drawbacks. We could add a lint when a local variable is not being promoted, but I suspect that there would be a lot of false positives, so I'm not sure anyone would find it useful enough to enable it. We could provide such information in a less obtrusive way, such as in the hover text you get when you hover over the variable, but (a) that limits the feature to only being useful to users of clients of server, and (b) makes the feature very hard to discover. If you (or anyone) has other ideas for ways we could support this or related needs, we'd love to discuss them. |
One thing that we could consider in the language is to add an "always promote if possible" "is" check The other thing we've discussed is allowing unsound promotion but adding dynamic checks when we can't prove it sound. I don't love this though since it can silently add more runtime checks that you don't expect. |
Right. Another way to spin that is to add a flow control structure whose express purpose is type testing + promotion. In other words, something along the lines of pattern matching. (And, if it bound a new variable, that would avoid the issues with promoting a mutated variable.) |
I guess in this particular case, I would have "done it right" by putting the variable in the closure if I knew that is what I needed to do (it just seems like a bug in the analyzer without any indication otherwise). |
This is a difficult one. I've seen an amazing number of bugs filed because people were expecting promotion to happen where it wasn't. It's enough to make me wonder whether the analyzer should just issue a hint whenever promotion gets disabled. But... that could get annoying if you're trying to be hint free. It certainly at least feels like something that we could have an opt-in lint for. cc @pq |
We have discussed a feature where promotion is made explicit by binding a new name: class ApplicationRefWithFutureOr {
run<R>(FutureOr<R> callback()) {
FutureOr<R> result;
var completer = new Completer<R>();
Zone.current.run(() {
try {
result = callback();
if (result is Future<R> future) { // <-- Declare `future`, init. with `result`.
future.then((ref) {
completer.complete(ref);
});
}
} catch (e) {
rethrow;
}
});
return result is Future ? completer.future : result;
}
} This The point is that the named typeTest only makes sense when promotion takes place, which means that it will enable developers to explicitly request promotion (so it's an error when promotion cannot take place, but that will probably never happen because this is a final variable, and Also note that the ability to test the value of an arbitrary expression This looks like a sensible way to address some of the issues raised here. |
I would personally support "optimistic promotion" where an If the variable If the compiler can analyze that the test isn't necessary, it can omit it. That's the case here - there is no unknown code executed between the test and the use. This will allow the compiler to be arbitrarily smart in removing the runtime check, instead of only allowing promotion when we can specify up-front in the spec that it's certainly safe. (I'd also love to make assignment and promote the variable to the static type of the left-hand side in the following code, but that does require a notion of continuation flow). |
CC @stereotype441 who is working on "not promoted because ..." messaging. |
This works as a workaround:
The text was updated successfully, but these errors were encountered: