Commit | Line | Data |
---|---|---|
e8af40ee | 1 | ;;; elint.el --- Lint Emacs Lisp |
020c3567 | 2 | |
ba318903 | 3 | ;; Copyright (C) 1997, 2001-2014 Free Software Foundation, Inc. |
020c3567 RS |
4 | |
5 | ;; Author: Peter Liljenberg <petli@lysator.liu.se> | |
6 | ;; Created: May 1997 | |
7 | ;; Keywords: lisp | |
8 | ||
9 | ;; This file is part of GNU Emacs. | |
10 | ||
d6cba7ae | 11 | ;; GNU Emacs is free software: you can redistribute it and/or modify |
020c3567 | 12 | ;; it under the terms of the GNU General Public License as published by |
d6cba7ae GM |
13 | ;; the Free Software Foundation, either version 3 of the License, or |
14 | ;; (at your option) any later version. | |
020c3567 RS |
15 | |
16 | ;; GNU Emacs is distributed in the hope that it will be useful, | |
17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
19 | ;; GNU General Public License for more details. | |
20 | ||
21 | ;; You should have received a copy of the GNU General Public License | |
d6cba7ae | 22 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. |
020c3567 RS |
23 | |
24 | ;;; Commentary: | |
25 | ||
e2d5a67f GM |
26 | ;; This is a linter for Emacs Lisp. Currently, it mainly catches |
27 | ;; misspellings and undefined variables, although it can also catch | |
020c3567 RS |
28 | ;; function calls with the wrong number of arguments. |
29 | ||
7a8ae964 GM |
30 | ;; To use, call elint-current-buffer or elint-defun to lint a buffer |
31 | ;; or defun. The first call runs `elint-initialize' to set up some | |
32 | ;; argument data, which may take a while. | |
020c3567 RS |
33 | |
34 | ;; The linter will try to "include" any require'd libraries to find | |
e2d5a67f | 35 | ;; the variables defined in those. There is a fair amount of voodoo |
020c3567 RS |
36 | ;; involved in this, but it seems to work in normal situations. |
37 | ||
020c3567 RS |
38 | ;;; To do: |
39 | ||
020c3567 | 40 | ;; * Adding type checking. (Stop that sniggering!) |
76f2c576 GM |
41 | ;; * Make eval-when-compile be sensitive to the difference between |
42 | ;; funcs and macros. | |
6974be68 | 43 | ;; * Requires within function bodies. |
76f2c576 GM |
44 | ;; * Handle defstruct. |
45 | ;; * Prevent recursive requires. | |
020c3567 RS |
46 | |
47 | ;;; Code: | |
48 | ||
ef501ef0 SM |
49 | (require 'help-fns) |
50 | ||
76f2c576 GM |
51 | (defgroup elint nil |
52 | "Linting for Emacs Lisp." | |
53 | :prefix "elint-" | |
54 | :group 'maint) | |
55 | ||
56 | (defcustom elint-log-buffer "*Elint*" | |
57 | "The buffer in which to log lint messages." | |
58 | :type 'string | |
59 | :safe 'stringp | |
60 | :group 'elint) | |
61 | ||
62 | (defcustom elint-scan-preloaded t | |
63 | "Non-nil means to scan `preloaded-file-list' when initializing. | |
64 | Otherwise, just scan the DOC file for functions and variables. | |
65 | This is faster, but less accurate, since it misses undocumented features. | |
66 | This may result in spurious warnings about unknown functions, etc." | |
67 | :type 'boolean | |
68 | :safe 'booleanp | |
69 | :group 'elint | |
70 | :version "23.2") | |
71 | ||
72 | (defcustom elint-ignored-warnings nil | |
73 | "If non-nil, a list of issue types that Elint should ignore. | |
74 | This is useful if Elint has trouble understanding your code and | |
75 | you need to suppress lots of spurious warnings. The valid list elements | |
76 | are as follows, and suppress messages about the indicated features: | |
77 | undefined-functions - calls to unknown functions | |
78 | unbound-reference - reference to unknown variables | |
79 | unbound-assignment - assignment to unknown variables | |
80 | macro-expansions - failure to expand macros | |
81 | empty-let - let-bindings with empty variable lists" | |
82 | :type '(choice (const :tag "Don't suppress any warnings" nil) | |
83 | (repeat :tag "List of issues to ignore" | |
84 | (choice (const undefined-functions | |
85 | :tag "Calls to unknown functions") | |
86 | (const unbound-reference | |
87 | :tag "Reference to unknown variables") | |
88 | (const unbound-assignment | |
89 | :tag "Assignment to unknown variables") | |
90 | (const macro-expansion | |
91 | :tag "Failure to expand macros") | |
92 | (const empty-let | |
93 | :tag "Let-binding with empty varlist")))) | |
94 | :safe (lambda (value) (or (null value) | |
95 | (and (listp value) | |
96 | (equal value | |
97 | (mapcar | |
98 | (lambda (e) | |
99 | (if (memq e | |
100 | '(undefined-functions | |
101 | unbound-reference | |
102 | unbound-assignment | |
103 | macro-expansion | |
104 | empty-let)) | |
105 | e)) | |
106 | value))))) | |
107 | :version "23.2" | |
108 | :group 'elint) | |
109 | ||
110 | (defcustom elint-directory-skip-re "\\(ldefs-boot\\|loaddefs\\)\\.el\\'" | |
111 | "If nil, a regexp matching files to skip when linting a directory." | |
112 | :type '(choice (const :tag "Lint all files" nil) | |
113 | (regexp :tag "Regexp to skip")) | |
114 | :safe 'string-or-null-p | |
115 | :group 'elint | |
116 | :version "23.2") | |
020c3567 | 117 | |
8e2eba09 RS |
118 | ;;; |
119 | ;;; Data | |
120 | ;;; | |
121 | ||
d501801e | 122 | (defconst elint-standard-variables |
a61344d6 GM |
123 | ;; Most of these are defined in C with no documentation. |
124 | ;; FIXME I don't see why they shouldn't just get doc-strings. | |
d501801e GM |
125 | '(vc-mode local-write-file-hooks activate-menubar-hook buffer-name-history |
126 | coding-system-history extended-command-history | |
d501801e | 127 | yes-or-no-p-history) |
a61344d6 GM |
128 | "Standard variables, excluding `elint-builtin-variables'. |
129 | These are variables that we cannot detect automatically for some reason.") | |
e2d5a67f GM |
130 | |
131 | (defvar elint-builtin-variables nil | |
76f2c576 GM |
132 | "List of built-in variables. Set by `elint-initialize'. |
133 | This is actually all those documented in the DOC file, which includes | |
134 | built-in variables and those from dumped Lisp files.") | |
e2d5a67f GM |
135 | |
136 | (defvar elint-autoloaded-variables nil | |
137 | "List of `loaddefs.el' variables. Set by `elint-initialize'.") | |
138 | ||
76f2c576 GM |
139 | (defvar elint-preloaded-env nil |
140 | "Environment defined by the preloaded (dumped) Lisp files. | |
141 | Set by `elint-initialize', if `elint-scan-preloaded' is non-nil.") | |
e2d5a67f | 142 | |
76f2c576 GM |
143 | (defconst elint-unknown-builtin-args |
144 | ;; encode-time allows extra arguments for use with decode-time. | |
145 | ;; For some reason, some people seem to like to use them in other cases. | |
146 | '((encode-time second minute hour day month year &rest zone)) | |
e2d5a67f GM |
147 | "Those built-ins for which we can't find arguments, if any.") |
148 | ||
76f2c576 | 149 | (defvar elint-extra-errors '(file-locked file-supersession ftp-error) |
fa5f7c5f | 150 | "Errors without `error-message' or `error-conditions' properties.") |
8e2eba09 | 151 | |
76f2c576 GM |
152 | (defconst elint-preloaded-skip-re |
153 | (regexp-opt '("loaddefs.el" "loadup.el" "cus-start" "language/" | |
154 | "eucjp-ms" "mule-conf" "/characters" "/charprop" | |
155 | "cp51932")) | |
156 | "Regexp matching elements of `preloaded-file-list' to ignore. | |
157 | We ignore them because they contain no definitions of use to Elint.") | |
158 | ||
020c3567 RS |
159 | ;;; |
160 | ;;; ADT: top-form | |
161 | ;;; | |
162 | ||
163 | (defsubst elint-make-top-form (form pos) | |
164 | "Create a top form. | |
165 | FORM is the form, and POS is the point where it starts in the buffer." | |
166 | (cons form pos)) | |
167 | ||
168 | (defsubst elint-top-form-form (top-form) | |
169 | "Extract the form from a TOP-FORM." | |
170 | (car top-form)) | |
171 | ||
172 | (defsubst elint-top-form-pos (top-form) | |
173 | "Extract the position from a TOP-FORM." | |
174 | (cdr top-form)) | |
175 | ||
176 | ;;; | |
177 | ;;; ADT: env | |
178 | ;;; | |
179 | ||
180 | (defsubst elint-make-env () | |
181 | "Create an empty environment." | |
182 | (list (list nil) nil nil)) | |
183 | ||
184 | (defsubst elint-env-add-env (env newenv) | |
185 | "Augment ENV with NEWENV. | |
186 | None of them is modified, and the new env is returned." | |
187 | (list (append (car env) (car newenv)) | |
e2d5a67f | 188 | (append (cadr env) (cadr newenv)) |
020c3567 RS |
189 | (append (car (cdr (cdr env))) (car (cdr (cdr newenv)))))) |
190 | ||
191 | (defsubst elint-env-add-var (env var) | |
192 | "Augment ENV with the variable VAR. | |
193 | The new environment is returned, the old is unmodified." | |
194 | (cons (cons (list var) (car env)) (cdr env))) | |
195 | ||
196 | (defsubst elint-env-add-global-var (env var) | |
197 | "Augment ENV with the variable VAR. | |
198 | ENV is modified so VAR is seen everywhere. | |
199 | ENV is returned." | |
200 | (nconc (car env) (list (list var))) | |
201 | env) | |
202 | ||
203 | (defsubst elint-env-find-var (env var) | |
204 | "Non-nil if ENV contains the variable VAR. | |
205 | Actually, a list with VAR as a single element is returned." | |
206 | (assq var (car env))) | |
207 | ||
208 | (defsubst elint-env-add-func (env func args) | |
209 | "Augment ENV with the function FUNC, which has the arguments ARGS. | |
210 | The new environment is returned, the old is unmodified." | |
211 | (list (car env) | |
e2d5a67f | 212 | (cons (list func args) (cadr env)) |
020c3567 RS |
213 | (car (cdr (cdr env))))) |
214 | ||
215 | (defsubst elint-env-find-func (env func) | |
216 | "Non-nil if ENV contains the function FUNC. | |
217 | Actually, a list of (FUNC ARGS) is returned." | |
e2d5a67f | 218 | (assq func (cadr env))) |
020c3567 RS |
219 | |
220 | (defsubst elint-env-add-macro (env macro def) | |
221 | "Augment ENV with the macro named MACRO. | |
222 | DEF is the macro definition (a lambda expression or similar). | |
223 | The new environment is returned, the old is unmodified." | |
224 | (list (car env) | |
e2d5a67f | 225 | (cadr env) |
020c3567 RS |
226 | (cons (cons macro def) (car (cdr (cdr env)))))) |
227 | ||
228 | (defsubst elint-env-macro-env (env) | |
229 | "Return the macro environment of ENV. | |
230 | This environment can be passed to `macroexpand'." | |
231 | (car (cdr (cdr env)))) | |
232 | ||
233 | (defsubst elint-env-macrop (env macro) | |
234 | "Non-nil if ENV contains MACRO." | |
235 | (assq macro (elint-env-macro-env env))) | |
236 | ||
237 | ;;; | |
238 | ;;; User interface | |
239 | ;;; | |
240 | ||
6974be68 GM |
241 | ;;;###autoload |
242 | (defun elint-file (file) | |
243 | "Lint the file FILE." | |
244 | (interactive "fElint file: ") | |
245 | (setq file (expand-file-name file)) | |
246 | (or elint-builtin-variables | |
247 | (elint-initialize)) | |
248 | (let ((dir (file-name-directory file))) | |
249 | (let ((default-directory dir)) | |
250 | (elint-display-log)) | |
251 | (elint-set-mode-line t) | |
252 | (with-current-buffer elint-log-buffer | |
253 | (unless (string-equal default-directory dir) | |
254 | (elint-log-message (format "\f\nLeaving directory `%s'" | |
255 | default-directory) t) | |
256 | (elint-log-message (format "Entering directory `%s'" dir) t) | |
257 | (setq default-directory dir)))) | |
258 | (let ((str (format "Linting file %s" file))) | |
259 | (message "%s..." str) | |
260 | (or noninteractive | |
261 | (elint-log-message (format "\f\n%s at %s" str (current-time-string)) t)) | |
262 | ;; elint-current-buffer clears log. | |
263 | (with-temp-buffer | |
264 | (insert-file-contents file) | |
bf01513f GM |
265 | (let ((buffer-file-name file) |
266 | (max-lisp-eval-depth (max 1000 max-lisp-eval-depth))) | |
6974be68 GM |
267 | (with-syntax-table emacs-lisp-mode-syntax-table |
268 | (mapc 'elint-top-form (elint-update-env))))) | |
269 | (elint-set-mode-line) | |
270 | (message "%s...done" str))) | |
271 | ||
272 | ;; cf byte-recompile-directory. | |
273 | ;;;###autoload | |
274 | (defun elint-directory (directory) | |
76f2c576 GM |
275 | "Lint all the .el files in DIRECTORY. |
276 | A complicated directory may require a lot of memory." | |
6974be68 GM |
277 | (interactive "DElint directory: ") |
278 | (let ((elint-running t)) | |
279 | (dolist (file (directory-files directory t)) | |
280 | ;; Bytecomp has emacs-lisp-file-regexp. | |
281 | (when (and (string-match "\\.el\\'" file) | |
282 | (file-readable-p file) | |
283 | (not (auto-save-file-name-p file))) | |
76f2c576 GM |
284 | (if (string-match elint-directory-skip-re file) |
285 | (message "Skipping file %s" file) | |
286 | (elint-file file))))) | |
6974be68 GM |
287 | (elint-set-mode-line)) |
288 | ||
7a8ae964 | 289 | ;;;###autoload |
020c3567 | 290 | (defun elint-current-buffer () |
7a8ae964 | 291 | "Lint the current buffer. |
fa5f7c5f | 292 | If necessary, this first calls `elint-initialize'." |
020c3567 | 293 | (interactive) |
7a8ae964 GM |
294 | (or elint-builtin-variables |
295 | (elint-initialize)) | |
e2d5a67f GM |
296 | (elint-clear-log (format "Linting %s" (or (buffer-file-name) |
297 | (buffer-name)))) | |
020c3567 | 298 | (elint-display-log) |
6974be68 | 299 | (elint-set-mode-line t) |
eb2b0009 | 300 | (mapc 'elint-top-form (elint-update-env)) |
53964682 | 301 | ;; Tell the user we're finished. This is terribly kludgy: we set |
6974be68 GM |
302 | ;; elint-top-form-logged so elint-log-message doesn't print the |
303 | ;; ** top form ** header... | |
304 | (elint-set-mode-line) | |
305 | (elint-log-message "\nLinting finished.\n" t)) | |
306 | ||
7f0fedda | 307 | |
7a8ae964 | 308 | ;;;###autoload |
020c3567 | 309 | (defun elint-defun () |
7a8ae964 | 310 | "Lint the function at point. |
fa5f7c5f | 311 | If necessary, this first calls `elint-initialize'." |
020c3567 | 312 | (interactive) |
7a8ae964 GM |
313 | (or elint-builtin-variables |
314 | (elint-initialize)) | |
020c3567 | 315 | (save-excursion |
e2d5a67f | 316 | (or (beginning-of-defun) (error "Lint what?")) |
020c3567 RS |
317 | (let ((pos (point)) |
318 | (def (read (current-buffer)))) | |
319 | (elint-display-log) | |
020c3567 RS |
320 | (elint-update-env) |
321 | (elint-top-form (elint-make-top-form def pos))))) | |
322 | ||
323 | ;;; | |
324 | ;;; Top form functions | |
325 | ;;; | |
326 | ||
327 | (defvar elint-buffer-env nil | |
fa5f7c5f | 328 | "The environment of an elisp buffer. |
020c3567 RS |
329 | Will be local in linted buffers.") |
330 | ||
331 | (defvar elint-buffer-forms nil | |
332 | "The top forms in a buffer. | |
333 | Will be local in linted buffers.") | |
334 | ||
335 | (defvar elint-last-env-time nil | |
336 | "The last time the buffers env was updated. | |
337 | Is measured in buffer-modified-ticks and is local in linted buffers.") | |
338 | ||
76f2c576 | 339 | ;; This is a minor optimization. It is local to every buffer, and so |
40ba43b4 | 340 | ;; does not prevent recursive requires. It does not list the requires |
76f2c576 GM |
341 | ;; of requires. |
342 | (defvar elint-features nil | |
343 | "List of all libraries this buffer has required, or that have been provided.") | |
344 | ||
020c3567 RS |
345 | (defun elint-update-env () |
346 | "Update the elint environment in the current buffer. | |
347 | Don't do anything if the buffer hasn't been changed since this | |
348 | function was called the last time. | |
349 | Returns the forms." | |
350 | (if (and (local-variable-p 'elint-buffer-env (current-buffer)) | |
351 | (local-variable-p 'elint-buffer-forms (current-buffer)) | |
352 | (local-variable-p 'elint-last-env-time (current-buffer)) | |
353 | (= (buffer-modified-tick) elint-last-env-time)) | |
354 | ;; Env is up to date | |
355 | elint-buffer-forms | |
356 | ;; Remake env | |
357 | (set (make-local-variable 'elint-buffer-forms) (elint-get-top-forms)) | |
76f2c576 | 358 | (set (make-local-variable 'elint-features) nil) |
020c3567 RS |
359 | (set (make-local-variable 'elint-buffer-env) |
360 | (elint-init-env elint-buffer-forms)) | |
76f2c576 | 361 | (if elint-preloaded-env |
61b108cc SM |
362 | ;; FIXME: This doesn't do anything! Should we setq the result to |
363 | ;; elint-buffer-env? | |
76f2c576 | 364 | (elint-env-add-env elint-preloaded-env elint-buffer-env)) |
020c3567 RS |
365 | (set (make-local-variable 'elint-last-env-time) (buffer-modified-tick)) |
366 | elint-buffer-forms)) | |
7f0fedda | 367 | |
020c3567 RS |
368 | (defun elint-get-top-forms () |
369 | "Collect all the top forms in the current buffer." | |
370 | (save-excursion | |
76f2c576 | 371 | (let (tops) |
020c3567 RS |
372 | (goto-char (point-min)) |
373 | (while (elint-find-next-top-form) | |
76f2c576 GM |
374 | (let ((elint-current-pos (point))) |
375 | ;; non-list check could be here too. errors may be out of seq. | |
376 | ;; quoted check cannot be elsewhere, since quotes skipped. | |
377 | (if (looking-back "'") | |
378 | ;; Eg cust-print.el uses ' as a comment syntax. | |
379 | (elint-warning "Skipping quoted form `'%.20s...'" | |
380 | (read (current-buffer))) | |
381 | (condition-case nil | |
382 | (setq tops (cons | |
383 | (elint-make-top-form (read (current-buffer)) | |
384 | elint-current-pos) | |
385 | tops)) | |
386 | (end-of-file | |
387 | (goto-char elint-current-pos) | |
388 | (error "Missing ')' in top form: %s" | |
389 | (buffer-substring elint-current-pos | |
390 | (line-end-position)))))))) | |
020c3567 RS |
391 | (nreverse tops)))) |
392 | ||
393 | (defun elint-find-next-top-form () | |
394 | "Find the next top form from point. | |
f0529b5b | 395 | Return nil if there are no more forms, t otherwise." |
020c3567 RS |
396 | (parse-partial-sexp (point) (point-max) nil t) |
397 | (not (eobp))) | |
398 | ||
79d1dabe | 399 | (defvar elint-env) ; from elint-init-env |
76f2c576 GM |
400 | |
401 | (defun elint-init-form (form) | |
79d1dabe | 402 | "Process FORM, adding to ELINT-ENV if recognized." |
76f2c576 GM |
403 | (cond |
404 | ;; Eg nnmaildir seems to use [] as a form of comment syntax. | |
405 | ((not (listp form)) | |
406 | (elint-warning "Skipping non-list form `%s'" form)) | |
407 | ;; Add defined variable | |
408 | ((memq (car form) '(defvar defconst defcustom)) | |
79d1dabe | 409 | (setq elint-env (elint-env-add-var elint-env (cadr form)))) |
76f2c576 GM |
410 | ;; Add function |
411 | ((memq (car form) '(defun defsubst)) | |
79d1dabe | 412 | (setq elint-env (elint-env-add-func elint-env (cadr form) (nth 2 form)))) |
76f2c576 GM |
413 | ;; FIXME needs a handler to say second arg is not a variable when we come |
414 | ;; to scan the form. | |
415 | ((eq (car form) 'define-derived-mode) | |
79d1dabe GM |
416 | (setq elint-env (elint-env-add-func elint-env (cadr form) ()) |
417 | elint-env (elint-env-add-var elint-env (cadr form)) | |
418 | elint-env (elint-env-add-var elint-env | |
419 | (intern (format "%s-map" (cadr form)))))) | |
76f2c576 | 420 | ((eq (car form) 'define-minor-mode) |
79d1dabe | 421 | (setq elint-env (elint-env-add-func elint-env (cadr form) '(&optional arg)) |
76f2c576 | 422 | ;; FIXME mode map? |
79d1dabe | 423 | elint-env (elint-env-add-var elint-env (cadr form)))) |
76f2c576 GM |
424 | ((and (eq (car form) 'easy-menu-define) |
425 | (cadr form)) | |
79d1dabe GM |
426 | (setq elint-env (elint-env-add-func elint-env (cadr form) '(event)) |
427 | elint-env (elint-env-add-var elint-env (cadr form)))) | |
76f2c576 GM |
428 | ;; FIXME it would be nice to check the autoloads are correct. |
429 | ((eq (car form) 'autoload) | |
79d1dabe | 430 | (setq elint-env (elint-env-add-func elint-env (cadr (cadr form)) 'unknown))) |
76f2c576 | 431 | ((eq (car form) 'declare-function) |
79d1dabe GM |
432 | (setq elint-env (elint-env-add-func |
433 | elint-env (cadr form) | |
494f4fc7 GM |
434 | (if (or (< (length form) 4) |
435 | (eq (nth 3 form) t) | |
436 | (unless (stringp (nth 2 form)) | |
437 | (elint-error "Malformed declaration for `%s'" | |
438 | (cadr form)) | |
439 | t)) | |
440 | 'unknown | |
441 | (nth 3 form))))) | |
76f2c576 GM |
442 | ((and (eq (car form) 'defalias) (listp (nth 2 form))) |
443 | ;; If the alias points to something already in the environment, | |
444 | ;; add the alias to the environment with the same arguments. | |
445 | ;; FIXME symbol-function, eg backquote.el? | |
79d1dabe GM |
446 | (let ((def (elint-env-find-func elint-env (cadr (nth 2 form))))) |
447 | (setq elint-env (elint-env-add-func elint-env (cadr (cadr form)) | |
76f2c576 GM |
448 | (if def (cadr def) 'unknown))))) |
449 | ;; Add macro, both as a macro and as a function | |
450 | ((eq (car form) 'defmacro) | |
79d1dabe | 451 | (setq elint-env (elint-env-add-macro elint-env (cadr form) |
76f2c576 | 452 | (cons 'lambda (cddr form))) |
79d1dabe | 453 | elint-env (elint-env-add-func elint-env (cadr form) (nth 2 form)))) |
76f2c576 GM |
454 | ((and (eq (car form) 'put) |
455 | (= 4 (length form)) | |
456 | (eq (car-safe (cadr form)) 'quote) | |
457 | (equal (nth 2 form) '(quote error-conditions))) | |
458 | (set (make-local-variable 'elint-extra-errors) | |
459 | (cons (cadr (cadr form)) elint-extra-errors))) | |
460 | ((eq (car form) 'provide) | |
461 | (add-to-list 'elint-features (eval (cadr form)))) | |
462 | ;; Import variable definitions | |
463 | ((memq (car form) '(require cc-require cc-require-when-compile)) | |
464 | (let ((name (eval (cadr form))) | |
465 | (file (eval (nth 2 form))) | |
466 | (elint-doing-cl (bound-and-true-p elint-doing-cl))) | |
467 | (unless (memq name elint-features) | |
468 | (add-to-list 'elint-features name) | |
469 | ;; cl loads cl-macs in an opaque manner. | |
470 | ;; Since cl-macs requires cl, we can just process cl-macs. | |
a464a6c7 SM |
471 | ;; FIXME: AFAIK, `cl' now behaves properly and does not need any |
472 | ;; special treatment any more. Can someone who understands this | |
473 | ;; code confirm? --Stef | |
76f2c576 GM |
474 | (and (eq name 'cl) (not elint-doing-cl) |
475 | ;; We need cl if elint-form is to be able to expand cl macros. | |
476 | (require 'cl) | |
477 | (setq name 'cl-macs | |
478 | file nil | |
479 | elint-doing-cl t)) ; blech | |
79d1dabe GM |
480 | (setq elint-env (elint-add-required-env elint-env name file)))))) |
481 | elint-env) | |
76f2c576 | 482 | |
020c3567 | 483 | (defun elint-init-env (forms) |
1936c9a9 | 484 | "Initialize the environment from FORMS." |
79d1dabe | 485 | (let ((elint-env (elint-make-env)) |
020c3567 RS |
486 | form) |
487 | (while forms | |
488 | (setq form (elint-top-form-form (car forms)) | |
489 | forms (cdr forms)) | |
76f2c576 GM |
490 | ;; FIXME eval-when-compile should be treated differently (macros). |
491 | ;; Could bind something that makes elint-init-form only check | |
492 | ;; defmacros. | |
493 | (if (memq (car-safe form) | |
494 | '(eval-and-compile eval-when-compile progn prog1 prog2 | |
495 | with-no-warnings)) | |
496 | (mapc 'elint-init-form (cdr form)) | |
497 | (elint-init-form form))) | |
79d1dabe | 498 | elint-env)) |
020c3567 RS |
499 | |
500 | (defun elint-add-required-env (env name file) | |
e2d5a67f | 501 | "Augment ENV with the variables defined by feature NAME in FILE." |
4cf8971b | 502 | (condition-case err |
020c3567 RS |
503 | (let* ((libname (if (stringp file) |
504 | file | |
505 | (symbol-name name))) | |
506 | ||
507 | ;; First try to find .el files, then the raw name | |
508 | (lib1 (locate-library (concat libname ".el") t)) | |
e2d5a67f | 509 | (lib (or lib1 (locate-library libname t)))) |
020c3567 | 510 | ;; Clear the messages :-/ |
76f2c576 GM |
511 | ;; (Messes up the "Initializing elint..." message.) |
512 | ;;; (message nil) | |
020c3567 | 513 | (if lib |
de3a1fe9 | 514 | (with-current-buffer (find-file-noselect lib) |
d62e5bf2 GM |
515 | ;; FIXME this doesn't use a temp buffer, because it |
516 | ;; stores the result in buffer-local variables so that | |
517 | ;; it can be reused. | |
76f2c576 GM |
518 | (elint-update-env) |
519 | (setq env (elint-env-add-env env elint-buffer-env))) | |
d62e5bf2 GM |
520 | ;;; (with-temp-buffer |
521 | ;;; (insert-file-contents lib) | |
522 | ;;; (with-syntax-table emacs-lisp-mode-syntax-table | |
523 | ;;; (elint-update-env)) | |
524 | ;;; (setq env (elint-env-add-env env elint-buffer-env)))) | |
6974be68 | 525 | ;;(message "Elint processed (require '%s)" name)) |
4cf8971b | 526 | (error "%s.el not found in load-path" libname))) |
020c3567 | 527 | (error |
4cf8971b KR |
528 | (message "Can't get variables from require'd library %s: %s" |
529 | name (error-message-string err)))) | |
020c3567 | 530 | env) |
7f0fedda | 531 | |
020c3567 RS |
532 | (defvar elint-top-form nil |
533 | "The currently linted top form, or nil.") | |
534 | ||
535 | (defvar elint-top-form-logged nil | |
fa5f7c5f | 536 | "The value t if the currently linted top form has been mentioned in the log buffer.") |
020c3567 RS |
537 | |
538 | (defun elint-top-form (form) | |
539 | "Lint a top FORM." | |
540 | (let ((elint-top-form form) | |
e2d5a67f GM |
541 | (elint-top-form-logged nil) |
542 | (elint-current-pos (elint-top-form-pos form))) | |
020c3567 RS |
543 | (elint-form (elint-top-form-form form) elint-buffer-env))) |
544 | ||
545 | ;;; | |
546 | ;;; General form linting functions | |
547 | ;;; | |
548 | ||
549 | (defconst elint-special-forms | |
550 | '((let . elint-check-let-form) | |
551 | (let* . elint-check-let-form) | |
552 | (setq . elint-check-setq-form) | |
553 | (quote . elint-check-quote-form) | |
76f2c576 | 554 | (function . elint-check-quote-form) |
020c3567 RS |
555 | (cond . elint-check-cond-form) |
556 | (lambda . elint-check-defun-form) | |
557 | (function . elint-check-function-form) | |
558 | (setq-default . elint-check-setq-form) | |
76f2c576 | 559 | (defalias . elint-check-defalias-form) |
020c3567 RS |
560 | (defun . elint-check-defun-form) |
561 | (defsubst . elint-check-defun-form) | |
562 | (defmacro . elint-check-defun-form) | |
563 | (defvar . elint-check-defvar-form) | |
564 | (defconst . elint-check-defvar-form) | |
7f0fedda | 565 | (defcustom . elint-check-defcustom-form) |
020c3567 | 566 | (macro . elint-check-macro-form) |
76f2c576 GM |
567 | (condition-case . elint-check-condition-case-form) |
568 | (if . elint-check-conditional-form) | |
569 | (when . elint-check-conditional-form) | |
570 | (unless . elint-check-conditional-form) | |
571 | (and . elint-check-conditional-form) | |
572 | (or . elint-check-conditional-form)) | |
020c3567 | 573 | "Functions to call when some special form should be linted.") |
7f0fedda | 574 | |
76f2c576 | 575 | (defun elint-form (form env &optional nohandler) |
020c3567 | 576 | "Lint FORM in the environment ENV. |
76f2c576 GM |
577 | Optional argument NOHANDLER non-nil means ignore `elint-special-forms'. |
578 | Returns the environment created by the form." | |
020c3567 RS |
579 | (cond |
580 | ((consp form) | |
581 | (let ((func (cdr (assq (car form) elint-special-forms)))) | |
76f2c576 | 582 | (if (and func (not nohandler)) |
020c3567 RS |
583 | ;; Special form |
584 | (funcall func form env) | |
585 | ||
586 | (let* ((func (car form)) | |
587 | (args (elint-get-args func env)) | |
588 | (argsok t)) | |
589 | (cond | |
590 | ((eq args 'undefined) | |
591 | (setq argsok nil) | |
76f2c576 GM |
592 | (or (memq 'undefined-functions elint-ignored-warnings) |
593 | (elint-error "Call to undefined function: %s" func))) | |
7f0fedda | 594 | |
020c3567 | 595 | ((eq args 'unknown) nil) |
7f0fedda | 596 | |
020c3567 RS |
597 | (t (setq argsok (elint-match-args form args)))) |
598 | ||
599 | ;; Is this a macro? | |
600 | (if (elint-env-macrop env func) | |
601 | ;; Macro defined in buffer, expand it | |
602 | (if argsok | |
e2d5a67f GM |
603 | ;; FIXME error if macro uses macro, eg bytecomp.el. |
604 | (condition-case nil | |
605 | (elint-form | |
606 | (macroexpand form (elint-env-macro-env env)) env) | |
607 | (error | |
76f2c576 GM |
608 | (or (memq 'macro-expansion elint-ignored-warnings) |
609 | (elint-error "Elint failed to expand macro: %s" func)) | |
6974be68 | 610 | env)) |
020c3567 RS |
611 | env) |
612 | ||
613 | (let ((fcode (if (symbolp func) | |
614 | (if (fboundp func) | |
e2d5a67f | 615 | (indirect-function func)) |
020c3567 RS |
616 | func))) |
617 | (if (and (listp fcode) (eq (car fcode) 'macro)) | |
618 | ;; Macro defined outside buffer | |
619 | (if argsok | |
620 | (elint-form (macroexpand form) env) | |
621 | env) | |
622 | ;; Function, lint its parameters | |
e2d5a67f | 623 | (elint-forms (cdr form) env)))))))) |
020c3567 RS |
624 | ((symbolp form) |
625 | ;; :foo variables are quoted | |
76f2c576 GM |
626 | (and (/= (aref (symbol-name form) 0) ?:) |
627 | (not (memq 'unbound-reference elint-ignored-warnings)) | |
628 | (elint-unbound-variable form env) | |
629 | (elint-warning "Reference to unbound symbol: %s" form)) | |
020c3567 RS |
630 | env) |
631 | ||
e2d5a67f | 632 | (t env))) |
020c3567 RS |
633 | |
634 | (defun elint-forms (forms env) | |
635 | "Lint the FORMS, accumulating an environment, starting with ENV." | |
636 | ;; grumblegrumbletailrecursiongrumblegrumble | |
6974be68 GM |
637 | (if (listp forms) |
638 | (dolist (f forms env) | |
639 | (setq env (elint-form f env))) | |
640 | ;; Loop macro? | |
bf01513f GM |
641 | (elint-error "Elint failed to parse form: %s" forms) |
642 | env)) | |
020c3567 | 643 | |
76f2c576 GM |
644 | (defvar elint-bound-variable nil |
645 | "Name of a temporarily bound symbol.") | |
646 | ||
020c3567 | 647 | (defun elint-unbound-variable (var env) |
fa5f7c5f | 648 | "Return t if VAR is unbound in ENV." |
a61344d6 GM |
649 | ;; #1063 suggests adding (symbol-file var) here, but I don't think |
650 | ;; this is right, because it depends on what files you happen to have | |
651 | ;; loaded at the time, which might not be the same when the code runs. | |
652 | ;; It also suggests adding: | |
653 | ;; (numberp (get var 'variable-documentation)) | |
654 | ;; (numberp (cdr-safe (get var 'variable-documentation))) | |
655 | ;; but this is not needed now elint-scan-doc-file exists. | |
e2d5a67f | 656 | (not (or (memq var '(nil t)) |
76f2c576 | 657 | (eq var elint-bound-variable) |
020c3567 | 658 | (elint-env-find-var env var) |
e2d5a67f GM |
659 | (memq var elint-builtin-variables) |
660 | (memq var elint-autoloaded-variables) | |
020c3567 RS |
661 | (memq var elint-standard-variables)))) |
662 | ||
663 | ;;; | |
664 | ;;; Function argument checking | |
665 | ;;; | |
666 | ||
667 | (defun elint-match-args (arglist argpattern) | |
668 | "Match ARGLIST against ARGPATTERN." | |
020c3567 RS |
669 | (let ((state 'all) |
670 | (al (cdr arglist)) | |
671 | (ap argpattern) | |
672 | (ok t)) | |
673 | (while | |
674 | (cond | |
675 | ((and (null al) (null ap)) nil) | |
676 | ((eq (car ap) '&optional) | |
677 | (setq state 'optional) | |
678 | (setq ap (cdr ap)) | |
679 | t) | |
680 | ((eq (car ap) '&rest) | |
681 | nil) | |
682 | ((or (and (eq state 'all) (or (null al) (null ap))) | |
683 | (and (eq state 'optional) (and al (null ap)))) | |
684 | (elint-error "Wrong number of args: %s, %s" arglist argpattern) | |
685 | (setq ok nil) | |
686 | nil) | |
687 | ((and (eq state 'optional) (null al)) | |
688 | nil) | |
689 | (t (setq al (cdr al) | |
690 | ap (cdr ap)) | |
691 | t))) | |
692 | ok)) | |
693 | ||
76f2c576 GM |
694 | (defvar elint-bound-function nil |
695 | "Name of a temporarily bound function symbol.") | |
696 | ||
020c3567 RS |
697 | (defun elint-get-args (func env) |
698 | "Find the args of FUNC in ENV. | |
699 | Returns `unknown' if we couldn't find arguments." | |
700 | (let ((f (elint-env-find-func env func))) | |
701 | (if f | |
e2d5a67f | 702 | (cadr f) |
020c3567 | 703 | (if (symbolp func) |
76f2c576 GM |
704 | (if (eq func elint-bound-function) |
705 | 'unknown | |
706 | (if (fboundp func) | |
707 | (let ((fcode (indirect-function func))) | |
708 | (if (subrp fcode) | |
709 | ;; FIXME builtins with no args have args = nil. | |
710 | (or (get func 'elint-args) 'unknown) | |
711 | (elint-find-args-in-code fcode))) | |
712 | 'undefined)) | |
020c3567 RS |
713 | (elint-find-args-in-code func))))) |
714 | ||
715 | (defun elint-find-args-in-code (code) | |
716 | "Extract the arguments from CODE. | |
717 | CODE can be a lambda expression, a macro, or byte-compiled code." | |
ef501ef0 SM |
718 | (let ((args (help-function-arglist code))) |
719 | (if (listp args) args 'unknown))) | |
020c3567 RS |
720 | |
721 | ;;; | |
722 | ;;; Functions to check some special forms | |
723 | ;;; | |
724 | ||
725 | (defun elint-check-cond-form (form env) | |
726 | "Lint a cond FORM in ENV." | |
76f2c576 | 727 | (dolist (f (cdr form)) |
e2d5a67f | 728 | (if (consp f) |
76f2c576 GM |
729 | (let ((test (car f))) |
730 | (cond ((equal test '(featurep (quote xemacs)))) | |
731 | ((equal test '(not (featurep (quote emacs))))) | |
732 | ;; FIXME (and (boundp 'foo) | |
733 | ((and (eq (car-safe test) 'fboundp) | |
734 | (= 2 (length test)) | |
735 | (eq (car-safe (cadr test)) 'quote)) | |
736 | (let ((elint-bound-function (cadr (cadr test)))) | |
737 | (elint-forms f env))) | |
738 | ((and (eq (car-safe test) 'boundp) | |
739 | (= 2 (length test)) | |
740 | (eq (car-safe (cadr test)) 'quote)) | |
741 | (let ((elint-bound-variable (cadr (cadr test)))) | |
742 | (elint-forms f env))) | |
743 | (t (elint-forms f env)))) | |
744 | (elint-error "cond clause should be a list: %s" f))) | |
745 | env) | |
020c3567 RS |
746 | |
747 | (defun elint-check-defun-form (form env) | |
748 | "Lint a defun/defmacro/lambda FORM in ENV." | |
e2d5a67f GM |
749 | (setq form (if (eq (car form) 'lambda) (cdr form) (cddr form))) |
750 | (mapc (lambda (p) | |
751 | (or (memq p '(&optional &rest)) | |
752 | (setq env (elint-env-add-var env p)))) | |
eb2b0009 | 753 | (car form)) |
020c3567 RS |
754 | (elint-forms (cdr form) env)) |
755 | ||
76f2c576 GM |
756 | (defun elint-check-defalias-form (form env) |
757 | "Lint a defalias FORM in ENV." | |
758 | (let ((alias (cadr form)) | |
759 | (target (nth 2 form))) | |
760 | (and (eq (car-safe alias) 'quote) | |
761 | (eq (car-safe target) 'quote) | |
762 | (eq (elint-get-args (cadr target) env) 'undefined) | |
763 | (elint-warning "Alias `%s' has unknown target `%s'" | |
764 | (cadr alias) (cadr target)))) | |
765 | (elint-form form env t)) | |
766 | ||
020c3567 RS |
767 | (defun elint-check-let-form (form env) |
768 | "Lint the let/let* FORM in ENV." | |
e2d5a67f | 769 | (let ((varlist (cadr form))) |
020c3567 | 770 | (if (not varlist) |
76f2c576 GM |
771 | (if (> (length form) 2) |
772 | ;; An empty varlist is not really an error. Eg some cl macros | |
773 | ;; can expand to such a form. | |
774 | (progn | |
775 | (or (memq 'empty-let elint-ignored-warnings) | |
776 | (elint-warning "Empty varlist in let: %s" form)) | |
777 | ;; Lint the body forms | |
778 | (elint-forms (cddr form) env)) | |
779 | (elint-error "Malformed let: %s" form) | |
020c3567 | 780 | env) |
020c3567 RS |
781 | ;; Check for (let (a (car b)) ...) type of error |
782 | (if (and (= (length varlist) 2) | |
783 | (symbolp (car varlist)) | |
784 | (listp (car (cdr varlist))) | |
785 | (fboundp (car (car (cdr varlist))))) | |
786 | (elint-warning "Suspect varlist: %s" form)) | |
020c3567 RS |
787 | ;; Add variables to environment, and check the init values |
788 | (let ((newenv env)) | |
e2d5a67f GM |
789 | (mapc (lambda (s) |
790 | (cond | |
791 | ((symbolp s) | |
792 | (setq newenv (elint-env-add-var newenv s))) | |
793 | ((and (consp s) (<= (length s) 2)) | |
794 | (elint-form (cadr s) | |
795 | (if (eq (car form) 'let) | |
796 | env | |
797 | newenv)) | |
798 | (setq newenv | |
799 | (elint-env-add-var newenv (car s)))) | |
800 | (t (elint-error | |
801 | "Malformed `let' declaration: %s" s)))) | |
eb2b0009 | 802 | varlist) |
020c3567 RS |
803 | |
804 | ;; Lint the body forms | |
e2d5a67f | 805 | (elint-forms (cddr form) newenv))))) |
020c3567 RS |
806 | |
807 | (defun elint-check-setq-form (form env) | |
808 | "Lint the setq FORM in ENV." | |
809 | (or (= (mod (length form) 2) 1) | |
76f2c576 GM |
810 | ;; (setq foo) is valid and equivalent to (setq foo nil). |
811 | (elint-warning "Missing value in setq: %s" form)) | |
020c3567 RS |
812 | (let ((newenv env) |
813 | sym val) | |
814 | (setq form (cdr form)) | |
815 | (while form | |
816 | (setq sym (car form) | |
817 | val (car (cdr form)) | |
818 | form (cdr (cdr form))) | |
819 | (if (symbolp sym) | |
76f2c576 GM |
820 | (and (not (memq 'unbound-assignment elint-ignored-warnings)) |
821 | (elint-unbound-variable sym newenv) | |
822 | (elint-warning "Setting previously unbound symbol: %s" sym)) | |
020c3567 RS |
823 | (elint-error "Setting non-symbol in setq: %s" sym)) |
824 | (elint-form val newenv) | |
825 | (if (symbolp sym) | |
826 | (setq newenv (elint-env-add-var newenv sym)))) | |
827 | newenv)) | |
7f0fedda | 828 | |
020c3567 RS |
829 | (defun elint-check-defvar-form (form env) |
830 | "Lint the defvar/defconst FORM in ENV." | |
831 | (if (or (= (length form) 2) | |
832 | (= (length form) 3) | |
76f2c576 GM |
833 | ;; Eg the defcalcmodevar macro can expand with a nil doc-string. |
834 | (and (= (length form) 4) (string-or-null-p (nth 3 form)))) | |
020c3567 RS |
835 | (elint-env-add-global-var (elint-form (nth 2 form) env) |
836 | (car (cdr form))) | |
837 | (elint-error "Malformed variable declaration: %s" form) | |
838 | env)) | |
7f0fedda KH |
839 | |
840 | (defun elint-check-defcustom-form (form env) | |
841 | "Lint the defcustom FORM in ENV." | |
842 | (if (and (> (length form) 3) | |
748fb1aa JPW |
843 | ;; even no. of keyword/value args ? |
844 | (zerop (logand (length form) 1))) | |
7f0fedda KH |
845 | (elint-env-add-global-var (elint-form (nth 2 form) env) |
846 | (car (cdr form))) | |
847 | (elint-error "Malformed variable declaration: %s" form) | |
848 | env)) | |
849 | ||
020c3567 RS |
850 | (defun elint-check-function-form (form env) |
851 | "Lint the function FORM in ENV." | |
852 | (let ((func (car (cdr-safe form)))) | |
853 | (cond | |
854 | ((symbolp func) | |
855 | (or (elint-env-find-func env func) | |
76f2c576 GM |
856 | ;; FIXME potentially bogus, since it uses the current |
857 | ;; environment rather than a clean one. | |
020c3567 RS |
858 | (fboundp func) |
859 | (elint-warning "Reference to undefined function: %s" form)) | |
860 | env) | |
861 | ((and (consp func) (memq (car func) '(lambda macro))) | |
862 | (elint-form func env)) | |
863 | ((stringp func) env) | |
864 | (t (elint-error "Not a function object: %s" form) | |
e2d5a67f | 865 | env)))) |
020c3567 RS |
866 | |
867 | (defun elint-check-quote-form (form env) | |
868 | "Lint the quote FORM in ENV." | |
869 | env) | |
870 | ||
871 | (defun elint-check-macro-form (form env) | |
872 | "Check the macro FORM in ENV." | |
873 | (elint-check-function-form (list (car form) (cdr form)) env)) | |
874 | ||
875 | (defun elint-check-condition-case-form (form env) | |
e2d5a67f | 876 | "Check the `condition-case' FORM in ENV." |
020c3567 RS |
877 | (let ((resenv env)) |
878 | (if (< (length form) 3) | |
879 | (elint-error "Malformed condition-case: %s" form) | |
e2d5a67f | 880 | (or (symbolp (cadr form)) |
020c3567 RS |
881 | (elint-warning "First parameter should be a symbol: %s" form)) |
882 | (setq resenv (elint-form (nth 2 form) env)) | |
e2d5a67f | 883 | (let ((newenv (elint-env-add-var env (cadr form))) |
020c3567 | 884 | errlist) |
e2d5a67f GM |
885 | (dolist (err (nthcdr 3 form)) |
886 | (setq errlist (car err)) | |
887 | (mapc (lambda (s) | |
888 | (or (get s 'error-conditions) | |
889 | (get s 'error-message) | |
890 | (memq s elint-extra-errors) | |
891 | (elint-warning | |
892 | "Not an error symbol in error handler: %s" s))) | |
eb2b0009 | 893 | (cond |
e2d5a67f GM |
894 | ((symbolp errlist) (list errlist)) |
895 | ((listp errlist) errlist) | |
896 | (t (elint-error "Bad error list in error handler: %s" | |
897 | errlist) | |
898 | nil))) | |
899 | (elint-forms (cdr err) newenv)))) | |
020c3567 | 900 | resenv)) |
7f0fedda | 901 | |
76f2c576 GM |
902 | ;; For the featurep parts, an alternative is to have |
903 | ;; elint-get-top-forms skip the irrelevant branches. | |
904 | (defun elint-check-conditional-form (form env) | |
905 | "Check the when/unless/and/or FORM in ENV. | |
906 | Does basic handling of `featurep' tests." | |
907 | (let ((func (car form)) | |
908 | (test (cadr form)) | |
909 | sym) | |
910 | ;; Misses things like (and t (featurep 'xemacs)) | |
911 | ;; Check byte-compile-maybe-guarded. | |
912 | (cond ((and (memq func '(when and)) | |
913 | (eq (car-safe test) 'boundp) | |
914 | (= 2 (length test)) | |
915 | (eq (car-safe (cadr test)) 'quote)) | |
916 | ;; Cf elint-check-let-form, which modifies the whole ENV. | |
917 | (let ((elint-bound-variable (cadr (cadr test)))) | |
918 | (elint-form form env t))) | |
919 | ((and (memq func '(when and)) | |
920 | (eq (car-safe test) 'fboundp) | |
921 | (= 2 (length test)) | |
922 | (eq (car-safe (cadr test)) 'quote)) | |
923 | (let ((elint-bound-function (cadr (cadr test)))) | |
924 | (elint-form form env t))) | |
925 | ;; Let's not worry about (if (not (boundp... | |
926 | ((and (eq func 'if) | |
927 | (eq (car-safe test) 'boundp) | |
928 | (= 2 (length test)) | |
929 | (eq (car-safe (cadr test)) 'quote)) | |
930 | (let ((elint-bound-variable (cadr (cadr test)))) | |
931 | (elint-form (nth 2 form) env)) | |
932 | (dolist (f (nthcdr 3 form)) | |
933 | (elint-form f env))) | |
934 | ((and (eq func 'if) | |
935 | (eq (car-safe test) 'fboundp) | |
936 | (= 2 (length test)) | |
937 | (eq (car-safe (cadr test)) 'quote)) | |
938 | (let ((elint-bound-function (cadr (cadr test)))) | |
939 | (elint-form (nth 2 form) env)) | |
940 | (dolist (f (nthcdr 3 form)) | |
941 | (elint-form f env))) | |
942 | ((and (memq func '(when and)) ; skip all | |
943 | (or (null test) | |
944 | (member test '((featurep (quote xemacs)) | |
945 | (not (featurep (quote emacs))))) | |
946 | (and (eq (car-safe test) 'and) | |
947 | (equal (car-safe (cdr test)) | |
948 | '(featurep (quote xemacs))))))) | |
949 | ((and (memq func '(unless or)) | |
950 | (equal test '(featurep (quote emacs))))) | |
951 | ((and (eq func 'if) | |
952 | (or (null test) ; eg custom-browse-insert-prefix | |
953 | (member test '((featurep (quote xemacs)) | |
954 | (not (featurep (quote emacs))))) | |
955 | (and (eq (car-safe test) 'and) | |
956 | (equal (car-safe (cdr test)) | |
957 | '(featurep (quote xemacs)))))) | |
958 | (dolist (f (nthcdr 3 form)) | |
959 | (elint-form f env))) ; lint the else branch | |
960 | ((and (eq func 'if) | |
961 | (equal test '(featurep (quote emacs)))) | |
962 | (elint-form (nth 2 form) env)) ; lint the if branch | |
963 | ;; Process conditional as normal, without handler. | |
964 | (t | |
965 | (elint-form form env t)))) | |
966 | env) | |
967 | ||
020c3567 RS |
968 | ;;; |
969 | ;;; Message functions | |
970 | ;;; | |
971 | ||
e2d5a67f GM |
972 | (defvar elint-current-pos) ; dynamically bound in elint-top-form |
973 | ||
974 | (defun elint-log (type string args) | |
975 | (elint-log-message (format "%s:%d:%s: %s" | |
7a8ae964 GM |
976 | (let ((f (buffer-file-name))) |
977 | (if f | |
978 | (file-name-nondirectory f) | |
979 | (buffer-name))) | |
afdceaec GM |
980 | (if (boundp 'elint-current-pos) |
981 | (save-excursion | |
982 | (goto-char elint-current-pos) | |
983 | (1+ (count-lines (point-min) | |
984 | (line-beginning-position)))) | |
985 | 0) ; unknown position | |
e2d5a67f GM |
986 | type |
987 | (apply 'format string args)))) | |
020c3567 RS |
988 | |
989 | (defun elint-error (string &rest args) | |
a62396cc | 990 | "Report a linting error. |
020c3567 | 991 | STRING and ARGS are thrown on `format' to get the message." |
e2d5a67f | 992 | (elint-log "Error" string args)) |
7f0fedda | 993 | |
020c3567 | 994 | (defun elint-warning (string &rest args) |
a62396cc | 995 | "Report a linting warning. |
e2d5a67f GM |
996 | See `elint-error'." |
997 | (elint-log "Warning" string args)) | |
020c3567 | 998 | |
6974be68 GM |
999 | (defun elint-output (string) |
1000 | "Print or insert STRING, depending on value of `noninteractive'." | |
1001 | (if noninteractive | |
1002 | (message "%s" string) | |
1003 | (insert string "\n"))) | |
1004 | ||
1005 | (defun elint-log-message (errstr &optional top) | |
1006 | "Insert ERRSTR last in the lint log buffer. | |
1007 | Optional argument TOP non-nil means pretend `elint-top-form-logged' is non-nil." | |
e2d5a67f | 1008 | (with-current-buffer (elint-get-log-buffer) |
020c3567 | 1009 | (goto-char (point-max)) |
e2d5a67f GM |
1010 | (let ((inhibit-read-only t)) |
1011 | (or (bolp) (newline)) | |
1012 | ;; Do we have to say where we are? | |
6974be68 GM |
1013 | (unless (or elint-top-form-logged top) |
1014 | (let* ((form (elint-top-form-form elint-top-form)) | |
1015 | (top (car form))) | |
1016 | (elint-output (cond | |
1017 | ((memq top '(defun defsubst)) | |
1018 | (format "\nIn function %s:" (cadr form))) | |
1019 | ((eq top 'defmacro) | |
1020 | (format "\nIn macro %s:" (cadr form))) | |
1021 | ((memq top '(defvar defconst)) | |
1022 | (format "\nIn variable %s:" (cadr form))) | |
1023 | (t "\nIn top level expression:")))) | |
e2d5a67f | 1024 | (setq elint-top-form-logged t)) |
6974be68 | 1025 | (elint-output errstr)))) |
020c3567 RS |
1026 | |
1027 | (defun elint-clear-log (&optional header) | |
1028 | "Clear the lint log buffer. | |
1029 | Insert HEADER followed by a blank line if non-nil." | |
e2d5a67f GM |
1030 | (let ((dir default-directory)) |
1031 | (with-current-buffer (elint-get-log-buffer) | |
1032 | (setq default-directory dir) | |
1033 | (let ((inhibit-read-only t)) | |
1034 | (erase-buffer) | |
1035 | (if header (insert header "\n")))))) | |
020c3567 RS |
1036 | |
1037 | (defun elint-display-log () | |
1038 | "Display the lint log buffer." | |
1039 | (let ((pop-up-windows t)) | |
1040 | (display-buffer (elint-get-log-buffer)) | |
1041 | (sit-for 0))) | |
1042 | ||
6974be68 GM |
1043 | (defvar elint-running) |
1044 | ||
1045 | (defun elint-set-mode-line (&optional on) | |
1046 | "Set the mode-line-process of the Elint log buffer." | |
1047 | (with-current-buffer (elint-get-log-buffer) | |
1048 | (and (eq major-mode 'compilation-mode) | |
1049 | (setq mode-line-process | |
1050 | (list (if (or on (bound-and-true-p elint-running)) | |
1051 | (propertize ":run" 'face 'compilation-warning) | |
1052 | (propertize ":finished" 'face 'compilation-info))))))) | |
1053 | ||
020c3567 RS |
1054 | (defun elint-get-log-buffer () |
1055 | "Return a log buffer for elint." | |
e2d5a67f GM |
1056 | (or (get-buffer elint-log-buffer) |
1057 | (with-current-buffer (get-buffer-create elint-log-buffer) | |
1058 | (or (eq major-mode 'compilation-mode) | |
1059 | (compilation-mode)) | |
1060 | (setq buffer-undo-list t) | |
1061 | (current-buffer)))) | |
7f0fedda | 1062 | |
020c3567 RS |
1063 | ;;; |
1064 | ;;; Initializing code | |
1065 | ;;; | |
7f0fedda | 1066 | |
76f2c576 GM |
1067 | (defun elint-put-function-args (func args) |
1068 | "Mark function FUNC as having argument list ARGS." | |
1069 | (and (symbolp func) | |
1070 | args | |
1071 | (not (eq args 'unknown)) | |
1072 | (put func 'elint-args args))) | |
1073 | ||
020c3567 | 1074 | ;;;###autoload |
7a8ae964 GM |
1075 | (defun elint-initialize (&optional reinit) |
1076 | "Initialize elint. | |
1077 | If elint is already initialized, this does nothing, unless | |
1078 | optional prefix argument REINIT is non-nil." | |
1079 | (interactive "P") | |
1080 | (if (and elint-builtin-variables (not reinit)) | |
1081 | (message "Elint is already initialized") | |
1082 | (message "Initializing elint...") | |
76f2c576 | 1083 | (setq elint-builtin-variables (elint-scan-doc-file) |
7a8ae964 | 1084 | elint-autoloaded-variables (elint-find-autoloaded-variables)) |
76f2c576 | 1085 | (mapc (lambda (x) (elint-put-function-args (car x) (cdr x))) |
7a8ae964 GM |
1086 | (elint-find-builtin-args)) |
1087 | (if elint-unknown-builtin-args | |
76f2c576 | 1088 | (mapc (lambda (x) (elint-put-function-args (car x) (cdr x))) |
7a8ae964 | 1089 | elint-unknown-builtin-args)) |
76f2c576 GM |
1090 | (when elint-scan-preloaded |
1091 | (dolist (lib preloaded-file-list) | |
1092 | ;; Skip files that contain nothing of use to us. | |
1093 | (unless (string-match elint-preloaded-skip-re lib) | |
1094 | (setq elint-preloaded-env | |
1095 | (elint-add-required-env elint-preloaded-env nil lib))))) | |
7a8ae964 | 1096 | (message "Initializing elint...done"))) |
020c3567 RS |
1097 | |
1098 | ||
76f2c576 GM |
1099 | ;; This includes all the built-in and dumped things with documentation. |
1100 | (defun elint-scan-doc-file () | |
1101 | "Scan the DOC file for function and variables. | |
ee7683eb | 1102 | Marks the function with their arguments, and returns a list of variables." |
e2d5a67f GM |
1103 | ;; Cribbed from help-fns.el. |
1104 | (let ((docbuf " *DOC*") | |
76f2c576 | 1105 | vars sym args) |
7a8ae964 GM |
1106 | (save-excursion |
1107 | (if (get-buffer docbuf) | |
1108 | (progn | |
1109 | (set-buffer docbuf) | |
1110 | (goto-char (point-min))) | |
1111 | (set-buffer (get-buffer-create docbuf)) | |
1112 | (insert-file-contents-literally | |
1113 | (expand-file-name internal-doc-file-name doc-directory))) | |
76f2c576 GM |
1114 | (while (re-search-forward "\1f\\([VF]\\)" nil t) |
1115 | (when (setq sym (intern-soft (buffer-substring (point) | |
1116 | (line-end-position)))) | |
1117 | (if (string-equal (match-string 1) "V") | |
1118 | ;; Excludes platform-specific stuff not relevant to the | |
1119 | ;; running platform. | |
1120 | (if (boundp sym) (setq vars (cons sym vars))) | |
1121 | ;; Function. | |
1122 | (when (fboundp sym) | |
1123 | (when (re-search-forward "\\(^(fn.*)\\)?\1f" nil t) | |
1124 | (backward-char 1) | |
1125 | ;; FIXME distinguish no args from not found. | |
1126 | (and (setq args (match-string 1)) | |
1127 | (setq args | |
1128 | (ignore-errors | |
1129 | (read | |
1130 | (replace-regexp-in-string "^(fn ?" "(" args)))) | |
1131 | (elint-put-function-args sym args)))))))) | |
1132 | vars)) | |
e2d5a67f GM |
1133 | |
1134 | (defun elint-find-autoloaded-variables () | |
1135 | "Return a list of all autoloaded variables." | |
1136 | (let (var vars) | |
1137 | (with-temp-buffer | |
1138 | (insert-file-contents (locate-library "loaddefs.el")) | |
1139 | (while (re-search-forward "^(defvar \\([[:alnum:]_-]+\\)" nil t) | |
1140 | (and (setq var (intern-soft (match-string 1))) | |
1141 | (boundp var) | |
1142 | (setq vars (cons var vars))))) | |
1143 | vars)) | |
1144 | ||
020c3567 | 1145 | (defun elint-find-builtins () |
e2d5a67f GM |
1146 | "Return a list of all built-in functions." |
1147 | (let (subrs) | |
6bdd9204 SM |
1148 | (mapatoms (lambda (s) (and (subrp (symbol-function s)) |
1149 | (push s subrs)))) | |
e2d5a67f | 1150 | subrs)) |
020c3567 RS |
1151 | |
1152 | (defun elint-find-builtin-args (&optional list) | |
e2d5a67f | 1153 | "Return a list of the built-in functions and their arguments. |
020c3567 RS |
1154 | If LIST is nil, call `elint-find-builtins' to get a list of all built-in |
1155 | functions, otherwise use LIST. | |
1156 | ||
e2d5a67f | 1157 | Each function is represented by a cons cell: |
020c3567 RS |
1158 | \(function-symbol . args) |
1159 | If no documentation could be found args will be `unknown'." | |
92bd667f GM |
1160 | (mapcar (lambda (f) |
1161 | (let ((doc (documentation f t))) | |
1162 | (or (and doc | |
1163 | (string-match "\n\n(fn\\(.*)\\)\\'" doc) | |
1164 | (ignore-errors | |
e2d5a67f GM |
1165 | ;; "BODY...)" -> "&rest BODY)". |
1166 | (read (replace-regexp-in-string | |
1167 | "\\([^ ]+\\)\\.\\.\\.)\\'" "&rest \\1)" | |
1168 | (format "(%s %s" f (match-string 1 doc)) t)))) | |
92bd667f GM |
1169 | (cons f 'unknown)))) |
1170 | (or list (elint-find-builtins)))) | |
020c3567 | 1171 | |
020c3567 RS |
1172 | (provide 'elint) |
1173 | ||
1174 | ;;; elint.el ends here |