-
-
Notifications
You must be signed in to change notification settings - Fork 230
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
Offer a migration tool to convert map/when to switch-cases #940
Comments
This could also be added to |
If we were to offer such migration in We could do a dumb conversion, ignoring all those issues. And the user would fix those by hand. |
Actually there is
Just use the name in the map expression, or am I missing something?
This should be doable. Just a matter of determining whether any of the body of the lambdas are not an expression body, which you would need a switch statement for, otherwise just default to a switch expression. The more difficult part is if it is used in an expression context. In which case you need a switch expression. In this case, although it is less readable you can use an immediately invoked function application: ((){..body})().
This is definitely a problem that needs a warning. However, the fix can potentially be automated.
The default case for maybeWhen should obviously be a catch all. For the exhaustive when / map, a throw statement should be good, though I would also output a list of all the places this was added due to not having a sealed class - so the user knows which classes they need to make sealed, or have an option for the migration tool to do a dry run, and only output the warnings for the sealed classes, so they can be fixed prior to actually migrating. Some classes might potentially be outside the users control, so it still should be able to migrate without sealed classes.
Wrapping in Future(()) should be fine for return values that are not awaited.
Your proposed solution looks fine. Other NotesAs far as the way to offer the migration to the users, I just want to point out that when we used https://pub.dev/packages/codemod for riverpod 1.0 or 0.14.0?, we ran into lots of issues with overlapping edits due to nesting patterns. This causes the whole migration to fail unfortunately. Either
Or you just do it via a lint / IDE refactoring which is probably most reliable for users. (This would also make it easy for users to alter the code and make small updates while they are reviewing it). However, due to the pervasive nature of Although there is some pushback on deprecating it (which I was initially opposed to as well), I do think that there is a large benefit to switching code to use dart's pattern matching -> especially when considering nested matches, more flexible orElse logic for dealing with union classes that have a common parent interface etc. That's before even mentioning the smaller generated code size. I think the option of generating the methods (not by default) should stick around at least till a macro version of freezed. The biggest thing that I would say about this is to make it very clear on the README that this has happened. Not everyone follows you on Twitter / GitHub, also not everyone looks at CHANGELOGs when running pub upgrade (even though they should). |
Great!
You're right. I was thinking that this could shadow existing variables. But when thinking about it that's what In fact the opposite case could be made. That the refactor could stop shadowing variables if we don't do this. So if someone does: int value;
obj.whem(
data: (value) {
print(value++);
}
) Then doing: int value;
switch (obj) {
case ExampleData(data: final value):
print(value++);
} would not be equivalent. As the ++ is now changing a different variable. About migrating non-sealed classes, I was thinking that: An example is Riverpod's AsyncValue with its loading state; It's not sealed quite yet, but folks can do: switch (AsyncValue()) {
AsyncData() =>...,
AsyncError() => ...,
_ => print('loading'),
} |
Your mention of codemod reminds me that the linter could technically work as a migration in itself. There's an issue in custom_lint to offer a If this is implemented, then implementing a fix in the IDE would also work as a widespread migration tool too. So maybe we should focus on the linter. |
one thing to consider about private subtypes. It can actually be quite common case since it allows to avoid name conflicts as well as simplifies class names. Forcing devs to switch completely to pattern matching can make transition quite painful |
The migration probably should look for name conflicts. And if so, use import aliasing to fix it. |
We could have:
And for the given Freezed class:
This would convert:
into:
Things to figure out:
There may be no local variable
Users may write:
In that scenario, there's no local "obj" variable equivalent when using a switch:
We could create a local variable, but that could get quite complex, as we need to figure out where to create that local variable exactly.
For instance, this may involve changing:
into:
Another thing to consider with this approach is: What should the variable name be.
We could name it based on the interface class name. So here
Example
->example
Sometimes we need a switch "statement", and sometimes a switch "expression"
When converting:
We would have to output:
Notice how:
case
don't end with a;
, but those with=>
docase
use a;
at the end of every expression, but switches with=>
use,
Sometimes the factory type is not public
In the example, both
ExampleError
/ExampleData
are public. But they could be private, and we could be in a file that does not have access to the private type.In that scenario, we cannot generate the when/map equivalent.
We could have a warning asking folks to make those objects public.
The freezed class may not be sealed
If the annotated
Example
class is not "sealed", then switches cannot be exhaustive. In this case, we'll need to consider all whens/maps as "maybeWhen/maybeMap". But we don't know how the current code should handle the default case.We could generate:
Closures may use function modifiers
Currently, users may write:
We cannot simply generate:
Because the
await
keyword wouldn't work here.Even if
fn
was async, the behavior after the refactor would be different. An unawaited function would now be awaited.We could do:
Future(() async {...})
For sync*/async* functions, we could stick to considering them unsupported for now.
Callbacks may be function tear-offs
Users could write:
In that case, the migration would likely be to invoke the functions by hand:
The text was updated successfully, but these errors were encountered: