Skip to content

A function reassigns or mutates one of its own parameters. Callers pass a value expecting the function to compute a result from it, not to have their argument silently changed underneath them — and a function that only reads its parameters is trivially testable input→output, with no need to inspect or snapshot the argument afterward.

ts
function withDiscount(price: number, rate: number) {
  price = price - price * rate;   // flagged — reassigns the `price` parameter
  return price;
}

function tagAsProcessed(item: { status: string }) {
  item.status = "processed";      // flagged — mutates the `item` parameter
  return item;
}
ts
// fix: compute a new value instead of writing back into the parameter
function withDiscount(price: number, rate: number) {
  const discounted = price - price * rate;
  return discounted;
}

function tagAsProcessed(item: { status: string }) {
  return { ...item, status: "processed" };
}

Scoped to simple-identifier parameters only — destructured ({ a, b }) and rest (...rest) parameters aren't tracked in this first version. Only single-level member/index writes are detected (x.foo = ..., x[0] = ...); nested writes (x.a.b = ...) and delete x.foo are not. A nested function inherits its enclosing function's watched parameters, so a closure mutating an outer parameter still flags — but tracking is name-based, so a nested local that happens to share a parameter's name is also flagged as if it were that parameter, a known false-positive edge case for this first version.

MIT License