packages feed

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

-- | Utilities for working with source locations
module Clang.HighLevel.SourceLoc (
    -- * Definition
    SingleLoc(..)
  , MultiLoc(..)
  , Range(..)
    -- * Comparisons
  , compareSingleLoc
  , rangeContainsLoc
    -- * Conversion
  , toMulti
  , toRange
  , fromSingle
  , fromRange
    -- * Get single location
  , clang_getExpansionLocation
  , clang_getPresumedLocation
  , clang_getSpellingLocation
  , clang_getFileLocation
    -- * Pretty-printing
    --
    -- We export these separately, because the 'Show' instance also adds quotes
    -- (in order to produce valid Haskell syntax).
  , ShowFile(..)
  , prettySingleLoc
  , prettyMultiLoc
  , prettyRangeSingleLoc
  , prettyRangeMultiLoc
    -- * Convenience wrappers
    -- * for @CXSourceLocation@
  , clang_getDiagnosticLocation
  , clang_getCursorLocation
  , clang_getCursorLocation'
  , clang_getTokenLocation
    -- ** for @CXSourceRange@
  , clang_getDiagnosticRange
  , clang_getDiagnosticFixIt
  , clang_Cursor_getSpellingNameRange
  , clang_getCursorExtent
  , clang_getTokenExtent
  ) where

import Control.Monad
import Control.Monad.IO.Class
import Data.List (intercalate)
import Data.Text (Text)
import Foreign.C
import GHC.Generics (Generic)
import GHC.Stack

import Clang.LowLevel.Core qualified as Core
import Clang.Paths

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

-- | A /single/ location in a file
--
-- See 'MultiLoc' for additional discussion.
data SingleLoc = SingleLoc {
      singleLocPath   :: !SourcePath
    , singleLocLine   :: !Int
    , singleLocColumn :: !Int
    , singleLocOffset :: !Int
    }
  deriving stock (Eq, Ord, Generic)

-- | Presumed location
--
-- Presumed locations arise from @#line@ directives, and as such don't provide
-- an offset.
data PresumedLoc = PresumedLoc {
      presumedLocPath   :: !SourcePath
    , presumedLocLine   :: !Int
    , presumedLocColumn :: !Int
    }
  deriving stock (Eq, Ord, Generic)


-- | Multiple related source locations
--
-- 'Core.CXSourceLocation' in @libclang@ corresponds to @SourceLocation@ in
-- @clang@, which can actually correspond to /multiple/ source locations in a
-- file; for example, in a header file such as
--
-- > #define M1 int
-- >
-- > struct ExampleStruct {
-- >   M1 m1;
-- >   ^
-- > };
--
-- then the source location at the caret (@^@) has an \"expansion location\",
-- which is the position at the caret, and a \"spelling location\", which
-- corresponds to the location of the @int@ token in the macro definition.
--
-- References:
--
-- * <https://clang.llvm.org/doxygen/classclang_1_1SourceLocation.html>
-- * <https://clang.llvm.org/doxygen/classclang_1_1SourceManager.html>
--   (@getExpansionLoc@, @getSpellingLoc@, @getDecomposedSpellingLoc@)
data MultiLoc = MultiLoc {
      -- | Expansion location
      --
      -- If the location refers into a macro expansion, this corresponds to the
      -- location of the macro expansion.
      --
      -- See <https://clang.llvm.org/doxygen/group__CINDEX__LOCATIONS.html#gadee4bea0fa34550663e869f48550eb1f>
      multiLocExpansion :: !SingleLoc

      -- | Presumed location
      --
      -- The given source location as specified in a @#line@ directive.
      --
      -- See <https://clang.llvm.org/doxygen/group__CINDEX__LOCATIONS.html#ga03508d9c944feeb3877515a1b08d36f9>
    , multiLocPresumed :: !(Maybe PresumedLoc)

      -- | Spelling location
      --
      -- If the location refers into a macro instantiation, this corresponds to
      -- the /original/ location of the spelling in the source file.
      --
      -- /WARNING/: This field is only populated correctly from @llvm >= 19.1.0@;
      -- prior to that this is equal to 'multiLocFile'.
      -- See <https://github.com/llvm/llvm-project/pull/72400>.
      --
      -- See <https://clang.llvm.org/doxygen/group__CINDEX__LOCATIONS.html#ga01f1a342f7807ea742aedd2c61c46fa0>
    , multiLocSpelling :: !(Maybe SingleLoc)

      -- | File location
      --
      -- If the location refers into a macro expansion, this corresponds to the
      -- location of the macro expansion.
      -- If the location points at a macro argument, this corresponds to the
      -- location of the use of the argument.
      --
      -- See <https://clang.llvm.org/doxygen/group__CINDEX__LOCATIONS.html#gae0ee9ff0ea04f2446832fc12a7fd2ac8>
    , multiLocFile :: !(Maybe SingleLoc)
    }
  deriving stock (Eq, Ord, Generic)

