<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: James Nesta</title>
    <description>The latest articles on DEV Community by James Nesta (@james_nesta_5ae4292330e7e).</description>
    <link>https://dev.to/james_nesta_5ae4292330e7e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1851320%2Fff2d31ce-e098-4001-a9de-61b2804bcdc9.jpg</url>
      <title>DEV Community: James Nesta</title>
      <link>https://dev.to/james_nesta_5ae4292330e7e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/james_nesta_5ae4292330e7e"/>
    <language>en</language>
    <item>
      <title>How Should You Use an Auto-Formatter?</title>
      <dc:creator>James Nesta</dc:creator>
      <pubDate>Sun, 28 Jul 2024 12:41:39 +0000</pubDate>
      <link>https://dev.to/james_nesta_5ae4292330e7e/test-5ge1</link>
      <guid>https://dev.to/james_nesta_5ae4292330e7e/test-5ge1</guid>
      <description>&lt;h1&gt;
  
  
  Auto-Formatter Repository Integration: On Save, on Pre-commit, or in CI
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Many code projects use an auto-formatter, such as &lt;a href="https://prettier.io/" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt;. Auto-formatters are fantastic because they save developers an enormous amount of time and keep the codebase consistent. The rest of this article assumes that you want to use one.&lt;/p&gt;

