serokell/importify

View on GitHub
src/Importify/Tree.hs

Summary

Maintainability
Test Coverage
{-# LANGUAGE ViewPatterns #-}

module Importify.Tree
       ( UnusedHidings (..)
       , UnusedSymbols (..)
       , removeImports
       ) where

import           Universum

import           Data.Generics.Aliases  (mkT)
import           Data.Generics.Schemes  (everywhere)
import           Data.List              (partition)
import           Extended.Data.List     (removeAtMultiple)
import           Language.Haskell.Exts  (CName, ImportDecl (..), ImportSpec (..),
                                         ImportSpecList (..), Name (..), Namespace (..),
                                         SrcSpanInfo (..))
import           Language.Haskell.Names (NameInfo (..), Scoped (..), Symbol)

import           Importify.Syntax       (InScoped, pullScopedInfo)

-- | @newtype@ wrapper for list of unused symbols.
newtype UnusedSymbols = UnusedSymbols { getUnusedSymbols :: [Symbol] }

-- | @newtype@ wrapper for list of unused symbols from @hiding@.
newtype UnusedHidings = UnusedHidings { getUnusedHidings :: [Symbol] }

-- | Remove a list of identifiers from 'ImportDecl's.
-- Next algorithm is used:
--
-- 1. Remove all identifiers inside specified list of types.
--    If list becomes empty then only type left.
-- @
--   import Module.Name (Type (HERE)) ⇒ import Module.Name (Type)
-- @
--
-- 2. Traverse every 'ImportSpec's and check matching with symbols.
-- @
--   import Module.Name (Type (something), HERE) ⇒ import Module.Name (Type (something))
-- @
--
-- 3. Translate empty imports into implicit import.
-- @
--    import Module.Name () ⇒ import Module.Name
-- @
--
-- 4. Remove all implicit imports preserving only initially implicit or empty.
removeImports :: UnusedSymbols        -- ^ List of symbols which should be removed
              -> UnusedHidings        -- ^ List of hidings which should be removed
              -> [InScoped ImportDecl] -- ^ Imports to be purified
              -> [InScoped ImportDecl]
removeImports (UnusedSymbols symbols) (UnusedHidings hidings) decls =
    (volatileImports ++)
  $ cleanDecls
  $ everywhere (mkT traverseToClean)
  $ everywhere (mkT $ traverseToRemove hidings True)
  $ everywhere (mkT $ traverseToRemove symbols False)
  $ everywhere (mkT $ traverseToRemoveThing symbols) decls
  where
    volatileImports = filter isVolatileImport decls

-- | Returns 'True' if the import is of either of the forms:
-- @
--   import Foo ()
--   import Foo
-- @
--
isVolatileImport :: InScoped ImportDecl -> Bool
isVolatileImport ImportDecl{ importSpecs = Just (ImportSpecList _ _ []) } = True
isVolatileImport ImportDecl{ importSpecs = Nothing }                      = True
isVolatileImport _                                                        = False

-- | Traverses 'ImportDecl's to remove symbols from 'IThingWith' specs.
traverseToRemoveThing :: [Symbol]
                      -> InScoped ImportSpec
                      -> InScoped ImportSpec
traverseToRemoveThing
    symbols
    (IThingWith (Scoped ni srcSpan@SrcSpanInfo{..}) name cnames)
  =
    case newCnames of
        [] -> IAbs (toScope emptySpan) (NoNamespace $ toScope emptySpan) name
        _  -> IThingWith (toScope newSpanInfo) name newCnames
  where
    emptySpan :: SrcSpanInfo
    emptySpan = SrcSpanInfo srcInfoSpan []

    toScope :: SrcSpanInfo -> Scoped SrcSpanInfo
    toScope = Scoped ni

    (newCnames, newSpanInfo) = removeSrcSpanInfoPoints isCNameNotInSymbols
                                                       cnames
                                                       srcSpan

    isCNameNotInSymbols :: InScoped CName -> Bool
    isCNameNotInSymbols (pullScopedInfo -> GlobalSymbol symbol _) = symbol `notElem` symbols
    isCNameNotInSymbols _                                         = False
traverseToRemoveThing _ spec = spec

-- | Traverses 'ImportSpecList' to remove identifiers from those lists.
traverseToRemove :: [Symbol]  -- ^
                 -> Bool
                 -> InScoped ImportSpecList
                 -> InScoped ImportSpecList
traverseToRemove symbols
                 yes'CleanIt
                 specList@(ImportSpecList (Scoped ni oldSpanInfo) isHiding specs)
    | isHiding == yes'CleanIt = newSpecs
    | otherwise               = specList
  where
    (neededSpecs, newSpanInfo) = removeSrcSpanInfoPoints (isSpecNeeded symbols)
                                                         specs
                                                         oldSpanInfo

    newSpecs = ImportSpecList (Scoped ni newSpanInfo)
                              isHiding
                              neededSpecs

-- | Removes 'SrcSpanInfo' points of deleted elements.
removeSrcSpanInfoPoints :: (a -> Bool)  -- ^ Keep entitity?
                        -> [a]          -- ^ List of entities
                        -> SrcSpanInfo  -- ^ Span info with points
                        -> ([a], SrcSpanInfo) -- ^ Kept entities and info w/o points
removeSrcSpanInfoPoints shouldKeepEntity entities SrcSpanInfo{..} =
    let indexedEntities = zip [1..] entities
        (neededEntities, unusedEntities) = partition (shouldKeepEntity . snd)
                                                     indexedEntities
        pointsCount = length srcInfoPoints
        unusedIds   = filter (< pointsCount)  -- don't remove index of ')'
                    $ map fst unusedEntities
        newPoints   = removeAtMultiple unusedIds srcInfoPoints
    in (map snd neededEntities, SrcSpanInfo srcInfoSpan newPoints)

