packages feed

hs-tags-0.1.5: Tags.hs

{-# LANGUAGE CPP                  #-}
{-# LANGUAGE FlexibleInstances    #-}
{-# LANGUAGE TypeFamilies         #-}

{-# LANGUAGE UndecidableInstances #-}

module Tags where

import Data.Function
import Data.List
import Data.Maybe
import Data.Map (Map)
import qualified Data.Map as Map

#if MIN_VERSION_ghc(8,10,1)
import GHC.Hs
#else
import HsSyn
#endif

#if MIN_VERSION_ghc(9,0,0)
import GHC.Types.SrcLoc
import GHC.Types.Name.Reader
import GHC.Types.Name.Occurrence
import qualified GHC.Types.Name as Name
import GHC.Data.FastString
import GHC.Data.Bag
import qualified GHC.Types.Basic as BasicTypes
#else
import SrcLoc
import RdrName
import OccName
import qualified Name
import FastString
import Bag
import qualified BasicTypes
#endif


data Pos = Pos { line, column :: Int }
           deriving (Eq, Ord)

data Tag = NoLoc String
         | Tag String FilePath Pos
  deriving (Eq, Ord)

-- | Removes duplicate /adjacent/ tags, ignoring the 'Pos' field.

removeDuplicates :: [Tag] -> [Tag]
removeDuplicates = map head . groupBy ((==) `on` everythingButPos)
  where
  dummyPos = Pos 0 0

  everythingButPos (Tag s f p) = Tag s f dummyPos
  everythingButPos t@NoLoc {}  = t

-- | Takes a list of (filename, file contents, tags) and generates
-- text for an etags file.

-- I found the etags file format on Wikipedia; I have not found an
-- authoritative definition of it.
--
-- For every file containing tags a section is generated.
-- Section header (two lines):
--   \x0c
--   <file name>,<size of the following lines in bytes>
-- This is followed by one line for every tag:
--   <text from start of line to end of tag>\x7f
--   <tag name>\x01
--   <line number>,<some form of offset in bytes>

showETags :: [(FilePath, String, [Tag])] -> String
showETags = concatMap showFile
  where
  showFile (f, contents, ts) =
    unlines ["\x0c", f ++ "," ++ show bytes] ++ ts'
    where
    ts' = unlines $ mapMaybe showTag ts

    -- TODO: This should be the length in _bytes_ of ts'. However,
    -- since the rest of this program seems to assume an 8-bit
    -- character encoding I just count the number of characters.
    bytes = length ts'

    lineMap = Map.fromList $ zip [1..] (lines contents)

    showTag (NoLoc _)   = Nothing
    showTag (Tag t f p) = Just $
      take' (column p) (lineMap ! line p) ++ t ++ "\x7f" ++
      t ++ "\x01" ++
      show (line p) ++ ",0"
      -- I don't know what the last offset is used for, so I have set
      -- it to 0. This seems to work.

    take' = tabAwareTake 0
    m ! k = Map.findWithDefault (error $ "Cannot find line " ++ show k ++ " in lineMap") k m

    -- A variant of take which is aware of tab characters. Uses tab
    -- size 8, and only recognises the ordinary ASCII horizontal tab
    -- ('\t'). The first argument is the position of the first
    -- character. Tabs are only expanded into spaces if necessary.
    tabAwareTake pos n s | n <= 0 = ""
    tabAwareTake pos n ""         = ""
    tabAwareTake pos n (c : s)
      | c /= '\t'    = c : tabAwareTake (pos + 1) (n - 1) s
      | stepSize > n = replicate n ' '
      | otherwise    = c : tabAwareTake nextTabStop (n - stepSize) s
      where
      tabSize     = 8
      nextTabStop = (pos `div` tabSize + 1) * tabSize
      stepSize    = nextTabStop - pos

instance Show Tag where
  show (Tag t f p) = intercalate "\t" [t, f, show (line p)]
  show (NoLoc t)   = unwords [t, ".", "0"]

srcLocTag :: SrcLoc -> Tag -> Tag
srcLocTag UnhelpfulLoc{} t         = t
#if MIN_VERSION_ghc(9,0,0)
srcLocTag (RealSrcLoc l _) (NoLoc t) =
#else
srcLocTag (RealSrcLoc l) (NoLoc t) =
#endif
  Tag t
      (unpackFS $ srcLocFile l)
      Pos { line   = srcLocLine l
            -- GHC 7 counts columns starting from 1.
          , column = srcLocCol l - 1
          }
srcLocTag _ t@Tag{}   = t

class TagName a where
  tagName :: a -> String

instance TagName RdrName where
  tagName x = case x of
    Unqual x  -> tagName x
    Qual _ x  -> tagName x
    Orig _ x  -> tagName x
    Exact x   -> tagName x

instance TagName OccName where
  tagName = unpackFS . occNameFS

instance TagName Name.Name where
  tagName = tagName . Name.nameOccName

#if MIN_VERSION_ghc(8,4,0)
instance (IdP pass ~ name, TagName name) => TagName (FieldOcc pass) where
#else
instance TagName a => TagName (FieldOcc a) where
#endif
#if MIN_VERSION_ghc(8,6,1)
  tagName (FieldOcc _ (L _ rdrName)) = tagName rdrName
  tagName (XFieldOcc _)              = missingImp "XFieldOcc"
#else
  tagName (FieldOcc (L _ rdrName) _) = tagName rdrName
#endif

class HasTags a where
  tags :: a -> [Tag]

instance HasTags Tag where
  tags x = [x]

instance HasTags a => HasTags [a] where
  tags = concatMap tags

instance (HasTags a, HasTags b) => HasTags (a, b) where
  tags (x, y) = tags x ++ tags y

instance HasTags a => HasTags (Maybe a) where
  tags = maybe [] tags

instance HasTags a => HasTags (Bag a) where
  tags = tags . bagToList

instance HasTags a => HasTags (Located a) where
  tags (L l x) = map (srcLocTag $ srcSpanStart l) $ tags x

newtype Name a = Name a

instance TagName name => HasTags (Name name) where
  tags (Name x) = [NoLoc $ tagName x]

tagsLN :: TagName name => Located name -> [Tag]
tagsLN = tags . fmap Name

tagsN :: TagName name => name -> [Tag]
tagsN = tags . Name

#if MIN_VERSION_ghc(9,0,0)
instance HasTags HsModule where
#else
#if MIN_VERSION_ghc(8,4,0)
instance ( IdP pass ~ name
         , TagName name
#if MIN_VERSION_ghc(8,10,1)
         , HasTags (XRec pass Pat)
#endif
         ) => HasTags (HsModule pass) where
#else
instance TagName name => HasTags (HsModule name) where
#endif
#endif
  tags HsModule{ hsmodExports = export
               , hsmodDecls   = decls
               } = tags decls -- TODO: filter exports

#if MIN_VERSION_ghc(8,4,0)
instance ( IdP pass ~ name
         , TagName name
#if MIN_VERSION_ghc(8,10,1)
         , HasTags (XRec pass Pat)
#endif
         ) => HasTags (HsDecl pass) where
#else
instance TagName name => HasTags (HsDecl name) where
#endif
  tags d = case d of
#if MIN_VERSION_ghc(8,10,1)
    KindSigD _ d  -> tags d
#endif
#if MIN_VERSION_ghc(8,6,1)
    TyClD _ d     -> tags d
    ValD _ d      -> tags d
    SigD _ d      -> tags d
    ForD _ d      -> tags d
    XHsDecl _     -> missingImp "XHsDecl"
#else
    TyClD d       -> tags d
    ValD d        -> tags d
    SigD d        -> tags d
    ForD d        -> tags d
#endif
    DocD{}        -> []
    SpliceD{}     -> []
    RuleD{}       -> []
    DefD{}        -> []
    InstD{}       -> []
    DerivD{}      -> []
    WarningD{}    -> []
    AnnD{}        -> []
    RoleAnnotD{}  -> []
#if !MIN_VERSION_ghc(8,6,1)
    VectD{}       -> []
#endif

#if MIN_VERSION_ghc(8,4,0)
instance (IdP pass ~ name, TagName name) => HasTags (FamilyDecl pass) where
#else
instance TagName name => HasTags (FamilyDecl name) where
#endif
  tags d = tagsLN (fdLName d)

instance HasTags (BasicTypes.Origin) where
  tags _ = []

#if MIN_VERSION_ghc(8,4,0)
instance (IdP pass ~ name, TagName name) => HasTags (TyClDecl pass) where
#else
instance TagName name => HasTags (TyClDecl name) where
#endif
#if MIN_VERSION_ghc(8,6,1)
  tags (FamDecl _ d) = tags d
#else
  tags (FamDecl d) = tags d
#endif
  tags d = tagsLN (tcdLName d) ++
    case d of
      DataDecl { tcdDataDefn = HsDataDefn { dd_cons = cons } }
        -> tags cons
      ClassDecl { tcdSigs = meths
                , tcdATs  = ats
                } -> tags (meths, ats)
      _ -> []

#if MIN_VERSION_ghc(8,4,0)
instance (IdP pass ~ name, TagName name) => HasTags (ConDecl pass) where
#else
instance TagName name => HasTags (ConDecl name) where
#endif
#if MIN_VERSION_ghc(8,6,1)
    tags (ConDeclGADT _ cns _ _ _ _ _ _) = concatMap tagsLN cns
    tags (ConDeclH98 _ cn _ _ _ cd _)    = tagsLN cn ++ tags cd
    tags (XConDecl _)                    = missingImp "XConDecl"
#else
  tags (ConDeclGADT cns _ _)    = concatMap tagsLN cns
  tags (ConDeclH98 cn _ _ cd _) = tagsLN cn ++ tags cd
#endif

#if MIN_VERSION_ghc(8,4,0)
instance (IdP pass ~ name, TagName name) => HasTags (ConDeclField pass) where
#else
instance TagName name => HasTags (ConDeclField name) where
#endif
#if MIN_VERSION_ghc(8,6,1)
  tags (ConDeclField _ x _ _) = concatMap tagsLN x
  tags (XConDeclField _)      = missingImp "XConDeclField"
#else
  tags (ConDeclField x _ _) = concatMap tagsLN x
#endif

-- Dummy instance.
instance HasTags (HsType name) where
  tags _ = []

#if MIN_VERSION_ghc(8,4,0)
instance ( IdP pass ~ name
         , TagName name
#if MIN_VERSION_ghc(8,10,1)
         , HasTags (XRec pass Pat)
#endif
         ) => HasTags (HsBind pass) where
#else
instance TagName name => HasTags (HsBind name) where
#endif
  tags d = case d of
    FunBind  { fun_id    = x   }      -> tagsLN x
    PatBind  { pat_lhs   = lhs }      -> tags lhs
    VarBind  { var_id    = x   }      -> tagsN x
    AbsBinds { abs_binds = bs  }      -> tags bs
#if !MIN_VERSION_ghc(8,4,1)
    AbsBindsSig { abs_sig_bind = bs } -> tags bs
#endif
#if MIN_VERSION_ghc(8,6,1)
    PatSynBind _ (PSB { psb_id = x }) -> tagsLN x
    PatSynBind _ XPatSynBind{}        -> missingImp "XPatSynBindPSB"
    XHsBindsLR{}                      -> missingImp "XHsBindsLR"
#else
    PatSynBind (PSB { psb_id = x }) -> tagsLN x
#endif


#if MIN_VERSION_ghc(8,4,0)
instance ( IdP pass ~ name
         , TagName name
#if MIN_VERSION_ghc(8,10,1)
         , HasTags (XRec pass Pat)
#endif
         ) => HasTags (Pat pass) where
#else
instance TagName name => HasTags (Pat name) where
#endif
  tags p = case p of
#if MIN_VERSION_ghc(8,6,1)
    VarPat _ x                 -> tagsLN x
#else
    VarPat x                   -> tagsLN x
#endif
#if MIN_VERSION_ghc(8,8,1)
    SigPat _ p _               -> tags p
#elif MIN_VERSION_ghc(8,6,1)
    SigPat _ p                 -> tags p
#endif
#if MIN_VERSION_ghc(8,6,1)
    LazyPat _ p                -> tags p
    AsPat _ x p                -> tags (fmap Name x, p)
    ParPat _ p                 -> tags p
    BangPat _ p                -> tags p
    ListPat _ ps               -> tags ps
    TuplePat _ ps _            -> tags ps
    XPat _                     -> missingImp "XPat"
#else
    LazyPat p                  -> tags p
    AsPat x p                  -> tags (fmap Name x, p)
    ParPat p                   -> tags p
    BangPat p                  -> tags p
    ListPat ps _ _             -> tags ps
    TuplePat ps _ _            -> tags ps
    SigPatIn p _               -> tags p
    SigPatOut p _              -> tags p
    PArrPat ps _               -> tags ps
#endif
#if MIN_VERSION_ghc(9,0,0)
    ConPat   { pat_args = ps } -> tags ps
#else
    ConPatIn _ ps              -> tags ps
    ConPatOut{ pat_args = ps } -> tags ps
    CoPat{}                    -> []
#endif
#if MIN_VERSION_ghc(8,6,1)
    NPlusKPat _ x _ _ _ _      -> tagsLN x
#else
    NPlusKPat x _ _ _ _ _      -> tagsLN x
#endif
    NPat{}                     -> []
    LitPat{}                   -> []
    WildPat{}                  -> []
    ViewPat{}                  -> []
    SplicePat{}                -> []
#if MIN_VERSION_ghc(8,2,0)
    SumPat{}                   -> missingImp "SumPat"
#endif

#if MIN_VERSION_ghc(9,0,0)
instance HasTags arg => HasTags (HsScaled pass arg) where
  tags = tags . hsScaledThing
#endif

instance (HasTags arg, HasTags recc) => HasTags (HsConDetails arg recc) where
  tags d = case d of
    PrefixCon as   -> tags as
    RecCon r       -> tags r
    InfixCon a1 a2 -> tags [a1, a2]

instance HasTags arg => HasTags (HsRecFields name arg) where
  tags (HsRecFields fs _) = tags fs

instance HasTags arg => HasTags (HsRecField name arg) where
  tags (HsRecField _ a _) = tags a

#if MIN_VERSION_ghc(8,4,0)
instance (IdP pass ~ name, TagName name) => HasTags (Sig pass) where
#else
instance TagName name => HasTags (Sig name) where
#endif
  tags d = case d of
#if MIN_VERSION_ghc(8,6,1)
    TypeSig _ x _       -> concatMap tagsLN x
#else
    TypeSig x _         -> concatMap tagsLN x
#endif
#if MIN_VERSION_ghc(8,6,1)
    PatSynSig _ x _      -> concatMap tagsLN x
#elif MIN_VERSION_ghc(8,2,0)
    PatSynSig x _       -> concatMap tagsLN x
#else
    PatSynSig x _       -> tagsLN x
#endif
#if MIN_VERSION_ghc(8,6,1)
    ClassOpSig _ _ x _   -> concatMap tagsLN x
#else
    ClassOpSig _ x _    -> concatMap tagsLN x
#endif
    FixSig{}            -> []
    InlineSig{}         -> []
    SpecSig{}           -> []
    SpecInstSig{}       -> []
    IdSig{}             -> []
    MinimalSig{}        -> []
#if MIN_VERSION_ghc(8,2,0)
    SCCFunSig{}         -> []
    CompleteMatchSig{}  -> []
#endif
#if MIN_VERSION_ghc(8,6,1)
    XSig{}              -> missingImp "XSig"
#endif

#if MIN_VERSION_ghc(8,4,0)
instance (IdP pass ~ name, TagName name) => HasTags (ForeignDecl pass) where
#else
instance TagName name => HasTags (ForeignDecl name) where
#endif
  tags d = case d of
#if MIN_VERSION_ghc(8,6,1)
    ForeignImport _ x _ _ -> tagsLN x
    XForeignDecl _        -> missingImp "XForeignDecl"
#else
    ForeignImport x _ _ _ -> tagsLN x
#endif
    ForeignExport{}       -> []

#if MIN_VERSION_ghc(8,10,1)
instance (IdP pass ~ name, TagName name) => HasTags (StandaloneKindSig pass) where
  tags d = case d of
    StandaloneKindSig _ x _ -> tagsLN x
    XStandaloneKindSig _    -> missingImp "XStandaloneKindSig"
#endif

missingImp :: String -> a
missingImp x = error $ "Missing implementation: " ++ x