module Language.PureScript.Linter.Imports
( lintImports
, Name(..)
, UsedImports()
) where
import Prelude.Compat
import Protolude (ordNub)
import Control.Monad (join, unless, foldM, (<=<))
import Control.Monad.Writer.Class
import Data.Function (on)
import Data.Foldable (for_)
import Data.List (find, intersect, groupBy, sortBy, (\\))
import Data.Maybe (mapMaybe)
import Data.Monoid (Sum(..))
import Data.Traversable (forM)
import qualified Data.Text as T
import qualified Data.Map as M
import Language.PureScript.AST.Declarations
import Language.PureScript.AST.SourcePos
import Language.PureScript.Crash
import Language.PureScript.Errors
import Language.PureScript.Names
import Language.PureScript.Sugar.Names.Common (warnDuplicateRefs)
import Language.PureScript.Sugar.Names.Env
import Language.PureScript.Sugar.Names.Imports
import qualified Language.PureScript.Constants as C
type UsedImports = M.Map ModuleName [Qualified Name]
lintImports
:: forall m
. MonadWriter MultipleErrors m
=> Module
-> Env
-> UsedImports
-> m ()
lintImports (Module _ _ _ _ Nothing) _ _ =
internalError "lintImports needs desugared exports"
lintImports (Module _ _ mn mdecls (Just mexports)) env usedImps = do
let scope = maybe nullImports (\(_, imps', _) -> imps') (M.lookup mn env)
usedImps' = foldr (elaborateUsed scope) usedImps exportedModules
numOpenImports = getSum $ foldMap (Sum . countOpenImports) mdecls
allowImplicit = numOpenImports == 1
imports = M.toAscList (findImports mdecls)
for_ imports $ \(mni, decls) ->
unless (isPrim mni) .
for_ decls $ \(ss, declType, qualifierName) -> do
let names = ordNub $ M.findWithDefault [] mni usedImps'
lintImportDecl env mni qualifierName names ss declType allowImplicit
for_ (M.toAscList (byQual imports)) $ \(mnq, entries) -> do
let mnis = ordNub $ map (\(_, _, mni) -> mni) entries
unless (length mnis == 1) $ do
let implicits = filter (\(_, declType, _) -> not $ isExplicit declType) entries
for_ implicits $ \(ss, _, mni) -> do
let names = ordNub $ M.findWithDefault [] mni usedImps'
usedRefs = findUsedRefs ss env mni (Just mnq) names
unless (null usedRefs) .
tell . errorMessage' ss $ ImplicitQualifiedImport mni mnq $ map (simplifyTypeRef $ const True) usedRefs
for_ imports $ \(mnq, imps) -> do
warned <- foldM (checkDuplicateImports mnq) [] (selfCartesianSubset imps)
let unwarned = imps \\ warned
duplicates
= join
. map tail
. filter ((> 1) . length)
. groupBy ((==) `on` defQual)
. sortBy (compare `on` defQual)
$ unwarned
for_ duplicates $ \(pos, _, _) ->
tell . errorMessage' pos $ DuplicateSelectiveImport mnq
for_ (imps \\ (warned ++ duplicates)) $ \(pos, typ, _) ->
warnDuplicateRefs pos DuplicateImportRef $ case typ of
Explicit refs -> refs
Hiding refs -> refs
_ -> []
for_ mexports $ \case
ModuleRef _ mnq ->
case M.lookup mnq (byQual imports) of
Just [(ss, Implicit, mni)] -> do
let names = ordNub $ M.findWithDefault [] mni usedImps'
usedRefs = findUsedRefs ss env mni (Just mnq) names
tell . errorMessage' ss $
ImplicitQualifiedImportReExport mni mnq
$ map (simplifyTypeRef $ const True) usedRefs
_ -> pure ()
_ -> pure ()
where
defQual :: ImportDef -> Maybe ModuleName
defQual (_, _, q) = q
selfCartesianSubset :: [a] -> [(a, a)]
selfCartesianSubset (x : xs) = [(x, y) | y <- xs] ++ selfCartesianSubset xs
selfCartesianSubset [] = []
countOpenImports :: Declaration -> Int
countOpenImports (ImportDeclaration _ mn' Implicit Nothing)
| not (isPrim mn' || mn == mn') = 1
countOpenImports (ImportDeclaration _ mn' (Hiding _) Nothing)
| not (isPrim mn' || mn == mn') = 1
countOpenImports _ = 0
isPrim :: ModuleName -> Bool
isPrim = (== ModuleName [ProperName C.prim])
byQual
:: [(ModuleName, [(SourceSpan, ImportDeclarationType, Maybe ModuleName)])]
-> M.Map ModuleName [(SourceSpan, ImportDeclarationType, ModuleName)]
byQual = foldr goImp M.empty
where
goImp (mni, xs) acc = foldr (goDecl mni) acc xs
goDecl mni (ss', declType, Just qmn) acc =
let entry = (ss', declType, mni)
in M.alter (Just . maybe [entry] (entry :)) qmn acc
goDecl _ _ acc = acc
exportedModules :: [ModuleName]
exportedModules = ordNub $ mapMaybe extractModule mexports
where
extractModule (ModuleRef _ mne) = Just mne
extractModule _ = Nothing
elaborateUsed :: Imports -> ModuleName -> UsedImports -> UsedImports
elaborateUsed scope mne used =
foldr go used
$ extractByQual mne (importedTypeClasses scope) TyClassName
++ extractByQual mne (importedTypeOps scope) TyOpName
++ extractByQual mne (importedTypes scope) TyName
++ extractByQual mne (importedDataConstructors scope) DctorName
++ extractByQual mne (importedValues scope) IdentName
++ extractByQual mne (importedValueOps scope) ValOpName
where
go :: (ModuleName, Qualified Name) -> UsedImports -> UsedImports
go (q, name) = M.alter (Just . maybe [name] (name :)) q
extractByQual
:: ModuleName
-> M.Map (Qualified a) [ImportRecord a]
-> (a -> Name)
-> [(ModuleName, Qualified Name)]
extractByQual k m toName = mapMaybe go (M.toList m)
where
go (q@(Qualified mnq _), is)
| isUnqualified q =
case find (isQualifiedWith k) (map importName is) of
Just (Qualified _ name) -> Just (k, Qualified mnq (toName name))
_ -> Nothing
| isQualifiedWith k q =
case importName (head is) of
Qualified (Just mn') name -> Just (mn', Qualified mnq (toName name))
_ -> internalError "unqualified name in extractByQual"
go _ = Nothing
simplifyTypeRef :: (ProperName 'TypeName -> Bool) -> DeclarationRef -> DeclarationRef
simplifyTypeRef shouldOpen (TypeRef ss name (Just dctors))
| not (null dctors) && shouldOpen name = TypeRef ss name Nothing
simplifyTypeRef _ other = other
lintImportDecl
:: forall m
. MonadWriter MultipleErrors m
=> Env
-> ModuleName
-> Maybe ModuleName
-> [Qualified Name]
-> SourceSpan
-> ImportDeclarationType
-> Bool
-> m Bool
lintImportDecl env mni qualifierName names ss declType allowImplicit =
case declType of
Implicit -> case qualifierName of
Nothing ->
if null allRefs
then unused
else unless' allowImplicit (checkImplicit ImplicitImport)
Just q -> unless' (q `elem` mapMaybe getQual names) unused
Hiding _ -> unless' allowImplicit (checkImplicit HidingImport)
Explicit [] -> unused
Explicit declrefs -> checkExplicit declrefs
where
checkImplicit
:: (ModuleName -> [DeclarationRef] -> SimpleErrorMessage)
-> m Bool
checkImplicit warning =
if null allRefs
then unused
else warn (warning mni (map (simplifyTypeRef $ const True) allRefs))
checkExplicit
:: [DeclarationRef]
-> m Bool
checkExplicit declrefs = do
let idents = ordNub (mapMaybe runDeclRef declrefs)
dctors = mapMaybe (getDctorName <=< disqualifyFor qualifierName) names
usedNames = mapMaybe (matchName (typeForDCtor mni) <=< disqualifyFor qualifierName) names
diff = idents \\ usedNames
didWarn <- case (length diff, length idents) of
(0, _) -> return False
(n, m) | n == m -> unused
_ -> warn (UnusedExplicitImport mni diff qualifierName $ map simplifyTypeRef' allRefs)
didWarn' <- forM (mapMaybe getTypeRef declrefs) $ \(tn, c) -> do
let allCtors = dctorsForType mni tn
unless' (TyName tn `notElem` usedNames) $
case (c, dctors `intersect` allCtors) of
(_, []) | c /= Just [] -> warn (UnusedDctorImport mni tn qualifierName $ map simplifyTypeRef' allRefs)
(Just ctors, dctors') ->
let ddiff = ctors \\ dctors'
in unless' (null ddiff) . warn $ UnusedDctorExplicitImport mni tn ddiff qualifierName $ map simplifyTypeRef' allRefs
_ -> return False
return (didWarn || or didWarn')
where
simplifyTypeRef' :: DeclarationRef -> DeclarationRef
simplifyTypeRef' = simplifyTypeRef (\name -> any (isMatch name) declrefs)
where
isMatch name (TypeRef _ name' Nothing) = name == name'
isMatch _ _ = False
unused :: m Bool
unused = warn (UnusedImport mni)
warn :: SimpleErrorMessage -> m Bool
warn err = tell (errorMessage' ss err) >> return True
unless' :: Bool -> m Bool -> m Bool
unless' False m = m
unless' True _ = return False
allRefs :: [DeclarationRef]
allRefs = findUsedRefs ss env mni qualifierName names
dtys
:: ModuleName
-> M.Map (ProperName 'TypeName) ([ProperName 'ConstructorName], ModuleName)
dtys mn = maybe M.empty exportedTypes $ envModuleExports <$> mn `M.lookup` env
dctorsForType
:: ModuleName
-> ProperName 'TypeName
-> [ProperName 'ConstructorName]
dctorsForType mn tn = maybe [] fst $ tn `M.lookup` dtys mn
typeForDCtor
:: ModuleName
-> ProperName 'ConstructorName
-> Maybe (ProperName 'TypeName)
typeForDCtor mn pn = fst <$> find (elem pn . fst . snd) (M.toList (dtys mn))
findUsedRefs
:: SourceSpan
-> Env
-> ModuleName
-> Maybe ModuleName
-> [Qualified Name]
-> [DeclarationRef]
findUsedRefs ss env mni qn names =
let
classRefs = TypeClassRef ss <$> mapMaybe (getClassName <=< disqualifyFor qn) names
valueRefs = ValueRef ss <$> mapMaybe (getIdentName <=< disqualifyFor qn) names
valueOpRefs = ValueOpRef ss <$> mapMaybe (getValOpName <=< disqualifyFor qn) names
typeOpRefs = TypeOpRef ss <$> mapMaybe (getTypeOpName <=< disqualifyFor qn) names
types = mapMaybe (getTypeName <=< disqualifyFor qn) names
kindRefs = KindRef ss <$> mapMaybe (getKindName <=< disqualifyFor qn) names
dctors = mapMaybe (getDctorName <=< disqualifyFor qn) names
typesWithDctors = reconstructTypeRefs dctors
typesWithoutDctors = filter (`M.notMember` typesWithDctors) types
typesRefs
= map (flip (TypeRef ss) (Just [])) typesWithoutDctors
++ map (\(ty, ds) -> TypeRef ss ty (Just ds)) (M.toList typesWithDctors)
in sortBy compDecRef $ classRefs ++ typeOpRefs ++ typesRefs ++ kindRefs ++ valueRefs ++ valueOpRefs
where
reconstructTypeRefs
:: [ProperName 'ConstructorName]
-> M.Map (ProperName 'TypeName) [ProperName 'ConstructorName]
reconstructTypeRefs = foldr accumDctors M.empty
where
accumDctors dctor =
M.alter (Just . maybe [dctor] (dctor :)) (findTypeForDctor mni dctor)
findTypeForDctor
:: ModuleName
-> ProperName 'ConstructorName
-> ProperName 'TypeName
findTypeForDctor mn dctor =
case mn `M.lookup` env of
Just (_, _, exps) ->
case find (elem dctor . fst . snd) (M.toList (exportedTypes exps)) of
Just (ty, _) -> ty
Nothing -> internalError $ "missing type for data constructor " ++ T.unpack (runProperName dctor) ++ " in findTypeForDctor"
Nothing -> internalError $ "missing module " ++ T.unpack (runModuleName mn) ++ " in findTypeForDctor"
matchName
:: (ProperName 'ConstructorName -> Maybe (ProperName 'TypeName))
-> Name
-> Maybe Name
matchName lookupDc (DctorName x) = TyName <$> lookupDc x
matchName _ ModName{} = Nothing
matchName _ name = Just name
runDeclRef :: DeclarationRef -> Maybe Name
runDeclRef (ValueRef _ ident) = Just $ IdentName ident
runDeclRef (ValueOpRef _ op) = Just $ ValOpName op
runDeclRef (TypeRef _ pn _) = Just $ TyName pn
runDeclRef (TypeOpRef _ op) = Just $ TyOpName op
runDeclRef (TypeClassRef _ pn) = Just $ TyClassName pn
runDeclRef _ = Nothing
checkDuplicateImports
:: MonadWriter MultipleErrors m
=> ModuleName
-> [ImportDef]
-> (ImportDef, ImportDef)
-> m [ImportDef]
checkDuplicateImports mn xs ((_, t1, q1), (pos, t2, q2)) =
if t1 == t2 && q1 == q2
then do
tell . errorMessage' pos $ DuplicateImport mn t2 q2
return $ (pos, t2, q2) : xs
else return xs