changeset 112281:697cfa263439

info-xref.el Version 3. * lisp/info-xref.el (info-xref-check, info-xref-check-all): Move commentary details into docstrings for better visibility. Use compilation-mode for the results buffer. (info-xref-output, info-xref-output-error, info-xref-with-output) (info-xref-filename, info-xref-in-progress): New internals for this. (info-xref-check-list, info-xref-check-buffer) (info-xref-check-all-custom): Use those. (info-xref-output-buffer): Rename from info-xref-results-buffer. (info-xref-output-heading): Rename from info-xref-filename-heading. (info-xref-good, info-xref-bad, info-xref-xfile-alist) (info-xref-filename-heading): Move to output managing section. (info-xref-docstrings): New command checking "Info node `(foo)Bar'" (info-xref-lock-file-p, info-xref-with-file): New helpers for it. (info-xref-subfile-p): Move to generic section with those two. (info-xref-check-node): New function split from info-xref-check-buffer, shared by info-xref-docstrings. (info-xref-goto-node-p): Move to a checking section with that func. (info-xref-unavail): New counter. (info-xref-check-node): Use it. (info-xref-with-output): Show count of unavailables at end of output. (info-xref-all-info-files): Exclude ".*" dotfiles. Ignore broken symlinks. Exclude .texi files. Exclude Emacs backup files. (info-xref-check-all-custom): Fix quietening viper-mode and gnus-registry-install -- use setq not let so as not to unbind after load.
author Glenn Morris <rgm@gnu.org>
date Sat, 15 Jan 2011 17:59:33 -0800
parents a0a140d8d84d
children bcf4b132f3d5
files lisp/ChangeLog lisp/info-xref.el
diffstat 2 files changed, 474 insertions(+), 233 deletions(-) [+]
line wrap: on
line diff
--- a/lisp/ChangeLog	Sun Jan 16 01:04:22 2011 +0000
+++ b/lisp/ChangeLog	Sat Jan 15 17:59:33 2011 -0800
@@ -1,3 +1,33 @@
+2011-01-16  Kevin Ryde  <user42@zip.com.au>
+
+	* info-xref.el: Version 3.
+	(info-xref-check, info-xref-check-all): Move commentary details
+	into docstrings for better visibility.
+	Use compilation-mode for the results buffer.
+	(info-xref-output, info-xref-output-error, info-xref-with-output)
+	(info-xref-filename, info-xref-in-progress):
+	New internals for this.
+	(info-xref-check-list, info-xref-check-buffer)
+	(info-xref-check-all-custom): Use those.
+	(info-xref-output-buffer): Rename from info-xref-results-buffer.
+	(info-xref-output-heading): Rename from info-xref-filename-heading.
+	(info-xref-good, info-xref-bad, info-xref-xfile-alist)
+	(info-xref-filename-heading): Move to output managing section.
+	(info-xref-docstrings): New command checking "Info node	`(foo)Bar'"
+	(info-xref-lock-file-p, info-xref-with-file): New helpers for it.
+	(info-xref-subfile-p): Move to generic section with those two.
+	(info-xref-check-node): New function split from
+	info-xref-check-buffer, shared by info-xref-docstrings.
+	(info-xref-goto-node-p): Move to a checking section with that func.
+	(info-xref-unavail): New counter.
+	(info-xref-check-node): Use it.
+	(info-xref-with-output): Show count of unavailables at end of output.
+	(info-xref-all-info-files): Exclude ".*" dotfiles.  Ignore broken
+	symlinks.  Exclude .texi files.	 Exclude Emacs backup files.
+	(info-xref-check-all-custom): Fix quietening viper-mode and
+	gnus-registry-install -- use setq not let so as not to unbind
+	after load.
+
 2011-01-16  Juri Linkov  <juri@jurta.org>
 
 	* isearch.el (isearch-abort): Don't quit if search has
--- a/lisp/info-xref.el	Sun Jan 16 01:04:22 2011 +0000
+++ b/lisp/info-xref.el	Sat Jan 15 17:59:33 2011 -0800
@@ -5,6 +5,7 @@
 
 ;; Author: Kevin Ryde <user42@zip.com.au>
 ;; Keywords: docs
+;; Version: 3
 
 ;; This file is part of GNU Emacs.
 
@@ -23,209 +24,172 @@
 
 ;;; Commentary:
 