-- | Returns 'False' if 'ImportSpec' is not needed.
isSpecNeeded :: [Symbol] -> InScoped ImportSpec -> Bool
isSpecNeeded symbols (IVar _ name)          = isNameNeeded name symbols
isSpecNeeded symbols (IAbs _ _ name)        = isNameNeeded name symbols
isSpecNeeded symbols (IThingAll _ name)     = isNameNeeded name symbols
isSpecNeeded symbols (IThingWith _ name []) = isNameNeeded name symbols
isSpecNeeded _       (IThingWith _ _ (_:_)) = True -- Do not remove if cnames list is not empty

-- | Returns 'False' if 'Name' is not needed. On top level
-- elements inside 'ImportSpec' annotated by 'ImportPart'. But
-- this constructor contains list of symbols. So it's needed if
-- at least one element inside list is needed.
isNameNeeded :: InScoped Name -> [Symbol] -> Bool
isNameNeeded (pullScopedInfo -> ImportPart symbols) unusedSymbols =
    any (`notElem` unusedSymbols) symbols
isNameNeeded _ _ =
    True

-- | Traverses 'ImportDecl's to remove empty non-@hiding@ import specs.
traverseToClean :: InScoped ImportDecl -> InScoped ImportDecl
traverseToClean decl@ImportDecl{ importSpecs = Just (ImportSpecList _ False []) } =
    decl { importSpecs = Nothing }
traverseToClean decl = decl

-- | First remove all imports with no lists and then remove
-- 'ImportSpecList' from empty @hiding@ imports.
cleanDecls :: [InScoped ImportDecl] -> [InScoped ImportDecl]
cleanDecls = map removeHidingList . filter isDeclNeeded
  where
    removeHidingList :: InScoped ImportDecl -> InScoped ImportDecl
    removeHidingList decl@ImportDecl{ importSpecs = Just (ImportSpecList _ True []) } =
        decl { importSpecs = Nothing }
    removeHidingList decl = decl

    isDeclNeeded :: InScoped ImportDecl -> Bool
    isDeclNeeded = isJust . importSpecs