packages feed

libclang-bindings-0.1.0.0: src/Clang/HighLevel/Diagnostics.hs

module Clang.HighLevel.Diagnostics (
    Diagnostic(..)
  , FixIt(..)
  , clang_getDiagnostics
  , diagnosticIsError
  ) where

import Control.Exception
import Control.Monad.IO.Class
import Data.Text (Text)
import Data.Text qualified as Text
import Foreign.C

import Clang.Enum.Bitfield
import Clang.Enum.Simple
import Clang.HighLevel.SourceLoc (MultiLoc, Range)
import Clang.HighLevel.SourceLoc qualified as SourceLoc
import Clang.LowLevel.Core

{-------------------------------------------------------------------------------
  Definition
-------------------------------------------------------------------------------}

data Diagnostic = Diagnostic {
      -- | Formatted by @libclang@ in a manner that is suitable for display
      diagnosticFormatted :: Text

      -- | Severity
    , diagnosticSeverity :: SimpleEnum CXDiagnosticSeverity

      -- | Source location (where Clang would print the caret @^@)
    , diagnosticLocation  :: MultiLoc

      -- | Text of the diagnostic
    , diagnosticSpelling  :: Text

      -- | The command line option that enabled this diagnostic
    , diagnosticOption :: Maybe Text

      -- | The @libclang@ option to disable this diagnostic
    , diagnosticDisabledBy :: Maybe Text

      -- | Diagnostic category
    , diagnosticCategory :: Int

      -- | Rendered category
    , diagnosticCategoryText :: Text

      -- | Source range associated with the diagnostic
      --
      -- A diagnostic's source ranges highlight important elements in the source
      -- code. On the command line, Clang displays source ranges by underlining
      -- them with @~@ characters.
    , diagnosticRanges :: [Range MultiLoc]

      -- | Fix-it hints
    , diagnosticFixIts :: [FixIt]

      -- | Child diagnostics
    , diagnosticChildren  :: [Diagnostic]
    }
  deriving stock (Show, Eq)
  deriving anyclass (Exception)

-- | Suggestion to fix the code
--
-- Fix-its are described in terms of a source range whose contents should be
-- replaced by a string. This approach generalizes over three kinds of
-- operations: removal of source code (the range covers the code to be removed
-- and the replacement string is empty), replacement of source code (the range
-- covers the code to be replaced and the replacement string provides the new
-- code), and insertion (both the start and end of the range point at the
-- insertion location, and the replacement string provides the text to insert).
data FixIt = FixIt {
      -- | Replacement range
      --
      -- The replacement range is the source range whose contents will be
      -- replaced with the returned replacement string. Note that source ranges
      -- are half-open ranges [a, b), so the source code should be replaced from
      -- a and up to (but not including) b.
      fixItRange :: Range MultiLoc

      -- | Text that should replace the source code
    , fixItReplacement :: Text
    }
  deriving stock (Show, Eq)

-- TODO <https://github.com/well-typed/libclang-bindings/issues/73>
--
-- Probably separate into Info/Warning/Error (issue #175).
diagnosticIsError :: Diagnostic -> Bool
diagnosticIsError diag =
    case fromSimpleEnum (diagnosticSeverity diag) of
      Right CXDiagnostic_Error -> True
      Right CXDiagnostic_Fatal -> True
      Left _unknownSeverity    -> True
      Right _otherwise         -> False

{-------------------------------------------------------------------------------
  Top-level
-------------------------------------------------------------------------------}

clang_getDiagnostics ::
     MonadIO m
  => CXTranslationUnit
  -> Maybe (BitfieldEnum CXDiagnosticDisplayOptions)
     -- ^ Display options for constructing 'diagnosticFormatted'
     --
     -- If 'Nothing', uses 'clang_defaultDiagnosticDisplayOptions'.
  -> m [Diagnostic]
clang_getDiagnostics unit mDisplayOptions = do
    displayOptions <- case mDisplayOptions of
                        Just displayOptions -> return displayOptions
                        Nothing -> clang_defaultDiagnosticDisplayOptions
    getAll unit clang_getNumDiagnostics $ getDiagnostic displayOptions

{-------------------------------------------------------------------------------
  Get all diagnostics
-------------------------------------------------------------------------------}

getDiagnostic ::
     MonadIO m
  => BitfieldEnum CXDiagnosticDisplayOptions
  -> CXTranslationUnit
  -> CUInt
  -> m Diagnostic
getDiagnostic displayOptions unit i = liftIO $
    bracket (clang_getDiagnostic unit i) clang_disposeDiagnostic $
      reify displayOptions

getDiagnosticInSet ::
     MonadIO m
  => BitfieldEnum CXDiagnosticDisplayOptions
  -> CXDiagnosticSet
  -> CUInt
  -> m Diagnostic
getDiagnosticInSet displayOptions set i = liftIO $
    bracket (clang_getDiagnosticInSet set i) clang_disposeDiagnostic $
      reify displayOptions

reify ::
     MonadIO m
  => BitfieldEnum CXDiagnosticDisplayOptions
  -> CXDiagnostic
  -> m Diagnostic
reify displayOptions diag = do
    diagnosticFormatted    <- clang_formatDiagnostic diag displayOptions
    diagnosticSeverity     <- clang_getDiagnosticSeverity diag
    diagnosticLocation     <- SourceLoc.clang_getDiagnosticLocation diag
    diagnosticSpelling     <- clang_getDiagnosticSpelling diag
    (mOption, mDisabledBy) <- clang_getDiagnosticOption diag
    diagnosticCategory     <- fromIntegral <$> clang_getDiagnosticCategory diag
    diagnosticCategoryText <- clang_getDiagnosticCategoryText diag
    diagnosticRanges       <- getAll diag clang_getDiagnosticNumRanges $
                                SourceLoc.clang_getDiagnosticRange
    diagnosticFixIts       <- getAll diag clang_getDiagnosticNumFixIts $
                                getDiagnosticFixIt
    diagnosticChildren     <- getChildDiagnostics displayOptions diag
    return $ Diagnostic {
          diagnosticFormatted
        , diagnosticSeverity
        , diagnosticLocation
        , diagnosticSpelling
        , diagnosticOption     = nonEmpty mOption
        , diagnosticDisabledBy = nonEmpty mDisabledBy
        , diagnosticCategory
        , diagnosticCategoryText
        , diagnosticRanges
        , diagnosticFixIts
        , diagnosticChildren
        }
  where
    nonEmpty :: Text -> Maybe Text
    nonEmpty bs
      | Text.null bs = Nothing
      | otherwise    = Just bs

getChildDiagnostics ::
     MonadIO m
  => BitfieldEnum CXDiagnosticDisplayOptions
  -> CXDiagnostic
  -> m [Diagnostic]
getChildDiagnostics displayOptions diag = do
    set <- clang_getChildDiagnostics diag
    getAll set clang_getNumDiagnosticsInSet $
      getDiagnosticInSet displayOptions

getDiagnosticFixIt ::
     MonadIO m
  => CXDiagnostic
  -> CUInt
  -> m FixIt
getDiagnosticFixIt diag i =
    uncurry FixIt <$> SourceLoc.clang_getDiagnosticFixIt diag i

{-------------------------------------------------------------------------------
  Auxiliary
-------------------------------------------------------------------------------}

getAll :: Monad m => a -> (a -> m CUInt) -> (a -> CUInt -> m b) -> m [b]
getAll x getCount getElem = do
    count <- getCount x
    if count == 0
      then return []
      else mapM (getElem x) [0 .. pred count]