From 364c3dbc12e7b6d41ab449dd495c96d08874310e Mon Sep 17 00:00:00 2001 From: Daniel Colascione Date: Thu, 27 Mar 2025 16:04:51 -0400 Subject: [PATCH] Help find-function find methods defined inside macros * doc/lispref/functions.texi (Finding Definitions): Document the expanded definition-finding extension mechanism. * etc/NEWS: Briefly describe the new feature. * lisp/emacs-lisp/cl-generic.el (cl--generic-find-defgeneric-regexp): Use defconst now that we no longer have purespace. (cl--generic-search-method-make-form-matcher): New function. * lisp/emacs-lisp/find-func.el (find-function-regexp-alist) (find-function-search-for-symbol): Parse out the new factory function. (find-function--search-by-expanding-macros): Try using it when searching for definitions by expanding macros. --- doc/lispref/functions.texi | 18 +++++++++-- etc/NEWS | 3 +- lisp/emacs-lisp/cl-generic.el | 29 +++++++++++++++-- lisp/emacs-lisp/find-func.el | 61 +++++++++++++++++++++++------------ 4 files changed, 83 insertions(+), 28 deletions(-) diff --git a/doc/lispref/functions.texi b/doc/lispref/functions.texi index 1279b15b819..83acbff0885 100644 --- a/doc/lispref/functions.texi +++ b/doc/lispref/functions.texi @@ -849,9 +849,16 @@ The alist @code{find-function-regexp-alist} associates object types with a regexp or function that finds the definition of that object in its source file. Each element's car is a symbol the describes the type of object, or @code{nil} to identify functions defined with @code{defun}. -Each element's cdr is a symbol: either the value of that symbol is a -string interpreted as a regexp, or that symbol names a function that can -find the definition. +Each element's cdr can be: + +@itemize +@item +A symbol whose value is a string interpreted as a regexp +@item +A symbol naming a function that can find the definition +@item +A cons cell where the car is a regexp (or function that returns one) and the cdr is a function that creates a matcher for macroexpanded forms +@end itemize A regexp string is actually a format string, and @code{%s} will be substituted with the name of the symbol we are looking for. @@ -859,6 +866,11 @@ substituted with the name of the symbol we are looking for. A function will be called with one argument, the (symbol for) the object we are searching for. +The form-matcher function in a cons cell value is called with one argument (the +symbol being sought) and should return a function that takes a form and returns +non-nil if the form defines the sought symbol. This is useful for finding +definitions that are created by macro expansion. + @cindex @code{definition-name} (symbol property) If the function to be found is defined by a macro, it may be hard for Emacs to find the definition site in the source code. A macro call may diff --git a/etc/NEWS b/etc/NEWS index 1bd2fd6d486..823e4c0cde3 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -75,7 +75,8 @@ the 'standard-display-table's extra slots with Unicode characters. Please see the documentation of that function to see which slots of the display table it changes. -+++ +--- +** 'find-function' can now find cl-defmethod invocations hidden inside macros. ** Child frames are now supported on TTY frames. This supports use-cases like Posframe, Corfu, and child frames acting like tooltips. diff --git a/lisp/emacs-lisp/cl-generic.el b/lisp/emacs-lisp/cl-generic.el index 9a7fe26eaf3..1086bf67614 100644 --- a/lisp/emacs-lisp/cl-generic.el +++ b/lisp/emacs-lisp/cl-generic.el @@ -1082,13 +1082,36 @@ MET-NAME is as returned by `cl--generic-load-hist-format'." nil t) (re-search-forward base-re nil t)))) -;; WORKAROUND: This can't be a defconst due to bug#21237. -(defvar cl--generic-find-defgeneric-regexp "(\\(?:cl-\\)?defgeneric[ \t]+%s\\_>") +(defun cl--generic-search-method-make-form-matcher (met-name) + (let ((name (car met-name)) + (qualifiers (cadr met-name)) + (specializers (cddr met-name))) + (lambda (form) + (pcase form + (`(cl-generic-define-method + (function ,(pred (eq name))) + (quote ,(and (pred listp) m-qualifiers)) + (quote ,(and (pred listp) m-args)) + ,_call-con + ,_function) + (ignore-errors + (let* ((m-spec-args (car (cl--generic-split-args m-args))) + (m-specializers + (mapcar (lambda (spec-arg) + (if (eq '&context (car-safe (car spec-arg))) + spec-arg (cdr spec-arg))) + m-spec-args))) + (and (equal qualifiers m-qualifiers) + (equal specializers m-specializers))))))))) + +(defconst cl--generic-find-defgeneric-regexp "(\\(?:cl-\\)?defgeneric[ \t]+%s\\_>") (with-eval-after-load 'find-func (defvar find-function-regexp-alist) (add-to-list 'find-function-regexp-alist - `(cl-defmethod . ,#'cl--generic-search-method)) + `(cl-defmethod + . (,#'cl--generic-search-method + . #'cl--generic-search-method-make-form-matcher))) (add-to-list 'find-function-regexp-alist '(cl-defgeneric . cl--generic-find-defgeneric-regexp))) diff --git a/lisp/emacs-lisp/find-func.el b/lisp/emacs-lisp/find-func.el index 455095c9be6..c2101617ac3 100644 --- a/lisp/emacs-lisp/find-func.el +++ b/lisp/emacs-lisp/find-func.el @@ -144,6 +144,16 @@ Instead of regexp variable, types can be mapped to functions as well, in which case the function is called with one argument (the object we're looking for) and it should search for it. +A value can also be a cons (REGEX . EXPANDED-FORM-MATCHER-FACTORY). +REGEX is as above; EXPANDED-FORM-MATCHER-FACTORY is a function of one +argument, the same as we'd pass to a REGEX function, that returns +another function of one argument that returns true if we're looking at a +macroexpanded form that defines what we're looking for. If you want to +use EXPANDED-FORM-MATCHER-FACTORY exclusively, you can set REGEX to a +never-match regex and force the fallback to +EXPANDED-FORM-MATCHER-FACTORY. The buffer to search is current during +the call to EXPANDED-FORM-MATCHER-FACTORY. + Symbols can have their own version of this alist on the property `find-function-type-alist'. See the function `find-function-update-type-alist'.") @@ -434,7 +444,13 @@ The search is done in the source for library LIBRARY." (regexp-symbol (or (and (symbolp symbol) (alist-get type (get symbol 'find-function-type-alist))) - (alist-get type find-function-regexp-alist)))) + (alist-get type find-function-regexp-alist))) + (form-matcher-factory + (and (functionp (cdr-safe regexp-symbol)) + (cdr regexp-symbol))) + (regexp-symbol (if form-matcher-factory + (car regexp-symbol) + regexp-symbol))) (with-current-buffer (find-file-noselect filename) (let ((regexp (if (functionp regexp-symbol) regexp-symbol (format (symbol-value regexp-symbol) @@ -474,7 +490,8 @@ The search is done in the source for library LIBRARY." ;; expands macros until it finds the symbol. (cons (current-buffer) (find-function--search-by-expanding-macros - (current-buffer) symbol type)))))))))) + (current-buffer) symbol type + form-matcher-factory)))))))))) ;;;###autoload (defun find-function-update-type-alist (symbol type variable) @@ -506,19 +523,13 @@ Return t if any PRED returns t." (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) +(defun find-function--search-by-expanding-macros + (buf symbol type matcher-factory) "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) + (with-current-buffer buf + (when-let* ((expected-symbol-p + (cond ((null type) + (lambda (form) ;; Check if a given form is a `defalias' to ;; SYM, the function name we are searching ;; for. All functions in Emacs Lisp @@ -526,20 +537,28 @@ Return t if any PRED returns t." ;; after several steps of macroexpansion. (and (eq (car-safe form) 'defalias) (equal (car-safe (cdr form)) - `(quote ,symbol)))) - ((eq type 'defvar) + `(quote ,symbol))))) + ((eq type 'defvar) + (lambda (form) ;; Variables generated by macros ultimately ;; expand to `defvar'. (and (eq (car-safe form) 'defvar) - (eq (car-safe (cdr form)) symbol))) - (t nil))))) + (eq (car-safe (cdr form)) symbol)))) + (matcher-factory + (funcall matcher-factory symbol))))) + (catch 'found + (save-excursion + (goto-char (point-min)) + (condition-case nil + (while t (when (find-function--any-subform-p - (find-function--try-macroexpand form) + (find-function--try-macroexpand + (read (current-buffer))) 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)))))) + (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.