-- | Range
--
-- 'Core.CXSourceRange' corresponds to @SourceRange@ in @clang@
-- <https://clang.llvm.org/doxygen/classclang_1_1SourceLocation.html>,
-- and therefore to @Range MultiLoc@; see 'MultiLoc' for additional discussion.
data Range a = Range {
      rangeStart :: !a
    , rangeEnd   :: !a
    }
  deriving stock (Eq, Ord, Generic)
  deriving stock (Functor, Foldable, Traversable)

{-------------------------------------------------------------------------------
  Comparisons
-------------------------------------------------------------------------------}

-- | Compare locations
--
-- Returns 'Nothing' if the locations aren't in the same file.
compareSingleLoc :: SingleLoc -> SingleLoc -> Maybe Ordering
compareSingleLoc a b = do
    guard $ singleLocPath a == singleLocPath b
    return $
      compare
        (singleLocLine a, singleLocColumn a)
        (singleLocLine b, singleLocColumn b)

-- | Check if a location falls within the given range
--
-- Treats the range as half-open, with an inclusive lower bound and exclusive
-- upper bound (following 'Core.CXSourceRange').
--
-- Returns 'Nothing' if the three locations are not all in the same file.
rangeContainsLoc :: Range SingleLoc -> SingleLoc -> Maybe Bool
rangeContainsLoc Range{rangeStart, rangeEnd} loc = do
    afterStart <- (/= LT) <$> compareSingleLoc loc rangeStart
    beforeEnd  <- (== LT) <$> compareSingleLoc loc rangeEnd
    return $ afterStart && beforeEnd

{-------------------------------------------------------------------------------
  Show instances

  Technically speaking the validity of these instances depends on 'IsString'
  instances which we do not (yet?) define.
-------------------------------------------------------------------------------}

instance Show SingleLoc         where show = show . prettySingleLoc ShowFile
instance Show MultiLoc          where show = show . prettyMultiLoc  ShowFile
instance Show (Range SingleLoc) where show = show . prettyRangeSingleLoc
instance Show (Range MultiLoc)  where show = show . prettyRangeMultiLoc

