branch: elpa/flymake-collection
commit 95c3a3ff3bf1fb0486794bbe5880d99eabb6eb06
Author: Mohsin Kaleem <mohk...@kisara.moe>
Commit: Mohsin Kaleem <mohk...@kisara.moe>

    (flymake-rest): Add macro for checker definition
---
 flymake-rest-define.el    | 229 ++++++++++++++++++++++++++++++++++++++++++++++
 flymake-rest-enumerate.el |  31 +++++++
 flymake-rest-rx.el        | 115 +++++++++++++++++++++++
 flymake-rest.el           |  65 +++++++++++++
 4 files changed, 440 insertions(+)

diff --git a/flymake-rest-define.el b/flymake-rest-define.el
new file mode 100644
index 0000000000..c5b184d217
--- /dev/null
+++ b/flymake-rest-define.el
@@ -0,0 +1,229 @@
+;;; flymake-rest-define.el --- A macro to simplify checker creation -*- 
lexical-binding: t -*-
+
+;;; Commentary:
+
+;; This file provides a macro, adapted heavily from 
[[https://github.com/karlotness/flymake-quickdef/blob/150c5839768a3d32f988f9dc08052978a68f2ad7/flymake-quickdef.el][flymake-quickdef]],
+;;
+;; A shallow fork of 
[[https://github.com/karlotness/flymake-quickdef][flymake-quickdef]] supporting 
more flycheck like features.
+;; TODO: Finish
+
+;; TODO: license
+
+;;; Code:
+
+(require 'flymake)
+
+;;;###autoload
+(defvar-local flymake-rest-define--procs nil)
+
+(defmacro flymake-rest-define (name &optional docstring &rest defs)
+  "Quickly define a backend for use with Flymake.
+This macro creates a new function NAME which is suitable for use with the
+variable `flymake-diagnostic-functions'.
+
+DEFS is a plist of values used to setup the backend. The only required fields
+in DEFS is :command and :error-parser.
+
+Available Variables
+
+fmqd-source, fmqd-temp-file, fmdq-temp-dir, fmqd-context. TODO: Document.
+
+Body Definitions
+
+The overall execution of the produced function first makes use of (1)
+:write-type, (2) :source-inplace, (3) :pre-let, and (3) :pre-check. Next
+a process is created using (4) :command. Once the process is finished
+:error-parser is called (until it returns nil) to get the next diagnostic
+which is then provided to flymake. (5) :title if provided is used to
+suffix the messages for each diagnostic.
+
+:write-type specifies how the process for flymake should recieve the input.
+It should be one of 'pipe or 'file (defaulting to 'pipe). When set to file
+a temporary file will be created copying the contents of the current-buffer.
+The variable fmqd-temp-file and fmqd-temp-dir will be bound in the body
+of the rest of the keywords that provide access to the temp-file. When set
+to pipe after the process has been started all of the current buffers input
+will be passed to the process through standard-input.
+
+:source-inplace is a boolean that sets fmqd-temp-dir to the current working
+directory. By default this is nil and the temp-file used for :write-type 'file
+will be set to a folder in the systems temporary directory.
+
+:pre-let is a `let*' form that is assigned after any backend-agnostic let
+forms have been setup.
+
+:pre-check is a lisp form that will be executed immeadiately before any pending
+checker processes are killed and a new process is begun. It can check 
conditions
+to ensure launching the checker program is possible. If something is wrong it
+should signal an error.
+
+:command is a lip form which evaluates to a list of strings that will be used 
to
+start the checker process. It should be suitable for use as the :command 
argument
+to the `make-process' function.
+
+:error-parser is a lisp-form that should, each time it is evaluated, return the
+next diagnostic from the checker output. The result should be a value that can
+be passed to the `flymake-make-diagnostic' function. Once there're no more
+diagnostics to parse this form should evaluate to nil."
+  (declare (indent defun) (doc-string 2))
+  (unless lexical-binding
+    (error "Need lexical-binding for flymake-rest-define (%s)" name))
+  (or (stringp docstring)
+      (setq defs (cons docstring defs)
+            docstring nil))
+  (dolist (elem '(:command :error-parser))
+    (unless (plist-get defs elem)
+      (error "Missing flymake backend definition `%s'" elem)))
+  (let* ((write-type (or (eval (plist-get defs :write-type)) 'pipe))
+         (source-inplace (plist-get defs :source-inplace))
+         (temp-dir-symb (intern "fmqd-temp-dir"))
+         (temp-file-symb (intern "fmqd-temp-file"))
+         (err-symb (intern "fmqd-err"))
+         (diags-symb (intern "diags"))
+         (proc-symb (intern "proc"))
+         (source-symb (intern "fmqd-source"))
+         (current-diags-symb (intern "diag"))
+         (cleanup-form (when (and (eq write-type 'file)
+                                  (not source-inplace))
+                         `((delete-directory ,temp-dir-symb t))))
+         (not-obsolete-form `((eq ,proc-symb (plist-get (buffer-local-value 
'flymake-rest-define--procs ,source-symb) ',name)))))
+    ;; Sanitise parsed inputs from `defs'.
+    (unless (memq write-type '(file pipe nil))
+      (error "Invalid `:write-type' value `%s'" write-type))
+
+    `(defun ,name (report-fn &rest _args)
+       ,docstring
+       (let* ((,source-symb (current-buffer))
+              (fmqd-context nil)
+              ,@(when (eq write-type 'file)
+                  `((,temp-dir-symb
+                     ,@(let ((forms (append (when source-inplace
+                                              `((when-let ((file 
(buffer-file-name)))
+                                                  (file-name-directory file))
+                                                default-directory))
+                                            '((make-temp-file "flymake-" t)))))
+                         (if (> (length forms) 1)
+                             `((or ,@forms))
+                           forms)))
+                    (,temp-file-symb
+                     (concat
+                      (file-name-as-directory ,temp-dir-symb)
+                      (concat ".flymake_"
+                              (file-name-nondirectory (or (buffer-file-name)
+                                                          (buffer-name))))))))
+              ,@(plist-get defs :pre-let))
+         ;; With vars defined, do :pre-check.
+         ,@(when-let ((pre-check (plist-get defs :pre-check)))
+             `((condition-case ,err-symb
+                   (progn ,pre-check)
+                 (error ,@cleanup-form
+                        (signal (car ,err-symb) (cdr ,err-symb))))))
+         ;; Kill any running (obsolete) processes for current checker and 
buffer.
+         (let ((,proc-symb (plist-get flymake-rest-define--procs ',name)))
+           (when (process-live-p ,proc-symb)
+             (kill-process ,proc-symb)
+             (flymake-log :debug "Killing earlier checker process %s" 
,proc-symb)))
+
+         ;; Kick-start checker process.
+         (save-restriction
+           (widen)
+           ;; Write the current file out before starting checker.
+           ,@(when (eq write-type 'file)
+               `((write-region nil nil ,temp-file-symb nil 'silent)))
+           (let (proc)
+             (setq proc
+                   (make-process
+                    :name ,(concat (symbol-name name) "-flymake")
+                    :noquery t
+                    :connection-type 'pipe
+                    :buffer (generate-new-buffer ,(concat " *" (symbol-name 
name) "-flymake*"))
+                    :command
+                    (let ((cmd ,(plist-get defs :command)))
+                      (prog1 cmd
+                        (flymake-log :debug "Checker command is %s" cmd)))
+                    :sentinel
+                    (lambda (,proc-symb _event)
+                      (unless (process-live-p ,proc-symb)
+                        (unwind-protect
+                            (if ,@not-obsolete-form
+                                (with-current-buffer ,source-symb
+                                  ;; First read diagnostics from process 
buffer referencing the source buffer.
+                                  (let ((,diags-symb nil) ,current-diags-symb)
+                                    ;; Widen the source buffer to ensure 
`flymake-diag-region' is correct.
+                                    (save-restriction
+                                      (widen)
+                                      (with-current-buffer (process-buffer 
,proc-symb)
+                                        (goto-char (point-min))
+                                        (save-match-data
+                                          (while (setq ,current-diags-symb 
,(plist-get defs :error-parser))
+                                            (let* ((diag-beg (nth 1 
,current-diags-symb))
+                                                   (diag-end (nth 2 
,current-diags-symb))
+                                                   (diag-type (nth 3 
,current-diags-symb)))
+                                              (if (and (integer-or-marker-p 
diag-beg)
+                                                       (integer-or-marker-p 
diag-end))
+                                                  ;; Skip any diagnostics with 
a type of nil
+                                                  ;; This makes it easier to 
filter some out.
+                                                  (when diag-type
+                                                    ;; Include the checker 
name/title in the message.
+                                                    ,@(when (plist-get defs 
:title)
+                                                        `((setf (nth 4 
,current-diags-symb)
+                                                                (concat (nth 4 
,current-diags-symb)
+                                                                        
,(concat
+                                                                          " ("
+                                                                          
(propertize (plist-get defs :title)
+                                                                               
       'face 'flymake-rest-checker)
+                                                                          
")")))))
+
+                                                    (push (apply 
#'flymake-make-diagnostic ,current-diags-symb)
+                                                          ,diags-symb))
+                                                (with-current-buffer 
,source-symb
+                                                  (flymake-log :error "Got 
invalid buffer position %s or %s in %s"
+                                                               diag-beg 
diag-end ,proc-symb))))))))
+                                    ;; Pass reports back to the 
callback-function when still not-obsolete.
+                                    (if ,@not-obsolete-form
+                                        (progn
+                                          (let ((status (process-exit-status 
,proc-symb)))
+                                            (when (and (eq (length 
,diags-symb) 0)
+                                                       (not (eq status 0)))
+                                              (flymake-log :warning
+                                                           "Checker gave no 
diagnostics but had a non-zero exit status %d\nStderr:" status
+                                                           
(with-current-buffer (process-buffer ,proc-symb)
+                                                             (format "%s" 
(buffer-substring-no-properties
+                                                                           
(point-min) (point-max)))))))
+                                          (funcall report-fn (nreverse 
,diags-symb)))
+                                      ;; In case the check was cancelled after 
processing began but before it finished.
+                                      (flymake-log :warning "Canceling 
obsolete check %s" ,proc-symb)))
+                                  (flymake-log :warning "Canceling obsolete 
check %s" ,proc-symb)))
+                          ;; Finished linting, cleanup any temp-files and then 
kill proc buffer.
+                          ,@cleanup-form
+                          (kill-buffer (process-buffer ,proc-symb)))))))
+             ;; Push the new-process to the process to the process alist.
+             (setq flymake-rest-define--procs
+                   (plist-put flymake-rest-define--procs ',name ,proc-symb))
+             ;; If piping, send data to the process.
+             ,@(when (eq write-type 'pipe)
+                 `((process-send-region proc (point-min) (point-max))
+                   (process-send-eof proc)))
+             ,proc-symb))))))
+
+(defun flymake-rest-parse-json (output)
+  "Helper for `flymake-rest-define' to parse JSON output OUTPUT.
+
+Adapted from `flycheck-parse-json'. This reads a bunch of JSON-Lines
+like output from OUTPUT into a list and then returns it."
+  (let (objects
+        (json-array-type 'list)
+        (json-false nil))
+    (with-temp-buffer
+      (insert output)
+      (goto-char (point-min))
+      (while (not (eobp))
+        (when (memq (char-after) '(?\{ ?\[))
+          (push (json-parse-buffer
+                 :object-type 'alist :array-type 'list
+                 :null-object nil :false-object nil)
+                objects))
+        (forward-line)))
+    objects))
+
+(provide 'flymake-rest-define)
diff --git a/flymake-rest-enumerate.el b/flymake-rest-enumerate.el
new file mode 100644
index 0000000000..964fac8447
--- /dev/null
+++ b/flymake-rest-enumerate.el
@@ -0,0 +1,31 @@
+;;; flymakflymake-backend-parse-enumerate!to simplify checker creation -*- 
lexical-binding: t -*-
+
+;;; Commentary:
+;; TODO
+;; TODO: license
+
+;;; Code:
+
+(defmacro flymake-rest-parse-enumerate (gen &rest body)
+  "Error parser for `flymake-backend-define' which parses all of
+the diagnostics at once using GEN and then preparing them one-at-a-time
+with BODY.
+
+The value of the current entry from GEN in BODY will be set to the variable
+`it'. BODY should evaluate to a form that can be passed to
+`flymake-make-diagnostic'."
+  (declare (indent 1))
+  (let ((context-var (intern "fmqd-context")))
+    `(progn
+       (unless (alist-get 'enumerated ,context-var)
+         (push (cons 'entries ,gen) ,context-var)
+         (push '(enumerated t) ,context-var))
+       (let (it res)
+         ;; While we haven't found a new diagnostic to return, BUT there're
+         ;; still diagnostics that can be found in the parsed checker output.
+         (while (and (not res)
+                     (setq it (pop (alist-get 'entries ,context-var))))
+           (setq res ,@body))
+         res))))
+
+(provide 'flymake-rest-enumerate)
diff --git a/flymake-rest-rx.el b/flymake-rest-rx.el
new file mode 100644
index 0000000000..7f14291432
--- /dev/null
+++ b/flymake-rest-rx.el
@@ -0,0 +1,115 @@
+;;; flymake-rest-rest.el --- A macro to simplify checker creation -*- 
lexical-binding: t -*-
+
+;;; Commentary:
+;; TODO
+;; TODO: license
+
+;;; Code:
+
+(defconst flymake-rest-rx-constituents
+  `((file-name ,(lambda (body)
+                  (rx-to-string
+                   `(group-n 1 ,@(or (cdr body)
+                                     '((minimal-match
+                                        (one-or-more not-newline)))))
+                   t))
+               0 nil) ;; group 1
+    (line . ,(rx (group-n 2 (one-or-more digit))))
+    (column . ,(rx (group-n 3 (one-or-more digit))))
+    (message ,(lambda (body)
+                (rx-to-string
+                 `(group-n 4 ,@(or (cdr body)
+                                   '((minimal-match
+                                      (one-or-more not-newline)))))
+                 t))
+             0 nil)
+    (id ,(lambda (body)
+           (rx-to-string `(group-n 5 ,@(cdr body)) t))
+        0 nil)
+    (end-line . ,(rx (group-n 6 (one-or-more digit))))
+    (end-column . ,(rx (group-n 7 (one-or-more digit))))))
+
+(defmacro flymake-rest-parse-rx (regexps)
+  "Helper for `flymake-rest-define' which tries to emulate flychecks
+:error-parsers.
+
+This macro generates a parser that for each line of output from the
+checker process, runs one or more REGEXPs on it and then converts the
+result to a valid flymake diagnostic (that can be passed back to
+`flymake-make-diagnostic').
+
+TODO: describe arguments.
+"
+  (unless (> (length regexps) 0)
+    (error "Must supply at least one regexp for error, warning or note"))
+  ;; To avoid having to rematch each diagnostic more than once we append
+  ;; a special extra capture group (greater than all the ones above) that
+  ;; simply matches the empty string. Then we can index the groups after
+  ;; 7 and use that to determine the severity of the symbol.
+  (setq regexps
+        (cl-loop for (severity . regex) in regexps
+                 with count = 7
+                 do (setq count (1+ count))
+                 collect (cons `(seq ,@regex (group-n ,count ""))
+                               (intern (concat ":" (symbol-name severity))))))
+
+  (let ((combined-regex
+         (let ((rx-constituents (append flymake-rest-rx-constituents
+                                        rx-constituents nil)))
+           (rx-to-string `(or ,@(mapcar #'car regexps))
+                         'no-group)))
+        (severity-seq (mapcar #'cdr regexps)))
+    ;; Because if this evaluates to nil `flymake-rest-define' thinks there
+    ;; are no-more diagnostics to be parsed, we wrap it in a loop that exits
+    ;; the moment we find a match, but otherwise keeps moving through 
diagnostics
+    ;; until there actually aren't any more to match.
+    `(let (res
+           file-name line column message id end-line end-column severity-ix)
+       (while (and (not res)
+                   (search-forward-regexp ,combined-regex nil t))
+         (setq
+          res
+          (save-match-data
+            (save-excursion
+              (setq file-name (match-string 1)
+                    line (match-string 2)
+                    column (match-string 3)
+                    message (match-string 4)
+                    id (match-string 5)
+                    end-line (match-string 6)
+                    end-column (match-string 7)
+                    severity-ix (seq-find #'match-string
+                                          (number-sequence 0 ,(- (length 
regexps) 1))))
+              (cond
+               ;; Log an error when any of the required fields are missing.
+               ,@(cl-loop for it in '(severity-ix line message)
+                          collect
+                          `((not ,it)
+                            (flymake-log :error
+                                         ,(format
+                                           "Matched diagnostic didn't capture 
a %s group"
+                                           (symbol-name it)))
+                            nil))
+               (t
+                (let ((loc (flymake-diag-region fmqd-source
+                                                (string-to-number line)
+                                                (when column
+                                                  (string-to-number column))))
+                      (loc-end (when end-line
+                                 (flymake-diag-region fmqd-source
+                                                      (string-to-number 
end-line)
+                                                      (when end-column
+                                                        (string-to-number 
end-column))))))
+                  (when loc-end
+                    (setcdr loc (cdr loc-end)))
+                  (list fmqd-source
+                        (car loc)
+                        (cdr loc)
+                        (nth severity-ix (quote ,severity-seq))
+                        (concat
+                         (when id
+                           (concat (propertize id 'face 'flymake-diag-id!) " 
"))
+                         message)))))))))
+       res)))
+
+(provide 'flymake-rest-rx)
diff --git a/flymake-rest.el b/flymake-rest.el
new file mode 100644
index 0000000000..588ba88b8c
--- /dev/null
+++ b/flymake-rest.el
@@ -0,0 +1,65 @@
+;;; flymake-rest.el --- Core features for flymake-rest -*- lexical-binding: t 
-*-
+
+;; Copyright (C) 2021 Mohsin Kaleem
+
+;; Author: Mohsin Kaleem <mohk...@kisara.moe>
+;; Created: 15 June 2021
+;; Homepage: https://github.com/mohkale/flymake-rest
+;; Keywords: language tools
+;; Package-Requires: ((emacs "26.1") (flymake "1"))
+;; SPDX-License-Identifier: MIT
+;; Version: 3.1
+
+;; Copyright (c) 2021 Mohsin Kaleem
+;;
+;; Permission is hereby granted, free of charge, to any person obtaining a copy
+;; of this software and associated documentation files (the "Software"), to 
deal
+;; in the Software without restriction, including without limitation the rights
+;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+;; copies of the Software, and to permit persons to whom the Software is
+;; furnished to do so, subject to the following conditions:
+;;
+;; The above copyright notice and this permission notice shall be included in 
all
+;; copies or substantial portions of the Software.
+;;
+;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
FROM,
+;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
THE
+;; SOFTWARE.
+
+;;; Commentary:
+
+;; flymake-rest is a helper for migrating from flycheck to flymake.
+;;
+;; This includes the definition of several diagnostic functions, hooks
+;; to specify the precedence and preferred order of them and the means
+;; to easily configure flymake linting.
+;;
+;; For more see [[file:README.org][README.org]].
+
+;; Please see https://github.com/mohkale/flymake-rest for more
+;; information.
+
+;;; Code:
+
+(defgroup flymake-rest nil
+  "Flymake flycheck compatibility"
+  :prefix "flymake-rest")
+
+(defgroup consult-faces nil
+  "Faces used by flymake-rest."
+  :group 'flymake-rest
+  :group 'faces)
+
+(defface flymake-rest-checker
+  '((t (:inherit (dired-directory bold))))
+  "Title of a checker as shown in the diagnostic message.")
+
+(defface flymake-rest-diag-id
+  '((t (:inherit font-lock-type-face)))
+  "Id of a diagnostic.")
+
+(provide 'flymake-rest)

Reply via email to