summaryrefslogtreecommitdiff
path: root/tw/home/files/emacs-packages/flymake-guile.el
blob: edfbce82d7fecc1fe935047c411e920aa1913217 (about) (plain)
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
;;; flymake-guile.el --- Flymake checker using `guild compile'  -*- lexical-binding: t -*-
;;; Commentary:
;;; "guild compile" compiles Guile code to bytecode and can output a few basic
;;; warnings.  Let's use this as a linter!
;;; Code:

(require 'custom)
(require 'flymake)
(require 'geiser-impl)   ; for `geiser-active-implementations'

(defcustom flymake-guile-guild-executable "guild"
  "The guild executable to use.  This will be looked up in $PATH."
  :type '(string)
  :risky t
  :group 'flymake-guile)

(defvar-local flymake-guile--flymake-proc nil
  "The latest invocation of guild compile.")

(defvar-local flymake-guile--temp-file nil
  "The temporary file name to pass to guild.")

(defun flymake-guile--encode-filename (buffer-name)
  "Create a safe temporary file name from BUFFER-NAME."
  (concat "/tmp/flymake-guile-"
          (string-replace
           "/" "!"   ; we don't want to create subdirs under /tmp
           (or buffer-name
               (format "temp-%s.scm"
                       (random most-positive-fixnum))))))

;; See info node: (flymake)An annotated example backend.
(defun flymake-guile (report-fn &rest _args)
  "Run guild compile and report diagnostics from it using REPORT-FN.
Any running invocations are killed before running another one."
  (unless (executable-find flymake-guile-guild-executable)
    (funcall report-fn :panic
             :explanation "Cannot find `flymake-guile-guild-executable' program")
    (error "Cannot find guild executable"))

  (unless flymake-guile--temp-file
    (setq-local flymake-guile--temp-file (flymake-guile--encode-filename (buffer-file-name))))

  ;; Kill previous check, if it's still running.
  (when (process-live-p flymake-guile--flymake-proc)
    (kill-process flymake-guile--flymake-proc))

  ;; This needs `lexical-binding'.
  (let ((source (current-buffer))
        ;; Copy `flymake-guile--temp-file' to a local var so that we can refer to it in the `lambda' below.
        (temp-file flymake-guile--temp-file))
    (save-restriction
      (widen)
      ;; Send the buffer to guild on stdin.
      (with-temp-file flymake-guile--temp-file
        (insert-buffer-substring-no-properties source))
      (setq flymake-guile--flymake-proc
            (make-process
             :name "flymake-guild" :noquery t :connection-type 'pipe
             ;; Direct output to a temporary buffer.
             :buffer (generate-new-buffer " *flymake-guile*")
             ;; Guild can't read from stdin; it needs a file.
             :command (list flymake-guile-guild-executable "compile"
                            ;; See "guild --warn=help" for details.
                            ;; "--warn=unsupported-warning"  ; ignore unsupported warning types
                            ;; "--warn=unused-variable"  ; too many false positives from macros
                            "--warn=unused-toplevel"
                            "--warn=shadowed-toplevel"
                            "--warn=unbound-variable"
                            "--warn=macro-use-before-definition"
                            "--warn=use-before-definition"
                            "--warn=non-idempotent-definition"
                            "--warn=arity-mismatch"
                            "--warn=duplicate-case-datum"
                            "--warn=bad-case-datum"
                            "--warn=format"
                            "-L" (expand-file-name
                                  (project-root (project-current nil (file-name-directory
                                                                      (buffer-file-name source)))))
                            flymake-guile--temp-file)
             :sentinel
             (lambda (proc _event)
               "Parse diagnostic messages once the process PROC has exited."
               ;; Check the process has actually exited, not just been suspended.
               (when (memq (process-status proc) '(exit signal))
                 (unwind-protect
                     ;; Only proceed if we've got the "latest" process.
                     (if (with-current-buffer source (not (eq proc flymake-guile--flymake-proc)))
                         (flymake-log :warning "Canceling obsolete check %s" proc)
                       (with-current-buffer (process-buffer proc)
                         (goto-char (point-min))
                         (cl-loop
                          with msg-regexp = (rx bol (literal temp-file) ":"          ; filename
                                                (group (+ digit)) ":"                ; line
                                                (group (+ digit)) ": "               ; column
                                                (group (or "warning" "error")) ": "  ; type
                                                (group (+ not-newline)) eol)         ; message
                          while (search-forward-regexp msg-regexp nil t)
                          for (beg . end) = (flymake-diag-region
                                             source   ; we filter for messages matching our buffer in the regexp
                                             (string-to-number (match-string 1))
                                             ;; guild outputs 0-based column numbers
                                             (1+ (string-to-number (match-string 2))))
                          for type = (pcase (match-string 3)
                                       ("warning" :warning)
                                       ("error" :error)
                                       (type (error "Unknown guild error type %s" type)))
                          collect (flymake-make-diagnostic source beg end type (match-string 4))
                          into diags
                          finally (funcall report-fn diags))))
                   ;; Clean up temporary buffer.
                   (kill-buffer (process-buffer proc))
                   (delete-file temp-file)))))))))

(defun flymake-guile-enable ()
  "Set up the Guile checker for flymake, if in a Guile buffer."
  (when (memq 'guile geiser-active-implementations)
    (add-hook 'flymake-diagnostic-functions #'flymake-guile nil t)))

(add-hook 'scheme-mode-hook #'flymake-guile-enable)

(provide 'flymake-guile)
;;; flymake-guile.el ends here