[ANN] JuliaFormatter v2 updates

TL;DR

If you’re using Default or Blue styles with JuliaFormatter.jl, v2.10 is (probably) production ready. It’s extensively tested and produces idempotent formatting as long as you disable certain options which are known to be problematic.


Background

Hello! You may have seen a couple of recent posts about JuliaFormatter (1, 2). If you haven’t been following this:

  • JuliaFormatter v2 was released in Oct 2024, with a large-scale migration from CSTParser.jl to JuliaSyntax.jl.

  • v2 has unfortunately been quite buggy, with a number of people sticking to v1, and others switching to Runic.jl. There are many reasons for this (please feel free to ask if you’re interested!) but fundamentally it’s because formatting is really difficult.

  • Since early this year JuliaFormatter hasn’t been actively maintained, with Dominique (the creator and previous maintainer) citing burnout.

In May we agreed to move JuliaFormatter to an organisation. I’ve been working on trying to bring v2 up to a level where it can be depended upon. (Yes, Claude has helped!)

What’s happened?

It would be impossible to enumerate everything that’s changed, but here are the highlights:

Bug fixes

Loads of bugs have been fixed. Particularly nasty ones included the meaning of array literals being silently changed (1, 2) and syntax transformations firing when they shouldn’t (1, 2, 3).

Testing

JuliaFormatter’s tests previously consisted of its unit tests plus some vendored codebases. Given that the v2 move to JuliaSyntax managed to pass all these CI tests, but broke a ton of code elsewhere, it seems reasonable to say that this wasn’t sufficient.

I’ve added tests which check that formatting succeeds and is idempotent across approximately 5 million lines of Julia code. These have caught dozens more bugs that hadn’t previously been reported, and going forward will help to minimise the chances of introducing new bugs.

LSP compatibility

We’ve upgraded to use JuliaSyntax v1, which makes JuliaFormatter v2 compatible with LanguageServer.jl and thus the Julia VSCode extension.

JETLS.jl also supports JuliaFormatter, and in recent versions line-range formatting was introduced which allows you to format only specific parts of an expression.

Syntax transformations are more conservative

JuliaFormatter has a number of options which change the AST of your code. always_use_return, which adds return to function bodies, is a simple example. In v2.10 these are always disabled by default inside macros and Exprs because they can change the meaning of code.

This can be overly conservative because, for example, many macros are “just” things like @testset and @inline which don’t pattern-match on the AST. However, JuliaFormatter has no way of knowing this and so I choose to err on the safe side. You can re-enable them inside macros if you want, by enabling the transform_syntax_in_macros option.

Note even with this turned on, some transformations are still disabled inside macros (see the docs for details). Furthermore, transformations are always disabled inside Exprs and there is no way to re-enable this.

Idempotence

Ideally a formatter should be idempotent, i.e., formatting text that has been already been formatted should not cause any changes. JuliaFormatter succeeded at this in many cases, but I think it’s safe to say that it hasn’t really been a priority until now.

JuliaFormatter performs formatting in several passes, or stages. The main way where idempotence can be lost is if a decision made at a later pass affected one made in an earlier pass (meaning that the second time formatting was performed, it would go down a different path). See e.g. this example. The corresponding solution is to either decouple these decisions, or to pre-calculate the later one so that we had enough information at the earlier stage.

Unfortunately, some options are fundamentally incompatible with this. These are described in the docs. Specifically, for Default style, I recommend adding the following to .JuliaFormatter.toml:

v2_stable_multiline_strings = true

and for Blue style:

v2_stable_multiline_strings = true
conditional_to_if = false
pipe_to_function_call = false

The other options that are enabled in Default and Blue styles are unlikely to cause any problems (as evidenced by testing).

What about SciML and YAS styles?

SciML and YAS styles come with default options which can cause lack of idempotence, and I haven’t yet investigated these thoroughly. They also aren’t tested thoroughly in CI (there are unit tests, but they aren’t tested against external corpora like Default and Blue styles). They also contain more custom code than Default and Blue styles.

I’m therefore quite wary that there may be more correctness bugs lurking with these styles which just haven’t been discovered yet. This is the main reason why I can’t yet promise that SciML and YAS styles are reliable.

Thanks

Finally I’d just (again) like to say thanks to everyone who opened issues or PRs! I really appreciate it.

I would also be very pleased if you, dear reader, would like to take a gamble on Default or Blue styles now and tell me if anything is messed up!

Please do be aware that formatting will be different from that in previous versions. There’s genuinely no way that I can promise that formatting won’t change. But, hopefully, it will be to your liking.

Thank you so much both for the super detailed update and for taking over the maintenance and fixing so many things!

While I personally did move to Runic – I was convinced at the last JuliaCon that rules set in stone are a good idea, and it was not due to bugs, I think it is super important that such a heavily used package like JuliaFormatter is taken care of. I personally would not have the strength to fight through so many bugs as I saw open issues on that package. So really really thanks for taking over and all the work you do!

Thank you! :slight_smile:

I was convinced at the last JuliaCon that rules set in stone are a good idea

Yes, in fact, personally I agree. I think the proliferation of options in JuliaFormatter has been good in some ways but if I were to redo it from scratch, I would probably not have made it as extensive. For one, formatters are meant to take away formatting decisions from programmers, and introducing configuration options just adds those decisions back. But pragmatically, having loads of options is also a nightmare to test, and it’s a big reason why it’s impossible to make any meaningful guarantees of stability.

That said, I don’t expect that any configuration options will be removed from JuliaFormatter v2 because the configurability is the (biggest) factor that sets it apart from Runic. It might happen if I ever release a v3, but that’s a big if, and even so I’d rather make a new package than do a v3.

I should say that Runic is very well-written (I’ve read the codebase, not super in detail but enough to appreciate it). The only real reason why one might not want to use it is if you don’t like its stylistic choices.

Thank you for your amazing work!

Runic is fine but the lack of capability to format line breaks is a true deal break for me. JuliaFormatter is amazing because I can replicate the design choice I have been using since forever.

I think it is really just a matter of taste. I like both the non-config and what Runic produces.
But that is a very very personal opinion.

I totally see a lot of reasons to use JuliaFormatter: If one wants to have consistency, for example to a wider code base in some sense or has a personal preference that needs to be configurable – JuliaFormatter it is!