module CheckPVP where import qualified ModuleSet import qualified Language.Haskell.Exts.Parser as Parser import qualified Language.Haskell.Exts.Syntax as Syntax import qualified Language.Haskell.Exts.SrcLoc as SrcLoc import Language.Haskell.Exts.Pretty (prettyPrint, ) import qualified Distribution.Package as Pkg import qualified Distribution.Simple.LocalBuildInfo as LBI import qualified Distribution.Simple.PackageIndex as PkgIdx import qualified Distribution.Simple.Configure as Configure import qualified Distribution.Simple.Setup as Setup import qualified Distribution.InstalledPackageInfo as InstPkg import qualified Distribution.ModuleName as DistModuleName import Distribution.Package (PackageName(PackageName), ) import Distribution.Simple.Utils (findFileWithExtension', ) import qualified Distribution.Verbosity as Verbosity import qualified Distribution.Version as Version import qualified Distribution.ReadE as ReadE import Distribution.Version (Version, ) import Distribution.Text (display, ) import qualified System.Environment as Env import qualified System.IO as IO import System.Console.GetOpt (ArgOrder(RequireOrder), OptDescr(Option), ArgDescr(NoArg, ReqArg), getOpt, usageInfo, ) import System.Exit (exitSuccess, exitFailure, ) import System.FilePath ((), ) import Text.Printf (printf, hPrintf, ) import qualified Control.Monad.Exception.Synchronous as Exc import qualified Control.Monad.Trans.Class as MT import qualified Data.NonEmpty as NonEmpty import qualified Data.Foldable as Fold import qualified Data.Monoid.HT as Mn import qualified Data.List.Reverse.StrictSpine as ListRev import qualified Data.List.HT as ListHT import qualified Data.List as List import qualified Data.Map as Map import qualified Data.Set as Set import Control.Monad (when, ) import Control.Functor.HT (void, ) import Data.Maybe (mapMaybe, ) import Data.Set (Set, ) import Data.Foldable (foldMap, forM_, ) data Flags = Flags { flagHelp :: Bool, flagVerbosity :: Verbosity.Verbosity, flagBuildDir :: FilePath, flagClassifyDependencies :: Bool, flagClassifyGrouped :: Bool, flagWarnings :: Bool, flagCheckLibrary, flagCheckExecutables, flagCheckTestSuites, flagCheckBenchmarks :: Bool, flagExcludedModules :: Set Syntax.ModuleName, flagLoadPackageIndex :: Bool, flagCriticalModules :: PkgIdx.PackageIndex -> [DepAttrs] -> ModuleSet.T, flagCriticalModified :: Bool } defaultFlags :: Flags defaultFlags = Flags { flagHelp = False, flagVerbosity = Verbosity.silent, flagBuildDir = Setup.defaultDistPref, flagClassifyDependencies = False, flagClassifyGrouped = False, flagWarnings = True, flagCheckLibrary = True, flagCheckExecutables = True, flagCheckTestSuites = True, flagCheckBenchmarks = True, flagExcludedModules = Set.empty, flagLoadPackageIndex = True, flagCriticalModules = \pkgIdx depAttrs -> ModuleSet.fromSet $ dependentModules pkgIdx depAttrs, flagCriticalModified = False } options :: [OptDescr (Flags -> Exc.Exceptional String Flags)] options = Option ['h'] ["help"] (NoArg (\flags -> return $ flags{flagHelp = True})) "show options" : Option ['v'] ["verbose"] (ReqArg (\str flags -> fmap (\n -> flags{flagVerbosity = n}) $ Exc.fromEither $ ReadE.runReadE Verbosity.flagToVerbosity str) "N") "verbosity level: 0..3" : Option [] ["builddir"] (ReqArg (\str flags -> return $ flags{flagBuildDir = str}) "DIR") (printf "directory to look for package configuration (default %s)" $ flagBuildDir defaultFlags) : Option [] ["classify-dependencies"] (NoArg (\flags -> return $ flags{flagClassifyDependencies = True})) "print diagnostics of version ranges in Build-Depends fields" : Option [] ["disable-warnings"] (NoArg (\flags -> return $ flags{flagWarnings = False})) "suppress warnings" : Option [] ["include-all"] (NoArg (\flags -> if flagCriticalModified flags then Exc.throw "include-all option must be the first amongst module set modifiers" else return $ (modifyFlagCritical (const ModuleSet.full) flags) {flagLoadPackageIndex = False})) "check all imports, ignore package database" : Option [] ["include-import"] (ReqArg (\str flags -> return $ modifyFlagCritical (ModuleSet.insert (Syntax.ModuleName str)) flags) "MODULE") "check import of MODULE" : Option [] ["exclude-import"] (ReqArg (\str flags -> return $ modifyFlagCritical (ModuleSet.delete (Syntax.ModuleName str)) flags) "MODULE") "ignore import of MODULE" : Option [] ["include-dependency"] (ReqArg (\str flags -> return $ modifyFlagCriticalWithPkgIdx True (\pkgIdx -> ModuleSet.insertSet (moduleSetFromPackage pkgIdx str)) flags) "PKG") "check all imports from PKG" : Option [] ["exclude-dependency"] (ReqArg (\str flags -> return $ modifyFlagCriticalWithPkgIdx True (\pkgIdx -> ModuleSet.deleteSet (moduleSetFromPackage pkgIdx str)) flags) "PKG") "ignore all imports from PKG" : Option [] ["exclude-module"] (ReqArg (\str flags -> return $ flags{flagExcludedModules = Set.insert (Syntax.ModuleName str) $ flagExcludedModules flags}) "MODULE") "do not check MODULE" : Option [] ["exclude-library"] (NoArg (\flags -> return $ flags{flagCheckLibrary = False})) "do not check library" : Option [] ["exclude-executables"] (NoArg (\flags -> return $ flags{flagCheckExecutables = False})) "do not check executables" : Option [] ["exclude-testsuites"] (NoArg (\flags -> return $ flags{flagCheckTestSuites = False})) "do not check testsuites" : Option [] ["exclude-benchmarks"] (NoArg (\flags -> return $ flags{flagCheckBenchmarks = False})) "do not check benchmarks" : [] modifyFlagCritical :: (ModuleSet.T -> ModuleSet.T) -> Flags -> Flags modifyFlagCritical modify = modifyFlagCriticalWithPkgIdx False (const modify) modifyFlagCriticalWithPkgIdx :: Bool -> (PkgIdx.PackageIndex -> ModuleSet.T -> ModuleSet.T) -> Flags -> Flags modifyFlagCriticalWithPkgIdx loadPkgIdx modify flags = flags { flagCriticalModules = \pkgIdx depAttrs -> modify pkgIdx $ flagCriticalModules flags pkgIdx depAttrs, flagCriticalModified = True, flagLoadPackageIndex = loadPkgIdx || flagLoadPackageIndex flags } moduleSetFromPackage :: PkgIdx.PackageIndex -> String -> Set Syntax.ModuleName moduleSetFromPackage pkgIdx name = lookupPkgModuleSet pkgIdx (Pkg.PackageName name) getFlags :: Exc.ExceptionalT String IO Flags getFlags = do argv <- MT.lift Env.getArgs let (opts, args, errors) = getOpt RequireOrder options argv when (not (null errors)) $ Exc.throwT $ concat $ errors when (not (null args)) $ Exc.throwT $ "I have no usage for the arguments " ++ show args flags <- Exc.ExceptionalT $ return $ foldl (>>=) (return defaultFlags) opts when (flagHelp flags) (MT.lift $ Env.getProgName >>= \programName -> putStrLn (usageInfo ("Usage: " ++ programName ++ " [OPTIONS]") options) >> exitSuccess) return flags data CheckFlags = CheckFlags { criticalModule :: Syntax.ModuleName -> Bool, showWarnings :: Bool } makeCheckFlags :: Flags -> [DepAttrs] -> IO CheckFlags makeCheckFlags flags classified = do pkgIdx <- if flagLoadPackageIndex flags then loadPackageIndex (flagBuildDir flags) else return $ error "no package index loaded" let modIdx = flagCriticalModules flags pkgIdx classified return $ CheckFlags { criticalModule = flip ModuleSet.member modIdx, showWarnings = flagWarnings flags } findMainModule :: [FilePath] -> FilePath -> IO (Maybe (FilePath, FilePath)) findMainModule sourceDirs path = do maybeMainPath <- findFileWithExtension' [""] sourceDirs path case maybeMainPath of Nothing -> do void $ hPrintf IO.stderr "main module %s not found" path return Nothing Just mainPath -> return $ Just mainPath loadPackageIndex :: FilePath -> IO PkgIdx.PackageIndex loadPackageIndex buildDir = fmap LBI.installedPkgs $ Configure.getPersistBuildConfig buildDir dependentModules :: PkgIdx.PackageIndex -> [DepAttrs] -> Set Syntax.ModuleName dependentModules pkgIdx = foldMap (lookupPkgModuleSet pkgIdx) . map depPkgName . filter (\dep -> case depUpperBoundClass dep of Open -> True Lax _ -> True Generous _ _ -> True Tight _ -> False) lookupPkgModuleSet :: PkgIdx.PackageIndex -> Pkg.PackageName -> Set Syntax.ModuleName lookupPkgModuleSet pkgIdx name = Set.fromList $ map syntaxFromDistModuleName $ concatMap InstPkg.exposedModules $ concatMap snd $ PkgIdx.lookupPackageName pkgIdx name excludeModules :: Set Syntax.ModuleName -> [DistModuleName.ModuleName] -> [DistModuleName.ModuleName] excludeModules set = filter (not . flip Set.member set . syntaxFromDistModuleName) syntaxFromDistModuleName :: DistModuleName.ModuleName -> Syntax.ModuleName syntaxFromDistModuleName = Syntax.ModuleName . List.intercalate "." . DistModuleName.components checkModules :: CheckFlags -> [(FilePath, FilePath)] -> IO () checkModules flags paths = forM_ paths $ \(dir,path) -> do let dirPath = dir path txt <- readFile dirPath case Parser.parseWithMode (Parser.defaultParseMode {Parser.parseFilename = dirPath}) txt of Parser.ParseFailed loc msg -> hPrintf IO.stderr "\n%s\n %s\n" (formatSrcLoc loc) msg Parser.ParseOk (Syntax.Module _loc _name _pragma _warn _export imports _decls) -> checkImports flags imports checkImports :: CheckFlags -> [Syntax.ImportDecl] -> IO () checkImports flags imports = do forM_ (filter (criticalModule flags . Syntax.importModule) imports) $ \imp -> do let problems = Mn.when (not $ strictImport imp) ["lax import"] ++ Mn.when (showWarnings flags) (Mn.when (Fold.any fst $ Syntax.importSpecs imp) ["Warning: hiding import"] ++ (flip map (implicitSpecs imp) $ \name -> "Warning: open constructor or method list for " ++ prettyPrint name)) when (not $ null problems) $ do void $ printf "\n%s:\n Problems encountered in import of %s:\n" (formatSrcLoc $ Syntax.importLoc imp) (unpackModuleName $ Syntax.importModule imp) putStr $ unlines $ map (replicate 8 ' ' ++) problems let conflictAbbrevs = Map.toAscList $ Map.filter (\mods -> Set.size mods >= 2 && (not $ Set.null $ Set.filter (criticalModule flags . snd) mods)) $ Map.fromListWith Set.union $ mapMaybe (\imp -> fmap (\impAs -> (impAs, Set.singleton (Syntax.importLoc imp, Syntax.importModule imp))) (Syntax.importAs imp)) $ imports forM_ conflictAbbrevs $ \(impAs, conflicts) -> do void $ printf "\nMultiple modules imported with abbreviation \"%s\":\n" (unpackModuleName impAs) forM_ (Set.toAscList conflicts) $ \(loc, modu) -> printf "\n%s:\n conflicting import of %s\n" (formatSrcLoc loc) (unpackModuleName modu) formatSrcLoc :: SrcLoc.SrcLoc -> String formatSrcLoc loc = printf "%s:%d:%d" (SrcLoc.srcFilename loc) (SrcLoc.srcLine loc) (SrcLoc.srcColumn loc) unpackModuleName :: Syntax.ModuleName -> String unpackModuleName (Syntax.ModuleName str) = str strictImport :: Syntax.ImportDecl -> Bool strictImport imp = Syntax.importQualified imp || Fold.any (\(hide, _specs) -> not hide) (Syntax.importSpecs imp) implicitSpecs :: Syntax.ImportDecl -> [Syntax.Name] implicitSpecs imp = foldMap (\(hide, specs) -> if hide then [] else mapMaybe maybeImplicitSpec specs) (Syntax.importSpecs imp) maybeImplicitSpec :: Syntax.ImportSpec -> Maybe Syntax.Name maybeImplicitSpec spec = case spec of Syntax.IThingAll name -> Just name Syntax.IThingWith _ _ -> Nothing Syntax.IAbs _ -> Nothing Syntax.IVar _ -> Nothing data DepAttrs = DepAttrs { depPkgName :: PackageName, depMissingUpperBounds :: [Version.LowerBound], depInclusiveUpperBounds :: [Version], depUpperBoundClass :: BoundClass } data BoundClass = Open | Lax Int | Generous Int Int | Tight [Int] printUpperBoundDiagnostics :: Flags -> DepAttrs -> IO () printUpperBoundDiagnostics flags depAttrs = let warn = (,) True info = (,) False msgs = (flip map (depMissingUpperBounds depAttrs) $ \(Version.LowerBound ver typ) -> warn $ printf "missing upper bound associated with lower bound \"%s\"" $ display $ case typ of Version.InclusiveBound -> Version.orLaterVersion ver Version.ExclusiveBound -> Version.laterVersion ver) ++ (flip map (depInclusiveUpperBounds depAttrs) $ \uppBnd -> warn $ printf "found inclusive upper bound %s" $ display uppBnd) ++ case depUpperBoundClass depAttrs of Open -> [] Lax x -> [warn $ printf "upper bound %d is too lax" x] Generous x y -> [info $ printf "upper bound %d.%d requires strict imports" x y] Tight xs -> [info $ printf "upper bound %s is tight" $ List.intercalate "." $ map show xs] filteredMsgs = map snd $ if flagClassifyDependencies flags then msgs else filter fst msgs in if flagClassifyGrouped flags then when (not $ null filteredMsgs) $ putStr $ unlines $ "" : unpackPkgName (depPkgName depAttrs) : map (replicate 4 ' ' ++) filteredMsgs else forM_ filteredMsgs $ \msg -> printf "%s: %s\n" (unpackPkgName $ depPkgName depAttrs) msg classifyDependencies :: [Pkg.Dependency] -> [DepAttrs] classifyDependencies deps = flip map deps $ \(Pkg.Dependency dependName rng) -> let intervals = Version.asVersionIntervals rng maybeUpperBound Version.NoUpperBound = Nothing maybeUpperBound (Version.UpperBound ver bnd) = Just (ver, bnd) isExclusiveBound Version.ExclusiveBound = True isExclusiveBound Version.InclusiveBound = False (upperBounds, noUpperBounds) = ListHT.partitionMaybe (maybeUpperBound . snd) intervals (exclusiveUpperBounds, inclusiveUpperBounds) = ListHT.partition (isExclusiveBound . snd) upperBounds branches = case NonEmpty.fetch exclusiveUpperBounds of Nothing -> [] Just xs -> NonEmpty.minimumKey length $ fmap (ListRev.dropWhile (0==) . Version.versionBranch . fst) xs boundClass = case branches of [] -> Open [x] -> Lax x [x,y] -> Generous x y _ -> Tight branches in DepAttrs { depPkgName = dependName, depMissingUpperBounds = map fst noUpperBounds, depInclusiveUpperBounds = map fst inclusiveUpperBounds, depUpperBoundClass = boundClass } exitFailureMsg :: String -> IO () exitFailureMsg msg = do IO.hPutStrLn IO.stderr $ "Aborted: " ++ msg exitFailure unpackPkgName :: PackageName -> String unpackPkgName (PackageName name) = name