Warn about misplaced or duplicated function/macro declarations

Doc strings, `declare` and `interactive` forms must appear in that
order and at most once each.  Complain if they don't, instead of
silently ignoring the problem (bug#55905).

* lisp/emacs-lisp/byte-run.el (byte-run--parse-body)
(byte-run--parse-declarations): New.
(defmacro, defun): Check for declaration well-formedness as
described above.  Clarify doc strings.  Refactor some common code.
* test/lisp/emacs-lisp/bytecomp-resources/fun-attr-warn.el:
* test/lisp/emacs-lisp/bytecomp-tests.el (bytecomp-fun-attr-warn):
New test.
This commit is contained in:
Mattias Engdegård 2022-06-17 17:06:05 +02:00
parent e9c50055ff
commit 73e75e18d1
3 changed files with 446 additions and 91 deletions

View file

@ -272,6 +272,75 @@ This is used by `declare'.")
(list 'function-put (list 'quote name) (list 'function-put (list 'quote name)
''no-font-lock-keyword (list 'quote val)))) ''no-font-lock-keyword (list 'quote val))))
(defalias 'byte-run--parse-body
#'(lambda (body allow-interactive)
"Decompose BODY into (DOCSTRING DECLARE INTERACTIVE BODY-REST WARNINGS)."
(let* ((top body)
(docstring nil)
(declare-form nil)
(interactive-form nil)
(warnings nil)
(warn #'(lambda (msg form)
(push (macroexp-warn-and-return msg nil nil t form)
warnings))))
(while
(and body
(let* ((form (car body))
(head (car-safe form)))
(cond
((or (and (stringp form) (cdr body))
(eq head :documentation))
(cond
(docstring (funcall warn "More than one doc string" top))
(declare-form
(funcall warn "Doc string after `declare'" declare-form))
(interactive-form
(funcall warn "Doc string after `interactive'"
interactive-form))
(t (setq docstring form)))
t)
((eq head 'declare)
(cond
(declare-form
(funcall warn "More than one `declare' form" form))
(interactive-form
(funcall warn "`declare' after `interactive'" form))
(t (setq declare-form form)))
t)
((eq head 'interactive)
(cond
((not allow-interactive)
(funcall warn "No `interactive' form allowed here" form))
(interactive-form
(funcall warn "More than one `interactive' form" form))
(t (setq interactive-form form)))
t))))
(setq body (cdr body)))
(list docstring declare-form interactive-form body warnings))))
(defalias 'byte-run--parse-declarations
#'(lambda (name arglist clauses construct declarations-alist)
(let* ((cl-decls nil)
(actions
(mapcar
#'(lambda (x)
(let ((f (cdr (assq (car x) declarations-alist))))
(cond
(f (apply (car f) name arglist (cdr x)))
;; Yuck!!
((and (featurep 'cl)
(memq (car x) ;C.f. cl--do-proclaim.
'(special inline notinline optimize warn)))
(push (list 'declare x) cl-decls)
nil)
(t
(macroexp-warn-and-return
(format-message "Unknown %s property `%S'"
construct (car x))
nil nil nil (car x))))))
clauses)))
(cons actions cl-decls))))
(defvar macro-declarations-alist (defvar macro-declarations-alist
(cons (cons
(list 'debug #'byte-run--set-debug) (list 'debug #'byte-run--set-debug)
@ -289,7 +358,7 @@ This is used by `declare'.")
(defalias 'defmacro (defalias 'defmacro
(cons (cons
'macro 'macro
#'(lambda (name arglist &optional docstring &rest body) #'(lambda (name arglist &rest body)
"Define NAME as a macro. "Define NAME as a macro.
When the macro is called, as in (NAME ARGS...), When the macro is called, as in (NAME ARGS...),
the function (lambda ARGLIST BODY...) is applied to the function (lambda ARGLIST BODY...) is applied to
@ -300,115 +369,72 @@ DECLS is a list of elements of the form (PROP . VALUES). These are
interpreted according to `macro-declarations-alist'. interpreted according to `macro-declarations-alist'.
The return value is undefined. The return value is undefined.
\(fn NAME ARGLIST &optional DOCSTRING DECL &rest BODY)" \(fn NAME ARGLIST [DOCSTRING] [DECL] BODY...)"
;; We can't just have `decl' as an &optional argument, because we need (let* ((parse (byte-run--parse-body body nil))
;; to distinguish (docstring (nth 0 parse))
;; (defmacro foo (arg) (bar) nil) (declare-form (nth 1 parse))
;; from (body (nth 3 parse))
;; (defmacro foo (arg) (bar)). (warnings (nth 4 parse))
(let ((decls (cond (declarations
((eq (car-safe docstring) 'declare) (and declare-form (byte-run--parse-declarations
(prog1 (cdr docstring) (setq docstring nil))) name arglist (cdr declare-form) 'macro
((and (stringp docstring) macro-declarations-alist))))
(eq (car-safe (car body)) 'declare)) (setq body (nconc warnings body))
(prog1 (cdr (car body)) (setq body (cdr body))))))) (setq body (nconc (cdr declarations) body))
(if docstring (setq body (cons docstring body)) (if docstring
(if (null body) (setq body '(nil)))) (setq body (cons docstring body)))
;; Can't use backquote because it's not defined yet! (if (null body)
(setq body '(nil)))
(let* ((fun (list 'function (cons 'lambda (cons arglist body)))) (let* ((fun (list 'function (cons 'lambda (cons arglist body))))
(def (list 'defalias (def (list 'defalias
(list 'quote name) (list 'quote name)
(list 'cons ''macro fun))) (list 'cons ''macro fun))))
(declarations
(mapcar
#'(lambda (x)
(let ((f (cdr (assq (car x) macro-declarations-alist))))
(if f (apply (car f) name arglist (cdr x))
(macroexp-warn-and-return
(format-message
"Unknown macro property %S in %S"
(car x) name)
nil nil nil (car x)))))
decls)))
;; Refresh font-lock if this is a new macro, or it is an
;; existing macro whose 'no-font-lock-keyword declaration
;; has changed.
(if (and
;; If lisp-mode hasn't been loaded, there's no reason
;; to flush.
(fboundp 'lisp--el-font-lock-flush-elisp-buffers)
(or (not (fboundp name)) ;; new macro
(and (fboundp name) ;; existing macro
(member `(function-put ',name 'no-font-lock-keyword
',(get name 'no-font-lock-keyword))
declarations))))
(lisp--el-font-lock-flush-elisp-buffers))
(if declarations (if declarations
(cons 'prog1 (cons def declarations)) (cons 'prog1 (cons def (car declarations)))
def)))))) def))))))
;; Now that we defined defmacro we can use it! ;; Now that we defined defmacro we can use it!
(defmacro defun (name arglist &optional docstring &rest body) (defmacro defun (name arglist &rest body)
"Define NAME as a function. "Define NAME as a function.
The definition is (lambda ARGLIST [DOCSTRING] BODY...). The definition is (lambda ARGLIST [DOCSTRING] [INTERACTIVE] BODY...).
See also the function `interactive'.
DECL is a declaration, optional, of the form (declare DECLS...) where DECL is a declaration, optional, of the form (declare DECLS...) where
DECLS is a list of elements of the form (PROP . VALUES). These are DECLS is a list of elements of the form (PROP . VALUES). These are
interpreted according to `defun-declarations-alist'. interpreted according to `defun-declarations-alist'.
INTERACTIVE is an optional `interactive' specification.
The return value is undefined. The return value is undefined.
\(fn NAME ARGLIST &optional DOCSTRING DECL &rest BODY)" \(fn NAME ARGLIST [DOCSTRING] [DECL] [INTERACTIVE] BODY...)"
;; We can't just have `decl' as an &optional argument, because we need
;; to distinguish
;; (defun foo (arg) (toto) nil)
;; from
;; (defun foo (arg) (toto)).
(declare (doc-string 3) (indent 2)) (declare (doc-string 3) (indent 2))
(or name (error "Cannot define '%s' as a function" name)) (or name (error "Cannot define '%s' as a function" name))
(if (null (if (null
(and (listp arglist) (and (listp arglist)
(null (delq t (mapcar #'symbolp arglist))))) (null (delq t (mapcar #'symbolp arglist)))))
(error "Malformed arglist: %s" arglist)) (error "Malformed arglist: %s" arglist))
(let ((decls (cond (let* ((parse (byte-run--parse-body body t))
((eq (car-safe docstring) 'declare) (docstring (nth 0 parse))
(prog1 (cdr docstring) (setq docstring nil))) (declare-form (nth 1 parse))
((and (stringp docstring) (interactive-form (nth 2 parse))
(eq (car-safe (car body)) 'declare)) (body (nth 3 parse))
(prog1 (cdr (car body)) (setq body (cdr body))))))) (warnings (nth 4 parse))
(if docstring (setq body (cons docstring body)) (declarations
(if (null body) (setq body '(nil)))) (and declare-form (byte-run--parse-declarations
(let ((declarations name arglist (cdr declare-form) 'defun
(mapcar defun-declarations-alist))))
#'(lambda (x) (setq body (nconc warnings body))
(let ((f (cdr (assq (car x) defun-declarations-alist)))) (setq body (nconc (cdr declarations) body))
(cond (if interactive-form
(f (apply (car f) name arglist (cdr x))) (setq body (cons interactive-form body)))
;; Yuck!! (if docstring
((and (featurep 'cl) (setq body (cons docstring body)))
(memq (car x) ;C.f. cl-do-proclaim. (if (null body)
'(special inline notinline optimize warn))) (setq body '(nil)))
(push (list 'declare x) (let ((def (list 'defalias
(if (stringp docstring)
(if (eq (car-safe (cadr body)) 'interactive)
(cddr body)
(cdr body))
(if (eq (car-safe (car body)) 'interactive)
(cdr body)
body)))
nil)
(t
(macroexp-warn-and-return
(format-message "Unknown defun property `%S' in %S"
(car x) name)
nil nil nil (car x))))))
decls))
(def (list 'defalias
(list 'quote name) (list 'quote name)
(list 'function (list 'function
(cons 'lambda (cons 'lambda
(cons arglist body)))))) (cons arglist body))))))
(if declarations (if declarations
(cons 'prog1 (cons def declarations)) (cons 'prog1 (cons def (car declarations)))
def)))) def))))

View file

@ -0,0 +1,266 @@
;;; -*- lexical-binding: t -*-
;; Correct
(defun faw-str-decl-code (x)
"something"
(declare (pure t))
(print x))
(defun faw-doc-decl-code (x)
(:documentation "something")
(declare (pure t))
(print x))
(defun faw-str-int-code (x)
"something"
(interactive "P")
(print x))
(defun faw-doc-int-code (x)
(:documentation "something")
(interactive "P")
(print x))
(defun faw-decl-int-code (x)
(declare (pure t))
(interactive "P")
(print x))
(defun faw-str-decl-int-code (x)
"something"
(declare (pure t))
(interactive "P")
(print x))
(defun faw-doc-decl-int-code (x)
(:documentation "something")
(declare (pure t))
(interactive "P")
(print x))
;; Correct (last string is return value)
(defun faw-str ()
"something")
(defun faw-decl-str ()
(declare (pure t))
"something")
(defun faw-decl-int-str ()
(declare (pure t))
(interactive)
"something")
(defun faw-str-str ()
"something"
"something else")
(defun faw-doc-str ()
(:documentation "something")
"something else")
;; Incorrect (bad order)
(defun faw-int-decl-code (x)
(interactive "P")
(declare (pure t))
(print x))
(defun faw-int-str-code (x)
(interactive "P")
"something"
(print x))
(defun faw-int-doc-code (x)
(interactive "P")
(:documentation "something")
(print x))
(defun faw-decl-str-code (x)
(declare (pure t))
"something"
(print x))
(defun faw-decl-doc-code (x)
(declare (pure t))
(:documentation "something")
(print x))
(defun faw-str-int-decl-code (x)
"something"
(interactive "P")
(declare (pure t))
(print x))
(defun faw-doc-int-decl-code (x)
(:documentation "something")
(interactive "P")
(declare (pure t))
(print x))
(defun faw-int-str-decl-code (x)
(interactive "P")
"something"
(declare (pure t))
(print x))
(defun faw-int-doc-decl-code (x)
(interactive "P")
(:documentation "something")
(declare (pure t))
(print x))
(defun faw-int-decl-str-code (x)
(interactive "P")
(declare (pure t))
"something"
(print x))
(defun faw-int-decl-doc-code (x)
(interactive "P")
(declare (pure t))
(:documentation "something")
(print x))
(defun faw-decl-int-str-code (x)
(declare (pure t))
(interactive "P")
"something"
(print x))
(defun faw-decl-int-doc-code (x)
(declare (pure t))
(interactive "P")
(:documentation "something")
(print x))
(defun faw-decl-str-int-code (x)
(declare (pure t))
"something"
(interactive "P")
(print x))
(defun faw-decl-doc-int-code (x)
(declare (pure t))
(:documentation "something")
(interactive "P")
(print x))
;; Incorrect (duplication)
(defun faw-str-str-decl-int-code (x)
"something"
"something else"
(declare (pure t))
(interactive "P")
(print x))
(defun faw-str-doc-decl-int-code (x)
"something"
(:documentation "something else")
(declare (pure t))
(interactive "P")
(print x))
(defun faw-doc-str-decl-int-code (x)
(:documentation "something")
"something else"
(declare (pure t))
(interactive "P")
(print x))
(defun faw-doc-doc-decl-int-code (x)
(:documentation "something")
(:documentation "something else")
(declare (pure t))
(interactive "P")
(print x))
(defun faw-str-decl-str-int-code (x)
"something"
(declare (pure t))
"something else"
(interactive "P")
(print x))
(defun faw-doc-decl-str-int-code (x)
(:documentation "something")
(declare (pure t))
"something else"
(interactive "P")
(print x))
(defun faw-str-decl-doc-int-code (x)
"something"
(declare (pure t))
(:documentation "something else")
(interactive "P")
(print x))
(defun faw-doc-decl-doc-int-code (x)
(:documentation "something")
(declare (pure t))
(:documentation "something else")
(interactive "P")
(print x))
(defun faw-str-decl-decl-int-code (x)
"something"
(declare (pure t))
(declare (indent 1))
(interactive "P")
(print x))
(defun faw-doc-decl-decl-int-code (x)
(:documentation "something")
(declare (pure t))
(declare (indent 1))
(interactive "P")
(print x))
(defun faw-str-decl-int-decl-code (x)
"something"
(declare (pure t))
(interactive "P")
(declare (indent 1))
(print x))
(defun faw-doc-decl-int-decl-code (x)
(:documentation "something")
(declare (pure t))
(interactive "P")
(declare (indent 1))
(print x))
(defun faw-str-decl-int-int-code (x)
"something"
(declare (pure t))
(interactive "P")
(interactive "p")
(print x))
(defun faw-doc-decl-int-int-code (x)
(:documentation "something")
(declare (pure t))
(interactive "P")
(interactive "p")
(print x))
(defun faw-str-int-decl-int-code (x)
"something"
(interactive "P")
(declare (pure t))
(interactive "p")
(print x))
(defun faw-doc-int-decl-int-code (x)
(:documentation "something")
(interactive "P")
(declare (pure t))
(interactive "p")
(print x))

View file

@ -1580,6 +1580,69 @@ EXPECTED-POINT BINDINGS (MODES \\='\\='(ruby-mode js-mode python-mode)) \
(should (equal (get fname 'lisp-indent-function) 1)) (should (equal (get fname 'lisp-indent-function) 1))
(should (equal (aref bc 4) "tata\n\n(fn X)"))))) (should (equal (aref bc 4) "tata\n\n(fn X)")))))
(ert-deftest bytecomp-fun-attr-warn ()
;; Check that warnings are emitted when doc strings, `declare' and
;; `interactive' forms don't come in the proper order, or more than once.
(let* ((filename "fun-attr-warn.el")
(el (ert-resource-file filename))
(elc (concat el "c"))
(text-quoting-style 'grave))
(with-current-buffer (get-buffer-create "*Compile-Log*")
(let ((inhibit-read-only t))
(erase-buffer))
(byte-compile-file el)
(let ((expected
'("70:4: Warning: `declare' after `interactive'"
"74:4: Warning: Doc string after `interactive'"
"79:4: Warning: Doc string after `interactive'"
"84:4: Warning: Doc string after `declare'"
"89:4: Warning: Doc string after `declare'"
"96:4: Warning: `declare' after `interactive'"
"102:4: Warning: `declare' after `interactive'"
"108:4: Warning: `declare' after `interactive'"
"106:4: Warning: Doc string after `interactive'"
"114:4: Warning: `declare' after `interactive'"
"112:4: Warning: Doc string after `interactive'"
"118:4: Warning: Doc string after `interactive'"
"119:4: Warning: `declare' after `interactive'"
"124:4: Warning: Doc string after `interactive'"
"125:4: Warning: `declare' after `interactive'"
"130:4: Warning: Doc string after `declare'"
"136:4: Warning: Doc string after `declare'"
"142:4: Warning: Doc string after `declare'"
"148:4: Warning: Doc string after `declare'"
"159:4: Warning: More than one doc string"
"165:4: Warning: More than one doc string"
"171:4: Warning: More than one doc string"
"178:4: Warning: More than one doc string"
"186:4: Warning: More than one doc string"
"192:4: Warning: More than one doc string"
"200:4: Warning: More than one doc string"
"206:4: Warning: More than one doc string"
"215:4: Warning: More than one `declare' form"
"222:4: Warning: More than one `declare' form"
"230:4: Warning: More than one `declare' form"
"237:4: Warning: More than one `declare' form"
"244:4: Warning: More than one `interactive' form"
"251:4: Warning: More than one `interactive' form"
"258:4: Warning: More than one `interactive' form"
"257:4: Warning: `declare' after `interactive'"
"265:4: Warning: More than one `interactive' form"
"264:4: Warning: `declare' after `interactive'")))
(goto-char (point-min))
(let ((actual nil))
(while (re-search-forward
(rx bol (* (not ":")) ":"
(group (+ digit) ":" (+ digit) ": Warning: "
(or "More than one " (+ nonl) " form"
(: (+ nonl) " after " (+ nonl))))
eol)
nil t)
(push (match-string 1) actual))
(setq actual (nreverse actual))
(should (equal actual expected)))))))
;; Local Variables: ;; Local Variables:
;; no-byte-compile: t ;; no-byte-compile: t
;; End: ;; End: