CurryInfo: currydoc-5.0.0 / CurryDoc.Info.Comments

classes: Info
 
documentation: Info
 
name: Info
 CurryDoc.Info.Comments
operations: Info
 associateCurryDoc commentString isExportSection isOldStyleComment lookupClass lookupCons lookupDataDecl lookupField lookupFunc lookupInstance lookupNewDecl lookupRecord lookupTypeDecl lookupTypeSig readComments splitNestedComment
sourcecode: Info
 
{- |
     Author  : Kai-Oliver Prott
     Version : March 2025

     Operations and datatypes to read comment files and
     match comments to declarations.
-}
module CurryDoc.Info.Comments
  (-- * Main functions
   readComments, associateCurryDoc,
   -- * Comment functions
   splitNestedComment, commentString, isOldStyleComment, isExportSection,
   -- * Lookup functions
   lookupFunc, lookupCons, lookupField, lookupClass, lookupRecord, lookupInstance,
   lookupRecord, lookupTypeSig, lookupNewDecl, lookupDataDecl, lookupTypeDecl,
   -- * Datatypes
   Comment(..),
   CommentedDecl(..), ExportEntry(..), CommentedConstr(..), CommentedField )
 where

import CurryDoc.Data.AnaInfo
import CurryDoc.Info.Goodies

import AbstractCurry.Types
import AbstractCurry.Select

import Data.Char           ( isSpace )
import Data.Maybe          ( listToMaybe, mapMaybe, fromJust
                           , fromMaybe, maybeToList )
import Data.List           ( partition, init, last, isPrefixOf )
import System.Directory    ( doesFileExist )
import System.Path         ( getFileInPath )
import System.FilePath     ( takeFileName, (</>), (<.>))
import System.CurryPath    ( lookupModuleSourceInLoadPath, getLoadPathForModule
                           , inCurrySubdir, stripCurrySuffix )
import System.FrontendExec ( FrontendParams, FrontendTarget (..), defaultParams
                           , setQuiet, callFrontend, callFrontendWithParams )
import System.IO
import Curry.Span
import Curry.SpanInfo
import Curry.Types

data Comment = NestedComment String
             | LineComment   String
  deriving (Show, Read)

-- | Classification of a comment
data CDocComment = Pre     { comment :: Comment }
                 | Post    { comment :: Comment }
                 | None    { comment :: Comment }
                 | Section { comment :: Comment, nest :: Int }

-- | CurryDoc representation of declarations
data CommentedDecl
  = CommentedTypeDecl QName [Comment]
  | CommentedDataDecl QName [Comment] [CommentedConstr]
  | CommentedNewtypeDecl QName [Comment] CommentedConstr
  | CommentedClassDecl QName [Comment] [CommentedDecl]
  | CommentedInstanceDecl QName [CTypeExpr] [Comment] [CommentedDecl]
  | CommentedFunctionDecl QName [Comment]
  | CommentedTypeSig [QName] [Comment] [(CTypeExpr, [Comment])]
  | CommentedExternalDecl [QName] [Comment]
  | CommentedExternalData QName [Comment]
  | UnsupportedDecl [Comment]

data CommentedConstr
  = CommentedConstr QName [Comment]
  | CommentedConsOp QName [Comment]
  | CommentedRecord QName [Comment] [CommentedField]

type CommentedField = ([QName], [Comment])

data ExportEntry a = ExportEntry a
                   | ExportEntryModule MName
                   | ExportSection Comment Int [ExportEntry a]
  deriving (Show, Read)

-- | Reads the comments from a specified module
readComments :: String -> IO [(Span, Comment)]
readComments progname =
   readCommentsWithParseOptions progname (setQuiet True defaultParams)

-- | Reads the comments with further options from a specified module
readCommentsWithParseOptions :: String -> FrontendParams -> IO [(Span, Comment)]
readCommentsWithParseOptions progname options = do
  mbsrc <- lookupModuleSourceInLoadPath progname
  case mbsrc of
    Nothing -> do -- no source file, try to find Comments file in load path:
      filename <- fromJust <$> getFileInPath (commentsFileName (takeFileName progname))
      readCommentsFile filename
    Just (dir,_) -> do
      callFrontendWithParams COMMS options progname
      readCommentsFile (commentsFileName (dir </> takeFileName progname))

-- | Get the comments filename of a curry programm
commentsFileName :: String -> String
commentsFileName prog = inCurrySubdir (stripCurrySuffix prog) <.> "cycom"

-- | Reads the comments from a specified file
readCommentsFile :: String -> IO [(Span, Comment)]
readCommentsFile filename = do
  filecontents <- readCommentsFileRaw filename
  return (read filecontents)

-- | Reads the text from a specified file containing comments
readCommentsFileRaw :: String -> IO String
readCommentsFileRaw filename = do
  extfcy <- doesFileExist filename
  if extfcy
   then readFileContents filename
   else do let subdirfilename = inCurrySubdir filename
           exdirtfcy <- doesFileExist subdirfilename
           if exdirtfcy
            then readFileContents subdirfilename
            else error ("EXISTENCE ERROR: Comment file '" ++ filename ++
                        "' does not exist")
 where
  readFileContents fn = openFile fn ReadMode >>= hGetContents

-- | Associates given comments with declarations from given module
--   based on the source code positions
associateCurryDoc :: [(Span, Comment)] -> Module a
                  -> ([CommentedDecl], [Comment], Maybe [ExportEntry QName])
associateCurryDoc []       (Module _   _ _ _ ex _ _ ) =
  ([], [], maybe Nothing (Just . associateExports []) ex)
associateCurryDoc xs@(_:_) (Module spi _ _ _ ex im ds) =
  let (header, rest) = associateCurryDocHeader spi sp xs'
      exportList     = maybe Nothing (Just . associateExports xs') ex
      matchings      = cleanup $ merge $ associateCurryDocDecls rest ds Nothing
  in  (matchings, header, exportList)
  where xs' = map (\(sp',c) -> (sp', classifyComment c)) xs
        sp = case (im, ds) of
          ((i:_), _   ) -> getSrcSpan i
          (_    ,(d:_)) -> getSrcSpan d
          _             -> NoSpan

-- Associate Export with comments
associateExports :: [(Span, CDocComment)] -> ExportSpec -> [ExportEntry QName]
associateExports cs e = case e of
  Exporting (SpanInfo _ (sp:_)) ex
    -> associateExportList (skipUntilAfter sp cs) ex
  _ -> error $ "CurryDoc.Info.Comments.associateExports: " ++
               "Invalid SpanInfo in ExportList"

-- Associate ExportList with comments
associateExportList :: [(Span, CDocComment)] -> [Export] -> [ExportEntry QName]
associateExportList _  []       = []
associateExportList cs (e : es) =
  if getSrcSpan e `isBeforeList` (map fst cs) -- True for null cs
    then genExportEntry e : associateExportList cs es
    else let ((_,c):cs') = cs
             es' = associateExportList cs' (e:es)
         in case c of
              Section com n -> genExportSection n com es'
              _             -> es'
  where
    genExportEntry (Export _ q)           = ExportEntry       (qIdentToQName q)
    genExportEntry (ExportTypeAll _ q)    = ExportEntry       (qIdentToQName q)
    genExportEntry (ExportTypeWith _ q _) = ExportEntry       (qIdentToQName q)
    genExportEntry (ExportModule _ m)     = ExportEntryModule (mIdentToMName m)

-- | generate ExportSection from given nesting, comment
--   and the following ExportEntries
genExportSection :: Int -> Comment -> [ExportEntry QName] -> [ExportEntry QName]
genExportSection n c es =
  let (this, next) = span (\e -> not (isExportSection e) ||
                                     (sectionNesting e >= n)) es
      newComment   = case c of
        LineComment   s -> LineComment   $ drop n s
        NestedComment s -> NestedComment $ drop n s
  in  ExportSection newComment n this : next

sectionNesting :: ExportEntry a -> Int
sectionNesting e = case e of
   ExportSection _ n _ -> n
   _                   -> error $ "CurryDoc.Info.Comments.sectionNesting: "
                                  ++ "No ExportSection"

-- | Associates comments to the header of the module
--   (until the first declaration)
associateCurryDocHeader
                :: SpanInfo                           -- ^ module SpanInfo
                -> Span                               -- ^ first decl span
                -> [(Span, CDocComment)]              -- ^ to be matched
                -> ([Comment], [(Span, CDocComment)]) -- ^ (matched, rest)
associateCurryDocHeader spi@(SpanInfo _ (spm : ss)) sp (c:cs) =
  case c of
    (sp', Pre  _)
      | sp' `isAfter` sp      -> ([],c:cs)
      | vertDist sp' spm >= 0 ->
        let (match, next)   = getToMatch spm sp' (c:cs) isPre
            (matched, rest) = associateCurryDocHeader spi sp next
        in (map (comment . snd) match ++ matched, rest)
      | otherwise             ->
        let (matched, rest) = associateCurryDocHeader spi sp cs
        in (matched, c:rest)

    (sp', Post _)
      | sp' `isAfter` sp      -> ([],c:cs)
      | (vertDist sp' sp  >= 1
          || isNoSpan sp) &&
        isAfter sp' (last ss) ->
          let (match, next)   = getToMatch sp sp' (c:cs) isPost
              (matched, rest) = associateCurryDocHeader spi sp next
          in (map (comment . snd) match ++ matched, rest)
      | otherwise             ->
          let (matched, rest) = associateCurryDocHeader spi sp cs
          in  (matched, c:rest)

    (sp'  , _      )
      | sp' `isAfter` sp      -> ([],c:cs)
      | otherwise             ->
          let (matched, rest) = associateCurryDocHeader spi sp cs
          in  (matched, c:rest)
associateCurryDocHeader (SpanInfo _ []) _ (c:cs) = ([], c:cs)
associateCurryDocHeader NoSpanInfo      _ (c:cs) = ([], c:cs)
associateCurryDocHeader _               _ []     = ([], [])

-- | Associate the declarations of a module wih comments
--   based on the comment type and the vertical distance to the declaration
associateCurryDocDecls :: [(Span, CDocComment)]
                       -> [Decl a]
                       -> Maybe (Decl a) -- ^ previous decl
                       -> [CommentedDecl]
associateCurryDocDecls    []                _      _    = []
associateCurryDocDecls cs@(_         : _  ) []     prev = matchLast cs prev
associateCurryDocDecls cs@((sp, cdc) : cs') (d:ds) prev =
  case cdc of
    Pre  _ | vertDist sp spd >= 0 ->
               let (match, next) = getToMatch (stripStart spd) sp cs isPre
               in  associateCurryDocDeclPre match d
                     : associateCurryDocDecls next (d:ds) prev
           | otherwise ->
               associateCurryDocDecls cs ds (Just d)

    Post _ | vertDist sp spd >= 1 ->
               case prev of
                 Nothing -> associateCurryDocDecls cs (d:ds) prev
                 Just d' -> let (match, next) = getToMatch spd sp cs isPost
                            in  associateCurryDocDeclPost match d'
                                  : associateCurryDocDecls next (d:ds) prev
           | vertDist sp spd == 0 ->
               let (match, next) = getToMatch spNextd sp cs isPost
               in  associateCurryDocDeclPost match d
                     : associateCurryDocDecls next (d:ds) prev
           | otherwise ->
               associateCurryDocDecls cs ds (Just d)

    _ -> associateCurryDocDecls cs' (d:ds) prev
  where spd  = getSrcSpan d
        spNextd = case ds of
          []     -> NoSpan
          (d':_) -> getSrcSpan d'

-- match any comments that are left after the last declaration
matchLast :: [(Span, CDocComment)]
          -> Maybe (Decl a)
          -> [CommentedDecl]
matchLast []                        (Just _) = []
matchLast _                         Nothing  = []
matchLast (c@(_, Post    _  ) : cs) (Just d) =
  let (match, next) = getToMatch (getSrcSpan d) NoSpan (c:cs) isPost
  in  associateCurryDocDeclPost match d
        : matchLast next (Just d)
matchLast (  (_, None    _  ) : cs) (Just d) = matchLast cs (Just d)
matchLast (  (_, Pre     _  ) : cs) (Just d) = matchLast cs (Just d)
matchLast (  (_, Section _ _) : cs) (Just d) = matchLast cs (Just d)

-- Associate specific pre comments to a specific declaration
associateCurryDocDeclPre :: [(Span, CDocComment)]
                         -> Decl a
                         -> CommentedDecl
associateCurryDocDeclPre xs d@(FunctionDecl _ _ f _) =
  let (match, _) = getToMatch (getSrcSpan d) NoSpan xs isPre
  in  CommentedFunctionDecl (identToQName f) (map (comment . snd) match)
associateCurryDocDeclPre xs d@(ExternalDecl _ fs) =
  let (match, _) = getToMatch (getSrcSpan d) NoSpan xs isPre
  in  CommentedExternalDecl (map (\(Var _ i) -> identToQName i) fs)
                            (map (comment . snd) match)
associateCurryDocDeclPre xs d@(ExternalDataDecl _ f _) =
  let (match, _) = getToMatch (getSrcSpan d) NoSpan xs isPre
  in  CommentedExternalData (identToQName f) (map (comment . snd) match)
associateCurryDocDeclPre xs d@(TypeDecl _ f _ _) =
  let (match, _) = getToMatch (getSrcSpan d) NoSpan xs isPre
  in  CommentedTypeDecl (identToQName f) (map (comment . snd) match)
associateCurryDocDeclPre xs (ClassDecl spi _ _ f _ _ ds) =
  let (result, rest) = associateCurryDocHeader spi sp xs
      sp             = case ds of
                         (d:_) -> getSrcSpan d
                         _     -> NoSpan
  in  CommentedClassDecl (identToQName f) result
                         (associateCurryDocDecls rest ds Nothing)
associateCurryDocDeclPre xs (InstanceDecl spi _ _ f ts ds) =
  let (result, rest) = associateCurryDocHeader spi sp xs
      sp             = case ds of
                         (d:_) -> getSrcSpan d
                         _     -> NoSpan
  in  CommentedInstanceDecl (qIdentToQName f) (map typeExprToCType ts) result
                            (associateCurryDocDecls rest ds Nothing)
associateCurryDocDeclPre xs d@(NewtypeDecl _ f _ c _) =
  let (match, rest ) = getToMatch (getSrcSpan d) NoSpan xs isPre
      (cons,  rest') = getToMatch (getSrcSpan c) NoSpan rest isPre
      ccon           = case c of
        NewConstrDecl _ cn _
          -> CommentedConstr (identToQName cn) (map (comment . snd) cons)
        NewRecordDecl spi cn (idt, ty) ->
          let SpanInfo _ (sp:_) = spi -- sp = '{'
              field = matchFieldsPre [FieldDecl NoSpanInfo [idt] ty]
                                     (skipUntilAfter sp rest')
          in  CommentedRecord (identToQName cn) (map (comment . snd) cons)
                              field
  in postProcessDataDecl $ 
      CommentedNewtypeDecl (identToQName f) (map (comment . snd) match) ccon
associateCurryDocDeclPre xs d@(DataDecl _ f _ [] _) =
  let (match, _) = getToMatch (getSrcSpan d) NoSpan xs isPre
  in  postProcessDataDecl $ 
       CommentedDataDecl (identToQName f) (map (comment . snd) match) []
associateCurryDocDeclPre xs d@(DataDecl spi f _ (c:cs) _) =
  let (match, rest) = getToMatch (getSrcSpan d) NoSpan xs isPre
      SpanInfo _ (_:sp:_) = spi
  in postProcessDataDecl $
      CommentedDataDecl (identToQName f) (map (comment . snd) match)
                        (matchConstructorsPre (c:cs) (skipUntilAfter sp rest))
associateCurryDocDeclPre xs d@(TypeSig _ fs
  (QualTypeExpr (SpanInfo _ (s:ss)) _ ty)) =
  let (match, rest) = getToMatch (getSrcSpan d) NoSpan xs isPre
      sp = last (s:ss) -- throw away everything until '=>'
  in postProcessTypeSig $
      CommentedTypeSig (map identToQName fs) (map (comment . snd) match)
                       (matchArgumentsPre ty (skipUntilAfter sp rest))
associateCurryDocDeclPre xs d@(TypeSig spi fs
  (QualTypeExpr (SpanInfo _ []) _ ty)) =
   let (match, rest) = getToMatch (getSrcSpan d) NoSpan xs isPre
       SpanInfo _ [sp] = spi
   in postProcessTypeSig $ 
       CommentedTypeSig (map identToQName fs) (map (comment . snd) match)
                        (matchArgumentsPre ty (skipUntilAfter sp rest))
associateCurryDocDeclPre xs (InfixDecl _ _ _ _) = UnsupportedDecl
  (map (comment . snd) xs)
associateCurryDocDeclPre xs (DefaultDecl   _ _) = UnsupportedDecl
  (map (comment . snd) xs)
associateCurryDocDeclPre xs (PatternDecl _ _ _) = UnsupportedDecl
  (map (comment . snd) xs)
associateCurryDocDeclPre xs (FreeDecl      _ _) = UnsupportedDecl
  (map (comment . snd) xs)
associateCurryDocDeclPre _ (TypeSig _ _ (QualTypeExpr NoSpanInfo _ _)) =
  error "associateCurryDocDeclPre: NoSpanInfo in QualTypeExpr"

-- | Matches pre comments to arguments in a typesig
matchArgumentsPre :: TypeExpr -> [(Span, CDocComment)]
                  -> [(CTypeExpr, [Comment])]
matchArgumentsPre ty cs = case ty of
  ArrowType _ ty1 ty2 ->
    let (match, rest) = getToMatch (getSrcSpan ty) NoSpan cs isPre
    in  (typeExprToCType ty1, map (comment . snd) match)
          : matchArgumentsPre ty2 (skipUntilAfter (getSrcSpan ty1) rest)
  _                   ->
    let (match, _) = getToMatch (getSrcSpan ty) NoSpan cs isPre
    in  [(typeExprToCType ty , map (comment . snd) match)]

-- match pre comments to constructors
matchConstructorsPre :: [ConstrDecl] -> [(Span, CDocComment)]
                     -> [CommentedConstr]
matchConstructorsPre []       _  = []
matchConstructorsPre (RecordDecl spi f fs:cns) cs =
  let SpanInfo stop (sp:_) = spi
      (match, rest)        = getToMatch stop NoSpan cs isPre
      fields               = matchFieldsPre fs (skipUntilAfter sp cs)
  in  CommentedRecord (identToQName f) (map (comment . snd) match) fields
        : matchConstructorsPre cns (skipUntilAfter stop rest)
matchConstructorsPre (ConstrDecl spi f _ :cns) cs =
  let stop          = getSrcSpan spi
      (match, rest) = getToMatch stop NoSpan cs isPre
  in  CommentedConstr (identToQName f) (map (comment . snd) match)
        : matchConstructorsPre cns (skipUntilAfter stop rest)
matchConstructorsPre (ConOpDecl spi _ f _ :cns) cs =
  let stop          = getSrcSpan spi
      (match, rest) = getToMatch stop NoSpan cs isPre
  in  CommentedConsOp (identToQName f) (map (comment . snd) match)
        : matchConstructorsPre cns (skipUntilAfter stop rest)

-- match pre comments to record fields
matchFieldsPre :: [FieldDecl] -> [(Span, CDocComment)] -> [CommentedField]
matchFieldsPre []                            _  = []
matchFieldsPre (f@(FieldDecl _ idts _) : fs) cs =
  let (match, rest) = getToMatch (getSrcSpan f) NoSpan cs isPre
  in (map identToQName idts, map (comment . snd) match)
       : matchFieldsPre fs (skipUntilAfter (getSrcSpan f) rest)

associateCurryDocDeclPost :: [(Span, CDocComment)]
                          -> Decl a
                          -> CommentedDecl
associateCurryDocDeclPost xs d@(FunctionDecl _ _ f _) =
  CommentedFunctionDecl (identToQName f)
                        (map (comment . snd)
                             (skipUntilAfter (getSrcSpan d) xs))
associateCurryDocDeclPost xs d@(ExternalDecl _ fs) =
  CommentedExternalDecl (map (\(Var _ i) -> identToQName i) fs)
                        (map (comment . snd)
                             (skipUntilAfter (getSrcSpan d) xs))
associateCurryDocDeclPost xs d@(ExternalDataDecl _ f _) =
  CommentedExternalData (identToQName f)
                        (map (comment . snd)
                             (skipUntilAfter (getSrcSpan d) xs))
associateCurryDocDeclPost xs d@(TypeDecl _ f _ _) =
  CommentedTypeDecl (identToQName f) (map (comment . snd)
                    (skipUntilAfter (getSrcSpan d) xs))
associateCurryDocDeclPost xs (ClassDecl spi _ _ f _ _ ds) =
  let (result, rest) = associateCurryDocHeader spi sp xs
      sp = case ds of
             (d:_) -> getSrcSpan d
             _     -> NoSpan
  in  CommentedClassDecl (identToQName f) result
                         (associateCurryDocDecls rest ds Nothing)
associateCurryDocDeclPost xs (InstanceDecl spi _ _ f ts ds) =
  let (result, rest) = associateCurryDocHeader spi sp xs
      sp = case ds of
             (d:_) -> getSrcSpan d
             _     -> NoSpan
  in  CommentedInstanceDecl (qIdentToQName f) (map typeExprToCType ts) result
                            (associateCurryDocDecls rest ds Nothing)
associateCurryDocDeclPost xs (NewtypeDecl _ f _ c []) = --no deriving
  CommentedNewtypeDecl (identToQName f) [] -- thus cannot have post comments
                       (matchNewConstrPost c
                          (skipUntilAfter (getSrcSpan c) xs) xs)
associateCurryDocDeclPost xs (NewtypeDecl spi f _ c (_:_)) =
  let SpanInfo sp ss = spi
      (match, rest)  = getToMatch (last ss) NoSpan
                         (skipUntilAfter (getSrcSpan c) xs) isPost
  in CommentedNewtypeDecl (identToQName f)
                          (map (comment . snd) (skipUntilAfter sp rest))
                          (matchNewConstrPost c match xs)
associateCurryDocDeclPost xs d@(DataDecl _ f _ [] _) = -- cannot have deriving
  CommentedDataDecl (identToQName f)
                    (map (comment . snd) (skipUntilAfter (getSrcSpan d) xs)) []
associateCurryDocDeclPost xs (DataDecl _ f _ (c:cs) []) = -- no deriving
  CommentedDataDecl (identToQName f) []
                    (matchConstructorsPost (c:cs) xs)
associateCurryDocDeclPost xs (DataDecl spi f _ (c:cs) (_:_)) =
  let SpanInfo sp ss = spi
      (declC, consC) = partition ((`isAfter` sp) . fst) xs
      spDeriving     = ss !! (length cs + 2)
  in CommentedDataDecl (identToQName f) (map (comment . snd) declC)
                       (matchConstructorsPost (c:cs)
                         (filter ((`isBefore` spDeriving) . fst) consC))
associateCurryDocDeclPost xs (TypeSig _ fs (QualTypeExpr _ _ ty)) =
  CommentedTypeSig (map identToQName fs) [] (matchArgumentsPost ty xs)
associateCurryDocDeclPost xs (InfixDecl _ _ _ _) = UnsupportedDecl
  (map (comment . snd) xs)
associateCurryDocDeclPost xs (DefaultDecl   _ _) = UnsupportedDecl
  (map (comment . snd) xs)
associateCurryDocDeclPost xs (PatternDecl _ _ _) = UnsupportedDecl
  (map (comment . snd) xs)
associateCurryDocDeclPost xs (FreeDecl      _ _) = UnsupportedDecl
  (map (comment . snd) xs)

matchNewConstrPost :: NewConstrDecl -> [(Span, CDocComment)]
                   -> [(Span, CDocComment)] -> CommentedConstr
matchNewConstrPost (NewConstrDecl _ cn _) cs _ =
    CommentedConstr (identToQName cn)
                       (map (comment . snd) cs)
matchNewConstrPost (NewRecordDecl spiR cn (idt, ty)) cs xs =
    let SpanInfo _ ssR = spiR
        field = matchFieldsPost (last ssR) [FieldDecl NoSpanInfo [idt] ty]
                                (skipUntilAfter (getSrcSpan ty) xs)
    in CommentedRecord (identToQName cn) (map (comment . snd) cs) field

matchArgumentsPost :: TypeExpr -> [(Span, CDocComment)]
                   -> [(CTypeExpr, [Comment])]
matchArgumentsPost ty cs = case ty of
  ArrowType spi ty1 ty2 ->
    let SpanInfo _ [sp] = spi -- arrow span
        (match, rest) = getToMatch sp NoSpan
                          (skipUntilAfter (getSrcSpan ty1) cs) isPost
    in  (typeExprToCType ty1, map (comment . snd) match)
          : matchArgumentsPost ty2 rest
  _                     ->
    [(typeExprToCType ty , map (comment . snd)
                               (skipUntilAfter (getSrcSpan ty) cs))]

matchConstructorsPost :: [ConstrDecl] -> [(Span, CDocComment)]
                      -> [CommentedConstr]
matchConstructorsPost []           _  = []
matchConstructorsPost [c]          cs = case c of
  RecordDecl spi f fs   ->
    let SpanInfo spR (sp:ss) = spi
    in  [CommentedRecord (identToQName f)
                         (map (comment . snd) (skipUntilAfter spR cs))
                         (matchFieldsPost (last ss) fs (skipUntilAfter sp cs))]
  ConOpDecl _ _ f _ ->
    [CommentedConsOp (identToQName f)
                     (map (comment . snd) (skipUntilAfter (getSrcSpan c) cs))]
  ConstrDecl _ f _ ->
    [CommentedConstr (identToQName f)
                     (map (comment . snd) (skipUntilAfter (getSrcSpan c) cs))]
matchConstructorsPost (RecordDecl spi f fs:cn':cns) cs =
  let SpanInfo _ (sp:ss) = spi
      stop          = getSrcSpan cn'
      cs'           = skipUntilAfter (getSrcSpan spi) cs
      (match, rest) = getToMatch stop NoSpan cs' isPost
      fields        = matchFieldsPost (last ss) fs (skipUntilAfter sp cs)
  in  CommentedRecord (identToQName f) (map (comment . snd) match) fields
        : matchConstructorsPost (cn':cns) rest
matchConstructorsPost (ConstrDecl spi f _:cn':cns) cs =
  let stop          = getSrcSpan cn'
      cs'           = skipUntilAfter (getSrcSpan spi) cs
      (match, rest) = getToMatch stop NoSpan cs' isPost
  in  CommentedConstr (identToQName f) (map (comment . snd) match)
        : matchConstructorsPost (cn':cns) rest
matchConstructorsPost (ConOpDecl spi _ f _:cn':cns) cs =
  let stop          = getSrcSpan cn'
      cs'           = skipUntilAfter (getSrcSpan spi) cs
      (match, rest) = getToMatch stop NoSpan cs' isPost
  in  CommentedConsOp (identToQName f) (map (comment . snd) match)
        : matchConstructorsPost (cn':cns) rest

matchFieldsPost :: Span -- ^ Until
                -> [FieldDecl]
                -> [(Span, CDocComment)]
                -> [CommentedField]
matchFieldsPost _  []                                  _  = []
matchFieldsPost sp [f@(FieldDecl _ idts _)]            cs =
  let (match, _) = getToMatch sp NoSpan (skipUntilAfter (getSrcSpan f) cs)
                              isPost
  in  [(map identToQName idts, map (comment . snd) match)]
matchFieldsPost sp (f1@(FieldDecl _ idts _) : f2 : fs) cs =
  let (match, rest) = getToMatch (getSrcSpan f2) NoSpan
                                 (skipUntilAfter (getSrcSpan f1) cs) isPost
  in (map identToQName idts, map (comment . snd) match)
        : matchFieldsPost sp (f2:fs) (skipUntilAfter (getSrcSpan f2) rest)


-- relies on the fact that for subsequent entries of the same decl,
-- all comments in the first are before the comments of the second
-- and vice versa
merge :: [CommentedDecl] -> [CommentedDecl]
merge []                                 = []
merge [x]                                = [x]
merge (x1:x2:xs) = case (x1, x2) of
   (CommentedTypeDecl f1 cs1, CommentedTypeDecl f2 cs2)
     | f1 == f2 -> merge (CommentedTypeDecl f1 (cs1 ++ cs2) : xs)
   (CommentedDataDecl f1 cs1 cns1, CommentedDataDecl f2 cs2 cns2)
     | f1 == f2 -> merge (CommentedDataDecl f1 (cs1 ++ cs2)
                                            (zipWith zipCons cns1 cns2) : xs)
   (CommentedNewtypeDecl f1 cs1 cns1, CommentedNewtypeDecl f2 cs2 cns2)
     | f1 == f2 -> merge (CommentedNewtypeDecl f1 (cs1 ++ cs2)
                                               (zipCons cns1 cns2) : xs)
   (CommentedClassDecl f1 cs1 ds1, CommentedClassDecl f2 cs2 ds2)
     | f1 == f2 -> merge (CommentedClassDecl f1 (cs1 ++ cs2)
                                             (mergeLocal ds1 ds2) : xs)
   (CommentedInstanceDecl f1 ty1 cs1 ds1, CommentedInstanceDecl f2 ty2 cs2 ds2)
     | ty1 == ty2 &&
       f1 == f2 -> merge (CommentedInstanceDecl f1 ty1 (cs1 ++ cs2)
                                                (mergeLocal ds1 ds2) : xs)
   (CommentedFunctionDecl f1 cs1, CommentedFunctionDecl f2 cs2)
     | f1 == f2 -> merge (CommentedFunctionDecl f1 (cs1 ++ cs2) : xs)
   (CommentedTypeSig f1 cs1 ps1, CommentedTypeSig f2 cs2 ps2)
     | f1 == f2 -> merge (CommentedTypeSig f1 (cs1 ++ cs2)
                                              (zipWith zipPair ps1 ps2) : xs)
   (CommentedExternalDecl f1 cs1, CommentedExternalDecl f2 cs2)
     | f1 == f2 -> merge (CommentedExternalDecl f1 (cs1 ++ cs2) : xs)
   (CommentedExternalData f1 cs1, CommentedExternalData f2 cs2)
     | f1 == f2 -> merge (CommentedExternalData f1 (cs1 ++ cs2) : xs)
   _ -> x1 : merge (x2 : xs)

  where
    zipPair (a1, b1) (_, b2) = (a1, b1 ++ b2)

    zipCons a b = case (a, b) of
      ((CommentedConstr n1 cs1), (CommentedConstr _ cs2))
              -> CommentedConstr n1 (cs1 ++ cs2)
      ((CommentedRecord n1 cs1 fs1), (CommentedRecord _ cs2 fs2))
              -> CommentedRecord n1 (cs1 ++ cs2) (zipWith zipPair fs1 fs2)
      ((CommentedConsOp n1 cs1), (CommentedConsOp _ cs2))
              -> CommentedConsOp n1 (cs1 ++ cs2)
      _       -> error "Comment.merge.zipCons: different constructors"

-- | merge non-toplevel declarations,
--   as the assumption from above does not hold there
mergeLocal :: [CommentedDecl] -> [CommentedDecl] -> [CommentedDecl]
mergeLocal []     _   = []
mergeLocal (d:ds) ds' = (case d of
  CommentedTypeSig      f _ _ -> maybe d (combine d) (lookupTypeSig f ds')
  CommentedFunctionDecl f _   -> maybe d (combine d) (lookupFunc    f ds')
  _                           -> d)
  : mergeLocal ds ds'
  where combine d1 d2 = head (merge [d1, d2])


skipUntilAfter :: Span -> [(Span, a)] -> [(Span, a)]
skipUntilAfter sp = filter (( `isAfter` sp) . fst)

getToMatch :: Span                  -- ^ until
           -> Span                  -- ^ last undiscarded comment span
           -> [(Span, CDocComment)] -- ^ next comments
           -> (CDocComment -> Bool) -- ^ predicate to test the comment type
           -> ([(Span, CDocComment)], [(Span, CDocComment)])
getToMatch _    _    []             _ = ([], [])
getToMatch stop last ((sp, c) : cs) p =
  if (sp `isBefore` stop || isNoSpan stop)            -- pos is ok
     && (p c || (isNone c && vertDist last sp <= 1))  -- CDocType is ok
    then add (sp, c) (getToMatch stop sp cs p)
    else ([], (sp, c) : cs)
  where add x (xs, rest) = (x:xs, rest)

-------------------------------------------------------------------------------
-- Splitting of TypeSigs with multiple idents and field decls inside DataDecls
-- and filtering of UnsupportedDecls
-- also translates CommentedExternalDecl/Data
-- to CommmentedFunctionDecls/DataDecls

cleanup :: [CommentedDecl] -> [CommentedDecl]
cleanup [] = []
cleanup (d@(CommentedTypeDecl                _ _) : ds) = d :  cleanup ds
cleanup (d@(CommentedFunctionDecl            _ _) : ds) = d :  cleanup ds
cleanup (d@(CommentedNewtypeDecl           _ _ _) : ds) = d :  cleanup ds
cleanup (  (UnsupportedDecl                    _) : ds) =      cleanup ds
cleanup (  (CommentedExternalData           f cs) : ds) =
            CommentedDataDecl f cs []                       :  cleanup ds
cleanup (  (CommentedDataDecl           f cs cns) : ds) =
            CommentedDataDecl f cs (map cleanupConstr cns)  :  cleanup ds
cleanup (  (CommentedClassDecl          f cs ds') : ds) =
            CommentedClassDecl f cs (cleanup ds')           :  cleanup ds
cleanup (  (CommentedInstanceDecl    f ty cs ds') : ds) =
            CommentedInstanceDecl f ty cs (cleanup ds')     :  cleanup ds
cleanup (  (CommentedExternalDecl          fs cs) : ds) =
            map (\i -> CommentedFunctionDecl i cs) fs       ++ cleanup ds
cleanup (  (CommentedTypeSig        idts cs args) : ds) =
            map (\i -> CommentedTypeSig [i] cs args) idts   ++ cleanup ds

cleanupConstr :: CommentedConstr -> CommentedConstr
cleanupConstr c = case c of
  CommentedRecord f cs fs
    -> CommentedRecord f cs (concatMap cleanupField fs)
  _ -> c

constrName :: CommentedConstr -> QName
constrName (CommentedConstr n _  ) = n
constrName (CommentedRecord n _ _) = n
constrName (CommentedConsOp n _  ) = n

cleanupField :: CommentedField -> [CommentedField]
cleanupField (ns, cs) = map (\n -> ([n], cs)) ns

-------------------------------------------------------------------------------
-- utility for matching and conversions while matching

isPre, isPost, isNone, isSection :: CDocComment -> Bool
isPre  Pre     {} = True
isPre  Post    {} = False
isPre  None    {} = False
isPre  Section {} = False

isPost Pre     {} = False
isPost Post    {} = True
isPost None    {} = False
isPost Section {} = False

isNone Pre     {} = False
isNone Post    {} = False
isNone None    {} = True
isNone Section {} = True -- not a typo

isSection Pre     {} = False
isSection Post    {} = False
isSection None    {} = False
isSection Section {} = True

isExportSection :: ExportEntry a -> Bool
isExportSection e = case e of
  ExportSection _ _ _-> True
  _                  -> False

classifyComment :: Comment -> CDocComment
classifyComment (NestedComment s)
  | "{- |" `isPrefixOf` s = Pre     $ NestedComment $ dropLast2 $ drop 4 s
  | "{- ^" `isPrefixOf` s = Post    $ NestedComment $ dropLast2 $ drop 4 s
  | "{- *" `isPrefixOf` s = Section ( NestedComment $ dropLast2 $ drop 3 s ) n
  | otherwise             = None    $ NestedComment $ dropLast2 $ drop 2 s
  where n = length $ takeWhile (=='*') $ drop 3 s
        dropLast2 = init . init
classifyComment (LineComment   s)
  | "---"      ==       s = Pre     $ LineComment               $ drop 3 s
  | "--- " `isPrefixOf` s = Pre     $ LineComment               $ drop 4 s
  | "-- |" `isPrefixOf` s = Pre     $ LineComment               $ drop 4 s
  | "-- ^" `isPrefixOf` s = Post    $ LineComment               $ drop 4 s
  | "-- *" `isPrefixOf` s = Section ( LineComment               $ drop 3 s ) n
  | otherwise             = None    $ LineComment               $ drop 2 s
  where n = length $ takeWhile (=='*') $ drop 3 s

commentString :: Comment -> String
commentString (LineComment   s) = s
commentString (NestedComment s) = s

isOldStyleComment :: Comment -> Bool
isOldStyleComment (LineComment   s)
  | s == "---" || "--- " `isPrefixOf` s = True
  | otherwise                           = False
isOldStyleComment (NestedComment _)     = False

splitNestedComment :: Comment -> [Comment]
splitNestedComment c@(LineComment   _) = [c]
splitNestedComment   (NestedComment s) = map LineComment $ lines s

-------------------------------------------------------------------------------
-- Utility for handling old-style documentation comments.

-- | High-level representation of old-style comments
--   consisting of either a simple comment or a field comment
--   of shape "@field fieldname fieldcomment".
data OldStyleComment 
  = OldStyleComment Comment                          -- ^ Simple comment
  | OldStyleField   OldStyleFieldType String Comment -- ^ Field comment 
 deriving Show

-- | Field types for comments with "type-key-value" triples.
data OldStyleFieldType = Param | Return | Cons 
 deriving (Eq, Show)

-- | Post-processes a type signature by associating @parameter and @return 
--   comments with the appropriate type expression. We can simply add the 
--   comments as annotations to the type expression in-order, because the 
--   CurryDoc specification forces a strict left-to-right order of annotations.
--   That is, the parameter names are purely cosmetic and do not affect
--   which comment is associated with which parameter.
--
--   Consider the following example:
--
--       --- Some function!
--       --- @param x First parameter
--       --- @param y Second parameter
--       --- @returns Return value
--       f :: Int -> a -> Int
--
--   This is transformed to a CurryDoc representation as follows:
--
--       -- | Some function!
--       f :: Int -- ^ First parameter
--         -> a   -- ^ Second parameter
--         -> Int -- ^ Return value
--
postProcessTypeSig :: CommentedDecl -> CommentedDecl
postProcessTypeSig cd = case cd of
  CommentedTypeSig f cs ts ->
    -- Split comments into normal comments, @param comments, and @return comments.
    -- Drop @cons comments, as they are misplaced in type signatures.
    let (fcs1,  cs1) = partition isField $ commentsToOldStyleComments cs
        (fcsR, fcsP) = partition (hasField Return) $ filter (not . hasField Cons) fcs1
    in CommentedTypeSig f (map oldStyleCommentToComment cs1) $ insertReturn fcsR $ insertDescs fcsP ts 
  _ -> cd
 where 
  -- Inserts @param comments into the list of type expressions.
  insertDescs :: [OldStyleComment] -> [(CTypeExpr, [Comment])] -> [(CTypeExpr, [Comment])]
  insertDescs fcs ts = case ts of
    []     -> []
    t:ts'  -> case fcs of
      []     -> t : insertDescs fcs ts'
      f':fs   -> insertDesc t f' : insertDescs fs ts'

  -- Inserts an @param comment into a type expression.
  insertDesc :: (CTypeExpr, [Comment]) -> OldStyleComment -> (CTypeExpr, [Comment])
  insertDesc (t, cs) c = (t, cs ++ [oldStyleCommentToComment c])

  -- Inserts an @return comment into the last type expression.
  insertReturn :: [OldStyleComment] -> [(CTypeExpr, [Comment])] -> [(CTypeExpr, [Comment])]
  insertReturn fcs ts = case ts of
                                        -- If there are multiple @return comments,
                                        -- we only insert the first one.
    [t]    -> if null fcs then [t] else [insertDesc t $ head fcs]
    t:ts'  -> t : insertReturn fcs ts'
    []     -> []

-- | For the old documentation style, this function moves constructor
--   comments to the associated constructor declarations within
--   the data declaration.
--
--   Consider the following example:
--
--       --- Some data type
--       --- @cons C Some comment documenting the constructor C
--       data D a = C | D
--
--   This is transformed to a CurryDoc representation as follows:
-- 
--       data D a = C -- ^ Some comment documenting the constructor C
--                | D
--
postProcessDataDecl :: CommentedDecl -> CommentedDecl
postProcessDataDecl cd = case cd of
  CommentedDataDecl f cs cns -> 
    let (ccs', cs') = partition (hasField Cons) $ commentsToOldStyleComments cs
    in CommentedDataDecl f (map oldStyleCommentToComment cs') $ map (addToConstructor ccs') cns
  CommentedNewtypeDecl f cs cn ->
    let (ccs', cs') = partition (hasField Cons) $ commentsToOldStyleComments cs
    in CommentedNewtypeDecl f (map oldStyleCommentToComment cs') $ addToConstructor ccs' cn
  _ -> cd
 where
  addToConstructor :: [OldStyleComment] -> CommentedConstr -> CommentedConstr
  addToConstructor ccs c = case c of
    CommentedConstr n cs'    -> CommentedConstr n (consCom n ++ cs')
    CommentedRecord n cs' fs -> CommentedRecord n (consCom n ++ cs') fs
    CommentedConsOp n cs'    -> CommentedConsOp n (consCom n ++ cs')
   where 
    -- Retrieves the associated comment for some constructor name.
    consCom n = maybeToList $ lookupConsComment (snd n) ccs

-- | Looks up a constructor comment in a list of old-style comments.
lookupConsComment :: String -> [OldStyleComment] -> Maybe Comment
lookupConsComment = lookupFieldComment Cons

-- | Looks up a field comment in a list of old-style comments.
lookupFieldComment :: OldStyleFieldType -> String -> [OldStyleComment] -> Maybe Comment
lookupFieldComment _ _ []     = Nothing
lookupFieldComment ft n (c:cs) = case c of
  OldStyleField f n' c' | n == n' && ft == f -> Just c'
  _ -> lookupFieldComment ft n cs

-- | Converts a list of comments to old-style comments.
--
--   Consecutive comments are merged if the the first comment is a field comment
--   and the following comments are indented normal comments.
commentsToOldStyleComments :: [Comment] -> [OldStyleComment]
commentsToOldStyleComments = mergeOldStyleComments . map commentToOldStyleComment 

-- | Merges consecutive old-style comments where field comments are followed by
--   indented simple comments.
mergeOldStyleComments :: [OldStyleComment] -> [OldStyleComment]
mergeOldStyleComments [] = []
mergeOldStyleComments (oc:ocs) = case oc of
  OldStyleField ft name c -> 
    let (indented, rest) = takeIndentedComments ocs (getIndent oc)
        merged = if null indented 
                  then oc 
                  else OldStyleField ft name (mergeCommentContents c (map oldStyleCommentToComment indented))
    in merged : mergeOldStyleComments rest
  _ -> oc : mergeOldStyleComments ocs

-- | Takes all immediately following comments that are indented more than the
--   reference indentation level.
takeIndentedComments :: [OldStyleComment] -> Int -> ([OldStyleComment], [OldStyleComment])
takeIndentedComments [] _ = ([], [])
takeIndentedComments (oc:ocs) refIndent = case oc of
  OldStyleComment _ ->
    if getIndent oc > refIndent
      then let (more, rest) = takeIndentedComments ocs refIndent
           in (oc:more, rest)
      else ([], oc:ocs)
  _ -> ([], oc:ocs)

-- | Merges the contents of multiple comments into a single comment.
mergeCommentContents :: Comment -> [Comment] -> Comment
mergeCommentContents c cs = let content = unwords $ map commentString (c:cs)
                            in setCommentString c content

-- | Degradation of a comment to an old-style comment.
--   If the comment is of shape "@field fieldname fieldcomment",
--   it is converted to an 'OldStyleField' comment. 
commentToOldStyleComment :: Comment -> OldStyleComment
commentToOldStyleComment c = case words $ commentString c of
  (field:name:rest) -> maybe (OldStyleComment c) 
                             makeFieldComment
                               $ stringToOldStyleField field
   where 
    -- Creates a field comment from a field type, name, and comment.
    -- In case of an @return, in comparison to an @param or @cons, 
    -- there is no field name.
    makeFieldComment :: OldStyleFieldType -> OldStyleComment
    makeFieldComment f = case f of
      Return -> OldStyleField f ""   (setCommentString c $ cleanupFieldComment (name:rest))
      _      -> OldStyleField f name (setCommentString c $ cleanupFieldComment rest) 

    -- Drops preceding "-", if present, and concatenates the rest.
    cleanupFieldComment :: [String] -> String
    cleanupFieldComment ss = case ss of
      "-":ss' -> unwords ss'
      _       -> unwords ss
  _ -> OldStyleComment c

-- | Returns the indentation of an old-style comment, i.e., 
--   the number of spaces before the comment string.
getIndent :: OldStyleComment -> Int
getIndent oc = if indent == length str then 0 else indent
 where
  c      = oldStyleCommentToComment oc
  str    = commentString c
  indent = length $ takeWhile (== ' ') str

-- | Converts a string to an old-style field. Returns Nothing if the
--   string does not represent a field. The string must start with "@"
--   and be followed by a valid field name.
stringToOldStyleField :: String -> Maybe OldStyleFieldType
stringToOldStyleField s = case s of
  "@param"  -> Just Param
  "@return" -> Just Return
  "@cons"   -> Just Cons
  _         -> Nothing

-- | Checks if an old-style comment has a specific field.
hasField :: OldStyleFieldType -> OldStyleComment -> Bool
hasField f (OldStyleField f' _ _) = f == f'
hasField _ (OldStyleComment    _) = False

-- | Checks if an old-style comment is a field.
isField :: OldStyleComment -> Bool
isField (OldStyleField _ _ _) = True
isField (OldStyleComment   _) = False

-- | Extracts the documentation part of some old-style comment.
--
--   For a simple comment, the comment itself is returned.
--   For a comment of shape `@<field> <field name> <field comment>`,
--   the `<field comment>` is returned.
oldStyleCommentToComment :: OldStyleComment -> Comment
oldStyleCommentToComment (OldStyleComment   c) = c
oldStyleCommentToComment (OldStyleField _ _ c) = c

-- | Sets the string of a comment while preserving the comment type.
setCommentString :: Comment -> String -> Comment
setCommentString (LineComment   _) s = LineComment s
setCommentString (NestedComment _) s = NestedComment s

-------------------------------------------------------------------------------
-- lookup entries

lookupClass :: QName -> [CommentedDecl] -> Maybe CommentedDecl
lookupClass _ []     = Nothing
lookupClass n (d:ds) = case d of
  CommentedClassDecl n' _ _
    | n =~= n' -> Just d
  _            -> lookupClass n ds

lookupInstance :: QName -> [CTypeExpr] -> [CommentedDecl] -> Maybe CommentedDecl
lookupInstance _ _  []     = Nothing
lookupInstance n ts (d:ds) = case d of
  CommentedInstanceDecl n' ts' _ _
    | n =~= n' && sameTypes ts' -> Just d
  _                             -> lookupInstance n ts ds
 where
  sameTypes ts' = length ts == length ts' && and (zipWith (=~~=) ts ts')

lookupFunc :: QName -> [CommentedDecl] -> Maybe CommentedDecl
lookupFunc _ []     = Nothing
lookupFunc n (d:ds) = case d of
  CommentedFunctionDecl n' _
    | n =~= n' -> Just d
  _            -> lookupFunc n ds

lookupTypeDecl :: QName -> [CommentedDecl] -> Maybe CommentedDecl
lookupTypeDecl _ []     = Nothing
lookupTypeDecl n (d:ds) = case d of
  CommentedTypeDecl n' _
    | n =~= n' -> Just d
  _            -> lookupTypeDecl n ds

lookupDataDecl :: QName -> [CommentedDecl] -> Maybe CommentedDecl
lookupDataDecl _ []     = Nothing
lookupDataDecl n (d:ds) = case d of
  CommentedDataDecl n' _ _
    | n =~= n' -> Just d
  _            -> lookupDataDecl n ds

lookupNewDecl :: QName -> [CommentedDecl] -> Maybe CommentedDecl
lookupNewDecl _ []     = Nothing
lookupNewDecl n (d:ds) = case d of
  CommentedNewtypeDecl n' _ _
    | n =~= n' -> Just d
  _            -> lookupNewDecl n ds

lookupTypeSig :: [QName] -> [CommentedDecl] -> Maybe CommentedDecl
lookupTypeSig _ []     = Nothing
lookupTypeSig n (d:ds) = case d of
  CommentedTypeSig n' _ _
    | all (uncurry (=~=)) (zip n n')
       -> Just d
  _    -> lookupTypeSig n ds

lookupField :: QName -> [CommentedField] -> Maybe CommentedField
lookupField _ []     = Nothing
lookupField n (f:fs) = case f of
  ([n'], _)
    | n =~= n' -> Just f
  _            -> lookupField n fs

lookupRecord :: QName -> [CommentedConstr] -> Maybe CommentedConstr
lookupRecord _ []     = Nothing
lookupRecord n (c:cs) = case c of
  CommentedRecord n' _ _
    | n =~= n' -> Just c
  _            -> lookupRecord n cs

lookupCons :: QName -> [CommentedConstr] -> Maybe CommentedConstr
lookupCons _ []     = Nothing
lookupCons n (c:cs) = case c of
  CommentedConstr n'  _
    | n =~= n' -> Just c
  CommentedConsOp n'  _
    | n =~= n' -> Just c
  _            -> lookupCons n cs
types: Info
 Comment CommentedConstr CommentedDecl CommentedField ExportEntry
unsafe: Info
 safe