fallow for Neovim. See your unused code, duplicates, and health in a native split.
Powered by fallow, a sub-second static analysis engine for JS/TS. No tree-sitter, no LSP, no config needed on the Neovim side.
- Neovim >= 0.10
- fallow CLI
- A TypeScript or JavaScript project with a
package.json - A Nerd Font if you want the icons (optional)
- nvim-web-devicons for file icons in the panel (optional)
{
"xeind/vallow.nvim",
cmd = { "Vallow", "VallowRefresh", "VallowSearch" },
keys = {
{ "<leader>V", "<cmd>Vallow<cr>", desc = "Vallow: toggle" },
{ "<leader>vr", "<cmd>VallowRefresh<cr>", desc = "Vallow: refresh" },
{ "<leader>vs", "<cmd>VallowSearch<cr>", desc = "Vallow: search findings" },
{ "<leader>va", "<cmd>Vallow action<cr>", desc = "Vallow: act on finding" },
{ "]v", "<cmd>Vallow next<cr>", desc = "Vallow: next finding" },
{ "[v", "<cmd>Vallow prev<cr>", desc = "Vallow: previous finding" },
},
opts = {},
}npm install -g fallow # global (recommended)
npm install --save-dev fallow # local to project
cargo install fallow # or via CargoOr let vallow fetch the standalone binary:
:VallowInstall " latest release into stdpath("data")/vallow/bin
:VallowInstall 3.22.0 " a specific version
:VallowInstall lsp " fallow-lsp
:VallowUpdate " latest againThe download uses curl. Release signatures (.sig) are published but vallow
does not verify them.
vallow looks for the binary in this order:
fallow_cmd, when you set it to something other than"fallow"<project>/node_modules/.bin/fallowfallowonPATHstdpath("data")/vallow/bin/fallow- nothing found: vallow offers the download once per session
A local install therefore needs no configuration. To pin one explicitly:
require("vallow").setup({ fallow_cmd = "./node_modules/.bin/fallow" })| Command | What it does |
|---|---|
:Vallow |
Toggle the panel |
:Vallow health |
Open the panel on a section: health, issues, duplicates, unused_code, architecture |
:Vallow dashboard |
Health dashboard float: score gauge, penalties, vital signs, trend, gate verdicts |
:Vallow audit [ref] |
Review the files changed since ref (default main, else master) |
:Vallow production |
Toggle production mode (test/dev files excluded) and re-run |
:Vallow watch |
Toggle watch mode: one fallow watch process streams results on every file change |
:Vallow action |
Ignore, trace or explain the finding under the cursor in a source buffer |
:Vallow path {file} |
Trace the shortest import path from the current buffer to {file} (fallow 3.24+). gt on a boundary violation traces its two files the same way |
:Vallow stacktrace |
Map a stack trace to source: the visual range (:'<,'>Vallow stacktrace), else the + register, fills the quickfix list with one entry per frame fallow resolves (fallow 3.24+) |
:Vallow next / :Vallow prev |
Jump to the next / previous vallow diagnostic, skipping LSP and linters |
:Vallow fix |
Preview fallow fix --dry-run, apply it after a confirm |
:Vallow refresh |
Re-run fallow and refresh |
:Vallow search |
Search findings |
:VallowRefresh |
Re-run fallow and refresh |
:VallowInspect |
Inspect the current file: exports, importers, findings, health |
:VallowSearch |
Search findings with snacks / telescope / fzf-lua / vim.ui.select |
:VallowInstall [lsp] [version] |
Download the fallow (or fallow-lsp) binary |
:VallowUpdate |
Download the latest fallow binary |
Press <CR> on any issue to jump to the file and line.
All remappable via setup({ keymaps = ... }).
| Key | Action |
|---|---|
<CR> |
Jump to file (edit in previous window) |
o |
Jump in horizontal split |
v |
Jump in vertical split |
t |
Jump in new tab |
L / H |
Next / previous tab (cycle sections) |
]c / [c |
Jump to next / previous section header |
<Tab> / za |
Toggle fold |
zo / zc |
Open / close fold |
zR / zM |
Open / close all folds |
K |
Detail float: path, name, kind, fix suggestions, cycle chain |
f |
Filter by path or name |
F |
Clear filter |
% |
Filter findings to the file under cursor |
i |
Audit mode: hide findings the change inherited |
gi |
Inspect the file on this row |
x |
Ignore this finding: .fallowrc.json/.fallowrc.jsonc entry or suppression comment |
ge |
Explain this issue type (fallow explain) |
gt |
Trace an unused export, a dependency, or a boundary violation (import path) |
d |
Diff the two largest instances of a clone group |
D |
Health dashboard float |
X |
Preview the fixes fallow would make, apply them after a confirm |
gf |
Open picker (fuzzy search all findings) |
P |
Peek at file in floating window (whole range for clones and functions) |
Q |
Send to quickfix |
y |
Yank path:line |
r |
Refresh |
q |
Close |
? |
Show keymap help |
Closing and reopening the panel keeps the tab, folds, filter, audit i flag,
and cursor row. With follow_buffer = true the filter follows the file you
enter; a file with no findings shows the filter line and an empty list.
Sections and categories shown only when they have findings.
| Section | Categories |
|---|---|
| UNUSED CODE | Unused Exports, Types, Members, Files, Dependencies, Misplaced Deps, Unlisted Deps |
| ISSUES | Unresolved Imports, Circular Deps, Package Cycles, Duplicate Exports, Stale Suppressions, Deprecated In Use |
| DUPLICATES | Clone Groups |
| HEALTH | Complexity, Hotspots, Refactoring Targets, Stale Flags (opt-in) |
| ARCHITECTURE | Boundary Violations |
Severity is color-coded: errors red, warnings yellow, hints grey.
Stale Flags lists feature flags old enough to delete, from
fallow flags --retirement (fallow 3.30 and later). Each row shows the flag
name, its age in days from git, and the file and line of its definition.
<CR> jumps there. The report costs one extra fallow process per run (git
blame for the ages), so it is off by default. Add "flags" to analyses to
turn it on. On an older fallow the category stays empty, with no notice.
fallow 3.33 and later hides unused exports, types, and members inside a file that is itself unused. The panel header shows a dim "N findings hidden in unused files" line with the count. Delete the file and the findings go too.
fallow 3.24 and later marks a dead-code finding with reachability_caveats
when it could not read or parse every file. A file it skipped may hold the
import that keeps the code alive. The panel shows a dim ? before the row
(the ? help float lists it), and the diagnostic message ends with
" (incomplete analysis)". Severity does not change. Check the finding by hand
before you delete anything; fallow fix skips it too.
require("vallow").setup({
fallow_cmd = "fallow",
fallow_args = {}, -- extra CLI flags forwarded verbatim
timeout_ms = 60000, -- kill fallow after this long (type-aware pass can take 120 s)
env = {}, -- extra env for fallow, e.g. { FALLOW_TYPE_AWARE_TIMEOUT_SECS = "300" }
-- Which analyses to run. Remove entries to skip them entirely.
-- Add "flags" for the Stale Flags category (fallow 3.30+, one extra process).
analyses = { "dead-code", "dupes", "health" },
-- Reorder tabs. Omitted sections keep their default order after listed ones.
-- section_order = { "health", "issues", "duplicates", "unused_code", "architecture" },
window = {
position = "right", -- "bottom" | "top" | "left" | "right"
size = 0.5,
},
max_items = 30, -- items per category before "N more..." expands
manifest_overlay = true, -- virtual text per dependency line in package.json
auto_refresh = false, -- re-run fallow silently 300 ms after a JS/TS file save
follow_buffer = false, -- with the panel open, filter it to the file you enter (like `%`)
watch = { enabled = false }, -- one long-lived `fallow watch` streaming results instead
cache = true, -- reopen with the last run's results, marked (stale), while fallow re-runs
diagnostics = {
enabled = true,
virtual_text = true, -- false hides the inline text, keeps signs and float
current_buffer_only = false, -- true limits diagnostics to the current buffer
virtual_lines = false, -- dim "ƒ cyclomatic N · cognitive M" above complex functions
-- Per finding category: turn it off, or change its severity
-- ("error" | "warn" | "info" | "hint").
categories = {
unused_exports = { enabled = true, severity = "hint" },
unresolved_imports = { enabled = true, severity = "error" },
-- ... one entry per category, see :help vallow-config
},
},
statusline = {
prefix = "vallow ", -- " " for a Nerd Font icon
},
-- Grey out unused files in the file explorers. Each explorer also needs a
-- one-line hook in its own setup, see "File explorers" below.
integrations = {
nvim_tree = true,
neo_tree = true,
oil = true,
},
keymaps = {
close = "q",
jump = "<CR>",
refresh = "r",
toggle_fold = nil, -- unset by default; za/zo/zc/zR/zM always work
next_tab = "L",
prev_tab = "H",
next_section = "]c",
prev_section = "[c",
filter = "f",
clear_filter = "F",
pick = "gf",
},
})All highlight groups (VallowHeader, VallowPath, VallowName, VallowSevError, …)
link to standard Neovim groups and work with any colorscheme. Override as needed:
vim.api.nvim_set_hl(0, "VallowHeader", { fg = "#bb9af7", bold = true })-- lualine
require("lualine").setup({
sections = {
lualine_x = { { require("vallow").statusline, color = { fg = "#f9c74f" } } },
},
})
-- raw
vim.o.statusline = "%{%v:lua.require('vallow').statusline()%}"Shows vallow 42 when issues exist, vallow ✓ when clean, empty when not run.
require("lualine").setup({
sections = { lualine_x = { require("vallow").lualine() } },
})Shows vallow E:2 W:5 H:40, coloured per severity with VallowSevError,
VallowSevWarn, and VallowSevHint. A spinner runs while fallow does,
vallow ✓ means no findings, and vallow ! means fallow failed.
Open a package.json inside the project and each dependency line gets virtual
text at the end: unused, unused (dev), type-only, test-only, or
dev dep in prod. Packages your code imports but never listed are named on the
closing brace of the dependencies block. Turn it off with
manifest_overlay = false.
require("vallow").setup({ watch = { enabled = true } })One fallow watch --format json process per project root stays up and streams
a fresh result whenever a file changes. Findings, diagnostics, and the panel
update without a save hook, and auto_refresh stops spawning runs while the
watcher is up. :Vallow watch toggles it. The process dies with Neovim and
moves with the project root.
fallow streams the dead-code analysis only, so duplicates and health keep the
values of the last full run. Press r for those.
-- telescope
require("telescope").load_extension("vallow") -- then :Telescope vallow
-- snacks
Snacks.picker.vallow() -- or Snacks.picker("vallow")Both list every finding with its category, path:line, and name, and preview
the file at the finding line. The snacks source registers itself when snacks is
loaded; if you load snacks later, call
require("vallow.picker").register_snacks() yourself. :VallowSearch and gf
keep working with snacks, telescope, fzf-lua, or vim.ui.select.
Files fallow reports as unused are greyed out with VallowUnusedFile (linked to
Comment). Each explorer reads its hooks from its own setup, so add one line there:
-- nvim-tree
require("nvim-tree").setup({
renderer = {
decorators = {
"Git", "Open", "Hidden", "Modified", "Bookmark", "Diagnostics", "Copied", "Cut",
require("vallow.integrations.nvim_tree").decorator,
},
},
})
-- neo-tree
require("neo-tree").setup({
components = { name = require("vallow.integrations.neo_tree").name },
})
-- oil
require("oil").setup({
view_options = { highlight_filename = require("vallow.integrations.oil").highlight_filename },
})The decorations update after every fallow run and stay until the next one. Turn
any of them off with integrations = { nvim_tree = false, neo_tree = false, oil = false }.
vallow fires User VallowResults after each run, so you can hook your own code:
vim.api.nvim_create_autocmd("User", { pattern = "VallowResults", callback = function() ... end })vallow passes the project root to fallow. Configure fallow via .fallowrc.json:
fallow initSuppress findings inline:
// fallow-ignore-next-line unused-export
export function keepThisPublic() {}Tests run headless against a fixture project in tests/fixture/. They need
fallow on PATH.
make test # headless harness: runner, panel render, error path
make lint # stylua --check lua
{ "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json", "entry": ["src/index.ts"], "ignorePatterns": ["**/*.test.ts", "dist/**"], "ignoreDependencies": ["typescript"], "rules": { "unused-export": "error", "unused-file": "warn", "circular-dependency": "warn" } }