packages feed

ariadne-0.1.2.2: Ariadne/ModuleDB/ParseResolve.hs

{-# LANGUAGE FlexibleContexts, MultiParamTypeClasses #-}
module Ariadne.ModuleDB.ParseResolve where

import Ariadne.GlobalNameIndex
import Ariadne.Index
import Ariadne.Types
import qualified Ariadne.SrcMap as SrcMap
import Ariadne.ModuleDB.Types


import Language.Haskell.Names
import Language.Haskell.Names.Interfaces
import Language.Haskell.Names.SyntaxUtils
import Language.Haskell.Names.Imports
import qualified Language.Haskell.Names.GlobalSymbolTable as Global
import Language.Haskell.Exts.Annotated hiding (parse)
import Language.Haskell.Exts.Annotated.CPP
import Distribution.HaskellSuite.Packages
import Distribution.Simple.Compiler (PackageDB(..))

import Control.Applicative
import Control.Arrow
import Control.Monad.Trans
import Control.Monad
import Control.Monad.State
import Control.Exception
import Text.Printf
import System.FilePath
import System.Directory
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified System.Log.Logger as L
import Data.Maybe
import qualified Data.Foldable as F
import Data.Proxy
import Data.Lens

-- | Start to watch the path, and if/when it exists, parse and record it
include :: (MonadState Storage m, MonadIO m) => FilePath -> m ()
include path = do
  alreadyPresent <- gets (Set.member path . getL watchedFiles)
  unless alreadyPresent $ do
    liftIO . L.debugM "ariadne.moduledb" $ printf "Including %s in the set of watched files" path
    exists <- liftIO $ doesFileExist path
    watchedFiles %= Set.insert path
    when exists $ update path

-- | Read/parse/analyse the given path and update the internal state
-- (Storage) accordingly
update
  :: (MonadState Storage m, MonadIO m)
  => FilePath -- the file to update
  -> m ()
update path = do
  readSources path

  (paths, sources) <- (Map.keys &&& Map.elems) `liftM` access moduleSources

  pkgs <- liftIO $
   ((F.fold <$> mapM (getInstalledPackages (Proxy :: Proxy NamesDB)) [GlobalPackageDB, UserPackageDB])
    :: IO Packages)

  (resolved, impTbls) <- liftIO (liftM unzip $
   (flip evalNamesModuleT pkgs $ do
      errs <- computeInterfaces defaultLang defaultExts sources
      forM sources $ \parsed -> do
        let extSet = moduleExtensions defaultLang defaultExts parsed
        impTbl <- fmap snd $ processImports extSet $ getImports parsed
        resolved <- annotateModule defaultLang defaultExts parsed
        return (resolved, impTbl))
    :: IO ([Module (Scoped SrcSpan)], [Global.Table]))

  let
    gIndex :: GlobalNameIndex
    gIndex = Map.unions $
      zipWith
        (\src impTbl -> mkGlobalNameIndex impTbl (getPointLoc <$> src))
        sources impTbls

    srcMaps :: [SrcMap.SrcMap Origin]
    srcMaps = map (mkSrcMap gIndex) resolved

  moduleSrcMaps ~= Map.fromAscList (zip paths srcMaps)

  return ()

-- | Helper for update. Reads/parses the source and also calls include on
-- every imprted module
readSources
  :: (MonadState Storage m, MonadIO m)
  => FilePath -- the file to update
  -> m ()
readSources path = do
  exists <- liftIO $ doesFileExist path
  if not exists
    then do
      {-liftIO . L.debugM "ariadne.parser" $
        printf "%s: not found at %s" modname path-}
      return ()
    else do
      parseResult <- liftIO $ parse path
      case parseResult of
        ParseFailed loc msg -> do
          liftIO . L.warningM "ariadne.parser" $
            printf "Failed to parse %s (%s: %s)" path (prettyPrint loc) msg
          return ()
        ParseOk parsed -> do
          let
            modname@(ModuleName _ modnameS) = getModuleName parsed
            root = rootPath path modname
          liftIO . L.debugM "ariadne.parser" $
            printf "Parsed %s at %s" modnameS path
          moduleSources %= Map.insert path (srcInfoSpan <$> parsed)
          mapM_ (include . modNameToPath root) (importedModules parsed)

-- these should probably come from the Cabal file
defaultLang = Haskell2010
defaultExts = []

parse :: FilePath -> IO (ParseResult (Module SrcSpanInfo))
parse path = do
  ast <- fmap fst <$>
    parseFileWithCommentsAndCPP
      defaultCpphsOptions
      defaultParseMode
        { parseFilename = path
        , ignoreLinePragmas = False
        , fixities = Just [] }
      path
  -- Sometimes the AST throws an exception when forcing the result, such
  -- as "Ambiguous infix expression". Very annoying!
  catch (evaluate ast) $ \e ->
    return $ ParseFailed noLoc { srcFilename = path } (show (e :: ErrorCall))

-- | Get the module's root path, based on its path and the module name
rootPath :: FilePath -> ModuleName l -> FilePath
rootPath path (ModuleName _ modname) =
  -- the algorithm is simple: count the number of components in the module
  -- name, and go that number of levels up
  let
    numLevels = length $ filter (== '.') modname
    root = (foldr (.) id $ replicate (numLevels+1) takeDirectory) path
  in root

-- FIXME support lhs etc.
modNameToPath
  :: FilePath -- ^ root path
  -> ModuleNameS -- ^ module name
  -> FilePath -- ^ module path
modNameToPath root name = root </> map dotToSlash name <.> "hs"
  where
    dotToSlash '.' = '/'
    dotToSlash c = c

importedModules :: Module a -> [ModuleNameS]
importedModules mod =
  map ((\(ModuleName _ s) -> s) . importModule) $ getImports mod