-;; This file implements some simple checking of external cross references in
-;; info files, by attempting to visit the nodes specified.
-;;
-;; "makeinfo" checks references internal to a document, but not external
-;; references, which makes it rather easy for mistakes to creep in or node
-;; name changes to go unnoticed.  `Info-validate' doesn't check external
-;; references either.
+;; This is some simple checking of external cross references in info files,
+;; docstrings and custom-links by attempting to visit the nodes specified.
 ;;
-;; `M-x info-xref-check' checks one file.  When invoked from an Info-mode or
-;; texinfo-mode buffer, the current info file is the default at the prompt.
+;; `M-x info-xref-check' checks a single info file.  See the docstring for
+;; details.
 ;;
-;; `M-x info-xref-check-all' looks at everything in the normal info path.
-;; This might be a lot of files but it's a good way to check the consistency
-;; of the whole system.
-;;
-;; Results are shown in a buffer.  The format is a bit rough, but hopefully
-;; there won't be too many problems normally, and correcting them is a
-;; manual process anyway, a case of finding the right spot in the original
-;; .texi and finding what node it ought to point to.
+;; `M-x info-xref-check-all' checks all info files in Info-directory-list.
+;; This is a good way to check the consistency of the whole system.
 ;;
-;; When a target info file doesn't exist there's clearly no way to validate
-;; node references within it.  A message is given for missing target files
-;; (once per source document), it could be simply that the target hasn't
-;; been installed, or it could be a mistake in the reference.
-;;
-;; Indirect info files are understood, just pass the top-level foo.info to
-;; `info-xref-check' and it traverses all sub-files.  Compressed info files
-;; are accepted too, as usual for `Info-mode'.
+;; `M-x info-xref-check-all-custom' loads up all defcustom variables and
+;; checks any info references in them.
 ;;
