serokell/importify

View on GitHub
src/Importify/Main/File.hs

Summary

Maintainability
Test Coverage
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeApplications      #-}

-- | Contains implementation of @importify file@ command.

module Importify.Main.File
       ( OutputOptions (..)
       , importifyFileOptions
       , importifyFileContent
       ) where

import Universum

import Fmt (fmt, (+|), (|+))
import Language.Haskell.Exts (Comment (..), Extension, ImportDecl, Module (..), ModuleHead,
                              ModuleName (..), SrcSpanInfo, ann, exactPrint, parseExtension,
                              parseFileContentsWithComments)
import Language.Haskell.Exts.Parser (ParseMode (..), defaultParseMode)
import Language.Haskell.Names (Environment, Scoped, annotate, loadBase, readSymbols)
import Language.Haskell.Names.Imports (annotateImportDecls, importTable)
import Language.Haskell.Names.SyntaxUtils (getModuleName)
import Path (Abs, Dir, File, Path, Rel, fromAbsFile, fromRelFile, parseRelDir, parseRelFile, (</>))
import Path.IO (doesDirExist, getCurrentDir)

import Extended.System.Wlog (printError, printNotice)
import Importify.Cabal (ExtensionsMap, ModulesBundle (..), ModulesMap, TargetId, targetIdDir)
import Importify.ParseException (eitherParseResult, setMpeFile)
import Importify.Path (decodeFileOrMempty, doInsideDir, extensionsPath, importifyPath, lookupToRoot,
                       modulesPath, symbolsPath)
import Importify.Pretty (printLovelyImports)
import Importify.Resolution (collectUnusedImplicitImports, collectUnusedSymbolsBy, hidingUsedIn,
                             isKnownImport, removeImplicitImports, removeUnusedQualifiedImports,
                             symbolUsedIn)
import Importify.Syntax (importSlice, switchHidingImports, unscope)
import Importify.Tree (UnusedHidings (UnusedHidings), UnusedSymbols (UnusedSymbols), removeImports)

import qualified Data.Foldable as Foldable (toList)
import qualified Data.HashMap.Strict as HM
import qualified Data.Map as M

-- | This data type dictates how output of @importify@ should be
-- outputed.
data OutputOptions = ToConsole        -- ^ Print to console
                   | InPlace          -- ^ Change file in-place
                   | ToFile FilePath  -- ^ Print to specified file
                   deriving (Show)

newtype ImportifyFileException = IFE Text

-- | Run @importify file@ command with given options.
importifyFileOptions :: OutputOptions -> FilePath -> IO ()
importifyFileOptions options srcFile = do
    srcPath   <- parseRelFile srcFile
    foundRoot <- lookupToRoot (doesDirExist . (</> importifyPath)) srcPath
    case foundRoot of
        Nothing ->
            printError "Directory '.importify' is not found. Either cache for project \
                       \is not created or not running from project directory."
        Just (rootDir, srcFromRootPath) -> do
            curDir          <- getCurrentDir
            importifyResult <- doInsideDir rootDir (importifyFileContent $ curDir </> srcFromRootPath)
            handleOptions importifyResult
  where
    handleOptions :: Either ImportifyFileException Text -> IO ()
    handleOptions (Left (IFE msg))    = printError msg
    handleOptions (Right modifiedSrc) = case options of
        ToConsole -> putText modifiedSrc
        InPlace   -> writeFile srcFile modifiedSrc
        ToFile to -> writeFile to      modifiedSrc

-- | Return result of @importify file@ command.
importifyFileContent :: Path Abs File -> IO (Either ImportifyFileException Text)
importifyFileContent srcPath = do
    let srcFile    = fromAbsFile srcPath

    modulesMap <- readModulesMap
    extensions <- readExtensions srcPath modulesMap

    whenNothing_ (HM.lookup (fromAbsFile srcPath) modulesMap) $
        printNotice $ "File '"+|srcFile|+"' is not cached: new file or caching error"

    src <- readFile srcFile
    let parseResult = eitherParseResult
                    $ parseFileContentsWithComments (defaultParseMode { extensions = extensions, fixities = Just [] })
                    $ toString src

    case parseResult of
        Left exception       -> return $ Left $ IFE $ setMpeFile srcFile exception |+ ""
        Right (ast,comments) -> importifyAst src modulesMap comments ast

importifyAst :: Text
             -> ModulesMap
             -> [Comment]
             -> Module SrcSpanInfo
             -> IO (Either ImportifyFileException Text)
