Skip to content
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

unnecessary_cast quick-fix unexpected result in switch expression #56072

Open
FMorschel opened this issue May 27, 2024 · 7 comments
Open

unnecessary_cast quick-fix unexpected result in switch expression #56072

FMorschel opened this issue May 27, 2024 · 7 comments
Labels
analyzer-quick-fix area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. P3 A lower priority bug or feature request type-enhancement A request for a change that isn't a bug

Comments

@FMorschel
Copy link
Contributor

Describe the issue
When using the quick-fix for unnecessary_cast in a switch expression the removed type cast is the outer one but should be all the inner ones.

To Reproduce

T a<T>(int x, bool test) {
  return switch (x) {
    0 => 0 as T,
    1 => 1 as T,
    _ => throw UnimplementedError()
  } as T;
}

Expected behaviour

T a<T>(int x, bool test) {
  return switch (x) {
    0 => 0,    // Removed cast here
    1 => 1,    // Removed cast here
    _ => throw UnimplementedError()
  } as T;
}

Additional context
This should be taken into account because it is way more verbose to write the cast in every line than at the end.

@FMorschel
Copy link
Contributor Author

Alternatively, the assist to cast a switch statement to a switch expression could move the casts. If the reviewer agrees, I can create a new issue if necessary.

@srawlins
Copy link
Member

This one's pretty tricky. How things are implemented now, the inner two casts are not unnecessary because 0's static type is different from T and 1's static type is different from T. So those casts do something, and they can fail at runtime. Then when we've gotten to the type of the switch expression, it is computed to be T, so that outside cast is unnecessary.

But I think broadly your issue raises a valid UX improvement. A list literal is a similar example:

List<int> f(Object a, Object b) => [a as int, b as int] as List<int>;

The outer most cast is considered unnecessary. So we offer a fix to remove it. But we could offer an additional fix to tidy things up internally as well. A fix to "make the cast necessary" by removing inner casts.

Actually, casting a List with a new type argument is dangerous. Maybe better examples would be:

int g(Object a, Object b) => (1 == 2 ? a as int : b as int) as int;

int h(Object? a, Object b) => (a as int? ?? b as int) as int;

@srawlins srawlins added the type-enhancement A request for a change that isn't a bug label May 31, 2024
@bwilkerson bwilkerson removed the type-enhancement A request for a change that isn't a bug label Jun 24, 2024
@bwilkerson bwilkerson transferred this issue from dart-lang/linter Jun 24, 2024
@dart-github-bot
Copy link
Collaborator

Summary: The unnecessary_cast quick-fix in switch expressions removes only the outer cast, while it should remove all inner casts, leading to more verbose code. The expected behavior is to remove all unnecessary casts within the switch expression.

@dart-github-bot dart-github-bot added area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. triage-automation See https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot. type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) labels Jun 24, 2024
@bwilkerson bwilkerson added P3 A lower priority bug or feature request analyzer-quick-fix type-enhancement A request for a change that isn't a bug and removed type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) triage-automation See https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot. labels Jun 24, 2024
@FMorschel
Copy link
Contributor Author

@srawlins
Copy link
Member

WDYT, @bwilkerson @scheglov ? This one makes me a little squirmy, because it feels unpredictable. If we stick to immediate inner casts, that's a little better for me. But it kind of introduces an inconsistency. Like for conditional expressions, take:

int f() {
  return (1 == 2 ? a as int : 2 == 3 ? b as int : c as int) as int;
}

I imagine we would only offer to un-cast the a as int cast. If we would instead want to remove inner casts that are arbitrarily deep... ooh. Seems very bug-prone, hard-to-define, arbitrary (would we have a set of expression types that we go into, and others we don't?).

An example of the "arbitrary" issue. I'm not sure what immediate expressions we should look into, even not considering going deep (but then the same question applies to going deep, if we want to go deep). So here's another perfectly valid example, like the top one in the issue here, and I think I've seen something like this in real life:

int f<T>(List<T> list) {
  return list.fold(
    list.first as int,
    (value, e) => value + (e as int),
  ) as int;
}

Again the outermost as int is only unnecessary because the other as int casts are present. But one an argument in an argument list, and the other is in a binary expression (+), in an expression function (=>) in an argument.

We don't have to come up with a perfect list of cases before we land a quick fix; it's not a "breaking change" to offer quick fixes in more cases. But in order for the quick fix to carry its weight, I think we need a few cases (definitely more than 2; we've got examples of like 6 so far), and we probably need examples that don't feel super contrived. I think I've seen this list.fold example in real code, but I certainly cannot figure out why I've had code that needed (dangerous) casts like this. Tricky.

@FMorschel
Copy link
Contributor Author

I imagine we would only offer to un-cast the a as int cast.

I've tested it with the current CL and this is the current behaviour. Both b and c still keep their casts. If the expression was something like:

(1 == 2 ? a as int : (2 == 3 ? b as int : c as int) as int) as int;

With another condition that was casted yet again, the bulk fix (we could discuss removing it as well, I don't actually expect lots of code like this) would make it into:

(1 == 2 ? a : (2 == 3 ? b : c)) as int;

About the list, I'm not sure how we could do something that would handle it but it's probably doable. Althoug I'd lean on teaching the user that something like this is possible (not sure how to do that here but it would be way better):

int f<T>(List<T> list) {
  return switch (list) {
    List<int> list => list.fold(
        list.first,
        (value, e) => value + e,
      ),
    _ => throw ArgumentError('Invalid list type'),
  };
}

@bwilkerson
Copy link
Member

For quite a while now I've been leaning toward having smaller more composable pieces of functionality. There is a drawback, in that users often have to take more steps to accomplish their bigger goals, but the composability aspect means that it's often possible to support more user needs with fewer resources.

If I were to apply that perspective to this problem I would say that the quick fix for an unnecessary_cast should only remove the unnecessary cast, but that we should instead have an assist that will take a language structure (a switch expression, conditional expression, list literal, and maybe others) and hoist the type promotions to a higher level.

In the original example, after the outer as is removed, notice that the right-hand side of every case is either cast to T or has type Never, which would allow the cast to be moved outside the switch expression. The same operation would apply to the conditional expression examples.

But I would say that it should work on one level at a time. In the case of the nested conditional expressions it should work only on the innermost conditional expression (that is, it shouldn't try to figure out that it's possible to convert the outermost and innermost simultaneously). If it's applied to the innermost expression then it would become available for the outermost.

This approach would be simple to understand, predictable in the way it works, and allow the greatest amount of flexibility and control for users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
analyzer-quick-fix area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. P3 A lower priority bug or feature request type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

4 participants