packages feed

moonlight-pale-0.1.0.0: src-ghc-surface/Moonlight/Pale/Ghc/ModuleSurface.hs

{-| GHC-parsed module identities, imports, and exports. -}
module Moonlight.Pale.Ghc.ModuleSurface
  ( ParsedModuleName,
    mkParsedModuleName,
    unParsedModuleName,
    ParsedName,
    mkParsedName,
    unParsedName,
    ModuleSurface (..),
    ModuleSurfaceError (..),
    ExportSpec (..),
    ExportItem (..),
    ExportChildSpec (..),
    explicitExportNames,
    GhcParseFailure (..),
    renderGhcParseFailure,
    parseWithGhcParser,
    parseHsModule,
    moduleIdentity,
    moduleImportNames,
    moduleExportNames,
    moduleExportIdentifiers,
    exportedIdentifier,
    wrappedNameIdentifier,
    rdrNameIdentifier,
    moduleSurfaceFromGhcPs,
  )
where

import Data.Kind (Type)
import Data.Function ((&))
import Data.List (find)
import Data.Maybe (mapMaybe)
import Data.Set (Set)
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified GHC.Data.EnumSet as EnumSet
import GHC.Data.FastString (mkFastString)
import GHC.Data.StringBuffer (StringBuffer, stringToStringBuffer)
import GHC.Driver.DynFlags (Language (..), languageExtensions)
import GHC.Driver.Flags (OnOff (..), WarningFlag, impliedXFlags)
import GHC.Driver.Session (flagSpecFlag, flagSpecName, xFlags)
import GHC.Hs
  ( GhcPs,
    HsModule (..),
    IE (..),
    IEWildcard (..),
    IEWrappedName (..),
    ImportDecl (..),
    LIE,
    LIEWrappedName,
  )
import GHC.LanguageExtensions.Type (Extension)
import GHC.Parser (parseModule)
import GHC.Parser.Errors.Ppr ()
import GHC.Parser.Header (getOptions)
import GHC.Parser.Lexer
  ( P (..),
    ParseResult (..),
    PState,
    getPsErrorMessages,
    initParserState,
    mkParserOpts,
  )
import GHC.Types.Name.Occurrence (occNameString)
import GHC.Types.Name.Reader (RdrName, rdrNameOcc)
import GHC.Types.SrcLoc (GenLocated, mkRealSrcLoc, unLoc)
import GHC.Types.Error (defaultOpts, isEmptyMessages)
import GHC.Unit.Module.Warnings (emptyWarningCategorySet)
import GHC.Utils.Error
  ( DiagOpts (..),
    pprMessages,
  )
import GHC.Utils.Outputable (defaultSDocContext, showSDocUnsafe)
import Language.Haskell.Syntax.Module.Name (moduleNameString)
import Moonlight.Core (IdentifierToken, mkIdentifierTokenWith, renderIdentifierToken)
import Moonlight.Core (isCompactName, isQualifiedModuleName)

type ParsedModuleNameNamespace :: Type
data ParsedModuleNameNamespace

type ParsedModuleName :: Type
newtype ParsedModuleName = ParsedModuleName (IdentifierToken ParsedModuleNameNamespace)
  deriving stock (Eq, Ord, Show)

type ParsedNameNamespace :: Type
data ParsedNameNamespace

type ParsedName :: Type
newtype ParsedName = ParsedName (IdentifierToken ParsedNameNamespace)
  deriving stock (Eq, Ord, Show)

type ModuleSurface :: Type
data ModuleSurface = ModuleSurface
  { surfaceModuleName :: Maybe ParsedModuleName,
    surfaceImportedModules :: Set ParsedModuleName,
    surfaceExports :: !ExportSpec
  }
  deriving stock (Eq, Show)

type ExportSpec :: Type
data ExportSpec
  = ImplicitExports
  | ExplicitExports ![ExportItem]
  deriving stock (Eq, Ord, Show)

