The following works only with lexical binding, because with dynamic binding (defvar foo)
has no real effect. It checks whether let-binding the variable affects the dynamic value or not. I used the term "declared" instead of "defined", because (defvar foo)
doesn't quite feel like a full definition to me (e.g., it only applies to the file it's in) so it's more like C's "declare".
;; -*- lexical-binding: t -*-
(defvar foo 2)
(defvar bar)
(defmacro check-var (var)
`(message "%s %s"
',var
(cond ((boundp ',var) "has been initialized")
((let ((,var t)) (boundp ',var))
"has been declared, but not initialized")
(t "has NOT been declared nor initialized"))))
(check-var foo)
(check-var bar)
(check-var qux)