Skip to content

Commit 87c68e1

Browse files
hsenaggaryverhaegen-da
authored andcommitted
ghcide: support hs-boot files (#2827)
1 parent 6176093 commit 87c68e1

File tree

5 files changed

+80
-24
lines changed

5 files changed

+80
-24
lines changed

extension/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"languages": [{
2323
"id": "haskell",
2424
"extensions": [
25-
"hs"
25+
"hs",
26+
"hs-boot"
2627
]
2728
}],
2829
"configuration": {

src/Development/IDE/Core/Compile.hs

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import qualified GHC.LanguageExtensions as LangExt
4545
import Control.Monad.Extra
4646
import Control.Monad.Except
4747
import Control.Monad.Trans.Except
48+
import Data.Function
49+
import Data.Ord
4850
import qualified Data.Text as T
4951
import Data.IORef
5052
import Data.List.Extra
@@ -149,7 +151,12 @@ mkTcModuleResult tcm = do
149151
-- | Setup the environment that GHC needs according to our
150152
-- best understanding (!)
151153
setupEnv :: GhcMonad m => [TcModuleResult] -> m ()
152-
setupEnv tms = do
154+
setupEnv tmsIn = do
155+
-- if both a .hs-boot file and a .hs file appear here, we want to make sure that the .hs file
156+
-- takes precedence, so put the .hs-boot file earlier in the list
157+
let isSourceFile = (==HsBootFile) . ms_hsc_src . pm_mod_summary . tm_parsed_module . tmrModule
158+
tms = sortBy (compare `on` Down . isSourceFile) tmsIn
159+
153160
session <- getSession
154161

155162
let mss = map (pm_mod_summary . tm_parsed_module . tmrModule) tms
@@ -191,24 +198,17 @@ loadModuleHome tmr = modifySession $ \e ->
191198
-- name and its imports.
192199
getImportsParsed :: DynFlags ->
193200
GHC.ParsedSource ->
194-
Either [FileDiagnostic] (GHC.ModuleName, [(Maybe FastString, Located GHC.ModuleName)])
201+
Either [FileDiagnostic] (GHC.ModuleName, [(Bool, (Maybe FastString, Located GHC.ModuleName))])
195202
getImportsParsed dflags (L loc parsed) = do
196203
let modName = maybe (GHC.mkModuleName "Main") GHC.unLoc $ GHC.hsmodName parsed
197204

198-
-- refuse source imports
199-
let srcImports = filter (ideclSource . GHC.unLoc) $ GHC.hsmodImports parsed
200-
when (not $ null srcImports) $ Left $
201-
concat
202-
[ diagFromString "imports" mloc ("Illegal source import of " <> GHC.moduleNameString (GHC.unLoc $ GHC.ideclName i))
203-
| L mloc i <- srcImports ]
204-
205205
-- most of these corner cases are also present in https://hackage.haskell.org/package/ghc-8.6.1/docs/src/HeaderInfo.html#getImports
206206
-- but we want to avoid parsing the module twice
207207
let implicit_prelude = xopt GHC.ImplicitPrelude dflags
208208
implicit_imports = Hdr.mkPrelImports modName loc implicit_prelude $ GHC.hsmodImports parsed
209209

210210
-- filter out imports that come from packages
211-
return (modName, [(fmap sl_fs $ ideclPkgQual i, ideclName i)
211+
return (modName, [(ideclSource i, (fmap sl_fs $ ideclPkgQual i, ideclName i))
212212
| i <- map GHC.unLoc $ implicit_imports ++ GHC.hsmodImports parsed
213213
, GHC.moduleNameString (GHC.unLoc $ ideclName i) /= "GHC.Prim"
214214
])
@@ -227,10 +227,10 @@ getModSummaryFromBuffer fp contents dflags parsed = do
227227

228228
let modLoc = ModLocation
229229
{ ml_hs_file = Just fp
230-
, ml_hi_file = replaceExtension fp "hi"
231-
, ml_obj_file = replaceExtension fp "o"
230+
, ml_hi_file = derivedFile "hi"
231+
, ml_obj_file = derivedFile "o"
232232
#ifndef GHC_STABLE
233-
, ml_hie_file = replaceExtension fp "hie"
233+
, ml_hie_file = derivedFile "hie"
234234
#endif
235235
-- This does not consider the dflags configuration
236236
-- (-osuf and -hisuf, object and hi dir.s).
@@ -245,21 +245,27 @@ getModSummaryFromBuffer fp contents dflags parsed = do
245245
-- To avoid silent issues where something is not processed because the date
246246
-- has not changed, we make sure that things blow up if they depend on the
247247
-- date.
248-
, ms_textual_imps = imports
248+
, ms_textual_imps = [imp | (False, imp) <- imports]
249249
, ms_hspp_file = fp
250250
, ms_hspp_opts = dflags
251251
, ms_hspp_buf = Just contents
252252

253253
-- defaults:
254-
, ms_hsc_src = HsSrcFile
254+
, ms_hsc_src = sourceType
255255
, ms_obj_date = Nothing
256256
, ms_iface_date = Nothing
257257
#ifndef GHC_STABLE
258258
, ms_hie_date = Nothing
259259
#endif
260-
, ms_srcimps = [] -- source imports are not allowed
260+
, ms_srcimps = [imp | (True, imp) <- imports]
261261
, ms_parsed_mod = Nothing
262262
}
263+
where
264+
(sourceType, derivedFile) =
265+
let (stem, ext) = splitExtension fp in
266+
if "-boot" `isSuffixOf` ext
267+
then (HsBootFile, \newExt -> stem <.> newExt ++ "-boot")
268+
else (HsSrcFile , \newExt -> stem <.> newExt)
263269

264270
-- | Run CPP on a file
265271
runCpp :: DynFlags -> FilePath -> Maybe SB.StringBuffer -> IO SB.StringBuffer

src/Development/IDE/Core/Rules.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,12 @@ getLocatedImportsRule =
183183
define $ \GetLocatedImports file -> do
184184
pm <- use_ GetParsedModule file
185185
let ms = pm_mod_summary pm
186-
let imports = ms_textual_imps ms
186+
let imports = [(False, imp) | imp <- ms_textual_imps ms] ++ [(True, imp) | imp <- ms_srcimps ms]
187187
env <- useNoFile_ GhcSession
188188
let dflags = addRelativeImport pm $ hsc_dflags env
189189
opt <- getIdeOptions
190-
(diags, imports') <- fmap unzip $ forM imports $ \(mbPkgName, modName) -> do
191-
diagOrImp <- locateModule dflags (optExtensions opt) getFileExists modName mbPkgName
190+
(diags, imports') <- fmap unzip $ forM imports $ \(isSource, (mbPkgName, modName)) -> do
191+
diagOrImp <- locateModule dflags (optExtensions opt) getFileExists modName mbPkgName isSource
192192
case diagOrImp of
193193
Left diags -> pure (diags, Left (modName, Nothing))
194194
Right (FileImport path) -> pure ([], Left (modName, Just path))

src/Development/IDE/Import/FindImports.hs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,18 @@ locateModuleFile :: MonadIO m
4242
=> DynFlags
4343
-> [String]
4444
-> (NormalizedFilePath -> m Bool)
45+
-> Bool
4546
-> ModuleName
4647
-> m (Maybe NormalizedFilePath)
47-
locateModuleFile dflags exts doesExist modName = do
48-
let candidates = [ toNormalizedFilePath (prefix </> M.moduleNameSlashes modName <.> ext) | prefix <- importPaths dflags, ext <- exts]
48+
locateModuleFile dflags exts doesExist isSource modName = do
49+
let candidates =
50+
[ toNormalizedFilePath (prefix </> M.moduleNameSlashes modName <.> maybeBoot ext)
51+
| prefix <- importPaths dflags, ext <- exts]
4952
findM doesExist candidates
53+
where
54+
maybeBoot ext
55+
| isSource = ext ++ "-boot"
56+
| otherwise = ext
5057

5158
-- | locate a module in either the file system or the package database. Where we go from *daml to
5259
-- Haskell
@@ -57,15 +64,16 @@ locateModule
5764
-> (NormalizedFilePath -> m Bool)
5865
-> Located ModuleName
5966
-> Maybe FastString
67+
-> Bool
6068
-> m (Either [FileDiagnostic] Import)
61-
locateModule dflags exts doesExist modName mbPkgName = do
69+
locateModule dflags exts doesExist modName mbPkgName isSource = do
6270
case mbPkgName of
6371
-- if a package name is given we only go look for a package
6472
Just _pkgName -> lookupInPackageDB dflags
6573
Nothing -> do
6674
-- first try to find the module as a file. If we can't find it try to find it in the package
6775
-- database.
68-
mbFile <- locateModuleFile dflags exts doesExist $ unLoc modName
76+
mbFile <- locateModuleFile dflags exts doesExist isSource $ unLoc modName
6977
case mbFile of
7078
Nothing -> lookupInPackageDB dflags
7179
Just file -> return $ Right $ FileImport file

test/exe/Main.hs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,47 @@ diagnosticTests = testGroup "diagnostics"
125125
, [(DsError, (1, 7), "Cyclic module dependency between ModuleA, ModuleB")]
126126
)
127127
]
128+
, testSession "cyclic module dependency with hs-boot" $ do
129+
let contentA = T.unlines
130+
[ "module ModuleA where"
131+
, "import {-# SOURCE #-} ModuleB"
132+
]
133+
let contentB = T.unlines
134+
[ "module ModuleB where"
135+
, "import ModuleA"
136+
]
137+
let contentBboot = T.unlines
138+
[ "module ModuleB where"
139+
]
140+
_ <- openDoc' "ModuleA.hs" "haskell" contentA
141+
_ <- openDoc' "ModuleB.hs" "haskell" contentB
142+
_ <- openDoc' "ModuleB.hs-boot" "haskell" contentBboot
143+
expectDiagnostics []
144+
, testSession "correct reference used with hs-boot" $ do
145+
let contentB = T.unlines
146+
[ "module ModuleB where"
147+
, "import {-# SOURCE #-} ModuleA"
148+
]
149+
let contentA = T.unlines
150+
[ "module ModuleA where"
151+
, "import ModuleB"
152+
, "x = 5"
153+
]
154+
let contentAboot = T.unlines
155+
[ "module ModuleA where"
156+
]
157+
let contentC = T.unlines
158+
[ "module ModuleC where"
159+
, "import ModuleA"
160+
-- this reference will fail if it gets incorrectly
161+
-- resolved to the hs-boot file
162+
, "y = x"
163+
]
164+
_ <- openDoc' "ModuleB.hs" "haskell" contentB
165+
_ <- openDoc' "ModuleA.hs" "haskell" contentA
166+
_ <- openDoc' "ModuleA.hs-boot" "haskell" contentAboot
167+
_ <- openDoc' "ModuleC.hs" "haskell" contentC
168+
expectDiagnostics []
128169
, testSession "redundant import" $ do
129170
let contentA = T.unlines ["module ModuleA where"]
130171
let contentB = T.unlines

0 commit comments

Comments
 (0)