{-# LANGUAGE RecordWildCards, ScopedTypeVariables #-} module Check(check) where import Hi import Cabal import Util import Data.Maybe import Data.List.Extra import Data.Tuple.Extra import System.Info.Extra import qualified Data.HashSet as Set import qualified Data.HashMap.Strict as Map import Warning data S = S {pkg :: PackageName ,hi :: HiKey -> Hi ,sections :: [(CabalSection, ([HiKey], [HiKey], [ModuleName]))] } check :: (HiKey -> Hi) -> PackageName -> [(CabalSection, ([HiKey], [HiKey], [ModuleName]))] -> [Warning] check hi pkg sections2 = map (\x -> x{warningSections = sort $ warningSections x}) $ warnReusedModuleBetweenSections s ++ warnRedundantPackageDependency s ++ warnIncorrectOtherModules s ++ warnUnusedExport s ++ warnNotCompiled s ++ warnUnusedImport s where s = S{..} sections = map (second $ \(a,b,c) -> let aa = nubOrd a in (aa,nubOrd b \\ aa,c)) sections2 warnNotCompiled :: S -> [Warning] warnNotCompiled S{..} = [ Warning pkg [cabalSectionType s] "Module not compiled" Nothing (Just m) Nothing | (s, (_, _, missing)) <- sections, m <- missing] warnReusedModuleBetweenSections :: S -> [Warning] warnReusedModuleBetweenSections S{..} = [ Warning pkg ss "Module reused between components" Nothing (Just $ hiModuleName $ hi m) Nothing | (m, ss) <- groupSort [(x, cabalSectionType c) | (c, (x1,x2,_)) <- sections, x <- x1++x2] , length ss > 1] warnRedundantPackageDependency :: S -> [Warning] warnRedundantPackageDependency S{..} = [ Warning pkg [cabalSectionType] "Redundant build-depends entry" (Just p) Nothing Nothing | (CabalSection{..}, (x1,x2,_)) <- sections , let usedPackages = Set.unions $ map (Set.map fst . hiImportPackageModule . hi) $ x1 ++ x2 , not $ "" `Set.member` usedPackages -- Sometimes we don't get the package name at all, e.g. https://gitlab.haskell.org/ghc/ghc/issues/16886 , p <- Set.toList $ Set.fromList cabalPackages `Set.difference` usedPackages , p /= if isWindows then "unix" else "Win32" -- ignore packages that must be conditional on the other platform , p /= "semigroups" -- ignore packages that are often conditional , p /= "base" -- used by Paths_ modules which we have thrown away by this point ] warnIncorrectOtherModules :: S -> [Warning] warnIncorrectOtherModules S{..} = concat [ [Warning pkg [cabalSectionType] "Missing other-modules entry" Nothing (Just m) Nothing | m <- Set.toList missing] ++ [Warning pkg [cabalSectionType] "Excessive other-modules entry" Nothing (Just m) Nothing | m <- Set.toList excessive] | (CabalSection{..}, (external, internal,_)) <- sections , let imports = Map.fromList [(hiModuleName, hiImportModule) | Hi{..} <- map hi $ external ++ internal] , let missing = Set.filter (not . isPathsModule) $ Set.unions (Map.elems imports) `Set.difference` Set.fromList (Map.keys imports) , let excessive = Set.fromList (map (hiModuleName . hi) internal) `Set.difference` reachable (\k -> maybe [] Set.toList $ Map.lookup k imports) (map (hiModuleName . hi) external) ] -- Primarily looking for import Foo() where Foo is not an orphan warnUnusedImport :: S -> [Warning] warnUnusedImport S{..} = [ Warning pkg [cabalSectionType] "Unused import" Nothing (Just $ hiModuleName mod) (Just $ hiModuleName imp) | (CabalSection{..}, (external, internal,_)) <- sections , let mods = Map.fromList $ map ((hiModuleName &&& id) . hi) $ external ++ internal , mod <- Map.elems mods , imp <- mapMaybe (`Map.lookup` mods) $ Set.toList $ hiImportModule mod `Set.difference` (Set.map identModule (hiImportIdent mod) `Set.union` hiImportOrphan mod) , Set.null $ hiImportIdent mod `Set.intersection` hiExportIdent imp -- reexporting for someone else , Set.null $ Set.map snd (hiImportPackageModule mod) `Set.intersection` Set.map identModule (hiExportIdent imp) -- reexporting for another package , Set.null $ Set.map identModule (Set.filter (isHaskellCtor . identName) $ hiExportIdent imp) `Set.difference` Set.insert (hiModuleName imp) (hiImportModule imp) -- reexport a type for another package, see #15 ] warnUnusedExport :: S -> [Warning] warnUnusedExport S{..} = [ Warning pkg ss "Weeds exported" Nothing (Just $ hiModuleName $ hi m) (Just i) | (m,(ss,is)) <- Map.toList unused, i <- Set.toList is] where unionsWith f = foldr (Map.unionWith f) Map.empty -- important: for an identifer to be unused, it must be unused in all sections that use that key unused = unionsWith (\(s1,i1) (s2,i2) -> (s1++s2, i1 `Set.intersection` i2)) [ Map.fromList [(k, ([cabalSectionType], Set.fromList $ Map.lookupDefault [] (hiModuleName $ hi k) bad)) | k <- internal ++ external] | (CabalSection{..}, (external, internal,_)) <- sections , let bad = Map.fromListWith (++) $ map (identModule &&& return . identName) $ notUsedOrExposed (map hi external) (map hi internal)] notUsedOrExposed :: [Hi] -> [Hi] -> [Ident] notUsedOrExposed external internal = Set.toList $ privateAPI `Set.difference` Set.unions [publicAPI,supported,usedAnywhere] where modules = Map.fromList [(hiModuleName x, x) | x <- external ++ internal] -- things exported from this package publicAPI = Set.unions $ map hiExportIdent external -- Types that are required to define things that are public supported = Set.unions [ Map.lookupDefault Set.empty x hiSignatures | (m, xs) <- groupSort $ map (identModule &&& identName) $ Set.toList $ Set.union publicAPI usedAnywhere , Just Hi{..} <- [Map.lookup m modules], x <- xs] -- things that are defined in other modules and exported -- (ignoring field name since they provide handy documentation) privateAPI = Set.unions [ Set.filter ((==) hiModuleName . identModule) $ hiExportIdent `Set.difference` hiFieldName | Hi{..} <- internal] -- things that are used anywhere, if someone imports and exports something -- assume that isn't also a use (find some redundant warnings) usedAnywhere = Set.unions [ hiImportIdent `Set.difference` hiExportIdent | Hi{..} <- external ++ internal]