check-declare.el 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. ;;; check-declare.el --- Check declare-function statements
  2. ;; Copyright (C) 2007-2017 Free Software Foundation, Inc.
  3. ;; Author: Glenn Morris <rgm@gnu.org>
  4. ;; Keywords: lisp, tools, maint
  5. ;; This file is part of GNU Emacs.
  6. ;; GNU Emacs is free software: you can redistribute it and/or modify
  7. ;; it under the terms of the GNU General Public License as published by
  8. ;; the Free Software Foundation, either version 3 of the License, or
  9. ;; (at your option) any later version.
  10. ;; GNU Emacs is distributed in the hope that it will be useful,
  11. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ;; GNU General Public License for more details.
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;; The byte-compiler often warns about undefined functions that you
  18. ;; know will actually be defined when it matters. The `declare-function'
  19. ;; statement allows you to suppress these warnings. This package
  20. ;; checks that all such statements in a file or directory are accurate.
  21. ;; The entry points are `check-declare-file' and `check-declare-directory'.
  22. ;; For more information, see Info node `(elisp)Declaring Functions'.
  23. ;;; TODO:
  24. ;; 1. Warn about functions marked as obsolete, eg
  25. ;; password-read-and-add in smime.el.
  26. ;; 2. defmethod, defclass argument checking.
  27. ;; 3. defclass also defines -p and -child-p.
  28. ;;; Code:
  29. (defconst check-declare-warning-buffer "*Check Declarations Warnings*"
  30. "Name of buffer used to display any `check-declare' warnings.")
  31. (defun check-declare-locate (file basefile)
  32. "Return the relative name of FILE.
  33. Expands files with a \".c\" or \".m\" extension relative to the Emacs
  34. \"src/\" directory. Otherwise, `locate-library' searches for FILE.
  35. If that fails, expands FILE relative to BASEFILE's directory part.
  36. The returned file might not exist. If FILE has an \"ext:\" prefix, so does
  37. the result."
  38. (let ((ext (string-match "^ext:" file))
  39. tfile)
  40. (if ext
  41. (setq file (substring file 4)))
  42. (setq file
  43. (if (member (file-name-extension file) '("c" "m"))
  44. (expand-file-name file (expand-file-name "src" source-directory))
  45. (if (setq tfile (locate-library file))
  46. (progn
  47. (setq tfile
  48. (replace-regexp-in-string "\\.elc\\'" ".el" tfile))
  49. (if (and (not (file-exists-p tfile))
  50. (file-exists-p (concat tfile ".gz")))
  51. (concat tfile ".gz")
  52. tfile))
  53. (setq tfile (expand-file-name file
  54. (file-name-directory basefile)))
  55. (if (or (file-exists-p tfile)
  56. (string-match "\\.el\\'" tfile))
  57. tfile
  58. (concat tfile ".el")))))
  59. (setq file (file-relative-name file))
  60. (if ext (concat "ext:" file)
  61. file)))
  62. (defun check-declare-scan (file)
  63. "Scan FILE for `declare-function' calls.
  64. Return a list with elements of the form (FNFILE FN ARGLIST FILEONLY),
  65. where only the first two elements need be present. This claims that FNFILE
  66. defines FN, with ARGLIST. FILEONLY non-nil means only check that FNFILE
  67. exists, not that it defines FN. This is for function definitions that we
  68. don't know how to recognize (e.g. some macros)."
  69. (let (alist)
  70. (with-temp-buffer
  71. (insert-file-contents file)
  72. ;; FIXME we could theoretically be inside a string.
  73. (while (re-search-forward "^[ \t]*\\((declare-function\\)[ \t\n]" nil t)
  74. (let ((pos (match-beginning 1)))
  75. (goto-char pos)
  76. (let ((form (ignore-errors (read (current-buffer))))
  77. len fn formfile fnfile arglist fileonly)
  78. (if (and
  79. ;; Exclude element of byte-compile-initial-macro-environment.
  80. (or (listp (cdr form)) (setq form nil))
  81. (> (setq len (length form)) 2)
  82. (< len 6)
  83. (setq formfile (nth 2 form))
  84. (symbolp (setq fn (cadr form)))
  85. (setq fn (symbol-name fn)) ; later we use as a search string
  86. (stringp formfile)
  87. (setq fnfile (check-declare-locate formfile file))
  88. ;; Use t to distinguish unspecified arglist from empty one.
  89. (or (eq t (setq arglist (if (> len 3)
  90. (nth 3 form)
  91. t)))
  92. (listp arglist))
  93. (symbolp (setq fileonly (nth 4 form))))
  94. (setq alist (cons (list fnfile fn arglist fileonly) alist))
  95. (when form
  96. (check-declare-warn file (or fn "unknown function")
  97. (if (stringp formfile) formfile
  98. "unknown file")
  99. "Malformed declaration"
  100. (line-number-at-pos pos))))))))
  101. alist))
  102. (autoload 'byte-compile-arglist-signature "bytecomp")
  103. (defgroup check-declare nil
  104. "Check declare-function statements."
  105. :group 'tools)
  106. (defcustom check-declare-ext-errors nil
  107. "When non-nil, warn about functions not found in :ext."
  108. :version "25.1"
  109. :type 'boolean)
  110. (defun check-declare-verify (fnfile fnlist)
  111. "Check that FNFILE contains function definitions matching FNLIST.
  112. Each element of FNLIST has the form (FILE FN ARGLIST FILEONLY), where
  113. only the first two elements need be present. This means FILE claimed FN
  114. was defined in FNFILE with the specified ARGLIST. FILEONLY non-nil means
  115. to only check that FNFILE exists, not that it actually defines FN.
  116. Returns nil if all claims are found to be true, otherwise a list
  117. of errors with elements of the form \(FILE FN TYPE), where TYPE
  118. is a string giving details of the error."
  119. (let ((cflag (member (file-name-extension fnfile) '("c" "m")))
  120. (ext (string-match "^ext:" fnfile))
  121. re fn sig siglist arglist type errlist minargs maxargs)
  122. (if ext
  123. (setq fnfile (substring fnfile 4)))
  124. (if (file-regular-p fnfile)
  125. (with-temp-buffer
  126. (insert-file-contents fnfile)
  127. ;; defsubst's don't _have_ to be known at compile time.
  128. (setq re (format (if cflag
  129. "^[ \t]*\\(DEFUN\\)[ \t]*([ \t]*\"%s\""
  130. "^[ \t]*(\\(fset[ \t]+'\\|\
  131. cl-def\\(?:generic\\|method\\)\\|\
  132. def\\(?:un\\|subst\\|foo\\|method\\|class\\|\
  133. ine-\\(?:derived\\|generic\\|\\(?:global\\(?:ized\\)?-\\)?minor\\)-mode\\|\
  134. \\(?:ine-obsolete-function-\\)?alias[ \t]+'\\|\
  135. ine-overloadable-function\\)\\)\
  136. [ \t]*%s\\([ \t;]+\\|$\\)")
  137. (regexp-opt (mapcar 'cadr fnlist) t)))
  138. (while (re-search-forward re nil t)
  139. (skip-chars-forward " \t\n")
  140. (setq fn (match-string 2)
  141. type (match-string 1)
  142. ;; (min . max) for a fixed number of arguments, or
  143. ;; arglists with optional elements.
  144. ;; (min) for arglists with &rest.
  145. ;; sig = 'err means we could not find an arglist.
  146. sig (cond (cflag
  147. (or
  148. (when (search-forward "," nil t 3)
  149. (skip-chars-forward " \t\n")
  150. ;; Assuming minargs and maxargs on same line.
  151. (when (looking-at "\\([0-9]+\\)[ \t]*,[ \t]*\
  152. \\([0-9]+\\|MANY\\|UNEVALLED\\)")
  153. (setq minargs (string-to-number
  154. (match-string 1))
  155. maxargs (match-string 2))
  156. (cons minargs (unless (string-match "[^0-9]"
  157. maxargs)
  158. (string-to-number
  159. maxargs)))))
  160. 'err))
  161. ((string-match
  162. "\\`define-\\(derived\\|generic\\)-mode\\'"
  163. type)
  164. '(0 . 0))
  165. ((string-match
  166. "\\`define\\(-global\\(ized\\)?\\)?-minor-mode\\'"
  167. type)
  168. '(0 . 1))
  169. ;; Prompt to update.
  170. ((string-match
  171. "\\`define-obsolete-function-alias\\>"
  172. type)
  173. 'obsolete)
  174. ;; Can't easily check arguments in these cases.
  175. ((string-match "\\`\\(def\\(alias\\|class\\)\\|\
  176. fset\\|\\(?:cl-\\)?defmethod\\)\\>" type)
  177. t)
  178. ((looking-at "\\((\\|nil\\)")
  179. (byte-compile-arglist-signature
  180. (read (current-buffer))))
  181. (t
  182. 'err))
  183. ;; alist of functions and arglist signatures.
  184. siglist (cons (cons fn sig) siglist)))))
  185. (dolist (e fnlist)
  186. (setq arglist (nth 2 e)
  187. type
  188. (if (not re)
  189. (when (or check-declare-ext-errors (not ext))
  190. "file not found")
  191. (if (not (setq sig (assoc (cadr e) siglist)))
  192. (unless (nth 3 e) ; fileonly
  193. "function not found")
  194. (setq sig (cdr sig))
  195. (cond ((eq sig 'obsolete) ; check even when no arglist specified
  196. "obsolete alias")
  197. ;; arglist t means no arglist specified, as
  198. ;; opposed to an empty arglist.
  199. ((eq arglist t) nil)
  200. ((eq sig t) nil) ; eg defalias - can't check arguments
  201. ((eq sig 'err)
  202. "arglist not found") ; internal error
  203. ((not (equal (byte-compile-arglist-signature
  204. arglist)
  205. sig))
  206. "arglist mismatch")))))
  207. (when type
  208. (setq errlist (cons (list (car e) (cadr e) type) errlist))))
  209. errlist))
  210. (defun check-declare-sort (alist)
  211. "Sort a list with elements FILE (FNFILE ...).
  212. Returned list has elements FNFILE (FILE ...)."
  213. (let (file fnfile rest sort a)
  214. (dolist (e alist)
  215. (setq file (car e))
  216. (dolist (f (cdr e))
  217. (setq fnfile (car f)
  218. rest (cdr f))
  219. (if (setq a (assoc fnfile sort))
  220. (setcdr a (append (cdr a) (list (cons file rest))))
  221. (setq sort (cons (list fnfile (cons file rest)) sort)))))
  222. sort))
  223. (defun check-declare-warn (file fn fnfile type &optional line)
  224. "Warn that FILE made a false claim about FN in FNFILE.
  225. TYPE is a string giving the nature of the error.
  226. Optional LINE is the claim's line number; otherwise, search for the claim.
  227. Display warning in `check-declare-warning-buffer'."
  228. (let ((warning-prefix-function
  229. (lambda (level entry)
  230. (insert (format "%s:%d:" (file-relative-name file) (or line 0)))
  231. entry))
  232. (warning-fill-prefix " "))
  233. (unless line
  234. (with-current-buffer (find-file-noselect file)
  235. (goto-char (point-min))
  236. (when (and (not line)
  237. (re-search-forward
  238. (format "(declare-function[ \t\n]+%s" fn) nil t))
  239. (goto-char (match-beginning 0))
  240. (setq line (line-number-at-pos)))))
  241. (display-warning 'check-declare
  242. (format-message "said `%s' was defined in %s: %s"
  243. fn (file-relative-name fnfile) type)
  244. nil check-declare-warning-buffer)))
  245. (declare-function compilation-forget-errors "compile" ())
  246. (defun check-declare-files (&rest files)
  247. "Check veracity of all `declare-function' statements in FILES.
  248. Return a list of any errors found."
  249. (if (get-buffer check-declare-warning-buffer)
  250. (kill-buffer check-declare-warning-buffer))
  251. (let ((buf (get-buffer-create check-declare-warning-buffer))
  252. alist err errlist)
  253. (with-current-buffer buf
  254. (unless (derived-mode-p 'compilation-mode)
  255. (compilation-mode))
  256. (setq mode-line-process
  257. '(:propertize ":run" face compilation-mode-line-run))
  258. (let ((inhibit-read-only t))
  259. (insert "\f\n"))
  260. (compilation-forget-errors))
  261. (dolist (file files)
  262. (setq alist (cons (cons file (check-declare-scan file)) alist)))
  263. ;; Sort so that things are ordered by the files supposed to
  264. ;; contain the defuns.
  265. (dolist (e (check-declare-sort alist))
  266. (if (setq err (check-declare-verify (car e) (cdr e)))
  267. (setq errlist (cons (cons (car e) err) errlist))))
  268. (setq errlist (nreverse errlist))
  269. ;; Sort back again so that errors are ordered by the files
  270. ;; containing the declare-function statements.
  271. (dolist (e (check-declare-sort errlist))
  272. (dolist (f (cdr e))
  273. (check-declare-warn (car e) (cadr f) (car f) (nth 2 f))))
  274. (with-current-buffer buf
  275. (setq mode-line-process
  276. '(:propertize ":exit" face compilation-mode-line-run))
  277. (force-mode-line-update))
  278. errlist))
  279. ;;;###autoload
  280. (defun check-declare-file (file)
  281. "Check veracity of all `declare-function' statements in FILE.
  282. See `check-declare-directory' for more information."
  283. (interactive "fFile to check: ")
  284. (or (file-exists-p file)
  285. (error "File `%s' not found" file))
  286. (check-declare-files file))
  287. ;;;###autoload
  288. (defun check-declare-directory (root)
  289. "Check veracity of all `declare-function' statements under directory ROOT.
  290. Returns non-nil if any false statements are found."
  291. (interactive "DDirectory to check: ")
  292. (setq root (directory-file-name (file-relative-name root)))
  293. (or (file-directory-p root)
  294. (error "Directory `%s' not found" root))
  295. (let ((files (process-lines find-program root
  296. "-name" "*.el"
  297. "-exec" grep-program
  298. "-l" "^[ \t]*(declare-function" "{}" "+")))
  299. (when files
  300. (apply #'check-declare-files files))))
  301. (provide 'check-declare)
  302. ;;; check-declare.el ends here.