Skip to content

Commit c4a1b43

Browse files
haji-aliraxod502
andauthored
Provide an option to use Emacs advice system (#63)
Co-authored-by: Radon Rosborough <radon@intuitiveexplanations.com>
1 parent ad8b185 commit c4a1b43

2 files changed

Lines changed: 124 additions & 39 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,12 @@ that takes effect, but `el-patch` retains a record of both patches,
627627
meaning they can be inspected and validated individually. See
628628
[#29](https://github.com/radian-software/el-patch/issues/29).
629629

630+
You may also define patches of functions as `:override` advices
631+
instead of overriding the original definition. This is done by setting
632+
`el-patch-use-advice` to a non-nil value (either dynamically around a
633+
patch or globally). The patched function must have the same name and
634+
number of arguments as the original function.
635+
630636
## Usage with byte-compiled init-file
631637

632638
`el-patch` does not need to be loaded at runtime just to define

el-patch.el

Lines changed: 118 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,18 @@ loaded. You can toggle the `use-package' integration later using
151151
"Non-nil means to validate patches when byte-compiling."
152152
:type 'boolean)
153153

154+
(defcustom el-patch-use-advice nil
155+
"Non-nil causes el-patch to use Emacs' advice system for patching functions.
156+
This can be set globally or bound dynamically around a patch.
157+
158+
An advice is used if the patched function has the same name and
159+
the same number of arguments as the original.
160+
161+
An advice takes precedence over subsequent non-advice patches.
162+
You may need to un-advice or un-patch a function to apply a new
163+
patch."
164+
:type 'boolean)
165+
154166
;;;; Internal variables
155167

156168
(defvar el-patch-variant nil
@@ -222,6 +234,13 @@ This function lives halfway between `copy-sequence' and
222234
(cons (car tree) (el-patch--copy-semitree (cdr tree)))
223235
tree))
224236