importifyAst src modulesMap comments ast@(Module _ _ _ imports _) =
    Right <$> case importSlice imports of
        Nothing           -> return src
        Just (start, end) -> do
            let codeLines        = lines src
            let (preamble, rest) = splitAt (start - 1) codeLines
            let (impText, decls) = splitAt (end - start + 1) rest

            environment       <- loadEnvironment modulesMap
            let newImports     = removeUnusedImports ast imports environment
            let printedImports = printLovelyImports start end comments impText newImports

            return $ unlines preamble
                  <> unlines printedImports
                  <> unlines decls
importifyAst _ _ _ _ = return $ Left $ IFE "Module wasn't parsed correctly"

readModulesMap :: IO ModulesMap
readModulesMap = decodeFileOrMempty (importifyPath </> modulesPath) pure

readExtensions :: Path Abs File -> ModulesMap -> IO [Extension]
readExtensions srcPath modulesMap =
    case HM.lookup (fromAbsFile srcPath) modulesMap of
        Nothing                -> return []
        Just ModulesBundle{..} -> do
            packagePath <- parseRelDir $ toString mbPackage
            projectPath <- getCurrentDir
            let pathToExtensions = projectPath
                               </> importifyPath
                               </> symbolsPath
                               </> packagePath
                               </> extensionsPath

            let lookupExtensions = fromMaybe [] . getExtensions mbTarget
            decodeFileOrMempty @ExtensionsMap
                               pathToExtensions
                               (return . lookupExtensions)

getExtensions :: TargetId -> ExtensionsMap -> Maybe [Extension]
getExtensions targetId = fmap (map parseExtension) . HM.lookup targetId

loadEnvironment :: ModulesMap -> IO Environment
loadEnvironment modulesMap = do
    baseEnvironment <- loadBase

    let moduleBundles = HM.elems modulesMap
    packages <- forM moduleBundles $ \ModulesBundle{..} -> do
        packagePath     <- parseRelDir  $ toString mbPackage
        symbolsFilePath <- parseRelFile $ mbModule ++ ".symbols"
        targetPath      <- parseRelDir $ toString $ targetIdDir mbTarget
        let pathToSymbols = importifyPath
                        </> symbolsPath
                        </> packagePath
                        </> targetPath
                        </> symbolsFilePath
        moduleSymbols <- readSymbols (fromRelFile pathToSymbols)
        pure (ModuleName () mbModule, moduleSymbols)

    return $ M.union baseEnvironment (M.fromList packages)

-- | Remove all unused entities in given module from given list of imports.
-- Algorithm performs next steps:
-- -1. Load environment
--  0. Collect annotations for module and imports.
--  1. Remove unused implicit imports.
--  2. Remove unused symbols from explicit list.
--  3. Remove unused hidings from explicit lists.
--  4. Remove unused qualified imports.
removeUnusedImports
    :: Module SrcSpanInfo        -- ^ Module where symbols should be removed
    -> [ImportDecl SrcSpanInfo]  -- ^ Imports from module
    -> Environment
    -> [ImportDecl SrcSpanInfo]
removeUnusedImports ast imports environment = do
    -- return exports to search for qualified imports there later
    let (annotations, moduleHead) = annotateModule ast environment

    let symbolTable    = importTable environment ast
    let hidingTable    = importTable environment $ switchHidingImports ast
    let annotatedDecls = annotateImportDecls (getModuleName ast) environment imports

    -- ordNub needed because name can occur as Qual and as UnQual
    -- but we don't care about qualification
    let unusedCollector = ordNub ... collectUnusedSymbolsBy
    let unusedSymbols   = unusedCollector (`symbolUsedIn` annotations) symbolTable
    let unusedHidings   = unusedCollector (`hidingUsedIn` annotations) hidingTable
    let unusedImplicits = collectUnusedImplicitImports (`symbolUsedIn` annotations)
                        $ filter (isKnownImport environment) annotatedDecls

    -- Remove all collected info from imports
    let withoutUnusedImplicits = removeImplicitImports unusedImplicits
                                                       annotatedDecls
    let withoutUnusedSymbols   = map unscope
                               $ removeImports (UnusedSymbols unusedSymbols)
                                               (UnusedHidings unusedHidings)
                                               withoutUnusedImplicits
    let withoutUnusedQuals     = removeUnusedQualifiedImports withoutUnusedSymbols
                                                              moduleHead
                                                              annotations
                                                              unusedImplicits

    withoutUnusedQuals

-- | Annotates module but drops import annotations because they can contain GlobalSymbol
-- annotations and collectUnusedSymbols later does its job by looking for GlobalSymbol
annotateModule :: Module SrcSpanInfo
               -> Environment
               -> ([Scoped SrcSpanInfo], Maybe (ModuleHead SrcSpanInfo))
annotateModule ast environment =
    let (Module l mhead mpragmas _mimports mdecls) = annotate environment ast
    in (Foldable.toList (Module l mhead mpragmas [] mdecls), fmap unscope mhead)