Subject: bug#45443: 28.0.50; Can't find definition of compilation--message->loc

From: Daniel Martín <[email-masked]>
Date: Sun, 27 Dec 2020 01:51:29 +0100

Drew Adams <[email-masked]> writes:

>> It's a general problem with uses of cl-defstruct and similar
>> constructs: they generate functions and macros that the Help functions
>> are unable to find.
>
> Yep.  It's a big doc/help problem, IMO.  And
> we've moved more and more stuff to things like
> `cl-defstruct' (which in itself is fine).
>
> I don't see a good general solution to this
> problem.  But it is, I think, something
> important for the quintessential
> "self-documenting" editor.  I truly hope that
> some smart and ambitious hacker tackles this.

One possible approach is, if the regular expression code fails to find a
location, we can fall back to expand macros until we find the definition
(a defalias in the case of a function, or a defvar in the case of a
variable), or we reach the end of the file.

I attach a first implementation of this approach, with some tests for
the function definition that Richard wanted to find.  I'm sure there are
still missing cases that are interesting (closures?).  Give it a try and
see if there are still relevant symbols whose definition Emacs is unable
to locate.  I'm also open to feedback about the macro expansion logic.
Could it be more efficient?  Thanks.

>From 34662c47b359792e048bb7020d7eb0fbf94e7838 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Mart=C3=ADn?= <[email-masked]>
Date: Sun, 27 Dec 2020 01:07:26 +0100
Subject: [PATCH] Improve "find definition" in *Help* buffers

*
lisp/emacs-lisp/find-func.el (find-function--search-by-expanding-macros):
New internal function that searches for a function or variable by
expanding macros in a buffer.
* lisp/emacs-lisp/find-func.el (find-function-search-for-symbol): If
our regexp algorithm could not find a location for the symbol
definition, resort to find-function--search-by-expanding-macros.
* test/lisp/emacs-lisp/find-func-tests.el: Add a automatic test for a
function and variable generated by a macro.
* etc/NEWS: Advertise the improved functionality.
---
 etc/NEWS                                |  6 +++
 lisp/emacs-lisp/find-func.el            | 65 ++++++++++++++++++++++++-
 test/lisp/emacs-lisp/find-func-tests.el | 10 ++++
 3 files changed, 80 insertions(+), 1 deletion(-)

diff --git a/etc/NEWS b/etc/NEWS
index a320acb5fa..2bfaccd2ef 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -114,6 +114,12 @@ choosing a group, or clicking a button in the "*Help*" buffers when
 looking at the doc string of a function that belongs to one of these
 groups.
 
+---
+** Improved "find definition" feature of *Help* buffers.
+Now clicking on the link to find the definition of functions generated
+by 'cl-defstruct', or variables generated by 'define-derived-mode',
+for example, will go to the exact place where they are defined.
+
 ** New variable 'redisplay-skip-initial-frame' to enable batch redisplay tests.
 Setting it to nil forces the redisplay to do its job even in the
 initial frame used in batch mode.
diff --git a/lisp/emacs-lisp/find-func.el b/lisp/emacs-lisp/find-func.el
index 074e7db295..7796a72ecf 100644
--- a/lisp/emacs-lisp/find-func.el
+++ b/lisp/emacs-lisp/find-func.el
@@ -389,7 +389,70 @@ find-function-search-for-symbol
                   (progn
                     (beginning-of-line)
                     (cons (current-buffer) (point)))
-                (cons (current-buffer) nil)))))))))
+                ;; If the regexp search didn't find the location of
+                ;; the symbol (for example, because it is generated by
+                ;; a macro), try a slightly more expensive search that
+                ;; expands macros until it finds the symbol.
+                (cons (current-buffer)
+                      (find-function--search-by-expanding-macros
+                       (current-buffer) symbol type))))))))))
+
+(defun find-function--try-macroexpand (form)
+  "Try to macroexpand FORM in full or partially.
+This is a best-effort operation in which if macroexpansion fails,
+this function returns FORM as is."
+  (ignore-errors
+    (or
+     (macroexpand-all form)
+     (macroexpand-1 form)
+     form)))
+
+(defun find-function--any-subform-p (form pred)
+  "Walk FORM and apply PRED to its subexpressions.
+Return t if any PRED returns t."
+  (cond
+   ((not (consp form)) nil)
+   ((funcall pred form) t)
+   (t
+    (cl-destructuring-bind (left-child . right-child) form
+      (or
+       (find-function--any-subform-p left-child pred)
+       (find-function--any-subform-p right-child pred))))))
+
+(defun find-function--search-by-expanding-macros (buf symbol type)
+  "Expand macros in BUF to search for the definition of SYMBOL of TYPE."
+  (catch 'found
+    (with-current-buffer buf
+      (save-excursion
+        (goto-char (point-min))
+        (condition-case nil
+            (while t
+              (let ((form (read (current-buffer)))
+                    (expected-symbol-p
+                      (lambda (form)
+                        (cond
+                         ((null type)
+                          ;; Check if a given form is a `defalias' to
+                          ;; SYM, the function name we are searching
+                          ;; for.  All functions in Emacs Lisp
+                          ;; ultimately expand to a `defalias' form
+                          ;; after several steps of macroexpansion.
+                          (and (eq (car-safe form) 'defalias)
+                               (equal (car-safe (cdr form))
+                                      `(quote ,symbol))))
+                         ((eq type 'defvar)
+                          ;; Variables generated by macros ultimately
+                          ;; expand to `defvar'.
+                          (and (eq (car-safe form) 'defvar)
+                               (eq (car-safe (cdr form)) symbol)))
+                         (t nil)))))
+                (when (find-function--any-subform-p
+                       (find-function--try-macroexpand form)
+                       expected-symbol-p)
+                  ;; We want to return the location at the beginning
+                  ;; of the macro, so move back one sexp.
+                  (throw 'found (progn (backward-sexp) (point))))))
+          (end-of-file nil))))))
 
 (defun find-function-library (function &optional lisp-only verbose)
   "Return the pair (ORIG-FUNCTION . LIBRARY) for FUNCTION.
diff --git a/test/lisp/emacs-lisp/find-func-tests.el b/test/lisp/emacs-lisp/find-func-tests.el
index d77eb6757f..03df4bb9ff 100644
--- a/test/lisp/emacs-lisp/find-func-tests.el
+++ b/test/lisp/emacs-lisp/find-func-tests.el
@@ -43,5 +43,15 @@ find-func-tests--library-completion
                      (concat data-directory (kbd "n x / TAB RET"))
                    (read-library-name)))))
 
+;; Avoid a byte-compilation warning that may confuse people reading
+;; the result of the following test.
+(declare-function compilation--message->loc nil "compile")
+
+(ert-deftest find-func-tests--locate-macro-generated-symbols () ;bug#45443
+  (should (cdr (find-function-search-for-symbol
+                #'compilation--message->loc nil "compile")))
+  (should (cdr (find-function-search-for-symbol
+                'c-mode-hook 'defvar "cc-mode"))))
+
 (provide 'find-func-tests)
 ;;; find-func-tests.el ends here
-- 
2.28.0