aboutsummaryrefslogtreecommitdiffstats
path: root/elpa/compat-28.1.1.0/compat-macs.el
blob: e1dcf81eff0d20df38360576e0b7c8c0be1b105b (plain) (blame)
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
;;; compat-macs.el --- Compatibility Macros           -*- lexical-binding: t; -*-

;; Copyright (C) 2021, 2022 Free Software Foundation, Inc.

;; Author: Philip Kaludercic <philipk@posteo.net>
;; Keywords: lisp

;; This program 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.

;; This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; These macros are used to define compatibility functions, macros and
;; advice.

;;; Code:

(defmacro compat--ignore (&rest _)
  "Ignore all arguments."
  nil)

(defvar compat--generate-function #'compat--generate-minimal
  "Function used to generate compatibility code.
The function must take six arguments: NAME, DEF-FN, INSTALL-FN,
CHECK-FN, ATTR and TYPE.  The resulting body is constructed by
invoking the functions DEF-FN (passed the \"realname\" and the
version number, returning the compatibility definition), the
INSTALL-FN (passed the \"realname\" and returning the
installation code), CHECK-FN (passed the \"realname\" and
returning a check to see if the compatibility definition should
be installed).  ATTR is a plist used to modify the generated
code.  The following attributes are handled, all others are
ignored:

- :min-version :: Prevent the compatibility definition from begin
  installed in versions older than indicated (string).

- :max-version :: Prevent the compatibility definition from begin
  installed in versions newer than indicated (string).

- :feature :: The library the code is supposed to be loaded
  with (via `eval-after-load').

- :cond :: Only install the compatibility code, iff the value
  evaluates to non-nil.

  For prefixed functions, this can be interpreted as a test to
  `defalias' an existing definition or not.

- :no-highlight :: Do not highlight this definition as
  compatibility function.

- :version :: Manual specification of the version the compatee
  code was defined in (string).

- :realname :: Manual specification of a \"realname\" to use for
  the compatibility definition (symbol).

- :notes :: Additional notes that a developer using this
  compatibility function should keep in mind.

- :prefix :: Add a `compat-' prefix to the name, and define the
  compatibility code unconditionally.

TYPE is used to set the symbol property `compat-type' for NAME.")

(defun compat--generate-minimal (name def-fn install-fn check-fn attr type)
  "Generate a leaner compatibility definition.
See `compat-generate-function' for details on the arguments NAME,
DEF-FN, INSTALL-FN, CHECK-FN, ATTR and TYPE."
  (let* ((min-version (plist-get attr :min-version))
         (max-version (plist-get attr :max-version))
         (feature (plist-get attr :feature))
         (cond (plist-get attr :cond))
         (version (or (plist-get attr :version)
                      (let ((file (or (bound-and-true-p byte-compile-current-file)
                                      load-file-name
                                      (buffer-file-name))))
                        ;; Guess the version from the file the macro is
                        ;; being defined in.
                        (cond
                         ((not file) emacs-version)
                         ((string-match
                           "compat-\\([[:digit:]]+\\)\\.\\(?:elc?\\)\\'"
                           file)
                          (match-string 1 file))
                         ((error "No version number could be extracted"))))))
         (realname (or (plist-get attr :realname)
                       (intern (format "compat--%S" name))))
         (check (cond
                 ((or (and min-version
                           (version< emacs-version min-version))
                      (and max-version
                           (version< max-version emacs-version)))
                  '(compat--ignore))
                 ((plist-get attr :prefix)
                  '(progn))
                 ((and version (version<= version emacs-version) (not cond))
                  '(compat--ignore))
                 (`(when (and ,(if cond cond t)
                              ,(funcall check-fn)))))))
    (cond
     ((and (plist-get attr :prefix) (memq type '(func macro))
           (string-match "\\`compat-\\(.+\\)\\'" (symbol-name name))
           (let* ((actual-name (intern (match-string 1 (symbol-name name))))
                  (body (funcall install-fn actual-name version)))
             (when (and (version<= version emacs-version)
                        (fboundp actual-name))
               `(,@check
                 ,(if feature
                      ;; See https://nullprogram.com/blog/2018/02/22/:
                      `(eval-after-load ,feature `(funcall ',(lambda () ,body)))
                    body))))))
     ((plist-get attr :realname)
      `(progn
         ,(funcall def-fn realname version)
         (,@check
          ,(let ((body (funcall install-fn realname version)))
             (if feature
                 ;; See https://nullprogram.com/blog/2018/02/22/:
                 `(eval-after-load ,feature `(funcall ',(lambda () ,body)))
               body)))))
     ((let* ((body (if (eq type 'advice)
                       `(,@check
                         ,(funcall def-fn realname version)
                         ,(funcall install-fn realname version))
                     `(,@check ,(funcall def-fn name version)))))
        (if feature
            ;; See https://nullprogram.com/blog/2018/02/22/:
            `(eval-after-load ,feature `(funcall ',(lambda () ,body)))
          body))))))

(defun compat--generate-minimal-no-prefix (name def-fn install-fn check-fn attr type)
  "Generate a leaner compatibility definition.
See `compat-generate-function' for details on the arguments NAME,
DEF-FN, INSTALL-FN, CHECK-FN, ATTR and TYPE."
  (unless (plist-get attr :prefix)
    (compat--generate-minimal name def-fn install-fn check-fn attr type)))

(defun compat--generate-verbose (name def-fn install-fn check-fn attr type)
  "Generate a more verbose compatibility definition, fit for testing.
See `compat-generate-function' for details on the arguments NAME,
DEF-FN, INSTALL-FN, CHECK-FN, ATTR and TYPE."
  (let* ((min-version (plist-get attr :min-version))
         (max-version (plist-get attr :max-version))
         (feature (plist-get attr :feature))
         (cond (plist-get attr :cond))
         (version (or (plist-get attr :version)
                      (let ((file (or (bound-and-true-p byte-compile-current-file)
                                      load-file-name
                                      (buffer-file-name))))
                        ;; Guess the version from the file the macro is
                        ;; being defined in.
                        (cond
                         ((not file) emacs-version)
                         ((string-match
                           "compat-\\([[:digit:]]+\\)\\.\\(?:elc?\\)\\'"
                           file)
                          (match-string 1 file))
                         ((error "No version number could be extracted"))))))
         (realname (or (plist-get attr :realname)
                       (intern (format "compat--%S" name))))
         (body `(progn
                  (unless (or (null (get ',name 'compat-def))
                              (eq (get ',name 'compat-def) ',realname))
                    (error "Duplicate compatibility definition: %s (was %s, now %s)"
                           ',name (get ',name 'compat-def) ',realname))
                  (put ',name 'compat-def ',realname)
                  ,(funcall install-fn realname version))))
    `(progn
       (put ',realname 'compat-type ',type)
       (put ',realname 'compat-version ,version)
       (put ',realname 'compat-min-version ,min-version)
       (put ',realname 'compat-max-version ,max-version)
       (put ',realname 'compat-doc ,(plist-get attr :note))
       ,(funcall def-fn realname version)
       (,@(cond
           ((or (and min-version
                     (version< emacs-version min-version))
                (and max-version
                     (version< max-version emacs-version)))
            '(compat--ignore))
           ((plist-get attr :prefix)
            '(progn))
           ((and version (version<= version emacs-version) (not cond))
            '(compat--ignore))
           (`(when (and ,(if cond cond t)
                        ,(funcall check-fn)))))
        ,(if feature
             ;; See https://nullprogram.com/blog/2018/02/22/:
             `(eval-after-load ,feature `(funcall ',(lambda () ,body)))
           body)))))

(defun compat-generate-common (name def-fn install-fn check-fn attr type)
  "Common code for generating compatibility definitions.
See `compat-generate-function' for details on the arguments NAME,
DEF-FN, INSTALL-FN, CHECK-FN, ATTR and TYPE."
  (when (and (plist-get attr :cond) (plist-get attr :prefix))
    (error "A prefixed function %s cannot have a condition" name))
  (funcall compat--generate-function
           name def-fn install-fn check-fn attr type))

(defun compat-common-fdefine (type name arglist docstring rest)
  "Generate compatibility code for a function NAME.
TYPE is one of `func', for functions and `macro' for macros, and
`advice' ARGLIST is passed on directly to the definition, and
DOCSTRING is prepended with a compatibility note.  REST contains
the remaining definition, that may begin with a property list of
attributes (see `compat-generate-common')."
  (let ((oldname name) (body rest))
    (while (keywordp (car body))
      (setq body (cddr body)))
    ;; It might be possible to set these properties otherwise.  That
    ;; should be looked into and implemented if it is the case.
    (when (and (listp (car-safe body)) (eq (caar body) 'declare))
      (when (version<= emacs-version "25")
        (delq (assq 'side-effect-free (car body)) (car body))
        (delq (assq 'pure (car body)) (car body))))
    ;; Check if we want an explicitly prefixed function
    (when (plist-get rest :prefix)
      (setq name (intern (format "compat-%s" name))))
    (compat-generate-common
     name
     (lambda (realname version)
       `(,(cond
           ((memq type '(func advice)) 'defun)
           ((eq type 'macro) 'defmacro)
           ((error "Unknown type")))
         ,realname ,arglist
         ;; Prepend compatibility notice to the actual
         ;; documentation string.
         ,(let ((type (cond
                       ((eq type 'func) "function")
                       ((eq type 'macro) "macro")
                       ((eq type 'advice) "advice")
                       ((error "Unknown type")))))
            (if version
                (format
                 "[Compatibility %s for `%S', defined in Emacs %s]\n\n%s"
                 type oldname version docstring)
              (format
               "[Compatibility %s for `%S']\n\n%s"
               type oldname docstring)))
         ;; Advice may use the implicit variable `oldfun', but
         ;; to avoid triggering the byte compiler, we make
         ;; sure the argument is used at least once.
         ,@(if (eq type 'advice)
               (cons '(ignore oldfun) body)
             body)))
     (lambda (realname _version)
       (cond
        ((memq type '(func macro))
         ;; Functions and macros are installed by
         ;; aliasing the name of the compatible
         ;; function to the name of the compatibility
         ;; function.
         `(defalias ',name #',realname))
        ((eq type 'advice)
         `(advice-add ',name :around #',realname))))
     (lambda ()
       (cond
        ((memq type '(func macro))
         `(not (fboundp ',name)))
        ((eq type 'advice) t)))
     rest type)))

(defmacro compat-defun (name arglist docstring &rest rest)
  "Define NAME with arguments ARGLIST as a compatibility function.
The function must be documented in DOCSTRING.  REST may begin
with a plist, that is interpreted by the macro but not passed on
to the actual function.  See `compat-generate-common' for a
listing of attributes.

The definition will only be installed, if the version this
function was defined in, as indicated by the `:version'
attribute, is greater than the current Emacs version."
  (declare (debug (&define name (&rest symbolp)
                           stringp
                           [&rest keywordp sexp]
                           def-body))
           (doc-string 3) (indent 2))
  (compat-common-fdefine 'func name arglist docstring rest))

(defmacro compat-defmacro (name arglist docstring &rest rest)
  "Define NAME with arguments ARGLIST as a compatibility macro.
The macro must be documented in DOCSTRING.  REST may begin
with a plist, that is interpreted by this macro but not passed on
to the actual macro.  See `compat-generate-common' for a
listing of attributes.

The definition will only be installed, if the version this
function was defined in, as indicated by the `:version'
attribute, is greater than the current Emacs version."
  (declare (debug compat-defun) (doc-string 3) (indent 2))
  (compat-common-fdefine 'macro name arglist docstring rest))

(defmacro compat-advise (name arglist docstring &rest rest)
  "Define NAME with arguments ARGLIST as a compatibility advice.
The advice function must be documented in DOCSTRING.  REST may
begin with a plist, that is interpreted by this macro but not
passed on to the actual advice function.  See
`compat-generate-common' for a listing of attributes.  The advice
wraps the old definition, that is accessible via using the symbol
`oldfun'.

The advice will only be installed, if the version this function
was defined in, as indicated by the `:version' attribute, is
greater than the current Emacs version."
  (declare (debug compat-defun) (doc-string 3) (indent 2))
  (compat-common-fdefine 'advice name (cons 'oldfun arglist) docstring rest))

(defmacro compat-defvar (name initval docstring &rest attr)
  "Declare compatibility variable NAME with initial value INITVAL.
The obligatory documentation string DOCSTRING must be given.

The remaining arguments ATTR form a plist, modifying the
behaviour of this macro.  See `compat-generate-common' for a
listing of attributes.  Furthermore, `compat-defvar' also handles
the attribute `:local' that either makes the variable permanent
local with a value of `permanent' or just buffer local with any
non-nil value."
  (declare (debug (name form stringp [&rest keywordp sexp]))
           (doc-string 3) (indent 2))
  ;; Check if we want an explicitly prefixed function
  (let ((oldname name))
    (when (plist-get attr :prefix)
      (setq name (intern (format "compat-%s" name))))
    (compat-generate-common
     name
     (lambda (realname version)
       (let ((localp (plist-get attr :local)))
         `(progn
            (,(if (plist-get attr :constant) 'defconst 'defvar)
             ,realname ,initval
             ;; Prepend compatibility notice to the actual
             ;; documentation string.
             ,(if version
                  (format
                   "[Compatibility variable for `%S', defined in Emacs %s]\n\n%s"
                   oldname version docstring)
                (format
                 "[Compatibility variable for `%S']\n\n%s"
                 oldname docstring)))
            ;; Make variable as local if necessary
            ,(cond
              ((eq localp 'permanent)
               `(put ',realname 'permanent-local t))
              (localp
               `(make-variable-buffer-local ',realname))))))
     (lambda (realname _version)
       `(defvaralias ',name ',realname))
     (lambda ()
       `(not (boundp ',name)))
     attr 'variable)))

(provide 'compat-macs)
;;; compat-macs.el ends here