I suppose that itβs not even possible to pinpoint variable binding regions, or at least not to include, for example, a byte compiler (or parts of it) or reimplementation of parts of Emacs Lisp semantics.
The key problem is macros. They are not hygienic in Emacs Lisp. Thus, any macro can introduce arbitrary local bindings . In fact, many macros do this, such as the dolist , condition-case and pcase from the standard library, or the anaphoric list processing function from dash.el , to name a popular third-party library.
Using these macros, it becomes impossible to determine the scope of variables only from the syntactical context. Take the following example:
(condition-case err (--each my-fancy-list (my-fancy-function it nil t)) (error (message "Error %S happened: %s" (car err) (cadr err))))
Unaware of the condition-case and --each , are err and it locally related? If so, in which sub-expressions are they related, for example. err is connected in all subexpressions or only with the form of the handler (the latter matters)?
To determine the scope of a variable in such cases, you either need to maintain an exhaustive white list of macros along with your binding properties, or you need to expand the macros to dynamically determine their binding properties (for example, look for let in the extended body).
Both of these approaches are a lot of work in their implementation and have disadvantages. The whitelist of macro definitions is almost naturally incomplete, incorrect, and obsolete (just look at the complex linking semantics of pcase ), while expanding macros requires a macro definition to be present, which is not always the case, for example, if you are editing Emacs Lisp using the above dash .el, without actually installing this library.
However, expanding macros is probably the best effort, and even better, you don't need to implement it yourself. The Emacs Lisp byte compiler already does this, and it warns of references to free variables, as well as lexical bindings also included in unused lexical variables. So, the byte compiles your files!
At best, avoid calling byte-compile-file from your Emacs startup and instead write the Makefile to the byte compiler in a new Emacs instance to have a clean environment:
SRCS = foo.el OBJECTS = $(SRCS:.el=.elc) .PHONY: compile compile : $(OBJECTS) %.elc : %.el $(EMACS) -Q --batch -f batch-byte-compile $<
For more complex libraries, use the -L flag for Emacs to configure the correct load-path for compilation.