Ghostcode finds unreferenced code in Vim9 plugins. It scans a project for functions, methods, classes, fields, enum values, and script-level variables, then reports every one that nothing in the project ever calls or reads.
- Vim 9.1 or later, compiled with Vim9 script and class support.
- Logger, which shows Ghostcode's messages.
Install Ghostcode and Logger with your plugin manager.
vim-plug
Plug 'Galvarcus/Logger'
Plug 'Galvarcus/ghostcode'Native package
git clone https://github.com/Galvarcus/Logger.git \
~/.vim/pack/vendor/start/Logger
git clone https://github.com/Galvarcus/ghostcode.git \
~/.vim/pack/vendor/start/ghostcodeGhostcode adds one command:
:GhostCode [path]path is the root directory of the project to scan and supports
file completion. It defaults to the current working directory when
omitted.
Ghostcode puts its findings in the quickfix list and opens the list when it finds ghost code. It also shows a summary through Logger:
Ghostcode - ghostcode.vim - Report: 23 symbols, 7 ghost, 3 unresolved
Logger reads its options for Ghostcode from variables named
g:logger_ghostcode_<option>. See the Logger README for each option.
Each quickfix entry names the kind of symbol, its name, its file, and its line:
testplugin/autoload/util.vim|16 col 1| [ghost] function TrulyDeadFunction
testplugin/autoload/util.vim|37 col 1| [ghost] method DeadMethod
testplugin/autoload/util.vim|58 col 1| [ghost] variable g_never_read
To run Ghostcode headlessly, for example in CI, add Ghostcode and
Logger to runtimepath and source Ghostcode's plugin/ script before
you run the command. -u NONE skips Vim's normal startup, so Vim does
not load plugins and :GhostCode is not defined until you source the
script.
Commands given with -c run before VimEnter. Logger keeps messages
sent before VimEnter in a queue, so the summary does not show in
this mode. Write the quickfix list to a file to get the results:
vim -u NONE -N -es \
--cmd 'set rtp+=/path/to/ghostcode,/path/to/Logger' \
-c 'runtime plugin/ghostcode.vim' \
-c 'GhostCode /path/to/project' \
-c 'vim9cmd writefile(getqflist()->mapnew((_, i) => printf("%s:%d: %s", bufname(i.bufnr), i.lnum, i.text)), "ghostcode_results.txt")' \
-c 'qa!'Ghostcode has one setting:
let g:ghostcode_exports_are_api = v:trueBy default Ghostcode treats an exported symbol as public API and never reports it. Vim9 cannot export a method or a field by itself, so every method and field of an exported class counts as exported too, unless its name starts with an underscore.
Set the value to v:false for a plugin whose exports only serve its own
files, as in an application. Ghostcode then reports an export that
nothing in the project uses, like any other symbol. From the command
line, add -c 'let g:ghostcode_exports_are_api = 0' to the command
above, after runtime plugin/ghostcode.vim.
defandexport defclassandexport class, includingextendsandimplementsenumandexport enum, and enum values (Color.Red)interfaceandexport interfacedeclarations- class methods, static methods, and fields
- module-level script variables and class fields
- type annotations (
var x: Foo,def F(): Foo,list<Foo>) - direct calls (
Foo()), method calls (Foo.Bar(),this.Method()), and calls through a locally-typed variable or parameter, including a two-level field-then-method chain onthis(this.field.Method()) - a method called directly on a constructor result
(
Foo.new(args).Bar()) - bare funcref and value usage of a known symbol (
var Ref = Foo,timer_start(1000, Foo),{callback: Foo}) call(),function(), andexecute(), where the target is a string literal or bare identifier- every
importform: relative paths,import autoload, and named imports (import {Foo} from '...') - exported symbols, treated as public API and never reported as dead unless you turn that off, see Settings
- the classic
plugin#Function()autoload calling convention - top-level code in scripts that Vim sources directly (
plugin/,ftplugin/,ftdetect/,syntax/,indent/,compiler/,colors/,keymap/,lang/, and theirafter/copies) and intests/, all treated as entry points that need no call site
- local variables declared inside a function body. Ghostcode analyzes reachability between module-level symbols, not local dead stores
- dynamic dispatch built from string concatenation, computed indices, or anything else that isn't a literal identifier or string
- method calls on a variable with no declared or inferable type. When a variable's type can't be determined, Ghostcode falls back to finding a same-name match across the project. When several symbols share the name, it counts every one of them as used
- a variable that the code only writes and never reads. A write counts as a use
Ghostcode is conservative by design: anything it can't prove is unreferenced is left alone rather than reported.
Run the tests from the root of the repository, with Logger on the runtime path:
vim -es -u NONE -N --cmd 'set rtp+=.,/path/to/Logger' \
-c 'runtime plugin/ghostcode.vim' \
-c 'source tests/harness.vim' -c 'quit'
cat tests/results.txtThe file reads All tests passed when every test passes.
GNU GPL 3.0