-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Lint for using a type literal in a switch case #59087
Comments
@pq @bwilkerson Do we want to make it s lint, or a warning inside the analyzer? |
It isn't wrong to use a type literal in a switch case, so I see this as enforcing a coding style, which makes it a lint. |
I'm all for this and agree with Brian that a lint is the right place to implement. |
Yes, if it were up to me (and it's not, this is really for the tooling teams to decide), I would make it a lint too. For me, warnings all represent some kind of dead code: Code you can remove or simplify and the result is a program with the same semantics. |
The other major use for warnings is for code that's guaranteed to be wrong, but for reasons that the type system isn't able to express. |
Can you give me an example of the latter? |
We have a warning telling you that the callback can't be used as an error handler:
The type system can't tell us that because the type of the parameter is |
Oh, interesting. So there's some ad hoc overloading support in the analyzer effectively? |
I'm not sure I'd call it overloading, we're just doing some type checking that isn't (can't be?) specified in the declaration of |
Note that there is a connection to dart-lang/language#2894, about 'case constants that will never be An identifier which is a type literal is known to have primitive equality. This makes it possible to know at compile-time that its operator In other words, this lint could recognize class A {}
void main() {
switch (A()) {
case int: break; // Warning.
case const (List<int>): break; // Warning.
case == Map<int, String>: break; // No warning, seems like it should be included.
case int(hashCode: int): break; // Warning.
}
switch (1 as dynamic) {
case int: break;
case const (List<int>): break;
case == Map<int, String>: break;
case int(hashCode: int): break; // Warning.
}
} I think the work requested in this issue could presumably be done most easily by generalizing that warning a bit. It is true that we wouldn't get a warning for a very general matched value type (including |
…priate. Bug: https://github.com/dart-lang/linter/issues/4195 Change-Id: I5cf06da31aed9347002684580c106747a057e1f1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/290701 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
About Not as an excuse (unlike the above), but we also have |
Ah, my bad, it won't work in that case: The matched value gets to run operator By the way, I think this means that it is not going to be a safe rule to replace |
Fair. I'm fine with the lint suggesting users replace |
Do you mean |
Well, I thought some more about this lint, and start doubting how useful it is. Matching @eernstg example shows that we already report a warning for |
I didn't realize that we already report lint errors for places where a type literal won't match the static type of the matched value. Given that, I think that's sufficient for now. |
Just relying on type is not sufficient. Take: Map<String, dynamic> json = ...;
if (json case {"type": "value", "value": int}) {
print("An integer value");
} else {
print("Not an integer value");
} The plain (I really want to make a plain type literal as a pattern an error now, to push people towards either |
After @mit-mit made this mistake several times, we talked about it more in the language meeting. Since this is a long comment, here's the summary up front:
We can't change the pattern syntax (and even if we could, we don't know what we'd change it to), so this seems like a good candidate for a lint. Discussing it at the meeting this morning, I think there was fairly broad agreement that the lint should report all uses of identifier patterns that resolve to type literals, even in cases where a type literal might be what you want. So it would fire in all of: // Supertype of Type:
switch (obj as Object) { case int: ... }
// Type:
switch (obj as Type) { case int: ... }
// Nullable Type:
switch (obj as Type?) { case int: ... }
// Dynamic:
switch (obj as dynamic) { case int: ... } Basically treat typed literals as scorched Earth. When the lint fires on a type literal, it should suggest a fix. Based on my analysis, by far the most likely thing a user meant is to write a type test. So it should first suggest something like, "If you meant to test if the object has type Then something like "If you do mean to test that the matched value is equal to the type literal We could also suggest This lint doesn't need to be (and at this point, won't be) in 3.0, but it would be helpful whenever it can become available. We'll probably put it in the recommended lint set, possibly even in core. Here's the rest of the context... Looking at type literals in existing switchesI looked into existing switch statements to try to get a feel for how often type literals appear. This should tell us how important of a use case that is and how often writing a type literal in a case is deliberate. My analysis is pretty rough because I can't precisely tell if an identifier is a type literal or not without doing resolution. Instead, I just assume that it's a type literal if the name is one of the built in lowercase types (
So it's very rare for a user to deliberately use a type literal in a switch in order to see if a value is equal to some type object. Looking through several of the switches that do, most are like: switch (drawable.runtimeType) {
case Pixel:
Pixel p = drawable as Pixel;
putPixel(p);
break;
case Line:
Line l = drawable as Line;
drawLine(l);
break;
case WireFrameTriangle:
WireFrameTriangle wt = drawable as WireFrameTriangle;
drawWireframeTriangle(wt);
break;
case FilledTriangle:
FilledTriangle ft = drawable as FilledTriangle;
drawFilledTriangle(ft);
break;
// ...
default:
debugPrint('can\'t draw : unknown type: ?');
} So they're calling switch (drawable) {
case Pixel p:
putPixel(p);
case Line l:
drawLine(l);
case WireFrameTriangle wt:
drawWireframeTriangle(wt);
case FilledTriangle ft:
drawFilledTriangle(ft);
// ...
default:
debugPrint('can\'t draw : unknown type: ?');
} So, if nothing else, that left me feeling good that patterns will make it easier to write type testing switches correctly and compactly. There are a handful of switches I saw that are switching on a type parameter, like: switch (T) {
case Uri:
return Uri.parse(v) as T;
case DateTime:
return DateTime.fromMicrosecondsSinceEpoch((v * 1000 * 1000).round())
as T;
case Duration:
return Duration(microseconds: (v * 1000 * 1000).round()) as T;
case String:
case num:
case bool:
return v;
default:
return factory == null ? v : factory(v);
} Those are a more reasonable use of a type literal since you don't have an instance to type test. Overall, the conclusion I see is that a type literal in a switch case or pattern is very rarely deliberate. Going forward, when a user writes one, they will almost certainly be intended to write a type test. So allowing type literals in patterns and having them mean "equivalent to the type object" is very likely a serious footgun. Existing errors, warnings, and lintsThe existing static analysis may catch some of these mistakes incidentally. For example, if you write: test(num n) {
switch (n) {
case int: print('integer');
case double: print('float');
}
} The analyzer reports:
But the error here is only because test(Map<String, dynamic> json) {
switch (json) {
case {'id': int}: print('integer id');
}
} |
Great analysis! While we're at it, I find this lint message hard to read. Anyone has suggestions for how to potentially improve it?
|
I think if we have a lint that fires on all type literals and routes them to what they most likely intend to write, a type test, then almost no one will ever see that warning message. |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as resolved.
This comment was marked as resolved.
@johnpryan tells me that this issue seems to be mentioned as a common footgun on socials. @scheglov @bwilkerson @pq thoughts on implementing the lint @munificent describes above? |
https://dart-review.googlesource.com/c/sdk/+/305420 for 'Convert to constant pattern' quick fix. |
Bug: https://github.com/dart-lang/linter/issues/4195 Change-Id: I76eeaaa8d8c9508c10a81d1eecd0bc46c8c7d1f5 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/305420 Commit-Queue: Konstantin Shcheglov <scheglov@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Nice! |
Note: a cherry pick discussion for this is happening in the comments of dart-lang/linter#4358 |
Cherry-pick prepared: #52516 |
…tern` Linter branch: https://github.com/dart-lang/linter/commits/sdk-3.0 Motivating discussion: https://github.com/dart-lang/linter/issues/4195 Fixes: #52516 Change-Id: I9ae8ff9765b6c977eab8a3c0f190d03a9c0f2710 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/305823 Reviewed-by: Samuel Rawlins <srawlins@google.com> Commit-Queue: Phil Quitslund <pquitslund@google.com>
🦅 Cherry pick landed (but not yet in a release). |
Consider:
In Dart both before 3.0 and in 3.0 with patterns, this case does not match integers, it matches only the type object
int
itself. Prior to 3.0, there was no way to do a type test in a switch, so users would rarely ever think to write the name of a type and expect it to type test.With patterns, we expect type tests to be common in switches. There are two idiomatic ways to write them:
(The first, an object pattern, is useful when you want to do further destructuring on the value after testing its type. It's likely the idiomatic style to use in algebraic datatype style code. The latter, a wildcard variable declaration, allows you to match any possible type annotation. Object patterns only support named types, but not more complex type annotations like records, functions, nullable types, etc.)
Because type tests are common in patterns, we expect that users will sometimes write a type literal when they meant to do a type test. This is valid code (though in some cases it will lead to non-exhaustive switch errors), but it probably doesn't do what the user wants.
We tried hard to tweak the syntax to avoid this footgun, but every other approach we came up with seemed to be worse overall. We also considered making it an error to have a constant in a pattern that resolves to a type literal. This would force users away from the footgun... but it's also a breaking change. There is code in the wild today that deliberately uses type literals in switch cases to test for type objects.
Given that, I think it would help the user experience if we added a lint that reports a type literal being used as a constant in a switch case (in a Dart 3.0 library). Instead, it should suggest that users use either an object pattern or variable pattern if they meant to do a type test, or an explicit
==
pattern if they really did mean to compare for equality to a type literal.The heuristic I'd suggest is:
If the matched value type is
Type
, then suggest they change the case fromcase SomeType
tocase == SomeType
. That has the same semantics, but makes it clearer to the reader that a type equality test is intended (and silences the lint).Otherwise, if the matched value type is anything else, suggest that they use
case SomeType _
if they indended to do a type test and== SomeType
otherwise.This lint will be useful whenever it arrives, but nothing is blocked in 3.0 around having it. It should just help users avoid what's likely a mistake.
The text was updated successfully, but these errors were encountered: