packages feed

doctest-extract-0.1: src/Parse.hs

module Parse where

import Test.DocTest.Parse (DocTest, parseComment)
import Test.DocTest.Location (Located(Located), unLoc)

import qualified ModuleName

import qualified System.Path as Path

import qualified Data.NonEmpty as NonEmpty
import qualified Data.List.Reverse.StrictSpine as Rev
import qualified Data.List.HT as ListHT
import Data.Functor.Identity (Identity, runIdentity)
import Data.Traversable (traverse)
import Data.NonEmpty ((!:))
import Data.Maybe.HT (toMaybe)
import Data.Maybe (fromMaybe, isNothing)
import Data.Tuple.HT (mapFst)
import Data.Char (isSpace)

import qualified Control.Monad.Trans.State as MS
import qualified Control.Functor.HT as FuncHT
import Control.Monad (guard)
import Control.Applicative (optional, (<$>), (<|>))



type Line = Int
type Column = Int

data Module a =
   Module {
      moduleName :: ModuleName.T,
      modulePath :: Path.AbsRelFile,
      modulePragma :: Maybe (Located Line [String]),
      moduleSetup :: Maybe (Located Line [Located Column String]),
      moduleContent :: [a]
   } deriving (Show)

emptyModule :: Module a -> Bool
emptyModule (Module _ _ pragma setup tests) =
   null tests && isNothing pragma && isNothing setup


moduleFromLines ::
   ModuleName.T -> Path.AbsRelFile ->
   [Located Line String] -> Module [Located (Line, Column) String]
moduleFromLines modName path nLines =
   let (pragmas,setup) =
         FuncHT.unzip $ fmap extractPragmas $ extractInit "setup" nLines
   in Module {
         moduleName = modName,
         modulePath = path,
         modulePragma = pragmas,
         moduleSetup = setup,
         moduleContent = extractComments nLines
      }


type FlexParser = MS.StateT (Located Column String)
type LineParser = FlexParser Maybe

parseLine :: (Monad m) => FlexParser m a -> String -> m a
parseLine p line = MS.evalStateT p $ Located 0 line

parseWithLineNumber ::
   (Monad m) =>
   FlexParser m () -> Located n String -> m (Located (n,Column) String)
parseWithLineNumber p (Located n line) =
   parseLine (p >> getWithLineNumber n) line

getWithLineNumber ::
   (Monad m) => n -> FlexParser m (Located (n,Column) String)
getWithLineNumber n = MS.gets $ \(Located col line) -> Located (n,col) line

nextChar :: LineParser Char
nextChar = MS.StateT $ \(Located col line0) -> do
   (c,line1) <- ListHT.viewL line0
   return (c, Located (col+1) line1)

matchChar :: Char -> LineParser ()
matchChar c = guard . (c==) =<< nextChar

match :: String -> LineParser ()
match = mapM_ matchChar

matchEnd :: LineParser ()
matchEnd = MS.gets (null . unLoc) >>= guard

{-
For @m = Maybe@ it holds

> trap p = p <|> return ()
-}
trap :: (Monad m) => LineParser () -> FlexParser m ()
trap p = MS.modify $ \line -> fromMaybe line $ MS.execStateT p line

class (Monad m) => ToMaybe m where monadToMaybe :: m a -> Maybe a
instance ToMaybe Maybe where monadToMaybe = id
instance ToMaybe Identity where monadToMaybe = Just . runIdentity

skip :: (ToMaybe m) => (Char -> Bool) -> FlexParser m ()
skip cond =
   let go = trap (nextChar >>= guard . cond >> MS.mapStateT monadToMaybe go)
   in go

skipSpaces :: (ToMaybe m) => FlexParser m ()
skipSpaces = skip isSpace


matchCommentHead ::
   String -> Located n String ->
   Maybe ([Located n String] ->
          (NonEmpty.T [] (Located (n, Column) String), [Located n String]))
matchCommentHead ident (Located n line) =
   let matchIdent = match ident >> skipSpaces >> getWithLineNumber n
   in flip parseLine line $ do
         skipSpaces
         singleLineComments <$> (match "-- " >> matchIdent)
            <|>
            multiLineComment <$>
               (match "{-" >> optional (matchChar ' ') >> matchIdent)

extractPragmas ::
   Located Line [Located col String] ->
   (Located Line [String], Located Line [Located col String])
extractPragmas (Located start initial) =
   let (pragmas,setup) =
         ListHT.spanJust
            (\(Located _ ln) ->
               ListHT.maybePrefixOf ":set " ln
               <|>
               toMaybe (null $ dropWhile isSpace ln) "")
            initial
   in (Located start pragmas, Located (start + length pragmas) setup)

extractInit ::
   String -> [Located n String] -> Maybe (Located n [Located Column String])
extractInit name =
   fmap (\fsuff ->
      let NonEmpty.Cons (Located (n,col) ln) lns = fst $ uncurry ($) fsuff
      in Located n $
         ((Located col $
           if null ln then "" else "-- junk after setup keyword: " ++ ln) : ) $
         ListHT.takeWhileJust $
         map
            (parseLine (skipSpaces >> match ">>>" >>
                        (matchChar ' ' <|> matchEnd) >> MS.get) . unLoc)
            lns)
   .
   ListHT.dropWhileNothing (matchCommentHead ('$':name))

extractComments :: [Located n String] -> [[Located (n, Column) String]]
extractComments =
   let go ts =
         case ListHT.dropWhileNothing (matchCommentHead "|") ts of
            Nothing -> []
            Just (splitPart,suffix) ->
               let (part,remainder) = splitPart suffix
               in NonEmpty.flatten part : go remainder
   in go

singleLineComments ::
   Located (n, Column) String -> [Located n String] ->
   (NonEmpty.T [] (Located (n, Column) String), [Located n String])
singleLineComments firstLine suffix =
   mapFst (firstLine!:) $
   ListHT.spanJust
      (parseWithLineNumber $ skipSpaces >> match "--" >> skipSpaces)
      suffix

multiLineComment ::
   Located (n, Column) String -> [Located n String] ->
   (NonEmpty.T [] (Located (n, Column) String), [Located n String])
multiLineComment firstLine0 suffix0 =
   let maybeClosing = ListHT.maybeSuffixOf "-}" . Rev.dropWhile isSpace
   in case traverse maybeClosing firstLine0 of
         Just firstLine1 -> (NonEmpty.singleton firstLine1, suffix0)
         Nothing ->
            let (part,msuffix1) =
                  ListHT.breakJust (traverse maybeClosing) suffix0
                unindent = runIdentity . parseWithLineNumber skipSpaces
            in mapFst (NonEmpty.appendRight (firstLine0 !: map unindent part)) $
               case msuffix1 of
                  Nothing -> ([], [])
                  Just (lastLine,suffix1) -> ([unindent lastLine], suffix1)

numberedLines :: String -> [Located Line String]
numberedLines = zipWith Located [1..] . lines


-- | Convert documentation to `Example`s.
parseModule :: Module [Located pos String] -> Module [Located pos DocTest]
parseModule modu =
   modu {moduleContent =
            filter (not . null) $ map parseComment $ moduleContent modu}