type ExportItem :: Type
data ExportItem
  = ExportValue !ParsedName
  | ExportType !ParsedName !ExportChildSpec
  | ExportPattern !ParsedName
  | ExportModule !ParsedModuleName
  deriving stock (Eq, Ord, Show)

type ExportChildSpec :: Type
data ExportChildSpec
  = NoExportedChildren
  | AllExportedChildren
  | ExplicitExportedChildren ![ParsedName]
  deriving stock (Eq, Ord, Show)

type ModuleSurfaceError :: Type
data ModuleSurfaceError
  = InvalidSurfaceModuleName !String
  | InvalidImportedModuleName !String
  | InvalidExportedName !String
  | InvalidReexportedModuleName !String
  deriving stock (Eq, Ord, Show)

type GhcParseFailure :: Type
data GhcParseFailure
  = LanguagePragmaHeaderRejected !FilePath !String
  | SourceParseRejected !FilePath !String
  deriving stock (Eq, Ord, Show)

renderGhcParseFailure :: GhcParseFailure -> String
renderGhcParseFailure = \case
  LanguagePragmaHeaderRejected sourcePath rendered ->
    sourcePath <> ": malformed LANGUAGE/OPTIONS_GHC header\n" <> rendered
  SourceParseRejected sourcePath rendered ->
    sourcePath <> ": parse failure\n" <> rendered

mkParsedModuleName :: String -> Maybe ParsedModuleName
mkParsedModuleName =
  fmap ParsedModuleName . mkIdentifierTokenWith isQualifiedModuleName . Text.pack

unParsedModuleName :: ParsedModuleName -> String
unParsedModuleName (ParsedModuleName identifierToken) =
  Text.unpack (renderIdentifierToken identifierToken)

mkParsedName :: String -> Maybe ParsedName
mkParsedName =
  fmap ParsedName . mkIdentifierTokenWith isCompactName . Text.pack

unParsedName :: ParsedName -> String
unParsedName (ParsedName identifierToken) =
  Text.unpack (renderIdentifierToken identifierToken)

parseHsModule :: FilePath -> String -> Either GhcParseFailure (HsModule GhcPs)
parseHsModule sourcePath moduleContents =
  unLoc <$> parseWithGhcParser sourcePath moduleContents parseModule

parseWithGhcParser :: FilePath -> String -> P a -> Either GhcParseFailure a
parseWithGhcParser sourcePath sourceContents parser = do
  enabledExtensions <- parserExtensions sourcePath sourceBuffer
  let parserState =
        initParserState
          (mkParserOpts enabledExtensions parserDiagOpts False False False False)
          sourceBuffer
          (mkRealSrcLoc (mkFastString sourcePath) 1 1)
  case unP parser parserState of
    POk _ parsedValue -> Right parsedValue
    PFailed parserStateValue ->
      Left (SourceParseRejected sourcePath (renderParseFailure parserStateValue))
  where
    sourceBuffer = stringToStringBuffer sourceContents

moduleIdentity :: HsModule GhcPs -> Either ModuleSurfaceError (Maybe String)
moduleIdentity =
  fmap (fmap unParsedModuleName . surfaceModuleName)
    . moduleSurfaceFromGhcPs

moduleImportNames :: HsModule GhcPs -> Either ModuleSurfaceError (Set String)
moduleImportNames =
  fmap (Set.map unParsedModuleName . surfaceImportedModules)
    . moduleSurfaceFromGhcPs

moduleExportNames :: HsModule GhcPs -> Either ModuleSurfaceError (Maybe (Set String))
moduleExportNames =
  fmap
    (fmap (Set.map unParsedName) . explicitExportNames . surfaceExports)
    . moduleSurfaceFromGhcPs

moduleExportIdentifiers :: Maybe (GenLocated l [LIE GhcPs]) -> Either ModuleSurfaceError (Maybe (Set String))
moduleExportIdentifiers maybeExports =
  case maybeExports of
    Nothing -> Right Nothing
    Just exports ->
      Just . Set.fromList
        <$> traverse exportedIdentifier (unLoc exports)

