Biome
-
An Unsafe Autofix Can Change What a Test Covers
The serializer has a test for an array hole:
// biome-ignore lint/suspicious/noSparseArray: an array hole is the case under test expect(safeStringify([1, , 3])).toBe('[1,"[undefined]",3]');Replacing the hole with an explicit
undefinedleaves the expected output unchanged. It also removes the case the test was meant to cover.Biome offers that replacement as an unsafe lint fix. It isn’t an ordinary formatting change, and it doesn’t happen with
--writealone.Absent and undefined aren’t the same input
The middle position in
[1, , 3]has no own element. In[1, undefined, 3], the element exists and its value isundefined.Reading either position by index produces
undefinedin these arrays, but array methods don’t always treat them alike.Array.prototype.mapskips empty slots. If the serializer mapped a sanitizing function over the sparse array, the callback wouldn’t run for the hole. The hole would remain in the result, and JSON serialization would render it asnull.The implementation instead uses an indexed loop. It reads each position and passes the value through the sanitizer, producing the logger’s explicit
"[undefined]"marker for the hole.With that implementation, both inputs produce:
[1,"[undefined]",3]The output is intentionally the same. The inputs still need separate tests because a future implementation change could handle one correctly and the other incorrectly.
The original comment blamed the wrong command
The comment above the test said
biome check --writewould replace the hole. Without the suppression, that command reports the rule violation and leaves the sparse array intact.Adding
--unsafeenabled the replacement:[1, , 3] → [1, undefined, 3]Biome classifies the fix as unsafe because it can change behavior. The corrected comment needs to name the command that opts into that change.
I like having the distinction in the tooling. A normal cleanup command should not quietly make this decision for the test. An explicit unsafe-fix pass still needs review, in source files as well as tests.
The suppression is narrow and has a concrete reason: this particular hole is deliberate test input.
A passing rewritten test can lose its purpose
After the replacement, the assertion still passes against the indexed-loop implementation. It now checks explicit
undefined, not an absent element.A later refactor from the loop to
mapcould keep that rewritten test passing while changing sparse-array output. The original hole test would catch the difference.Coverage numbers don’t settle this. The line can still execute, and some coverage measures may remain unchanged, without preserving the original input case. There is no general guarantee that every coverage metric would be identical.
The review question is simpler: does this test still contain the input it was written to exercise?
Odd syntax in a fixture can be a mistake, but it can also be the entire reason the test exists. Before accepting an autofix, read the assertion and the reason for the unusual input together.
Here, the extra comma isn’t clutter. Removing it removes the sparse-array case.
Sources
- Biome noSparseArray — replacing holes with explicit
undefinedis an unsafe fix. - Biome unsafe fixes — opting into behavior-changing fixes.
- Array methods and empty slots — differences in how array methods handle holes.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
- Biome noSparseArray — replacing holes with explicit
-
Why Svelte 5 Wants `let` Instead of `const` (And Why Your Linter Is Confused)
If you’ve been working with Svelte 5 and a linter like Biome, you might have run into this sitauation:
use
constinstead ofletHowever, Svelte actually needs that
let.Have you ever wondered why this is?
Here is an attempt to explain it to you.
Svelte 5’s reactivity model is different from the rest of the TypeScript world, and some of our tooling hasn’t quite caught up yet.
The
constRule Everyone KnowsIn standard TypeScript and React, the
prefer-construle is a solid best practice.If you declare a variable and never reassign it, use
const. It communicates intent clearly: this binding won’t change. You would thinkBut there is confusion when it comes to objects that are defined as
const.Let’s take a look at a React example:
// React — const makes perfect sense here const [count, setCount] = useState(0); const handleClick = () => setCount(count + 1);count is a number (primitive). setCount is a function.
Neither can modify itself so it makes sense to use
const.Svelte 5 Plays by Different Rules
Svelte 5 introduced runes — reactive primitives like
$state(),$derived(), and$props()that bring fine-grained reactivity directly into JavaScript.Svelte compiler transforms these declarations into getters and setters behind the scenes.
The value does get reassigned, even if your code looks like a simple variable.
<script lang="ts"> let count = $state(0); </script> <button onclick={() => count++}> Clicked {count} times </button>Biome Gets This Wrong
But they are trying to make it right. Biome added experimental Svelte support in v2.3.0, but it has a significant limitation: it only analyzes the
<script>block in isolation.It doesn’t see what happens in the template. So when Biome looks at this:
<script lang="ts"> let isOpen = $state(false); </script> <button onclick={() => isOpen = !isOpen}> Toggle </button>It only sees
let isOpen = $state(false)and thinks: “this variable is never reassigned, useconst.”It completely misses the
isOpen = !isOpenhappening in the template markup.If you run
biome check --write, it will automatically changelettoconstand break your app.The Biome team has acknowledged this as an explicit limitation of their partial Svelte support.
For now, the workaround is to either disable the
useConstrule for Svelte files or addbiome-ignorecomments where needed.What About ESLint?
The Svelte ESLint plugin community has proposed two new rules to handle this properly:
svelte/rune-prefer-let(which recommendsletfor rune declarations) and a Svelte-awaresvelte/prefer-constthat understands reactive declarations. These would give you proper linting without the false positives.My Take
Svelte 5’s runes are special and deserve their own way of handling variable declarations.
React hooks are different.
Svelte’s compiler rewrites your variable.
I like Svelte.
Please fix Biome.
Don’t make me use eslint.