doccheck-0.1.0.0: src/Documentation/DocCheck.hs
-- |
-- Module : Documentation.DocCheck
-- Copyright : (c) Mateusz Kowalczyk 2013,
-- License : GPLv3
--
-- Maintainer : fuuzetsu@fuuzetsu.co.uk
-- Stability : experimental
-- Portability : portable
{-# LANGUAGE OverloadedStrings #-}
module Documentation.DocCheck where
import Bag (bagToList)
import Control.Applicative ((*>), (<$>))
import Control.Monad (unless, liftM2)
import qualified Data.Attoparsec.Text as A (takeWhile, Parser, parseOnly)
import Data.Text (Text, pack)
import DynFlags (DynFlag(Opt_Haddock), getDynFlags, dopt_set)
import FastString (unpackFS)
import GHC (Ghc, HsDocString(..), DocDecl(..), HsDecl(..),
GenLocated(L), hsmodDecls, parser)
import System.Directory (doesFileExist, doesDirectoryExist)
import System.Directory.Tree (AnchoredDirTree(..), DirTree(..),
filterDir, readDirectoryWith,
flattenDir)
import System.FilePath (takeExtension)
import System.Exit (exitFailure)
-- | Extracts Haddock documentation from all the files provided. Note that any
-- modules that fail to parse will be reported as such. Note that the program
-- will still exit succesfully even if some files don't parse but no potential
-- warnings are found in regards to documentation.
extractDocs
:: [(FilePath, String)] -- ^ Files and their sources to extract the docs from
-> Ghc ([(FilePath, String)], [(FilePath, [String])])
extractDocs files = do
dflags' <- flip dopt_set Opt_Haddock <$> getDynFlags
let psed = map (\(f, s) -> (f, parser s dflags' f)) files
return (failedParses psed, map stripLoc $ rightLocs psed)
where
-- Determine which modules failed to parse
failedParses xs = [ (f, show $ bagToList m) | (f, Left m) <- xs ]
-- Pull out module declarations from successful parses, drop location
rightLocs xs = [ (f, hsmodDecls x) | (f, Right (_, L _ x)) <- xs ]
-- Get out Haddock comment strings from amongst other module declarations
stripLoc (f, ls) = (f, [ docDeclToString x | (L _ (DocD x)) <- ls ])
-- | Extracts a 'String' from 'DocDecl's by unpacking the 'FastString's
-- with 'unpackFs' packed in the 'HsDocString' data type.
docDeclToString :: DocDecl -> String
docDeclToString (DocCommentNext (HsDocString x)) = unpackFS x
docDeclToString (DocCommentPrev (HsDocString x)) = unpackFS x
docDeclToString (DocCommentNamed _ (HsDocString x)) = unpackFS x
docDeclToString (DocGroup _ (HsDocString x)) = unpackFS x
-- | Finds potential problems for each comment in each file and
-- formats the warning messages.
findIssues :: [(FilePath, [String])] -> [String]
findIssues fs = filter (not . null) $ map warn fs
where
warn :: (FilePath, [String]) -> String
warn (p, docs) =
case [ (d, x) |
(d, Just x) <- map (\d' -> (d', runParsers $ pack d')) docs ] of
[] -> []
xs -> "Potential problems in " ++ p
++ " :\n" ++ unlines (map formatProblems xs)
where
formatProblems :: (String, [String]) -> String
formatProblems (doc, issues) =
unlines $ map (++ " in ‘" ++ doc ++ "’") issues
-- | Runs multiple parsers on each of the strings and collects results of any
-- parsers that succeed. Note that these results will are used as the warning
-- messages so each parser should be in form of
-- @p = someParsing *> return \\"warning message for this parser\\"@
runParsers :: Text -> Maybe [String]
runParsers d = case [ x | Right x <- map (`A.parseOnly` d) parsers ] of
[] -> Nothing
xs -> Just xs
where
parsers :: [A.Parser String]
parsers = [escapingEmph, htmlEmph]
where
escapingEmph = A.takeWhile (/= '/') *> "/"
*> A.takeWhile (`notElem` "\n\\/") *> "\\"
*> return ("attempting to escape a character "
++ "inside of emphasis")
htmlEmph = A.takeWhile (/= '/') *> "/"
*> A.takeWhile (`notElem` "\n&/") *> "&#"
*> return "HTML sequence inside of emphasis"
-- | Walks the file system looking for Haskell source files, starting from the
-- given file.
getHaskellFiles :: FilePath -> IO [FilePath]
getHaskellFiles s = do
exists <- liftM2 (||) (doesFileExist s) (doesDirectoryExist s)
unless exists $ putStrLn ("no such file or directory: " ++ s) >> exitFailure
_:/tree <- readDirectoryWith return s
return $ map flattenFiles $ filter f $ flattenDir $ filterDir myPred tree
where
myPred (Dir ('.':_) _) = False
myPred (File n _) = e == ".hs" || e == ".lhs"
where e = takeExtension n
myPred _ = True
f (File _ _) = True
f _ = False
flattenFiles (File _ path) = path
flattenFiles x = error $ "Got non-file into flattenFiles: " ++ show x