Exported Mutable Variable
ID |
javascript.exported_mutable_variable |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
JavaScript |
Tags |
code-smell, modules, state |
Description
Reports an exported let or var binding that the module reassigns after declaring it.
Both spellings of the export are covered: the declaration form export let x = 0 and the clause form export {x} naming a module-level binding. A re-export (export {x} from './other') is not — the binding belongs to the other module.
A binding that is never reassigned is not reported, whatever it is declared with: the defect is the mutation, and javascript.prefer_const is what asks for const. Destructuring patterns are out of scope, since each sub-binding carries its own symbol and the remediation for a mixed pattern is a rewrite rather than a keyword.
Rationale
// Bad — session.js
export let currentUser = null;
export function signIn(user) {
currentUser = user;
}
-
An ES module export is a live binding, not a copy. An importer that wrote
import {currentUser} from './session'sees the new value the moment the module assigns it, at an instant the importer chose nothing about. -
It reads like a constant at both ends: nothing at the import site says the name can change, and the assignment that changes it is a plain line in the middle of some function.
-
The two halves disagree in one common case.
import {currentUser}tracks the binding, but destructuring the namespace (const {currentUser} = await import('./session')) copies it once, and a CommonJS consumer readingrequire('./session').currentUsergets whatever the value was at read time. The same module behaves differently depending on how it was imported.
Remediation
Export a function that reads the state, so every consumer goes through one door and the read is visibly a read.
// Good — session.js
let currentUser = null;
export function getCurrentUser() {
return currentUser;
}
export function signIn(user) {
currentUser = user;
}
// Also good — one exported object, mutated in place, so every import shares it
export const session = { currentUser: null };
export function signIn(user) {
session.currentUser = user;
}