237+
(defun el-patch--advice-name (name variant-name)
238+
"Return advice name for a given NAME and VARIANT-NAME."
239+
(intern
240+
(format "%S@%s@el-patch--advice"
241+
name
242+
(if variant-name (format "%S" el-patch-variant) ""))))
243+
225244
(defun el-patch--resolve (form new &optional table)
226245
"Resolve a patch FORM.
227246
Return a list of forms to be spliced into the surrounding
@@ -536,38 +555,82 @@ PATCH-DEFINITION is an unquoted list starting with `defun',
536555
(let ((definition (el-patch--resolve-definition patch-definition t)))
537556
;; Then we parse out the definition type and symbol name.
538557
(cl-destructuring-bind (type name . body) definition
539-
(let ((register-patch
540-
`(let ((table (or (bound-and-true-p el-patch--patches)
541-
(make-hash-table :test #'eq))))
542-
(setq el-patch--patches table)
543-
(setq table
544-
(puthash ',name
545-
(gethash
546-
',name table
547-
(make-hash-table :test #'eq))
548-
table))
549-
(setq table
550-
(puthash ',type
551-
(gethash
552-
',type table
553-
(make-hash-table :test #'eq))
554-
table))
555-
(puthash el-patch-variant ',patch-definition table))))
558+
(let* ((advise (and el-patch-use-advice
559+
;; Only advice functions
560+
(let* ((props (alist-get type
561+
el-patch-deftype-alist))
562+
(classifier (plist-get props :classify)))
563+
(and classifier
564+
(equal
565+
(caar (funcall classifier definition))
566+
'function)))
567+
;; Patches must have the same name and
568+
;; same number of arguments
569+
(let ((orig-def (el-patch--resolve-definition
570+
(cl-subseq patch-definition 0 3)
571+
nil)))
572+
;; Same name and same argument count
573+
(and (equal name (nth 1 orig-def))
574+
(equal (length (nth 2 definition))
575+
(length (nth 2 orig-def)))))
576+
'advice))
577+
(register-patch
578+
`(let ((table (or (bound-and-true-p el-patch--patches)
579+
(make-hash-table :test #'eq))))
580+
(setq el-patch--patches table)
581+
(setq table
582+
(puthash ',name
583+
(gethash
584+
',name table
585+
(make-hash-table :test #'eq))
586+
table))
587+
(setq table
588+
(puthash ',type
589+
(gethash
590+
',type table
591+
(make-hash-table :test #'equal))
592+
table))
593+
(puthash (cons ,(when advise `(quote ,advise))
594+
el-patch-variant)
595+
',patch-definition table))))
556596
;; If we need to validate the patch, then we also need to
557597
;; register it at compile-time, not just at runtime.
558598
(when (and el-patch-validate-during-compile byte-compile-current-file)
559599
(eval register-patch t)
560-
(el-patch-validate name type 'nomsg nil el-patch-variant))
600+
(el-patch-validate name type 'nomsg nil
601+
(cons advise el-patch-variant)))
602+
;; Check that `el-patch-variant' is not a cons or a string
603+
(when (or (consp el-patch-variant)
604+
(stringp el-patch-variant))
605+
(error "`el-patch-variant' cannot be a string or a cons"))
561606
`(progn
562607
;; Register the patch in our hash. We want to do this right
563608
;; away so that if there is an error then at least the user
564609
;; can undo the patch (as long as it is not too terribly
565610
;; wrong).
566611
,register-patch
567612
;; Now we actually overwrite the current definition.
568-
(el-patch--stealthy-eval
569-
,definition
570-
"This function was patched by `el-patch'."))))))
613+
,(if advise
614+
;; Use advice system
615+
(let ((advice-name (el-patch--advice-name name
616+
el-patch-variant)))
617+
`(progn
618+
(el-patch--stealthy-eval
619+
,(append
620+
(list (car definition) ;; Same type
621+
advice-name) ;; Different name
622+
;; Rest is the same
623+
(cddr definition))
624+
,(format
625+
;; The new line before the name is to avoid
626+
;; long doc strings
627+
"This advice was defined by `el-patch' for\n`%S'."
628+
name))
629+
(advice-add (quote ,name)
630+
:override (quote ,advice-name))))
631+
`(el-patch--stealthy-eval
632+
,definition
633+
"This definition was patched by `el-patch'.")))))))
571634

572635
;;;;; Removing patches
573636

@@ -579,10 +642,16 @@ patched. NAME, TYPE, and VARIANT are as returned by
579642
`el-patch-get'."
580643
(interactive (el-patch--select-patch))
581644
(if-let ((patch-definition (el-patch-get name type variant)))
582-
(eval `(el-patch--stealthy-eval
583-
,(el-patch--resolve-definition
584-
patch-definition nil)
585-
"This function was patched and then unpatched by `el-patch'."))
645+
(if (car variant)
646+
;; an advice, remove it
647+
(advice-remove name
648+
(el-patch--advice-name name (cdr variant)))
649+
;; Otherwise just re-evaluate original definition
650+
(eval
651+
`(el-patch--stealthy-eval
652+
,(el-patch--resolve-definition
653+
patch-definition nil)
654+
"This function was patched and then unpatched by `el-patch'.")))
586655
(error "There is no patch for %S %S" type name)))
587656

588657
;;;; Defining patch types
@@ -933,9 +1002,9 @@ See `el-patch-validate'."
9331002
(let ((type-hash (gethash type patch-hash)))
9341003
(dolist (variant (hash-table-keys type-hash))
9351004
(setq patch-count (1+ patch-count))
936-
(let ((el-patch-variant variant))
937-
(unless (el-patch-validate name type 'nomsg)
938-
(setq warning-count (1+ warning-count)))))))))
1005+
(unless (el-patch-validate name type 'nomsg nil
1006+
variant)
1007+
(setq warning-count (1+ warning-count))))))))
9391008
(cond
9401009
((zerop patch-count)
9411010
(user-error "No patches defined"))
@@ -1019,17 +1088,27 @@ nil; see `el-patch-variant')."
10191088
nil
10201089
'require-match)))))
10211090
(type-hash (gethash type patch-hash))
1022-
(options (mapcar #'symbol-name
1023-
(hash-table-keys type-hash)))
1024-
(variant (intern
1025-
(pcase (length options)
1026-
(0 (error "Internal `el-patch' error"))
1027-
(1 (car options))
1028-
(_ (completing-read
1029-
"Which variant? "
1030-
options
1031-
nil
1032-
'require-match))))))
1091+
(options (hash-table-keys type-hash))
1092+
(variant (pcase (length options)
1093+
(0 (error "Internal `el-patch' error"))
1094+
(1 (car options))
1095+
(_ (let ((completing-options
1096+
(mapcar (lambda (x)
1097+
(cons (format "%s%S"
1098+
(or (and (car x)
1099+
"Advice: ")
1100+
"")
1101+
(cdr x))
1102+
x))
1103+
(hash-table-keys type-hash))))
1104+
(alist-get
1105+
(completing-read
1106+
"Which variant? "
1107+
completing-options
1108+
nil
1109+
'require-match)
1110+
completing-options
1111+
nil nil 'equal))))))
10331112
(list name type variant)))
10341113

10351114
(defun el-patch--ediff-forms (name1 form1 name2 form2)

0 commit comments

Comments
 (0)