changeset 105137:1ca02a761eac

(elint): New custom group. (elint-log-buffer): Make it a defcustom. (elint-scan-preloaded, elint-ignored-warnings) (elint-directory-skip-re): New options. (elint-builtin-variables): Doc fix. (elint-preloaded-env): New variable. (elint-unknown-builtin-args): Add an entry for encode-time. (elint-extra-errors): Make it a variable rather than a constant. (elint-preloaded-skip-re): New constant. (elint-directory): Skip files matching elint-directory-skip-re. (elint-features): New variable, local to linted buffers. (elint-update-env): Initialize elint-features. Possibly add elint-preloaded-env to the buffer's environment. (elint-get-top-forms): Bind elint-current-pos, for log messages. Skip quoted forms. (elint-init-form): New function, extracted from elint-init-env. Make non-list forms a warning rather than an error. Add the mode-map for define-derived-mode. Handle define-minor-mode, easy-menu-define, put that adds an error-condition, and provide. When requiring cl, also require cl-macs. Really require cl, to handle some cl macros. Store required libraries in the list elint-features, so as not to re-load them. Treat cc-require like require. (elint-init-env): Call elint-init-form to do the work. Handle eval-and-compile and such like. (elint-add-required-env): Do not clear messages. (elint-special-forms): Add handlers for function, defalias, if, when, unless, and, or. (elint-form): Add optional argument to ignore elint-special-forms, useful to prevent recursive calls from handlers. Doc fix. Respect elint-ignored-warnings. (elint-form): Respect elint-ignored-warnings. (elint-bound-variable, elint-bound-function): New variables. (elint-unbound-variable): Respect elint-bound-variable. (elint-get-args): Respect elint-bound-function. (elint-check-cond-form): Add some simple handling for (f)boundp and featurep tests. (elint-check-defalias-form): New handler. (elint-check-let-form): Make an empty let a warning rather than an error. (elint-check-setq-form): Make an empty setq a warning rather than an error. Respect elint-ignored-warnings. (elint-check-defvar-form): Accept null doc-strings. (elint-check-conditional-form): New handler. Does some simple-minded checking of featurep and (f)boundp tests. (elint-put-function-args): New function. (elint-initialize): Use elint-scan-doc-file rather than elint-find-builtin-variables. Use elint-put-function-args. Possibly scan preloaded-file-list. (elint-scan-doc-file): Rename from elint-find-builtin-variables and extend to handle functions as well.
author Glenn Morris <rgm@gnu.org>
date Tue, 22 Sep 2009 02:28:28 +0000
parents efbb3447451f
children 515700d42454
files lisp/emacs-lisp/elint.el
diffstat 1 files changed, 397 insertions(+), 104 deletions(-) [+]
line wrap: on
line diff
--- a/lisp/emacs-lisp/elint.el	Tue Sep 22 02:24:20 2009 +0000
+++ b/lisp/emacs-lisp/elint.el	Tue Sep 22 02:28:28 2009 +0000
@@ -38,40 +38,118 @@
 
 ;;; To do:
 
-;; * List of variables and functions defined in dumped lisp files.
 ;; * Adding type checking. (Stop that sniggering!)
-;; * Handle eval-when-compile (eg for requires, being sensitive to the
-;;   difference between funcs and macros).
+;; * Make eval-when-compile be sensitive to the difference between
+;;   funcs and macros.
 ;; * Requires within function bodies.
+;; * Handle defstruct.
+;; * Prevent recursive requires.
 
 ;;; Code:
 
