Skip to content

Commit

Permalink
Add support for GHC JSON diagnostics
Browse files Browse the repository at this point in the history
  • Loading branch information
sol committed Oct 23, 2024
1 parent 91505d6 commit 9e9bfe8
Show file tree
Hide file tree
Showing 23 changed files with 463 additions and 97 deletions.
1 change: 1 addition & 0 deletions package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ other-extensions:

dependencies:
- base >= 4.11 && < 5
- pretty
- process
- fsnotify == 0.4.*
- time
Expand Down
11 changes: 10 additions & 1 deletion sensei.cabal

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 92 additions & 0 deletions src/GHC/Diagnostic.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{-# LANGUAGE DeriveAnyClass #-}
module GHC.Diagnostic (
Diagnostic(..)
, Span(..)
, Location(..)
, Severity(..)
, parse
, format
) where

import Prelude hiding ((<>), span, unlines)
import Imports hiding (empty, unlines)
import GHC.Generics (Generic)
import Data.Aeson (ToJSON(..), FromJSON(..), decode)
import Data.ByteString.Lazy (fromStrict)
import Text.PrettyPrint

data Diagnostic = Diagnostic {
version :: String
, ghcVersion :: String
, span :: Maybe Span
, severity :: Severity
, code :: Maybe Int
, message :: [String]
, hints :: [String]
} deriving (Eq, Show, Generic, ToJSON, FromJSON)

data Span = Span {
file :: FilePath
, start :: Location
, end :: Location
} deriving (Eq, Show, Generic, ToJSON, FromJSON)

data Location = Location {
line :: Int
, column :: Int
} deriving (Eq, Show, Generic, ToJSON, FromJSON)

data Severity = Warning | Error
deriving (Eq, Show, Generic, ToJSON, FromJSON)

parse :: ByteString -> Maybe Diagnostic
parse = decode . fromStrict

format :: Diagnostic -> ByteString
format diagnostic = encodeUtf8 . render $ unlines [
hang header 4 messageWithHints
, ""
, ""
]
where
header :: Doc
header = span <> colon <+> severity <> colon <+> code

span :: Doc
span = case diagnostic.span of
Nothing -> "<no location info>"
Just loc -> text loc.file <> colon <> int loc.start.line <> colon <> int loc.start.column

severity :: Doc
severity = case diagnostic.severity of
Warning -> "warning"
Error -> "error"

code :: Doc
code = case diagnostic.code of
Nothing -> empty
Just c -> brackets $ "GHC-" <> int c

message :: Doc
message = bulleted $ map verbatim diagnostic.message

hints :: [Doc]
hints = map verbatim diagnostic.hints

messageWithHints :: Doc
messageWithHints = case hints of
[] -> message
[h] -> message $$ hang (text "Suggested fix:") 2 h
hs -> message $$ hang (text "Suggested fixes:") 2 (bulleted hs)

bulleted :: [Doc] -> Doc
bulleted = \ case
[] -> empty
[doc] -> doc
docs -> vcat $ map (char '' <+>) docs

verbatim :: String -> Doc
verbatim = unlines . map text . lines

unlines :: [Doc] -> Doc
unlines = foldr ($+$) empty
24 changes: 18 additions & 6 deletions src/HTTP.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module HTTP (
import Imports hiding (strip, encodeUtf8)

import System.Directory
import Data.Aeson (ToJSON(..), encode)
import qualified Data.ByteString.Lazy as L
import Data.Text.Lazy.Encoding (encodeUtf8)
import Network.Wai
Expand All @@ -22,6 +23,7 @@ import Network.Wai.Handler.Warp (runSettingsSocket, defaultSettings)
import Network.Socket

import qualified Trigger
import GHC.Diagnostic

socketName :: FilePath -> String
socketName dir = dir </> ".sensei.sock"
Expand All @@ -35,8 +37,8 @@ newSocket = socket AF_UNIX Stream 0
withSocket :: (Socket -> IO a) -> IO a
withSocket = bracket newSocket close

withServer :: FilePath -> IO (Trigger.Result, String) -> IO a -> IO a
withServer dir trigger = withApplication dir (app trigger)
withServer :: FilePath -> IO (Trigger.Result, String, [Diagnostic]) -> IO a -> IO a
withServer dir = withApplication dir . app

withApplication :: FilePath -> Application -> IO a -> IO a
withApplication dir application action = do
Expand All @@ -59,8 +61,12 @@ withThread asyncAction action = do
takeMVar mvar
return r

app :: IO (Trigger.Result, String) -> Application
app trigger request respond = trigger >>= textPlain
app :: IO (Trigger.Result, String, [Diagnostic]) -> Application
app getLastResult request respond = case pathInfo request of
["diagnostics"] -> do
(_, _, diagnostics) <- getLastResult
respond $ json diagnostics
_ -> getLastResult >>= textPlain
where
color :: Either ByteString Bool
color = case join $ lookup "color" $ queryString request of
Expand All @@ -69,8 +75,8 @@ app trigger request respond = trigger >>= textPlain
Just "true" -> Right True
Just value -> Left $ "invalid value for color: " <> urlEncode True value

textPlain :: (Trigger.Result, FilePath) -> IO ResponseReceived
textPlain (result, xs) = case color of
textPlain :: (Trigger.Result, FilePath, [Diagnostic]) -> IO ResponseReceived
textPlain (result, xs, _diagnostics) = case color of
Left err -> respond $ responseLBS status400 [(hContentType, "text/plain")] (L.fromStrict err)
Right c -> respond $ responseLBS status [(hContentType, "text/plain")] (encodeUtf8 . fromString $ strip xs)
where
Expand All @@ -84,6 +90,12 @@ app trigger request respond = trigger >>= textPlain
Trigger.Failure -> status500
Trigger.Success -> status200

json :: ToJSON a => a -> Response
json value = responseLBS
status200
[("Content-Type", "application/json")]
(encode value)

-- |
-- Remove terminal sequences.
stripAnsi :: String -> String
Expand Down
13 changes: 11 additions & 2 deletions src/Imports.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Data.Functor as Imports ((<&>))
import Data.Bifunctor as Imports
import Data.Char as Imports
import Data.Either as Imports
import Data.List as Imports
import Data.List as Imports hiding (span)
import Data.Maybe as Imports
import Data.String as Imports
import Data.ByteString.Char8 as Imports (ByteString, pack, unpack)
Expand All @@ -26,6 +26,10 @@ import Control.Monad.IO.Class as Imports
import System.IO (Handle)
import GHC.IO.Handle.Internals (wantReadableHandle_)

import Data.Version as Imports (Version(..), showVersion, makeVersion)
import qualified Data.Version as Version
import Text.ParserCombinators.ReadP

import qualified Data.Text as T
import qualified Data.Text.Encoding as T

Expand Down Expand Up @@ -54,7 +58,12 @@ encodeUtf8 :: String -> ByteString
encodeUtf8 = T.encodeUtf8 . T.pack

decodeUtf8 :: ByteString -> String
decodeUtf8 = T.unpack . T.decodeUtf8
decodeUtf8 = T.unpack . T.decodeUtf8Lenient

strip :: String -> String
strip = reverse . dropWhile isSpace . reverse . dropWhile isSpace

parseVersion :: String -> Maybe Version
parseVersion xs = case [v | (v, "") <- readP_to_S Version.parseVersion xs] of
[v] -> Just v
_ -> Nothing
Loading

0 comments on commit 9e9bfe8

Please sign in to comment.