moduleSurfaceFromGhcPs :: HsModule GhcPs -> Either ModuleSurfaceError ModuleSurface
moduleSurfaceFromGhcPs moduleAst =
  ModuleSurface
    <$> traverse checkedSurfaceModuleName (moduleNameString . unLoc <$> hsmodName moduleAst)
    <*> (Set.fromList <$> traverse checkedImportedModuleName importedNames)
    <*> traverseExportSpec (hsmodExports moduleAst)
  where
    importedNames =
      fmap (moduleNameString . unLoc . ideclName . unLoc) (hsmodImports moduleAst)

    checkedSurfaceModuleName rawName =
      maybe (Left (InvalidSurfaceModuleName rawName)) Right (mkParsedModuleName rawName)

    checkedImportedModuleName rawName =
      maybe (Left (InvalidImportedModuleName rawName)) Right (mkParsedModuleName rawName)

traverseExportSpec :: Maybe (GenLocated l [LIE GhcPs]) -> Either ModuleSurfaceError ExportSpec
traverseExportSpec =
  maybe
    (Right ImplicitExports)
    (fmap ExplicitExports . traverse exportItem . unLoc)

exportedIdentifier :: LIE GhcPs -> Either ModuleSurfaceError String
exportedIdentifier =
  fmap exportItemIdentifier . exportItem

exportItem :: LIE GhcPs -> Either ModuleSurfaceError ExportItem
exportItem exportEntry =
  case unLoc exportEntry of
    IEVar _ wrappedNameValue _ ->
      exportWrappedName wrappedNameValue
    IEThingAbs _ wrappedNameValue _ ->
      ExportType <$> checkedWrappedName wrappedNameValue <*> pure NoExportedChildren
    IEThingAll _ wrappedNameValue _ ->
      ExportType <$> checkedWrappedName wrappedNameValue <*> pure AllExportedChildren
    IEThingWith _ wrappedNameValue wildcardValue childNames _ ->
      ExportType
        <$> checkedWrappedName wrappedNameValue
        <*> case wildcardValue of
          NoIEWildcard ->
            ExplicitExportedChildren <$> traverse checkedWrappedName childNames
          IEWildcard _ ->
            pure AllExportedChildren
    IEModuleContents _ moduleName ->
      let rawName = moduleNameString (unLoc moduleName)
       in maybe
            (Left (InvalidReexportedModuleName rawName))
            (Right . ExportModule)
            (mkParsedModuleName rawName)
    IEGroup {} ->
      Left (InvalidExportedName "<documentation-group>")
    IEDoc {} ->
      Left (InvalidExportedName "<documentation>")
    IEDocNamed {} ->
      Left (InvalidExportedName "<named-documentation>")

exportWrappedName :: LIEWrappedName GhcPs -> Either ModuleSurfaceError ExportItem
exportWrappedName wrappedNameValue =
  case unLoc wrappedNameValue of
    IEPattern {} -> ExportPattern <$> checkedWrappedName wrappedNameValue
    IEType {} -> ExportType <$> checkedWrappedName wrappedNameValue <*> pure NoExportedChildren
    IEData {} -> ExportType <$> checkedWrappedName wrappedNameValue <*> pure NoExportedChildren
    _ -> ExportValue <$> checkedWrappedName wrappedNameValue

wrappedNameIdentifier :: LIEWrappedName GhcPs -> Either ModuleSurfaceError String
wrappedNameIdentifier =
  fmap unParsedName . checkedWrappedName

checkedWrappedName :: LIEWrappedName GhcPs -> Either ModuleSurfaceError ParsedName
checkedWrappedName wrappedNameValue =
  maybe
    (Left (InvalidExportedName rawName))
    Right
    (mkParsedName rawName)
  where
    rawName = rdrNameIdentifier (unLoc wrappedLocatedName)
    wrappedLocatedName =
      case unLoc wrappedNameValue of
        IEName _ name -> name
        IEPattern _ name -> name
        IEType _ name -> name
        IEDefault _ name -> name
        IEData _ name -> name

