packages feed

ghci-dap-0.0.27.0: app-ghc-9.14/GHCi/UI/Exception.hs

{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilyDependencies #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DeriveGeneric #-}

module GHCi.UI.Exception
  ( GhciCommandError(..)
  , throwGhciCommandError
  , handleGhciCommandError
  , GhciMessage(..)
  , GhciMessageOpts(..)
  , fromGhcOpts
  , toGhcHint
  , GhciCommandMessage(..)
  , GhciMacroError(..)
  , GhciModuleError(..)
  , InvalidMacroStart(..)
  , GHCi
  ) where

import GHC.Prelude

import GHC.Driver.Errors.Types

import GHC.Iface.Errors.Ppr
import GHC.Iface.Errors.Types

import qualified GHC.LanguageExtensions as LangExt

import GHC.Tc.Errors.Ppr
import GHC.Tc.Errors.Types

import GHC.Types.Error.Codes
import GHC.Types.SrcLoc (interactiveSrcSpan)
import GHC.TypeLits

import GHC.Unit.State

import GHC.Utils.Outputable
import GHC.Utils.Error

import GHC.Generics
import GHC.Types.Error
import GHC.Types
import qualified GHC

import Control.Exception
import Control.Monad.Catch as MC (MonadCatch, catch)
import Control.Monad.IO.Class
import Data.List.NonEmpty (NonEmpty(..))

-- | A 'GhciCommandError' are messages that caused the abortion of a GHCi command.
newtype GhciCommandError =  GhciCommandError (Messages GhciMessage)

instance Exception GhciCommandError

instance Show GhciCommandError where
  -- We implement 'Show' because it's required by the 'Exception' instance, but diagnostics
  -- shouldn't be shown via the 'Show' typeclass, but rather rendered using the ppr functions.
  -- This also explains why there is no 'Show' instance for a 'MsgEnvelope'.
  show (GhciCommandError msgs) =
      renderWithContext defaultSDocContext
    . vcat
    . pprMsgEnvelopeBagWithLocDefault
    . getMessages
    $ msgs

-- | Perform the given action and call the exception handler if the action
-- throws a 'GhciCommandError'.  See 'GhciCommandError' for more information.
handleGhciCommandError :: (MonadCatch m) =>
                     (GhciCommandError -> m a) -- ^ exception handler
                  -> m a -- ^ action to perform
                  -> m a
handleGhciCommandError handler act =
  MC.catch act (\(e :: GhciCommandError) -> handler e)

throwGhciCommandError :: MonadIO m => GhciCommandMessage -> m a
throwGhciCommandError errorMessage =
  liftIO
    . throwIO
    . GhciCommandError
    . singleMessage
    $ mkPlainErrorMsgEnvelope interactiveSrcSpan (GhciCommandMessage errorMessage)

-- | The Options passed to 'diagnosticMessage'
-- in the 'Diagnostic' instance of 'GhciMessage'.
data GhciMessageOpts = GhciMessageOpts
  { ghcMessageOpts         :: DiagnosticOpts GhcMessage
  , ghciCommandMessageOpts :: DiagnosticOpts GhciCommandMessage
  }

-- | A 'GhciMessage' may either be a message that GHC emitted ('GhciGhcMessage'),
-- a message that is specific to GHCi ('GhciCommandMessage'),
-- or an external message ('GhciUnknownMessage') that can be used by external tools.
data GhciMessage where
  GhciCommandMessage :: GhciCommandMessage               -> GhciMessage
  GhciGhcMessage     :: GhcMessage                       -> GhciMessage
  GhciUnknownMessage :: UnknownDiagnosticFor GhciMessage -> GhciMessage

-- | A 'GhciHint' may either be a hint that GHC emitted ('GhciGhcHint')
-- or one that is specific to GHCi ('GhciCommandHint').
data GhciHint where
  GhciCommandHint :: GhciCommandHint -> GhciHint
  GhciGhcHint     :: GhcHint         -> GhciHint

-- | If this is a contained 'GhcHint' simply unwrap it,
-- otherwise use 'GhcHint's 'UnknownHint' constructor.
toGhcHint :: GhciHint -> GhcHint
toGhcHint (GhciGhcHint h)     = h
toGhcHint (GhciCommandHint h) = UnknownHint h

instance Outputable GhciHint where
  ppr = \case
    GhciCommandHint hint -> ppr hint
    GhciGhcHint     hint -> ppr hint

instance HasDefaultDiagnosticOpts GhciMessageOpts where
  defaultOpts = GhciMessageOpts
    (defaultDiagnosticOpts @GhcMessage)
    (defaultDiagnosticOpts @GhciCommandMessage)

-- | Create default 'GhciMessageOpts' but embed the given 'GhcMessageOpts'
fromGhcOpts :: DiagnosticOpts GhcMessage -> DiagnosticOpts GhciMessage
fromGhcOpts ghc_opts = defaultOpts { ghcMessageOpts = ghc_opts }

instance Diagnostic GhciMessage where
  type DiagnosticOpts GhciMessage = GhciMessageOpts
  type DiagnosticHint GhciMessage = GhciHint

  diagnosticMessage opts = \case
    GhciGhcMessage     m -> ghciDiagnosticMessage (ghcMessageOpts opts) m
    GhciCommandMessage m -> diagnosticMessage (ghciCommandMessageOpts opts) m
    GhciUnknownMessage (UnknownDiagnostic f _ m)
      -> diagnosticMessage (f opts) m

  diagnosticReason = \case
    GhciGhcMessage     m -> diagnosticReason m
    GhciCommandMessage m -> diagnosticReason m
    GhciUnknownMessage m -> diagnosticReason m

  diagnosticHints = \case
    GhciGhcMessage     m -> map GhciGhcHint     (ghciDiagnosticHints m)
    GhciCommandMessage m -> map GhciCommandHint (diagnosticHints m)
    GhciUnknownMessage m -> diagnosticHints m

  diagnosticCode = \case
    GhciGhcMessage     m -> diagnosticCode m
    GhciCommandMessage m -> diagnosticCode m
    GhciUnknownMessage m -> diagnosticCode m


-- | Modifications to hint messages which we want to display in GHCi.
ghciDiagnosticHints :: GhcMessage -> [GhcHint]
ghciDiagnosticHints msg = map modifyHintForGHCi (diagnosticHints msg)
  where
    modifyHintForGHCi :: GhcHint -> GhcHint
    modifyHintForGHCi = \case
      SuggestExtension extHint -> SuggestExtension $ modifyExtHintForGHCi extHint
      hint -> hint
    modifyExtHintForGHCi :: LanguageExtensionHint -> LanguageExtensionHint
    modifyExtHintForGHCi = \case
      SuggestSingleExtension    doc ext  -> SuggestSingleExtension    (suggestSetExt [ext] doc False) ext
      SuggestExtensionInOrderTo doc ext  -> SuggestExtensionInOrderTo (suggestSetExt [ext] doc False) ext
      SuggestAnyExtension       doc exts -> SuggestAnyExtension       (suggestSetExt exts  doc True ) exts
      SuggestExtensions         doc exts -> SuggestExtensions         (suggestSetExt exts  doc False) exts
    -- Suggest enabling extension with :set -X<ext>
    -- SuggestAnyExtension will be on multiple lines so the user can select which to enable without editing
    suggestSetExt :: [LangExt.Extension] -> SDoc -> Bool -> SDoc
    suggestSetExt exts doc enable_any = doc $$ hang header 2 exts_cmds
      where
        header = text "You may enable" <+> which <+> text "language extension" <> plural exts <+> text "in GHCi with:"
        which
          | [ _ext ] <- exts
          = text "this"
          | otherwise
          = if enable_any
            then text "these"
            else text "all of these"
        exts_cmds
          | enable_any
          = vcat $ map (\ext -> text ":set -X" <> ppr ext) exts
          | otherwise
          = text ":set" <> hcat (map (\ext -> text " -X" <> ppr ext) exts)

-- | Modifications to error messages which we want to display in GHCi
ghciDiagnosticMessage :: GhcMessageOpts -> GhcMessage -> DecoratedSDoc
ghciDiagnosticMessage ghc_opts msg =
  case msg of
    GhcTcRnMessage tc_msg ->
      case tcRnMessage (tcMessageOpts ghc_opts) tc_msg of
        Nothing -> diagnosticMessage ghc_opts msg
        Just sdoc -> sdoc
    GhcDriverMessage  (DriverInterfaceError err) ->
      case ghciInterfaceError err of
        Just sdoc -> mkSimpleDecorated sdoc
        Nothing -> diagnosticMessage ghc_opts msg
    GhcDriverMessage {} -> diagnosticMessage ghc_opts msg
    GhcPsMessage  {} -> diagnosticMessage ghc_opts msg
    GhcDsMessage  {} -> diagnosticMessage ghc_opts msg
    GhcUnknownMessage  {} -> diagnosticMessage ghc_opts msg
  where
    tcRnMessage tc_opts tc_msg =
      case tc_msg of
        TcRnInterfaceError err -> mkSimpleDecorated <$> (ghciInterfaceError err)
        TcRnMessageWithInfo unit_state msg_with_info ->
          case msg_with_info of
           TcRnMessageDetailed err_info wrapped_msg
             -> messageWithInfoDiagnosticMessage unit_state err_info
                  (tcOptsShowContext tc_opts)
                  <$> tcRnMessage tc_opts wrapped_msg
        TcRnWithHsDocContext ctxt wrapped_msg ->
          messageWithHsDocContext tc_opts ctxt <$> tcRnMessage tc_opts wrapped_msg
        _ -> Nothing

    opts = tcOptsIfaceOpts (tcMessageOpts ghc_opts)

    ghciInterfaceError (Can'tFindInterface err looking_for) =
      hangNotEmpty (lookingForHerald looking_for) 2 <$> ghciMissingInterfaceErrorDiagnostic err
    ghciInterfaceError _ = Nothing

    ghciMissingInterfaceErrorDiagnostic reason =
      case reason of
        CantFindErr us module_or_interface cfi ->
          Just (pprWithUnitState us $ cantFindErrorX pkg_hidden_hint may_show_locations module_or_interface cfi)
        _ -> Nothing
      where

        may_show_locations = mayShowLocations ":set -v" (ifaceShowTriedFiles opts)

        pkg_hidden_hint = pkgHiddenHint hidden_msg (ifaceBuildingCabalPackage opts)
          where
            hidden_msg pkg =
              text "You can run" <+>
              quotes (text ":set -package " <> ppr (unitPackageName pkg)) <+>
              text "to expose it." $$
              text "(Note: this unloads all the modules in the current scope.)"


data InvalidMacroStart = Colon | ExclamationMark

instance Outputable InvalidMacroStart where
  ppr = \case
    Colon
      -> "a colon"
    ExclamationMark
      -> "an exclamation mark"

data GhciMacroError
  = GhciMacroAlreadyDefined String
  | GhciMacroInvalidStart String InvalidMacroStart
  | GhciMacroNotDefined String
  | GhciMacroOverwritesBuiltin String
  deriving Generic

instance Diagnostic GhciMacroError where
  type DiagnosticOpts GhciMacroError = NoDiagnosticOpts
  type DiagnosticHint GhciMacroError = GhciCommandHint

  diagnosticMessage NoDiagnosticOpts = mkSimpleDecorated . \case
    GhciMacroAlreadyDefined name
      -> macro name "is already defined"
    GhciMacroInvalidStart name invStart
      -> macro name ("cannot start with" <+> ppr invStart)
    GhciMacroNotDefined name
      -> macro name "is not defined"
    GhciMacroOverwritesBuiltin name
      -> macro name "overwrites builtin command"
    where macro name what = "Macro" <+> quotes (text name) <+> what

  diagnosticReason = \case
    GhciMacroAlreadyDefined{}
      -> ErrorWithoutFlag
    GhciMacroInvalidStart{}
      -> ErrorWithoutFlag
    GhciMacroNotDefined{}
      -> ErrorWithoutFlag
    GhciMacroOverwritesBuiltin{}
      -> ErrorWithoutFlag

  diagnosticHints = \case
    GhciMacroAlreadyDefined{}
      -> [SuggestOverwrite]
    GhciMacroInvalidStart{}
      -> []
    GhciMacroNotDefined{}
      -> []
    GhciMacroOverwritesBuiltin{}
      -> [SuggestOverwrite]

  diagnosticCode = constructorCode @GHCi

data GhciModuleError
  = GhciModuleNotFound String
  | GhciNoModuleNameGuess
  | GhciNoModuleInfoForCurrentFile
  | GhciNoLocationInfoForModule GHC.ModuleName
  | GhciNoResolvedModules
  | GhciNoModuleForName GHC.Name
  | GhciNoMatchingModuleExport
  | GhciNoLocalModuleName !GHC.ModuleName
  | GhciModuleNameNotFound !GHC.ModuleName
  | GhciAmbiguousModuleName !GHC.ModuleName ![GHC.Module]
  deriving Generic

instance Diagnostic GhciModuleError where
  type DiagnosticOpts GhciModuleError = NoDiagnosticOpts
  type DiagnosticHint GhciModuleError = GhciCommandHint

  diagnosticMessage NoDiagnosticOpts = mkSimpleDecorated . \case
    GhciModuleNotFound modN
      -> "Module" <+> text modN <+> "not found"
    GhciNoModuleNameGuess
      -> "Couldn't guess that module name. Does it exist?"
    GhciNoModuleInfoForCurrentFile
      -> "No module info for current file! Try loading it?"
    GhciNoLocationInfoForModule name
      -> "Found a name, but no location information" <> dot <+> "The module is" <> colon <+> ppr name
    GhciNoResolvedModules
      -> "Couldn't resolve to any modules."
    GhciNoModuleForName name
      -> "No module for" <+> ppr name
    GhciNoMatchingModuleExport
      -> "No matching export in any local modules."
    GhciNoLocalModuleName modl
      -> "Module" <+> quotes (ppr modl) <+> "cannot be found locally"
    GhciModuleNameNotFound modl
      -> "module" <+> quotes (ppr modl) <+> "could not be found."
    GhciAmbiguousModuleName modl candidates
      -> "Module name" <+> quotes (ppr modl) <+> "is ambiguous" $+$
        vcat
          [ text "-" <+> ppr (GHC.moduleName m) <> colon <> ppr (GHC.moduleUnit m)
          | m <- candidates
          ]

  diagnosticReason = \case
    GhciModuleNotFound{} ->
      ErrorWithoutFlag
    GhciNoModuleNameGuess{} ->
      ErrorWithoutFlag
    GhciNoModuleInfoForCurrentFile{} ->
      ErrorWithoutFlag
    GhciNoLocationInfoForModule{} ->
      ErrorWithoutFlag
    GhciNoResolvedModules{} ->
      ErrorWithoutFlag
    GhciNoModuleForName{} ->
      ErrorWithoutFlag
    GhciNoMatchingModuleExport{} ->
      ErrorWithoutFlag
    GhciNoLocalModuleName{} ->
      ErrorWithoutFlag
    GhciModuleNameNotFound{} ->
      ErrorWithoutFlag
    GhciAmbiguousModuleName{} ->
      ErrorWithoutFlag

  diagnosticHints = \case
    GhciModuleNotFound{} ->
      []
    GhciNoModuleNameGuess{} ->
      []
    GhciNoModuleInfoForCurrentFile{} ->
      []
    GhciNoLocationInfoForModule{} ->
      []
    GhciNoResolvedModules{} ->
      []
    GhciNoModuleForName{} ->
      []
    GhciNoMatchingModuleExport{} ->
      []
    GhciNoLocalModuleName{} ->
      []
    GhciModuleNameNotFound{} ->
      []
    GhciAmbiguousModuleName{} ->
      []
  diagnosticCode = constructorCode @GHCi

-- | A Diagnostic emitted by GHCi while executing a command
-- these errors are assigned codes analogous to GHCs errors.
--
-- See 'GhciDiagnosticCode'.
data GhciCommandMessage
  -- module name errors
  = GhciMacroError GhciMacroError
  | GhciModuleError GhciModuleError
  -- others
  | GhciArgumentParseError SDoc
  | GhciCommandNotSupportedInMultiMode
  | GhciInvalidArgumentString String
  | GhciFileNotFound String
  | GhciCommandSyntaxError String
  | GhciInvalidPromptString
  | GhciPromptCallError String
  | GhciUnknownCommand String
  | GhciNoLastCommandAvailable
  | GhciUnknownFlag
     { ghciUnknownFlag :: !String
     , ghciUnknownFlagSuggestions :: [String]
     }
  | GhciNoSetEditor
  deriving Generic

-- | Hints that may be given by a 'GhciCommandMessage'
data GhciCommandHint
  = SuggestHelp
  | UseSetEditor
  | SuggestOverwrite
  | DidYouMean (NonEmpty String)

instance Outputable GhciCommandHint where
  ppr = \case
    SuggestHelp
      -> use "?" <+> "for help"
    DidYouMean (sugg :| rest)
      | [] <- rest -> "did you mean" <+> text sugg
      | otherwise  -> "did you mean one of" <> colon <+> nest 2 (hsep (map text (sugg : rest)))
    UseSetEditor
      -> use "set editor"
    SuggestOverwrite
      -> use "def!" <+> "to overwrite"
    where use cmd = "Use" <+> quotes (colon <> cmd)

instance Diagnostic GhciCommandMessage where
  type DiagnosticOpts GhciCommandMessage = NoDiagnosticOpts
  type DiagnosticHint GhciCommandMessage = GhciCommandHint

  diagnosticMessage opts (GhciMacroError err)  = diagnosticMessage opts err
  diagnosticMessage opts (GhciModuleError err) = diagnosticMessage opts err
  diagnosticMessage NoDiagnosticOpts err = mkSimpleDecorated $ case err of
    -- this can be improved with better/generic argument parsing
    GhciArgumentParseError ape
      -> ape
    GhciCommandNotSupportedInMultiMode
      -> "Command is not supported (yet) in multi-mode"
    -- In the future it would be better for toArgs to have a proper error type
    -- which could be used here and also help with reporting OPTIONS_GHC pragma errors better.
    GhciInvalidArgumentString str
      -> text str
    GhciCommandSyntaxError cmd
      -> "Syntax" <> colon $+$ nest 2 (colon <> text cmd)
    GhciInvalidPromptString
      -> "Can't parse prompt string. Use Haskell syntax"
    GhciUnknownCommand cmd
      -> "Unknown command" <+> quotes (colon <> text cmd)
    GhciNoLastCommandAvailable
      -> "There is no last command to perform"
    GhciFileNotFound f
      -> "File" <+> text f <+> "not found"
    GhciUnknownFlag { ghciUnknownFlag = flag }
      -> "Unrecognised flag" <> colon <+> text flag
    GhciPromptCallError err'
      -> "Error while calling prompt function" <> colon $$ nest 2 (quotes $ text err')
    GhciNoSetEditor
      -> "Editor not set"

  -- this might change in the future when we add more GHCi diagnostics
  diagnosticReason = \case
    GhciMacroError err
      -> diagnosticReason err
    GhciModuleError err
      -> diagnosticReason err
    GhciArgumentParseError{}
      -> ErrorWithoutFlag
    GhciCommandNotSupportedInMultiMode{}
      -> ErrorWithoutFlag
    GhciInvalidArgumentString{}
      -> ErrorWithoutFlag
    GhciFileNotFound {}
      -> ErrorWithoutFlag
    GhciCommandSyntaxError{}
      -> ErrorWithoutFlag
    GhciInvalidPromptString{}
      -> ErrorWithoutFlag
    GhciPromptCallError{}
      -> ErrorWithoutFlag
    GhciUnknownCommand{}
      -> ErrorWithoutFlag
    GhciNoLastCommandAvailable{}
      -> ErrorWithoutFlag
    GhciUnknownFlag{}
      -> ErrorWithoutFlag
    GhciNoSetEditor{}
      -> ErrorWithoutFlag

  diagnosticHints = \case
    GhciMacroError err
      -> diagnosticHints err
    GhciModuleError err
      -> diagnosticHints err
    GhciArgumentParseError{}
      -> []
    GhciCommandNotSupportedInMultiMode{}
      -> []
    GhciInvalidArgumentString{}
      -> []
    GhciFileNotFound{}
      -> []
    GhciCommandSyntaxError{}
      -> []
    GhciInvalidPromptString{}
      -> []
    GhciPromptCallError{}
      -> []
    GhciUnknownCommand{}
      -> [SuggestHelp]
    GhciNoLastCommandAvailable{}
      -> [SuggestHelp]
    GhciUnknownFlag { ghciUnknownFlagSuggestions = suggs }
      -> case suggs of
           []   -> []
           x:xs -> [DidYouMean (x :| xs)]
    GhciNoSetEditor{}
      -> [UseSetEditor]

  diagnosticCode = \case
    GhciMacroError  err -> diagnosticCode err
    GhciModuleError err -> diagnosticCode err
    err -> constructorCode @GHCi err

-- | type index for the ghci diagnostic code namespace
data GHCi

instance DiagnosticCodeNameSpace GHCi where
  type instance NameSpaceTag      GHCi     = "GHCi"
  type instance DiagnosticCodeFor GHCi con = GhciDiagnosticCode con
  type instance ConRecursIntoFor  GHCi con = GhciConRecursInto con

type GhciDiagnosticCode :: Symbol -> Nat
type family GhciDiagnosticCode c = n | n -> c where
  GhciDiagnosticCode "GhciCommandNotSupportedInMultiMode" = Outdated 83514
  GhciDiagnosticCode "GhciInvalidArgumentString"          = 68894
  GhciDiagnosticCode "GhciCommandSyntaxError"             = 72682
  GhciDiagnosticCode "GhciInvalidPromptString"            = 50882
  GhciDiagnosticCode "GhciPromptCallError"                = 22747
  GhciDiagnosticCode "GhciUnknownCommand"                 = 54713
  GhciDiagnosticCode "GhciNoLastCommandAvailable"         = 29130
  GhciDiagnosticCode "GhciUnknownFlag"                    = 15670
  GhciDiagnosticCode "GhciNoSetEditor"                    = 34086
  GhciDiagnosticCode "GhciMacroInvalidStart"              = 64996
  GhciDiagnosticCode "GhciMacroAlreadyDefined"            = 93909
  GhciDiagnosticCode "GhciMacroNotDefined"                = 40561
  GhciDiagnosticCode "GhciMacroOverwritesBuiltin"         = 86201
  GhciDiagnosticCode "GhciFileNotFound"                   = 31901
  GhciDiagnosticCode "GhciModuleNotFound"                 = 23305
  GhciDiagnosticCode "GhciNoModuleNameGuess"              = 21939
  GhciDiagnosticCode "GhciNoModuleInfoForCurrentFile"     = 96587
  GhciDiagnosticCode "GhciNoLocationInfoForModule"        = 12769
  GhciDiagnosticCode "GhciNoResolvedModules"              = 54909
  GhciDiagnosticCode "GhciNoModuleForName"                = 21847
  GhciDiagnosticCode "GhciNoMatchingModuleExport"         = 59723
  GhciDiagnosticCode "GhciArgumentParseError"             = 35671
  GhciDiagnosticCode "GhciNoLocalModuleName"              = 81235
  GhciDiagnosticCode "GhciModuleNameNotFound"             = 40475
  GhciDiagnosticCode "GhciAmbiguousModuleName"            = 59019

type GhciConRecursInto :: Symbol -> Maybe Type
type family GhciConRecursInto con where
  GhciConRecursInto "GhciMacroError"  = 'Just GhciMacroError
  GhciConRecursInto "GhciModuleError" = 'Just GhciModuleError
  GhciConRecursInto _                 = 'Nothing