Subject: [PATCH] Make byte-compiler warn about wide docstrings

From: Stefan Kangas <[email-masked]>
Date: Tue, 24 Nov 2020 17:36:34 -0800

Severity: wishlist

In etc/TODO we have:

** Make byte-compiler warn when a doc string is too wide

Please find attached a patch that implements this.  Notably, it omits
defuns, and ignores any lines containing `substitute-command-keys' style
substitutions.

I've been messing around with getting defuns to work, but I can't find a
way for it to work reliably.  The problem is that they are already macro
expanded when we start issuing warnings, so it's not trivial to reliably
separate defuns from other cases where lambda is used.  We also have
other macros like `define-derived-mode' that produces `defun's
themselves, and the line reporting gets messed up.  (The autogenerated
docstrings from these macros seem to generate too wide docstrings in
several cases.)

For `substitute-command-keys', it would be nice to get it to work, but I
don't think we can know the values of keymaps at compile-time.  Possibly
there is a good solution for this, but I couldn't find it.

As for the state of our own sources, we seem to have less than 100
docstrings that break our own conventions and generates a warning with
the patch.  It shouldn't take too much work to fix them.

(If you were to add defuns into the mix, we would get around 700
warnings for wide docstrings, several of which would be autogenerated.
We should probably look into that at some point...)

Thoughts?
From 03b343d0709625aa92fced65e56ad29ff6d84d0c Mon Sep 17 00:00:00 2001
From: Stefan Kangas <[email-masked]>
Date: Wed, 25 Nov 2020 02:25:57 +0100
Subject: [PATCH] Make byte-compiler warn about wide docstrings

* lisp/emacs-lisp/bytecomp.el (byte-compile--wide-docstring-p):
(byte-compile-docstring-length-warn): New defuns.
(byte-compile-docstring-max-column): New defcustom.
(byte-compile-warning-types, byte-compile-warnings): New value
'docstrings'.
(byte-compile-file-form-autoload, byte-compile-file-form-defvar):
(byte-compile-file-form-defvar-function, byte-compile-lambda):
(byte-compile-defvar, byte-compile-file-form-defalias): Warn about too
wide docstrings.

* test/lisp/emacs-lisp/bytecomp-tests.el
(bytecomp--define-warning-file-test): New macro.
(bytecomp-warn/warn-long-docstring-autoload\.el)
(bytecomp-warn/warn-long-docstring-custom-declare-variable\.el)
(bytecomp-warn/warn-long-docstring-defalias\.el)
(bytecomp-warn/warn-long-docstring-defconst\.el)
(bytecomp-warn/warn-long-docstring-define-abbrev-table\.el)
(bytecomp-warn/warn-long-docstring-define-obsolete-function-alias\.el)
(bytecomp-warn/warn-long-docstring-define-obsolete-variable-alias\.el)
(bytecomp-warn/warn-long-docstring-defun\.el)
(bytecomp-warn/warn-long-docstring-defvar\.el)
(bytecomp-warn/warn-long-docstring-defvaralias\.el)
(bytecomp-warn/warn-long-docstring-ignore-fill-column\.el)
(bytecomp-warn/warn-long-docstring-ignore\.el): New tests.
* test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-autoload.el:
* test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-custom-declare-variable.el:
* test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defalias.el:
* test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defconst.el:
* test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-abbrev-table.el:
* test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-obsolete-function-alias.el:
* test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-obsolete-variable-alias.el:
* test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defun.el:
* test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defvar.el:
* test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defvaralias.el:
* test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-ignore-fill-column.el:
* test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-ignore.el:
New files.
---
 etc/NEWS                                      |  9 ++-
 etc/TODO                                      |  2 -
 lisp/emacs-lisp/bytecomp.el                   | 71 ++++++++++++++++++-
 .../warn-long-docstring-autoload.el           |  3 +
 ...-long-docstring-custom-declare-variable.el |  4 ++
 .../warn-long-docstring-defalias.el           |  3 +
 .../warn-long-docstring-defconst.el           |  3 +
 ...warn-long-docstring-define-abbrev-table.el |  3 +
 ...ocstring-define-obsolete-function-alias.el |  3 +
 ...ocstring-define-obsolete-variable-alias.el |  3 +
 .../warn-long-docstring-defun.el              |  3 +
 .../warn-long-docstring-defvar.el             |  3 +
 .../warn-long-docstring-defvaralias.el        |  3 +
 .../warn-long-docstring-ignore-fill-column.el |  7 ++
 .../warn-long-docstring-ignore.el             |  7 ++
 test/lisp/emacs-lisp/bytecomp-tests.el        | 69 ++++++++++++++++++
 16 files changed, 191 insertions(+), 5 deletions(-)
 create mode 100644 test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-autoload.el
 create mode 100644 test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-custom-declare-variable.el
 create mode 100644 test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defalias.el
 create mode 100644 test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defconst.el
 create mode 100644 test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-abbrev-table.el
 create mode 100644 test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-obsolete-function-alias.el
 create mode 100644 test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-obsolete-variable-alias.el
 create mode 100644 test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defun.el
 create mode 100644 test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defvar.el
 create mode 100644 test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defvaralias.el
 create mode 100644 test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-ignore-fill-column.el
 create mode 100644 test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-ignore.el

diff --git a/etc/NEWS b/etc/NEWS
index 95f801f60c..e768657841 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1978,13 +1978,20 @@ menu handling.
 +++
 ** 'inhibit-nul-byte-detection' is renamed to 'inhibit-null-byte-detection'.
 
+** byte compiler
+
 +++
-** New byte-compiler check for missing dynamic variable declarations.
+*** New byte-compiler check for missing dynamic variable declarations.
 It is meant as an (experimental) aid for converting Emacs Lisp code
 to lexical binding, where dynamic (special) variables bound in one
 file can affect code in another.  For details, see the manual section
 "(Elisp) Converting to Lexical Binding".
 
+*** The byte-compiler now warns about too wide documentation strings.
+By default, it will warn if a documentation string is wider than the
+largest of 80 characters or 'fill-column'.  See the new user
+option 'byte-compile-docstring-max-column'.
+
 ---
 ** 'unload-feature' now also tries to undo additions to buffer-local hooks.
 
diff --git a/etc/TODO b/etc/TODO
index 8e93e7fb10..38dfda424f 100644
--- a/etc/TODO
+++ b/etc/TODO
@@ -572,8 +572,6 @@ Do this for some or all errors associated with using subprocesses.
 ** Maybe reinterpret 'parse-error' as a category of errors
 Put some other errors under it.
 
-** Make byte-compiler warn when a doc string is too wide
-
 ** Make byte-optimization warnings issue accurate line numbers
 
 ** Record the sxhash of the default value for customized variables
diff --git a/lisp/emacs-lisp/bytecomp.el b/lisp/emacs-lisp/bytecomp.el
index a687d291e9..1eaac6a343 100644
--- a/lisp/emacs-lisp/bytecomp.el
+++ b/lisp/emacs-lisp/bytecomp.el
@@ -297,7 +297,8 @@ byte-compile-error-on-warn
 (defconst byte-compile-warning-types
   '(redefine callargs free-vars unresolved
 	     obsolete noruntime cl-functions interactive-only
-	     make-local mapcar constants suspicious lexical lexical-dynamic)
+             make-local mapcar constants suspicious lexical lexical-dynamic
+             docstrings)
   "The list of warning types used when `byte-compile-warnings' is t.")
 (defcustom byte-compile-warnings t
   "List of warnings that the byte-compiler should issue (t for all).
@@ -322,6 +323,8 @@ byte-compile-warnings
   make-local  calls to make-variable-buffer-local that may be incorrect.
   mapcar      mapcar called for effect.
   constants   let-binding of, or assignment to, constants/nonvariables.
+  docstrings  docstrings that are too wide (no longer than 80 characters,
+              or `fill-column', whichever is bigger)
   suspicious  constructs that usually don't do what the coder wanted.
 
 If the list begins with `not', then the remaining elements specify warnings to
@@ -1581,6 +1584,64 @@ byte-compile-arglist-warn
            (if (equal sig1 '(1 . 1)) "argument" "arguments")
            (byte-compile-arglist-signature-string sig2)))))))
 
