norefs

What norefs finds

norefs loads your project with ts-morph and runs two passes.

Module-level checks

A finding at a higher level swallows the findings inside it: an unused file hides its exports and members, an unused export with zero references anywhere hides its members, and a type losing every member folds them into its one becomes empty finding. One line per problem, not fifty.

Verdicts

Every finding carries a verdict: the claim it makes, with its safety profile. “Unused” is not one claim — it is six:

Each soft verdict prints its evidence — the twin that reads the member, the boundary the type crosses. --fix only applies dead and over-exported findings; the rest wait for --fix-unsafe or your judgment.

Member-level checks

The member pass looks for six kinds of member owners:

A function is read through every return it has. Each literal it hands back is a shape of its own, so if (wide) return { handle, deadWide }; return { handle, deadNarrow } reports both dead keys — and a key more than one branch writes is reported only when every one of those branches is unread, because two branches of one shape share a single set of declarations. It is reported once then, on the branch that writes it first: one death, told once, on the line a reader edits before the other. --fix removes that copy and comes straight back for the next. A return of anything but a literal leaves the function alone.

Both object-literal owners are read to their full depth. A literal nested inside another is a shape of its own, so const cfg = { oven: { tray: 'steel', deadRack: 'wire' } } reports deadRack in const `cfg.oven` — but only where every read of oven keeps the value local. A property can hold one shape per element too: { cards: [{ title, deadNote }, { title, deadNote }] } reports deadNote on both cards, as long as every element stays local — read through a callback written on the spot, a for…of binding, or an index. The elements answer together, so a name any one of them holds a read on is alive on all of them. A read that hands the whole inner shape onward stops the descent there, and so does a property nothing reads: that property is the finding, and the members under it would tell one death twice. Limitations has the full rule.

For each property it finds, it asks norefs’ own project-wide reference index (via findReferencesAsNodes) whether anything reads it — one pass over every identifier, rather than a language-service query per declaration; see Speed. No references beyond the declaration itself means the property is unused.

Because the check is reference-based, it follows structural typing correctly — v.x resolves back to interface A { x: number } even without an explicit cast. See Limitations for where that breaks down.

When every member of a named interface or type alias is unused while the type itself is still referenced, the member findings fold into one: interface `X` becomes empty: all 6 members are dead. That is one logical fact, so it is one finding, carrying the most cautious verdict of the members it swallowed. Removing the members would leave an empty interface X {} behind, and only you know whether its consumers should go too — --fix never touches these. An interface that extends another is exempt — empty, it still works as an alias.

A shape written inline on a property or on a binding folds the same way, and reads property `labels` becomes empty: all 2 members are dead. { labels: { deadColor, deadFont } } has no declaration to answer for the inner shape — the property is what a reader would delete, so the property is the finding, whether the shape is written as a value or as a type. The property itself is still read: that is the only reason norefs looked inside it, since a nested shape is only read member by member where every read of the holding property keeps the value local. So the fold always leaves a read behind that now reaches nothing, and removing the property means removing that read too — a human’s call, exactly like an emptied interface’s consumers. Without the fold, --fix would delete the members and leave labels: {} sitting there: dead, and invisible to the next run.

A const box = { … } that loses every member folds onto the binding for the same reason — const `box` becomes empty: all 2 members are dead — but only while something still reads box. A binding nothing reads is not a fold: the members are reported one by one, and the cleanup pass takes the whole declaration with them. Nothing outlives that removal, so nothing needs your judgment.

Three kinds by their --only names

Most --only names read as the checks above: files, exports, types, members, dependencies, unlisted, misplaced, stranded. Three are less obvious, so here they are defined:

--only name What it reports
ns-exports An unused export whose namespace — a TS namespace or an import * as binding — is used, so the export may still be consumed dynamically
ns-types The same, for an exported type
empty-types A still-referenced type — or a property or binding holding an inline shape — that becomes empty once its unused members go: the folded becomes empty finding above

Production mode

Every finding norefs makes is relative to a question. The default question is “does anything in this repository use it?”, and the tests count — a member only a test reads is labelled test-only, not dead, because deleting it breaks something real.

--production asks the stricter question: what is left standing if the tests were not there at all?

norefs --production

Test, spec, stories, bench and config files — and everything under a harness directory — are treated as absent. Three things follow, and they are the whole definition:

devDependencies fall outside it too: they exist to build and test. So does the misplaced-dependency check, which needs both halves of the code to decide anything. A dependencies entry only the tests import is simply unused here.

It never combines with --fix. A production finding is dead to the shipping path and may be perfectly alive in the tests this run ignored — deleting it breaks them. That is the same reason test-only findings are never fixed either: the fix is deleting the tests too, and only you do that. norefs --production --fix is a usage error, exit code 2.

The two modes answer different questions, so run both: the default one to find what nothing uses, --production to find what only the scaffolding is holding up.


← All docs