Commit | Line | Data |
---|---|---|
d9026f5c JL |
1 | ;;; info-xref.el --- check external references in an Info document |
2 | ||
114f9c96 | 3 | ;; Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 |
ae940284 | 4 | ;; Free Software Foundation, Inc. |
dac15a1e | 5 | |
dac15a1e JB |
6 | ;; Author: Kevin Ryde <user42@zip.com.au> |
7 | ;; Keywords: docs | |
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 | ||
26 | ;; This file implements some simple checking of external cross references in | |
27 | ;; info files, by attempting to visit the nodes specified. | |
28 | ;; | |
29 | ;; "makeinfo" checks references internal to a document, but not external | |
30 | ;; references, which makes it rather easy for mistakes to creep in or node | |
31 | ;; name changes to go unnoticed. `Info-validate' doesn't check external | |
32 | ;; references either. | |
33 | ;; | |
34 | ;; `M-x info-xref-check' checks one file. When invoked from an Info-mode or | |
35 | ;; texinfo-mode buffer, the current info file is the default at the prompt. | |
36 | ;; | |
37 | ;; `M-x info-xref-check-all' looks at everything in the normal info path. | |
38 | ;; This might be a lot of files but it's a good way to check the consistency | |
39 | ;; of the whole system. | |
40 | ;; | |
41 | ;; Results are shown in a buffer. The format is a bit rough, but hopefully | |
42 | ;; there won't be too many problems normally, and correcting them is a | |
43 | ;; manual process anyway, a case of finding the right spot in the original | |
44 | ;; .texi and finding what node it ought to point to. | |
45 | ;; | |
46 | ;; When a target info file doesn't exist there's clearly no way to validate | |
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. | |
0332a905 | 61 | ;; |
0332a905 JL |
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. | |
dac15a1e | 65 | |
dac15a1e JB |
66 | ;;; Code: |
67 | ||
68 | (require 'info) | |
69 | ||
70 | (defconst info-xref-results-buffer "*info-xref results*" | |
71 | "Name of the buffer for info-xref results.") | |
72 | ||
73 | ;;;###autoload | |
74 | (defun info-xref-check (filename) | |
75 | "Check external references in FILENAME, an info document." | |
76 | (interactive | |
77 | (list | |
78 | (let* ((default-filename | |
79 | (cond ((eq major-mode 'Info-mode) | |
80 | Info-current-file) | |
81 | ((eq major-mode 'texinfo-mode) | |
82 | ;; look for @setfilename like makeinfo.el does | |
83 | (save-excursion | |
84 | (goto-char (point-min)) | |
85 | (if (re-search-forward | |
86 | "^@setfilename[ \t]+\\([^ \t\n]+\\)[ \t]*" | |
da05debc | 87 | (line-beginning-position 100) t) |
dac15a1e JB |
88 | (expand-file-name (match-string 1))))))) |
89 | (prompt (if default-filename | |
90 | (format "Info file (%s): " default-filename) | |
91 | "Info file: "))) | |
da05debc | 92 | (read-file-name prompt nil default-filename t)))) |
dac15a1e JB |
93 | (info-xref-check-list (list filename))) |
94 | ||
95 | ;;;###autoload | |
96 | (defun info-xref-check-all () | |
97 | "Check external references in all info documents in the usual path. | |
98 | The usual path is `Info-directory-list' and `Info-additional-directory-list'." | |
99 | (interactive) | |
100 | (info-xref-check-list (info-xref-all-info-files))) | |
101 | ||
102 | ;; An alternative to trying to get only top-level files here would be to | |
103 | ;; simply return all files, and have info-xref-check-list not follow | |
104 | ;; Indirect:. The current way seems a bit nicer though, because it gets the | |
105 | ;; proper top-level filename into the error messages, and suppresses | |
106 | ;; duplicate "not available" messages for all subfiles of a single document. | |
107 | ||
108 | (defun info-xref-all-info-files () | |
109 | "Return a list of all available info files. | |
110 | Only top-level files are returned, subfiles are excluded. | |
111 | ||
112 | Since info files don't have to have a .info suffix, all files in the | |
113 | relevant directories are considered, which might mean a lot of extraneous | |
114 | things are returned if for instance a source code directory is in the path." | |
115 | ||
116 | (info-initialize) ;; establish Info-directory-list | |
da05debc | 117 | (apply 'nconc |
dac15a1e JB |
118 | (mapcar |
119 | (lambda (dir) | |
120 | (let ((result nil)) | |
121 | (dolist (name (directory-files dir t)) | |
da05debc SM |
122 | (unless (or (file-directory-p name) (info-xref-subfile-p name)) |
123 | (push name result))) | |
124 | (nreverse result))) | |
dac15a1e JB |
125 | (append Info-directory-list Info-additional-directory-list)))) |
126 | ||
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 | ;; | |
623f1465 JB |
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) | |
dac15a1e JB |
159 | |
160 | (defun info-xref-check-list (filename-list) | |
161 | "Check external references in info documents in FILENAME-LIST." | |
162 | (pop-to-buffer info-xref-results-buffer t) | |
163 | (erase-buffer) | |
164 | (let ((info-xref-good 0) | |
165 | (info-xref-bad 0)) | |
166 | (dolist (info-xref-filename filename-list) | |
167 | (let ((info-xref-filename-heading | |
168 | (format "In file %s:\n" info-xref-filename)) | |
169 | (info-xref-xfile-alist nil)) | |
170 | (with-temp-message (format "Looking at %s" info-xref-filename) | |
171 | (with-temp-buffer | |
172 | (info-insert-file-contents info-xref-filename) | |
173 | (goto-char (point-min)) | |
174 | (if (re-search-forward "\^_\nIndirect:\n" nil t) | |
175 | (let ((dir (file-name-directory info-xref-filename))) | |
176 | (while (looking-at "\\(.*\\): [0-9]+\n") | |
177 | (let ((subfile (match-string 1))) | |
178 | (with-temp-buffer | |
179 | (info-insert-file-contents | |
180 | (expand-file-name subfile dir)) | |
181 | (info-xref-check-buffer))) | |
182 | (forward-line))) | |
183 | (info-xref-check-buffer)))))) | |
184 | (insert (format "done, %d good, %d bad\n" info-xref-good info-xref-bad)))) | |
185 | ||
186 | (defun info-xref-check-buffer () | |
187 | "Check external references in the info file in the current buffer. | |
399a26ce | 188 | This should be the raw file contents, not `Info-mode'." |
dac15a1e JB |
189 | (goto-char (point-min)) |
190 | (while (re-search-forward | |
0332a905 | 191 | "\\*[Nn]ote[ \n\t]+[^:]*:[ \n\t]+\\(\\(([^)]*)\\)[^.,]+\\)[.,]" |
dac15a1e JB |
192 | nil t) |
193 | (let* ((file (match-string 2)) | |
da05debc SM |
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))) | |
0332a905 JL |
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)) | |
dac15a1e JB |
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)) | |
0332a905 | 211 | (info-xref-output "No such node: %s\n" node))))))) |
dac15a1e | 212 | |
0332a905 JL |
213 | (defun info-xref-output (str &rest args) |
214 | "Emit a `format'-ed message STR+ARGS to the info-xref output buffer." | |
dac15a1e | 215 | (with-current-buffer info-xref-results-buffer |
0332a905 JL |
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))) | |
dac15a1e JB |
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) | |
da05debc | 235 | "Return t if it's possible to go to the given NODE." |
dac15a1e JB |
236 | (let ((oldbuf (current-buffer))) |
237 | (save-excursion | |
238 | (save-window-excursion | |
239 | (prog1 | |
240 | (condition-case err | |
241 | (progn | |
242 | (Info-goto-node node | |
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 | ||
0332a905 JL |
251 | ;;;###autoload |
252 | (defun info-xref-check-all-custom () | |
253 | "Check info references in all customize groups and variables. | |
254 | `custom-manual' and `info-link' entries in the `custom-links' list are checked. | |
255 | ||
256 | `custom-load' autoloads for all symbols are loaded in order to get all the | |
257 | link information. This will be a lot of lisp packages loaded, and can take | |
258 | quite a while." | |
259 | ||
260 | (interactive) | |
261 | (pop-to-buffer info-xref-results-buffer t) | |
262 | (erase-buffer) | |
263 | (let ((info-xref-filename-heading "")) | |
264 | ||
265 | ;; `custom-load-symbol' is not used, since it quietly ignores errors, | |
266 | ;; but we want to show them (since they may mean incomplete checking). | |
267 | ;; | |
268 | ;; Just one pass through mapatoms is made. There shouldn't be any new | |
269 | ;; custom-loads setup by packages loaded. | |
270 | ;; | |
271 | (info-xref-output "Loading custom-load autoloads ...\n") | |
272 | (require 'cus-start) | |
273 | (require 'cus-load) | |
274 | (let ((viper-mode nil)) ;; tell viper.el not to ask about viperizing | |
275 | (mapatoms | |
276 | (lambda (symbol) | |
277 | (dolist (load (get symbol 'custom-loads)) | |
278 | (cond ((symbolp load) | |
279 | (condition-case cause (require load) | |
280 | (error | |
281 | (info-xref-output "Symbol `%s': cannot require '%s: %s\n" | |
282 | symbol load cause)))) | |
283 | ;; skip if previously loaded | |
284 | ((assoc load load-history)) | |
285 | ((assoc (locate-library load) load-history)) | |
286 | (t | |
287 | (condition-case cause (load load) | |
288 | (error | |
289 | (info-xref-output "Symbol `%s': cannot load \"%s\": %s\n" | |
290 | symbol load cause))))))))) | |
291 | ||
292 | ;; Don't bother to check whether the info file exists as opposed to just | |
293 | ;; a missing node. If you have the lisp then you should have the | |
294 | ;; documentation, so missing node name will be the usual fault. | |
295 | ;; | |
296 | (info-xref-output "\nChecking custom-links references ...\n") | |
297 | (let ((good 0) | |
298 | (bad 0)) | |
299 | (mapatoms | |
300 | (lambda (symbol) | |
301 | (dolist (link (get symbol 'custom-links)) | |
302 | (when (memq (car link) '(custom-manual info-link)) | |
8a5e4b3b EZ |
303 | ;; skip :tag part of (custom-manual :tag "Foo" "(foo)Node") |
304 | (if (eq :tag (cadr link)) | |
305 | (setq link (cddr link))) | |
306 | (if (info-xref-goto-node-p (cadr link)) | |
0332a905 JL |
307 | (setq good (1+ good)) |
308 | (setq bad (1+ bad)) | |
309 | ;; symbol-file gives nil for preloaded variables, would need | |
310 | ;; to copy what describe-variable does to show the right place | |
311 | (info-xref-output "Symbol `%s' (in %s): cannot goto node: %s\n" | |
312 | symbol (symbol-file symbol) (cadr link))))))) | |
313 | (info-xref-output "%d good, %d bad\n" good bad)))) | |
314 | ||
dac15a1e JB |
315 | (provide 'info-xref) |
316 | ||
cbee283d | 317 | ;; arch-tag: 69d4d528-69ed-4cc2-8eb4-c666a0c1d5ac |
dac15a1e | 318 | ;;; info-xref.el ends here |