How it works

One pass over the syntax. No type checker.

MemGuard reads each file on its own, builds a small index of what the code does, and reports only where that index shows evidence of scale.

What a run does.

One file, followed all the way through. The panel keeps up as you read.

  1. 01

    Scan

    Walk the project for .js, .jsx, .ts and .tsx files. node_modules, .git, dist, build, coverage, .next and out are skipped unless your config says otherwise.

  2. 02

    Parse

    Each file goes through the TypeScript compiler API on its own. No program, no tsconfig resolution, no type checker, so a file that does not compile still gets read.

  3. 03

    Index

    One walk builds the scope index: which binding a name refers to and where else it is used, whether an expression traces back to an await, and whether a node runs once per loop iteration.

  4. 04

    Apply

    The five rules subscribe to the syntax they care about. Each one asks the index for evidence before it reports anything.

  5. 05

    Report

    A readable frame for a person, JSON for a pipeline, and an exit code that tells a failed run apart from a finding.

The threshold

Three questions before a copy is worth mentioning.

A spread is ordinary code, and a tool that flags every one of them gets muted in a week. MEM001 and MEM002 see a copy and then look for a reason to care: does it run inside a loop, does its source trace back to an await, is it never modified after it is created? When the answer to all three is no, nothing is reported.

function withLatest(users, extra) {
  const next = [...users];
  next.push(extra);
  return next;
}
// silent. the copy is modified, so it is doing work

function labels(users) {
  const copy = [...users];
  return copy.map(toLabel);
}
// MEM001. never modified after it is created

function tagsOf(users) {
  for (const user of users) {
    send([...user.tags]);
  }
}
// MEM001. created on every iteration

async function recent(db) {
  const rows = await db.query();
  return [...rows].sort(byDate);
}
// MEM001. the source traces back to an await

What it will not guess.

Each of these is a decision, not a gap waiting to be filled. Under-reporting is the safe direction for a tool you leave switched on.

It does not read other files.

Imported names create no bindings, so a cache evicted in another module is invisible to the rule looking at this one.

It does not track destructuring.

A name bound by const { a } = obj creates no binding at all, so rules stay silent rather than guess at a variable they cannot follow.

It does not know types.

MEM005 reads a name like findAll as a clue. It cannot tell whether the call returns ten rows or ten million.

It does not edit your code.

Changing how memory is held changes how an application behaves, so every finding ends at a suggestion.

Run it against your project.

Two commands, no install, and a report you can read before you decide anything.

Get the commands