module Development.IDE.Core.Preprocessor
( preprocessor
) where
import Development.IDE.GHC.CPP
import Development.IDE.GHC.Orphans()
import Development.IDE.GHC.Compat
import GhcMonad
import StringBuffer as SB
import Data.List.Extra
import System.FilePath
import System.IO.Extra
import Data.Char
import DynFlags
import qualified HeaderInfo as Hdr
import Development.IDE.Types.Diagnostics
import Development.IDE.Types.Location
import Development.IDE.GHC.Error
import SysTools (Option (..), runUnlit, runPp)
import Control.Monad.Trans.Except
import qualified GHC.LanguageExtensions as LangExt
import Data.Maybe
import Control.Exception.Safe (catch, throw)
import Data.IORef (IORef, modifyIORef, newIORef, readIORef)
import Data.Text (Text)
import qualified Data.Text as T
import Outputable (showSDoc)
import Control.DeepSeq (NFData(rnf))
import Control.Exception (evaluate)
import Control.Monad.IO.Class (MonadIO)
import Exception (ExceptionMonad)
preprocessor :: (ExceptionMonad m, HasDynFlags m, MonadIO m) => FilePath -> Maybe StringBuffer -> ExceptT [FileDiagnostic] m (StringBuffer, DynFlags)
preprocessor filename mbContents = do
(isOnDisk, contents) <-
if isLiterate filename then do
dflags <- getDynFlags
newcontent <- liftIO $ runLhs dflags filename mbContents
return (False, newcontent)
else do
contents <- liftIO $ maybe (hGetStringBuffer filename) return mbContents
let isOnDisk = isNothing mbContents
return (isOnDisk, contents)
dflags <- ExceptT $ parsePragmasIntoDynFlags filename contents
(isOnDisk, contents, dflags) <-
if not $ xopt LangExt.Cpp dflags then
return (isOnDisk, contents, dflags)
else do
cppLogs <- liftIO $ newIORef []
contents <- ExceptT
$ liftIO
$ (Right <$> (runCpp dflags {log_action = logAction cppLogs} filename
$ if isOnDisk then Nothing else Just contents))
`catch`
( \(e :: GhcException) -> do
logs <- readIORef cppLogs
case diagsFromCPPLogs filename (reverse logs) of
[] -> throw e
diags -> return $ Left diags
)
dflags <- ExceptT $ parsePragmasIntoDynFlags filename contents
return (False, contents, dflags)
if not $ gopt Opt_Pp dflags then
return (contents, dflags)
else do
contents <- liftIO $ runPreprocessor dflags filename $ if isOnDisk then Nothing else Just contents
dflags <- ExceptT $ parsePragmasIntoDynFlags filename contents
return (contents, dflags)
where
logAction :: IORef [CPPLog] -> LogAction
logAction cppLogs dflags _reason severity srcSpan _style msg = do
let log = CPPLog severity srcSpan $ T.pack $ showSDoc dflags msg
modifyIORef cppLogs (log :)
data CPPLog = CPPLog Severity SrcSpan Text
deriving (Show)
data CPPDiag
= CPPDiag
{ cdRange :: Range,
cdSeverity :: Maybe DiagnosticSeverity,
cdMessage :: [Text]
}
deriving (Show)
diagsFromCPPLogs :: FilePath -> [CPPLog] -> [FileDiagnostic]
diagsFromCPPLogs filename logs =
map (\d -> (toNormalizedFilePath' filename, ShowDiag, cppDiagToDiagnostic d)) $
go [] logs
where
go :: [CPPDiag] -> [CPPLog] -> [CPPDiag]
go acc [] = reverse $ map (\d -> d {cdMessage = reverse $ cdMessage d}) acc
go acc (CPPLog sev (RealSrcSpan span) msg : logs) =
let diag = CPPDiag (realSrcSpanToRange span) (toDSeverity sev) [msg]
in go (diag : acc) logs
go (diag : diags) (CPPLog _sev (UnhelpfulSpan _) msg : logs) =
go (diag {cdMessage = msg : cdMessage diag} : diags) logs
go [] (CPPLog _sev (UnhelpfulSpan _) _msg : logs) = go [] logs
cppDiagToDiagnostic :: CPPDiag -> Diagnostic
cppDiagToDiagnostic d =
Diagnostic
{ _range = cdRange d,
_severity = cdSeverity d,
_code = Nothing,
_source = Just "CPP",
_message = T.unlines $ cdMessage d,
_relatedInformation = Nothing,
_tags = Nothing
}
isLiterate :: FilePath -> Bool
isLiterate x = takeExtension x `elem` [".lhs",".lhs-boot"]
parsePragmasIntoDynFlags
:: (ExceptionMonad m, HasDynFlags m, MonadIO m)
=> FilePath
-> SB.StringBuffer
-> m (Either [FileDiagnostic] DynFlags)
parsePragmasIntoDynFlags fp contents = catchSrcErrors "pragmas" $ do
dflags0 <- getDynFlags
let opts = Hdr.getOptions dflags0 contents fp
liftIO $ evaluate $ rnf opts
(dflags, _, _) <- parseDynamicFilePragma dflags0 opts
return $ disableWarningsAsErrors dflags
runLhs :: DynFlags -> FilePath -> Maybe SB.StringBuffer -> IO SB.StringBuffer
runLhs dflags filename contents = withTempDir $ \dir -> do
let fout = dir </> takeFileName filename <.> "unlit"
filesrc <- case contents of
Nothing -> return filename
Just cnts -> do
let fsrc = dir </> takeFileName filename <.> "literate"
withBinaryFile fsrc WriteMode $ \h ->
hPutStringBuffer h cnts
return fsrc
unlit filesrc fout
SB.hGetStringBuffer fout
where
unlit filein fileout = SysTools.runUnlit dflags (args filein fileout)
args filein fileout = [
SysTools.Option "-h"
, SysTools.Option (escape filename)
, SysTools.FileOption "" filein
, SysTools.FileOption "" fileout ]
escape ('\\':cs) = '\\':'\\': escape cs
escape ('\"':cs) = '\\':'\"': escape cs
escape ('\'':cs) = '\\':'\'': escape cs
escape (c:cs) = c : escape cs
escape [] = []
runCpp :: DynFlags -> FilePath -> Maybe SB.StringBuffer -> IO SB.StringBuffer
runCpp dflags filename contents = withTempDir $ \dir -> do
let out = dir </> takeFileName filename <.> "out"
dflags <- pure $ addOptP "-D__GHCIDE__" dflags
case contents of
Nothing -> do
doCpp dflags True filename out
liftIO $ SB.hGetStringBuffer out
Just contents -> do
dflags <- return $ addIncludePathsQuote (takeDirectory filename) dflags
let inp = dir </> "___GHCIDE_MAGIC___"
withBinaryFile inp WriteMode $ \h ->
hPutStringBuffer h contents
doCpp dflags True inp out
let tweak x
| Just x <- stripPrefix "# " x
, "___GHCIDE_MAGIC___" `isInfixOf` x
, let num = takeWhile (not . isSpace) x
= "# " <> num <> " \"" <> map (\x -> if isPathSeparator x then '/' else x) filename <> "\""
| otherwise = x
stringToStringBuffer . unlines . map tweak . lines <$> readFileUTF8' out
runPreprocessor :: DynFlags -> FilePath -> Maybe SB.StringBuffer -> IO SB.StringBuffer
runPreprocessor dflags filename contents = withTempDir $ \dir -> do
let out = dir </> takeFileName filename <.> "out"
inp <- case contents of
Nothing -> return filename
Just contents -> do
let inp = dir </> takeFileName filename <.> "hs"
withBinaryFile inp WriteMode $ \h ->
hPutStringBuffer h contents
return inp
runPp dflags [SysTools.Option filename, SysTools.Option inp, SysTools.FileOption "" out]
SB.hGetStringBuffer out