forked from radian-software/el-patch
-
Notifications
You must be signed in to change notification settings - Fork 0
/
el-patch.el
1168 lines (1057 loc) · 45.6 KB
/
el-patch.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; el-patch.el --- Future-proof your Elisp -*- lexical-binding: t -*-
;; Copyright (C) 2016-2022 Radian LLC and contributors
;; Author: Radian LLC <contact+el-patch@radian.codes>
;; Created: 31 Dec 2016
;; Homepage: https://github.com/radian-software/el-patch
;; Keywords: extensions
;; Package-Requires: ((emacs "26"))
;; SPDX-License-Identifier: MIT
;; Version: 3.0
;;; Commentary:
;; el-patch allows you to override Emacs Lisp functions in a
;; future-proof way. Specifically, you can override a function by
;; providing an s-expression-based "patch", from which the "original"
;; and "modified" definitions can both be computed -- just like a Git
;; patch.
;; The "modified" definition is what is actually evaluated in your
;; init-file, but at any time you can ask el-patch to look up the
;; actual definition of the function and compare it to the patch's
;; "original" definition. If there is a difference -- meaning that the
;; original function definition was updated since you created the
;; patch -- el-patch will show you with Ediff. This means you know
;; when you might need to update your customizations (this is the
;; future-proof part).
;; el-patch also provides a powerful mechanism to help you lazy-load
;; packages. If you want to use a function from a package without
;; triggering its autoload (for instance, activating a minor mode or
;; defining keybindings), you can just copy its definition to your
;; init-file and declare it as a patch. Then you can freely use the
;; function, but you will still be notified of updates to the original
;; definition by el-patch so you will know when to update your copy of
;; the definition.
;; Please see https://github.com/radian-software/el-patch for more
;; information.
;;; Code:
;; To see the outline of this file, run M-x outline-minor-mode and
;; then press C-c @ C-t. To also show the top-level functions and
;; variable declarations in each section, run M-x occur with the
;; following query: ^;;;;* \|^(
;;;; Libraries
(require 'seq)
(require 'subr-x)
(require 'cl-lib)
(defvar use-package-keywords)
(declare-function use-package-normalize-forms "use-package")
(declare-function use-package-process-keywords "use-package")
;;;; User-facing variables
;;;###autoload
(defgroup el-patch nil
"Future-proof your Emacs Lisp customizations!"
:prefix "el-patch-"
:group 'lisp
:link '(url-link :tag "GitHub" "https://github.com/radian-software/el-patch")
:link '(emacs-commentary-link :tag "Commentary" "el-patch"))
(defcustom el-patch-use-aggressive-defvar nil
"When patching `defvar' or similar, override existing values.
This means that `el-patch-defvar', `el-patch-defconst', and
`el-patch-defcustom' will unbind the old variable definition
before evaluating the new one."
:type 'boolean)
(defun el-patch-default-require-function (feature &rest _args)
"Invoke `require' on FEATURE, printing warning if it is unavailable.
This is the default value for `el-patch-require-function'."
(condition-case-unless-debug e
(require feature)
(error (display-warning
'el-patch
(format "On el-patch-pre-validate-hook: %s"
(error-message-string e))
:error))))
(defcustom el-patch-require-function #'el-patch-default-require-function
"Function to `require' a feature in `el-patch-pre-validate-hook'.
This is passed all of the arguments of `el-patch-feature' as
quoted literals, and it should load the feature. This function
might be useful if, for example, some of your features are
provided by lazy-installed packages, and those packages need to
be installed before the features can be loaded."
:type 'function)
(defcustom el-patch-deftype-alist nil
"Alist of types of definitions that can be patched with `el-patch'.
The keys are definition types, like `defun', `define-minor-mode',
etc. The values are plists; the following keywords are accepted:
`:classify' - a function which may be called with a full
definition form (a list starting with e.g. `defun') and which
returns an alist detailing what it defines. In this alist, the
keys are symbols; only the values `function' and `variable' are
allowed. The values are names of functions and variables,
respectively, that are defined by the definition form. This
argument is mandatory.
`:locate' - a function which may be called with a full definition
form (a list starting with e.g. `defun') and which returns the
actual source code of the definition, as a list. If the patch
correct and up to date, then this will actually be the same as
the definition which was passed in. This argument is optional,
but required if you want patch validation to work.
`:declare' - a list to be put in a `declare' form of the
resulting `el-patch' macro, like:
((doc-string 2) (indent defun))
This argument is optional.
`:macro-name' - normally the name of the macro generated for
patching a `defun' is called `el-patch-defun', but you can
override that by providing this argument. This argument is
optional.
`:font-lock' - a function that can be called to set up font-lock
keywords (e.g., by calling `font-lock-add-keywords' with some
appropriate arguments). The function is called with one argument,
the macro name (e.g. `el-patch-defun')."
:type '(alist
:key-type symbol
:value-type (plist
:key-type (choice
(const :classify)
(const :locate)
(const :declare)
(const :macro-name))
:value-type sexp)))
(defcustom el-patch-enable-use-package-integration t
"Non-nil means to automatically enable `use-package' integration.
This variable has an effect only when the `el-patch' library is
loaded. You can toggle the `use-package' integration later using
\\[el-patch-use-package-mode]."
:type 'boolean)
(defcustom el-patch-validate-during-compile nil
"Non-nil means to validate patches when byte-compiling."
:type 'boolean)
;;;; Internal variables
(defvar el-patch-variant nil
"Advanced variable for defining patch variants.
This variable may be used to define multiple different patches
for the same object, and have them all be validated by
`el-patch'. Usage: dynamically bind this variable around an
invocation of `el-patch-defun', etc. As long as each patch
definition for the same object uses a different value for this
variable (including its default, nil), the patches will be
distinguishable (and hence can be validated) by `el-patch', even
though only the most recently defined one will actually take
effect.
Common use case: defining a separate patch for the same function
before and after a library is loaded.")
(defvar el-patch--patches (make-hash-table :test 'equal)
"Hash table of patches that have been defined.
This is a three-level hash table. The first-level keys are
symbols naming the objects that have been patched. The
second-level keys are definition types (symbols `defun',
`defmacro', etc.). The third-level keys are arbitrary symbols
\(always nil, unless `el-patch-variant' has been used). The values
are patch definitions, which are lists beginning with `defun',
`defmacro', etc.
Note that the object name symbols are from the versions of
patches that have been resolved in favor of the modified version,
when a patch renames a symbol.")
(defvar el-patch--not-present (make-symbol "el-patch--not-present")
"Value used as a default argument to `gethash'.")
;;;; Resolving patches
(defmacro el-patch--with-puthash (table kvs &rest body)
"Bind variables in hash TABLE according to KVS then eval BODY.
Each of the KVS is a list whose first element is the key and
whose second element is the value. After BODY is evaluated, the
original state of TABLE is restored. Return value is the result
of evaluating the last form in BODY."
(declare (indent 2))
`(let* ((table ,table)
(kvs ,kvs)
(original-kvs (mapcar (lambda (kv)
(list (car kv)
(gethash (cadr kv) table
el-patch--not-present)))
kvs)))
(prog2
(dolist (kv kvs)
(puthash (car kv) (cadr kv) table))
(progn ,@body)
(dolist (kv original-kvs)
;; Note that we can't distinguish between a missing value and
;; a value that is coincidentally equal to
;; `el-patch--not-present', due to limitations in the Emacs
;; Lisp hash table API.
(if (eq (car kv) el-patch--not-present)
(remhash (car kv) table)
(puthash (car kv) (cadr kv) table))))))
(defun el-patch--copy-semitree (tree)
"Copy the list TREE, and return the copy. The list may be improper.
This function lives halfway between `copy-sequence' and
`copy-tree', since it only recurses into cdrs."
(if (consp tree)
(cons (car tree) (el-patch--copy-semitree (cdr tree)))
tree))
(defun el-patch--resolve (form new &optional table)
"Resolve a patch FORM.
Return a list of forms to be spliced into the surrounding
s-expression. Resolve in favor of the original version if NEW is
nil; otherwise resolve in favor of the new version. TABLE is a
hash table of `el-patch-let' bindings, which maps symbols to
their bindings."
(let ((table (or table (make-hash-table :test 'equal))))
(cond
((consp form)
(let* ((directive (car form))
(this-directive (pcase directive
('el-patch-remove 'el-patch-add)
('el-patch-splice 'el-patch-wrap)
(_ directive)))
(inverted (not (equal this-directive directive)))
(this-new (if inverted (not new) new))
(resolve (lambda (form) (el-patch--resolve form new table))))
(pcase this-directive
((quote el-patch-add)
(when (<= (length form) 1)
(error "Not enough arguments (%d) for `%s'"
(1- (length form)) directive))
(when this-new
(cl-mapcan resolve (cdr form))))
((quote el-patch-swap)
(cond
((<= (length form) 2)
(error "Not enough arguments (%d) for `el-patch-swap'"
(1- (length form))))
((>= (length form) 4)
(error "Too many arguments (%d) in for `el-patch-swap'"
(1- (length form)))))
(funcall resolve
(if this-new
(cl-caddr form)
(cadr form))))
((quote el-patch-wrap)
(let ((triml (if (>= (length form) 3)
(nth 1 form)
0))
(trimr (if (>= (length form) 4)
(nth 2 form)
0))
(body (car (last form))))
(cond
((<= (length form) 1)
(error "Not enough arguments (%d) for `%s'"
(1- (length form)) directive))
((>= (length form) 5)
(error "Too many arguments (%d) for `%s'"
(1- (length form)) directive))
((not (listp body))
(error "Non-list (%s) as last argument for `%s'"
(car (last form)) directive))
((and (>= (length form) 3)
(not (integerp triml)))
(error "Non-integer (%s) as first argument for `%s'"
(nth 1 form) directive))
((and (>= (length form) 4)
(not (integerp trimr)))
(error "Non-integer (%s) as second argument for `%s'"
(nth 2 form) directive))
((< triml 0)
(error "Left trim less than zero (%d) for `%s'"
triml directive))
((< trimr 0)
(error "Right trim less than zero (%d) for `%s'"
trimr directive))
((> (+ triml trimr) (length body))
(error (concat "Combined trim (%d + %d) greater "
"than body length (%d) for `%s'")
triml trimr (length body) directive)))
(if this-new
(list (cl-mapcan resolve body))
(cl-mapcan resolve (nthcdr triml (butlast body trimr))))))
((quote el-patch-let)
(let ((bindings (nth 1 form))
(body (nthcdr 2 form)))
(cond
((<= (length form) 2)
(error "Not enough arguments (%d) for `el-patch-let'"
(1- (length form))))
((not (listp bindings))
(error "Non-list (%s) as first argument for `el-patch-let'"
bindings)))
(el-patch--with-puthash table
(mapcar
(lambda (kv)
(unless (symbolp (car kv))
(error "Non-symbol (%s) as binding for `el-patch-let'"
(car kv)))
(list (car kv)
(funcall resolve (cadr kv))))
bindings)
(cl-mapcan resolve body))))
((quote el-patch-literal)
(when (<= (length form) 1)
(error "Not enough arguments (%d) for `el-patch-literal'"
(1- (length form))))
(cdr form))
((quote el-patch-concat)
(when (<= (length form) 1)
(error "Not enough arguments (%d) for `el-patch-concat'"
(1- (length form))))
(list (apply #'concat (cl-mapcan resolve (cdr form)))))
(_
(let ((car-forms (funcall resolve (car form)))
(cdr-forms (funcall resolve (cdr form))))
(cond
((null car-forms) cdr-forms)
((null cdr-forms) car-forms)
(t
(let ((forms (nconc car-forms (butlast cdr-forms))))
(setf (nthcdr (length forms) forms)
(car (last cdr-forms)))
(list forms)))))))))
((vectorp form)
(list
(seq-mapcat
(lambda (subform)
(el-patch--resolve subform new table))
form
'vector)))
(t
(or
;; Copy since otherwise we may end up with the same list object
;; returned multiple times, which is not okay since lists
;; returned by this function may be modified destructively.
(el-patch--copy-semitree (gethash form table))
(list form))))))
(defun el-patch--resolve-definition (patch-definition new)
"Resolve a PATCH-DEFINITION.
PATCH-DEFINITION is a list starting with `defun', `defmacro',
etc. Return a list of the same format. Resolve in favor of the
original version if NEW is nil; otherwise resolve in favor of the
new version."
(cons (car patch-definition)
(cl-mapcan (lambda (form)
(el-patch--resolve form new))
(cdr patch-definition))))
;;;; Patch directives
;;;###autoload
(defmacro el-patch-add (&rest args)
"Patch directive for inserting forms.
In the original definition, the ARGS and their containing form
are removed. In the new definition, the ARGS are spliced into the
containing s-expression."
(declare (indent 0))
(ignore args)
`(error "Can't use `el-patch-add' outside of an `el-patch'"))
;;;###autoload
(defmacro el-patch-remove (&rest args)
"Patch directive for removing forms.
In the original definition, the ARGS are spliced into the
containing s-expression. In the new definition, the ARGS and
their containing form are removed."
(declare (indent 0))
(ignore args)
`(error "Can't use `el-patch-remove' outside of an `el-patch'"))
;;;###autoload
(defmacro el-patch-swap (old new)
"Patch directive for swapping forms.
In the original definition, OLD is spliced into the containing
s-expression. In the new definition, NEW is spliced instead."
(declare (indent 0))
(ignore old new)
`(error "Can't use `el-patch-swap' outside of an `el-patch'"))
;;;###autoload
(defmacro el-patch-wrap (&optional triml trimr args)
"Patch directive for wrapping forms.
TRIML and TRIMR are optional arguments. If only one is provided,
it is assumed to be TRIML. ARGS is required, and it must be a
list.
In the original definition, the ARGS are spliced into the
containing s-expression. If TRIML is provided, the first TRIML of
the ARGS are removed first. If TRIMR is provided, the last TRIMR
are also removed. In the new definition, the ARGS and their
containing list are spliced into the containing s-expression."
(declare (indent defun))
(ignore triml trimr args)
`(error "Can't use `el-patch-wrap' outside of an `el-patch'"))
;;;###autoload
(defmacro el-patch-splice (&optional triml trimr args)
"Patch directive for splicing forms.
TRIML and TRIMR are optional arguments. If only one is provided,
it is assumed to be TRIML. ARGS is required, and it must be a
list.
In the original definition, the ARGS and their containing list
are spliced into the containing s-expression. In the new
definition, the ARGS are spliced into the containing
s-expression. If TRIML is provided, the first TRIML of the ARGS
are removed first. If TRIMR is provided, the last TRIMR are also
removed."
(declare (indent defun))
(ignore triml trimr args)
`(error "Can't use `el-patch-splice' outside of an `el-patch'"))
;;;###autoload
(defmacro el-patch-let (varlist &rest args)
"Patch directive for creating local el-patch bindings.
Creates local bindings according to VARLIST, then splices ARGS
into both the original and new definitions. You may bind symbols
that are also patch directives, but the bindings will not have
effect if the symbols are used at the beginning of a list (they
will act as patch directives)."
(declare (indent 1))
(ignore varlist args)
`(error "Can't use `el-patch-let' outside of an `el-patch'"))
;;;###autoload
(defmacro el-patch-literal (&rest args)
"Patch directive for treating patch directives literally.
ARGS are spliced into the containing s-expression, but are not
processed further by el-patch."
(declare (indent 0))
(ignore args)
`(error "Can't use `el-patch-literal' outside of an `el-patch'"))
;;;###autoload
(defmacro el-patch-concat (&rest args)
"Patch directive for modifying string literals.
ARGS should resolve to strings; those strings are passed to
`concat' and spliced into the containing s-expression in both the
original and new definitions."
(declare (indent 0))
(ignore args)
`(error "Can't use `el-patch-concat' outside of an `el-patch'"))
;;;; Applying patches
(defmacro el-patch--stealthy-eval (definition &optional docstring-note)
"Evaluate DEFINITION without updating `load-history'.
DEFINITION should be an unquoted list beginning with `defun',
`defmacro', `define-minor-mode', etc. DOCSTRING-NOTE, if given,
is a sentence to put in brackets at the end of the docstring."
(let* ((type (nth 0 definition))
(props (alist-get type el-patch-deftype-alist)))
(unless props
(error "Unregistered definition type `%S'" type))
(let* ((classify (plist-get props :classify))
(docstring-idx
(nth 1 (assq 'doc-string (plist-get props :declare)))))
(unless classify
(error
"Definition type `%S' has no `:classify' in `el-patch-deftype-alist'"
type))
(when (and docstring-note docstring-idx)
(let ((old-docstring (nth docstring-idx definition)))
(if (stringp old-docstring)
(let ((new-docstring
(concat
old-docstring
(format "\n\n[%s]" docstring-note))))
(setq definition (cl-copy-list definition))
(setf (nth docstring-idx definition)
new-docstring))
(setq definition (append
(butlast definition
(- (length definition) docstring-idx))
(cons (format "[%s]" docstring-note)
(nthcdr docstring-idx definition)))))))
(let* ((classification
(funcall classify definition))
(items
(cl-remove-if
(lambda (item)
(member item current-load-list))
(mapcar
(lambda (entry)
(pcase (car entry)
(`function (cons 'defun (cdr entry)))
(`variable (cdr entry))
(_ (error
"Unexpected classification type `%S'" (car entry)))))
classification))))
`(prog2
;; Using a `progn' here so that the `prog2' above will
;; correctly cause the evaluated definition to be
;; returned, even if `el-patch-use-aggressive-defvar' is
;; nil.
(progn
,@(when el-patch-use-aggressive-defvar
(cl-mapcan
(lambda (entry)
(when (eq (car entry) 'variable)
`((makunbound ',(cdr entry)))))
classification)))
,definition
,@(mapcar (lambda (item)
`(setq current-load-list
(remove ',item current-load-list)))
items))))))
;;;###autoload
(defmacro el-patch--definition (patch-definition)
"Activate a PATCH-DEFINITION and update `el-patch--patches'.
PATCH-DEFINITION is an unquoted list starting with `defun',
`defmacro', etc., which may contain patch directives."
;; First we resolve the patch definition in favor of the modified
;; version, because that is the version we need to activate (no
;; validation happens here).
(let ((definition (el-patch--resolve-definition patch-definition t)))
;; Then we parse out the definition type and symbol name.
(cl-destructuring-bind (type name . body) definition
(let ((register-patch
`(let ((table (or (bound-and-true-p el-patch--patches)
(make-hash-table :test #'eq))))
(setq el-patch--patches table)
(setq table
(puthash ',name
(gethash
',name table
(make-hash-table :test #'eq))
table))
(setq table
(puthash ',type
(gethash
',type table
(make-hash-table :test #'eq))
table))
(puthash el-patch-variant ',patch-definition table))))
;; If we need to validate the patch, then we also need to
;; register it at compile-time, not just at runtime.
(when (and el-patch-validate-during-compile byte-compile-current-file)
(eval register-patch t)
(el-patch-validate name type 'nomsg nil el-patch-variant))
`(progn
;; Register the patch in our hash. We want to do this right
;; away so that if there is an error then at least the user
;; can undo the patch (as long as it is not too terribly
;; wrong).
,register-patch
;; Now we actually overwrite the current definition.
(el-patch--stealthy-eval
,definition
"This function was patched by `el-patch'."))))))
;;;;; Removing patches
;;;###autoload
(defun el-patch-unpatch (name type variant)
"Remove the patch given by the PATCH-DEFINITION.
This restores the original functionality of the object being
patched. NAME, TYPE, and VARIANT are as returned by
`el-patch-get'."
(interactive (el-patch--select-patch))
(if-let ((patch-definition (el-patch-get name type variant)))
(eval `(el-patch--stealthy-eval
,(el-patch--resolve-definition
patch-definition nil)
"This function was patched and then unpatched by `el-patch'."))
(error "There is no patch for %S %S" type name)))
;;;; Defining patch types
;;;###autoload
(cl-defmacro el-patch-deftype
(type &rest kwargs &key classify locate declare macro-name font-lock)
"Allow `el-patch' to patch definitions of the given TYPE.
TYPE is a symbol like `defun', `define-minor-mode', etc. This
updates `el-patch-deftype-alist' (which see for explanations of
CLASSIFY, LOCATE, DECLARE, MACRO-NAME, and FONT-LOCK) with the
provided KWARGS and defines a macro named like `el-patch-defun',
`el-patch-define-minor-mode', etc. (which can be overridden by
MACRO-NAME)."
(declare (indent defun))
(ignore locate)
(unless classify
(error "You must specify `:classify' in calls to `el-patch-deftype'"))
(let ((macro-name (or macro-name (intern (format "el-patch-%S" type)))))
`(progn
(unless (bound-and-true-p el-patch-deftype-alist)
(setq el-patch-deftype-alist nil))
(setf (alist-get ',type el-patch-deftype-alist)
;; Make sure we don't accidentally create self-modifying
;; code if somebody decides to mutate
;; `el-patch-deftype-alist'.
(copy-tree ',kwargs))
,@(when font-lock
`((,font-lock ',macro-name)))
(defmacro ,macro-name
(name &rest args)
,(format "Use `el-patch' to override a `%S' form.
The ARGS are the same as for `%S'."
type type)
,@(when declare
`((declare ,@declare)))
(list #'el-patch--definition (cl-list* ',type name args))))))
;;;;; Classification functions
(defun el-patch-classify-variable (definition)
"Classify the items defined by a variable DEFINITION.
DEFINITION is a list starting with `defvar' or similar."
(list (cons 'variable (nth 1 definition))))
(defun el-patch-classify-function (definition)
"Classify the items defined by a function DEFINITION.
DEFINITION is a list starting with `defun' or similar."
(list (cons 'function (nth 1 definition))))
(defun el-patch-classify-define-minor-mode (definition)
"Classify the items defined by a minor mode DEFINITION.
DEFINITION is a list starting with `define-minor-mode' or
similar."
(let* ((function-name (nth 1 definition))
(variable-name (nth 1 definition))
(kw-args (nthcdr 3 definition)))
(dotimes (_ 3)
(unless (keywordp (car kw-args))
(setq kw-args (cdr kw-args))))
(while (keywordp (car kw-args))
(when (eq (car kw-args) :variable)
(setq variable-name (car kw-args)))
(setq kw-args (nthcdr 2 kw-args)))
(list (cons 'function function-name)
(cons 'variable variable-name))))
;;;;; Font-lock functions
;;;###autoload
(defun el-patch-fontify-as-defun (name)
"Fontify `el-patch' macro with given NAME as function definition."
(font-lock-add-keywords
'emacs-lisp-mode
`((,(concat
(format "(\\(%S\\)\\>[[:blank:]]+\\(" name)
lisp-mode-symbol-regexp
"\\)[[:blank:]]")
(2 font-lock-function-name-face)))))
;;;###autoload
(defun el-patch-fontify-as-variable (name)
"Fontify `el-patch' macro with given NAME as variable definition."
(font-lock-add-keywords
'emacs-lisp-mode
`((,(concat
(format "(\\(%S\\)\\>[[:blank:]]+\\(" name)
lisp-mode-symbol-regexp
"\\)[[:blank:]]")
(2 font-lock-variable-name-face)))))
;;;;; Location functions
(defmacro el-patch-wrap-locator (&rest body)
"Wrap the operation of `find-function-noselect' or similar.
This disables local variables and messaging, saves the current
buffer and point, etc. BODY is executed within this context. It
is assumed that BODY finds the appropriate file in a buffer using
`get-file-buffer', and then returns a cons cell with the buffer
and point for the beginning of some Lisp form. The return value
is the Lisp form, read from the buffer at point."
(declare (indent 0))
`(let* (;; Since Emacs actually opens the source file in a (hidden)
;; buffer, it can try to apply local variables, which might
;; result in an annoying interactive prompt. Let's disable
;; that.
(enable-local-variables nil)
(enable-dir-local-variables nil)
;; This is supposed to be noninteractive so we also suppress
;; all the messages. This has the side effect of masking all
;; debugging messages (you can use `insert' instead, or
;; temporarily remove these bindings), but there are just so
;; many different messages that can happen for various
;; reasons and I haven't found any other standard way to
;; suppress them.
(inhibit-message t)
(message-log-max nil)
;; Now we actually execute BODY to move point to the right
;; file and location.
(buffer-point (save-excursion
;; This horrifying bit of hackery on
;; `get-file-buffer' prevents
;; `find-function-noselect' from returning
;; an existing buffer, so that later on when
;; we jump to the definition, we don't
;; temporarily scroll the window if the
;; definition happens to be in the *current*
;; buffer.
(cl-letf (((symbol-function #'get-file-buffer)
#'ignore))
;; Because we get an error if the function
;; doesn't have a definition anywhere.
(ignore-errors
,@body))))
(defun-buffer (car buffer-point))
(defun-point (cdr buffer-point)))
(prog1 (and defun-buffer
defun-point
(with-current-buffer defun-buffer
(save-excursion
(goto-char defun-point)
(read (current-buffer)))))
(when defun-buffer
(kill-buffer defun-buffer)))))
(defun el-patch-locate-variable (definition)
"Return the source code of DEFINITION.
DEFINITION is a list starting with `defvar' or similar."
(el-patch-wrap-locator
(find-variable-noselect (nth 1 definition))))
(defun el-patch-locate-function (definition)
"Return the source code of DEFINITION.
DEFINITION is a list starting with `defun' or similar."
(el-patch-wrap-locator
(find-function-noselect (nth 1 definition) 'lisp-only)))
;;;;; Predefined patch types
;;;###autoload(require 'el-patch-stub)
;;;###autoload(el-patch--deftype-stub-setup)
;; These are alphabetized.
;;;###autoload
(el-patch-deftype cl-defun
:classify el-patch-classify-function
:locate el-patch-locate-function
:font-lock el-patch-fontify-as-defun
:declare ((doc-string 3)
(indent defun)))
;;;###autoload
(el-patch-deftype defconst
:classify el-patch-classify-variable
:locate el-patch-locate-variable
:font-lock el-patch-fontify-as-variable
:declare ((doc-string 3)
(indent defun)))
;;;###autoload
(el-patch-deftype defcustom
:classify el-patch-classify-variable
:locate el-patch-locate-variable
:font-lock el-patch-fontify-as-variable
:declare ((doc-string 3)
(indent defun)))
;;;###autoload
(el-patch-deftype define-minor-mode
:classify el-patch-classify-define-minor-mode
:locate el-patch-locate-function
:font-lock el-patch-fontify-as-defun
:declare ((doc-string 2)
(indent defun)))
;;;###autoload
(el-patch-deftype defmacro
:classify el-patch-classify-function
:locate el-patch-locate-function
:font-lock el-patch-fontify-as-defun
:declare ((doc-string 3)
(indent defun)))
;;;###autoload
(el-patch-deftype defsubst
:classify el-patch-classify-function
:locate el-patch-locate-function
:font-lock el-patch-fontify-as-defun
:declare ((doc-string 3)
(indent defun)))
;;;###autoload
(el-patch-deftype defun
:classify el-patch-classify-function
:locate el-patch-locate-function
:font-lock el-patch-fontify-as-defun
:declare ((doc-string 3)
(indent defun)))
;;;###autoload
(el-patch-deftype defvar
:classify el-patch-classify-variable
:locate el-patch-locate-variable
:font-lock el-patch-fontify-as-variable
:declare ((doc-string 3)
(indent defun)))
;;;; Validating patches
(defcustom el-patch-pre-validate-hook nil
"Hook run before `el-patch-validate-all'.
Also run before `el-patch-validate' if a prefix argument is
provided. This hook should contain functions that make sure all
of your patches are defined (for example, you might need to load
some features if your patches are lazily defined)."
:type 'hook)
(defcustom el-patch-post-validate-hook nil
"Hook run after `el-patch-validate-all'.
Also run after `el-patch-validate' if a prefix argument is
provided. This hook should contain functions that undo any
patching that might have taken place in
`el-patch-pre-validate-hook', if you do not want the patches to
be defined permanently."
:type 'hook)
(defun el-patch--locate (definition)
"Return the Lisp form corresponding to the given DEFINITION.
Return nil if such a definition cannot be found. (That would
happen if the definition were generated dynamically.) TYPE is a
symbol `defun', `defmacro', etc. which is used to determine
whether the symbol is a function or variable."
(let* ((type (nth 0 definition))
(props (alist-get type el-patch-deftype-alist))
(locator (plist-get props :locate)))
(unless locator
(error
"Definition type `%S' has no `:locate' in `el-patch-deftype-alist'"
type))
(funcall locator definition)))
;;;###autoload
(defun el-patch-validate (name type &optional nomsg run-hooks variant)
"Validate the patch with given NAME and TYPE.
This means el-patch will attempt to find the original definition
for the function, and verify that it is the same as the original
function assumed by the patch. A warning will be signaled if the
original definition for a patched function cannot be found, or if
there is a difference between the actual and expected original
definitions.
If multiple variants exist for the same patch, then select the
one specified by VARIANT (defaults to nil, like
`el-patch-variant'). For advanced usage only.
Interactively, use `completing-read' to select a function to
inspect the patch of.
NAME is a symbol naming the object being patched; TYPE is a
symbol `defun', `defmacro', etc.
Returns nil if the patch is not valid, and otherwise returns t.
If NOMSG is non-nil, does not signal a message when the patch is
valid.
If RUN-HOOKS is non-nil, runs `el-patch-pre-validate-hook' and
`el-patch-post-validate-hook'. Interactively, this happens unless
a prefix argument is provided.
See also `el-patch-validate-all'."
(interactive (progn
(unless current-prefix-arg
(run-hooks 'el-patch-pre-validate-hook))
(cl-destructuring-bind (name type variant)
(el-patch--select-patch)
(list
name type nil
(unless current-prefix-arg
'post-only)
variant))))
(unless (member run-hooks '(nil post-only))
(run-hooks 'el-patch-pre-validate-hook))
(unwind-protect
(progn
(let* ((patch-definition (el-patch-get name type variant))
(type (car patch-definition))
(expected-definition (el-patch--resolve-definition
patch-definition nil))
(name (cadr expected-definition))
(actual-definition (el-patch--locate expected-definition)))
(cond
((not actual-definition)
(display-warning
'el-patch
(format "Could not find definition of %S `%S'" type name))
nil)
((not (equal expected-definition actual-definition))
(display-warning
'el-patch
(format (concat "Definition of %S `%S' differs from what "
"is assumed by its patch")
type name))
nil)
(t
(unless nomsg
(message "Patch is valid"))
t))))
(when run-hooks
(run-hooks 'el-patch-post-validate-hook))))
;;;###autoload
(defun el-patch-validate-all ()
"Validate all currently defined patches.
Runs `el-patch-pre-validate-hook' and
`el-patch-post-validate-hook'.
See `el-patch-validate'."
(interactive)
(run-hooks 'el-patch-pre-validate-hook)
(unwind-protect
(let ((patch-count 0)
(warning-count 0))
(dolist (name (hash-table-keys el-patch--patches))
(let ((patch-hash (gethash name el-patch--patches)))
(dolist (type (hash-table-keys patch-hash))
(let ((type-hash (gethash type patch-hash)))
(dolist (variant (hash-table-keys type-hash))
(setq patch-count (1+ patch-count))
(let ((el-patch-variant variant))
(unless (el-patch-validate name type 'nomsg)
(setq warning-count (1+ warning-count)))))))))
(cond
((zerop patch-count)
(user-error "No patches defined"))
((zerop warning-count)
(if (= patch-count 1)
(message "Patch is valid (only one defined)")
(message "All %d patches are valid" patch-count)))
((= patch-count warning-count)
(if (= patch-count 1)
(message "Patch is invalid (only one defined)")
(message "All %d patches are invalid" patch-count)))
(t
(message "%s valid, %s invalid"
(if (= warning-count (1- patch-count))
"1 patch is"
(format "%d patches are" (- patch-count warning-count)))
(if (= warning-count 1)
"1 patch is"
(format "%d patches are" warning-count))))))
(run-hooks 'el-patch-post-validate-hook)))
;;;;; el-patch-feature
;;;###autoload
(defmacro el-patch-feature (feature &rest args)
"Declare that some patches are only defined after FEATURE is loaded.
This is a convenience macro that creates a function for invoking
`require' on that feature, and then adds it to
`el-patch-pre-validate-hook' so that your patches are loaded and
`el-patch' can properly validate them.
FEATURE should be an unquoted symbol. ARGS, if given, are passed
unchanged along with FEATURE to `el-patch-require-function' when
`el-patch-validate-all' is called."
(let ((defun-name (intern (format "el-patch-require-%S" feature))))
`(progn
(defun ,defun-name ()
(funcall el-patch-require-function ',feature ,@args))
(add-hook 'el-patch-pre-validate-hook #',defun-name))))
;;;; Viewing patches
;;;###autoload
(defun el-patch-get (name type &optional variant)
"Return the patch for object NAME of the given TYPE.
NAME is a symbol for the name of the definition that was patched,
and TYPE is a symbol `defun', `defmacro', etc. If the patch could
not be found, return nil.
If VARIANT is provided, select that variant of the patch. This is
useful only if patches were defined using `el-patch-variant'."
(condition-case nil
(gethash variant (gethash type (gethash name el-patch--patches)))
(error nil)))
(defun el-patch--select-patch ()
"Use `completing-read' to select a patched function.
Return a list of three elements, the name (a symbol) of the
object being patched, the type (a symbol `defun', `defmacro',
etc.) of the definition, and the patch variant (a symbol, usually
nil; see `el-patch-variant')."
(let* ((options (mapcar #'symbol-name (hash-table-keys el-patch--patches)))