packages feed

check-pvp-0.0.3: src/CheckPVP.hs

module CheckPVP where

import qualified ModuleSet
import ModuleSet (ModuleName)

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, unPackageName, )
import Distribution.Simple.PackageIndex (InstalledPackageIndex, )
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.Applicative ((<$>), )
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 ModuleName,
      flagLoadPackageIndex :: Bool,
      flagCriticalModules ::
         InstalledPackageIndex -> [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 (moduleName str)) flags)
         "MODULE")
      "check import of MODULE" :
   Option [] ["exclude-import"]
      (ReqArg
         (\str flags -> return $
            modifyFlagCritical (ModuleSet.delete (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 (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 ->
   (InstalledPackageIndex -> 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 :: InstalledPackageIndex -> String -> Set ModuleName
moduleSetFromPackage pkgIdx name =
   lookupPkgModuleSet pkgIdx $ Pkg.mkPackageName 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 :: 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 InstalledPackageIndex
loadPackageIndex buildDir =
   fmap LBI.installedPkgs $
   Configure.getPersistBuildConfig buildDir

dependentModules ::
   InstalledPackageIndex -> [DepAttrs] -> Set ModuleName
dependentModules pkgIdx =
   foldMap (lookupPkgModuleSet pkgIdx) .
   map depPkgName .
   filter
      (\dep ->
         case depUpperBoundClass dep of
            Open -> True
            Lax _ -> True
            Generous _ _ -> True
            Tight _ -> False)

lookupPkgModuleSet ::
   InstalledPackageIndex -> Pkg.PackageName -> Set ModuleName
lookupPkgModuleSet pkgIdx name =
   Set.fromList $
   map (syntaxFromDistModuleName . InstPkg.exposedName) $
   concatMap InstPkg.exposedModules $
   concatMap snd $
   PkgIdx.lookupPackageName pkgIdx name

excludeModules ::
   Set ModuleName ->
   [DistModuleName.ModuleName] -> [DistModuleName.ModuleName]
excludeModules set =
   filter (not . flip Set.member set . syntaxFromDistModuleName)

moduleName :: String -> Syntax.ModuleName ()
moduleName = Syntax.ModuleName ()

syntaxFromDistModuleName :: DistModuleName.ModuleName -> ModuleName
syntaxFromDistModuleName =
   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 modu ->
            case modu of
               Syntax.Module _loc _head _pragma imports _decls ->
                  checkImports flags imports
               Syntax.XmlPage _loc _name _pragma _xname _xattr _exp0 _exp1 ->
                  hPrintf IO.stderr "\n%s: Don't how to process XML.\n" path
               Syntax.XmlHybrid
                     _loc _head _pragma imports _decls
                     _xname _xattr _exp0 _exp1 ->
                  checkImports flags imports

checkImports :: CheckFlags -> [Syntax.ImportDecl SrcLoc.SrcSpanInfo] -> IO ()
checkImports flags imports = do
   forM_
      (filter
         (criticalModule flags . fmap (const ()) . Syntax.importModule)
         imports) $
            \imp -> do
      let problems =
             Mn.when (not $ strictImport imp) ["lax import"]
             ++
             Mn.when (showWarnings flags)
                (Mn.when
                    (Fold.any hidingImport $ 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 $ SrcLoc.getPointLoc $ Syntax.importAnn 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.importAnn imp,
                           const () <$> 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 $ SrcLoc.getPointLoc 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 l -> String
unpackModuleName (Syntax.ModuleName _ str) = str

hidingImport :: Syntax.ImportSpecList l -> Bool
hidingImport (Syntax.ImportSpecList _ hide _specs) = hide

strictImport :: Syntax.ImportDecl l -> Bool
strictImport imp =
   Syntax.importQualified imp
   ||
   Fold.any (not . hidingImport) (Syntax.importSpecs imp)

implicitSpecs :: Syntax.ImportDecl l -> [Syntax.Name l]
implicitSpecs imp =
   foldMap
      (\(Syntax.ImportSpecList _ hide specs) ->
         if hide then [] else mapMaybe maybeImplicitSpec specs)
      (Syntax.importSpecs imp)

maybeImplicitSpec :: Syntax.ImportSpec l -> Maybe (Syntax.Name l)
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 $
                    "" :
                    unPackageName (depPkgName depAttrs) :
                    map (replicate 4 ' ' ++) filteredMsgs
         else forM_ filteredMsgs $ \msg ->
                 printf "%s: %s\n" (unPackageName $ 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.versionNumbers . 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