deriving stock instance {-# OVERLAPPABLE #-} Show a => Show (Range a)

{-------------------------------------------------------------------------------
  Pretty-printing

  These instances mimic the behaviour of @SourceLocation::print@ and
  @SourceRange::print@ in @clang@.
-------------------------------------------------------------------------------}

data ShowFile = ShowFile | HideFile

prettySingleLoc :: ShowFile -> SingleLoc -> String
prettySingleLoc showFile loc = case showFile of
    -- Use space instead of first colon to avoid GHC literate preprocessor mangling
    ShowFile -> getSourcePath singleLocPath ++ " "
                  ++ show singleLocLine ++ ":" ++ show singleLocColumn
    HideFile -> show singleLocLine ++ ":" ++ show singleLocColumn
  where
    SingleLoc{singleLocPath, singleLocLine, singleLocColumn} = loc

prettyMultiLoc :: ShowFile -> MultiLoc -> String
prettyMultiLoc showFile multiLoc =
    intercalate " " . concat $ [
        [ prettySingleLoc showFile multiLocExpansion ]
      , [ "<Presumed=" ++ presumed loc ++ ">" | Just loc <- [multiLocPresumed] ]
      , [ "<Spelling=" ++ single   loc ++ ">" | Just loc <- [multiLocSpelling] ]
      , [ "<File="     ++ single   loc ++ ">" | Just loc <- [multiLocFile]     ]
      ]
  where
    MultiLoc{
        multiLocExpansion
      , multiLocPresumed
      , multiLocSpelling
      , multiLocFile} = multiLoc

    presumed :: PresumedLoc -> [Char]
    presumed loc = single $ SingleLoc{
          singleLocPath   = presumedLocPath   loc
        , singleLocLine   = presumedLocLine   loc
        , singleLocColumn = presumedLocColumn loc
        , singleLocOffset = 0 -- not used for pretty-printing
        }

    single :: SingleLoc -> [Char]
    single loc =
        prettySingleLoc
          (if singleLocPath loc == singleLocPath multiLocExpansion
             then HideFile
             else ShowFile)
          loc

prettyRangeSingleLoc :: Range SingleLoc -> String
prettyRangeSingleLoc = prettySourceRangeWith
      singleLocPath
      prettySingleLoc

prettyRangeMultiLoc :: Range MultiLoc -> String
prettyRangeMultiLoc =
    prettySourceRangeWith
      (singleLocPath . multiLocExpansion)
      prettyMultiLoc

prettySourceRangeWith ::
     (a -> SourcePath)
  -> (ShowFile -> a -> String)
  -> Range a -> String
prettySourceRangeWith path pretty Range{rangeStart, rangeEnd} = concat [
      "<"
    , pretty ShowFile rangeStart
    , "-"
    , pretty
        (if path rangeStart == path rangeEnd then HideFile else ShowFile)
        rangeEnd
    , ">"
    ]

{-------------------------------------------------------------------------------
  Conversion
-------------------------------------------------------------------------------}

toMulti :: MonadIO m => Core.CXSourceLocation -> m MultiLoc
toMulti location = do
    expansion <- clang_getExpansionLocation location

    let differentSingle :: SingleLoc -> Maybe SingleLoc
        differentSingle loc = do
            guard $ singleLocPath   loc /= singleLocPath expansion
            guard $ singleLocLine   loc /= singleLocLine expansion
            guard $ singleLocColumn loc /= singleLocColumn expansion
            -- We don't compare the file offset
            return loc

        differentPresumed :: PresumedLoc -> Maybe PresumedLoc
        differentPresumed loc = do
            guard $ presumedLocPath   loc /= singleLocPath expansion
            guard $ presumedLocLine   loc /= singleLocLine expansion
            guard $ presumedLocColumn loc /= singleLocColumn expansion
            return loc

    MultiLoc expansion
      <$> (differentPresumed <$> clang_getPresumedLocation location)
      <*> (differentSingle   <$> clang_getSpellingLocation location)
      <*> (differentSingle   <$> clang_getFileLocation     location)


toRange :: MonadIO m => Core.CXSourceRange -> m (Range MultiLoc)
toRange = toRangeWith toMulti

fromSingle ::
     (MonadIO m, HasCallStack)
  => Core.CXTranslationUnit -> SingleLoc -> m Core.CXSourceLocation
fromSingle unit SingleLoc{singleLocPath, singleLocLine, singleLocColumn} = do
     let SourcePath path = singleLocPath
     file <- Core.clang_getFile unit path
     Core.clang_getLocation
       unit
       file
       (fromIntegral singleLocLine)
       (fromIntegral singleLocColumn)

fromRange ::
     (MonadIO m, HasCallStack)
  => Core.CXTranslationUnit -> Range SingleLoc -> m Core.CXSourceRange
fromRange unit Range{rangeStart, rangeEnd} = do
    rangeStart' <- fromSingle unit rangeStart
    rangeEnd'   <- fromSingle unit rangeEnd
    Core.clang_getRange rangeStart' rangeEnd'

{-------------------------------------------------------------------------------
  Get single location
-------------------------------------------------------------------------------}

clang_getExpansionLocation :: MonadIO m => Core.CXSourceLocation -> m SingleLoc
clang_getExpansionLocation location =
    toSingle =<< Core.clang_getExpansionLocation location

clang_getPresumedLocation :: MonadIO m => Core.CXSourceLocation -> m PresumedLoc
clang_getPresumedLocation location =
    toPresumed <$> Core.clang_getPresumedLocation location

clang_getSpellingLocation :: MonadIO m => Core.CXSourceLocation -> m SingleLoc
clang_getSpellingLocation location =
    toSingle =<< Core.clang_getSpellingLocation location

clang_getFileLocation :: MonadIO m => Core.CXSourceLocation -> m SingleLoc
clang_getFileLocation location =
    toSingle =<< Core.clang_getFileLocation location

{-------------------------------------------------------------------------------
  Convenience wrappers for @CXSourceLocation@
-------------------------------------------------------------------------------}

-- | Retrieve the source location of the given diagnostic.
clang_getDiagnosticLocation :: MonadIO m => Core.CXDiagnostic -> m MultiLoc
clang_getDiagnosticLocation diagnostic =
    toMulti =<< Core.clang_getDiagnosticLocation diagnostic

-- | Retrieve the physical location of the source constructor referenced by the
-- given cursor.
clang_getCursorLocation :: MonadIO m => Core.CXCursor -> m MultiLoc
clang_getCursorLocation cursor =
    toMulti =<< Core.clang_getCursorLocation cursor

-- | Like 'clang_getCursorLocation', but only retrieve the expansion location
clang_getCursorLocation' :: MonadIO m => Core.CXCursor -> m SingleLoc
clang_getCursorLocation' cursor =
    clang_getExpansionLocation =<< Core.clang_getCursorLocation cursor

-- | Retrieve the source location of the given token.
clang_getTokenLocation ::
     MonadIO m
  => Core.CXTranslationUnit -> Core.CXToken -> m MultiLoc
clang_getTokenLocation unit token =
    toMulti =<< Core.clang_getTokenLocation unit token

{-------------------------------------------------------------------------------
  Convenience wrappers for @CXSourceRange@
-------------------------------------------------------------------------------}

-- | Retrieve a source range associated with the diagnostic.
clang_getDiagnosticRange ::
     MonadIO m
  => Core.CXDiagnostic -> CUInt -> m (Range MultiLoc)
clang_getDiagnosticRange diagnostic range =
    toRange =<< Core.clang_getDiagnosticRange diagnostic range

-- | Retrieve the replacement information for a given fix-it.
clang_getDiagnosticFixIt ::
     MonadIO m
  => Core.CXDiagnostic
  -> CUInt
  -> m (Range MultiLoc, Text)
clang_getDiagnosticFixIt diagnostic fixit = do
    (range, replacement) <- Core.clang_getDiagnosticFixIt diagnostic fixit
    (, replacement) <$> toRange range

-- | Retrieve a range for a piece that forms the cursors spelling name.
clang_Cursor_getSpellingNameRange ::
     MonadIO m
  => Core.CXCursor
  -> CUInt
  -> CUInt
  -> m (Maybe (Range MultiLoc))
clang_Cursor_getSpellingNameRange cursor pieceIndex options = do
    mRange <- Core.clang_Cursor_getSpellingNameRange cursor pieceIndex options
    case mRange of
      Nothing    -> return Nothing
      Just range -> Just <$> toRangeWith toMulti range

-- | Retrieve the physical extent of the source construct referenced by the
-- given cursor.
clang_getCursorExtent :: MonadIO m => Core.CXCursor -> m (Range MultiLoc)
clang_getCursorExtent cursor =
    toRange =<< Core.clang_getCursorExtent cursor

-- | Retrieve a source range that covers the given token.
clang_getTokenExtent ::
     MonadIO m
  => Core.CXTranslationUnit
  -> Core.CXToken
  -> m (Range MultiLoc)
clang_getTokenExtent unit token =
    toRange =<< Core.clang_getTokenExtent unit token

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

toSingle :: MonadIO m => (Core.CXFile, CUInt, CUInt, CUInt) -> m SingleLoc
toSingle (file, line, column, offset) = do
    path <- Core.clang_getFileName file
    return SingleLoc{
        singleLocPath   = SourcePath   path
      , singleLocLine   = fromIntegral line
      , singleLocColumn = fromIntegral column
      , singleLocOffset = fromIntegral offset
      }

toPresumed :: (Text, CUInt, CUInt) -> PresumedLoc
toPresumed (path, line, column) = PresumedLoc{
      presumedLocPath   = SourcePath   path
    , presumedLocLine   = fromIntegral line
    , presumedLocColumn = fromIntegral column
    }

toRangeWith ::
     MonadIO m
  => (Core.CXSourceLocation -> m a)
  -> Core.CXSourceRange -> m (Range a)
toRangeWith f range =
    Range
      <$> (f =<< Core.clang_getRangeStart range)
      <*> (f =<< Core.clang_getRangeEnd   range)