packages feed

sensei-0.9.0: src/GHC/Diagnostic.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE NoImplicitPrelude #-}
module GHC.Diagnostic (
  module Diagnostic
, Action(..)
, analyze
, apply
#ifdef TEST
, extractIdentifiers
, applyReplace
#endif
) where

import           Imports

import           System.IO
import           Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.ByteString.Char8 as B
import           Data.ByteString.Builder (hPutBuilder)

import           GHC.Diagnostic.Type as Diagnostic

data Action = Choices [Action] | AddExtension FilePath Text | Replace Span Text
  deriving (Eq, Show)

analyze :: Diagnostic -> Maybe Action
analyze diagnostic = analyzeCode <|> analyzeHints
  where
    analyzeCode :: Maybe Action
    analyzeCode = redundantImport
      where
        redundantImport :: Maybe Action
        redundantImport = matchCode 66111 >> removeLines

    matchCode :: Int -> Maybe ()
    matchCode expected = guard $ diagnostic.code == Just expected

    removeLines :: Maybe Action
    removeLines = Replace <$> diagnosticLines <*> pure ""

    diagnosticLines :: Maybe Span
    diagnosticLines = do
      span <- diagnostic.span
      return span {
        start = Location span.start.line 1
      , end = Location (span.end.line + 1) 1
      }

    analyzeHints :: Maybe Action
    analyzeHints = head $ mapMaybe analyzeHint diagnostic.hints

    analyzeHint :: String -> Maybe Action
    analyzeHint (T.pack -> hint) =
          perhapsYouIntendedToUse
      <|> enableAnyOfTheFollowingExtensions
      <|> perhapsUse
      <|> perhapsUseOneOfThese
      where
        perhapsYouIntendedToUse :: Maybe Action
        perhapsYouIntendedToUse = do
          AddExtension . (.file) <$> diagnostic.span <*> T.stripPrefix "Perhaps you intended to use " hint

        enableAnyOfTheFollowingExtensions :: Maybe Action
        enableAnyOfTheFollowingExtensions = do
          file <- (.file) <$> diagnostic.span
          T.stripPrefix "Enable any of the following extensions: " hint
            >>= head . reverse . map (AddExtension file) . T.splitOn ", "

        perhapsUse :: Maybe Action
        perhapsUse = Replace <$> diagnostic.span <*> (takeIdentifier <$> T.stripPrefix "Perhaps use `" hint)
          where
            takeIdentifier :: Text -> Text
            takeIdentifier = T.takeWhile (/= '\'')

        perhapsUseOneOfThese :: Maybe Action
        perhapsUseOneOfThese = do
          replaces <- Replace <$> diagnostic.span
          Choices . map replaces . extractIdentifiers <$> T.stripPrefix "Perhaps use one of these:" hint

extractIdentifiers :: Text -> [Text]
extractIdentifiers input = case T.breakOn "`" >>> snd >>> T.breakOn "\'" $ input of
  (T.drop 1 -> identifier, rest)
    | T.null rest -> []
    | otherwise -> identifier : extractIdentifiers rest

apply :: FilePath -> Maybe Int -> Action -> IO ()
apply dir c = relativeTo dir >>> go c
  where
    go :: Maybe Int -> Action -> IO ()
    go choice = \ case
      Choices choices -> do
        traverse_ (go Nothing) (head $ drop (maybe 0 pred choice) choices)
      AddExtension file name -> do
        old <- B.readFile file
        withFile file WriteMode $ \ h -> do
          hPutBuilder h $ "{-# LANGUAGE " <> T.encodeUtf8Builder name <> " #-}\n"
          B.hPutStr h old
      Replace span substitute -> do
        input <- B.readFile span.file <&> B.lines
        B.writeFile span.file . B.unlines $
          applyReplace span.start span.end substitute input

relativeTo :: FilePath -> Action -> Action
relativeTo dir = \ case
  Choices choices -> Choices $ map (relativeTo dir) choices
  AddExtension file name -> AddExtension (dir </> file) name
  Replace span substitute -> Replace span { file = dir </> span.file } substitute

applyReplace :: Location -> Location -> Text -> [ByteString] -> [ByteString]
applyReplace start end substitute input =
  let
    (before, rest) = splitAt (start.line - 1) input

    after :: [ByteString]
    after = drop (end.line - start.line + 1) rest

    decodedLines :: [Text]
    decodedLines = map T.decodeUtf8Lenient rest
  in case do
    firstLine <- head $ decodedLines
    lastLine <- head $ drop (end.line - start.line) decodedLines
    return $ T.take (start.column - 1) firstLine <> substitute <> T.drop (end.column - 1) lastLine
  of
    Nothing -> input
    Just substituted -> before ++ T.encodeUtf8 substituted : after