+(defun byte-compile--wide-docstring-p (docstring col)
+  "Return t if string DOCSTRING is wider than COL.
+Ignore any `substitute-command-keys' substitutions."
+  (string-match
+   (format "^.\\{%s,\\}$" (int-to-string (1+ col)))
+   ;; Ignore any `substitute-command-keys' substitutions.
+   (replace-regexp-in-string
+    (rx "\\" (or "="
+                 (seq "[" (* (not "]")) "]")
+                 (seq "<" (* (not ">")) ">")
+                 (seq "{" (* (not "}")) "}")))
+    ""
+    docstring)))
+
+(defcustom byte-compile-docstring-max-column 80
+  "Length that a doc string can be before the byte-compiler reports a warning."
+  :group 'bytecomp
+  :type 'integer
+  :safe #'integerp
+  :version "28.1")
+
+(defun byte-compile-docstring-length-warn (form)
+  "Warn if documentation string of FORM is too wide.
+It is too wide if it is longer than the largest of `fill-column'
+and `byte-compile-docstring-max-column'."
+  ;; TODO: This has some limitations that it would be nice to fix.
+  ;; 1. We don't try to handle defuns.  It is somewhat tricky to get
+  ;;    it right since `defun' is a macro.  Also, some macros
+  ;;    themselves produce defuns (e.g. `define-derived-mode').
+  ;; 2. We ignore any line where there is a `subsititute-command-keys'
+  ;;    replacement.  We can't reliably do these replacements, since
+  ;;    the value of the keymaps in general can't be known at compile
+  ;;    time.
+  (when (byte-compile-warning-enabled-p 'docstrings)
+    (let ((col (max byte-compile-docstring-max-column fill-column))
+          kind name docs)
+      (pcase (car form)
+        ((or 'autoload 'custom-declare-variable 'defalias
+             'defconst 'define-abbrev-table
+             'defvar 'defvaralias)
+         (setq kind (nth 0 form))
+         (setq name (nth 1 form))
+         (setq docs (nth 3 form)))
+        ;;;; Here is how one could add lambda's here:
+        ;; ('lambda
+        ;;   (setq kind "")   ; can't be "function", unfortunately
+        ;;   (setq docs (and (stringp (nth 2 form))
+        ;;                   (nth 2 form))))
+        )
+      (when (and (consp name) (eq (car name) 'quote))
+        (setq name (cadr name)))
+      (setq name (if name (format " `%s'" name) ""))
+      (when (and kind docs (stringp docs)
+                 (byte-compile--wide-docstring-p docs col))
+        (byte-compile-warn "%s%s docstring wider than %s characters"
+                           kind name col))))
+  form)
+
 (defvar byte-compile-cl-functions nil
   "List of functions defined in CL.")
 
