hsdev-0.1.4.1: src/HsDev/Symbols.hs
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module HsDev.Symbols (
-- * Information
export,
passImportList,
importName, import_,
Symbol(..),
unnamedModuleId,
sortDeclarations, moduleLocals,
setDefinedIn, dropExternals, clearDefinedIn,
moduleLocalDeclarations, moduleModuleDeclarations,
Locals(..),
decl, definedIn, declarationLocals, scopes,
mergeExported,
-- * Functions
importQualifier,
-- * Utility
Canonicalize(..),
locateProject, searchProject,
locateSourceDir,
moduleOpts,
-- * Modifiers
addDeclaration,
-- * Other
unalias,
-- * Reexportss
module HsDev.Symbols.Types,
module HsDev.Symbols.Class,
module HsDev.Symbols.Documented
) where
import Control.Applicative
import Control.Arrow
import Control.Lens (view, set, over)
import Control.Monad.Trans.Maybe
import Control.Monad.Except
import Data.Function (on)
import Data.List
import Data.Maybe (fromMaybe)
import Data.Ord (comparing)
import Data.Text (Text)
import qualified Data.Text as T (concat)
import System.Directory
import System.FilePath
import HsDev.Symbols.Types
import HsDev.Symbols.Class
import HsDev.Symbols.Documented (Documented(..))
import HsDev.Util (searchPath)
-- | Get name of export
export :: Export -> Text
export (ExportName Nothing n _) = n
export (ExportName (Just q) n _) = T.concat [q, ".", n]
export (ExportModule m) = m
-- | Check whether name pass import list
passImportList :: ImportList -> Text -> Bool
passImportList (ImportList hiding names) n
| hiding = n `notElem` names
| otherwise = n `elem` names
-- | Get import module name
importName :: Import -> Text
importName i = fromMaybe (view importModuleName i) $ view importAs i
-- | Simple import
import_ :: Text -> Import
import_ n = Import n False Nothing Nothing Nothing
-- | Imported module can be accessed via qualifier
importQualifier :: Maybe Text -> Import -> Bool
importQualifier Nothing i
| not (view importIsQualified i) = True
| otherwise = False
importQualifier (Just q) i
| q == view importModuleName i = True
| Just q == view importAs i = True
| otherwise = False
unnamedModuleId :: ModuleLocation -> ModuleId
unnamedModuleId = ModuleId ""
sortDeclarations :: [Declaration] -> [Declaration]
sortDeclarations = sortBy (comparing (view declarationName))
-- | Bring locals to top
moduleLocals :: Module -> Module
moduleLocals m = set moduleDeclarations (moduleLocalDeclarations m) m
-- | Set all declaration `definedIn` to this module
setDefinedIn :: Module -> Module
setDefinedIn m = over moduleDeclarations (map (`definedIn` view moduleId m)) m
-- | Drop all declarations, that not defined in this module
dropExternals :: Module -> Module
dropExternals m = over moduleDeclarations (filter ((/= Just (view moduleId m)) . view declarationDefined)) m
-- | Clear `definedIn` information
clearDefinedIn :: Module -> Module
clearDefinedIn = over moduleDeclarations (map (set declarationDefined Nothing))
-- | Get declarations with locals
moduleLocalDeclarations :: Module -> [Declaration]
moduleLocalDeclarations =
sortDeclarations .
concatMap declarationLocals' .
view moduleDeclarations
where
declarationLocals' :: Declaration -> [Declaration]
declarationLocals' d = d : declarationLocals d
-- | Get list of declarations as ModuleDeclaration
moduleModuleDeclarations :: Module -> [ModuleDeclaration]
moduleModuleDeclarations m = [ModuleDeclaration (view moduleId m) d | d <- view moduleDeclarations m]
class Locals a where
locals :: a -> [Declaration]
where_ :: a -> [Declaration] -> a
instance Locals Declaration where
locals = locals . view declaration
where_ d ds = over declaration (`where_` ds) d
decl :: Text -> DeclarationInfo -> Declaration
decl n = Declaration n Nothing Nothing Nothing Nothing
definedIn :: Declaration -> ModuleId -> Declaration
definedIn d m = set declarationDefined (Just m) d
declarationLocals :: Declaration -> [Declaration]
declarationLocals d = locals $ view declaration d
-- | Get scopes of @Declaration@, where @Nothing@ is global scope
scopes :: Declaration -> [Maybe Text]
scopes d = globalScope $ map (Just . importName) is where
is = fromMaybe [] $ view declarationImported d
globalScope
| any (not . view importIsQualified) is = (Nothing :)
| otherwise = id
instance Locals DeclarationInfo where
locals (Function _ ds _) = ds
locals _ = []
where_ (Function n s r) ds = Function n (s ++ ds) r
where_ d _ = d
-- | Merge @ModuleDeclaration@ into @ExportedDeclaration@
mergeExported :: [ModuleDeclaration] -> [ExportedDeclaration]
mergeExported =
map merge' .
groupBy ((==) `on` declId) .
sortBy (comparing declId)
where
declId :: ModuleDeclaration -> (Text, Maybe ModuleId)
declId = view moduleDeclaration >>> (view declarationName &&& view declarationDefined)
merge' :: [ModuleDeclaration] -> ExportedDeclaration
merge' [] = error "mergeExported: impossible"
merge' ds@(d:_) = ExportedDeclaration (map (view declarationModuleId) ds) (view moduleDeclaration d)
-- | Canonicalize all paths within something
class Canonicalize a where
canonicalize :: a -> IO a
instance Canonicalize FilePath where
canonicalize = canonicalizePath
instance Canonicalize Cabal where
canonicalize Cabal = return Cabal
canonicalize (Sandbox p) = fmap Sandbox $ canonicalizePath p
instance Canonicalize Project where
canonicalize (Project nm p c desc) = liftM3 (Project nm) (canonicalizePath p) (canonicalizePath c) (return desc)
instance Canonicalize ModuleLocation where
canonicalize (FileModule f p) = liftM2 FileModule (canonicalizePath f) (traverse canonicalize p)
canonicalize (CabalModule c p n) = fmap (\c' -> CabalModule c' p n) $ canonicalize c
canonicalize (ModuleSource m) = return $ ModuleSource m
-- | Find project file is related to
locateProject :: FilePath -> IO (Maybe Project)
locateProject file = do
file' <- canonicalizePath file
isDir <- doesDirectoryExist file'
if isDir then locateHere file' else locateParent (takeDirectory file')
where
locateHere path = do
cts <- filter (not . null . takeBaseName) <$> getDirectoryContents path
return $ fmap (project . (path </>)) $ find ((== ".cabal") . takeExtension) cts
locateParent dir = do
cts <- filter (not . null . takeBaseName) <$> getDirectoryContents dir
case find ((== ".cabal") . takeExtension) cts of
Nothing -> if isDrive dir then return Nothing else locateParent (takeDirectory dir)
Just cabalf -> return $ Just $ project (dir </> cabalf)
-- | Search project up
searchProject :: FilePath -> IO (Maybe Project)
searchProject file = runMaybeT $ searchPath file (MaybeT . locateProject) <|> mzero
-- | Locate source dir of file
locateSourceDir :: FilePath -> IO (Maybe (Extensions FilePath))
locateSourceDir f = runMaybeT $ do
file <- liftIO $ canonicalizePath f
p <- MaybeT $ locateProject file
proj <- MaybeT $ fmap (either (const Nothing) Just) $ runExceptT $ loadProject p
MaybeT $ return $ findSourceDir proj file
-- | Options for GHC of module and project
moduleOpts :: [ModulePackage] -> Module -> [String]
moduleOpts pkgs m = case view moduleLocation m of
FileModule file proj -> concat [
["-i" ++ s | s <- srcDirs],
concatMap extensionsOpts exts,
hidePackages,
["-package " ++ p | p <- deps, p `elem` pkgs']]
where
infos' = maybe [] (`fileTargets` file) proj
srcDirs = concatMap (view infoSourceDirs) infos'
exts = map (file `withExtensions`) infos'
deps = concatMap (view infoDepends) infos'
pkgs' = map (view packageName) pkgs
hidePackages
| null infos' = []
| otherwise = ["-hide-all-packages"]
_ -> []
-- | Add declaration to module
addDeclaration :: Declaration -> Module -> Module
addDeclaration decl' = over moduleDeclarations (sortDeclarations . (decl' :))
-- | Unalias import name
unalias :: Module -> Text -> [Text]
unalias m alias = [view importModuleName i | i <- view moduleImports m, view importAs i == Just alias]