;;; flycheck-clojure.el --- Flycheck: Clojure support -*- lexical-binding: t; -*- ;; Copyright © 2014 Peter Fraenkel ;; Copyright (C) 2014 Sebastian Wiesner ;; ;; Author: Peter Fraenkel ;; Sebastian Wiesner ;; Maintainer: Peter Fraenkel ;; URL: https://github.com/clojure-emacs/squiggly-clojure ;; Package-Version: 20160704.1221 ;; 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 . ;;; 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)) (defcustom flycheck-clojure-inject-dependencies-at-jack-in t "When nil, do not inject repl dependencies (i.e. the linters/checkers) at `cider-jack-in' time." :group 'flycheck-clojure :type 'boolean) (defvar flycheck-clojure-dep-version "0.1.6" "Version of `acyclic/squiggly-clojure' compatible with this version of flycheck-clojure.") ;;;###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-tooling-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))) (defun flycheck-clojure-inject-jack-in-dependencies () "Inject the REPL dependencies of flycheck-clojure at `cider-jack-in'. If injecting the dependencies is not preferred set `flycheck-clojure-inject-dependencies-at-jack-in' to nil." (when (and flycheck-clojure-inject-dependencies-at-jack-in (boundp 'cider-jack-in-dependencies)) (add-to-list 'cider-jack-in-dependencies `("acyclic/squiggly-clojure" ,flycheck-clojure-dep-version)))) ;;;###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)) (flycheck-clojure-inject-jack-in-dependencies)) (provide 'flycheck-clojure) ;; Local Variables: ;; indent-tabs-mode: nil ;; End: ;;; flycheck-clojure.el ends here