|
|
;;; flycheck-haskell.el --- Flycheck: Cabal projects and sandboxes -*- lexical-binding: t; -*- |
|
|
|
|
|
;; Copyright (C) 2014, 2015 Sebastian Wiesner <swiesner@lunaryorn.com> |
|
|
;; Copyright (C) 2015 Michael Alan Dorman <mdorman@ironicdesign.com> |
|
|
;; Copyright (C) 2015 Alex Rozenshteyn <rpglover64@gmail.com> |
|
|
;; Copyright (C) 2014 Gracjan Polak <gracjanpolak@gmail.com> |
|
|
|
|
|
;; Author: Sebastian Wiesner <swiesner@lunaryorn.com> |
|
|
;; URL: https://github.com/flycheck/flycheck-haskell |
|
|
;; Keywords: tools, convenience |
|
|
;; Version: 0.8-cvs |
|
|
;; Package-Requires: ((emacs "24.1") (flycheck "0.22") (haskell-mode "13.7") (dash "2.4.0") (let-alist "1.0.1")) |
|
|
|
|
|
;; 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: |
|
|
|
|
|
;; Configure Haskell syntax checking by Flycheck. |
|
|
|
|
|
;;;; Cabal support |
|
|
|
|
|
;; Try to find Cabal project files for Haskell buffers, and configure the |
|
|
;; Haskell syntax checkers in Flycheck according to the contents of the Cabal |
|
|
;; file: |
|
|
;; |
|
|
;; - Add all source directories to the GHC search path |
|
|
;; - Add build directories from Cabal to the GHC search path to speed up |
|
|
;; checking and support non-Haskell modules such as hsc files |
|
|
;; - Add auto-generated files from Cabal to the GHC search path |
|
|
;; - Set the language from Cabal |
|
|
;; - Enable language extensions from Cabal |
|
|
|
|
|
;;;; Cabal sandboxes |
|
|
|
|
|
;; Try to find a Cabal sandbox configuration for this project, and configure the |
|
|
;; Haskell syntax checkers in Flycheck to use the package database from the |
|
|
;; Sandbox. |
|
|
|
|
|
;;;; Setup |
|
|
|
|
|
;; (add-hook 'flycheck-mode-hook #'flycheck-haskell-setup) |
|
|
|
|
|
;;; Code: |
|
|
|
|
|
(eval-when-compile |
|
|
(require 'rx) |
|
|
(require 'let-alist)) |
|
|
|
|
|
(require 'haskell-cabal) |
|
|
(require 'flycheck) |
|
|
(require 'dash) |
|
|
|
|
|
|
|
|
;;; Customization |
|
|
|
|
|
(defgroup flycheck-haskell nil |
|
|
"Haskell support for Flycheck." |
|
|
:prefix "flycheck-haskell-" |
|
|
:group 'flycheck |
|
|
:link '(url-link :tag "Github" "https://github.com/flycheck/flycheck-haskell")) |
|
|
|
|
|
(defcustom flycheck-haskell-runghc-command |
|
|
(if (executable-find "stack") |
|
|
'("stack" "--verbosity" "silent" "runghc") |
|
|
'("runghc")) |
|
|
"Command for `runghc'. |
|
|
|
|
|
This library uses `runghc' to run various Haskell helper scripts |
|
|
to extract information from Cabal files. This option provides |
|
|
the command to invoke `runghc'. The default is to use `stack' |
|
|
and otherwise fall back to standard `runghc'." |
|
|
:type '(repeat (string :tag "Command")) |
|
|
:risky t |
|
|
:group 'flycheck-haskell) |
|
|
|
|
|
|
|
|
;;; Cabal support |
|
|
(defconst flycheck-haskell-directory |
|
|
(file-name-directory (if load-in-progress |
|
|
load-file-name |
|
|
(buffer-file-name))) |
|
|
"The package directory of flycheck-haskell.") |
|
|
|
|
|
(defconst flycheck-haskell-helper |
|
|
(expand-file-name "get-cabal-configuration.hs" flycheck-haskell-directory) |
|
|
"The helper to dump the Cabal configuration.") |
|
|
|
|
|
(defconst flycheck-haskell-flags-helper |
|
|
(expand-file-name "get-flags.hs" flycheck-haskell-directory) |
|
|
"The helper to get compiler flags for the Cabal helper.") |
|
|
|
|
|
(defun flycheck-haskell-runghc-command (args) |
|
|
"Create a runghc command with ARGS. |
|
|
|
|
|
Take the base command from `flycheck-haskell-runghc-command'." |
|
|
(append flycheck-haskell-runghc-command args nil)) |
|
|
|
|
|
(defun flycheck-haskell--get-flags () |
|
|
"Get GHC flags to run the Cabal helper." |
|
|
(ignore-errors |
|
|
(apply #'process-lines |
|
|
(flycheck-haskell-runghc-command |
|
|
(list flycheck-haskell-flags-helper))))) |
|
|
|
|
|
(defun flycheck-haskell-read-cabal-configuration (cabal-file) |
|
|
"Read the Cabal configuration from CABAL-FILE." |
|
|
(let* ((args (append (flycheck-haskell--get-flags) |
|
|
(list flycheck-haskell-helper cabal-file))) |
|
|
(command (flycheck-haskell-runghc-command args))) |
|
|
(with-temp-buffer |
|
|
(pcase (apply 'call-process (car command) nil t nil (cdr command)) |
|
|
(0 (goto-char (point-min)) |
|
|
(read (current-buffer))) |
|
|
(retcode (message "Reading Haskell configuration failed with exit code %s and output:\n%s" |
|
|
retcode (buffer-string)) |
|
|
nil))))) |
|
|
|
|
|
|
|
|
;;; Cabal configuration caching |
|
|
(defconst flycheck-haskell-config-cache (make-hash-table :test 'equal) |
|
|
"Cache of Cabal configuration. |
|
|
|
|
|
A hash table, mapping the name of a cabal file to a |
|
|
cons-cell `(MODTIME . CONFIG)', where MODTIME is the modification |
|
|
time of the cabal file, and CONFIG the extracted configuration.") |
|
|
|
|
|
(defun flycheck-haskell-clear-config-cache () |
|
|
"Clear the cache of configurations." |
|
|
(interactive) |
|
|
(clrhash flycheck-haskell-config-cache)) |
|
|
|
|
|
(defun flycheck-haskell-get-cached-configuration (cabal-file) |
|
|
"Get the cached configuration for CABAL-FILE. |
|
|
|
|
|
Return the cached configuration, or nil, if there is no cache |
|
|
entry, or if the cache entry is outdated." |
|
|
(pcase-let* ((cache-entry (gethash cabal-file flycheck-haskell-config-cache)) |
|
|
(`(,modtime . ,config) cache-entry)) |
|
|
(when (and modtime (file-exists-p cabal-file)) |
|
|
(let ((current-modtime (nth 5 (file-attributes cabal-file)))) |
|
|
(if (time-less-p modtime current-modtime) |
|
|
;; The entry is outdated, drop it. `remhash' always |
|
|
;; returns nil, so we are safe to use it here. |
|
|
(remhash cabal-file flycheck-haskell-config-cache) |
|
|
;; The configuration is up to date, use it |
|
|
config))))) |
|
|
|
|
|
(defun flycheck-haskell-read-and-cache-configuration (cabal-file) |
|
|
"Read and cache configuration from CABAL-FILE. |
|
|
|
|
|
Return the configuration." |
|
|
(let ((modtime (nth 5 (file-attributes cabal-file))) |
|
|
(config (flycheck-haskell-read-cabal-configuration cabal-file))) |
|
|
(puthash cabal-file (cons modtime config) flycheck-haskell-config-cache) |
|
|
config)) |
|
|
|
|
|
(defun flycheck-haskell-get-configuration (cabal-file) |
|
|
"Get the Cabal configuration from CABAL-FILE. |
|
|
|
|
|
Get the configuration either from our cache, or by reading the |
|
|
CABAL-FILE. |
|
|
|
|
|
Return the configuration." |
|
|
(or (flycheck-haskell-get-cached-configuration cabal-file) |
|
|
(flycheck-haskell-read-and-cache-configuration cabal-file))) |
|
|
|
|
|
|
|
|
;;; Cabal sandbox support |
|
|
(defconst flycheck-haskell-cabal-config "cabal.config" |
|
|
"The file name of a Cabal configuration.") |
|
|
|
|
|
(defconst flycheck-haskell-cabal-config-keys '(with-compiler) |
|
|
"Keys to parse from a Cabal configuration file.") |
|
|
|
|
|
(defconst flycheck-haskell-sandbox-config "cabal.sandbox.config" |
|
|
"The file name of a Cabal sandbox configuration.") |
|
|
|
|
|
(defconst flycheck-haskell-sandbox-config-keys '(package-db) |
|
|
"Keys to parse from a Cabal sandbox configuration.") |
|
|
|
|
|
(defmacro flycheck-haskell-with-config-file-buffer (file-name &rest body) |
|
|
"Eval BODY in a buffer with the contents of FILE-NAME." |
|
|
(declare (indent 1)) |
|
|
`(with-temp-buffer |
|
|
(insert-file-contents ,file-name) |
|
|
(goto-char (point-min)) |
|
|
,@body)) |
|
|
|
|
|
(defun flycheck-haskell-get-config-value (key) |
|
|
"Get the value of a configuration KEY from this buffer. |
|
|
|
|
|
KEY is a symbol denoting the key whose value to get. Return |
|
|
a `(KEY . VALUE)' cons cell." |
|
|
(save-excursion |
|
|
(goto-char (point-min)) |
|
|
(-when-let (setting (haskell-cabal-get-setting (symbol-name key))) |
|
|
(cons key (substring-no-properties setting))))) |
|
|
|
|
|
(defun flycheck-haskell-parse-config-file (keys config-file) |
|
|
"Parse KEYS from CONFIG-FILE. |
|
|
|
|
|
KEYS is a list of symbols. Return an alist with all parsed |
|
|
KEYS." |
|
|
(flycheck-haskell-with-config-file-buffer config-file |
|
|
(mapcar #'flycheck-haskell-get-config-value keys))) |
|
|
|
|
|
(defun flycheck-haskell-find-config (config-file) |
|
|
"Find a CONFIG-FILE for the current buffer. |
|
|
|
|
|
Return the absolute path of CONFIG-FILE as string, or nil if |
|
|
CONFIG-FILE was not found." |
|
|
(-when-let (root-dir (locate-dominating-file (buffer-file-name) config-file)) |
|
|
(expand-file-name config-file root-dir))) |
|
|
|
|
|
(defun flycheck-haskell-get-cabal-config () |
|
|
"Get Cabal configuration for the current buffer. |
|
|
|
|
|
Return an alist with the Cabal configuration for the current |
|
|
buffer." |
|
|
(-when-let (file-name (flycheck-haskell-find-config |
|
|
flycheck-haskell-cabal-config)) |
|
|
(flycheck-haskell-parse-config-file flycheck-haskell-cabal-config-keys |
|
|
file-name))) |
|
|
|
|
|
(defun flycheck-haskell-get-sandbox-config () |
|
|
"Get sandbox configuration for the current buffer. |
|
|
|
|
|
Return an alist with the sandbox configuration for the current |
|
|
buffer." |
|
|
(-when-let (file-name (flycheck-haskell-find-config |
|
|
flycheck-haskell-sandbox-config)) |
|
|
(flycheck-haskell-parse-config-file flycheck-haskell-sandbox-config-keys |
|
|
file-name))) |
|
|
|
|
|
|
|
|
;;; Buffer setup |
|
|
(defun flycheck-haskell-process-configuration (config) |
|
|
"Process the a Cabal CONFIG." |
|
|
(let-alist config |
|
|
(setq-local flycheck-ghc-search-path |
|
|
(append .build-directories .source-directories |
|
|
flycheck-ghc-search-path)) |
|
|
(setq-local flycheck-ghc-language-extensions |
|
|
(append .extensions .languages |
|
|
flycheck-ghc-language-extensions)) |
|
|
(setq-local flycheck-ghc-args |
|
|
(append .other-options flycheck-ghc-args)))) |
|
|
|
|
|
(defun flycheck-haskell-configure () |
|
|
"Set paths and package database for the current project." |
|
|
(interactive) |
|
|
(when (and (buffer-file-name) (file-directory-p default-directory)) |
|
|
(-when-let* ((cabal-file (haskell-cabal-find-file)) |
|
|
(config (flycheck-haskell-get-configuration cabal-file))) |
|
|
(flycheck-haskell-process-configuration config)) |
|
|
|
|
|
(let-alist (flycheck-haskell-get-cabal-config) |
|
|
(when .with-compiler |
|
|
(setq-local flycheck-haskell-ghc-executable .with-compiler))) |
|
|
|
|
|
(let-alist (flycheck-haskell-get-sandbox-config) |
|
|
(when .package-db |
|
|
(setq-local flycheck-ghc-package-databases |
|
|
(cons .package-db flycheck-ghc-package-databases)) |
|
|
(setq-local flycheck-ghc-no-user-package-database t))))) |
|
|
|
|
|
;;;###autoload |
|
|
(defun flycheck-haskell-setup () |
|
|
"Setup Haskell support for Flycheck. |
|
|
|
|
|
If the current file is part of a Cabal project, configure |
|
|
Flycheck to take the module paths of the Cabal projects into |
|
|
account. |
|
|
|
|
|
Also search for Cabal sandboxes and add them to the module search |
|
|
path as well." |
|
|
(add-hook 'hack-local-variables-hook #'flycheck-haskell-configure)) |
|
|
|
|
|
(provide 'flycheck-haskell) |
|
|
|
|
|
;; Local Variables: |
|
|
;; indent-tabs-mode: nil |
|
|
;; coding: utf-8 |
|
|
;; End: |
|
|
|
|
|
;;; flycheck-haskell.el ends here
|
|
|
|