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

Switch expression always needs assignment #60019

Closed
FMorschel opened this issue Jan 30, 2025 · 6 comments
Closed

Switch expression always needs assignment #60019

FMorschel opened this issue Jan 30, 2025 · 6 comments
Labels
area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion.

Comments

@FMorschel
Copy link
Contributor

FMorschel commented Jan 30, 2025

I'm unsure if this is intended. If you have the following code:

enum Direction{
  none,
  left,
  right;
}

void foo(Direction direction) {
  switch (direction) {
      Direction.left => _doThis,
      Direction.right => _doThat, 
      Direction.none => _doNothing,
    }();
}

I was expecting the (); at the end of the switch to work as a starting return or assignment would. But as of right now, this is the output:

The type 'Direction' is not exhaustively matched by the switch cases since it doesn't match 'Direction.none'.
Try adding a default case or cases that match 'Direction.none'.
Expected to find 'case'.
Dead code.
Try removing the code, or fixing the code before it so that it can be reached.

Image

So I can't even use the quick-fix to create any of the functions because it expects a case keyword before the first Direction. Is this wrong or is specified somewhere? If this is expected, could we change this to allow this case?

@FMorschel FMorschel added the area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. label Jan 30, 2025
@FMorschel
Copy link
Contributor Author

Also, something weird (not actual code, just trying around):

final _ = switch (direction) {
  Direction.none => animationController.animateTo,
  Direction.left => (
      v, {
      Duration? duration,
      Curve curve = Curves.linear,
    }) =>
        animationController.forward(from: v),
  Direction.right => (v, {
      Duration? duration,
      Curve curve = Curves.linear,
    }) => animationController.reverse(from: v),
}(0);

This has a warning when passing v to from:: The argument type 'dynamic' can't be assigned to the parameter type 'double?'.

But when you hover at the closing }( in the end, this is the expression type shown: Type: TickerFuture Function(double, {Curve curve, Duration? duration}).

And at both v declarations we have The type of v can't be inferred; a type must be explicitly provided. Try specifying the type of the parameter..

So the hover must be wrong or the diagnostics should not be there.

@bwilkerson
Copy link
Member

The problem with the first example is that a switch expression is not allowed to appear at the beginning of an expression statement. It will always be interpreted to be a switch statement, hence the requirement for a case.

You can get around the restriction by enclosing the switch expression in parentheses.

I'm not entirely certain what the problem is with the second example. I can't reproduce what you're seeing, so it might have something to do with the way the undefined names are defined.

@FMorschel
Copy link
Contributor Author

I've copied the signatures and made a test on pure Dart (no Flutter), so now you can reproduce it. Here is a full example:

enum Direction { none, left, right }

void foo(Direction direction) async {
  await (switch (direction) {
    Direction.none => _doNothing,
    Direction.left => (v) => _doThis(from: v),
    Direction.right => (v) => _doThat(from: v),
  })(0);
}

Future<void> _doThis({ double? from }) async {}

Future<void> _doThat({ double? from }) async {}

Future<void> _doNothing(
  double target, {
  Duration? duration,
  int curve = 0,
}) async {}

Even if you add , {duration, curve = 0} after both v as parameters to match _doNothing signature, it doesn't seem to understand that v is the same as double target.

While only one (_doNothing) or two (say you added the above only after Duration.left) of the above have the full signature, at the expression you get:

Image

Now when all of the above have the full signature this is the expression type:

Image

But for some weird reason, inside the expression, on the declarations of v, it doesn't know the type of the positional parameter to be a double.

@eernstg
Copy link
Member

eernstg commented Jan 31, 2025

Here is an easy check: Change from: v to from: v.whatever, and see that there are no compile-time errors. This implies that the type of v is indeed dynamic when it's passed to _doThis and _doThat.

I don't see any warnings or error messages, not even when this example is analyzed on the command line with strict-raw-types, strict-inference, and strict-casts, so I can't directly reproduce the reported behavior.

Anyway, the fact that v gets the inferred type dynamic is working as intended: The inference of parameter and return types of a function literal is specified here, and it is all specified in the first 3 lines of that section. It says that the type of a parameter is as specified on the parameter itself, using dynamic if the parameter does not have a type annotation.

This is the rule that applies when the function literal does not have a context type (that is, the context type is _).

In this case, we have an expression of the form s(0), and s does not get any context type in this situation, so the switch expression which is inside s gets no context type, so each of the branches of the switch expression gets no context type.

However, the resulting type of s turns out to be Future<void> Function(double, {Duration? duration, int curve})>>. The reason why we get the "correct" type at this point is that this is the type of _doNothing, and this type is a supertype of Future<void> Function(dynamic, {Duration? duration, int curve})>>, and the type of a switch expression is the standard upper bound of the types of its branches.

So it's all working as intended. Moreover, I don't see an easy way to adjust type inference (in particular, function literal return type inference) such that the type of v is inferred as double.

@lrhn
Copy link
Member

lrhn commented Jan 31, 2025

I think not being able to infer the type of v here is expected.
It indeed doesn't understand that v should be the same as double target, they're indpendent values with neither restraining the other in any way.

Simpler example:

void main() {
  int id(int x) => x;
  var f = (v) => id(v);
  print([f].runtimeType); // List<dynamic => int>
}

Parameter inference for functions does not try to find hints in the function body. It either gets a type from the context (this doesn't have a useful context type), or it uses dynamic.

Having an argument is not introducing a context. Even simpler example;

void main() {
  ((v) { print([v].runtimeType); }(0)); // List<dynamic>
}

Without a context type, function expression parameters need to be typed.

The duration and curve are probably also dynamic. They're then combined in an upper-bound computation with _doNothing and the upper bound of Future<void> Function(double, {Duration? duration, int curve}) and Future<void> Function(dynamic, {dynamic duration, dynamic curve}) is Future<void> Function(double, {Duration? duration, int curve}).
That's the type of the switch because it's constrained from above by the _doNothing type, not because the individual values all have that type.

To give a context, you can do:

void foo(Direction direction) async {
  Future<void> Function(double) todo = switch (direction) {
    Direction.none => _doNothing,
    Direction.left => (v) => _doThis(from: v),
    Direction.right => (v) => _doThat(from: v),
  };
  await todo(0);
}

Or just write (double v) when that's the type you need.

@FMorschel
Copy link
Contributor Author

Oh, I see. I figured it would type the same as the whole expression. Thank you both for the detailed explanations! I didn't realize that the Function literal had nothing to bind the expected type to, so I figured it would infer from the other switch cases, but this makes more sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion.
Projects
None yet
Development

No branches or pull requests

4 participants