;;; flycheck-haskell.el --- Flycheck: Cabal projects and sandboxes -*- lexical-binding: t; -*- ;; Copyright (C) 2014, 2015 Sebastian Wiesner ;; Copyright (C) 2015 Michael Alan Dorman ;; Copyright (C) 2015 Alex Rozenshteyn ;; Copyright (C) 2014 Gracjan Polak ;; Author: Sebastian Wiesner ;; 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 . ;;; 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