The Blurry Line Between Formatting and Style
June 19, 2023
Iāve been doing a lot of advocacy work on how to properly use formatters such as Prettier alongside linters such as ESLint.
- Formatters should be used for formatting (changes to code trivia/whitespace that donāt impact runtime, such as tabs vs. spaces)
- Linters should be used for logical issues (runtime behavior) and style (non-logical issues that do impact runtime behavior, such as sorting imports)
The delineation of formatting vs. stylistic concerns is normally pretty straightforward to follow. Turning off any ESLint rule that would conflict with Prettier is not much work these days: neither ESLint nor typescript-eslintās core rulesets enable any rules that violate that philosophy, and eslint-config-prettier can do it for you if youāre working with other configs.
But! There are some edge cases in formatting/style that can seem to cross the divide and make it unclear when to use which tool. Iāll cover some of those surprising intricacies in this post, along with how Iād suggest resolving them.
Formatting Can Technically Change Behavior
Did you know that
JavaScriptās
Function.prototype.toString()
returns the string equivalent of a function? That means any
tool that modifies the text of a function -including
formatters, minifiers, and transpilers- technically
can change the runtime behavior of code.
For example, this code would log a different message based on whether itās formatted with spaces or tabs:
function greet(name) {
console.log(`Hello, ${name}!`);
}
console.log(greet.toString().includes("\t") ? "tab" : "no tab");
In practice, exceedingly few real code stringifies functions, let alone cares about the formatting of the result. Iāve never seen it be an issue. But it is a nifty proof of concept that these concerns can matter!
AST Modifications
Formatters such as Prettier generally donāt make modifications to code that change how the code is represented by tooling (generally known as its AST, or Abstract Syntax Tree). That means some changes can surprisingly be considered out-of-scope for a formatter.
Most notably (for me),
Prettier does not have an option to enforce curly
brackets. Doing so would change the consequent (what follows
the if(...)
) of
if
statements from a
Statement (e.g.
console.log()
) to a
Block (e.g.
{ console.log(); }
).
That limitation makes sense on its own, but presents an inconvenience: enforcing curly brackets is arguably a formatting concern, not a stylistic one - and so shouldnāt be handled by linters! š«.
For a while, I compromised my principles and would include
usage of the
curly
rule
in my ESLint configs. But that always felt wrong - even if the
formatting couldnāt reasonably handle this AST modification on
its own, enforcing curly brackets really is something that
should be handled by the formatter.
prettier-plugin-curly
Thatās why I recently wrote & published my first Prettier
plugin:
prettier-plugin-curly
. It adds the enforcement of consistent brace style (i.e. the
curly
ruleās
all
option) to Prettier. I now use it in my
template-typescript-node-packageās
.prettierrc
.
{
"plugins": ["prettier-plugin-curly"],
"useTabs": true
}
If youāre interested in enforcing curly brackets as part of
your formatter, Iād encourage you to try
prettier-plugin-curly
out.
Itās the first time Iāve written a Prettier plugin and Iād
love to be told how to improve it. ā¤ļø
Order Matters
One last sometimes-surprisingly-impactful concern is ordering. Many developers -myself included- prefer ordering file imports, properties on objects and types, and other constructs in a predictable way, to make it easier to scan through them. Itās tempting to suggest ordering as within the realm of a formatter.
Imports
However, order does impact runtime behavior - especially for imports!
The order your files
import
and/or
require
each other can make
a difference because code files sometimes trigger side effects
when theyāre run.
Consider this set of three files, where running
index.js
causes logs to run
in the imported files in order of import:
// index.js
import { b } from "./b.js";
import { a } from "./a.js";
// a.js
console.log("A!");
export const a = "a";
// b.js
console.log("B!");
export const b = "b";
Your code might be triggering more intensive side effects,
such as registering CSS styles, calling
fetch()
, or reading/writing
files on disk. Changing the order of module
import
s is generally too
risky for formatters.
Property Destructures
In fact, even the ordering of destructured object properties can make a difference! Object properties defined as getters can introduce side effects.
For example, this code block logs
a: 0
and
b: 1
now, but alphabetizing
the { b, a }
to
{ a, b }
would cause it to
instead log a: 1
and
b: 0
:
let count = 0;
const values = {
get a() {
return `a: ${count++}`;
},
get b() {
return `b: ${count++}`;
},
};
const { b, a } = values;
console.log(a);
console.log(b);
Changes in runtime behavior from sorting imports or property destructures are still pretty rare, though they can happen (especially in the case of imports). I donāt recommend using a Prettier plugin for sorting.
eslint-plugin-perfectionist
Instead, I recommend using
eslint-plugin-perfectionist
to apply sorting at the lint level. This plugin provides a
nice comprehensive set of rules for sorting constructs in
JavaScript and TypeScript code.
{
extends: ["plugin:perfectionist/recommended-natural"],
plugins: ["perfectionist"],
}
eslint-plugin-perfectionist
is also pretty new -released mid-2023- and Iād highly
recommend trying it out. Itās enabled in my
template-typescript-node-packageās
.eslintrc.cjs
.
Summarizing
I think a succinct set of guidelines out of all of this would be:
- Formatters should never change code behavior (except for stringifying functions)
- Linting for style should auto-fix any remaining changes that donāt impact code behavior (except for rare edge cases such as order-dependent side effects)
What do you think? Are there other edge cases that need to be handled? Please let me know!
Resources
You can read more about the concerns of formatters vs. linters in the following posts Iāve written:
- GitHub ReadME Guide: Formatters, linters, and compilers: Oh my!
- My Configuring ESLint, Prettier, and TypeScript Together guide
- typescript-eslintās What About Formatting? page
This blog post was inspired by a
timely discussion thread on Twitter
with
@azat_io_en and
@helloklose.
Thanks to Azat for sharing out
eslint-plugin-perfectionist
and Oksel for asking great questions! š