aboutsummaryrefslogtreecommitdiff
path: root/tw/services/files/emacs-packages/flymake-guile.el
diff options
context:
space:
mode:
Diffstat (limited to 'tw/services/files/emacs-packages/flymake-guile.el')
-rw-r--r--tw/services/files/emacs-packages/flymake-guile.el123
1 files changed, 123 insertions, 0 deletions
diff --git a/tw/services/files/emacs-packages/flymake-guile.el b/tw/services/files/emacs-packages/flymake-guile.el
new file mode 100644
index 00000000..edfbce82
--- /dev/null
+++ b/tw/services/files/emacs-packages/flymake-guile.el
@@ -0,0 +1,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