Skip to content

Commit 8f50699

Browse files
jinwoococreature
authored andcommitted
Collect CPP error logs into diagnostics. (#296)
* Collect CPP error logs into diagnostics. Fixes https://github.com/digital-asset/ghcide/issues/87
1 parent b78efe3 commit 8f50699

File tree

3 files changed

+98
-1
lines changed

3 files changed

+98
-1
lines changed

src/Development/IDE/Core/Preprocessor.hs

+64-1
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@ import Data.Char
1818
import DynFlags
1919
import qualified HeaderInfo as Hdr
2020
import Development.IDE.Types.Diagnostics
21+
import Development.IDE.Types.Location
2122
import Development.IDE.GHC.Error
2223
import SysTools (Option (..), runUnlit, runPp)
2324
import Control.Monad.Trans.Except
2425
import qualified GHC.LanguageExtensions as LangExt
2526
import Data.Maybe
27+
import Control.Exception.Safe (catch, throw)
28+
import Data.IORef (IORef, modifyIORef, newIORef, readIORef)
29+
import Data.Text (Text)
30+
import qualified Data.Text as T
31+
import Outputable (showSDoc)
2632

2733

2834
-- | Given a file and some contents, apply any necessary preprocessors,
@@ -46,7 +52,18 @@ preprocessor filename mbContents = do
4652
if not $ xopt LangExt.Cpp dflags then
4753
return (isOnDisk, contents, dflags)
4854
else do
49-
contents <- liftIO $ runCpp dflags filename $ if isOnDisk then Nothing else Just contents
55+
cppLogs <- liftIO $ newIORef []
56+
contents <- ExceptT
57+
$ liftIO
58+
$ (Right <$> (runCpp dflags {log_action = logAction cppLogs} filename
59+
$ if isOnDisk then Nothing else Just contents))
60+
`catch`
61+
( \(e :: GhcException) -> do
62+
logs <- readIORef cppLogs
63+
case diagsFromCPPLogs filename (reverse logs) of
64+
[] -> throw e
65+
diags -> return $ Left diags
66+
)
5067
dflags <- ExceptT $ parsePragmasIntoDynFlags filename contents
5168
return (False, contents, dflags)
5269

@@ -57,6 +74,52 @@ preprocessor filename mbContents = do
5774
contents <- liftIO $ runPreprocessor dflags filename $ if isOnDisk then Nothing else Just contents
5875
dflags <- ExceptT $ parsePragmasIntoDynFlags filename contents
5976
return (contents, dflags)
77+
where
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 :)
82+
83+
84+
data CPPLog = CPPLog Severity SrcSpan Text
85+
deriving (Show)
86+
87+
88+
data CPPDiag
89+
= CPPDiag
90+
{ cdRange :: Range,
91+
cdSeverity :: Maybe DiagnosticSeverity,
92+
cdMessage :: [Text]
93+
}
94+
deriving (Show)
95+
96+
97+
diagsFromCPPLogs :: FilePath -> [CPPLog] -> [FileDiagnostic]
98+
diagsFromCPPLogs filename logs =
99+
map (\d -> (toNormalizedFilePath filename, ShowDiag, cppDiagToDiagnostic d)) $
100+
go [] logs
101+
where
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 =
115+
Diagnostic
116+
{ _range = cdRange d,
117+
_severity = cdSeverity d,
118+
_code = Nothing,
119+
_source = Just "CPP",
120+
_message = T.unlines $ cdMessage d,
121+
_relatedInformation = Nothing
122+
}
60123

61124

62125
isLiterate :: FilePath -> Bool

src/Development/IDE/GHC/Error.hs

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ module Development.IDE.GHC.Error
1616
, srcSpanToFilename
1717
, zeroSpan
1818
, realSpan
19+
20+
-- * utilities working with severities
21+
, toDSeverity
1922
) where
2023

2124
import Development.IDE.Types.Diagnostics as D

test/exe/Main.hs

+31
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
module Main (main) where
1010

1111
import Control.Applicative.Combinators
12+
import Control.Exception (catch)
1213
import Control.Monad
1314
import Control.Monad.IO.Class (liftIO)
1415
import qualified Data.Aeson as Aeson
@@ -41,6 +42,7 @@ main = defaultMain $ testGroup "HIE"
4142
void (message :: Session WorkDoneProgressEndNotification)
4243
, initializeResponseTests
4344
, completionTests
45+
, cppTests
4446
, diagnosticTests
4547
, codeActionTests
4648
, codeLensesTests
@@ -1009,6 +1011,35 @@ pluginTests = testSessionWait "plugins" $ do
10091011
)
10101012
]
10111013

1014+
cppTests :: TestTree
1015+
cppTests =
1016+
testCase "cpp" $ do
1017+
let content =
1018+
T.unlines
1019+
[ "{-# LANGUAGE CPP #-}",
1020+
"module Testing where",
1021+
"#ifdef FOO",
1022+
"foo = 42"
1023+
]
1024+
-- The error locations differ depending on which C-preprocessor is used.
1025+
-- Some give the column number and others don't (hence -1). Assert either
1026+
-- of them.
1027+
(run $ expectError content (2, -1))
1028+
`catch` ( \e -> do
1029+
let _ = e :: HUnitFailure
1030+
run $ expectError content (2, 1)
1031+
)
1032+
where
1033+
expectError :: T.Text -> Cursor -> Session ()
1034+
expectError content cursor = do
1035+
_ <- openDoc' "Testing.hs" "haskell" content
1036+
expectDiagnostics
1037+
[ ( "Testing.hs",
1038+
[(DsError, cursor, "error: unterminated")]
1039+
)
1040+
]
1041+
expectNoMoreDiagnostics 0.5
1042+
10121043
preprocessorTests :: TestTree
10131044
preprocessorTests = testSessionWait "preprocessor" $ do
10141045
let content =

0 commit comments

Comments
 (0)