-;; `info-xref-check-all' is rather permissive in what it considers an info
-;; file.  It has to be since info files don't necessarily have a ".info"
-;; suffix (eg. this is usual for the emacs manuals).  One consequence of
-;; this is that if for instance there's a source code directory in
-;; `Info-directory-list' then a lot of extraneous files might be read, which
-;; will be time consuming but should be harmless.
-;;
-;; `M-x info-xref-check-all-custom' is a related command, it goes through
-;; all info document references in customizable variables, checking them
-;; like info file cross references.
+;; `M-x info-xref-docstrings' checks docstring "Info node ..." hyperlinks in
+;; source files (and other files).
+
+;;; History:
+
+;; Version 3 - new M-x info-xref-docstrings, use compilation-mode
 
 ;;; Code:
 
 (require 'info)
-
-(defconst info-xref-results-buffer "*info-xref results*"
-  "Name of the buffer for info-xref results.")
-
-;;;###autoload
-(defun info-xref-check (filename)
-  "Check external references in FILENAME, an info document."
-  (interactive
-   (list
-    (let* ((default-filename
-             (cond ((eq major-mode 'Info-mode)
-                    Info-current-file)
-                   ((eq major-mode 'texinfo-mode)
-                    ;; look for @setfilename like makeinfo.el does
-                    (save-excursion
-                      (goto-char (point-min))
-                      (if (re-search-forward
-                           "^@setfilename[ \t]+\\([^ \t\n]+\\)[ \t]*"
-                           (line-beginning-position 100) t)
-                          (expand-file-name (match-string 1)))))))
-           (prompt (if default-filename
-                       (format "Info file (%s): " default-filename)
-                     "Info file: ")))
-      (read-file-name prompt nil default-filename t))))
-  (info-xref-check-list (list filename)))
+(eval-when-compile
+  (require 'cl)) ;; for `incf'
 
-;;;###autoload
-(defun info-xref-check-all ()
-  "Check external references in all info documents in the usual path.
-The usual path is `Info-directory-list' and `Info-additional-directory-list'."
-  (interactive)
-  (info-xref-check-list (info-xref-all-info-files)))
-
-;; An alternative to trying to get only top-level files here would be to
-;; simply return all files, and have info-xref-check-list not follow
-;; Indirect:.  The current way seems a bit nicer though, because it gets the
-;; proper top-level filename into the error messages, and suppresses
-;; duplicate "not available" messages for all subfiles of a single document.
+;;-----------------------------------------------------------------------------
+;; vaguely generic
 
-(defun info-xref-all-info-files ()
-  "Return a list of all available info files.
-Only top-level files are returned, subfiles are excluded.
-
-Since info files don't have to have a .info suffix, all files in the
-relevant directories are considered, which might mean a lot of extraneous
-things are returned if for instance a source code directory is in the path."
-
-  (info-initialize) ;; establish Info-directory-list
-  (apply 'nconc
-         (mapcar
-          (lambda (dir)
-            (let ((result nil))
-              (dolist (name (directory-files dir t))
-                (unless (or (file-directory-p name) (info-xref-subfile-p name))
-		  (push name result)))
-              (nreverse result)))
-          (append Info-directory-list Info-additional-directory-list))))
+(defun info-xref-lock-file-p (filename)
+  "Return non-nil if FILENAME is an Emacs lock file.
+A lock file is \".#foo.txt\" etc per `lock-buffer'."
+  (string-match "\\(\\`\\|\\/\\)\\.#" filename))
 
 (defun info-xref-subfile-p (filename)
   "Return t if FILENAME is an info subfile.
-If removing the last \"-<NUM>\" from the filename gives a file that exists,
-then consider FILENAME a subfile.  This is an imperfect test, we probably
-should open up the purported top file and see what subfiles it says."
+If removing the last \"-<NUM>\" from the filename gives a file
+which exists, then consider FILENAME a subfile.  This is an
+imperfect test, probably ought to open up the purported top file
+and see what subfiles it says."
   (and (string-match "\\`\\(\\([^-]*-\\)*[^-]*\\)-[0-9]+\\(.*\\)\\'" filename)
        (file-exists-p (concat (match-string 1 filename)
                               (match-string 3 filename)))))
 
+(defmacro info-xref-with-file (filename &rest body)
+  ;; checkdoc-params: (filename body)
+  "Evaluate BODY in a buffer containing the contents of FILENAME.
+If FILENAME is already in a buffer then that's used, otherwise a
+temporary buffer.
 
-;; Some dynamic variables are used to share information with sub-functions
-;; below.
-;;
-;; info-xref-filename-header - a heading message for the current top-level
-;;     filename, or "" when it's been printed.
-;;
-(defvar info-xref-xfile-alist)
-;;
-;; info-xref-good - count of good cross references.
-;;
-(defvar info-xref-good)
-;;
-;; info-xref-bad - count of bad cross references.
-;;
-(defvar info-xref-bad)
-;;
-;; info-xref-xfile-alist - indexed by "(foo)" with value nil or t according
-;;     to whether "(foo)" exists or not.  This is used to suppress duplicate
-;;     messages about foo not being available.  (Duplicates within one
-;;     top-level file that is.)
-;;
-(defvar info-xref-filename-heading)
+The current implementation uses `insert-file-contents' rather
+than `find-file-noselect' so as not to be held up by queries
+about local variables or possible weirdness in a major mode.
+`lm-with-file' does a similar thing, but it sets
+`emacs-lisp-mode' which is not wanted here."
+
+  (declare (debug t) (indent 1))
+  `(let* ((info-xref-with-file--filename ,filename)
+          (info-xref-with-file--body     (lambda () ,@body))
+          (info-xref-with-file--existing
+           (find-buffer-visiting info-xref-with-file--filename)))
+     (if info-xref-with-file--existing
+         (with-current-buffer info-xref-with-file--existing
+           (save-excursion
+             (funcall info-xref-with-file--body)))
+       (with-temp-buffer
+         (insert-file-contents ,filename)
+         (funcall info-xref-with-file--body)))))
+
+
+;;-----------------------------------------------------------------------------
+;; output buffer
+
+(defconst info-xref-output-buffer "*info-xref results*"
+  "Name of the buffer for info-xref results.")
+
+(defvar info-xref-good 0
+  "Count of good cross references, during info-xref processing.")
+(defvar info-xref-bad 0
+  "Count of bad cross references, during info-xref processing.")
+(defvar info-xref-unavail 0
+  "Count of unavailable cross references, during info-xref processing.")
+
+(defvar info-xref-output-heading ""
+  "A heading string, during info-xref processing.
+This is shown if there's an error, but not if successful.")
+
+(defvar info-xref-filename nil
+  "The current buffer's filename, during info-xref processing.
+When looking at file contents in a temp buffer there's no
+`buffer-file-name', hence this variable.")
+
+(defvar info-xref-xfile-alist nil
+  "Info files found or not found, during info-xref processing.
+Key is \"(foo)\" etc and value nil or t according to whether info
+manual \"(foo)\" exists or not.  This is used to suppress
+duplicate messages about foo not being available.  (Duplicates
+within one top-level file that is.)")
 
-(defun info-xref-check-list (filename-list)
-  "Check external references in info documents in FILENAME-LIST."
-  (pop-to-buffer info-xref-results-buffer t)
-  (erase-buffer)
-  (let ((info-xref-good 0)
-        (info-xref-bad  0))
-    (dolist (info-xref-filename filename-list)
-      (let ((info-xref-filename-heading
-             (format "In file %s:\n" info-xref-filename))
-            (info-xref-xfile-alist nil))
-        (with-temp-message (format "Looking at %s" info-xref-filename)
-          (with-temp-buffer
-            (info-insert-file-contents info-xref-filename)
-            (goto-char (point-min))
-            (if (re-search-forward "\^_\nIndirect:\n" nil t)
-                (let ((dir (file-name-directory info-xref-filename)))
-                  (while (looking-at "\\(.*\\): [0-9]+\n")
-                    (let ((subfile (match-string 1)))
-                      (with-temp-buffer
-                        (info-insert-file-contents
-                         (expand-file-name subfile dir))
-                        (info-xref-check-buffer)))
-                    (forward-line)))
-              (info-xref-check-buffer))))))
-    (insert (format "done, %d good, %d bad\n" info-xref-good info-xref-bad))))
+(defvar info-xref-in-progress nil)
+(defmacro info-xref-with-output (&rest body)
+  "Run BODY with an info-xref output buffer.
+This is meant to nest, so you can wrap it around a set of
+different info-xref checks and have them write to the one output
+buffer created by the outermost `info-xref-with-output', with an
+overall good/bad count summary inserted at the very end."
+
+  (declare (debug t))
+  `(save-excursion
+     (unless info-xref-in-progress
+       (display-buffer (get-buffer-create info-xref-output-buffer))
+       (set-buffer info-xref-output-buffer)
+       (setq buffer-read-only nil)
+       (fundamental-mode)
+       (erase-buffer)
+       (insert ";; info-xref output -*- mode: compilation -*-\n\n")
+       (compilation-mode)
+       (setq info-xref-good    0
+             info-xref-bad     0
+             info-xref-unavail 0
+             info-xref-xfile-alist nil))
+
+     (let ((info-xref-in-progress t)
+           (info-xref-output-heading ""))
+       ,@body)
 
-(defun info-xref-check-buffer ()
-  "Check external references in the info file in the current buffer.
-This should be the raw file contents, not `Info-mode'."
-  (goto-char (point-min))
-  (while (re-search-forward
-          "\\*[Nn]ote[ \n\t]+[^:]*:[ \n\t]+\\(\\(([^)]*)\\)[^.,]+\\)[.,]"
-          nil t)
-    (let* ((file (match-string 2))
-           (node ;; Canonicalize spaces: we could use "[\t\n ]+" but
-	    ;; we try to avoid uselessly replacing " " with " ".
-	    (replace-regexp-in-string "[\t\n][\t\n ]*\\| [\t\n ]+" " "
-				      (match-string 1) t t)))
-      (if (string-equal "()" file)
-          (info-xref-output "Empty filename part: %s\n" node)
-        ;; see if the file exists, if we haven't tried it before
-        (unless (assoc file info-xref-xfile-alist)
-          (let ((found (info-xref-goto-node-p file)))
-            (push (cons file found) info-xref-xfile-alist)
-            (unless found
-              (info-xref-output "Not available to check: %s\n" file))))
-        ;; if the file exists, try the node
-        (when (cdr (assoc file info-xref-xfile-alist))
-          (if (info-xref-goto-node-p node)
-              (setq info-xref-good (1+ info-xref-good))
-            (setq info-xref-bad (1+ info-xref-bad))
-            (info-xref-output "No such node: %s\n" node)))))))
+     (unless info-xref-in-progress
+       (info-xref-output "done, %d good, %d bad, %d unavailable"
+                         info-xref-good info-xref-bad info-xref-unavail))))
+  
+(defun info-xref-output (fmt &rest args)
+  "Emit a `format'-ed message FMT+ARGS to the `info-xref-output-buffer'."
+  (with-current-buffer info-xref-output-buffer
+    (save-excursion
+      (goto-char (point-max))
+      (let ((inhibit-read-only t))
+        (insert info-xref-output-heading
+                (apply 'format fmt args)
+                "\n")))
+    (setq info-xref-output-heading "")
+    ;; all this info-xref can be pretty slow, display now so the user sees
+    ;; some progress
+    (sit-for 0)))
+(put 'info-xref-output 'byte-compile-format-like t)
 
-(defun info-xref-output (str &rest args)
-  "Emit a `format'-ed message STR+ARGS to the info-xref output buffer."
-  (with-current-buffer info-xref-results-buffer
-    (insert info-xref-filename-heading
-            (apply 'format str args))
-    (setq info-xref-filename-heading "")
-    ;; all this info-xref can be pretty slow, display now so the user can
-    ;; see some progress
-    (sit-for 0)))
+(defun info-xref-output-error (fmt &rest args)
+  "Emit a `format'-ed error FMT+ARGS to the `info-xref-output-buffer'.
+The error is attributed to `info-xref-filename' and the current
+buffer's line and column of point."
+  (apply 'info-xref-output
+         (concat "%s:%s:%s: " fmt)
+         info-xref-filename
+         (1+ (count-lines (point-min) (line-beginning-position)))
+         (1+ (current-column))
+         args))
+(put 'info-xref-output-error 'byte-compile-format-like t)
+
+
+;;-----------------------------------------------------------------------------
+;; node checking
 
 ;; When asking Info-goto-node to fork, *info* needs to be the current
 ;; buffer, otherwise it seems to clone the current buffer but then do the
 ;; goto-node in plain *info*.
 ;;
-;; We only fork if *info* already exists, if it doesn't then we can create
-;; and destroy just that instead of a new name.
+;; We only fork if *info* already exists, if it doesn't then can create and
+;; destroy just that instead of a new name.
 ;;
 ;; If Info-goto-node can't find the file, then no new buffer is created.  If
 ;; it finds the file but not the node, then a buffer is created.  Handle
@@ -246,71 +210,318 @@
                   t)
               (error nil))
           (unless (equal (current-buffer) oldbuf)
-            (kill-buffer (current-buffer))))))))
+            (kill-buffer)))))))
+
+(defun info-xref-check-node (node)
+
+  ;; Collapse spaces as per info.el and `help-make-xrefs'.
+  ;; Note defcustom :info-link nodes don't get this whitespace collapsing,
+  ;; they should be the exact node name ready to visit.
+  ;; `info-xref-check-all-custom' uses `info-xref-goto-node-p' and so
+  ;; doesn't come through here.
+  ;;
+  ;; Could use "[\t\n ]+" but try to avoid uselessly replacing " " with " ".
+  (setq node (replace-regexp-in-string "[\t\n][\t\n ]*\\| [\t\n ]+" " "
+                                       node t t))
+
+  (if (not (string-match "\\`([^)]*)" node))
+      (info-xref-output-error "no `(file)' part at start of node: %s\n" node)
+    (let ((file (match-string 0 node)))
+
+      (if (string-equal "()" file)
+          (info-xref-output-error "empty filename part: %s" node)
+
+        ;; see if the file exists, if haven't looked before
+        (unless (assoc file info-xref-xfile-alist)
+          (let ((found (info-xref-goto-node-p file)))
+            (push (cons file found) info-xref-xfile-alist)
+            (unless found
+              (info-xref-output-error "not available to check: %s\n    (this reported once per file)" file))))
+
+        ;; if the file exists, try the node
+        (cond ((not (cdr (assoc file info-xref-xfile-alist)))
+               (incf info-xref-unavail))
+              ((info-xref-goto-node-p node)
+               (incf info-xref-good))
+              (t
+               (incf info-xref-bad)
+               (info-xref-output-error "no such node: %s" node)))))))
+
+
+;;-----------------------------------------------------------------------------
+
+;;;###autoload
+(defun info-xref-check (filename)
+  "Check external references in FILENAME, an info document.
+Interactively from an `Info-mode' or `texinfo-mode' buffer the
+current info file is the default.
+
+Results are shown in a `compilation-mode' buffer.  The format is
+a bit rough, but there shouldn't be many problems normally.  The
+file:line:column: is the info document, but of course normally
+any correction should be made in the original .texi file.
+Finding the right place in the .texi is a manual process.
+
+When a target info file doesn't exist there's obviously no way to
+validate node references within it.  A message is given for
+missing target files once per source document.  It could be
+simply that you don't have the target installed, or it could be a
+mistake in the reference.
+
+Indirect info files are understood, just pass the top-level
+foo.info to `info-xref-check' and it traverses all sub-files.
+Compressed info files are accepted too as usual for `Info-mode'.
+
+\"makeinfo\" checks references internal to an info document, but
+not external references, which makes it rather easy for mistakes
+to creep in or node name changes to go unnoticed.
+`Info-validate' doesn't check external references either."
+
+  (interactive
+   (list
+    (let* ((default-filename
+             (cond ((eq major-mode 'Info-mode)
+                    Info-current-file)
+                   ((eq major-mode 'texinfo-mode)
+                    ;; look for @setfilename like makeinfo.el does
+                    (save-excursion
+                      (goto-char (point-min))
+                      (if (re-search-forward
+                           "^@setfilename[ \t]+\\([^ \t\n]+\\)[ \t]*"
+                           (line-beginning-position 100) t)
+                          (expand-file-name (match-string 1)))))))
+           (prompt (if default-filename
+                       (format "Info file (%s): " default-filename)
+                     "Info file: ")))
+      (read-file-name prompt nil default-filename t))))
+
+  (info-xref-check-list (list filename)))
+
+;;;###autoload
+(defun info-xref-check-all ()
+  "Check external references in all info documents in the info path.
+`Info-directory-list' and `Info-additional-directory-list' are
+the info paths.  See `info-xref-check' for how each file is
+checked.
+
+The search for \"all\" info files is rather permissive, since
+info files don't necessarily have a \".info\" extension and in
+particular the Emacs manuals normally don't.  If you have a
+source code directory in `Info-directory-list' then a lot of
+extraneous files might be read.  This will be time consuming but
+should be harmless."
+
+  (interactive)
+  (info-xref-check-list (info-xref-all-info-files)))
+
+;; An alternative for geting only top-level files here would be to simply
+;; return all files and have info-xref-check-list not follow "Indirect:".
+;; The current way seems better because it (potentially) gets the proper
+;; top-level filename into the error messages, and suppresses duplicate "not
+;; available" messages for all subfiles of a single document.
+
+(defun info-xref-all-info-files ()
+  "Return a list of all available info files.
+Only top level files are returned, subfiles are excluded.
+
+Since info files don't have to have a .info suffix, all files in
+the relevant directories are considered, which might mean a lot
+of extraneous things if for instance a source code directory is
+in the path."
+
+  (info-initialize) ;; establish Info-directory-list
+  (apply 'nconc
+         (mapcar
+          (lambda (dir)
+            (let ((result nil))
+              (dolist (name (directory-files
+                             dir
+                             t           ;; absolute filenames
+                             "\\`[^.]")) ;; not dotfiles, nor .# lockfiles
+                (when (and (file-exists-p name) ;; ignore broken symlinks
+                           (not (string-match "\\.te?xi\\'" name)) ;; not .texi
+                           (not (backup-file-name-p name))
+                           (not (file-directory-p name))
+                           (not (info-xref-subfile-p name)))
+		  (push name result)))
+              (nreverse result)))
+          (append Info-directory-list Info-additional-directory-list))))
+
+(defun info-xref-check-list (filename-list)
+  "Check external references in info documents in FILENAME-LIST."
+  (info-xref-with-output
+    (dolist (info-xref-filename filename-list)
+      (setq info-xref-xfile-alist nil)
+      (let ((info-xref-output-heading
+             (format "Info file %s\n" info-xref-filename)))
+        (with-temp-message (format "Looking at %s" info-xref-filename)
+          (with-temp-buffer
+            (info-insert-file-contents info-xref-filename)
+            (goto-char (point-min))
+            (if (search-forward "\^_\nIndirect:\n" nil t)
+                (let ((dir (file-name-directory info-xref-filename)))
+                  (while (looking-at "\\(.*\\): [0-9]+\n")
+                    (let ((info-xref-filename
+                           (expand-file-name (match-string 1) dir)))
+                      (with-temp-buffer
+                        (info-insert-file-contents info-xref-filename)
+                        (info-xref-check-buffer)))
+                    (forward-line)))
+              (info-xref-check-buffer))))))))
+
+(defun info-xref-check-buffer ()
+  "Check external references in the info file in the current buffer.
+This should be the raw file contents, not `Info-mode'."
+  (goto-char (point-min))
+  (while (re-search-forward
+          "\\*[Nn]ote[ \n\t]+[^:]*:[ \n\t]+\\(\\(([^)]*)\\)[^.,]+\\)[.,]"
+          nil t)
+    (save-excursion
+      (goto-char (match-beginning 1)) ;; start of nodename as error position
+      (info-xref-check-node (match-string 1)))))
+
+(defvar viper-mode) ;; quieten the byte compiler
+(defvar gnus-registry-install)
 
 ;;;###autoload
 (defun info-xref-check-all-custom ()
   "Check info references in all customize groups and variables.
-`custom-manual' and `info-link' entries in the `custom-links' list are checked.
+Info references can be in `custom-manual' or `info-link' entries
+of the `custom-links' for a variable.
 
-`custom-load' autoloads for all symbols are loaded in order to get all the
-link information.  This will be a lot of lisp packages loaded, and can take
-quite a while."
+Any `custom-load' autoloads in variables are loaded in order to
+get full link information.  This will be a lot of Lisp packages
+and can take a long time."
 
   (interactive)
-  (pop-to-buffer info-xref-results-buffer t)
-  (erase-buffer)
-  (let ((info-xref-filename-heading ""))
+  (info-xref-with-output
+
+   ;; `custom-load-symbol' is not used, since it quietly ignores errors, but
+   ;; we want to show them since they mean incomplete checking.
+   ;;
+   ;; Just one pass through mapatoms is made.  There shouldn't be any new
+   ;; custom-loads setup by packages loaded.
+   ;;
+   (info-xref-output "Loading custom-load autoloads ...")
+   (require 'cus-start)
+   (require 'cus-load)
+
+   ;; These are `setq' rather than `let' since a let would unbind the
+   ;; variables after viper.el/gnus-registry.el have loaded, defeating the
+   ;; defvars in those files.  Of course it'd be better if those files
+   ;; didn't make interactive queries on loading at all, to allow for
+   ;; programmatic loading like here.
+   (unless (boundp 'viper-mode)
+     (setq viper-mode nil))  ;; avoid viper.el ask about viperizing
+   (unless (boundp 'gnus-registry-install)
+     (setq gnus-registry-install nil))  ;; avoid gnus-registery.el querying
+
+   (mapatoms
+    (lambda (symbol)
+      (dolist (load (get symbol 'custom-loads))
+        (cond ((symbolp load)
+               (condition-case cause (require load)
+                 (error
+                  (info-xref-output "Symbol `%s': cannot require '%s: %s"
+                                    symbol load cause))))
+              ;; skip if previously loaded
+              ((assoc load load-history))
+              ((assoc (locate-library load) load-history))
+              (t
+               (condition-case err
+                   (load load)
+                 (error
+                  (info-xref-output "Symbol `%s': cannot load \"%s\": %s"
+                                    symbol load
+                                    (error-message-string err)))))))))
+
+   ;; Don't bother to check whether the info file exists as opposed to just
+   ;; a missing node.  If you have the code then you should have the
+   ;; documentation, so a wrong node name will be the usual fault.
+   ;;
+   (info-xref-output "\nChecking custom-links references ...")
+   (mapatoms
+    (lambda (symbol)
+      (dolist (link (get symbol 'custom-links))
+        (when (memq (car link) '(custom-manual info-link))
+          ;; skip :tag part of (custom-manual :tag "Foo" "(foo)Node")
+          (if (eq :tag (cadr link))
+              (setq link (cddr link)))
+          (if (info-xref-goto-node-p (cadr link))
+              (incf info-xref-good)
+            (incf info-xref-bad)
+            ;; symbol-file gives nil for preloaded variables, would need
+            ;; to copy what describe-variable does to show the right place
+            (info-xref-output "Symbol `%s' (file %s): cannot goto node: %s"
+                              symbol
+                              (symbol-file symbol 'defvar)
+                              (cadr link)))))))))
 
-    ;; `custom-load-symbol' is not used, since it quietly ignores errors,
-    ;; but we want to show them (since they may mean incomplete checking).
-    ;;
-    ;; Just one pass through mapatoms is made.  There shouldn't be any new
-    ;; custom-loads setup by packages loaded.
-    ;;
-    (info-xref-output "Loading custom-load autoloads ...\n")
-    (require 'cus-start)
-    (require 'cus-load)
-    (let ((viper-mode nil)) ;; tell viper.el not to ask about viperizing
-      (mapatoms
-       (lambda (symbol)
-         (dolist (load (get symbol 'custom-loads))
-           (cond ((symbolp load)
-                  (condition-case cause (require load)
-                    (error
-                     (info-xref-output "Symbol `%s': cannot require '%s: %s\n"
-                                       symbol load cause))))
-                 ;; skip if previously loaded
-                 ((assoc load load-history))
-                 ((assoc (locate-library load) load-history))
-                 (t
-                  (condition-case cause (load load)
-                    (error
-                     (info-xref-output "Symbol `%s': cannot load \"%s\": %s\n"
-                                       symbol load cause)))))))))
+;;;###autoload
+(defun info-xref-docstrings (filename-list)
+  ;; checkdoc-params: (filename-list)
+  "Check docstring info node references in source files.
+The given files are searched for docstring hyperlinks like
+
+    Info node `(elisp)Documentation Tips'
+
+and those links checked by attempting to visit the target nodes
+as per `info-xref-check' does.
+
+Interactively filenames are read as a wildcard pattern like
+\"foo*.el\", with the current file as a default.  Usually this
+will be lisp sources, but anything with such hyperlinks can be
+checked, including the Emacs .c sources (or the etc/DOC file of
+all builtins).
+
+Because info node hyperlinks are found by a simple regexp search
+in the files, the Lisp code checked doesn't have to be loaded,
+and links can be in the file commentary or elsewhere too.  Even
+.elc files can usually be checked successfully if you don't have
+the sources handy."
 
-    ;; Don't bother to check whether the info file exists as opposed to just
-    ;; a missing node.  If you have the lisp then you should have the
-    ;; documentation, so missing node name will be the usual fault.
-    ;;
-    (info-xref-output "\nChecking custom-links references ...\n")
-    (let ((good 0)
-          (bad  0))
-      (mapatoms
-       (lambda (symbol)
-         (dolist (link (get symbol 'custom-links))
-           (when (memq (car link) '(custom-manual info-link))
-	     ;; skip :tag part of (custom-manual :tag "Foo" "(foo)Node")
-	     (if (eq :tag (cadr link))
-		 (setq link (cddr link)))
-	     (if (info-xref-goto-node-p (cadr link))
-                 (setq good (1+ good))
-               (setq bad (1+ bad))
-               ;; symbol-file gives nil for preloaded variables, would need
-               ;; to copy what describe-variable does to show the right place
-               (info-xref-output "Symbol `%s' (in %s): cannot goto node: %s\n"
-                                 symbol (symbol-file symbol) (cadr link)))))))
-      (info-xref-output "%d good, %d bad\n" good bad))))
+  (interactive
+   (let* ((default       (and buffer-file-name
+                              (file-relative-name buffer-file-name)))
+          (prompt        (if default
+                             (format "Filename with wildcards (%s): "
+                                     default)
+                           "Filename with wildcards: "))
+          (pattern       (read-file-name prompt nil default))
+          (filename-list (file-expand-wildcards pattern
+                                                t))) ;; absolute filenames
+     (eval-and-compile
+       (require 'cl)) ;; for `remove-if'
+     (setq filename-list (remove-if 'info-xref-lock-file-p filename-list))
+     (unless filename-list
+       (error "No files: %S" pattern))
+     (list filename-list)))
+
+  (eval-and-compile
+    (require 'help-mode)) ;; for `help-xref-info-regexp'
+
+  (info-xref-with-output
+   (dolist (info-xref-filename filename-list)
+     (setq info-xref-xfile-alist nil)  ;; "not found"s once per file
+
+     (info-xref-with-file info-xref-filename
+       (goto-char (point-min))
+       (while (re-search-forward help-xref-info-regexp nil t)
+         (let ((node (match-string 2)))
+           (save-excursion
+             (goto-char (match-beginning 2)) ;; start of node as error position
+
+             ;; skip nodes with "%" as probably `format' strings such as in
+             ;; info-look.el
+             (unless (string-match "%" node)
+
+               ;; "(emacs)" is the default manual for docstring hyperlinks,
+               ;; per `help-make-xrefs'
+               (unless (string-match "\\`(" node)
+                 (setq node (concat "(emacs)" node)))
+
+               (info-xref-check-node node)))))))))
+
 
 (provide 'info-xref)