moonlight-pale-0.1.0.0: src-diagnostic-ghc/Moonlight/Pale/TestSupport/CompileHieFixture.hs
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TypeApplications #-}
{-| Isolated compilation of Haskell source into HIE artifacts and name oracles. -}
module Moonlight.Pale.TestSupport.CompileHieFixture
( HieFixtureModuleName,
mkHieFixtureModuleName,
CompileHieFixtureFailure (..),
CompiledHieFixture (..),
compileHieFixture,
)
where
import Control.Exception (IOException, displayException, try)
import Data.ByteString (ByteString)
import Data.ByteString qualified as ByteString
import Data.Char (isAlphaNum, isUpper)
import Data.Kind (Type)
import Data.List (sort)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NonEmpty
import Data.Text (Text)
import Data.Text qualified as Text
import Moonlight.Pale.Ghc.Hie.Oracle (ModuleNameOracle (..))
import Moonlight.Pale.Ghc.Hie.Read (HieReadError, indexHieRoots)
import Moonlight.Pale.Ghc.Hie.SourceKey
( HieSourceKeyKind,
hieArtifactOracle,
OracleLookup (..),
OracleQuery (..),
TriedKey,
lookupModuleOracle,
)
import System.Directory
( canonicalizePath,
createDirectoryIfMissing,
doesDirectoryExist,
findExecutable,
listDirectory,
makeAbsolute,
)
import System.Exit (ExitCode (..))
import System.FilePath
( joinPath,
normalise,
takeDirectory,
takeExtension,
(<.>),
(</>),
)
import System.IO (IOMode (WriteMode), withBinaryFile)
import System.IO.Temp (withSystemTempDirectory)
import System.Process
( CreateProcess (std_err, std_out),
StdStream (UseHandle),
proc,
waitForProcess,
withCreateProcess,
)
type HieFixtureModuleName :: Type
newtype HieFixtureModuleName = HieFixtureModuleName (NonEmpty Text)
deriving stock (Eq, Show)
type CompileHieFixtureFailure :: Type
data CompileHieFixtureFailure
= CompileHieFixtureInvalidModuleName !String
| CompileHieFixtureGhcNotFound
| CompileHieFixtureProcessLaunchFailed !FilePath !(NonEmpty String) !String
| CompileHieFixtureProcessFailed !FilePath !(NonEmpty String) !ExitCode !ByteString !ByteString
| CompileHieFixtureHieDecoderFailed !(NonEmpty HieReadError)
| CompileHieFixtureOracleMissing ![TriedKey]
| CompileHieFixtureOracleAmbiguous !HieSourceKeyKind !FilePath ![FilePath]
| CompileHieFixtureOracleIndexObstruction ![Int]
| CompileHieFixtureHieFileMissing !FilePath
| CompileHieFixtureMultipleHieFiles !FilePath !FilePath ![FilePath]
| CompileHieFixtureSourcePathDisagreement !FilePath !FilePath
| CompileHieFixtureIoFailed !String
deriving stock (Eq, Show)
type CompiledHieFixture :: Type
data CompiledHieFixture = CompiledHieFixture
{ compiledHieFixtureSourcePath :: !FilePath,
compiledHieFixtureSourceBytes :: !ByteString,
compiledHieFixtureHiePath :: !FilePath,
compiledHieFixtureHieBytes :: !ByteString,
compiledHieFixtureOracle :: !ModuleNameOracle,
compiledHieFixtureGhcPath :: !FilePath,
compiledHieFixtureGhcArguments :: !(NonEmpty String),
compiledHieFixtureStdout :: !ByteString,
compiledHieFixtureStderr :: !ByteString
}
deriving stock (Eq, Show)
mkHieFixtureModuleName :: String -> Either CompileHieFixtureFailure HieFixtureModuleName
mkHieFixtureModuleName rawModuleName =
case NonEmpty.nonEmpty (Text.splitOn (Text.singleton '.') (Text.pack rawModuleName)) of
Just moduleComponents
| all validModuleComponent (NonEmpty.toList moduleComponents) ->
Right (HieFixtureModuleName moduleComponents)
_ ->
Left (CompileHieFixtureInvalidModuleName rawModuleName)
compileHieFixture ::
HieFixtureModuleName ->
ByteString ->
IO (Either CompileHieFixtureFailure CompiledHieFixture)
compileHieFixture moduleName sourceBytes =
captureIoFailure $ do
maybeGhcPath <- findExecutable "ghc"
case maybeGhcPath of
Nothing ->
pure (Left CompileHieFixtureGhcNotFound)
Just discoveredGhcPath -> do
ghcPath <- makeAbsolute discoveredGhcPath
withSystemTempDirectory "moonlight-pale-hie-fixture" $ \temporaryRoot -> do
canonicalRoot <- canonicalizePath temporaryRoot
compileHieFixtureAtRoot ghcPath moduleName sourceBytes canonicalRoot
captureIoFailure ::
IO (Either CompileHieFixtureFailure fixture) ->
IO (Either CompileHieFixtureFailure fixture)
captureIoFailure action = do
result <- try @IOException action
pure
( case result of
Left ioFailure -> Left (CompileHieFixtureIoFailed (displayException ioFailure))
Right fixtureResult -> fixtureResult
)
compileHieFixtureAtRoot ::
FilePath ->
HieFixtureModuleName ->
ByteString ->
FilePath ->
IO (Either CompileHieFixtureFailure CompiledHieFixture)
compileHieFixtureAtRoot ghcPath moduleName sourceBytes temporaryRoot = do
let sourceDirectory = temporaryRoot </> "src"
hieDirectory = temporaryRoot </> "hie"
sourcePath = sourceDirectory </> moduleSourcePath moduleName
stdoutPath = temporaryRoot </> "ghc.stdout"
stderrPath = temporaryRoot </> "ghc.stderr"
ghcArguments =
"-fno-code"
:| [ "-fforce-recomp",
"-fwrite-ide-info",
"-hiedir",
hieDirectory,
sourcePath
]
createDirectoryIfMissing True (takeDirectory sourcePath)
createDirectoryIfMissing True hieDirectory
ByteString.writeFile sourcePath sourceBytes
processResult <- runGhcProcess ghcPath ghcArguments stdoutPath stderrPath
stdoutBytes <- ByteString.readFile stdoutPath
stderrBytes <- ByteString.readFile stderrPath
case processResult of
Left processLaunchFailure ->
pure (Left processLaunchFailure)
Right exitCode@(ExitFailure _) ->
pure
( Left
( CompileHieFixtureProcessFailed
ghcPath
ghcArguments
exitCode
stdoutBytes
stderrBytes
)
)
Right ExitSuccess ->
decodeCompiledFixture
ghcPath
ghcArguments
sourceDirectory
sourcePath
hieDirectory
stdoutBytes
stderrBytes
runGhcProcess ::
FilePath ->
NonEmpty String ->
FilePath ->
FilePath ->
IO (Either CompileHieFixtureFailure ExitCode)
runGhcProcess ghcPath ghcArguments stdoutPath stderrPath =
withBinaryFile stdoutPath WriteMode $ \stdoutHandle ->
withBinaryFile stderrPath WriteMode $ \stderrHandle -> do
processResult <-
try @IOException
( withCreateProcess
( (proc ghcPath (NonEmpty.toList ghcArguments))
{ std_out = UseHandle stdoutHandle,
std_err = UseHandle stderrHandle
}
)
(\_ _ _ processHandle -> waitForProcess processHandle)
)
pure
( case processResult of
Left processFailure ->
Left
( CompileHieFixtureProcessLaunchFailed
ghcPath
ghcArguments
(displayException processFailure)
)
Right exitCode ->
Right exitCode
)
decodeCompiledFixture ::
FilePath ->
NonEmpty String ->
FilePath ->
FilePath ->
FilePath ->
ByteString ->
ByteString ->
IO (Either CompileHieFixtureFailure CompiledHieFixture)
decodeCompiledFixture ghcPath ghcArguments sourceDirectory sourcePath hieDirectory stdoutBytes stderrBytes = do
hieFiles <- collectHieFiles hieDirectory
case hieFiles of
[] ->
pure (Left (CompileHieFixtureHieFileMissing hieDirectory))
firstHiePath : secondHiePath : remainingHiePaths ->
pure
( Left
( CompileHieFixtureMultipleHieFiles
firstHiePath
secondHiePath
remainingHiePaths
)
)
[hiePath] -> do
(hieReadErrors, oracleIndex) <- indexHieRoots [hieDirectory]
case NonEmpty.nonEmpty hieReadErrors of
Just decoderFailures ->
pure (Left (CompileHieFixtureHieDecoderFailed decoderFailures))
Nothing ->
retainSelectedFixture
ghcPath
ghcArguments
sourcePath
hiePath
stdoutBytes
stderrBytes
( lookupModuleOracle
oracleIndex
OracleQuery
{ oqGivenPath = normalise sourcePath,
oqAbsolutePath = Just (normalise sourcePath),
oqSourceRoots = [normalise sourceDirectory]
}
)
retainSelectedFixture ::
FilePath ->
NonEmpty String ->
FilePath ->
FilePath ->
ByteString ->
ByteString ->
OracleLookup ->
IO (Either CompileHieFixtureFailure CompiledHieFixture)
retainSelectedFixture ghcPath ghcArguments sourcePath hiePath stdoutBytes stderrBytes oracleLookup =
case oracleLookup of
OracleMissing triedKeys ->
pure (Left (CompileHieFixtureOracleMissing triedKeys))
OracleAmbiguous keyKind keyValue candidates ->
pure (Left (CompileHieFixtureOracleAmbiguous keyKind keyValue candidates))
OracleIndexObstruction missingOracleIds ->
pure (Left (CompileHieFixtureOracleIndexObstruction missingOracleIds))
OracleFound _ artifact
| normalise (mnoSourcePath (hieArtifactOracle artifact)) /= normalise sourcePath ->
pure
( Left
( CompileHieFixtureSourcePathDisagreement
(normalise sourcePath)
(normalise (mnoSourcePath (hieArtifactOracle artifact)))
)
)
| otherwise -> do
retainedSourceBytes <- ByteString.readFile sourcePath
retainedHieBytes <- ByteString.readFile hiePath
pure
( Right
CompiledHieFixture
{ compiledHieFixtureSourcePath = normalise sourcePath,
compiledHieFixtureSourceBytes = retainedSourceBytes,
compiledHieFixtureHiePath = normalise hiePath,
compiledHieFixtureHieBytes = retainedHieBytes,
compiledHieFixtureOracle = hieArtifactOracle artifact,
compiledHieFixtureGhcPath = ghcPath,
compiledHieFixtureGhcArguments = ghcArguments,
compiledHieFixtureStdout = stdoutBytes,
compiledHieFixtureStderr = stderrBytes
}
)
collectHieFiles :: FilePath -> IO [FilePath]
collectHieFiles directory = do
entries <- sort <$> listDirectory directory
concat <$> traverse (collectHiePath . (directory </>)) entries
collectHiePath :: FilePath -> IO [FilePath]
collectHiePath path = do
pathIsDirectory <- doesDirectoryExist path
if pathIsDirectory
then collectHieFiles path
else pure [normalise path | takeExtension path == ".hie"]
moduleSourcePath :: HieFixtureModuleName -> FilePath
moduleSourcePath (HieFixtureModuleName moduleComponents) =
joinPath (fmap Text.unpack (NonEmpty.toList moduleComponents)) <.> "hs"
validModuleComponent :: Text -> Bool
validModuleComponent moduleComponent =
case Text.uncons moduleComponent of
Just (initialCharacter, remainingCharacters) ->
isUpper initialCharacter
&& Text.all validModuleContinuationCharacter remainingCharacters
Nothing ->
False
validModuleContinuationCharacter :: Char -> Bool
validModuleContinuationCharacter character =
isAlphaNum character || character == '_' || character == '\''