diff options
| -rw-r--r-- | lisp/ChangeLog | 30 | ||||
| -rw-r--r-- | lisp/info-xref.el | 629 |
2 files changed, 450 insertions, 209 deletions
diff --git a/lisp/ChangeLog b/lisp/ChangeLog index 0a4dc699953..e23e8df6993 100644 --- a/lisp/ChangeLog +++ b/lisp/ChangeLog | |||
| @@ -1,3 +1,33 @@ | |||
| 1 | 2011-01-16 Kevin Ryde <user42@zip.com.au> | ||
| 2 | |||
| 3 | * info-xref.el: Version 3. | ||
| 4 | (info-xref-check, info-xref-check-all): Move commentary details | ||
| 5 | into docstrings for better visibility. | ||
| 6 | Use compilation-mode for the results buffer. | ||
| 7 | (info-xref-output, info-xref-output-error, info-xref-with-output) | ||
| 8 | (info-xref-filename, info-xref-in-progress): | ||
| 9 | New internals for this. | ||
| 10 | (info-xref-check-list, info-xref-check-buffer) | ||
| 11 | (info-xref-check-all-custom): Use those. | ||
| 12 | (info-xref-output-buffer): Rename from info-xref-results-buffer. | ||
| 13 | (info-xref-output-heading): Rename from info-xref-filename-heading. | ||
| 14 | (info-xref-good, info-xref-bad, info-xref-xfile-alist) | ||
| 15 | (info-xref-filename-heading): Move to output managing section. | ||
| 16 | (info-xref-docstrings): New command checking "Info node `(foo)Bar'" | ||
| 17 | (info-xref-lock-file-p, info-xref-with-file): New helpers for it. | ||
| 18 | (info-xref-subfile-p): Move to generic section with those two. | ||
| 19 | (info-xref-check-node): New function split from | ||
| 20 | info-xref-check-buffer, shared by info-xref-docstrings. | ||
| 21 | (info-xref-goto-node-p): Move to a checking section with that func. | ||
| 22 | (info-xref-unavail): New counter. | ||
| 23 | (info-xref-check-node): Use it. | ||
| 24 | (info-xref-with-output): Show count of unavailables at end of output. | ||
| 25 | (info-xref-all-info-files): Exclude ".*" dotfiles. Ignore broken | ||
| 26 | symlinks. Exclude .texi files. Exclude Emacs backup files. | ||
| 27 | (info-xref-check-all-custom): Fix quietening viper-mode and | ||
| 28 | gnus-registry-install -- use setq not let so as not to unbind | ||
| 29 | after load. | ||
| 30 | |||
| 1 | 2011-01-16 Juri Linkov <juri@jurta.org> | 31 | 2011-01-16 Juri Linkov <juri@jurta.org> |
| 2 | 32 | ||
| 3 | * isearch.el (isearch-abort): Don't quit if search has | 33 | * isearch.el (isearch-abort): Don't quit if search has |
diff --git a/lisp/info-xref.el b/lisp/info-xref.el index 086ef474762..a92d9d89da6 100644 --- a/lisp/info-xref.el +++ b/lisp/info-xref.el | |||
| @@ -5,6 +5,7 @@ | |||
| 5 | 5 | ||
| 6 | ;; Author: Kevin Ryde <user42@zip.com.au> | 6 | ;; Author: Kevin Ryde <user42@zip.com.au> |
| 7 | ;; Keywords: docs | 7 | ;; Keywords: docs |
| 8 | ;; Version: 3 | ||
| 8 | 9 | ||
| 9 | ;; This file is part of GNU Emacs. | 10 | ;; This file is part of GNU Emacs. |
| 10 | 11 | ||
| @@ -23,56 +24,259 @@ | |||
| 23 | 24 | ||
| 24 | ;;; Commentary: | 25 | ;;; Commentary: |
| 25 | 26 | ||
| 26 | ;; This file implements some simple checking of external cross references in | 27 | ;; This is some simple checking of external cross references in info files, |
| 27 | ;; info files, by attempting to visit the nodes specified. | 28 | ;; docstrings and custom-links by attempting to visit the nodes specified. |
| 28 | ;; | 29 | ;; |
| 29 | ;; "makeinfo" checks references internal to a document, but not external | 30 | ;; `M-x info-xref-check' checks a single info file. See the docstring for |
| 30 | ;; references, which makes it rather easy for mistakes to creep in or node | 31 | ;; details. |
| 31 | ;; name changes to go unnoticed. `Info-validate' doesn't check external | ||
| 32 | ;; references either. | ||
| 33 | ;; | 32 | ;; |
| 34 | ;; `M-x info-xref-check' checks one file. When invoked from an Info-mode or | 33 | ;; `M-x info-xref-check-all' checks all info files in Info-directory-list. |
| 35 | ;; texinfo-mode buffer, the current info file is the default at the prompt. | 34 | ;; This is a good way to check the consistency of the whole system. |
| 36 | ;; | 35 | ;; |
| 37 | ;; `M-x info-xref-check-all' looks at everything in the normal info path. | 36 | ;; `M-x info-xref-check-all-custom' loads up all defcustom variables and |
| 38 | ;; This might be a lot of files but it's a good way to check the consistency | 37 | ;; checks any info references in them. |
| 39 | ;; of the whole system. | ||
| 40 | ;; | 38 | ;; |
| 41 | ;; Results are shown in a buffer. The format is a bit rough, but hopefully | 39 | ;; `M-x info-xref-docstrings' checks docstring "Info node ..." hyperlinks in |
| 42 | ;; there won't be too many problems normally, and correcting them is a | 40 | ;; source files (and other files). |
| 43 | ;; manual process anyway, a case of finding the right spot in the original | 41 | |
| 44 | ;; .texi and finding what node it ought to point to. | 42 | ;;; History: |
| 45 | ;; | 43 | |
| 46 | ;; When a target info file doesn't exist there's clearly no way to validate | 44 | ;; Version 3 - new M-x info-xref-docstrings, use compilation-mode |
| 47 | ;; node references within it. A message is given for missing target files | ||
| 48 | ;; (once per source document), it could be simply that the target hasn't | ||
| 49 | ;; been installed, or it could be a mistake in the reference. | ||
| 50 | ;; | ||
| 51 | ;; Indirect info files are understood, just pass the top-level foo.info to | ||
| 52 | ;; `info-xref-check' and it traverses all sub-files. Compressed info files | ||
| 53 | ;; are accepted too, as usual for `Info-mode'. | ||
| 54 | ;; | ||
| 55 | ;; `info-xref-check-all' is rather permissive in what it considers an info | ||
| 56 | ;; file. It has to be since info files don't necessarily have a ".info" | ||
| 57 | ;; suffix (eg. this is usual for the emacs manuals). One consequence of | ||
| 58 | ;; this is that if for instance there's a source code directory in | ||
| 59 | ;; `Info-directory-list' then a lot of extraneous files might be read, which | ||
| 60 | ;; will be time consuming but should be harmless. | ||
| 61 | ;; | ||
| 62 | ;; `M-x info-xref-check-all-custom' is a related command, it goes through | ||
| 63 | ;; all info document references in customizable variables, checking them | ||
| 64 | ;; like info file cross references. | ||
| 65 | 45 | ||
| 66 | ;;; Code: | 46 | ;;; Code: |
| 67 | 47 | ||
| 68 | (require 'info) | 48 | (require 'info) |
| 49 | (eval-when-compile | ||
| 50 | (require 'cl)) ;; for `incf' | ||
| 51 | |||
| 52 | ;;----------------------------------------------------------------------------- | ||
| 53 | ;; vaguely generic | ||
| 54 | |||
| 55 | (defun info-xref-lock-file-p (filename) | ||
| 56 | "Return non-nil if FILENAME is an Emacs lock file. | ||
| 57 | A lock file is \".#foo.txt\" etc per `lock-buffer'." | ||
| 58 | (string-match "\\(\\`\\|\\/\\)\\.#" filename)) | ||
| 59 | |||
| 60 | (defun info-xref-subfile-p (filename) | ||
| 61 | "Return t if FILENAME is an info subfile. | ||
| 62 | If removing the last \"-<NUM>\" from the filename gives a file | ||
| 63 | which exists, then consider FILENAME a subfile. This is an | ||
| 64 | imperfect test, probably ought to open up the purported top file | ||
| 65 | and see what subfiles it says." | ||
| 66 | (and (string-match "\\`\\(\\([^-]*-\\)*[^-]*\\)-[0-9]+\\(.*\\)\\'" filename) | ||
| 67 | (file-exists-p (concat (match-string 1 filename) | ||
| 68 | (match-string 3 filename))))) | ||
| 69 | |||
| 70 | (defmacro info-xref-with-file (filename &rest body) | ||
| 71 | ;; checkdoc-params: (filename body) | ||
| 72 | "Evaluate BODY in a buffer containing the contents of FILENAME. | ||
| 73 | If FILENAME is already in a buffer then that's used, otherwise a | ||
| 74 | temporary buffer. | ||
| 75 | |||
| 76 | The current implementation uses `insert-file-contents' rather | ||
| 77 | than `find-file-noselect' so as not to be held up by queries | ||
| 78 | about local variables or possible weirdness in a major mode. | ||
| 79 | `lm-with-file' does a similar thing, but it sets | ||
| 80 | `emacs-lisp-mode' which is not wanted here." | ||
| 81 | |||
| 82 | (declare (debug t) (indent 1)) | ||
| 83 | `(let* ((info-xref-with-file--filename ,filename) | ||
| 84 | (info-xref-with-file--body (lambda () ,@body)) | ||
| 85 | (info-xref-with-file--existing | ||
| 86 | (find-buffer-visiting info-xref-with-file--filename))) | ||
| 87 | (if info-xref-with-file--existing | ||
| 88 | (with-current-buffer info-xref-with-file--existing | ||
| 89 | (save-excursion | ||
| 90 | (funcall info-xref-with-file--body))) | ||
| 91 | (with-temp-buffer | ||
| 92 | (insert-file-contents ,filename) | ||
| 93 | (funcall info-xref-with-file--body))))) | ||
| 94 | |||
| 95 | |||
| 96 | ;;----------------------------------------------------------------------------- | ||
| 97 | ;; output buffer | ||
| 69 | 98 | ||
| 70 | (defconst info-xref-results-buffer "*info-xref results*" | 99 | (defconst info-xref-output-buffer "*info-xref results*" |
| 71 | "Name of the buffer for info-xref results.") | 100 | "Name of the buffer for info-xref results.") |
| 72 | 101 | ||
| 102 | (defvar info-xref-good 0 | ||
| 103 | "Count of good cross references, during info-xref processing.") | ||
| 104 | (defvar info-xref-bad 0 | ||
| 105 | "Count of bad cross references, during info-xref processing.") | ||
| 106 | (defvar info-xref-unavail 0 | ||
| 107 | "Count of unavailable cross references, during info-xref processing.") | ||
| 108 | |||
| 109 | (defvar info-xref-output-heading "" | ||
| 110 | "A heading string, during info-xref processing. | ||
| 111 | This is shown if there's an error, but not if successful.") | ||
| 112 | |||
| 113 | (defvar info-xref-filename nil | ||
| 114 | "The current buffer's filename, during info-xref processing. | ||
| 115 | When looking at file contents in a temp buffer there's no | ||
| 116 | `buffer-file-name', hence this variable.") | ||
| 117 | |||
| 118 | (defvar info-xref-xfile-alist nil | ||
| 119 | "Info files found or not found, during info-xref processing. | ||
| 120 | Key is \"(foo)\" etc and value nil or t according to whether info | ||
| 121 | manual \"(foo)\" exists or not. This is used to suppress | ||
| 122 | duplicate messages about foo not being available. (Duplicates | ||
| 123 | within one top-level file that is.)") | ||
| 124 | |||
| 125 | (defvar info-xref-in-progress nil) | ||
| 126 | (defmacro info-xref-with-output (&rest body) | ||
| 127 | "Run BODY with an info-xref output buffer. | ||
| 128 | This is meant to nest, so you can wrap it around a set of | ||
| 129 | different info-xref checks and have them write to the one output | ||
| 130 | buffer created by the outermost `info-xref-with-output', with an | ||
| 131 | overall good/bad count summary inserted at the very end." | ||
| 132 | |||
| 133 | (declare (debug t)) | ||
| 134 | `(save-excursion | ||
| 135 | (unless info-xref-in-progress | ||
| 136 | (display-buffer (get-buffer-create info-xref-output-buffer)) | ||
| 137 | (set-buffer info-xref-output-buffer) | ||
| 138 | (setq buffer-read-only nil) | ||
| 139 | (fundamental-mode) | ||
| 140 | (erase-buffer) | ||
| 141 | (insert ";; info-xref output -*- mode: compilation -*-\n\n") | ||
| 142 | (compilation-mode) | ||
| 143 | (setq info-xref-good 0 | ||
| 144 | info-xref-bad 0 | ||
| 145 | info-xref-unavail 0 | ||
| 146 | info-xref-xfile-alist nil)) | ||
| 147 | |||
| 148 | (let ((info-xref-in-progress t) | ||
| 149 | (info-xref-output-heading "")) | ||
| 150 | ,@body) | ||
| 151 | |||
| 152 | (unless info-xref-in-progress | ||
| 153 | (info-xref-output "done, %d good, %d bad, %d unavailable" | ||
| 154 | info-xref-good info-xref-bad info-xref-unavail)))) | ||
| 155 | |||
| 156 | (defun info-xref-output (fmt &rest args) | ||
| 157 | "Emit a `format'-ed message FMT+ARGS to the `info-xref-output-buffer'." | ||
| 158 | (with-current-buffer info-xref-output-buffer | ||
| 159 | (save-excursion | ||
| 160 | (goto-char (point-max)) | ||
| 161 | (let ((inhibit-read-only t)) | ||
| 162 | (insert info-xref-output-heading | ||
| 163 | (apply 'format fmt args) | ||
| 164 | "\n"))) | ||
| 165 | (setq info-xref-output-heading "") | ||
| 166 | ;; all this info-xref can be pretty slow, display now so the user sees | ||
| 167 | ;; some progress | ||
| 168 | (sit-for 0))) | ||
| 169 | (put 'info-xref-output 'byte-compile-format-like t) | ||
| 170 | |||
| 171 | (defun info-xref-output-error (fmt &rest args) | ||
| 172 | "Emit a `format'-ed error FMT+ARGS to the `info-xref-output-buffer'. | ||
| 173 | The error is attributed to `info-xref-filename' and the current | ||
| 174 | buffer's line and column of point." | ||
| 175 | (apply 'info-xref-output | ||
| 176 | (concat "%s:%s:%s: " fmt) | ||
| 177 | info-xref-filename | ||
| 178 | (1+ (count-lines (point-min) (line-beginning-position))) | ||
| 179 | (1+ (current-column)) | ||
| 180 | args)) | ||
| 181 | (put 'info-xref-output-error 'byte-compile-format-like t) | ||
| 182 | |||
| 183 | |||
| 184 | ;;----------------------------------------------------------------------------- | ||
| 185 | ;; node checking | ||
| 186 | |||
| 187 | ;; When asking Info-goto-node to fork, *info* needs to be the current | ||
| 188 | ;; buffer, otherwise it seems to clone the current buffer but then do the | ||
| 189 | ;; goto-node in plain *info*. | ||
| 190 | ;; | ||
| 191 | ;; We only fork if *info* already exists, if it doesn't then can create and | ||
| 192 | ;; destroy just that instead of a new name. | ||
| 193 | ;; | ||
| 194 | ;; If Info-goto-node can't find the file, then no new buffer is created. If | ||
| 195 | ;; it finds the file but not the node, then a buffer is created. Handle | ||
| 196 | ;; this difference by checking before killing. | ||
| 197 | ;; | ||
| 198 | (defun info-xref-goto-node-p (node) | ||
| 199 | "Return t if it's possible to go to the given NODE." | ||
| 200 | (let ((oldbuf (current-buffer))) | ||
| 201 | (save-excursion | ||
| 202 | (save-window-excursion | ||
| 203 | (prog1 | ||
| 204 | (condition-case err | ||
| 205 | (progn | ||
| 206 | (Info-goto-node node | ||
| 207 | (when (get-buffer "*info*") | ||
| 208 | (set-buffer "*info*") | ||
| 209 | "xref - temporary")) | ||
| 210 | t) | ||
| 211 | (error nil)) | ||
| 212 | (unless (equal (current-buffer) oldbuf) | ||
| 213 | (kill-buffer))))))) | ||
| 214 | |||
| 215 | (defun info-xref-check-node (node) | ||
| 216 | |||
| 217 | ;; Collapse spaces as per info.el and `help-make-xrefs'. | ||
| 218 | ;; Note defcustom :info-link nodes don't get this whitespace collapsing, | ||
| 219 | ;; they should be the exact node name ready to visit. | ||
| 220 | ;; `info-xref-check-all-custom' uses `info-xref-goto-node-p' and so | ||
| 221 | ;; doesn't come through here. | ||
| 222 | ;; | ||
| 223 | ;; Could use "[\t\n ]+" but try to avoid uselessly replacing " " with " ". | ||
| 224 | (setq node (replace-regexp-in-string "[\t\n][\t\n ]*\\| [\t\n ]+" " " | ||
| 225 | node t t)) | ||
| 226 | |||
| 227 | (if (not (string-match "\\`([^)]*)" node)) | ||
| 228 | (info-xref-output-error "no `(file)' part at start of node: %s\n" node) | ||
| 229 | (let ((file (match-string 0 node))) | ||
| 230 | |||
| 231 | (if (string-equal "()" file) | ||
| 232 | (info-xref-output-error "empty filename part: %s" node) | ||
| 233 | |||
| 234 | ;; see if the file exists, if haven't looked before | ||
| 235 | (unless (assoc file info-xref-xfile-alist) | ||
| 236 | (let ((found (info-xref-goto-node-p file))) | ||
| 237 | (push (cons file found) info-xref-xfile-alist) | ||
| 238 | (unless found | ||
| 239 | (info-xref-output-error "not available to check: %s\n (this reported once per file)" file)))) | ||
| 240 | |||
| 241 | ;; if the file exists, try the node | ||
| 242 | (cond ((not (cdr (assoc file info-xref-xfile-alist))) | ||
| 243 | (incf info-xref-unavail)) | ||
| 244 | ((info-xref-goto-node-p node) | ||
| 245 | (incf info-xref-good)) | ||
| 246 | (t | ||
| 247 | (incf info-xref-bad) | ||
| 248 | (info-xref-output-error "no such node: %s" node))))))) | ||
| 249 | |||
| 250 | |||
| 251 | ;;----------------------------------------------------------------------------- | ||
| 252 | |||
| 73 | ;;;###autoload | 253 | ;;;###autoload |
| 74 | (defun info-xref-check (filename) | 254 | (defun info-xref-check (filename) |
| 75 | "Check external references in FILENAME, an info document." | 255 | "Check external references in FILENAME, an info document. |
| 256 | Interactively from an `Info-mode' or `texinfo-mode' buffer the | ||
| 257 | current info file is the default. | ||
| 258 | |||
| 259 | Results are shown in a `compilation-mode' buffer. The format is | ||
| 260 | a bit rough, but there shouldn't be many problems normally. The | ||
| 261 | file:line:column: is the info document, but of course normally | ||
| 262 | any correction should be made in the original .texi file. | ||
| 263 | Finding the right place in the .texi is a manual process. | ||
| 264 | |||
| 265 | When a target info file doesn't exist there's obviously no way to | ||
| 266 | validate node references within it. A message is given for | ||
| 267 | missing target files once per source document. It could be | ||
| 268 | simply that you don't have the target installed, or it could be a | ||
| 269 | mistake in the reference. | ||
| 270 | |||
| 271 | Indirect info files are understood, just pass the top-level | ||
| 272 | foo.info to `info-xref-check' and it traverses all sub-files. | ||
| 273 | Compressed info files are accepted too as usual for `Info-mode'. | ||
| 274 | |||
| 275 | \"makeinfo\" checks references internal to an info document, but | ||
| 276 | not external references, which makes it rather easy for mistakes | ||
| 277 | to creep in or node name changes to go unnoticed. | ||
| 278 | `Info-validate' doesn't check external references either." | ||
| 279 | |||
| 76 | (interactive | 280 | (interactive |
| 77 | (list | 281 | (list |
| 78 | (let* ((default-filename | 282 | (let* ((default-filename |
| @@ -90,98 +294,80 @@ | |||
| 90 | (format "Info file (%s): " default-filename) | 294 | (format "Info file (%s): " default-filename) |
| 91 | "Info file: "))) | 295 | "Info file: "))) |
| 92 | (read-file-name prompt nil default-filename t)))) | 296 | (read-file-name prompt nil default-filename t)))) |
| 297 | |||
| 93 | (info-xref-check-list (list filename))) | 298 | (info-xref-check-list (list filename))) |
| 94 | 299 | ||
| 95 | ;;;###autoload | 300 | ;;;###autoload |
| 96 | (defun info-xref-check-all () | 301 | (defun info-xref-check-all () |
| 97 | "Check external references in all info documents in the usual path. | 302 | "Check external references in all info documents in the info path. |
| 98 | The usual path is `Info-directory-list' and `Info-additional-directory-list'." | 303 | `Info-directory-list' and `Info-additional-directory-list' are |
| 304 | the info paths. See `info-xref-check' for how each file is | ||
| 305 | checked. | ||
| 306 | |||
| 307 | The search for \"all\" info files is rather permissive, since | ||
| 308 | info files don't necessarily have a \".info\" extension and in | ||
| 309 | particular the Emacs manuals normally don't. If you have a | ||
| 310 | source code directory in `Info-directory-list' then a lot of | ||
| 311 | extraneous files might be read. This will be time consuming but | ||
| 312 | should be harmless." | ||
| 313 | |||
| 99 | (interactive) | 314 | (interactive) |
| 100 | (info-xref-check-list (info-xref-all-info-files))) | 315 | (info-xref-check-list (info-xref-all-info-files))) |
| 101 | 316 | ||
| 102 | ;; An alternative to trying to get only top-level files here would be to | 317 | ;; An alternative for geting only top-level files here would be to simply |
| 103 | ;; simply return all files, and have info-xref-check-list not follow | 318 | ;; return all files and have info-xref-check-list not follow "Indirect:". |
| 104 | ;; Indirect:. The current way seems a bit nicer though, because it gets the | 319 | ;; The current way seems better because it (potentially) gets the proper |
| 105 | ;; proper top-level filename into the error messages, and suppresses | 320 | ;; top-level filename into the error messages, and suppresses duplicate "not |
| 106 | ;; duplicate "not available" messages for all subfiles of a single document. | 321 | ;; available" messages for all subfiles of a single document. |
| 107 | 322 | ||
| 108 | (defun info-xref-all-info-files () | 323 | (defun info-xref-all-info-files () |
| 109 | "Return a list of all available info files. | 324 | "Return a list of all available info files. |
| 110 | Only top-level files are returned, subfiles are excluded. | 325 | Only top level files are returned, subfiles are excluded. |
| 111 | 326 | ||
| 112 | Since info files don't have to have a .info suffix, all files in the | 327 | Since info files don't have to have a .info suffix, all files in |
| 113 | relevant directories are considered, which might mean a lot of extraneous | 328 | the relevant directories are considered, which might mean a lot |
| 114 | things are returned if for instance a source code directory is in the path." | 329 | of extraneous things if for instance a source code directory is |
| 330 | in the path." | ||
| 115 | 331 | ||
| 116 | (info-initialize) ;; establish Info-directory-list | 332 | (info-initialize) ;; establish Info-directory-list |
| 117 | (apply 'nconc | 333 | (apply 'nconc |
| 118 | (mapcar | 334 | (mapcar |
| 119 | (lambda (dir) | 335 | (lambda (dir) |
| 120 | (let ((result nil)) | 336 | (let ((result nil)) |
| 121 | (dolist (name (directory-files dir t)) | 337 | (dolist (name (directory-files |
| 122 | (unless (or (file-directory-p name) (info-xref-subfile-p name)) | 338 | dir |
| 339 | t ;; absolute filenames | ||
| 340 | "\\`[^.]")) ;; not dotfiles, nor .# lockfiles | ||
| 341 | (when (and (file-exists-p name) ;; ignore broken symlinks | ||
| 342 | (not (string-match "\\.te?xi\\'" name)) ;; not .texi | ||
| 343 | (not (backup-file-name-p name)) | ||
| 344 | (not (file-directory-p name)) | ||
| 345 | (not (info-xref-subfile-p name))) | ||
| 123 | (push name result))) | 346 | (push name result))) |
| 124 | (nreverse result))) | 347 | (nreverse result))) |
| 125 | (append Info-directory-list Info-additional-directory-list)))) | 348 | (append Info-directory-list Info-additional-directory-list)))) |
| 126 | 349 | ||
| 127 | (defun info-xref-subfile-p (filename) | ||
| 128 | "Return t if FILENAME is an info subfile. | ||
| 129 | If removing the last \"-<NUM>\" from the filename gives a file that exists, | ||
| 130 | then consider FILENAME a subfile. This is an imperfect test, we probably | ||
| 131 | should open up the purported top file and see what subfiles it says." | ||
| 132 | (and (string-match "\\`\\(\\([^-]*-\\)*[^-]*\\)-[0-9]+\\(.*\\)\\'" filename) | ||
| 133 | (file-exists-p (concat (match-string 1 filename) | ||
| 134 | (match-string 3 filename))))) | ||
| 135 | |||
| 136 | |||
| 137 | ;; Some dynamic variables are used to share information with sub-functions | ||
| 138 | ;; below. | ||
| 139 | ;; | ||
| 140 | ;; info-xref-filename-header - a heading message for the current top-level | ||
| 141 | ;; filename, or "" when it's been printed. | ||
| 142 | ;; | ||
| 143 | (defvar info-xref-xfile-alist) | ||
| 144 | ;; | ||
| 145 | ;; info-xref-good - count of good cross references. | ||
| 146 | ;; | ||
| 147 | (defvar info-xref-good) | ||
| 148 | ;; | ||
| 149 | ;; info-xref-bad - count of bad cross references. | ||
| 150 | ;; | ||
| 151 | (defvar info-xref-bad) | ||
| 152 | ;; | ||
| 153 | ;; info-xref-xfile-alist - indexed by "(foo)" with value nil or t according | ||
| 154 | ;; to whether "(foo)" exists or not. This is used to suppress duplicate | ||
| 155 | ;; messages about foo not being available. (Duplicates within one | ||
| 156 | ;; top-level file that is.) | ||
| 157 | ;; | ||
| 158 | (defvar info-xref-filename-heading) | ||
| 159 | |||
| 160 | (defun info-xref-check-list (filename-list) | 350 | (defun info-xref-check-list (filename-list) |
| 161 | "Check external references in info documents in FILENAME-LIST." | 351 | "Check external references in info documents in FILENAME-LIST." |
| 162 | (pop-to-buffer info-xref-results-buffer t) | 352 | (info-xref-with-output |
| 163 | (erase-buffer) | ||
| 164 | (let ((info-xref-good 0) | ||
| 165 | (info-xref-bad 0)) | ||
| 166 | (dolist (info-xref-filename filename-list) | 353 | (dolist (info-xref-filename filename-list) |
| 167 | (let ((info-xref-filename-heading | 354 | (setq info-xref-xfile-alist nil) |
| 168 | (format "In file %s:\n" info-xref-filename)) | 355 | (let ((info-xref-output-heading |
| 169 | (info-xref-xfile-alist nil)) | 356 | (format "Info file %s\n" info-xref-filename))) |
| 170 | (with-temp-message (format "Looking at %s" info-xref-filename) | 357 | (with-temp-message (format "Looking at %s" info-xref-filename) |
| 171 | (with-temp-buffer | 358 | (with-temp-buffer |
| 172 | (info-insert-file-contents info-xref-filename) | 359 | (info-insert-file-contents info-xref-filename) |
| 173 | (goto-char (point-min)) | 360 | (goto-char (point-min)) |
| 174 | (if (re-search-forward "\^_\nIndirect:\n" nil t) | 361 | (if (search-forward "\^_\nIndirect:\n" nil t) |
| 175 | (let ((dir (file-name-directory info-xref-filename))) | 362 | (let ((dir (file-name-directory info-xref-filename))) |
| 176 | (while (looking-at "\\(.*\\): [0-9]+\n") | 363 | (while (looking-at "\\(.*\\): [0-9]+\n") |
| 177 | (let ((subfile (match-string 1))) | 364 | (let ((info-xref-filename |
| 365 | (expand-file-name (match-string 1) dir))) | ||
| 178 | (with-temp-buffer | 366 | (with-temp-buffer |
| 179 | (info-insert-file-contents | 367 | (info-insert-file-contents info-xref-filename) |
| 180 | (expand-file-name subfile dir)) | ||
| 181 | (info-xref-check-buffer))) | 368 | (info-xref-check-buffer))) |
| 182 | (forward-line))) | 369 | (forward-line))) |
| 183 | (info-xref-check-buffer)))))) | 370 | (info-xref-check-buffer)))))))) |
| 184 | (insert (format "done, %d good, %d bad\n" info-xref-good info-xref-bad)))) | ||
| 185 | 371 | ||
| 186 | (defun info-xref-check-buffer () | 372 | (defun info-xref-check-buffer () |
| 187 | "Check external references in the info file in the current buffer. | 373 | "Check external references in the info file in the current buffer. |
| @@ -190,127 +376,152 @@ This should be the raw file contents, not `Info-mode'." | |||
| 190 | (while (re-search-forward | 376 | (while (re-search-forward |
| 191 | "\\*[Nn]ote[ \n\t]+[^:]*:[ \n\t]+\\(\\(([^)]*)\\)[^.,]+\\)[.,]" | 377 | "\\*[Nn]ote[ \n\t]+[^:]*:[ \n\t]+\\(\\(([^)]*)\\)[^.,]+\\)[.,]" |
| 192 | nil t) | 378 | nil t) |
| 193 | (let* ((file (match-string 2)) | ||
| 194 | (node ;; Canonicalize spaces: we could use "[\t\n ]+" but | ||
| 195 | ;; we try to avoid uselessly replacing " " with " ". | ||
| 196 | (replace-regexp-in-string "[\t\n][\t\n ]*\\| [\t\n ]+" " " | ||
| 197 | (match-string 1) t t))) | ||
| 198 | (if (string-equal "()" file) | ||
| 199 | (info-xref-output "Empty filename part: %s\n" node) | ||
| 200 | ;; see if the file exists, if we haven't tried it before | ||
| 201 | (unless (assoc file info-xref-xfile-alist) | ||
| 202 | (let ((found (info-xref-goto-node-p file))) | ||
| 203 | (push (cons file found) info-xref-xfile-alist) | ||
| 204 | (unless found | ||
| 205 | (info-xref-output "Not available to check: %s\n" file)))) | ||
| 206 | ;; if the file exists, try the node | ||
| 207 | (when (cdr (assoc file info-xref-xfile-alist)) | ||
| 208 | (if (info-xref-goto-node-p node) | ||
| 209 | (setq info-xref-good (1+ info-xref-good)) | ||
| 210 | (setq info-xref-bad (1+ info-xref-bad)) | ||
| 211 | (info-xref-output "No such node: %s\n" node))))))) | ||
| 212 | |||
| 213 | (defun info-xref-output (str &rest args) | ||
| 214 | "Emit a `format'-ed message STR+ARGS to the info-xref output buffer." | ||
| 215 | (with-current-buffer info-xref-results-buffer | ||
| 216 | (insert info-xref-filename-heading | ||
| 217 | (apply 'format str args)) | ||
| 218 | (setq info-xref-filename-heading "") | ||
| 219 | ;; all this info-xref can be pretty slow, display now so the user can | ||
| 220 | ;; see some progress | ||
| 221 | (sit-for 0))) | ||
| 222 | |||
| 223 | ;; When asking Info-goto-node to fork, *info* needs to be the current | ||
| 224 | ;; buffer, otherwise it seems to clone the current buffer but then do the | ||
| 225 | ;; goto-node in plain *info*. | ||
| 226 | ;; | ||
| 227 | ;; We only fork if *info* already exists, if it doesn't then we can create | ||
| 228 | ;; and destroy just that instead of a new name. | ||
| 229 | ;; | ||
| 230 | ;; If Info-goto-node can't find the file, then no new buffer is created. If | ||
| 231 | ;; it finds the file but not the node, then a buffer is created. Handle | ||
| 232 | ;; this difference by checking before killing. | ||
| 233 | ;; | ||
| 234 | (defun info-xref-goto-node-p (node) | ||
| 235 | "Return t if it's possible to go to the given NODE." | ||
| 236 | (let ((oldbuf (current-buffer))) | ||
| 237 | (save-excursion | 379 | (save-excursion |
| 238 | (save-window-excursion | 380 | (goto-char (match-beginning 1)) ;; start of nodename as error position |
| 239 | (prog1 | 381 | (info-xref-check-node (match-string 1))))) |
| 240 | (condition-case err | 382 | |
| 241 | (progn | 383 | (defvar viper-mode) ;; quieten the byte compiler |
| 242 | (Info-goto-node node | 384 | (defvar gnus-registry-install) |
| 243 | (when (get-buffer "*info*") | ||
| 244 | (set-buffer "*info*") | ||
| 245 | "xref - temporary")) | ||
| 246 | t) | ||
| 247 | (error nil)) | ||
| 248 | (unless (equal (current-buffer) oldbuf) | ||
| 249 | (kill-buffer (current-buffer)))))))) | ||
| 250 | 385 | ||
| 251 | ;;;###autoload | 386 | ;;;###autoload |
| 252 | (defun info-xref-check-all-custom () | 387 | (defun info-xref-check-all-custom () |
| 253 | "Check info references in all customize groups and variables. | 388 | "Check info references in all customize groups and variables. |
| 254 | `custom-manual' and `info-link' entries in the `custom-links' list are checked. | 389 | Info references can be in `custom-manual' or `info-link' entries |
| 390 | of the `custom-links' for a variable. | ||
| 255 | 391 | ||
| 256 | `custom-load' autoloads for all symbols are loaded in order to get all the | 392 | Any `custom-load' autoloads in variables are loaded in order to |
| 257 | link information. This will be a lot of lisp packages loaded, and can take | 393 | get full link information. This will be a lot of Lisp packages |
| 258 | quite a while." | 394 | and can take a long time." |
| 259 | 395 | ||
| 260 | (interactive) | 396 | (interactive) |
| 261 | (pop-to-buffer info-xref-results-buffer t) | 397 | (info-xref-with-output |
| 262 | (erase-buffer) | 398 | |
| 263 | (let ((info-xref-filename-heading "")) | 399 | ;; `custom-load-symbol' is not used, since it quietly ignores errors, but |
| 264 | 400 | ;; we want to show them since they mean incomplete checking. | |
| 265 | ;; `custom-load-symbol' is not used, since it quietly ignores errors, | 401 | ;; |
| 266 | ;; but we want to show them (since they may mean incomplete checking). | 402 | ;; Just one pass through mapatoms is made. There shouldn't be any new |
| 267 | ;; | 403 | ;; custom-loads setup by packages loaded. |
| 268 | ;; Just one pass through mapatoms is made. There shouldn't be any new | 404 | ;; |
| 269 | ;; custom-loads setup by packages loaded. | 405 | (info-xref-output "Loading custom-load autoloads ...") |
| 270 | ;; | 406 | (require 'cus-start) |
| 271 | (info-xref-output "Loading custom-load autoloads ...\n") | 407 | (require 'cus-load) |
| 272 | (require 'cus-start) | 408 | |
| 273 | (require 'cus-load) | 409 | ;; These are `setq' rather than `let' since a let would unbind the |
| 274 | (let ((viper-mode nil)) ;; tell viper.el not to ask about viperizing | 410 | ;; variables after viper.el/gnus-registry.el have loaded, defeating the |
| 275 | (mapatoms | 411 | ;; defvars in those files. Of course it'd be better if those files |
| 276 | (lambda (symbol) | 412 | ;; didn't make interactive queries on loading at all, to allow for |
| 277 | (dolist (load (get symbol 'custom-loads)) | 413 | ;; programmatic loading like here. |
| 278 | (cond ((symbolp load) | 414 | (unless (boundp 'viper-mode) |
| 279 | (condition-case cause (require load) | 415 | (setq viper-mode nil)) ;; avoid viper.el ask about viperizing |
| 280 | (error | 416 | (unless (boundp 'gnus-registry-install) |
| 281 | (info-xref-output "Symbol `%s': cannot require '%s: %s\n" | 417 | (setq gnus-registry-install nil)) ;; avoid gnus-registery.el querying |
| 282 | symbol load cause)))) | 418 | |
| 283 | ;; skip if previously loaded | 419 | (mapatoms |
| 284 | ((assoc load load-history)) | 420 | (lambda (symbol) |
| 285 | ((assoc (locate-library load) load-history)) | 421 | (dolist (load (get symbol 'custom-loads)) |
| 286 | (t | 422 | (cond ((symbolp load) |
| 287 | (condition-case cause (load load) | 423 | (condition-case cause (require load) |
| 288 | (error | 424 | (error |
| 289 | (info-xref-output "Symbol `%s': cannot load \"%s\": %s\n" | 425 | (info-xref-output "Symbol `%s': cannot require '%s: %s" |
| 290 | symbol load cause))))))))) | 426 | symbol load cause)))) |
| 291 | 427 | ;; skip if previously loaded | |
| 292 | ;; Don't bother to check whether the info file exists as opposed to just | 428 | ((assoc load load-history)) |
| 293 | ;; a missing node. If you have the lisp then you should have the | 429 | ((assoc (locate-library load) load-history)) |
| 294 | ;; documentation, so missing node name will be the usual fault. | 430 | (t |
| 295 | ;; | 431 | (condition-case err |
| 296 | (info-xref-output "\nChecking custom-links references ...\n") | 432 | (load load) |
| 297 | (let ((good 0) | 433 | (error |
| 298 | (bad 0)) | 434 | (info-xref-output "Symbol `%s': cannot load \"%s\": %s" |
| 299 | (mapatoms | 435 | symbol load |
| 300 | (lambda (symbol) | 436 | (error-message-string err))))))))) |
| 301 | (dolist (link (get symbol 'custom-links)) | 437 | |
| 302 | (when (memq (car link) '(custom-manual info-link)) | 438 | ;; Don't bother to check whether the info file exists as opposed to just |
| 303 | ;; skip :tag part of (custom-manual :tag "Foo" "(foo)Node") | 439 | ;; a missing node. If you have the code then you should have the |
| 304 | (if (eq :tag (cadr link)) | 440 | ;; documentation, so a wrong node name will be the usual fault. |
| 305 | (setq link (cddr link))) | 441 | ;; |
| 306 | (if (info-xref-goto-node-p (cadr link)) | 442 | (info-xref-output "\nChecking custom-links references ...") |
| 307 | (setq good (1+ good)) | 443 | (mapatoms |
| 308 | (setq bad (1+ bad)) | 444 | (lambda (symbol) |
| 309 | ;; symbol-file gives nil for preloaded variables, would need | 445 | (dolist (link (get symbol 'custom-links)) |
| 310 | ;; to copy what describe-variable does to show the right place | 446 | (when (memq (car link) '(custom-manual info-link)) |
| 311 | (info-xref-output "Symbol `%s' (in %s): cannot goto node: %s\n" | 447 | ;; skip :tag part of (custom-manual :tag "Foo" "(foo)Node") |
| 312 | symbol (symbol-file symbol) (cadr link))))))) | 448 | (if (eq :tag (cadr link)) |
| 313 | (info-xref-output "%d good, %d bad\n" good bad)))) | 449 | (setq link (cddr link))) |
| 450 | (if (info-xref-goto-node-p (cadr link)) | ||
| 451 | (incf info-xref-good) | ||
| 452 | (incf info-xref-bad) | ||
| 453 | ;; symbol-file gives nil for preloaded variables, would need | ||
| 454 | ;; to copy what describe-variable does to show the right place | ||
| 455 | (info-xref-output "Symbol `%s' (file %s): cannot goto node: %s" | ||
| 456 | symbol | ||
| 457 | (symbol-file symbol 'defvar) | ||
| 458 | (cadr link))))))))) | ||
| 459 | |||
| 460 | ;;;###autoload | ||
| 461 | (defun info-xref-docstrings (filename-list) | ||
| 462 | ;; checkdoc-params: (filename-list) | ||
| 463 | "Check docstring info node references in source files. | ||
| 464 | The given files are searched for docstring hyperlinks like | ||
| 465 | |||
| 466 | Info node `(elisp)Documentation Tips' | ||
| 467 | |||
| 468 | and those links checked by attempting to visit the target nodes | ||
| 469 | as per `info-xref-check' does. | ||
| 470 | |||
| 471 | Interactively filenames are read as a wildcard pattern like | ||
| 472 | \"foo*.el\", with the current file as a default. Usually this | ||
| 473 | will be lisp sources, but anything with such hyperlinks can be | ||
| 474 | checked, including the Emacs .c sources (or the etc/DOC file of | ||
| 475 | all builtins). | ||
| 476 | |||
| 477 | Because info node hyperlinks are found by a simple regexp search | ||
| 478 | in the files, the Lisp code checked doesn't have to be loaded, | ||
| 479 | and links can be in the file commentary or elsewhere too. Even | ||
| 480 | .elc files can usually be checked successfully if you don't have | ||
| 481 | the sources handy." | ||
| 482 | |||
| 483 | (interactive | ||
| 484 | (let* ((default (and buffer-file-name | ||
| 485 | (file-relative-name buffer-file-name))) | ||
| 486 | (prompt (if default | ||
| 487 | (format "Filename with wildcards (%s): " | ||
| 488 | default) | ||
| 489 | "Filename with wildcards: ")) | ||
| 490 | (pattern (read-file-name prompt nil default)) | ||
| 491 | (filename-list (file-expand-wildcards pattern | ||
| 492 | t))) ;; absolute filenames | ||
| 493 | (eval-and-compile | ||
| 494 | (require 'cl)) ;; for `remove-if' | ||
| 495 | (setq filename-list (remove-if 'info-xref-lock-file-p filename-list)) | ||
| 496 | (unless filename-list | ||
| 497 | (error "No files: %S" pattern)) | ||
| 498 | (list filename-list))) | ||
| 499 | |||
| 500 | (eval-and-compile | ||
| 501 | (require 'help-mode)) ;; for `help-xref-info-regexp' | ||
| 502 | |||
| 503 | (info-xref-with-output | ||
| 504 | (dolist (info-xref-filename filename-list) | ||
| 505 | (setq info-xref-xfile-alist nil) ;; "not found"s once per file | ||
| 506 | |||
| 507 | (info-xref-with-file info-xref-filename | ||
| 508 | (goto-char (point-min)) | ||
| 509 | (while (re-search-forward help-xref-info-regexp nil t) | ||
| 510 | (let ((node (match-string 2))) | ||
| 511 | (save-excursion | ||
| 512 | (goto-char (match-beginning 2)) ;; start of node as error position | ||
| 513 | |||
| 514 | ;; skip nodes with "%" as probably `format' strings such as in | ||
| 515 | ;; info-look.el | ||
| 516 | (unless (string-match "%" node) | ||
| 517 | |||
| 518 | ;; "(emacs)" is the default manual for docstring hyperlinks, | ||
| 519 | ;; per `help-make-xrefs' | ||
| 520 | (unless (string-match "\\`(" node) | ||
| 521 | (setq node (concat "(emacs)" node))) | ||
| 522 | |||
| 523 | (info-xref-check-node node))))))))) | ||
| 524 | |||
| 314 | 525 | ||
| 315 | (provide 'info-xref) | 526 | (provide 'info-xref) |
| 316 | 527 | ||