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

Add option to formatter to rewrite unless #13769

Closed
wants to merge 8 commits into from

Conversation

sabiwara
Copy link
Contributor

@sabiwara sabiwara commented Aug 9, 2024

This is still a draft in order to explore the possibility, not ready yet.

I haven't started to work on the --migrate flag in mix format yet, this is focusing on the unless rewrite itself.

Commits are organized to help the review:

  • 4dd0d08 is the rewrite itself (esp. see the tests)
  • e970466 shows what it did when run on the codebase
  • 2c89d85 is the only breaking change that needed to be fixed (defmacro unless)
  • the rest is manual tweaks I did to improve over the auto-generated ones

lib/mix/lib/mix/utils.ex Outdated Show resolved Hide resolved
@josevalim
Copy link
Member

Thank you @sabiwara. ❤️ I am generally ok with this change. I think the automatically rewriting will definitely leave some gaps for improvements, but I really can't see any loss of readability in any of these changes (before or after adjustments).

I'd love to hear folks thoughs on this one.

PS: Also a separate discussion, but we should probably pick a default name for all "rewrite" options. The two that exist today are called normalize_. But that's another PR.

@sabiwara sabiwara marked this pull request as ready for review August 24, 2024 04:33
@sabiwara
Copy link
Contributor Author

Rebased and opening for reviews. Opinions welcome.

If we agree on this one, the next step would be to add the --migrate flag to mix format which would include rewrite options, and soft deprecate unless.
I'd also like to revisit #11845 since it would be a good candidate for the migrate flag.

I'm still on the fence if we should include the existing normalize_ options in the --migrate flag and rename them, since this would be a small breaking change - but at least we're not validating format opts so it might be OK.

@josevalim
Copy link
Member

I think changing normalize is fine. If you are setting it today, is to set it to false, which will be the new behavior anyway. Plus if you ran it once, you don’t have to run it again, which makes it a perfect candidate for migrate.

@sabiwara
Copy link
Contributor Author

Sounds good.
Naming-wise, I think it would be good to have all the --migrate options prefixed by migrate_, e.g. migrate_unless.
And we could even have a meta-option migrate defaulting to false, which would itself be the default for all migrate_ options. WDYT?

If we are OK with this current PR, perhaps I can merge it and work on the flags in a follow-up PR?

@novaugust
Copy link
Contributor

novaugust commented Aug 29, 2024

I'd love to hear folks thoughs on this one.

I'm all for it! unless can be useful for natural language reading for native speakers, but (imo) is a somewhat surprising programming construct for folks who haven't seen it in other languages. lots of style guides recommend avoiding it in various cases.

the only thing i'll really miss is the succinctness of using it to raise exceptions (unless all_is_well?, do: raise "boom"). but, fewer equivalent choices for developers + standardization across teams/codebases is a big win.

styler rewrites some forms to unless, but changes lots of unless to if instead. happy to just if everything

that said, here's a very convoluted code snippet (people have done stranger) that i'd be curious to see a rewrite for:

unless x = false, do: !x

neeeeevermind, i saw it gets negated (didn't know you could wrap assignments in !, TIL)

iex(1)> unless x = false, do: !x
true
iex(2)> if  !(x = false), do: !x
true

@@ -1794,7 +1794,7 @@ defmodule FileTest do
stat = File.stat!(fixture)
assert stat.mode == 0o100666

unless windows?() do
if not windows?() do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are manual ! to not tweaks right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct (you can see individual commits to see what is automatic or tweaked)

Comment on lines +2479 to +2483
:>,
:>=,
:<,
:<=,
:in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and and or could be added here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish we could, but unfortunately this wouldn't be safe and might break code that is working today, since false or 42 is valid (also arguably pretty bad) but not (false or 42) would raise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahhh right, i always think that those are bool, bool -> bool, forgot it's bool, any -> any
i'll just join you in wishing we could then

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could try harder in these cases, checking if the right handside itself is a guard/comparison... but I'm not sure it's worth the extra complexity and it won't capture cases like and enabled? anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternatively, only use not with in, and for the 4 comparisons rewrite them to their opposite like with the equality operators:

{:>, meta, [left, right]} -> {:<=, meta, [left, right]}
{:>=, meta, [left, right]} -> {:<, meta, [left, right]}
...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hesitated between the two approaches, not sure which would be better. Maybe yours is more natural indeed.

Copy link
Contributor Author

@sabiwara sabiwara Sep 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually not might be better in this case, if we consider that the user explicitly wrote:

unless x > @limit do

over

if x <= @limit do

which they could have done in the first place, then

if not x > @limit do

might read better to them for some reason?

It is not critical though, people might see the diff and fix it to their preference eventually.
So I'm fine with either approach.

Copy link
Contributor

@novaugust novaugust Sep 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i think you're right. if not is a simpler change to reason over than if $swapped_equality_operator. thanks for spelling that scenario out.

@sabiwara
Copy link
Contributor Author

neeeeevermind, i saw it gets negated (didn't know you could wrap assignments in !, TIL)

Yes it is quite unreadable and people might want to do it differently, but it feels quite a rare case since assignment is mostly useful for if rather than unless.

@sabiwara
Copy link
Contributor Author

sabiwara commented Sep 6, 2024

Closing this one for now, merged #13808 and will reopen when I have something more ready with the full implementation.

@sabiwara sabiwara closed this Sep 6, 2024
@sabiwara sabiwara deleted the format-migrate branch September 6, 2024 12:49
@josevalim
Copy link
Member

josevalim commented Sep 6, 2024

@sabiwara we also don't need to rewrite all of them. If we can rewrite most, that will already help many codebases migrate.

@novaugust
Copy link
Contributor

May I ask for the context I'm missing on the motivations for this work? is unless on the way out for elixir 2.0?

@sabiwara
Copy link
Contributor Author

sabiwara commented Sep 6, 2024

@novaugust yes the idea is to eventually deprecate unless, but to offer the tools to make the transition more smooth.

@novaugust
Copy link
Contributor

cool -- if that's for sure happening i'll steal what you've started here and have styler drop unless as well then =)

Comment on lines +2505 to +2506
{op, _, [_, _]} when op in @bool_operators -> {:not, [], [condition]}
{guard, _, [_ | _]} when guard in @guards -> {:not, [], [condition]}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a thought i had while implementing this for styler -- being smart and using not here might be dangerous, because it's possible the user did something whacky like

import Kernel, except: [is_atom: 1]

def is_atom(...), do: :ok

better to just always use ! and leave it to the user to translate to not as they want?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind migrate option is that they rewrite the AST and cannot guarantee to work in edge cases where metaprogramming assumes a given AST or Kernel overwrites are done (after all people can have re-defined unless itself, or ==, etc...).

This particular one feels like an unlikely case, the need to overwriting is_atom feels low, the convention that only guards returning booleans should start with is_ should also discourage it... I think we're fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants