~manuel-uberti/flymake-proselint

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
7 2

[PATCH 1/4] Generate and parse JSON output

Details
Message ID
<20220911122128.44037-1-philipk@posteo.net>
DKIM signature
pass
Download raw message
Patch: +83 -35
* flymake-proselint.el
(flymake-proselint-sentinel-1): Add new function for translating JSON
objects to Flymake diagnostics.
(flymake-proselint-sentinel): Extract sentinel into a separate function.
(flymake-proselint-backend): Use 'flymake-proselint-sentinel'.
---
 flymake-proselint.el | 118 ++++++++++++++++++++++++++++++-------------
 1 file changed, 83 insertions(+), 35 deletions(-)

diff --git a/flymake-proselint.el b/flymake-proselint.el
index 91d1fec92b..4908197720 100644
--- a/flymake-proselint.el
+++ b/flymake-proselint.el
@@ -30,54 +30,102 @@

;;; Code:

(eval-when-compile
  (require 'subr-x)
  (require 'pcase))
(require 'flymake)

(defun flymake-proselint-sentinel-1 (source data)
  "Handle a successfully parsed DATA from SOURCE.
DATA is a list of error diagnostics that are converted into
Flymake diagnostic objects."
  (let (diags)
    (dolist (err (plist-get data :errors))
      (push (flymake-make-diagnostic
             source
             (plist-get err :start)
             (plist-get err :end)
             (pcase (plist-get err :severity)
               ("warning"	:warning)
               ("suggestion"	:note)
               (_		:error))
             (with-temp-buffer		;create a message
               (insert (plist-get err :message))
               (let ((replacements (plist-get err :replacements)))
                 (cond
                  ((or (eq replacements :null) (null replacements))
                   ;; There are no replacements.
                   )
                  ((stringp replacements)
                   (insert " (Replacement: " replacements ")"))
                  ((listp replacements)
                   (insert " (Replacements: "
                           (mapconcat
                            (lambda (r)
                              (plist-get r :unique))
                            replacements ", ")
                           ")"))))
               (buffer-string)))
            diags))
    diags))

(defvar-local flymake-proselint--flymake-proc nil)

(defun flymake-proselint-sentinel (proc _event)
  "Sentinel on PROC for handling Proselint response.
A successfully parsed message is passed onto the function
`flymake-proselint-sentinel-1' for further handling."
  (pcase (process-status proc)
    ('exit
     (let ((report-fn (process-get proc 'report-fn))
           (source (process-get proc 'source)))
       (unwind-protect
           (with-current-buffer (process-buffer proc)
             (goto-char (point-min))
             (cond
              ((with-current-buffer source
                 (not (eq proc flymake-proselint--flymake-proc)))
               (flymake-log :warning "Canceling obsolete check %s" proc))
              ((= (point-max) (point-min))
               (flymake-log :debug "Empty response"))
              ((condition-case err
                   (let ((response (json-parse-buffer :object-type 'plist
                                                      :array-type 'list)))
                     (if (string= (plist-get response :status) "success")
                         (thread-last
                           (plist-get response :data)
                           (flymake-proselint-sentinel-1 source)
                           (funcall report-fn))
                       (flymake-log :error "Check failed")))
                 (json-parse-error
                  (flymake-log :error "Invalid response: %S" err))))))
         (with-current-buffer source
           (setq flymake-proselint--flymake-proc nil))
         (kill-buffer (process-buffer proc)))))
    ('signal (kill-buffer (process-buffer proc)))))

(defun flymake-proselint-backend (report-fn &rest _args)
  "Flymake backend for Proselint.
REPORT-FN is the flymake reporter function.  See the Info
node (flymake) Backend functions for more details."
  (unless (executable-find "proselint")
    (user-error "Executable proselint not found on PATH"))

  (when (process-live-p flymake-proselint--flymake-proc)
    (kill-process flymake-proselint--flymake-proc))

  (let ((source (current-buffer)))
  (let ((proc (make-process
               :name "proselint-flymake" :noquery t :connection-type 'pipe
               :buffer (generate-new-buffer " *proselint-flymake*")
               :command '("proselint" "--json" "-")
               :sentinel #'flymake-proselint-sentinel)))
    (process-put proc 'source (current-buffer))
    (process-put proc 'report-fn report-fn)
    (setq flymake-proselint--flymake-proc proc)
    (save-restriction
      (widen)
      (setq
       flymake-proselint--flymake-proc
       (make-process
        :name "proselint-flymake" :noquery t :connection-type 'pipe
        :buffer (generate-new-buffer " *proselint-flymake*")
        :command '("proselint" "-")
        :sentinel
        (lambda (proc _event)
          (when (memq (process-status proc) '(exit signal))
            (unwind-protect
                (if (with-current-buffer source (eq proc flymake-proselint--flymake-proc))
                    (with-current-buffer (process-buffer proc)
                      (goto-char (point-min))
                      (cl-loop
                       while (search-forward-regexp
                              "^.+:\\([[:digit:]]+\\):\\([[:digit:]]+\\): \\(.+\\)$"
                              nil t)
                       for msg = (match-string 3)
                       for (beg . end) = (flymake-diag-region
                                          source
                                          (string-to-number (match-string 1))
                                          (string-to-number (match-string 2)))
                       collect (flymake-make-diagnostic source
                                                        beg
                                                        end
                                                        :warning
                                                        msg)
                       into diags
                       finally (funcall report-fn diags)))
                  (flymake-log :warning "Canceling obsolete check %s"
                               proc))
              (kill-buffer (process-buffer proc)))))))
      (process-send-region flymake-proselint--flymake-proc (point-min) (point-max))
      (process-send-eof flymake-proselint--flymake-proc))))
      (process-send-region proc (point-min) (point-max))
      (process-send-eof proc))))

;;;###autoload
(defun flymake-proselint-setup ()
-- 
2.37.3

[PATCH 3/4] Allow for file local Proselint configurations

Details
Message ID
<20220911122128.44037-3-philipk@posteo.net>
In-Reply-To
<20220911122128.44037-1-philipk@posteo.net> (view parent)
DKIM signature
pass
Download raw message
Patch: +196 -1
* flymake-proselint.el:
(flymake-proselint-max-errors): Add new option.
(flymake-proselint-enable): Add new option.
(flymake-proselint-disable): Add new option.
(flymake-proselint-config-directory): Add new option.
(flymake-proselint-generate-configuration): Add new function.
(flymake-proselint-backend): Use 'flymake-proselint-generate-configuration'.
---
 flymake-proselint.el | 197 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 196 insertions(+), 1 deletion(-)

diff --git a/flymake-proselint.el b/flymake-proselint.el
index c8e7d83608..2b2b2c965d 100644
--- a/flymake-proselint.el
+++ b/flymake-proselint.el
@@ -34,6 +34,7 @@
  (require 'subr-x)
  (require 'pcase))
(require 'flymake)
(require 'xdg)

(defgroup flymake-proselint ()
  "Flymake backend for proselint."
@@ -50,6 +51,197 @@ The following %-sequences are replaced:
  %c - the error code"
  :type 'string)

(defconst flymake-proselint--options
  ;; Regenerate using:
  ;;
  ;; (with-temp-buffer
  ;;   (call-process "proselint" nil t nil "--dump-config")
  ;;   (goto-char (point-min))
  ;;   (let ((config (json-parse-buffer :object-type 'alist)))
  ;;     (mapcar #'car (alist-get 'checks config))))
  '(airlinese.misc
    annotations.misc
    archaism.misc
    cliches.hell
    cliches.misc
    consistency.spacing
    consistency.spelling
    corporate_speak.misc
    cursing.filth
    cursing.nfl
    cursing.nword
    dates_times.am_pm
    dates_times.dates
    hedging.misc
    hyperbole.misc
    jargon.misc
    lexical_illusions.misc
    lgbtq.offensive_terms
    lgbtq.terms
    links.broken
    malapropisms.misc
    misc.apologizing
    misc.back_formations
    misc.bureaucratese
    misc.but
    misc.capitalization
    misc.chatspeak
    misc.commercialese
    misc.composition
    misc.currency
    misc.debased
    misc.false_plurals
    misc.illogic
    misc.inferior_superior
    misc.institution_name
    misc.latin
    misc.many_a
    misc.metaconcepts
    misc.metadiscourse
    misc.narcissism
    misc.not_guilty
    misc.phrasal_adjectives
    misc.preferred_forms
    misc.pretension
    misc.professions
    misc.punctuation
    misc.scare_quotes
    misc.suddenly
    misc.tense_present
    misc.waxed
    misc.whence
    mixed_metaphors.misc
    mondegreens.misc
    needless_variants.misc
    nonwords.misc
    oxymorons.misc
    psychology.misc
    redundancy.misc
    redundancy.ras_syndrome
    security.credit_card
    security.password
    sexism.misc
    skunked_terms.misc
    spelling.able_atable
    spelling.able_ible
    spelling.athletes
    spelling.em_im_en_in
    spelling.er_or
    spelling.in_un
    spelling.misc
    terms.animal_adjectives
    terms.denizen_labels
    terms.eponymous_adjectives
    terms.venery
    typography.diacritical_marks
    typography.exclamation
    typography.symbols
    uncomparables.misc
    weasel_words.misc
    weasel_words.very)
  "List of Proselint options.")

(defconst flymake-proselint--custom-type
  `(set :greedy t
	,@(mapcar
	   (lambda (opt)
	     ;; TODO: Add a :tag
	     `(const ,opt))
	   flymake-proselint--options))
  "Custom option type for proselint configurations.")

(defun flymake-proselint-safe-option-p (val)
  "Check if VAL is a safe (and valid) local value."
  (and (listp val)
       (catch 'fail
	 (dolist (elem val)
	   (unless (memq elem flymake-proselint--options)
	     (throw 'fail nil)))
	 t)))

(defcustom flymake-proselint-max-errors 1000
  "After how many errors should Proselint give up?
For this option to work, the user option
`flymake-proselint-config-directory' must be non-nil."
  :safe #'natnump
  :type 'natnum)

(defcustom flymake-proselint-enable '()
  "List of Proselint options to enable.
See `flymake-proselint--options' for a list of possible options.
For this option to work, the user option
`flymake-proselint-config-directory' must be non-nil."
  :safe #'flymake-proselint-safe-option-p
  :type flymake-proselint--custom-type)

(defcustom flymake-proselint-disable '()
  "List of Proselint options to disable.
See `flymake-proselint--options' for a list of possible options.
For this option to work, the user option
`flymake-proselint-config-directory' must be non-nil."
  :safe #'flymake-proselint-safe-option-p
  :type flymake-proselint--custom-type)

(defcustom flymake-proselint-config-directory
  (when-let* ((parent (or (xdg-cache-home) (temporary-file-directory)))
              (dir (expand-file-name "flymake-proselint" parent)))
    (condition-case nil
        (progn
          (unless (file-exists-p dir)
            (make-directory dir))
          dir)
      ;; In case there was an issue while creating the directory, this
      ;; option will just be disabled.
      (permission-denied nil)))
  "Directory to use for temporary configuration files.
If nil, `flymake-proselint' will not generate temporary
configuration files.  This means that the user options
`flymake-proselint-max-errors', `flymake-proselint-enable' and
`flymake-proselint-disable' will have no effect.."
  :type 'boolean)

(defun flymake-proselint-generate-configuration ()
  "Generate a configuration and return path.
This function uses the buffer local values of
`flymake-proselint-max-errors', `flymake-proselint-enable' and
`flymake-proselint-disable'.  If no configuration was
generated (either because it wasn't necessary or because disabled
by `flymake-proselint-inhibit-configuration'), the return value
will be nil."
  (unless (and flymake-proselint-config-directory
               (= flymake-proselint-max-errors 1000) ;default
               (null flymake-proselint-enable)
               (null flymake-proselint-disable))
    (let ((output (expand-file-name
                   ;; As done by `make-backup-file-name-1'
                   (secure-hash
                    'sha256
                    (prin1-to-string
                     (list (file-truename (buffer-file-name))
                           flymake-proselint-max-errors
                           flymake-proselint-enable
                           flymake-proselint-disable)))
                   flymake-proselint-config-directory)))
      (unless (file-exists-p output)
        (let* ((config (with-temp-buffer
                         (call-process "proselint" nil t nil "--dump-config")
                         (goto-char (point-min))
                         (json-parse-buffer :object-type 'alist)))
               (checks (alist-get 'checks config)))
          (setf (alist-get 'checks config)
                (mapcar
                 (lambda (opt)
                   (cons opt
                         (cond
                          ((memq opt flymake-proselint-disable) :false)
                          ((memq opt flymake-proselint-enable)  t)
                          ((alist-get opt checks)))))
                 flymake-proselint--options)
                (alist-get 'max_errors config)
                flymake-proselint-max-errors)
          (with-temp-file output (json-insert config))))
      output)))

(defun flymake-proselint-sentinel-1 (source data)
  "Handle a successfully parsed DATA from SOURCE.
DATA is a list of error diagnostics that are converted into
@@ -133,7 +325,10 @@ node (flymake) Backend functions for more details."
  (let ((proc (make-process
               :name "proselint-flymake" :noquery t :connection-type 'pipe
               :buffer (generate-new-buffer " *proselint-flymake*")
               :command '("proselint" "--json" "-")
               :command
               (if-let* ((conf (flymake-proselint-generate-configuration)))
                   (list "proselint" "--config" conf "--json" "-")
                 '("proselint" "--json" "-"))
               :sentinel #'flymake-proselint-sentinel)))
    (process-put proc 'source (current-buffer))
    (process-put proc 'report-fn report-fn)
-- 
2.37.3

[PATCH 2/4] Add user option 'flymake-proselint-message-format'

Details
Message ID
<20220911122128.44037-2-philipk@posteo.net>
In-Reply-To
<20220911122128.44037-1-philipk@posteo.net> (view parent)
DKIM signature
pass
Download raw message
Patch: +33 -17
* flymake-proselint.el (flymake-proselint): Add a group.
(flymake-proselint-message-format): Add a user option.
(flymake-proselint-sentinel-1): Use the user option.
---
 flymake-proselint.el | 50 +++++++++++++++++++++++++++++---------------
 1 file changed, 33 insertions(+), 17 deletions(-)

diff --git a/flymake-proselint.el b/flymake-proselint.el
index 4908197720..c8e7d83608 100644
--- a/flymake-proselint.el
+++ b/flymake-proselint.el
@@ -35,6 +35,21 @@
  (require 'pcase))
(require 'flymake)

(defgroup flymake-proselint ()
  "Flymake backend for proselint."
  :prefix "flymake-proselint-"
  :group 'flymake)

(defcustom flymake-proselint-message-format
  "%m%r"
  "A format string to generate diagnostic messages.
The following %-sequences are replaced:

  %m - the message text
  %r - replacement suggestions
  %c - the error code"
  :type 'string)

(defun flymake-proselint-sentinel-1 (source data)
  "Handle a successfully parsed DATA from SOURCE.
DATA is a list of error diagnostics that are converted into
@@ -49,23 +64,24 @@ Flymake diagnostic objects."
               ("warning"	:warning)
               ("suggestion"	:note)
               (_		:error))
             (with-temp-buffer		;create a message
               (insert (plist-get err :message))
               (let ((replacements (plist-get err :replacements)))
                 (cond
                  ((or (eq replacements :null) (null replacements))
                   ;; There are no replacements.
                   )
                  ((stringp replacements)
                   (insert " (Replacement: " replacements ")"))
                  ((listp replacements)
                   (insert " (Replacements: "
                           (mapconcat
                            (lambda (r)
                              (plist-get r :unique))
                            replacements ", ")
                           ")"))))
               (buffer-string)))
             (format-spec
              flymake-proselint-message-format
              `((?m . ,(plist-get err :message))
                (?c . ,(plist-get err :check))
                (?r . ,(let ((replacements (plist-get err :replacements)))
                         (cond
                          ((or (eq replacements :null) (null replacements))
                           ;; There are no replacements.
                           "")
                          ((stringp replacements)
                           (concat " (Replacement: " replacements ")"))
                          ((listp replacements)
                           (concat " (Replacements: "
                                   (mapconcat
                                    (lambda (r)
                                      (plist-get r :unique))
                                    replacements ", ")
                                   ")"))))))))
            diags))
    diags))

-- 
2.37.3

[PATCH 4/4] Add a user option for the Proselint executable name

Details
Message ID
<20220911122128.44037-4-philipk@posteo.net>
In-Reply-To
<20220911122128.44037-1-philipk@posteo.net> (view parent)
DKIM signature
pass
Download raw message
Patch: +9 -5
* flymake-proselint.el (flymake-proselint-executable): Add new option.
(flymake-proselint--options): Use new option.
(flymake-proselint-generate-configuration): Use new option.
(flymake-proselint-backend): Use new option.
---
 flymake-proselint.el | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/flymake-proselint.el b/flymake-proselint.el
index 2b2b2c965d..415c7c8195 100644
--- a/flymake-proselint.el
+++ b/flymake-proselint.el
@@ -41,6 +41,10 @@
  :prefix "flymake-proselint-"
  :group 'flymake)

(defcustom flymake-proselint-executable "proselint"
  "Name of the Proselint executable."
  :type 'string)

(defcustom flymake-proselint-message-format
  "%m%r"
  "A format string to generate diagnostic messages.
@@ -55,7 +59,7 @@ The following %-sequences are replaced:
  ;; Regenerate using:
  ;;
  ;; (with-temp-buffer
  ;;   (call-process "proselint" nil t nil "--dump-config")
  ;;   (call-process flymake-proselint-executable nil t nil "--dump-config")
  ;;   (goto-char (point-min))
  ;;   (let ((config (json-parse-buffer :object-type 'alist)))
  ;;     (mapcar #'car (alist-get 'checks config))))
@@ -224,7 +228,7 @@ will be nil."
                   flymake-proselint-config-directory)))
      (unless (file-exists-p output)
        (let* ((config (with-temp-buffer
                         (call-process "proselint" nil t nil "--dump-config")
                         (call-process flymake-proselint-executable nil t nil "--dump-config")
                         (goto-char (point-min))
                         (json-parse-buffer :object-type 'alist)))
               (checks (alist-get 'checks config)))
@@ -316,7 +320,7 @@ A successfully parsed message is passed onto the function
  "Flymake backend for Proselint.
REPORT-FN is the flymake reporter function.  See the Info
node (flymake) Backend functions for more details."
  (unless (executable-find "proselint")
  (unless (executable-find flymake-proselint-executable)
    (user-error "Executable proselint not found on PATH"))

  (when (process-live-p flymake-proselint--flymake-proc)
@@ -327,8 +331,8 @@ node (flymake) Backend functions for more details."
               :buffer (generate-new-buffer " *proselint-flymake*")
               :command
               (if-let* ((conf (flymake-proselint-generate-configuration)))
                   (list "proselint" "--config" conf "--json" "-")
                 '("proselint" "--json" "-"))
                   (list flymake-proselint-executable "--config" conf "--json" "-")
                 (list flymake-proselint-executable "--json" "-"))
               :sentinel #'flymake-proselint-sentinel)))
    (process-put proc 'source (current-buffer))
    (process-put proc 'report-fn report-fn)
-- 
2.37.3

Re: [PATCH 4/4] Add a user option for the Proselint executable name

Details
Message ID
<b3d4bbc6-776b-972c-d961-163ff9d950dd@inventati.org>
In-Reply-To
<20220911122128.44037-4-philipk@posteo.net> (view parent)
DKIM signature
pass
Download raw message
I applied all your patches. Thanks a lot for the contribution.

-- 
Manuel Uberti
https://manueluberti.eu
Details
Message ID
<877d25yni1.fsf@posteo.net>
In-Reply-To
<20220911122128.44037-1-philipk@posteo.net> (view parent)
DKIM signature
pass
Download raw message
Philip Kaludercic <philipk@posteo.net> writes:

> +                   (let ((response (json-parse-buffer :object-type 'plist
> +                                                      :array-type 'list)))

I forgot that `json-parse-buffer' was added in Emacs 27, so it will
either be necessary to raise the minimum Emacs version, or add Compat
(https://elpa.gnu.org/packages/compat.html) as a dependency.
Details
Message ID
<d4748acd-cc7a-6dfc-d6aa-8950dd38abdb@inventati.org>
In-Reply-To
<877d25yni1.fsf@posteo.net> (view parent)
DKIM signature
pass
Download raw message
On 15/09/22 11:51, Philip Kaludercic wrote:
> I forgot that `json-parse-buffer' was added in Emacs 27, so it will
> either be necessary to raise the minimum Emacs version, or add Compat
> (https://elpa.gnu.org/packages/compat.html) as a dependency.

No worries. I raised the minimum Emacs version.

-- 
Manuel Uberti
https://manueluberti.eu
Details
Message ID
<871qscvpid.fsf@posteo.net>
In-Reply-To
<d4748acd-cc7a-6dfc-d6aa-8950dd38abdb@inventati.org> (view parent)
DKIM signature
pass
Download raw message
Manuel Uberti <manuel.uberti@inventati.org> writes:

> On 15/09/22 11:51, Philip Kaludercic wrote:
>> I forgot that `json-parse-buffer' was added in Emacs 27, so it will
>> either be necessary to raise the minimum Emacs version, or add Compat
>> (https://elpa.gnu.org/packages/compat.html) as a dependency.
>
> No worries. I raised the minimum Emacs version.

Great, thank you!
Reply to thread Export thread (mbox)