explicitExportNames :: ExportSpec -> Maybe (Set ParsedName)
explicitExportNames = \case
  ImplicitExports ->
    Nothing
  ExplicitExports exportItems ->
    Just (Set.fromList (foldMap exportItemNames exportItems))

exportItemNames :: ExportItem -> [ParsedName]
exportItemNames = \case
  ExportValue parsedName -> [parsedName]
  ExportType parsedName childSpec -> parsedName : childNames childSpec
  ExportPattern parsedName -> [parsedName]
  ExportModule _ -> []
  where
    childNames = \case
      ExplicitExportedChildren names -> names
      NoExportedChildren -> []
      AllExportedChildren -> []

exportItemIdentifier :: ExportItem -> String
exportItemIdentifier = \case
  ExportValue parsedName -> unParsedName parsedName
  ExportType parsedName _ -> unParsedName parsedName
  ExportPattern parsedName -> unParsedName parsedName
  ExportModule parsedModuleName -> unParsedModuleName parsedModuleName

rdrNameIdentifier :: RdrName -> String
rdrNameIdentifier =
  occNameString . rdrNameOcc

renderParseFailure :: PState -> String
renderParseFailure parserStateValue =
  parserStateValue
    & getPsErrorMessages
    & pprMessages defaultOpts
    & showSDocUnsafe

type LanguagePragmaDirective :: Type
data LanguagePragmaDirective
  = UseLanguage !Language
  | EnableExtension !Extension
  | DisableExtension !Extension
  deriving stock (Eq, Show)

type ParserExtensionState :: Type
data ParserExtensionState = ParserExtensionState
  { pesEnabled :: !(EnumSet.EnumSet Extension),
    pesExplicitlyDisabled :: !(EnumSet.EnumSet Extension)
  }

parserExtensions ::
  FilePath ->
  StringBuffer ->
  Either GhcParseFailure (EnumSet.EnumSet Extension)
parserExtensions sourcePath sourceBuffer =
  let headerParserOptions =
        mkParserOpts
          (pesEnabled ghc2024ParserExtensionState)
          parserDiagOpts
          False
          False
          False
          False
      (headerMessages, locatedOptions) =
        getOptions
          headerParserOptions
          supportedLanguagePragmas
          sourceBuffer
          sourcePath
   in if isEmptyMessages headerMessages
        then
          Right
            ( pesEnabled
                ( foldl'
                    applyLanguagePragmaDirective
                    ghc2024ParserExtensionState
                    (mapMaybe (languagePragmaDirective . unLoc) locatedOptions)
                )
            )
        else
          Left
            ( LanguagePragmaHeaderRejected
                sourcePath
                (showSDocUnsafe (pprMessages defaultOpts headerMessages))
            )

parserExtensionStateForLanguage :: Language -> ParserExtensionState
parserExtensionStateForLanguage languageValue =
  closeImpliedExtensions
    ParserExtensionState
      { pesEnabled = EnumSet.fromList (languageExtensions (Just languageValue)),
        pesExplicitlyDisabled = EnumSet.empty
      }

ghc2024ParserExtensionState :: ParserExtensionState
ghc2024ParserExtensionState =
  parserExtensionStateForLanguage GHC2024

applyLanguagePragmaDirective :: ParserExtensionState -> LanguagePragmaDirective -> ParserExtensionState
applyLanguagePragmaDirective _ (UseLanguage languageValue) =
  parserExtensionStateForLanguage languageValue
applyLanguagePragmaDirective parserExtensionState (EnableExtension extensionValue) =
  closeImpliedExtensions
    parserExtensionState
      { pesEnabled = EnumSet.insert extensionValue (pesEnabled parserExtensionState),
        pesExplicitlyDisabled = EnumSet.delete extensionValue (pesExplicitlyDisabled parserExtensionState)
      }
