How to find all unused functions in JS buffers
The real power of Emacs lies in its extensibility. To be able to quickly hack some Elisp together to fix a specific problem right in your development environment is something quite unique to Emacs, and it makes it stand apart from other text editors.
I’m working on a fairly large JavaScript code base for which maintenance can sometimes be an issue.
Yesterday I wanted to quickly find all function definitions in a JavaScript file that were not referenced anymore in the project, so I decided to hack some Elisp to do that.
What do we already have?
Let’s see what building blocks are already available.
xref-js2 makes it easy to find all references to a specific function within a project, and js2-mode exposes an AST that can be visited.
All in all, what I want to achieve shouldn’t be too hard to implement!
First steps
I’m calling my small package js2-unused
, so all functions and variables will
have that prefix.
We’ll need some packages along the way, so let’s require them:
(require 'seq)
(require 'xref-js2)
(require 'subr-x)
The first step is to find all function definitions within the current buffer.
JS2-mode
has a function js2-visit-ast
that makes it really easy to traverse
the entire AST tree.
We can first define a variable that will hold all function definition names that we find:
(defvar js2-unused-definitions nil)
Now let’s traverse the AST and find all function definitions. We want to find:
- all assignments that assign to a function;
- all function declarations that are named (skipping anonymous functions).
(defun js2-unused--find-definitions ()
;; Reset the value before visiting the AST
(setq js2-unused-definitions nil)
(js2-visit-ast js2-mode-ast
#'js2-unused-visitor))
(defun js2-unused-visitor (node end-p)
"Add NODE's name to `js2-unused-definitions` if it is a function."
(unless end-p
(cond
;; assignment to a function
((and (js2-assign-node-p node)
(js2-function-node-p (js2-assign-node-right node)))
(push (js2-node-string (js2-assign-node-left node)) js2-unused-definitions))
;; function declaration (skipping anonymous ones)
((js2-function-node-p node)
(if-let ((name (js2-function-name node)))
(push name js2-unused-definitions))))
t))
Finding references using xref-js2
Now that we can find and store all function names in a list, let’s use
xref-js2
to filter the ones that are never referenced. If we find
unreferenced functions, we simply display a message listing them.
(defun js2-unused-functions ()
(interactive)
;; Make sure that JS2 has finished parsing the buffer
(js2-mode-wait-for-parse
(lambda ()
;; Walk the AST tree to find all function definitions
(js2-unused--find-definitions)
;; Use xref-js2 to filter the ones that are not referenced anywhere
(let ((unused (seq-filter (lambda (name)
(null (xref-js2--find-references
(js2-unused--unqualified-name name))))
js2-unused-definitions)))
;; If there are unreferenced function, display a message
(apply #'message (if unused
`("Unused functions in %s: %s "
,(file-name-nondirectory buffer-file-name)
,(mapconcat #'identity unused " "))
'("No unused function found")))))))
(defun js2-unused--unqualified-name (name)
"Return the local name of NAME.
foo.bar.baz => baz"
(save-match-data
(if (string-match "\\.\\([^.]+\\)$" name)
(match-string 1 name)
name)))
Conclusion
That’s it! In ~30 lines we can now find unreferenced functions in any JS file. Sure, the code is not perfect, far from it, but it was hacked together in 10 minutes and gets the job done.
Quickly writing some lisp code to fix a specific problem is something I do very
often. Most of the time, it’s code I throw away as soon as the task is
completed, but from time to time it’s something generic enough to be reused
later, in which case I save it in my emacs.d
, or make a proper package out of
it.
If you find this feature useful, you can grab it from my emacs.d.