(defgroup shadow nil
"Locate Emacs Lisp file shadowings."
:prefix "shadows-"
:group 'lisp)
(defcustom shadows-compare-text-p nil
"*If non-nil, then shadowing files are reported only if their text differs.
This is slower, but filters out some innocuous shadowing."
:type 'boolean
:group 'shadow)
(defun find-emacs-lisp-shadows (&optional path)
"Return a list of Emacs Lisp files that create shadows.
This function does the work for `list-load-path-shadows'.
We traverse PATH looking for shadows, and return a \(possibly empty\)
even-length list of files. A file in this list at position 2i shadows
the file in position 2i+1. Emacs Lisp file suffixes \(.el and .elc\)
are stripped from the file names in the list.
See the documentation for `list-load-path-shadows' for further information."
(or path (setq path load-path))
(let (true-names shadows files dir curr-files orig-dir files-seen-this-dir file)
(while path
(setq dir (directory-file-name (file-truename (or (car path) "."))))
(if (member dir true-names)
(or noninteractive
(and (car path)
(not (string= (car path) "."))
(message "Ignoring redundant directory %s" (car path))))
(setq true-names (append true-names (list dir)))
(setq dir (directory-file-name (or (car path) ".")))
(setq curr-files (if (file-accessible-directory-p dir)
(directory-files dir nil ".\\.elc?$" t)))
(and curr-files
(not noninteractive)
(message "Checking %d files in %s..." (length curr-files) dir))
(setq files-seen-this-dir nil)
(while curr-files
(setq file (car curr-files))
(setq file (substring
file 0 (if (string= (substring file -1) "c") -4 -3)))
(unless (or (member file files-seen-this-dir)
(member file '("subdirs")))
(setq files-seen-this-dir (cons file files-seen-this-dir))
(if (setq orig-dir (assoc file files))
(let ((base1 (concat (cdr orig-dir) "/" file))
(base2 (concat dir "/" file)))
(if (not (and shadows-compare-text-p
(shadow-same-file-or-nonexistent
(concat base1 ".el") (concat base2 ".el"))
(shadow-same-file-or-nonexistent
(concat base1 ".elc") (concat base2 ".elc"))))
(setq shadows
(append shadows (list base1 base2)))))
(setq files (cons (cons file dir) files))))
(setq curr-files (cdr curr-files))))
(setq path (cdr path)))
shadows))
(defun shadow-same-file-or-nonexistent (f1 f2)
(let ((exists1 (file-exists-p f1))
(exists2 (file-exists-p f2)))
(or (and (not exists1) (not exists2))
(and exists1 exists2
(or (equal (file-truename f1) (file-truename f2))
(and (= (nth 7 (file-attributes f1))
(nth 7 (file-attributes f2)))
(zerop (call-process "cmp" nil nil nil "-s" f1 f2))))))))
(defun list-load-path-shadows ()
"Display a list of Emacs Lisp files that shadow other files.
This function lists potential load-path problems. Directories in the
`load-path' variable are searched, in order, for Emacs Lisp
files. When a previously encountered file name is found again, a
message is displayed indicating that the later file is \"hidden\" by
the earlier.
For example, suppose `load-path' is set to
\(\"/usr/gnu/emacs/site-lisp\" \"/usr/gnu/emacs/share/emacs/19.30/lisp\"\)
and that each of these directories contains a file called XXX.el. Then
XXX.el in the site-lisp directory is referred to by all of:
\(require 'XXX\), \(autoload .... \"XXX\"\), \(load-library \"XXX\"\) etc.
The first XXX.el file prevents emacs from seeing the second \(unless
the second is loaded explicitly via load-file\).
When not intended, such shadowings can be the source of subtle
problems. For example, the above situation may have arisen because the
XXX package was not distributed with versions of emacs prior to
19.30. An emacs maintainer downloaded XXX from elsewhere and installed
it. Later, XXX was updated and included in the emacs distribution.
Unless the emacs maintainer checks for this, the new version of XXX
will be hidden behind the old \(which may no longer work with the new
emacs version\).
This function performs these checks and flags all possible
shadowings. Because a .el file may exist without a corresponding .elc
\(or vice-versa\), these suffixes are essentially ignored. A file
XXX.elc in an early directory \(that does not contain XXX.el\) is
considered to shadow a later file XXX.el, and vice-versa.
When run interactively, the shadowings \(if any\) are displayed in a
buffer called `*Shadows*'. Shadowings are located by calling the
\(non-interactive\) companion function, `find-emacs-lisp-shadows'."
(interactive)
(let* ((path (copy-sequence load-path))
(tem path)
toplevs)
(while tem
(if (file-exists-p (expand-file-name "simple.el" (car tem)))
(setq toplevs (cons (car tem) toplevs)))
(setq tem (cdr tem)))
(if (> (length toplevs) 1)
(let ((break (nth (- (length toplevs) 2) toplevs)))
(setq tem path)
(while tem
(if (eq (nth 1 tem) break)
(progn
(setcdr tem nil)
(setq tem nil)))
(setq tem (cdr tem)))))
(let* ((shadows (find-emacs-lisp-shadows path))
(n (/ (length shadows) 2))
(msg (format "%s Emacs Lisp load-path shadowing%s found"
(if (zerop n) "No" (concat "\n" (number-to-string n)))
(if (= n 1) " was" "s were"))))
(if (interactive-p)
(save-excursion
(let ((output-buffer (get-buffer-create "*Shadows*")))
(display-buffer output-buffer)
(set-buffer output-buffer)
(erase-buffer)
(while shadows
(insert (format "%s hides %s\n" (car shadows)
(car (cdr shadows))))
(setq shadows (cdr (cdr shadows))))
(insert msg "\n")))
(when shadows
(message "This site has duplicate Lisp libraries with the same name.
If a locally-installed Lisp library overrides a library in the Emacs release,
that can cause trouble, and you should probably remove the locally-installed
version unless you know what you are doing.\n")
(while shadows
(message "%s hides %s" (car shadows) (car (cdr shadows)))
(setq shadows (cdr (cdr shadows))))
(message "%s" msg))))))
(provide 'shadow)