&lt;p&gt;But at what point should auto-formatters be applied to the code? There are three stages to choose from, and you can choose one or more of them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On Save - The code editor (&lt;a href="https://en.wikipedia.org/wiki/Integrated_development_environment" rel="noopener noreferrer"&gt;IDE&lt;/a&gt;) auto-formats the code as soon as a file is saved.&lt;/li&gt;
&lt;li&gt;On Pre-Commit - A &lt;a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks" rel="noopener noreferrer"&gt;pre-commit hook&lt;/a&gt; ensures that all code is formatted before a commit.&lt;/li&gt;
&lt;li&gt;In CI (&lt;a href="https://en.wikipedia.org/wiki/Continuous_integration" rel="noopener noreferrer"&gt;Continuous Integration&lt;/a&gt;) - The CI pipeline checks formatting whenever a commit is pushed to the remote repository (using &lt;a href="https://docs.github.com/en/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt;, &lt;a href="https://docs.gitlab.com/runner/" rel="noopener noreferrer"&gt;GitLab Runner&lt;/a&gt;, etc.).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Different projects use different stages. But what combination is best?&lt;/p&gt;

&lt;p&gt;TLDR: format on save for immediate feedback, and check formatting in CI for enforcement. A pre-commit formatter adds little value between those two layers. The rest of this article will discuss the tradeoffs involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goals
&lt;/h2&gt;

&lt;p&gt;In this analysis, we want to optimize for &lt;a href="https://github.blog/enterprise-software/collaboration/developer-experience-what-is-it-and-why-should-you-care/" rel="noopener noreferrer"&gt;developer experience (DX)&lt;/a&gt;. In other words, we want our developers to be as productive as possible. At the same time, we still want to ensure that formatting violations do not slip through the cracks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time to Discrepancy Discovery
&lt;/h2&gt;

&lt;p&gt;One way to think about DX is the amount of time that passes before code is auto-formatted. Or, more generally, before discrepancies between the written code and the correctly formatted code are discovered.&lt;/p&gt;

&lt;p&gt;In general, we want to minimize that time because finding discrepancies as soon as possible allows us to fix them and iterate faster. Let's consider the three stages of auto-formatting checks with respect to time to discovery. We can plot them like so:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwkgpoo8weo6jz2b3nmk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwkgpoo8weo6jz2b3nmk.png" alt="Time to discrepancy discovery for on-save, pre-commit, and CI formatting checks" width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this framing, we see that "on save" is the best.&lt;/p&gt;

&lt;h2&gt;
  
  
  Formatting on Save vs Formatting on Type
&lt;/h2&gt;

&lt;p&gt;Auto-formatting on save has a short time to discovery because developers typically save a file once they are done modifying it. This means that the developer can immediately see what the auto-formatter did to their code, if anything. But there is a hypothetical option that is even faster: auto-formatting on type. In other words, you could imagine configuring the developer's IDE to summon the auto-formatter after every keystroke. (This would be a dot on the extreme left side of the chart above.)&lt;/p&gt;

&lt;p&gt;It may go without saying, but this option would not result in better DX. Since auto-formatters move code around, having your cursor jump all over the place as you type would be confusing and would probably reduce your productivity. Since an auto-formatter should preserve the code's &lt;a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree" rel="noopener noreferrer"&gt;AST&lt;/a&gt;, the developer gains nothing by seeing the formatting changes that early in the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual Formatting vs Auto-Formatting on Save
&lt;/h2&gt;

&lt;p&gt;Some developers do not like formatting on save and instead prefer to explicitly format the code with &lt;a href="https://stackoverflow.com/questions/29973357/how-do-you-format-code-in-visual-studio-code-vscode" rel="noopener noreferrer"&gt;a hotkey&lt;/a&gt;. In this workflow, they would have to press both the formatting hotkey and the save hotkey once they are finished modifying a file. But this workflow has a failure mode: what if they forget to format the file? Now, formatting errors can leak into the codebase. The beauty of auto-formatting on save is that it avoids this failure mode.&lt;/p&gt;

&lt;p&gt;We could also ask: what is the benefit of this extra, intermediate step? Since an auto-formatter should preserve the code's &lt;a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree" rel="noopener noreferrer"&gt;AST&lt;/a&gt;, there is no real reason to want unformatted code on disk. In unusual situations where you &lt;em&gt;really&lt;/em&gt; don't want to summon the auto-formatter, such as when troubleshooting whitespace, it is easy to use the editor's "Save Without Formatting" command (mapped to &lt;code&gt;Ctrl+K&lt;/code&gt;, &lt;code&gt;Ctrl+Shift+S&lt;/code&gt; by default in VS Code).&lt;/p&gt;

&lt;p&gt;For this reason, I think all developers should be encouraged to use auto-formatting on save. But if a developer &lt;em&gt;really&lt;/em&gt; does not like it, that's okay too, because repositories should also check formatting in CI to ensure that nothing slips through the cracks. (More on that later.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Formatting on Save vs Formatting on Pre-Commit
&lt;/h2&gt;

&lt;p&gt;Both auto-formatting on save and auto-formatting on pre-commit accomplish the goal of preventing unformatted code from reaching the remote repository. So which is better?&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool Visibility - Formatting on Save Wins
&lt;/h3&gt;

&lt;p&gt;Auto-formatting on save is nice because the developer gets to &lt;em&gt;immediately see what the auto-formatter did&lt;/em&gt;, if anything. With pre-commit formatting, however, the changes are performed less visibly.&lt;/p&gt;

&lt;p&gt;Realistically, since an auto-formatter should preserve the code's &lt;a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree" rel="noopener noreferrer"&gt;AST&lt;/a&gt;, this distinction does not matter much: the developer probably won't care about formatting-related changes in either context. But there is an argument for making tooling as transparent as possible: it helps us understand what the tooling is doing. This is especially helpful when it fails and you need to debug it!&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation Automation - It Depends
&lt;/h3&gt;

&lt;p&gt;Turning on auto-formatting on save in an editor can be somewhat automated. For example, if the team uses &lt;a href="https://code.visualstudio.com/" rel="noopener noreferrer"&gt;VS Code&lt;/a&gt;, then &lt;code&gt;.vscode/settings.json&lt;/code&gt; and &lt;code&gt;.vscode/extensions.json&lt;/code&gt; can be committed to the repository to provide good defaults and extension recommendations, respectively. Other editors, such as &lt;a href="https://www.jetbrains.com/webstorm/" rel="noopener noreferrer"&gt;WebStorm&lt;/a&gt;, offer similar functionality through shareable project-level files under &lt;code&gt;.idea/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The installation of pre-commit hooks can only be automated in certain cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When using the &lt;a href="https://pre-commit.com/" rel="noopener noreferrer"&gt;Python pre-commit framework&lt;/a&gt;, each developer must manually run &lt;code&gt;pre-commit install&lt;/code&gt; after cloning the repository.&lt;/li&gt;
&lt;li&gt;When using the &lt;a href="https://typicode.github.io/husky/" rel="noopener noreferrer"&gt;Husky pre-commit framework&lt;/a&gt;, installation can be somewhat automated through an &lt;a href="https://docs.npmjs.com/cli/v10/using-npm/scripts#life-cycle-scripts" rel="noopener noreferrer"&gt;npm &lt;code&gt;prepare&lt;/code&gt; script&lt;/a&gt;. In other words, the hooks are installed when the developer installs dependencies with a command such as &lt;code&gt;npm ci&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, with respect to automation, Husky is probably the clearest winner (contingent upon a JavaScript/TypeScript codebase in which developers reflexively install dependencies after cloning).&lt;/p&gt;

&lt;h3&gt;
  
  
  Editor Agnosticism - It Depends
&lt;/h3&gt;

&lt;p&gt;When considering DX, it is desirable for a codebase to be editor-agnostic. In other words, if a codebase is not strongly coupled to a specific editor, developers are free to use whichever IDE makes them most productive. So, if we care about editor agnosticism, pre-commit hooks are the clear winner.&lt;/p&gt;

&lt;p&gt;On the other hand, if everyone on the team uses the same editor, you aren't gaining much by being editor-agnostic. At the time of this writing (July 2024), &lt;a href="https://code.visualstudio.com/" rel="noopener noreferrer"&gt;VS Code&lt;/a&gt; is widely used across many language ecosystems. Some even consider it better than paid alternatives such as &lt;a href="https://www.jetbrains.com/webstorm/" rel="noopener noreferrer"&gt;WebStorm&lt;/a&gt;. Languages such as Java won't necessarily have one editor to rule them all, but if your language does, the benefit of editor agnosticism is obviously reduced.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration Maintenance - Tied
&lt;/h3&gt;

&lt;p&gt;Both editor-specific files and pre-commit files must live inside of the repository. Thus, the maintenance burden between them is a tie.&lt;/p&gt;

&lt;p&gt;The tie would be broken if the repository had to maintain files for two or more editors. This is uncommon, and I wouldn't recommend it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Git Commit Velocity - Tied
&lt;/h3&gt;

&lt;p&gt;It is common for skilled software developers to make lots of tiny commits. The basic idea is that smaller commits are easier for others on the team to understand and allow for more granular reverts when things go wrong. This practice is discussed in books such as Robert Martin's &lt;a href="https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882" rel="noopener noreferrer"&gt;&lt;em&gt;Clean Code&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Obviously, auto-formatting on save does not interfere with developers making commits. But pre-commit hooks do, and they are notorious for inhibiting productivity. Some developers have even said that they will &lt;a href="https://www.youtube.com/watch?v=LL01pLjcR5s" rel="noopener noreferrer"&gt;quit outright when pre-commit hooks are mandatory within an organization&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, while it is true that long-running pre-commit hooks inhibit productivity, this is not the case for hooks that run near-instantaneously. And code formatters fall into this category. For this reason, the negative stigma against pre-commit hooks is not relevant when considering auto-formatters. Thus, Git commit velocity is not really a concern for either option.&lt;/p&gt;

&lt;h3&gt;
  
  
  Complexity - Formatting on Save Wins
&lt;/h3&gt;

&lt;p&gt;Ideally, we should choose the solution that introduces the least complexity into the developer workflow. This should probably be the most important consideration!&lt;/p&gt;

&lt;p&gt;Overall, formatting on save is less complex than formatting in a pre-commit hook because most of the heavy lifting is done by the editor and its extension. Take &lt;a href="https://prettier.io/" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt;, the most common formatter for JavaScript and TypeScript. Since it requires &lt;a href="https://nodejs.org/en" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt;, we want to run it in a context that abstracts away the installation of the runtime.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the case of an IDE, the extension can bundle the formatter and its runtime dependencies. Thus, the &lt;a href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode" rel="noopener noreferrer"&gt;Prettier extension&lt;/a&gt; can work without a separate Node.js installation.&lt;/li&gt;
&lt;li&gt;With pre-commit hooks, the runtime must either be installed by every user, as with &lt;a href="https://typicode.github.io/husky/" rel="noopener noreferrer"&gt;Husky&lt;/a&gt;, or managed in an isolated environment by a framework such as &lt;a href="https://pre-commit.com/" rel="noopener noreferrer"&gt;&lt;code&gt;pre-commit&lt;/code&gt;&lt;/a&gt;. That additional runtime-management layer can complicate troubleshooting, including OpenSSL compatibility issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, formatting on save is much less likely to go wrong. And when things do go wrong, it is much easier to debug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Formatting on Save and Pre-Commit Together
&lt;/h2&gt;

&lt;p&gt;Even if you decide that formatting on save is slightly better than formatting on pre-commit, you might decide to use both solutions at the same time. But is that a good idea?&lt;/p&gt;

&lt;h3&gt;
  
  
  For - Defense in Depth
&lt;/h3&gt;

&lt;p&gt;The argument to use both is a &lt;a href="https://en.wikipedia.org/wiki/Defence_in_depth" rel="noopener noreferrer"&gt;defense in depth&lt;/a&gt; strategy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If we use only formatting on save, a developer's editor can become misconfigured and allow unformatted code to slip into the codebase.&lt;/li&gt;
&lt;li&gt;If we use only formatting on pre-commit, a developer's pre-commit hook can become misconfigured and allow unformatted code to slip into the codebase.&lt;/li&gt;
&lt;li&gt;If we use both, then each step guards against failures at the other step.&lt;/li&gt;
&lt;li&gt;It is still technically possible that the developer's editor and pre-commit hook can both be misconfigured, but this is much less likely than just one component failing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Against - We Have to Use CI Anyway
&lt;/h3&gt;

&lt;p&gt;Consider the case where the developer's editor and pre-commit hook are both misconfigured. Because this case exists, if we want 100% coverage, we have no choice but to also check for formatting in CI.&lt;/p&gt;

&lt;p&gt;Thus, lowering the probability of pushing unformatted code to the remote provides little additional protection because CI will catch the problem in either case.&lt;/p&gt;

&lt;p&gt;(To be clear, this is not an argument for &lt;em&gt;only&lt;/em&gt; having CI checks, because that is terrible DX. Rather, the argument is that formatting on pre-commit doesn't give us any additional DX wins on top of formatting on save.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Against - More Complexity Is Bad
&lt;/h3&gt;

&lt;p&gt;As engineers, we should strive to minimize complexity in our codebases and toolchains. It is pretty hard to justify the added complexity of a pre-commit hook when the payoff largely duplicates existing functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Downsides of CI Checks
&lt;/h2&gt;

&lt;p&gt;In the earlier visualization, we called CI the "worst" option because it is the furthest from the developer. Let's color this in a little bit to illustrate exactly how the bad DX comes about.&lt;/p&gt;

&lt;p&gt;CI runs can take a long time. Consider this common scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alice finishes work on her feature branch. She pushes it to the remote repository and knows from experience that CI will take about 30 minutes to complete.&lt;/li&gt;
&lt;li&gt;To stay productive, Alice immediately switches to a new branch to work on another feature.&lt;/li&gt;
&lt;li&gt;Thirty minutes pass, and Alice is now thinking deeply about a problem related to the new feature.&lt;/li&gt;
&lt;li&gt;Alice gets an email notification that CI has failed.&lt;/li&gt;
&lt;li&gt;Alice now has to perform a &lt;a href="https://ics.uci.edu/~gmark/CHI2005.pdf" rel="noopener noreferrer"&gt;very costly context switch&lt;/a&gt;, leaving her current problem and returning to work she set aside 30 minutes earlier.&lt;/li&gt;
&lt;li&gt;Alice finds and fixes the error. She pushes to remote.&lt;/li&gt;
&lt;li&gt;Alice now performs another costly context switch back to her new branch.&lt;/li&gt;
&lt;li&gt;Another 30 minutes pass, and Alice gets another email notification that CI has failed.&lt;/li&gt;
&lt;li&gt;The cycle repeats.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To avoid these kinds of time drains, we want to push checks closer to the developer whenever possible. Thankfully, auto-formatting allows us to do that with few other downsides.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI Checks Are Still Mandatory
&lt;/h2&gt;

&lt;p&gt;Using CI is not free: there is a modest maintenance cost, additional complexity, and the potential cost of hosting the CI platform and runners. But if we want to ensure that no formatting violations slip through the cracks, we don't have a choice: we &lt;em&gt;must&lt;/em&gt; check formatting in CI, regardless of which other steps we use.&lt;/p&gt;

&lt;p&gt;But the point is that we can use CI as a last-resort check instead of the primary check. By doing this, we get the best of both worlds: good DX and validation that is difficult to bypass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Given the previous discussion, I think the following is true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We have to check formatting in CI to ensure proper coverage because the earlier checks can be bypassed.&lt;/li&gt;
&lt;li&gt;We have to check formatting before pushing to the remote repository to ensure good DX.&lt;/li&gt;
&lt;li&gt;Auto-formatting on save is arguably superior to auto-formatting on pre-commit, mostly for reasons of visibility and complexity.&lt;/li&gt;
&lt;li&gt;Using both auto-formatting on save and auto-formatting on pre-commit offers little additional benefit and greatly increases the complexity of the tooling.&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
  </channel>
</rss>
