Commit | Line | Data |
---|---|---|
d9026f5c JL |
1 | ;;; info-xref.el --- check external references in an Info document |
2 | ||
ab422c4d | 3 | ;; Copyright (C) 2003-2013 Free Software Foundation, Inc. |
dac15a1e | 4 | |
dac15a1e JB |
5 | ;; Author: Kevin Ryde <user42@zip.com.au> |
6 | ;; Keywords: docs | |
444ee8dd | 7 | ;; Version: 3 |
dac15a1e | 8 | |
d9026f5c JL |
9 | ;; This file is part of GNU Emacs. |
10 | ||
eb3fa2cf | 11 | ;; GNU Emacs is free software: you can redistribute it and/or modify |
d9026f5c | 12 | ;; it under the terms of the GNU General Public License as published by |
eb3fa2cf GM |
13 | ;; the Free Software Foundation, either version 3 of the License, or |
14 | ;; (at your option) any later version. | |
d9026f5c JL |
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 | |
eb3fa2cf | 22 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. |
dac15a1e JB |
23 | |
24 | ;;; Commentary: | |
25 | ||
444ee8dd KR |
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. | |
dac15a1e | 28 | ;; |
444ee8dd KR |
29 | ;; `M-x info-xref-check' checks a single info file. See the docstring for |
30 | ;; details. | |
dac15a1e | 31 | ;; |
444ee8dd KR |
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. | |
dac15a1e | 34 | ;; |
444ee8dd KR |
35 | ;; `M-x info-xref-check-all-custom' loads up all defcustom variables and |
36 | ;; checks any info references in them. | |
dac15a1e | 37 | ;; |
444ee8dd KR |
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 | |
dac15a1e | 44 | |
dac15a1e JB |
45 | ;;; Code: |
46 | ||
47 | (require 'info) | |
9b057d61 GM |
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 | ||
444ee8dd KR |
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 | |
dac15a1e | 114 | |
444ee8dd | 115 | (defconst info-xref-output-buffer "*info-xref results*" |
dac15a1e JB |
116 | "Name of the buffer for info-xref results.") |
117 | ||
444ee8dd KR |
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)))) | |
06b60517 | 171 | |
444ee8dd KR |
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 | |
06b60517 | 220 | (condition-case nil |
444ee8dd KR |
221 | (progn |
222 | (Info-goto-node node | |
223 | (when (get-buffer "*info*") | |
224 | (set-buffer "*info*") | |
9b057d61 GM |
225 | "xref - temporary") |
226 | (not info-xref-case-fold)) | |
444ee8dd KR |
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))) | |
f58e0fd5 | 260 | (cl-incf info-xref-unavail)) |
444ee8dd | 261 | ((info-xref-goto-node-p node) |
f58e0fd5 | 262 | (cl-incf info-xref-good)) |
444ee8dd | 263 | (t |
f58e0fd5 | 264 | (cl-incf info-xref-bad) |
444ee8dd KR |
265 | (info-xref-output-error "no such node: %s" node))))))) |
266 | ||
267 | ||
268 | ;;----------------------------------------------------------------------------- | |
269 | ||
dac15a1e JB |
270 | ;;;###autoload |
271 | (defun info-xref-check (filename) | |
444ee8dd KR |
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 | ||
dac15a1e JB |
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]*" | |
da05debc | 308 | (line-beginning-position 100) t) |
dac15a1e JB |
309 | (expand-file-name (match-string 1))))))) |
310 | (prompt (if default-filename | |
311 | (format "Info file (%s): " default-filename) | |
312 | "Info file: "))) | |
da05debc | 313 | (read-file-name prompt nil default-filename t)))) |
444ee8dd | 314 | |
dac15a1e JB |
315 | (info-xref-check-list (list filename))) |
316 | ||
317 | ;;;###autoload | |
318 | (defun info-xref-check-all () | |
444ee8dd KR |
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 | ||
dac15a1e JB |
331 | (interactive) |
332 | (info-xref-check-list (info-xref-all-info-files))) | |
333 | ||
3ed8598c | 334 | ;; An alternative for getting only top-level files here would be to simply |
444ee8dd KR |
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. | |
dac15a1e JB |
339 | |
340 | (defun info-xref-all-info-files () | |
341 | "Return a list of all available info files. | |
444ee8dd | 342 | Only top level files are returned, subfiles are excluded. |
dac15a1e | 343 | |
444ee8dd KR |
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." | |
dac15a1e JB |
348 | |
349 | (info-initialize) ;; establish Info-directory-list | |
da05debc | 350 | (apply 'nconc |
dac15a1e JB |
351 | (mapcar |
352 | (lambda (dir) | |
353 | (let ((result nil)) | |
444ee8dd KR |
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))) | |
da05debc SM |
363 | (push name result))) |
364 | (nreverse result))) | |
dac15a1e JB |
365 | (append Info-directory-list Info-additional-directory-list)))) |
366 | ||
dac15a1e JB |
367 | (defun info-xref-check-list (filename-list) |
368 | "Check external references in info documents in FILENAME-LIST." | |
444ee8dd | 369 | (info-xref-with-output |
dac15a1e | 370 | (dolist (info-xref-filename filename-list) |
444ee8dd KR |
371 | (setq info-xref-xfile-alist nil) |
372 | (let ((info-xref-output-heading | |
373 | (format "Info file %s\n" info-xref-filename))) | |
dac15a1e JB |
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)) | |
444ee8dd | 378 | (if (search-forward "\^_\nIndirect:\n" nil t) |
dac15a1e JB |
379 | (let ((dir (file-name-directory info-xref-filename))) |
380 | (while (looking-at "\\(.*\\): [0-9]+\n") | |
444ee8dd KR |
381 | (let ((info-xref-filename |
382 | (expand-file-name (match-string 1) dir))) | |
dac15a1e | 383 | (with-temp-buffer |
444ee8dd | 384 | (info-insert-file-contents info-xref-filename) |
dac15a1e JB |
385 | (info-xref-check-buffer))) |
386 | (forward-line))) | |
444ee8dd | 387 | (info-xref-check-buffer)))))))) |
dac15a1e | 388 | |
f2136e1e GM |
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 | ||
dac15a1e JB |
406 | (defun info-xref-check-buffer () |
407 | "Check external references in the info file in the current buffer. | |
399a26ce | 408 | This should be the raw file contents, not `Info-mode'." |
dac15a1e | 409 | (goto-char (point-min)) |
f2136e1e | 410 | (while (re-search-forward info-xref-note-re nil t) |
dac15a1e | 411 | (save-excursion |
444ee8dd KR |
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) | |
dac15a1e | 417 | |
0332a905 JL |
418 | ;;;###autoload |
419 | (defun info-xref-check-all-custom () | |
420 | "Check info references in all customize groups and variables. | |
444ee8dd KR |
421 | Info references can be in `custom-manual' or `info-link' entries |
422 | of the `custom-links' for a variable. | |
0332a905 | 423 | |
444ee8dd KR |
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." | |
0332a905 JL |
427 | |
428 | (interactive) | |
444ee8dd KR |
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) | |
40ba43b4 | 449 | (setq gnus-registry-install nil)) ;; avoid gnus-registry.el querying |
444ee8dd KR |
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)) | |
f58e0fd5 SM |
483 | (cl-incf info-xref-good) |
484 | (cl-incf info-xref-bad) | |
444ee8dd KR |
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." | |
444ee8dd | 514 | (interactive |
a767645f | 515 | (let* ((default (and buffer-file-name |
444ee8dd | 516 | (file-relative-name buffer-file-name))) |
a767645f GM |
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) | |
e5c7913c | 528 | (file-directory-p file) |
a767645f | 529 | (push file newlist)))) |
444ee8dd KR |
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 | ||
0332a905 | 559 | |
dac15a1e JB |
560 | (provide 'info-xref) |
561 | ||
562 | ;;; info-xref.el ends here |