-(defvar elint-log-buffer "*Elint*"
-  "*The buffer to insert lint messages in.")
+(defgroup elint nil
+  "Linting for Emacs Lisp."
+  :prefix "elint-"
+  :group 'maint)
+
+(defcustom elint-log-buffer "*Elint*"
+  "The buffer in which to log lint messages."
+  :type 'string
+  :safe 'stringp
+  :group 'elint)
+
+(defcustom elint-scan-preloaded t
+  "Non-nil means to scan `preloaded-file-list' when initializing.
+Otherwise, just scan the DOC file for functions and variables.
+This is faster, but less accurate, since it misses undocumented features.
+This may result in spurious warnings about unknown functions, etc."
+  :type 'boolean
+  :safe 'booleanp
+  :group 'elint
+  :version "23.2")
+
+(defcustom elint-ignored-warnings nil
+  "If non-nil, a list of issue types that Elint should ignore.
+This is useful if Elint has trouble understanding your code and
+you need to suppress lots of spurious warnings.  The valid list elements
+are as follows, and suppress messages about the indicated features:
+  undefined-functions - calls to unknown functions
+  unbound-reference   - reference to unknown variables
+  unbound-assignment  - assignment to unknown variables
+  macro-expansions    - failure to expand macros
+  empty-let           - let-bindings with empty variable lists"
+  :type '(choice (const :tag "Don't suppress any warnings" nil)
+		 (repeat :tag "List of issues to ignore"
+			 (choice (const undefined-functions
+					:tag "Calls to unknown functions")
+				 (const unbound-reference
+					:tag "Reference to unknown variables")
+				 (const unbound-assignment
+					:tag "Assignment to unknown variables")
+				 (const macro-expansion
+					:tag "Failure to expand macros")
+				 (const empty-let
+					:tag "Let-binding with empty varlist"))))
+  :safe (lambda (value) (or (null value)
+			    (and (listp value)
+				 (equal value
+					(mapcar
+					 (lambda (e)
+					   (if (memq e
+						     '(undefined-functions
+						       unbound-reference
+						       unbound-assignment
+						       macro-expansion
+						       empty-let))
+					       e))
+					 value)))))
+  :version "23.2"
+  :group 'elint)
+
+(defcustom elint-directory-skip-re "\\(ldefs-boot\\|loaddefs\\)\\.el\\'"
+  "If nil, a regexp matching files to skip when linting a directory."
+  :type '(choice (const :tag "Lint all files" nil)
+		 (regexp :tag "Regexp to skip"))
+  :safe 'string-or-null-p
+  :group 'elint
+  :version "23.2")
 
 ;;;
 ;;; Data
 ;;;
 
-
 ;; FIXME does this serve any useful purpose now elint-builtin-variables exists?
 (defconst elint-standard-variables '(local-write-file-hooks vc-mode)
   "Standard buffer local variables, excluding `elint-builtin-variables'.")
 
 (defvar elint-builtin-variables nil
-  "List of built-in variables.  Set by `elint-initialize'.")
+  "List of built-in variables.  Set by `elint-initialize'.
+This is actually all those documented in the DOC file, which includes
+built-in variables and those from dumped Lisp files.")
 
 (defvar elint-autoloaded-variables nil
   "List of `loaddefs.el' variables.  Set by `elint-initialize'.")
 
-;; FIXME dumped variables and functions.
+(defvar elint-preloaded-env nil
+  "Environment defined by the preloaded (dumped) Lisp files.
+Set by `elint-initialize', if `elint-scan-preloaded' is non-nil.")
 
-(defconst elint-unknown-builtin-args nil
+(defconst elint-unknown-builtin-args
+  ;; encode-time allows extra arguments for use with decode-time.
+  ;; For some reason, some people seem to like to use them in other cases.
+  '((encode-time second minute hour day month year &rest zone))
   "Those built-ins for which we can't find arguments, if any.")
 
-(defconst elint-extra-errors '(file-locked file-supersession ftp-error)
+(defvar elint-extra-errors '(file-locked file-supersession ftp-error)
   "Errors without error-message or error-confitions properties.")
 
+(defconst elint-preloaded-skip-re
+  (regexp-opt '("loaddefs.el" "loadup.el" "cus-start" "language/"
+		"eucjp-ms" "mule-conf" "/characters" "/charprop"
+		"cp51932"))
+  "Regexp matching elements of `preloaded-file-list' to ignore.
+We ignore them because they contain no definitions of use to Elint.")
+
 ;;;
 ;;; ADT: top-form
 ;;;
@@ -188,7 +266,8 @@
 ;; cf byte-recompile-directory.
 ;;;###autoload
 (defun elint-directory (directory)
-  "Lint all the .el files in DIRECTORY."
+  "Lint all the .el files in DIRECTORY.
+A complicated directory may require a lot of memory."
   (interactive "DElint directory: ")
   (let ((elint-running t))
     (dolist (file (directory-files directory t))
@@ -196,7 +275,9 @@
       (when (and (string-match "\\.el\\'" file)
 		 (file-readable-p file)
 		 (not (auto-save-file-name-p file)))
-	(elint-file file))))
+	(if (string-match elint-directory-skip-re file)
+	    (message "Skipping file %s" file)
+	  (elint-file file)))))
   (elint-set-mode-line))
 
 ;;;###autoload
