packages feed

lentil-0.1.1.2: src/Lentil/Parse/Source.hs

-----------------------------------------------------------------------------
-- |
-- Module      :  Lentil.Parse.Source
-- Copyright   :  © 2015 Francesco Ariis
-- License     :  GPLv3 (see the LICENSE file)
--
-- Comments from source files
-----------------------------------------------------------------------------


module Lentil.Parse.Source where

import Text.Parsec hiding (Line)

import qualified Data.Char as C
import qualified System.Directory as D
import qualified System.IO as I
import qualified System.FilePath as SF

import Control.Applicative hiding (many, (<|>))

-----------
-- TYPES --
-----------

-- our parsers goal is simple: from a source file, only the comments and
-- newlines
type ParSource a = Parsec String () a

data ParSyntax = ParSyntax { psLineComms  :: [String],
                             psBlockComms :: [(String, String)],
                             psStringLits :: [Char],
                             psCharLits   :: [Char] }
                 deriving (Show)


------------
-- BLOCKS --
------------

-- TODO: ++ "\n" is inefficient [lint] [refactor]
lineComment :: String -> ParSource String
lineComment s = string s *> fmap (++ "\n") (manyTill anyChar newline)
                <?> "line comment"

blockComment :: (String, String) -> ParSource String
blockComment (i, e) = string i *> manyTill anyChar (try $ string e)
                      <?> "block comment"

-- quoted strings, escaped by \, by mauke^
litString :: Char -> ParSource String
litString ic = q *> many ((char '\\' *> anyChar) <|> noneOf no) <* q
               <?> "codestring"
    where q  = char ic
          no = C.showLitChar ic "\\"

-- quoted single character
litChar :: Char -> ParSource Char
litChar ic = q *> ((char '\\' *> anyChar) <|> anyChar) <* q
             <?> "char string sign"
    where q = char ic
    -- TODO: fallo diventare string così non impazzisci nelle altre parti
    --       [refactor]

-- a program is instructions to the computer. Ends when you meet
-- a well formed element from above (linecomm, blockcom, stringlit,
-- charLit)
program :: ParSyntax -> ParSource String
program ps = manyTill1 anyChar (endp <|> ("" <$ eof))
             <?> "program"
    where
          -- endp :: [ParSource String]
          endp  = lookAhead $ choice $ map try $ map string posts ++ [lchars]

          -- every unambiguous symbol (init comments, listring char)
          posts  = psLineComms ps ++ map fst (psBlockComms ps) ++
                   (map (:[]) $ psStringLits ps)

          -- ambiguous symbols (pall'aaa'foo is valid haskell identifier,
          -- so it should stay here
          lchars = "" <$ choice (map litChar (psCharLits ps))

          -- TODO: lint program parser [lint]


-- occhio che il primo carattere non è controllato da ED
-- TODO: manyTill1 broken? occhio che al primon on chiama ed! [test]
manyTill1 :: ParSource a -> ParSource b -> ParSource [a]
manyTill1 p ed = (:) <$> p <*> manyTill p ed


------------
-- SOURCE --
------------

-- given a set of lineparsers / blockparsers
source :: ParSyntax -> ParSource String
source ps@(ParSyntax lc bc sl cl) =
                choice (map (try . lineComment) lc) -- line comment
            <|> choice (map (try . blockComment) bc) -- block comment
            <|> (choice (map (onlynl . try . litString) sl))
            <|> ("" <$ (choice (map (try . litChar) cl))) -- todo ugly parens [lint] [refactor]
            <|> (onlynl $ program ps)
            <?> "source file"
    where onlynl a = fmap (filter (== '\n')) a

sources :: ParSyntax -> ParSource String
sources ps =   fmap concat (many1 (source ps))
           <|> ("" <$ eof)


--------------
-- SYNTAXES --
--------------

-- TODO: add langparsers che sia estensibile e leggibile
--       a compilazione [design] [u:3]

langParser :: String -> Maybe (ParSource String)
langParser fp | ext `elem` [".hs", ".lhs"]         = Just haskell
              | ext `elem` [".c", ".h"]            = Just c
              | ext `elem` [".cpp", ".hpp"]        = Just c -- C++
              | ext `elem` [".java"]               = Just c -- Java
              | ext `elem` [".js"]                 = Just javascript
              | ext `elem` [".py"]                 = Just python
              | ext `elem` [".rb"]                 = Just ruby
              | ext `elem` [".pas", ".pp", ".inc"] = Just pascal
              | ext `elem` [".txt"]                = Just text
              | otherwise                          = Nothing
    where ext = SF.takeExtension fp

haskell, c, pascal, text :: ParSource String
haskell    = sources $ ParSyntax ["--"] [("{-", "-}")] ['"']  ['\'']
c          = sources $ ParSyntax ["//"] [("/*", "*/")] ['"']  ['\'']
javascript = sources $ ParSyntax ["//"] [("/*", "*/")] ['"', '\''] []
pascal     = sources $ ParSyntax ["//"] [("{",  "}" ),
                                         ("(*", "*)")] ['\''] []
python     = sources $ ParSyntax ["#"]  [("\"\"\"",
                                          "\"\"\"")]   ['"', '\''] []
ruby       = sources $ ParSyntax ["#"] [("=begin",
                                         "=end")]      ['"', '\''] []
text       = many anyChar


-------------
-- PARSERS --
-------------


commentParser :: FilePath -> IO (Maybe String)
commentParser fp = case langParser fp of
                     Nothing -> return Nothing
                     Just p  -> fileParser fp p >>=
                                return . Just . ('\n':)
                                -- TODO: ugly hack to allow tODOs to be
                                -- parseable even in first line. This is
                                -- coupled with another hack in parser
                                -- `issue` (another module). [refactor]
                                -- [lint] [duct]


-- errors to stderr
fileParser :: FilePath -> ParSource String -> IO String
fileParser fp p = rfe fp >>= \cs ->
                  case runParSource p fp cs of
                      Left l  -> perr (fp ++ " : parse error " ++ show l) >>
                                 return []
                      Right r -> return r
    where rfe :: FilePath -> IO String
          rfe _ = D.doesFileExist fp >>= \fb ->
                  if fb then readFile fp
                        else perr (fp ++ " : no such file") >> return ""

          perr cs = I.hPutStrLn I.stderr cs

runParSource :: ParSource a -> FilePath -> String -> Either ParseError a
runParSource p fp cs = runParser p () fp cs