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