public
Last active

Integration of sandboxed GHC and HLint with Emacs' Flymake.

  • Download Gist
haskell-flymake.el
Emacs Lisp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
;; Copyright 2013 Guilherme Azzi
 
;; Permission is hereby granted, free of charge, to any person obtaining a copy
;; of this software and associated documentation files (the "Software"), to deal
;; in the Software without restriction, including without limitation the rights
;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
;; copies of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:
 
;; The above copyright notice and this permission notice shall be included in
;; all copies or substantial portions of the Software.
 
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
;; THE SOFTWARE.
 
 
;;;;;;;;
;; How to use this:
 
;; When cabal uses a sandbox, it maintains a local package database,
;; located within the sandbox directory and named after the platform
;; currently used, e.g.:
;; .cabal-sandbox/x86_64-linux-ghc-7.6.3-packages.conf.d
;; Cabal-dev should work in a similar way.
;;
;; When GHC is called, it must be passed this package database
;; directory on the '-package-db' option. This is what this
;; flymake configuration does.
;;
;; The user must therefore specify the package-db directory when asked
 
;;;;;;;;
;; Possible improvements:
;; - Better identification of the package-db, perhaps reading configuration files
 
 
 
;; Please set this according to your configuration.
(defvar ghc-flymake-script (expand-file-name "~/.emacs.d/scripts/hslint.py"))
 
 
(defvar ghc-sandbox-package-db-location nil
"Location of the current package-db that should be used by GHC when
called by flymake for syntax checking of the current buffer.")
(make-variable-buffer-local 'ghc-sandbox-package-db-location)
 
(defvar ghc-sandbox-package-db-location-set nil
"Indicates if the user was already asked about the use and
location of a sandbox package-db for the current buffer.")
(make-variable-buffer-local 'ghc-sandbox-package-db-location-set)
(defvar ghc-sandbox-location-history nil
"List of package-dbs previously selected by the user.")
 
 
(defun ghc-flymake-set-sandbox ()
"Configures the use of a sandbox for syntax-checking with GHC."
(interactive)
 
(setq ghc-sandbox-package-db-location
(if (yes-or-no-p "Use ghc sandbox? ")
(expand-file-name (read-file-name "Location of sandbox package database: "))
nil))
(setq ghc-sandbox-package-db-location-set t))
 
 
(defun haskell-flymake-init ()
"When flymake triggers, generates a tempfile containing the
contents of the current buffer, runs GHC-FLYMAKE-SCRIPT on it,
and deletes the temporary file.
 
Uses the configured sandbox package-db, and if the user
didn't yet configure it, asks about it."
(when (not ghc-sandbox-package-db-location-set)
(ghc-flymake-set-sandbox))
(let* ((temp-file (flymake-init-create-temp-buffer-copy
'flymake-create-temp-intemp))
(local-file (file-relative-name
temp-file
(file-name-directory buffer-file-name)))
(hslint (concat dotemacs-dir "scripts/hslint.py")))
(list ghc-flymake-script
(cons temp-file
(when ghc-sandbox-package-db-location
(list "-package-db" ghc-sandbox-package-db-location))))))
hslint.py
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
#!/usr/bin/env python
 
# Copyright 2013 Guilherme Azzi
 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
 
# This script is based on a perl script called 'hslint' which was, at the
# time of this writing, available from multiple online sources, and whose
# authorship could not be determined by the author of the present script.
 
"""Script used to integrate GHC and HLint with Emacs' Flymake.
 
Usage: hslint.py FILE [GHC_OPTS]
 
Runs ghc and hlint on the given file, using the given
ghc options, and reformats their output for Flymake.
"""
 
 
 
##
## Configuration variables
ENCODING = 'utf-8'
 
GHC = "ghc"
GHC_OPTIONS = ['-Wall'] # e.g. ['-fglasgow-exts']
GHC_PACKAGES = [] # e.g. ['QuickCheck']
 
PROJ_TREE_DEPTH = 5 # Assume the root directory for the project
# is at most n steps up the filesystem tree
 
HLINT = "hlint"
HLINT_OPTIONS = []
 
 
 
##
## Actual code (modify at your own risk)
import re
import sys
from subprocess import check_output, CalledProcessError, STDOUT
 
 
# Extract the command line arguments
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
hsfile = sys.argv[1]
ghcparams = sys.argv[2:]
 
# Initialize the messages for error cases
ghc_message, hlint_message = b'', b''
 
# Assemble the compiler command
ghc_command = [GHC] + GHC_OPTIONS
for package in GHC_PACKAGES:
ghc_command.extend(['-package', package])
for num_backtracks in range(1,NUM_BACKTRACKING_INCLUDES+1):
include_dir = '/'.join( '..' for i in range(num_backtracks))
ghc_command.append('-i'+include_dir)
 
ghc_command.extend(ghcparams)
ghc_command.extend([ '--make', '-fno-code', hsfile ])
 
# Run the compiler and keep its output
try:
ghc_message = check_output(ghc_command, stderr=STDOUT)
except CalledProcessError as e:
ghc_message = e.output
 
# If compiler succeeded, run hlint
else:
try:
hlint_message = check_output([HLINT, hsfile], stderr=STDOUT)
except CalledProcessError as e:
hlint_message = e.output
 
 
# Parse the ghc output, outputing errors in the correct format
for line in str(ghc_message, ENCODING).splitlines():
 
match = re.match("(^\\S+\\.l?hs)(:\\d*:\\d*:)\\s?(.*)", line)
if match:
print('\n',
match.group(1),
match.group(2),
match.group(3).strip(),
sep='', end=' ')
else:
print(line.strip(), end=' ')
 
print()
 
 
# Parse the hlint output, outputing errors in the correct format
for line in str(hlint_message, ENCODING).splitlines():
 
match = re.match("(^\\S+\\.l?hs)(:\\d*:\\d*:)\\s?(.*)", line)
if match:
print('\n',
match.group(1),
match.group(2),
# HLint errors should be considered warnings
match.group(3).strip().replace('Error', 'Warning', 1),
sep='', end=' ')
else:
print(line.strip(), end=' ')
 
print()

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.