check-pvp-0.0.1: src/Main.hs
module Main (main) where
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.PackageDescription.Configuration as Config
import qualified Distribution.PackageDescription as P
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.PackageDescription.Parse (readPackageDescription, )
import Distribution.Package (PackageName(PackageName), )
import Distribution.Simple.Utils
(defaultPackageDesc, findModuleFiles, findFileWithExtension',
notice, )
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.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, maybeToList, )
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,
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 $ 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 $ ComplementSet Set.empty) flags)
{flagLoadPackageIndex = False}))
"check all imports, ignore package database" :
Option [] ["include-import"]
(ReqArg
(\str flags -> return $
modifyFlagCritical (insertModule (Syntax.ModuleName str)) flags)
"MODULE")
"check import of MODULE" :
Option [] ["exclude-import"]
(ReqArg
(\str flags -> return $
modifyFlagCritical (deleteModule (Syntax.ModuleName str)) flags)
"MODULE")
"ignore import of MODULE" :
Option [] ["include-dependency"]
(ReqArg
(\str flags -> return $
modifyFlagCriticalWithPkgIdx True
(\pkgIdx -> insertModuleSet (moduleSetFromPackage pkgIdx str)) flags)
"PKG")
"check all imports from PKG" :
Option [] ["exclude-dependency"]
(ReqArg
(\str flags -> return $
modifyFlagCriticalWithPkgIdx True
(\pkgIdx -> deleteModuleSet (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 -> ModuleSet) -> Flags -> Flags
modifyFlagCritical modify =
modifyFlagCriticalWithPkgIdx False (const modify)
modifyFlagCriticalWithPkgIdx ::
Bool ->
(PkgIdx.PackageIndex -> ModuleSet -> ModuleSet) ->
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)
data ModuleSet =
ModuleSet (Set Syntax.ModuleName)
| ComplementSet (Set Syntax.ModuleName)
memberModule :: Syntax.ModuleName -> ModuleSet -> Bool
memberModule modu (ModuleSet set) = Set.member modu set
memberModule modu (ComplementSet set) = not $ Set.member modu set
insertModule :: Syntax.ModuleName -> ModuleSet -> ModuleSet
insertModule modu (ModuleSet set) = ModuleSet $ Set.insert modu set
insertModule modu (ComplementSet set) = ComplementSet $ Set.delete modu set
deleteModule :: Syntax.ModuleName -> ModuleSet -> ModuleSet
deleteModule modu (ModuleSet set) = ModuleSet $ Set.delete modu set
deleteModule modu (ComplementSet set) = ComplementSet $ Set.insert modu set
insertModuleSet :: Set Syntax.ModuleName -> ModuleSet -> ModuleSet
insertModuleSet new (ModuleSet set) = ModuleSet $ Set.union new set
insertModuleSet new (ComplementSet set) = ComplementSet $ Set.difference set new
deleteModuleSet :: Set Syntax.ModuleName -> ModuleSet -> ModuleSet
deleteModuleSet new (ModuleSet set) = ModuleSet $ Set.difference set new
deleteModuleSet new (ComplementSet set) = ComplementSet $ Set.union new set
main :: IO ()
main =
Exc.resolveT exitFailureMsg $ 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)
MT.lift $ run flags
data CheckFlags =
CheckFlags {
criticalModule :: Syntax.ModuleName -> Bool,
showWarnings :: Bool
}
run :: Flags -> IO ()
run flags = do
let verbosity = flagVerbosity flags
pdfile <- defaultPackageDesc verbosity
desc <-
fmap Config.flattenPackageDescription $
readPackageDescription verbosity pdfile
notice verbosity "Package description"
let classified = classifyDependencies $ P.buildDepends desc
mapM_ (printUpperBoundDiagnostics flags) classified
pkgIdx <-
if flagLoadPackageIndex flags
then loadPackageIndex (flagBuildDir flags)
else return $ error "no package index loaded"
let modIdx = flagCriticalModules flags pkgIdx classified
let checkFlags =
CheckFlags {
criticalModule = flip memberModule modIdx,
showWarnings = flagWarnings flags
}
when (flagCheckLibrary flags) $ do
notice verbosity "Library"
P.withLib desc $ \lib -> do
let bi = P.libBuildInfo lib
modules =
excludeModules (flagExcludedModules flags) $
P.exposedModules lib
++
P.otherModules bi
sourceDirs = P.hsSourceDirs bi
checkModules checkFlags =<< findModuleFiles sourceDirs ["hs"] modules
when (flagCheckExecutables flags) $ do
notice verbosity "Executables"
P.withExe desc $ \exe -> do
let name = P.exeName exe
notice verbosity name
let bi = P.buildInfo exe
modules =
excludeModules (flagExcludedModules flags) $
P.otherModules bi
sourceDirs = P.hsSourceDirs bi
mainPath <- findMainModule sourceDirs $ P.modulePath exe
paths <- findModuleFiles sourceDirs ["hs"] modules
checkModules checkFlags $ maybeToList mainPath ++ paths
when (flagCheckTestSuites flags) $ do
notice verbosity "Test-Suites"
P.withTest desc $ \exe -> do
let name = P.testName exe
notice verbosity name
let bi = P.testBuildInfo exe
modules =
excludeModules (flagExcludedModules flags) $
P.otherModules bi
sourceDirs = P.hsSourceDirs bi
paths <- findModuleFiles sourceDirs ["hs"] modules
mainPath <-
case P.testInterface exe of
P.TestSuiteExeV10 _ path -> findMainModule sourceDirs path
_ -> return Nothing
checkModules checkFlags $ maybeToList mainPath ++ paths
when (flagCheckBenchmarks flags) $ do
notice verbosity "Benchmarks"
P.withBenchmark desc $ \exe -> do
let name = P.benchmarkName exe
notice verbosity name
let bi = P.benchmarkBuildInfo exe
modules =
excludeModules (flagExcludedModules flags) $
P.otherModules bi
sourceDirs = P.hsSourceDirs bi
paths <- findModuleFiles sourceDirs ["hs"] modules
mainPath <-
case P.benchmarkInterface exe of
P.BenchmarkExeV10 _ path -> findMainModule sourceDirs path
_ -> return Nothing
checkModules checkFlags $ maybeToList mainPath ++ paths
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 (ListHT.dropWhileRev (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