@@ -2446,7 +2507,8 @@ byte-compile-file-form-autoload
              (delq (assq funsym byte-compile-unresolved-functions)
                    byte-compile-unresolved-functions)))))
   (if (stringp (nth 3 form))
-      form
+      (prog1 form
+        (byte-compile-docstring-length-warn form))
     ;; No doc string, so we can compile this as a normal form.
     (byte-compile-keep-pending form 'byte-compile-normal-call)))
 
@@ -2474,6 +2536,7 @@ byte-compile-file-form-defvar
   (if (and (null (cddr form))		;No `value' provided.
            (eq (car form) 'defvar))     ;Just a declaration.
       nil
+    (byte-compile-docstring-length-warn form)
     (cond ((consp (nth 2 form))
            (setq form (copy-sequence form))
            (setcar (cdr (cdr form))
@@ -2497,6 +2560,7 @@ byte-compile-file-form-defvar-function
        (if (byte-compile-warning-enabled-p 'suspicious)
            (byte-compile-warn
             "Alias for `%S' should be declared before its referent" newname)))))
+  (byte-compile-docstring-length-warn form)
   (byte-compile-keep-pending form))
 
 (put 'custom-declare-variable 'byte-hunk-handler
@@ -2895,6 +2959,7 @@ byte-compile-lambda
     (unless (eq 'lambda (car-safe fun))
       (error "Not a lambda list: %S" fun))
     (byte-compile-set-symbol-position 'lambda))
+  (byte-compile-docstring-length-warn fun)
   (byte-compile-check-lambda-list (nth 1 fun))
   (let* ((arglist (nth 1 fun))
          (arglistvars (byte-compile-arglist-vars arglist))
@@ -4672,6 +4737,7 @@ byte-compile-defvar
              (byte-compile-warning-enabled-p 'lexical (nth 1 form)))
     (byte-compile-warn "global/dynamic var `%s' lacks a prefix"
                        (nth 1 form)))
+  (byte-compile-docstring-length-warn form)
   (let ((fun (nth 0 form))
 	(var (nth 1 form))
 	(value (nth 2 form))
@@ -4746,6 +4812,7 @@ byte-compile-file-form-defalias
       ;; - `arg' is the expression to which it is defined.
       ;; - `rest' is the rest of the arguments.
       (`(,_ ',name ,arg . ,rest)
+       (byte-compile-docstring-length-warn form)
        (pcase-let*
            ;; `macro' is non-nil if it defines a macro.
            ;; `fun' is the function part of `arg' (defaults to `arg').
diff --git a/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-autoload.el b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-autoload.el
new file mode 100644
index 0000000000..96deb1bbb0
--- /dev/null
+++ b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-autoload.el
@@ -0,0 +1,3 @@
+;;; -*- lexical-binding: t -*-
+(autoload 'foox "foo"
+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
diff --git a/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-custom-declare-variable.el b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-custom-declare-variable.el
new file mode 100644
index 0000000000..2a4700bfda
--- /dev/null
+++ b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-custom-declare-variable.el
@@ -0,0 +1,4 @@
+;;; -*- lexical-binding: t -*-
+(custom-declare-variable
+ 'foo t
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
diff --git a/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defalias.el b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defalias.el
new file mode 100644
index 0000000000..a4235d22bd
--- /dev/null
+++ b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defalias.el
@@ -0,0 +1,3 @@
+;;; -*- lexical-binding: t -*-
+(defalias 'foo #'ignore
+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
diff --git a/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defconst.el b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defconst.el
new file mode 100644
index 0000000000..946f01989a
--- /dev/null
+++ b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defconst.el
@@ -0,0 +1,3 @@
+;;; -*- lexical-binding: t -*-
+(defconst foo-bar nil
+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
diff --git a/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-abbrev-table.el b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-abbrev-table.el
new file mode 100644
index 0000000000..3da9ccd48c
--- /dev/null
+++ b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-abbrev-table.el
@@ -0,0 +1,3 @@
+;;; -*- lexical-binding: t -*-
+(define-abbrev-table 'foo ()
+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
diff --git a/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-obsolete-function-alias.el b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-obsolete-function-alias.el
new file mode 100644
index 0000000000..fea841b12e
--- /dev/null
+++ b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-obsolete-function-alias.el
@@ -0,0 +1,3 @@
+;;; -*- lexical-binding: t -*-
+(define-obsolete-function-alias 'foo #'ignore "99.1"
+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
diff --git a/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-obsolete-variable-alias.el b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-obsolete-variable-alias.el
new file mode 100644
index 0000000000..2d5f201cb6
--- /dev/null
+++ b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-define-obsolete-variable-alias.el
@@ -0,0 +1,3 @@
+;;; -*- lexical-binding: t -*-
+(define-obsolete-variable-alias 'foo 'ignore "99.1"
+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
diff --git a/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defun.el b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defun.el
new file mode 100644
index 0000000000..94b0e80c97
--- /dev/null
+++ b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defun.el
@@ -0,0 +1,3 @@
+;;; -*- lexical-binding: t -*-
+(defun foo ()
+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
diff --git a/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defvar.el b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defvar.el
new file mode 100644
index 0000000000..4bf8c17c3f
--- /dev/null
+++ b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defvar.el
@@ -0,0 +1,3 @@
+;;; -*- lexical-binding: t -*-
+(defvar foo-bar nil
+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
diff --git a/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defvaralias.el b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defvaralias.el
new file mode 100644
index 0000000000..52fdc17f5b
--- /dev/null
+++ b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-defvaralias.el
@@ -0,0 +1,3 @@
+;;; -*- lexical-binding: t -*-
+(defvaralias 'foo-bar #'ignore
+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
diff --git a/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-ignore-fill-column.el b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-ignore-fill-column.el
new file mode 100644
index 0000000000..1ff554f370
--- /dev/null
+++ b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-ignore-fill-column.el
@@ -0,0 +1,7 @@
+;;; -*- lexical-binding: t -*-
+(defvar foo-bar nil
+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
+
+;; Local Variables:
+;; fill-column: 100
+;; End:
diff --git a/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-ignore.el b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-ignore.el
new file mode 100644
index 0000000000..c80ddd180d
--- /dev/null
+++ b/test/lisp/emacs-lisp/bytecomp-resources/warn-long-docstring-ignore.el
@@ -0,0 +1,7 @@
+;;; -*- lexical-binding: t -*-
+(defvar foo-bar nil
+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
+
+;; Local Variables:
+;; byte-compile-docstring-max-column: 100
+;; End:
diff --git a/test/lisp/emacs-lisp/bytecomp-tests.el b/test/lisp/emacs-lisp/bytecomp-tests.el
index 680aa514a2..11db749539 100644
--- a/test/lisp/emacs-lisp/bytecomp-tests.el
+++ b/test/lisp/emacs-lisp/bytecomp-tests.el
@@ -26,6 +26,7 @@
 ;;; Commentary:
 
 (require 'ert)
+(require 'ert-x)
 (require 'cl-lib)
 (require 'subr-x)
 (require 'bytecomp)
@@ -536,6 +537,74 @@ bytecomp-warn-variable-lacks-prefix
   (bytecomp--with-warning-test "foo.*lacks a prefix"
     '(defvar foo nil)))
 
+(defvar bytecomp-tests--docstring (make-string 100 ?x))
+
+(ert-deftest bytecomp-warn-long-docstring/defconst ()
+  (bytecomp--with-warning-test "defconst.*foo.*wider than.*characters"
+    `(defconst foo t ,bytecomp-tests--docstring)))
+
+(ert-deftest bytecomp-warn-long-docstring/defvar ()
+  (bytecomp--with-warning-test "defvar.*foo.*wider than.*characters"
+    `(defvar foo t ,bytecomp-tests--docstring)))
+
+(defmacro bytecomp--define-warning-file-test (file re-warning &optional reverse)
+  `(ert-deftest ,(intern (format "bytecomp-warn/%s" file)) ()
+     :expected-result ,(if reverse :failed :passed)
+     (with-current-buffer (get-buffer-create "*Compile-Log*")
+       (let ((inhibit-read-only t)) (erase-buffer))
+       (byte-compile-file ,(ert-resource-file file))
+       (ert-info ((buffer-string) :prefix "buffer: ")
+         (should (re-search-forward ,re-warning))))))
+
+(bytecomp--define-warning-file-test
+ "warn-long-docstring-autoload.el"
+ "autoload.*foox.*wider than.*characters")
+
+(bytecomp--define-warning-file-test
+ "warn-long-docstring-custom-declare-variable.el"
+ "custom-declare-variable.*foo.*wider than.*characters")
+
+(bytecomp--define-warning-file-test
+ "warn-long-docstring-defalias.el"
+ "defalias.*foo.*wider than.*characters")
+
+(bytecomp--define-warning-file-test
+ "warn-long-docstring-defconst.el"
+ "defconst.*foo.*wider than.*characters")
+
+(bytecomp--define-warning-file-test
+ "warn-long-docstring-define-abbrev-table.el"
+ "define-abbrev.*foo.*wider than.*characters")
+
+(bytecomp--define-warning-file-test
+ "warn-long-docstring-define-obsolete-function-alias.el"
+ "defalias.*foo.*wider than.*characters")
+
+(bytecomp--define-warning-file-test
+ "warn-long-docstring-define-obsolete-variable-alias.el"
+ "defvaralias.*foo.*wider than.*characters")
+
+;; TODO: We don't yet issue warnings for defuns.
+(bytecomp--define-warning-file-test
+ "warn-long-docstring-defun.el"
+ "wider than.*characters" 'reverse)
+
+(bytecomp--define-warning-file-test
+ "warn-long-docstring-defvar.el"
+ "defvar.*foo.*wider than.*characters")
+
+(bytecomp--define-warning-file-test
+ "warn-long-docstring-defvaralias.el"
+ "defvaralias.*foo.*wider than.*characters")
+
+(bytecomp--define-warning-file-test
+ "warn-long-docstring-ignore-fill-column.el"
+ "defvar.*foo.*wider than.*characters" 'reverse)
+
+(bytecomp--define-warning-file-test
+ "warn-long-docstring-ignore.el"
+ "defvar.*foo.*wider than.*characters" 'reverse)
+
 (ert-deftest test-eager-load-macro-expansion ()
   (test-byte-comp-compile-and-load nil
     '(progn (defmacro abc (arg) 1) (defun def () (abc 2))))
-- 
2.29.2