You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

206 lines
8.0 KiB

;;; flycheck-clojure.el --- Flycheck: Clojure support -*- lexical-binding: t; -*-
;; Copyright © 2014 Peter Fraenkel
;; Copyright (C) 2014 Sebastian Wiesner <swiesner@lunaryorn.com>
;;
;; Author: Peter Fraenkel <pnf@podsnap.com>
;; Sebastian Wiesner <swiesner@lunaryorn.com>
;; Maintainer: Peter Fraenkel <pnf@podsnap.com>
;; URL: https://github.com/clojure-emacs/squiggly-clojure
;; Package-Version: 20150831.631
;; Version: 1.1.0
;; Package-Requires: ((cider "0.8.1") (flycheck "0.22-cvs1") (let-alist "1.0.1") (emacs "24"))
;; This file is not part of GNU Emacs.
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Add Clojure support to Flycheck.
;;
;; Provide syntax checkers to check Clojure code using a running Cider repl.
;;
;; Installation:
;;
;; (eval-after-load 'flycheck '(flycheck-clojure-setup))
;;; Code:
(require 'cider-client)
(require 'flycheck)
(require 'json)
(require 'url-parse)
(eval-when-compile (require 'let-alist))
;;;###autoload
(defun flycheck-clojure-parse-cider-errors (value checker)
"Parse cider errors from JSON VALUE from CHECKER.
Return a list of parsed `flycheck-error' objects."
;; Parse the nested JSON from Cider. The outer JSON contains the return value
;; from Cider, and the inner JSON the errors returned by the individual
;; checker.
(let ((error-objects (json-read-from-string (json-read-from-string value))))
(mapcar (lambda (o)
(let-alist o
;; Use the file name reported by the syntax checker, but only if
;; its absolute, because typed reports relative file names that
;; are hard to expand correctly, since they are relative to the
;; source directory (not the project directory).
(let* ((parsed-file (when .file
(url-filename
(url-generic-parse-url .file))))
(filename (if (and parsed-file
(file-name-absolute-p parsed-file))
parsed-file
(buffer-file-name))))
(flycheck-error-new-at .line .column (intern .level) .msg
:checker checker
:filename filename))))
error-objects)))
(defun cider-flycheck-eval (input callback)
"Send the request INPUT and register the CALLBACK as the response handler.
Uses the tooling session, with no specified namespace."
(cider-nrepl-request:eval input callback))
(defun flycheck-clojure-may-use-cider-checker ()
"Determine whether a cider checker may be used.
Checks for `cider-mode', and a current nREPL connection.
Standard predicate for cider checkers."
(let ((connection-buffer (cider-default-connection :no-error)))
(and (bound-and-true-p cider-mode)
connection-buffer
(buffer-live-p (get-buffer connection-buffer))
(clojure-find-ns))))
(defun flycheck-clojure-start-cider (checker callback)
"Start a cider syntax CHECKER with CALLBACK."
(let ((ns (clojure-find-ns))
(form (get checker 'flycheck-clojure-form)))
(cider-flycheck-eval
(funcall form ns)
(nrepl-make-response-handler
(current-buffer)
(lambda (buffer value)
(funcall callback 'finished
(with-current-buffer buffer
(flycheck-clojure-parse-cider-errors value checker))))
nil ; stdout
nil ; stderr
(lambda (_)
;; If the evaluation completes without returning any value, there has
;; gone something wrong. Ideally, we'd report *what* was wrong, but
;; `nrepl-make-response-handler' is close to useless for this :(,
;; because it just `message's for many status codes that are errors for
;; us :(
(funcall callback 'errored "Done with no errors"))
(lambda (_buffer ex _rootex _sess)
(funcall callback 'errored
(format "Form %s of checker %s failed: %s"
form checker ex))))))
)
(defun flycheck-clojure-define-cider-checker (name docstring &rest properties)
"Define a Cider syntax checker with NAME, DOCSTRING and PROPERTIES.
NAME, DOCSTRING, and PROPERTIES are like for
`flycheck-define-generic-checker', except that `:start' and
`:modes' are invalid PROPERTIES. A syntax checker defined with
this function will always check in `clojure-mode', and only if
`cider-mode' is enabled.
Instead of `:start', this syntax checker requires a `:form
FUNCTION' property. FUNCTION takes the current Clojure namespace
as single argument, and shall return a string containing a
Clojure form to be sent to Cider to check the current buffer."
(declare (indent 1)
(doc-string 2))
(let* ((form (plist-get properties :form))
(orig-predicate (plist-get properties :predicate)))
(when (plist-get :start properties)
(error "Checker %s may not have :start" name))
(when (plist-get :modes properties)
(error "Checker %s may not have :modes" name))
(unless (functionp form)
(error ":form %s of %s not a valid function" form name))
(apply #'flycheck-define-generic-checker
name docstring
:start #'flycheck-clojure-start-cider
:modes '(clojure-mode)
:predicate (if orig-predicate
(lambda ()
(and (flycheck-clojure-may-use-cider-checker)
(funcall orig-predicate)))
#'flycheck-clojure-may-use-cider-checker)
properties)
(put name 'flycheck-clojure-form form)))
(flycheck-clojure-define-cider-checker 'clojure-cider-eastwood
"A syntax checker for Clojure, using Eastwood in Cider.
See URL `https://github.com/jonase/eastwood' and URL
`https://github.com/clojure-emacs/cider/' for more information."
:form (lambda (ns)
(format "(do (require 'squiggly-clojure.core) (squiggly-clojure.core/check-ew '%s))"
ns))
:next-checkers '(clojure-cider-kibit clojure-cider-typed))
(flycheck-clojure-define-cider-checker 'clojure-cider-kibit
"A syntax checker for Clojure, using Kibit in Cider.
See URL `https://github.com/jonase/kibit' and URL
`https://github.com/clojure-emacs/cider/' for more information."
:form (lambda (ns)
(format
"(do (require 'squiggly-clojure.core) (squiggly-clojure.core/check-kb '%s %s))"
ns
;; Escape file name for Clojure
(flycheck-sexp-to-string (buffer-file-name))))
:predicate (lambda () (buffer-file-name))
:next-checkers '(clojure-cider-typed))
(flycheck-clojure-define-cider-checker 'clojure-cider-typed
"A syntax checker for Clojure, using Typed Clojure in Cider.
See URL `https://github.com/clojure-emacs/cider/' and URL
`https://github.com/clojure/core.typed' for more information."
:form (lambda (ns)
(format
"(do (require 'squiggly-clojure.core) (squiggly-clojure.core/check-tc '%s))"
ns)))
;;;###autoload
(defun flycheck-clojure-setup ()
"Setup Flycheck for Clojure."
(interactive)
;; Add checkers in reverse order, because `add-to-list' adds to front.
(dolist (checker '(clojure-cider-typed
clojure-cider-kibit
clojure-cider-eastwood))
(add-to-list 'flycheck-checkers checker)))
(provide 'flycheck-clojure)
;; Local Variables:
;; indent-tabs-mode: nil
;; End:
;;; flycheck-clojure.el ends here