@@ -249,6 +330,12 @@
   "The last time the buffers env was updated.
 Is measured in buffer-modified-ticks and is local in linted buffers.")
 
+;; This is a minor optimization.  It is local to every buffer, and so
+;; does not prevent recursive requirs.  It does not list the requires
+;; of requires.
+(defvar elint-features nil
+  "List of all libraries this buffer has required, or that have been provided.")
+
 (defun elint-update-env ()
   "Update the elint environment in the current buffer.
 Don't do anything if the buffer hasn't been changed since this
@@ -262,26 +349,37 @@
       elint-buffer-forms
     ;; Remake env
     (set (make-local-variable 'elint-buffer-forms) (elint-get-top-forms))
+    (set (make-local-variable 'elint-features) nil)
     (set (make-local-variable 'elint-buffer-env)
 	 (elint-init-env elint-buffer-forms))
+    (if elint-preloaded-env
+	(elint-env-add-env elint-preloaded-env elint-buffer-env))
     (set (make-local-variable 'elint-last-env-time) (buffer-modified-tick))
     elint-buffer-forms))
 
 (defun elint-get-top-forms ()
   "Collect all the top forms in the current buffer."
   (save-excursion
-    (let ((tops nil))
+    (let (tops)
       (goto-char (point-min))
       (while (elint-find-next-top-form)
-	(let ((pos (point)))
-	  (condition-case nil
-	      (setq tops (cons
-			  (elint-make-top-form (read (current-buffer)) pos)
-			  tops))
-	    (end-of-file
-	     (goto-char pos)
-	     (error "Missing ')' in top form: %s"
-		    (buffer-substring pos (line-end-position)))))))
+	(let ((elint-current-pos (point)))
+	  ;; non-list check could be here too. errors may be out of seq.
+	  ;; quoted check cannot be elsewhere, since quotes skipped.
+	  (if (looking-back "'")
+	      ;; Eg cust-print.el uses ' as a comment syntax.
+	      (elint-warning "Skipping quoted form `'%.20s...'"
+			   (read (current-buffer)))
+	    (condition-case nil
+		(setq tops (cons
+			    (elint-make-top-form (read (current-buffer))
+						 elint-current-pos)
+			    tops))
+	      (end-of-file
+	       (goto-char elint-current-pos)
+	       (error "Missing ')' in top form: %s"
+		      (buffer-substring elint-current-pos
+					(line-end-position))))))))
       (nreverse tops))))
 
 (defun elint-find-next-top-form ()
@@ -290,6 +388,81 @@
   (parse-partial-sexp (point) (point-max) nil t)
   (not (eobp)))
 
+(defvar env)				; from elint-init-env
+
+(defun elint-init-form (form)
+  "Process FORM, adding to ENV if recognized."
+  (cond
+   ;; Eg nnmaildir seems to use [] as a form of comment syntax.
+   ((not (listp form))
+    (elint-warning "Skipping non-list form `%s'" form))
+   ;; Add defined variable
+   ((memq (car form) '(defvar defconst defcustom))
+    (setq env (elint-env-add-var env (cadr form))))
+   ;; Add function
+   ((memq (car form) '(defun defsubst))
+    (setq env (elint-env-add-func env (cadr form) (nth 2 form))))
+   ;; FIXME needs a handler to say second arg is not a variable when we come
+   ;; to scan the form.
+   ((eq (car form) 'define-derived-mode)
+    (setq env (elint-env-add-func env (cadr form) ())
+	  env (elint-env-add-var env (cadr form))
+	  env (elint-env-add-var env (intern (format "%s-map" (cadr form))))))
+   ((eq (car form) 'define-minor-mode)
+    (setq env (elint-env-add-func env (cadr form) '(&optional arg))
+	  ;; FIXME mode map?
+	  env (elint-env-add-var env (cadr form))))
+   ((and (eq (car form) 'easy-menu-define)
+	 (cadr form))
+    (setq env (elint-env-add-func env (cadr form) '(event))
+	  env (elint-env-add-var env (cadr form))))
+   ;; FIXME it would be nice to check the autoloads are correct.
+   ((eq (car form) 'autoload)
+    (setq env (elint-env-add-func env (cadr (cadr form)) 'unknown)))
+   ((eq (car form) 'declare-function)
+    (setq env (elint-env-add-func env (cadr form)
+				  (if (or (< (length form) 4)
+					  (eq (nth 3 form) t))
+				      'unknown
+				    (nth 3 form)))))
+   ((and (eq (car form) 'defalias) (listp (nth 2 form)))
+    ;; If the alias points to something already in the environment,
+    ;; add the alias to the environment with the same arguments.
+    ;; FIXME symbol-function, eg backquote.el?
+    (let ((def (elint-env-find-func env (cadr (nth 2 form)))))
+      (setq env (elint-env-add-func env (cadr (cadr form))
+				    (if def (cadr def) 'unknown)))))
+   ;; Add macro, both as a macro and as a function
+   ((eq (car form) 'defmacro)
+    (setq env (elint-env-add-macro env (cadr form)
+				   (cons 'lambda (cddr form)))
+	  env (elint-env-add-func env (cadr form) (nth 2 form))))
+   ((and (eq (car form) 'put)
+	 (= 4 (length form))
+	 (eq (car-safe (cadr form)) 'quote)
+	 (equal (nth 2 form) '(quote error-conditions)))
+    (set (make-local-variable 'elint-extra-errors)
+	 (cons (cadr (cadr form)) elint-extra-errors)))
+   ((eq (car form) 'provide)
+    (add-to-list 'elint-features (eval (cadr form))))
+   ;; Import variable definitions
+   ((memq (car form) '(require cc-require cc-require-when-compile))
+    (let ((name (eval (cadr form)))
+	  (file (eval (nth 2 form)))
+	  (elint-doing-cl (bound-and-true-p elint-doing-cl)))
+      (unless (memq name elint-features)
+	(add-to-list 'elint-features name)
+	;; cl loads cl-macs in an opaque manner.
+	;; Since cl-macs requires cl, we can just process cl-macs.
+	(and (eq name 'cl) (not elint-doing-cl)
+	     ;; We need cl if elint-form is to be able to expand cl macros.
+	     (require 'cl)
+	     (setq name 'cl-macs
+		   file nil
+		   elint-doing-cl t)) ; blech
+	(setq env (elint-add-required-env env name file))))))
+  env)
+
 (defun elint-init-env (forms)
   "Initialize the environment from FORMS."
   (let ((env (elint-make-env))
@@ -297,45 +470,14 @@
     (while forms
       (setq form (elint-top-form-form (car forms))
 	    forms (cdr forms))
-      (cond
-       ;; Eg nnmaildir seems to use [] as a form of comment syntax.
-       ((not (listp form))
-	(elint-error "Skipping non-list form `%s'" form))
-       ;; Add defined variable
-       ((memq (car form) '(defvar defconst defcustom))
-	(setq env (elint-env-add-var env (cadr form))))
-       ;; Add function
-       ((memq (car form) '(defun defsubst))
-	(setq env (elint-env-add-func env (cadr form) (nth 2 form))))
-       ((eq (car form) 'define-derived-mode)
-	(setq env (elint-env-add-func env (cadr form) ())
-	      env (elint-env-add-var env (cadr form))))
-       ;; FIXME it would be nice to check the autoloads are correct.
-       ((eq (car form) 'autoload)
-	(setq env (elint-env-add-func env (cadr (cadr form)) 'unknown)))
-       ((eq (car form) 'declare-function)
-	(setq env (elint-env-add-func env (cadr form)
-				      (if (or (< (length form) 4)
-					      (eq (nth 3 form) t))
-					'unknown
-					(nth 3 form)))))
-       ((and (eq (car form) 'defalias) (listp (nth 2 form)))
-	;; If the alias points to something already in the environment,
-	;; add the alias to the environment with the same arguments.
-	(let ((def (elint-env-find-func env (cadr (nth 2 form)))))
-	  ;; FIXME warn if the alias target is unknown.
-	  (setq env (elint-env-add-func env (cadr (cadr form))
-					(if def (cadr def) 'unknown)))))
-       ;; Add macro, both as a macro and as a function
-       ((eq (car form) 'defmacro)
-	(setq env (elint-env-add-macro env (cadr form)
-				       (cons 'lambda (cddr form)))
-	      env (elint-env-add-func env (cadr form) (nth 2 form))))
-       ;; Import variable definitions
-       ((eq (car form) 'require)
-	(let ((name (eval (cadr form)))
-	      (file (eval (nth 2 form))))
-	  (setq env (elint-add-required-env env name file))))))
+      ;; FIXME eval-when-compile should be treated differently (macros).
+      ;; Could bind something that makes elint-init-form only check
+      ;; defmacros.
+      (if (memq (car-safe form)
+		'(eval-and-compile eval-when-compile progn prog1 prog2
+				   with-no-warnings))
+	  (mapc 'elint-init-form (cdr form))
+	(elint-init-form form)))
     env))
 
 (defun elint-add-required-env (env name file)
@@ -349,15 +491,16 @@
 	     (lib1 (locate-library (concat libname ".el") t))
 	     (lib (or lib1 (locate-library libname t))))
 	;; Clear the messages :-/
-	(message nil)
+	;; (Messes up the "Initializing elint..." message.)
+;;;	(message nil)
 	(if lib
 	    (save-excursion
 	      ;; FIXME this doesn't use a temp buffer, because it
 	      ;; stores the result in buffer-local variables so that
 	      ;; it can be reused.
- 	      (set-buffer (find-file-noselect lib))
- 	      (elint-update-env)
- 	      (setq env (elint-env-add-env env elint-buffer-env)))
+	      (set-buffer (find-file-noselect lib))
+	      (elint-update-env)
+	      (setq env (elint-env-add-env env elint-buffer-env)))
 	      ;;; (with-temp-buffer
 	      ;;; 	(insert-file-contents lib)
 	      ;;; 	(with-syntax-table emacs-lisp-mode-syntax-table
@@ -391,10 +534,12 @@
     (let* . elint-check-let-form)
     (setq . elint-check-setq-form)
     (quote . elint-check-quote-form)
+    (function . elint-check-quote-form)
     (cond . elint-check-cond-form)
     (lambda . elint-check-defun-form)
     (function . elint-check-function-form)
     (setq-default . elint-check-setq-form)
+    (defalias . elint-check-defalias-form)
     (defun . elint-check-defun-form)
     (defsubst . elint-check-defun-form)
     (defmacro . elint-check-defun-form)
@@ -402,16 +547,22 @@
     (defconst . elint-check-defvar-form)
     (defcustom . elint-check-defcustom-form)
     (macro . elint-check-macro-form)
-    (condition-case . elint-check-condition-case-form))
+    (condition-case . elint-check-condition-case-form)
+    (if . elint-check-conditional-form)
+    (when . elint-check-conditional-form)
+    (unless . elint-check-conditional-form)
+    (and . elint-check-conditional-form)
+    (or . elint-check-conditional-form))
   "Functions to call when some special form should be linted.")
 
-(defun elint-form (form env)
+(defun elint-form (form env &optional nohandler)
   "Lint FORM in the environment ENV.
-The environment created by the form is returned."
+Optional argument NOHANDLER non-nil means ignore `elint-special-forms'.
+Returns the environment created by the form."
   (cond
    ((consp form)
     (let ((func (cdr (assq (car form) elint-special-forms))))
-      (if func
+      (if (and func (not nohandler))
 	  ;; Special form
 	  (funcall func form env)
 
@@ -421,7 +572,8 @@
 	  (cond
 	   ((eq args 'undefined)
 	    (setq argsok nil)
-	    (elint-error "Call to undefined function: %s" func))
+	    (or (memq 'undefined-functions elint-ignored-warnings)
+		(elint-error "Call to undefined function: %s" func)))
 
 	   ((eq args 'unknown) nil)
 
@@ -436,7 +588,8 @@
 		      (elint-form
 		       (macroexpand form (elint-env-macro-env env)) env)
 		    (error
-		     (elint-error "Elint failed to expand macro: %s" func)
+		     (or (memq 'macro-expansion elint-ignored-warnings)
+			 (elint-error "Elint failed to expand macro: %s" func))
 		     env))
 		env)
 
@@ -453,9 +606,10 @@
 		(elint-forms (cdr form) env))))))))
    ((symbolp form)
     ;; :foo variables are quoted
-    (if (and (/= (aref (symbol-name form) 0) ?:)
-	     (elint-unbound-variable form env))
-	(elint-warning "Reference to unbound symbol: %s" form))
+    (and (/= (aref (symbol-name form) 0) ?:)
+	 (not (memq 'unbound-reference elint-ignored-warnings))
+	 (elint-unbound-variable form env)
+	 (elint-warning "Reference to unbound symbol: %s" form))
     env)
 
    (t env)))
@@ -470,9 +624,13 @@
     (elint-error "Elint failed to parse form: %s" forms)
     env))
 
+(defvar elint-bound-variable nil
+  "Name of a temporarily bound symbol.")
+
 (defun elint-unbound-variable (var env)
   "T if VAR is unbound in ENV."
   (not (or (memq var '(nil t))
+	   (eq var elint-bound-variable)
 	   (elint-env-find-var env var)
 	   (memq var elint-builtin-variables)
 	   (memq var elint-autoloaded-variables)
@@ -509,6 +667,9 @@
 	    t)))
     ok))
 
+(defvar elint-bound-function nil
+  "Name of a temporarily bound function symbol.")
+
 (defun elint-get-args (func env)
   "Find the args of FUNC in ENV.
 Returns `unknown' if we couldn't find arguments."
@@ -516,13 +677,15 @@
     (if f
 	(cadr f)
       (if (symbolp func)
-	  (if (fboundp func)
-	      (let ((fcode (indirect-function func)))
-		(if (subrp fcode)
-		    ;; FIXME builtins with no args have args = nil.
-		    (or (get func 'elint-args) 'unknown)
-		  (elint-find-args-in-code fcode)))
-	    'undefined)
+	  (if (eq func elint-bound-function)
+	      'unknown
+	    (if (fboundp func)
+		(let ((fcode (indirect-function func)))
+		  (if (subrp fcode)
+		      ;; FIXME builtins with no args have args = nil.
+		      (or (get func 'elint-args) 'unknown)
+		    (elint-find-args-in-code fcode)))
+	      'undefined))
 	(elint-find-args-in-code func)))))
 
 (defun elint-find-args-in-code (code)
@@ -543,10 +706,25 @@
 
 (defun elint-check-cond-form (form env)
   "Lint a cond FORM in ENV."
-  (dolist (f (cdr form) env)
+  (dolist (f (cdr form))
     (if (consp f)
-	(elint-forms f env)
-      (elint-error "cond clause should be a list: %s" f))))
+	(let ((test (car f)))
+	  (cond ((equal test '(featurep (quote xemacs))))
+		((equal test '(not (featurep (quote emacs)))))
+		;; FIXME (and (boundp 'foo)
+		((and (eq (car-safe test) 'fboundp)
+		      (= 2 (length test))
+		      (eq (car-safe (cadr test)) 'quote))
+		 (let ((elint-bound-function (cadr (cadr test))))
+		   (elint-forms f env)))
+		((and (eq (car-safe test) 'boundp)
+		      (= 2 (length test))
+		      (eq (car-safe (cadr test)) 'quote))
+		 (let ((elint-bound-variable (cadr (cadr test))))
+		   (elint-forms f env)))
+		(t (elint-forms f env))))
+      (elint-error "cond clause should be a list: %s" f)))
+  env)
 
 (defun elint-check-defun-form (form env)
   "Lint a defun/defmacro/lambda FORM in ENV."
@@ -557,12 +735,30 @@
 	(car form))
   (elint-forms (cdr form) env))
 
+(defun elint-check-defalias-form (form env)
+  "Lint a defalias FORM in ENV."
+  (let ((alias (cadr form))
+	(target (nth 2 form)))
+    (and (eq (car-safe alias) 'quote)
+	 (eq (car-safe target) 'quote)
+	 (eq (elint-get-args (cadr target) env) 'undefined)
+	 (elint-warning "Alias `%s' has unknown target `%s'"
+			(cadr alias) (cadr target))))
+  (elint-form form env t))
+
 (defun elint-check-let-form (form env)
   "Lint the let/let* FORM in ENV."
   (let ((varlist (cadr form)))
     (if (not varlist)
-	(progn
-	  (elint-error "Missing varlist in let: %s" form)
+	(if (> (length form) 2)
+	    ;; An empty varlist is not really an error.  Eg some cl macros
+	    ;; can expand to such a form.
+	    (progn
+	      (or (memq 'empty-let elint-ignored-warnings)
+		  (elint-warning "Empty varlist in let: %s" form))
+	      ;; Lint the body forms
+	      (elint-forms (cddr form) env))
+	  (elint-error "Malformed let: %s" form)
 	  env)
       ;; Check for (let (a (car b)) ...) type of error
       (if (and (= (length varlist) 2)
@@ -593,7 +789,8 @@
 (defun elint-check-setq-form (form env)
   "Lint the setq FORM in ENV."
   (or (= (mod (length form) 2) 1)
-      (elint-error "Missing value in setq: %s" form))
+      ;; (setq foo) is valid and equivalent to (setq foo nil).
+      (elint-warning "Missing value in setq: %s" form))
   (let ((newenv env)
 	sym val)
     (setq form (cdr form))
@@ -602,8 +799,9 @@
 	    val (car (cdr form))
 	    form (cdr (cdr form)))
       (if (symbolp sym)
-	  (if (elint-unbound-variable sym newenv)
-	      (elint-warning "Setting previously unbound symbol: %s" sym))
+	  (and (not (memq 'unbound-assignment elint-ignored-warnings))
+	       (elint-unbound-variable sym newenv)
+	       (elint-warning "Setting previously unbound symbol: %s" sym))
 	(elint-error "Setting non-symbol in setq: %s" sym))
       (elint-form val newenv)
       (if (symbolp sym)
@@ -614,7 +812,8 @@
   "Lint the defvar/defconst FORM in ENV."
   (if (or (= (length form) 2)
 	  (= (length form) 3)
-	  (and (= (length form) 4) (stringp (nth 3 form))))
+	  ;; Eg the defcalcmodevar macro can expand with a nil doc-string.
+	  (and (= (length form) 4) (string-or-null-p (nth 3 form))))
           (elint-env-add-global-var (elint-form (nth 2 form) env)
 			     (car (cdr form)))
     (elint-error "Malformed variable declaration: %s" form)
@@ -636,6 +835,8 @@
     (cond
      ((symbolp func)
       (or (elint-env-find-func env func)
+	  ;; FIXME potentially bogus, since it uses the current
+	  ;; environment rather than a clean one.
 	  (fboundp func)
 	  (elint-warning "Reference to undefined function: %s" form))
       env)
@@ -680,6 +881,72 @@
 	  (elint-forms (cdr err) newenv))))
     resenv))
 
+;; For the featurep parts, an alternative is to have
+;; elint-get-top-forms skip the irrelevant branches.
+(defun elint-check-conditional-form (form env)
+  "Check the when/unless/and/or FORM in ENV.
+Does basic handling of `featurep' tests."
+  (let ((func (car form))
+	(test (cadr form))
+	sym)
+    ;; Misses things like (and t (featurep 'xemacs))
+    ;; Check byte-compile-maybe-guarded.
+    (cond ((and (memq func '(when and))
+		(eq (car-safe test) 'boundp)
+		(= 2 (length test))
+		(eq (car-safe (cadr test)) 'quote))
+	   ;; Cf elint-check-let-form, which modifies the whole ENV.
+	   (let ((elint-bound-variable (cadr (cadr test))))
+	     (elint-form form env t)))
+	  ((and (memq func '(when and))
+		(eq (car-safe test) 'fboundp)
+		(= 2 (length test))
+		(eq (car-safe (cadr test)) 'quote))
+	   (let ((elint-bound-function (cadr (cadr test))))
+	     (elint-form form env t)))
+	  ;; Let's not worry about (if (not (boundp...
+	  ((and (eq func 'if)
+		(eq (car-safe test) 'boundp)
+		(= 2 (length test))
+		(eq (car-safe (cadr test)) 'quote))
+	   (let ((elint-bound-variable (cadr (cadr test))))
+	     (elint-form (nth 2 form) env))
+	   (dolist (f (nthcdr 3 form))
+	     (elint-form f env)))
+	  ((and (eq func 'if)
+		(eq (car-safe test) 'fboundp)
+		(= 2 (length test))
+		(eq (car-safe (cadr test)) 'quote))
+	   (let ((elint-bound-function (cadr (cadr test))))
+	     (elint-form (nth 2 form) env))
+	   (dolist (f (nthcdr 3 form))
+	     (elint-form f env)))
+	  ((and (memq func '(when and))	; skip all
+		(or (null test)
+		    (member test '((featurep (quote xemacs))
+				   (not (featurep (quote emacs)))))
+		    (and (eq (car-safe test) 'and)
+			 (equal (car-safe (cdr test))
+				'(featurep (quote xemacs)))))))
+	  ((and (memq func '(unless or))
+		(equal test '(featurep (quote emacs)))))
+	  ((and (eq func 'if)
+		(or (null test)	      ; eg custom-browse-insert-prefix
+		    (member test '((featurep (quote xemacs))
+				   (not (featurep (quote emacs)))))
+		    (and (eq (car-safe test) 'and)
+			 (equal (car-safe (cdr test))
+				'(featurep (quote xemacs))))))
+	   (dolist (f (nthcdr 3 form))
+	     (elint-form f env)))	; lint the else branch
+	  ((and (eq func 'if)
+		(equal test '(featurep (quote emacs))))
+	   (elint-form (nth 2 form) env)) ; lint the if branch
+	  ;; Process conditional as normal, without handler.
+	  (t
+	   (elint-form form env t))))
+  env)
+
 ;;;
 ;;; Message functions
 ;;;
@@ -779,6 +1046,13 @@
 ;;; Initializing code
 ;;;
 
+(defun elint-put-function-args (func args)
+  "Mark function FUNC as having argument list ARGS."
+  (and (symbolp func)
+       args
+       (not (eq args 'unknown))
+       (put func 'elint-args args)))
+
 ;;;###autoload
 (defun elint-initialize (&optional reinit)
   "Initialize elint.
@@ -788,23 +1062,29 @@
   (if (and elint-builtin-variables (not reinit))
       (message "Elint is already initialized")
     (message "Initializing elint...")
-    (setq elint-builtin-variables (elint-find-builtin-variables)
+    (setq elint-builtin-variables (elint-scan-doc-file)
 	  elint-autoloaded-variables (elint-find-autoloaded-variables))
-    (mapc (lambda (x) (or (not (symbolp (car x)))
-			  (eq (cdr x) 'unknown)
-			  (put (car x) 'elint-args (cdr x))))
+    (mapc (lambda (x) (elint-put-function-args (car x) (cdr x)))
 	  (elint-find-builtin-args))
     (if elint-unknown-builtin-args
-	(mapc (lambda (x) (put (car x) 'elint-args (cdr x)))
+	(mapc (lambda (x) (elint-put-function-args (car x) (cdr x)))
 	      elint-unknown-builtin-args))
+    (when elint-scan-preloaded
+      (dolist (lib preloaded-file-list)
+	;; Skip files that contain nothing of use to us.
+	(unless (string-match elint-preloaded-skip-re lib)
+	  (setq elint-preloaded-env
+		(elint-add-required-env elint-preloaded-env nil lib)))))
     (message "Initializing elint...done")))
 
 
-(defun elint-find-builtin-variables ()
-  "Return a list of all built-in variables."
+;; This includes all the built-in and dumped things with documentation.
+(defun elint-scan-doc-file ()
+  "Scan the DOC file for function and variables.
+Marks the function wih their arguments, and returns a list of variables."
   ;; Cribbed from help-fns.el.
   (let ((docbuf " *DOC*")
-	vars var)
+	vars sym args)
     (save-excursion
       (if (get-buffer docbuf)
 	  (progn
@@ -813,12 +1093,25 @@
 	(set-buffer (get-buffer-create docbuf))
 	(insert-file-contents-literally
 	 (expand-file-name internal-doc-file-name doc-directory)))
-      (while (search-forward "V" nil t)
-	(and (setq var (intern-soft
-			(buffer-substring (point) (line-end-position))))
-	     (boundp var)
-	     (setq vars (cons var vars))))
-      vars)))
+      (while (re-search-forward "\\([VF]\\)" nil t)
+	(when (setq sym (intern-soft (buffer-substring (point)
+						       (line-end-position))))
+	  (if (string-equal (match-string 1) "V")
+	      ;; Excludes platform-specific stuff not relevant to the
+	      ;; running platform.
+	      (if (boundp sym) (setq vars (cons sym vars)))
+	    ;; Function.
+	    (when (fboundp sym)
+	      (when (re-search-forward "\\(^(fn.*)\\)?" nil t)
+		(backward-char 1)
+		;; FIXME distinguish no args from not found.
+		(and (setq args (match-string 1))
+		     (setq args
+			   (ignore-errors
+			     (read
+			      (replace-regexp-in-string "^(fn ?" "(" args))))
+		     (elint-put-function-args sym args))))))))
+    vars))
 
 (defun elint-find-autoloaded-variables ()
   "Return a list of all autoloaded variables."