applyLanguagePragmaDirective parserExtensionState (DisableExtension extensionValue) =
  closeImpliedExtensions
    parserExtensionState
      { pesEnabled = EnumSet.delete extensionValue (pesEnabled parserExtensionState),
        pesExplicitlyDisabled = EnumSet.insert extensionValue (pesExplicitlyDisabled parserExtensionState)
      }

closeImpliedExtensions :: ParserExtensionState -> ParserExtensionState
closeImpliedExtensions parserExtensionState =
  let nextState =
        foldl'
          applyImpliedExtension
          parserExtensionState
          impliedXFlags
   in if sameParserExtensionState nextState parserExtensionState
        then parserExtensionState
        else closeImpliedExtensions nextState

sameParserExtensionState :: ParserExtensionState -> ParserExtensionState -> Bool
sameParserExtensionState leftState rightState =
  EnumSet.toList (pesEnabled leftState) == EnumSet.toList (pesEnabled rightState)
    && EnumSet.toList (pesExplicitlyDisabled leftState) == EnumSet.toList (pesExplicitlyDisabled rightState)

applyImpliedExtension :: ParserExtensionState -> (Extension, OnOff Extension) -> ParserExtensionState
applyImpliedExtension parserExtensionState (triggerExtension, impliedDirective)
  | EnumSet.member triggerExtension (pesEnabled parserExtensionState) =
      applyImpliedDirective parserExtensionState impliedDirective
  | otherwise =
      parserExtensionState

applyImpliedDirective :: ParserExtensionState -> OnOff Extension -> ParserExtensionState
applyImpliedDirective parserExtensionState (On extensionValue)
  | EnumSet.member extensionValue (pesExplicitlyDisabled parserExtensionState) =
      parserExtensionState
  | otherwise =
      parserExtensionState
        { pesEnabled = EnumSet.insert extensionValue (pesEnabled parserExtensionState)
        }
applyImpliedDirective parserExtensionState (Off extensionValue) =
  parserExtensionState
    { pesEnabled = EnumSet.delete extensionValue (pesEnabled parserExtensionState)
    }

languagePragmaDirective :: String -> Maybe LanguagePragmaDirective
languagePragmaDirective optionToken =
  Text.stripPrefix (Text.pack "-X") (Text.pack optionToken)
    >>= directiveForName . Text.unpack
  where
    directiveForName token =
      case languageName token of
        Just languageValue ->
          Just (UseLanguage languageValue)
        Nothing ->
          case extensionNamed token of
            Just extensionValue ->
              Just (EnableExtension extensionValue)
            Nothing ->
              DisableExtension <$> noExtension token

languageName :: String -> Maybe Language
languageName token =
  find
    ((== token) . show)
    ([minBound .. maxBound] :: [Language])

noExtension :: String -> Maybe Extension
noExtension token =
  case Text.stripPrefix (Text.pack "No") (Text.pack token) of
    Nothing -> Nothing
    Just extensionName -> extensionNamed (Text.unpack extensionName)

extensionNamed :: String -> Maybe Extension
extensionNamed extensionName =
  flagSpecFlag
    <$> find
      ((== extensionName) . flagSpecName)
      xFlags

supportedLanguagePragmas :: [String]
supportedLanguagePragmas =
  languageNames
    <> extensionNames
    <> fmap ("No" <>) extensionNames
  where
    languageNames =
      fmap show ([minBound .. maxBound] :: [Language])
    extensionNames =
      fmap flagSpecName xFlags

parserDiagOpts :: DiagOpts
parserDiagOpts =
  DiagOpts
    { diag_warning_flags = EnumSet.empty :: EnumSet.EnumSet WarningFlag,
      diag_fatal_warning_flags = EnumSet.empty :: EnumSet.EnumSet WarningFlag,
      diag_custom_warning_categories = emptyWarningCategorySet,
      diag_fatal_custom_warning_categories = emptyWarningCategorySet,
      diag_warn_is_error = False,
      diag_reverse_errors = False,
      diag_max_errors = Nothing,
      diag_ppr_ctx = defaultSDocContext
    }