packages feed

hdocs 0.2.0.0 → 0.3.0.0

raw patch · 6 files changed

+274/−194 lines, 6 files

Files

hdocs.cabal view
@@ -1,5 +1,5 @@ name:                hdocs
-version:             0.2.0.0
+version:             0.3.0.0
 synopsis:            Haskell docs tool
 description:
   Tool and library to get docs for installed packages and source files.
@@ -24,6 +24,8 @@ library
   hs-source-dirs: src
   exposed-modules:
+    HDocs.Base
+    HDocs.Haddock
     HDocs.Module
   build-depends:
     base == 4.6.*,
@@ -52,6 +54,7 @@     bytestring == 0.10.*,
     containers == 0.5.*,
     filepath == 1.3.*,
+    mtl == 2.1.*,
     network == 2.4.*,
     text == 0.11.*
 
@@ -62,4 +65,5 @@   build-depends:
     base == 4.6.*,
     hdocs,
-    containers == 0.5.*
+    containers == 0.5.*,
+    mtl == 2.1.*
+ src/HDocs/Base.hs view
@@ -0,0 +1,80 @@+module HDocs.Base (
+	ModuleDocMap,
+	withInitializedPackages, configSession,
+	formatDoc, formatDocs
+	) where
+
+import Data.Char (isSpace)
+import Data.Map (Map)
+import qualified Data.Map as M
+
+import Documentation.Haddock
+
+import DynFlags
+import GHC
+import GHC.Paths
+import qualified GhcMonad as GHC (liftIO)
+import Name (occNameString)
+import Packages
+
+-- | Documentation in module
+type ModuleDocMap = Map String (Doc String)
+
+-- | Run action with initialized packages
+withInitializedPackages :: [String] -> (DynFlags -> IO a) -> IO a
+withInitializedPackages ghcOpts cont = do
+	runGhc (Just libdir) $ do
+		fs <- getSessionDynFlags
+		defaultCleanupHandler fs $ do
+			(fs', _, _) <- parseDynamicFlags fs (map noLoc ghcOpts)
+			setSessionDynFlags fs'
+			(result, _) <- GHC.liftIO $ initPackages fs'
+			GHC.liftIO $ cont result
+
+-- | Config GHC session
+configSession :: [String] -> IO DynFlags
+configSession ghcOpts = do
+	runGhc (Just libdir) $ do
+		fs <- getSessionDynFlags
+		defaultCleanupHandler fs $ do
+			(fs', _, _) <- parseDynamicFlags fs (map noLoc ghcOpts)
+			setSessionDynFlags fs'
+			(result, _) <- GHC.liftIO $ initPackages fs'
+			return result
+
+-- | Format documentation to plain text.
+formatDoc :: Doc String -> String
+formatDoc = trim . go where
+	go DocEmpty = ""
+	go (DocAppend a b) = go a ++ go b
+	go (DocString str) = trimSpaces str
+	go (DocParagraph p) = go p ++ "\n"
+	go (DocIdentifier i) = i
+	go (DocIdentifierUnchecked (mname, occname)) = moduleNameString mname ++ "." ++ occNameString occname
+	go (DocModule m) = m
+	go (DocEmphasis e) = "*" ++ go e ++ "*"
+	go (DocMonospaced e) = "`" ++ go e ++ "`"
+	go (DocUnorderedList i) = unlines (map (("* " ++) . go) i)
+	go (DocOrderedList i) = unlines (zipWith (\i' x -> show i' ++ ". " ++ go x) ([1..] :: [Integer]) i)
+	go (DocDefList xs) = unlines (map (\(i,x) -> go i ++ ". " ++ go x) xs)
+	go (DocCodeBlock block) = unlines (map ("    " ++) (lines (go block))) ++ "\n"
+	go (DocHyperlink (Hyperlink url label)) = maybe url (\l -> l ++ "[" ++ url ++ "]") label
+	go (DocPic pic) = pic
+	go (DocAName name) = name
+	go (DocExamples exs) = unlines (map formatExample exs)
+
+	formatExample :: Example -> String
+	formatExample (Example expr result) = "    > " ++ expr ++ unlines (map ("    " ++) result)
+
+	trimSpaces [] = []
+	trimSpaces [s] = [s]
+	trimSpaces (' ':' ':ss) = trimSpaces (' ':ss)
+	trimSpaces (x:y:ss) = x : trimSpaces(y:ss)
+
+	trim :: String -> String
+	trim = p . p where
+		p = reverse . dropWhile isSpace
+
+-- | Format docs to plain text
+formatDocs :: ModuleDocMap -> Map String String
+formatDocs = M.map formatDoc
+ src/HDocs/Haddock.hs view
@@ -0,0 +1,97 @@+module HDocs.Haddock (
+	-- * Documentation functions
+	readInstalledDocs,
+	readHaddock,
+	readSource,
+
+	-- * Extract docs
+	installedInterfaceDocs, installedInterfacesDocs,
+	interfaceDocs,	
+
+	-- * Utility functions
+	haddockFiles,
+	readInstalledInterfaces, readPackageInterfaces,
+	lookupDoc, lookupNameDoc,
+
+	module HDocs.Base
+	) where
+
+import Control.Applicative
+import Control.Arrow
+import Control.Exception
+import Control.Monad.Error
+import Data.Map (Map)
+import qualified Data.Map as M
+import Data.Maybe (listToMaybe)
+
+import Documentation.Haddock
+
+import DynFlags
+import Module
+import Name
+import PackageConfig
+
+import HDocs.Base
+
+-- | Read all installed docs
+readInstalledDocs :: [String] -> ErrorT String IO (Map String ModuleDocMap)
+readInstalledDocs opts = do
+	fs <- haddockFiles opts
+	liftM M.unions $ forM fs $ \f -> (readHaddock f) `mplus` (return M.empty)
+
+-- | Read docs from .haddock file
+readHaddock :: FilePath -> ErrorT String IO (Map String ModuleDocMap)
+readHaddock f = M.fromList . map installedInterfaceDocs <$> readInstalledInterfaces f
+
+-- | Read docs for haskell module
+readSource :: [String] -> FilePath -> ErrorT String IO (String, ModuleDocMap)
+readSource opts f = do
+	ifaces <- liftIO $ createInterfaces ([Flag_Verbosity "0", Flag_NoWarnings] ++ map Flag_OptGhc opts) [f]
+	iface <- maybe (throwError $ "Failed to load docs for " ++ f) return $ listToMaybe ifaces
+	return $ interfaceDocs iface
+
+-- | Get docs for 'InstalledInterface'
+installedInterfaceDocs :: InstalledInterface -> (String, ModuleDocMap)
+installedInterfaceDocs = stringize . (instMod &&& instDocMap)
+
+-- | Get docs for 'InstalledInterface's
+installedInterfacesDocs :: [InstalledInterface] -> Map String ModuleDocMap
+installedInterfacesDocs = M.fromList . map installedInterfaceDocs
+
+-- | Get docs for 'Interface'
+interfaceDocs :: Interface -> (String, ModuleDocMap)
+interfaceDocs = stringize . (ifaceMod &&& ifaceDocMap)
+
+-- | Get list of haddock files in package db
+haddockFiles :: [String] -> ErrorT String IO [FilePath]
+haddockFiles opts = ErrorT $ withInitializedPackages opts $ return . maybe
+	(Left "Package database empty")
+	(Right . concatMap haddockInterfaces) .
+	pkgDatabase
+
+-- | Read installed interface
+readInstalledInterfaces :: FilePath -> ErrorT String IO [InstalledInterface]
+readInstalledInterfaces f = do
+	ifile <- liftError $ ErrorT $ readInterfaceFile freshNameCache f
+	return $ ifInstalledIfaces ifile
+
+-- | Read installed interfaces for package
+readPackageInterfaces :: PackageConfig -> ErrorT String IO [InstalledInterface]
+readPackageInterfaces = liftM concat . mapM readInstalledInterfaces . haddockInterfaces
+
+-- | Lookup doc
+lookupDoc :: String -> String -> Map String ModuleDocMap -> Maybe (Doc String)
+lookupDoc m n = M.lookup m >=> M.lookup n
+
+-- | Lookup doc for Name
+lookupNameDoc :: Name -> Map String ModuleDocMap -> Maybe (Doc String)
+lookupNameDoc n = lookupDoc (moduleNameString $ moduleName $ nameModule n) (getOccString n)
+
+stringize :: (Module, Map Name (Doc Name)) -> (String, ModuleDocMap)
+stringize = moduleNameString . moduleName *** strDoc where
+	strDoc = M.mapKeys getOccString . M.map (fmap getOccString)
+
+liftError :: ErrorT String IO a -> ErrorT String IO a
+liftError = ErrorT . handle onErr . runErrorT where
+	onErr :: SomeException -> IO (Either String a)
+	onErr = return . Left . show
src/HDocs/Module.hs view
@@ -1,196 +1,82 @@ module HDocs.Module (
-	-- * Types
-	ModuleDocMap,
-	DocsM,
-	runDocsM,
+	-- * Get module docs
+	moduleDocs, installedDocs,
 
-	-- * Helpers
-	withInitializedPackages,
-	configSession,
-	moduleInterface,
-	packageInterface,
-	formatDoc,
+	-- * Utility
+	exportsDocs,
 
-	-- * Get module docs
-	moduleDocs,
-	fileDocs,
-	docs
+	module HDocs.Base
 	) where
 
-import Control.Arrow
-import Control.Exception
-import Control.Monad.State
+import Control.Applicative
 import Control.Monad.Error
 
-import Data.Either
-import Data.Char (isSpace)
+import Data.List
 import Data.Map (Map)
 import qualified Data.Map as M
+import Data.Maybe
 
 import Documentation.Haddock
 
-import System.FilePath (takeExtension)
-
 import DynFlags
-import GHC
-import GHC.Paths (libdir)
-import qualified GhcMonad as GHC (liftIO)
 import Module
-import Name (getOccString, occNameString)
 import Packages
-
--- | Documentations in module
-type ModuleDocMap = Map String (Doc String)
-
-withInitializedPackages :: [String] -> (DynFlags -> IO a) -> IO a
-withInitializedPackages ghcOpts cont = do
-	runGhc (Just libdir) $ do
-		fs <- getSessionDynFlags
-		defaultCleanupHandler fs $ do
-			(fs', _, _) <- parseDynamicFlags fs (map noLoc ghcOpts)
-			setSessionDynFlags fs'
-			(result, _) <- GHC.liftIO $ initPackages fs
-			GHC.liftIO $ cont result
-
-configSession :: [String] -> IO DynFlags
-configSession ghcOpts = do
-	runGhc (Just libdir) $ do
-		fs <- getSessionDynFlags
-		defaultCleanupHandler fs $ do
-			(fs', _, _) <- parseDynamicFlags fs (map noLoc ghcOpts)
-			setSessionDynFlags fs'
-			(result, _) <- GHC.liftIO $ initPackages fs'
-			return result
+import Name
 
--- | Docs state
-type DocsM a = ErrorT String (StateT (Map String ModuleDocMap) IO) a
+import HDocs.Base
+import qualified HDocs.Haddock as H
 
--- | Run docs monad
-runDocsM :: DocsM a -> IO (Either String a)
-runDocsM act = catch (evalStateT (runErrorT act) M.empty) onError where
-	onError :: SomeException -> IO (Either String a)
-	onError = return . Left . show
+-- | Load docs for all exported module symbols
+moduleDocs :: [String] -> String -> ErrorT String IO ModuleDocMap
+moduleDocs opts mname = ErrorT $ withInitializedPackages opts (runErrorT . moduleDocs') where
+	moduleDocs' :: DynFlags -> ErrorT String IO ModuleDocMap
+	moduleDocs' d = do
+		pkg <- case pkgs of
+			[] -> throwError $ "Module " ++ mname ++ " not found"
+			[pkg] -> return pkg
+			_ -> throwError $ "Module " ++ mname ++ " found in several packages: " ++ intercalate ", " (map pkgId pkgs)
+		ifaces <- H.readPackageInterfaces pkg
+		iface <- maybe
+			(throwError $ "Module " ++ mname ++ " not found in package " ++ pkgId pkg)
+			return
+			(find ((== mname) . moduleNameString . moduleName . instMod) ifaces)
 
-class DocInterface a where
-	getModule :: a -> Module
-	getDocMap :: a -> DocMap Name
-	getExports :: a -> [Name]
-	loadInterfaces :: [String] -> String -> IO [a]
+		depsfaces <- liftM concat $ mapM H.readPackageInterfaces $
+			map (getPackageDetails (pkgState d)) $ ifacePackageDeps iface
 
-instance DocInterface InstalledInterface where
-	getModule = instMod
-	getDocMap = instDocMap
-	getExports = instExports
-	loadInterfaces opts m = withInitializedPackages opts $ \d ->
-		liftM (map snd) $ moduleInterface d (mkModuleName m)
+		let
+			deps = filter (ifaceDep iface) $ ifaces ++ depsfaces
 
-instance DocInterface Interface where
-	getModule = ifaceMod
-	getDocMap = ifaceDocMap
-	getExports = const [] -- ifaceExports
-	loadInterfaces opts m = do
-		createInterfaces (noOutput ++ map Flag_OptGhc opts) [m]
+		return $ snd $ exportsDocs (H.installedInterfacesDocs deps) iface
 		where
-			noOutput = [
-				Flag_Verbosity "0",
-				Flag_NoWarnings]
-
--- | Load docs from interface
-interfaceDocs :: DocInterface a => a -> [String] -> String -> DocsM ModuleDocMap
-interfaceDocs h opts m = do
-	loaded <- gets (M.lookup m)
-	case loaded of
-		Just d' -> return d'
-		Nothing -> do
-			ifaces <- liftM (`asTypeOf` [h]) $ liftIO $ loadInterfaces opts m
-			when (null ifaces) $ throwError $ "Unable to load interface for module: " ++ m
-			modify $ M.insert m $ M.unions $ map interfaceNameMap ifaces
-			let
-				reexports = filter ((/= m) . moduleNameString . moduleName . nameModule) $ concatMap getExports ifaces
-			docs' <- liftM M.unions $ forM reexports $ \nm -> do
-				doc <- liftM (M.lookup (getOccString nm)) $ interfaceDocs h opts (moduleNameString . moduleName . nameModule $ nm)
-				return $ maybe M.empty (M.singleton (getOccString nm)) doc
-			modify $ M.update (Just . M.union docs') m
-			result <- gets $ M.lookup m
-			maybe (throwError $ "Error loading module " ++ m) return result
-
--- | Load module documentation
-moduleDocs :: [String] -> String -> DocsM ModuleDocMap
-moduleDocs = interfaceDocs (undefined :: InstalledInterface)
-
--- | Load file documentation
-fileDocs :: [String] -> FilePath -> DocsM ModuleDocMap
-fileDocs = interfaceDocs (undefined :: Interface)
-
--- | Load docs for file or module
-docs :: [String] -> String -> DocsM ModuleDocMap
-docs opts m
-	| takeExtension m `elem` [".hs", ".lhs"] = fileDocs opts m
-	| otherwise = moduleDocs opts m
-
--- | Load installed interface
-moduleInterface :: DynFlags -> ModuleName -> IO [(PackageConfig, InstalledInterface)]
-moduleInterface d mname = do
-	result <- liftIO $ getPackagesByModule d mname
-	liftM concat $ mapM packageInterface' $ either (const []) id result
-	where
-		packageInterface' p = liftM (zip (repeat p)) $ packageInterface d mname p
-
-packageInterface :: DynFlags -> ModuleName -> PackageConfig -> IO [InstalledInterface]
-packageInterface _ mname package = do
-	files <- getHaddockInterfaceByPackage package
-	case partitionEithers files of
-		([], []) -> return []
-		(_:_, _) -> return []
-		(_, files') -> return $ concatMap (filter ((== mname) . moduleName . instMod) . ifInstalledIfaces) files'
-
--- | Format documentation to plain text.
-formatDoc :: Doc String -> String
-formatDoc = trim . go where
-	go DocEmpty = ""
-	go (DocAppend a b) = go a ++ go b
-	go (DocString str) = trimSpaces str
-	go (DocParagraph p) = go p ++ "\n"
-	go (DocIdentifier i) = i
-	go (DocIdentifierUnchecked (mname, occname)) = moduleNameString mname ++ "." ++ occNameString occname
-	go (DocModule m) = m
-	go (DocEmphasis e) = "*" ++ go e ++ "*"
-	go (DocMonospaced e) = "`" ++ go e ++ "`"
-	go (DocUnorderedList i) = unlines (map (("* " ++) . go) i)
-	go (DocOrderedList i) = unlines (zipWith (\i' x -> show i' ++ ". " ++ go x) ([1..] :: [Integer]) i)
-	go (DocDefList xs) = unlines (map (\(i,x) -> go i ++ ". " ++ go x) xs)
-	go (DocCodeBlock block) = unlines (map ("    " ++) (lines (go block))) ++ "\n"
-	go (DocHyperlink (Hyperlink url label)) = maybe url (\l -> l ++ "[" ++ url ++ "]") label
-	go (DocPic pic) = pic
-	go (DocAName name) = name
-	go (DocExamples exs) = unlines (map formatExample exs)
-
-	formatExample :: Example -> String
-	formatExample (Example expr result) = "    > " ++ expr ++ unlines (map ("    " ++) result)
-
-	trimSpaces [] = []
-	trimSpaces [s] = [s]
-	trimSpaces (' ':' ':ss) = trimSpaces (' ':ss)
-	trimSpaces (x:y:ss) = x : trimSpaces(y:ss)
+			pkgs = filter exposed $ map fst $ lookupModuleInAllPackages d (mkModuleName mname)
 
--- | Get a map from names to doc string
-interfaceNameMap :: DocInterface a => a -> Map String (Doc String)
-interfaceNameMap = M.fromList . map (getOccString *** fmap getOccString) . M.toList . getDocMap
+	namePackage :: Name -> PackageId
+	namePackage = modulePackageId . nameModule
 
--- | Search for a module's package with suggestions if not found
-getPackagesByModule :: DynFlags -> ModuleName -> IO (Either [Module] [PackageConfig])
-getPackagesByModule d m = return $ fmap (map fst) $ lookupModuleWithSuggestions d m
+	ifacePackageDeps :: InstalledInterface -> [PackageId]
+	ifacePackageDeps i = (modulePackageId $ instMod i) `delete` (nub . map namePackage . instExports $ i)
 
--- | Trim string
-trim :: String -> String
-trim = p . p where
-	p = reverse . dropWhile isSpace
+	ifaceDep :: InstalledInterface -> InstalledInterface -> Bool
+	ifaceDep i idep = instMod i /= instMod idep && instMod idep `elem` map nameModule (instExports i)
 
--- | Show the package name
-showPackageName :: PackageIdentifier -> String
-showPackageName = packageIdString . mkPackageId
+	pkgId :: PackageConfig -> String
+	pkgId = display . installedPackageId
 
--- | Get the Haddock interfaces of the given package.
-getHaddockInterfaceByPackage :: PackageConfig -> IO [Either String InterfaceFile]
-getHaddockInterfaceByPackage = mapM (readInterfaceFile freshNameCache) . haddockInterfaces
+-- | Load docs for all installed modules
+installedDocs :: [String] -> ErrorT String IO (Map String ModuleDocMap)
+installedDocs opts = ErrorT $ withInitializedPackages opts (runErrorT . installedDocs') where
+	installedDocs' :: DynFlags -> ErrorT String IO (Map String ModuleDocMap)
+	installedDocs' d = do
+		fs <- maybe (throwError "Package database empty") (return . concatMap haddockInterfaces) $ pkgDatabase d
+		ifaces <- liftM concat $ mapM ((`mplus` return []) . H.readInstalledInterfaces) fs
+		let
+			idocs = H.installedInterfacesDocs ifaces
+		return $ M.fromList $ map (exportsDocs idocs) ifaces
 
+-- | Get docs for 'InstalledInterface' with its exports docs
+exportsDocs :: Map String ModuleDocMap -> InstalledInterface -> (String, ModuleDocMap)
+exportsDocs docs iface = (iname, snd (H.installedInterfaceDocs iface) `M.union` edocs) where
+	iname = moduleNameString $ moduleName $ instMod iface
+	edocs = M.fromList $ mapMaybe findDoc (instExports iface)
+	findDoc n = ((,) (getOccString n)) <$> (H.lookupNameDoc n docs)
tests/Test.hs view
@@ -3,19 +3,25 @@ 	) where
 
 import Control.Monad
+import Control.Monad.Error
 import qualified Data.Map as M
 
 import System.Exit
 
 import HDocs.Module
+import HDocs.Haddock
 
 -- | This is main function
 main :: IO ()
-main = do
-	sdocs <- runDocsM (fileDocs [] "tests/Test.hs")
-	tdocs <- either (\e -> putStrLn e >> exitFailure) (return . M.lookup "main") sdocs
-	when (fmap formatDoc tdocs /= Just "This is main function") exitFailure
-	edocs <- runDocsM (moduleDocs [] "Prelude")
-	mdocs <- either (\e -> putStrLn e >> exitFailure) (return . M.lookup "null") edocs
-	when (fmap formatDoc mdocs /= Just "Test whether a list is empty.") exitFailure
-	exitSuccess
+main = runErrorT main' >>= either (\e -> putStrLn e >> exitFailure) (\_ -> exitSuccess) where
+	main' :: ErrorT String IO ()
+	main' = do
+		sdocs <- fmt $ liftM snd $ readSource [] "tests/Test.hs"
+		check "Documentation for main"
+			(M.lookup "main" sdocs == Just "This is main function")
+		edocs <- fmt $ moduleDocs [] "Prelude"
+		check "Documentation for Prelude.null"
+			(M.lookup "null" edocs == Just "Test whether a list is empty.")
+		where
+			check str p = if p then return () else throwError str
+			fmt = liftM formatDocs
tools/hdocs.hs view
@@ -2,23 +2,21 @@ 	main
 	) where
 
+import Control.Monad.Error
 import Data.Aeson
 import Data.Aeson.Encode.Pretty (encodePretty)
-import Data.ByteString.Lazy (ByteString, toStrict)
-import Data.Maybe
-import Data.Map (Map)
+import Data.ByteString.Lazy (toStrict)
 import qualified Data.Map as M
-import Data.Monoid
-import Data.String
-import qualified Data.Text as T
-import qualified Data.Text.Encoding as T
-import Network (PortNumber)
+import Data.Monoid (Monoid(..))
+import qualified Data.Text as T (unpack, pack)
+import qualified Data.Text.Encoding as T (decodeUtf8)
 
+import HDocs.Haddock
 import HDocs.Module
 
 import System.Console.GetOpt
-import System.Environment
-import System.FilePath
+import System.Environment (getArgs)
+import System.FilePath (takeExtension)
 import System.IO
 
 data HDocsOptions = HDocsOptions {
@@ -54,13 +52,21 @@ 		jsonError err = object [
 			T.pack "error" .= err]
 
-		loadDocs m f = do
-			docs <- runDocsM (docs (optionGHC cfg) m)
-			putStrLn $ either (toStr . jsonError) toStr (fmap (M.map formatDoc) docs >>= f)
+		run :: ToJSON a => ErrorT String IO a -> IO ()
+		run act = runErrorT act >>= putStrLn . either (toStr . jsonError) toStr
 
+		loadDocs :: String -> ErrorT String IO ModuleDocMap
+		loadDocs m
+			| takeExtension m == ".hs" = liftM snd $ readSource (optionGHC cfg) m
+			| otherwise = moduleDocs (optionGHC cfg) m
+
 	case cmds of
-		[m] -> loadDocs m return
-		[m, n] -> loadDocs m $ maybe (Left $ "Symbol '" ++ n ++ "' not found") (return . M.singleton n) . M.lookup n
+		["dump", "r"] -> run $ liftM (M.map formatDocs) $ installedDocs (optionGHC cfg)
+		["dump"] -> run $ liftM (M.map formatDocs) $ readInstalledDocs (optionGHC cfg)
+		[m] -> run $ liftM formatDocs $ loadDocs m
+		[m, n] -> run $ liftM formatDocs $ do
+			docs <- loadDocs m
+			maybe (throwError $ "Symbol '" ++ n ++ "' not found") (return . M.singleton n) $ M.lookup n docs
 		_ -> printUsage
 
 printUsage :: IO ()
@@ -68,5 +74,6 @@ 	"Usage:",
 	"  hdocs <module> - get docs for module/file",
 	"  hdocs <module> <name> - get docs for name in module/file",
+	"  hdocs dump [r] - dump all installed docs, if [r], find docs for reexported declarations",
 	"",
 	usageInfo "flags" opts]