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.

300 lines
11 KiB

;;; 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