1-- Copyright (c) 2019 The DAML Authors. All rights reserved.
2-- SPDX-License-Identifier: Apache-2.0
4module Development.IDE.Core.Preprocessor
8import Development.IDE.GHC.CPP
9import Development.IDE.GHC.Orphans()
10import Development.IDE.GHC.Compat
12import StringBuffer as SB
19import qualified HeaderInfo as Hdr
20import Development.IDE.Types.Diagnostics
21import Development.IDE.Types.Location
22import Development.IDE.GHC.Error
23import SysTools (Option (..), runUnlit, runPp)
24import Control.Monad.Trans.Except
25import qualified GHC.LanguageExtensions as LangExt
27import Control.Exception.Safe (catch, throw)
28import Data.IORef (IORef, modifyIORef, newIORef, readIORef)
29import Data.Text (Text)
30import qualified Data.Text as T
31import Outputable (showSDoc)
34-- | Given a file and some contents, apply any necessary preprocessors,
35-- e.g. unlit/cpp. Return the resulting buffer and the DynFlags it implies.
36preprocessor :: GhcMonad m => FilePath -> Maybe StringBuffer -> ExceptT [FileDiagnostic] m (StringBuffer, DynFlags)
37preprocessor filename mbContents = do
39 (isOnDisk, contents) <-
40 if isLiterate filename then do
42 newcontent <- liftIO $ runLhs dflags filename mbContents
43 return (False, newcontent)
45 contents <- liftIO $ maybe (hGetStringBuffer filename) return mbContents
46 let isOnDisk = isNothing mbContents
47 return (isOnDisk, contents)
50 dflags <- ExceptT $ parsePragmasIntoDynFlags filename contents
51 (isOnDisk, contents, dflags) <-
52 if not $ xopt LangExt.Cpp dflags then
53 return (isOnDisk, contents, dflags)
55 cppLogs <- liftIO $ newIORef []
58 $ (Right <$> (runCpp dflags {log_action = logAction cppLogs} filename
59 $ if isOnDisk then Nothing else Just contents))
61 ( \(e :: GhcException) -> do
62 logs <- readIORef cppLogs
63 case diagsFromCPPLogs filename (reverse logs) of
65 diags -> return $ Left diags
67 dflags <- ExceptT $ parsePragmasIntoDynFlags filename contents
68 return (False, contents, dflags)
70 -- Perform preprocessor
71 if not $ gopt Opt_Pp dflags then
72 return (contents, dflags)
74 contents <- liftIO $ runPreprocessor dflags filename $ if isOnDisk then Nothing else Just contents
75 dflags <- ExceptT $ parsePragmasIntoDynFlags filename contents
76 return (contents, dflags)
78 logAction :: IORef [CPPLog] -> LogAction
79 logAction cppLogs dflags _reason severity srcSpan _style msg = do
80 let log = CPPLog severity srcSpan $ T.pack $ showSDoc dflags msg
81 modifyIORef cppLogs (log :)
84data CPPLog = CPPLog Severity SrcSpan Text
91 cdSeverity :: Maybe DiagnosticSeverity,
97diagsFromCPPLogs :: FilePath -> [CPPLog] -> [FileDiagnostic]
98diagsFromCPPLogs filename logs =
99 map (\d -> (toNormalizedFilePath filename, ShowDiag, cppDiagToDiagnostic d)) $
102 -- On errors, CPP calls logAction with a real span for the initial log and
103 -- then additional informational logs with `UnhelpfulSpan`. Collect those
104 -- informational log messages and attaches them to the initial log message.
105 go :: [CPPDiag] -> [CPPLog] -> [CPPDiag]
106 go acc [] = reverse $ map (\d -> d {cdMessage = reverse $ cdMessage d}) acc
107 go acc (CPPLog sev span@(RealSrcSpan _) msg : logs) =
108 let diag = CPPDiag (srcSpanToRange span) (toDSeverity sev) [msg]
109 in go (diag : acc) logs
110 go (diag : diags) (CPPLog _sev (UnhelpfulSpan _) msg : logs) =
111 go (diag {cdMessage = msg : cdMessage diag} : diags) logs
112 go [] (CPPLog _sev (UnhelpfulSpan _) _msg : logs) = go [] logs
113 cppDiagToDiagnostic :: CPPDiag -> Diagnostic
114 cppDiagToDiagnostic d =
116 { _range = cdRange d,
117 _severity = cdSeverity d,
119 _source = Just "CPP",
120 _message = T.unlines $ cdMessage d,
121 _relatedInformation = Nothing
125isLiterate :: FilePath -> Bool
126isLiterate x = takeExtension x `elem` [".lhs",".lhs-boot"]
129-- | This reads the pragma information directly from the provided buffer.
130parsePragmasIntoDynFlags
134 -> m (Either [FileDiagnostic] DynFlags)
135parsePragmasIntoDynFlags fp contents = catchSrcErrors "pragmas" $ do
136 dflags0 <- getSessionDynFlags
137 let opts = Hdr.getOptions dflags0 contents fp
138 (dflags, _, _) <- parseDynamicFilePragma dflags0 opts
142-- | Run (unlit) literate haskell preprocessor on a file, or buffer if set
143runLhs :: DynFlags -> FilePath -> Maybe SB.StringBuffer -> IO SB.StringBuffer
144runLhs dflags filename contents = withTempDir $ \dir -> do
145 let fout = dir </> takeFileName filename <.> "unlit"
146 filesrc <- case contents of
147 Nothing -> return filename
149 let fsrc = dir </> takeFileName filename <.> "literate"
150 withBinaryFile fsrc WriteMode $ \h ->
151 hPutStringBuffer h cnts
154 SB.hGetStringBuffer fout
156 unlit filein fileout = SysTools.runUnlit dflags (args filein fileout)
157 args filein fileout = [
159 , SysTools.Option (escape filename) -- name this file
160 , SysTools.FileOption "" filein -- input file
161 , SysTools.FileOption "" fileout ] -- output file
162 -- taken from ghc's DriverPipeline.hs
163 escape ('\\':cs) = '\\':'\\': escape cs
164 escape ('\"':cs) = '\\':'\"': escape cs
165 escape ('\'':cs) = '\\':'\'': escape cs
166 escape (c:cs) = c : escape cs
170-- | Run CPP on a file
171runCpp :: DynFlags -> FilePath -> Maybe SB.StringBuffer -> IO SB.StringBuffer
172runCpp dflags filename contents = withTempDir $ \dir -> do
173 let out = dir </> takeFileName filename <.> "out"
176 -- Happy case, file is not modified, so run CPP on it in-place
177 -- which also makes things like relative #include files work
178 -- and means location information is correct
179 doCpp dflags True filename out
180 liftIO $ SB.hGetStringBuffer out
183 -- Sad path, we have to create a version of the path in a temp dir
184 -- __FILE__ macro is wrong, ignoring that for now (likely not a real issue)
186 -- Relative includes aren't going to work, so we fix that by adding to the include path.
187 dflags <- return $ addIncludePathsQuote (takeDirectory filename) dflags
189 -- Location information is wrong, so we fix that by patching it afterwards.
190 let inp = dir </> "___GHCIDE_MAGIC___"
191 withBinaryFile inp WriteMode $ \h ->
192 hPutStringBuffer h contents
193 doCpp dflags True inp out
195 -- Fix up the filename in lines like:
196 -- # 1 "C:/Temp/extra-dir-914611385186/___GHCIDE_MAGIC___"
198 | Just x <- stripPrefix "# " x
199 , "___GHCIDE_MAGIC___" `isInfixOf` x
200 , let num = takeWhile (not . isSpace) x
201 -- important to use /, and never \ for paths, even on Windows, since then C escapes them
202 -- and GHC gets all confused
203 = "# " <> num <> " \"" <> map (\x -> if isPathSeparator x then '/' else x) filename <> "\""
205 stringToStringBuffer . unlines . map tweak . lines <$> readFileUTF8' out
208-- | Run a preprocessor on a file
209runPreprocessor :: DynFlags -> FilePath -> Maybe SB.StringBuffer -> IO SB.StringBuffer
210runPreprocessor dflags filename contents = withTempDir $ \dir -> do
211 let out = dir </> takeFileName filename <.> "out"
212 inp <- case contents of
213 Nothing -> return filename
215 let inp = dir </> takeFileName filename <.> "hs"
216 withBinaryFile inp WriteMode $ \h ->
217 hPutStringBuffer h contents
219 runPp dflags [SysTools.Option filename, SysTools.Option inp, SysTools.FileOption "" out]
220 SB.hGetStringBuffer out
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list