Skip to content
GalvarcusPublic

About

A vim9 scanner for ghost (unreferenced) code in vim9script plugins

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

22 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ghostcode

Vim License

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.

Requirements

  • Vim 9.1 or later, compiled with Vim9 script and class support.
  • Logger, which shows Ghostcode's messages.

Installation

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/ghostcode

Usage

Ghostcode 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

Running from the command line

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!'

Settings

Ghostcode has one setting:

let g:ghostcode_exports_are_api = v:true

By 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.

What Ghostcode detects

  • def and export def
  • class and export class, including extends and implements
  • enum and export enum, and enum values (Color.Red)
  • interface and export interface declarations
  • 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 on this (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(), and execute(), where the target is a string literal or bare identifier
  • every import form: 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 their after/ copies) and in tests/, all treated as entry points that need no call site

What Ghostcode doesn't detect

  • 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.

Development

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.txt

The file reads All tests passed when every test passes.

License

GNU GPL 3.0

About

A vim9 scanner for ghost (unreferenced) code in vim9script plugins

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages