diff --git a/admin/README b/admin/README index 3e86319f2a3..3f2aae3fe84 100644 --- a/admin/README +++ b/admin/README @@ -53,6 +53,10 @@ be used to debug Emacs with dense colormaps (PseudoColor). Check doc strings against documentation. +** cl-lib-deps-report.el + +Audit Lisp files for cl-lib usage and missing requires. + ** cus-test.el Tests for custom types and load problems. diff --git a/admin/cl-lib-deps-report.el b/admin/cl-lib-deps-report.el new file mode 100755 index 00000000000..37d741161ac --- /dev/null +++ b/admin/cl-lib-deps-report.el @@ -0,0 +1,162 @@ +:;exec emacs -Q --batch -l "$0" -- "$@" # -*- lexical-binding: t -*- +;;; cl-lib-deps-report.el --- report cl-lib dependencies in lisp files -*- lexical-binding: t -*- + +;; Copyright (C) 2026 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;; Generate an Org report of cl-lib macro/function usage and missing +;; compile-time/runtime requires for files under lisp/. + +;;; Code: + +(require 'cl-lib) +(require 'org) + +(setq debug-on-error nil) + +(defun cl-lib-deps-report--scan-file (file symbol-re macros funcs) + "Return cl-lib usage data for FILE using SYMBOL-RE, MACROS, and FUNCS. +Exclude tokens found in strings or comments, and return a list with +dependency flags, require kind, and sorted symbol lists." + (with-temp-buffer + (insert-file-contents file) + (with-syntax-table emacs-lisp-mode-syntax-table + (let ((tokens '()) + (total-req 0) + (eval-req 0)) + (goto-char (point-min)) + (while (re-search-forward symbol-re nil t) + (let ((ppss (syntax-ppss))) + (unless (or (nth 3 ppss) (nth 4 ppss)) + (push (match-string 0) tokens)))) + (setq tokens (cl-delete-duplicates tokens :test #'string=)) + (let* ((macro-toks (cl-remove-if-not (lambda (tok) (member tok macros)) tokens)) + (func-toks (cl-remove-if-not (lambda (tok) (member tok funcs)) tokens)) + (macro-dep (and macro-toks t)) + (func-dep (and func-toks t))) + (goto-char (point-min)) + (while (re-search-forward "(require[[:space:]\n]*'cl-lib" nil t) + (let ((ppss (syntax-ppss))) + (unless (or (nth 3 ppss) (nth 4 ppss)) + (setq total-req (1+ total-req))))) + (goto-char (point-min)) + (while (re-search-forward "(eval-when-compile[[:space:]\n]*(require[[:space:]\n]*'cl-lib" nil t) + (let ((ppss (syntax-ppss))) + (unless (or (nth 3 ppss) (nth 4 ppss)) + (setq eval-req (1+ eval-req))))) + (let* ((runtime-req (> total-req eval-req)) + (eval-req-present (> eval-req 0)) + (require-kind + (cond + ((and (= total-req 0) (= eval-req 0)) "no") + ((> total-req eval-req) "runtime") + (t "compile-time")))) + (list macro-dep func-dep require-kind runtime-req eval-req-present + (sort macro-toks #'string<) + (sort func-toks #'string<)))))))) + +(defun cl-lib-deps-report--main (args) + "Generate an Org report of cl-lib dependencies under a Lisp directory. +ARGS should be `command-line-args-left', which starts with \"--\" when +invoked via the file's exec stub." + (let* ((script-dir (file-name-directory (or load-file-name buffer-file-name))) + (default-root (expand-file-name "../lisp" script-dir)) + ;; `command-line-args-left' includes a \"--\" sentinel from the exec stub. + (args (if (and args (string= (car args) "--")) (cdr args) args)) + (root (or (car args) default-root))) + (unless (file-directory-p root) + (princ (format "%s: Directory not found: %s\n" (or load-file-name "cl-lib-deps-report.el") root)) + (kill-emacs 1)) + (let* ((candidate-re "cl-[[:alnum:]-]+\\*?") + (symbol-re "\\_") + (pattern (format "%s|\\(require[[:space:]]*'cl-lib|\\(eval-when-compile[[:space:]]*\\(require[[:space:]]*'cl-lib" + candidate-re)) + (files + (let ((cmd (format "find %s -type f -name '*.el' -print0 | xargs -0 grep -l -E %s || true" + (shell-quote-argument root) + (shell-quote-argument pattern)))) + (with-temp-buffer + (call-process "sh" nil t nil "-c" cmd) + (split-string (buffer-string) "\n" t)))) + (macros '()) + (funcs '())) + (mapatoms + (lambda (sym) + (when (and (symbolp sym) + (string-prefix-p "cl-" (symbol-name sym))) + (cond + ((macrop sym) (push (symbol-name sym) macros)) + ((fboundp sym) (push (symbol-name sym) funcs)))))) + (setq macros (sort macros #'string<)) + (setq funcs (sort funcs #'string<)) + (setq files (sort files #'string<)) + (with-temp-buffer + (org-mode) + (insert (format "* cl-lib dependency report (%s)\n" root)) + (insert "** files\n") + (insert "| file | cl- macros used | cl- functions used | require |\n") + (insert "|------|-----------------|--------------------|---------|\n") + (let (runtime-missing compile-missing require-unneeded) + (dolist (file files) + (when (file-regular-p file) + (cl-destructuring-bind (macro-dep func-dep require-kind runtime-req eval-req-present macro-toks func-toks) + (cl-lib-deps-report--scan-file file symbol-re macros funcs) + (when (and func-dep (not runtime-req)) + (push (list file func-toks) runtime-missing)) + (when (and macro-dep (not eval-req-present)) + (push (list file macro-toks) compile-missing)) + (when (and (not func-dep) (not macro-dep) + (or runtime-req eval-req-present)) + (push file require-unneeded)) + (let ((skip + (or (and (not macro-dep) (not func-dep) + (string= require-kind "no")) + (and func-dep (string= require-kind "runtime")) + (and macro-dep (not func-dep) + (string= require-kind "compile-time"))))) + (unless skip + (insert (format "| %s | %s | %s | %s |\n" + file + (if macro-dep "yes" "no") + (if func-dep "yes" "no") + require-kind))))))) + (org-table-align) + (insert "** runtime dependency missing require\n") + (dolist (entry (sort runtime-missing (lambda (a b) (string< (car a) (car b))))) + (insert (format "- %s (%s)\n" + (car entry) + (mapconcat (lambda (s) (format "~%s~" s)) (cadr entry) ", ")))) + (insert "\n** compile-time dependency missing eval-when-compile require\n") + (dolist (entry (sort compile-missing (lambda (a b) (string< (car a) (car b))))) + (insert (format "- %s (%s)\n" + (car entry) + (mapconcat (lambda (s) (format "~%s~" s)) (cadr entry) ", ")))) + (insert "\n** no dependency but require present\n") + (dolist (f (sort require-unneeded #'string<)) + (insert (format "- %s\n" f))) + (insert "\n* Summary\n") + (insert (format "- Total files audited: %d\n" (length files))) + (insert (format "- Redundant requires found: %d\n" (length require-unneeded))) + (insert (format "- Missing runtime requires: %d\n" (length runtime-missing))) + (insert (format "- Missing compile-time requires: %d\n" (length compile-missing)))) + (princ (buffer-string)))))) + +(cl-lib-deps-report--main command-line-args-left) + +;;; cl-lib-deps-report.el ends here