packages feed

package-vt-0.1.3.3: package-vt.hs

{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-} 
module Main ( main ) where

import Control.Applicative ( (<$>) )
import Control.Exception
import Data.Algorithm.Diff ( getGroupedDiff, DI(..) )
import Data.Maybe ( catMaybes, fromJust, fromMaybe )
import Data.Monoid
import Data.Data
import Data.Typeable
import Data.List ( sort, group )
import Text.Printf ( printf )
import Language.Haskell.Exts -- too many things to list
import System.Environment ( getArgs )
import System.FilePath

-- Cabal stuff

import Distribution.PackageDescription
import Distribution.PackageDescription.Parse ( readPackageDescription )
import Distribution.PackageDescription.Configuration
import qualified Distribution.Verbosity as Verbosity
import qualified Distribution.ModuleName as ModuleName


data VersionChange = NoChange
                   | ChangeABC { info, details :: String }
                   | ChangeAB { info, details :: String }
                     deriving (Eq,Show,Read,Ord)

data FileType = HSFile String
              | CabalFile String
                     deriving (Ord,Eq,Show,Read)

-- | Custom datatype for parse exceptions
data ParseError = ParseError String deriving (Ord,Eq,Data,Show,Read,Typeable)
instance Exception ParseError

-- | Monoid instance for VersionChange describes which change has stronger priority.
instance Monoid VersionChange where
    mempty = NoChange

    mappend x y = if val x <= val y
                   then y 
                   else x 
                       where
                         val NoChange{} = 0
                         val ChangeABC{} = 1
                         val ChangeAB{} = 2

main :: IO ()
main = do
  (f1:f2:_) <- getArgs
  let files = map chooseFileType [f1,f2]
      chooseFileType fn = case takeExtension fn of
                            ".hs" -> HSFile fn
                            ".lhs" -> HSFile fn
                            ".cabal" -> CabalFile fn
                            ext -> error $ printf "Bad file extension: %s" (show ext)

  r <- case files of
    [HSFile f1, HSFile f2] -> catMaybes . (:[]) <$> compareHSFiles [] f1 f2
    [CabalFile c1, CabalFile c2] -> compareCabalFiles c1 c2
    _ -> error $ printf "Unsupported filetype combination: %s" (show files)
  
  putStrLn "Single changes:"
  putStrLn . concatMap pprintVC $ sort r
  putStrLn "Biggest change comes from:"
  putStrLn . pprintVC . mconcat $ r

-- | pretty print 'VersionChange'

pprintVC :: VersionChange -> String
pprintVC NoChange = "NoChange : no changes suggested."
pprintVC (ChangeABC info details) = printf "ChangeABC : %s\n---\n%s\n---" info details
pprintVC (ChangeAB info details) = printf "ChangeAB : %s\n---\n%s\n---" info details

-- |  Predicates for matching with certain diff cases
isFirst, isSecond, isBoth :: (DI,a) -> Bool
isFirst  = (==F) . fst
isSecond = (==S) . fst
isBoth   = (==B) . fst

-- | Utility function used in compareCabalFiles and compareHSFiles
diffGetVC :: (Eq a, Show a) => String -> String -> [a] -> [a] -> (Maybe VersionChange, [(DI,[a])])
diffGetVC info1 info2 xs ys = 
    let diff = getGroupedDiff xs ys
        aux pred = unlines . map (unlines . map show . snd) . filter pred $ diff
        r | any isFirst diff = Just $ ChangeAB info1 (aux isFirst)
          | any isSecond diff = Just $ ChangeABC info2 (aux isSecond)
          | otherwise = Nothing
    in
      (r,diff)

-- | nubOrd
nubOrd :: (Ord a) => [a] -> [a]
nubOrd = map head . group . sort 

-- | Compare two package descriptions from 
compareCabalFiles :: FilePath -> FilePath -> IO [VersionChange]
compareCabalFiles fOld fNew = do
  let readParse :: FilePath -> IO (FilePath,[FilePath])
      readParse fn = do
        cabal <- flattenPackageDescription <$> readPackageDescription Verbosity.normal fn
        let --
            modules = map ModuleName.toFilePath . sort . libModules $ cabal
            sourceDirs = (nubOrd . sort . concatMap hsSourceDirs . allBuildInfo $ cabal) ++ ["."]
            -- XXX: use multiple source dirs
        return (head sourceDirs,modules)

  (dirO, modsOld) <- readParse fOld
  (dirN, modsNew) <- readParse fNew
  let (vc,diff) = diffGetVC "Module(s) removed/renamed." 
                              ("Module(s) added." ++ 
                               "(Consider AB change if new modules are likely to cause name collisions)")
                               modsOld
                               modsNew
                               

  hsDiffs <- sequence [ compareHSFiles (glasgowExts ++ [CPP,MultiParamTypeClasses])
                                       (dropFileName fOld </> dirO </> f <.> "hs") 
                                       (dropFileName fNew </> dirN </> f <.> "hs") | 
                                       f <- concatMap snd . filter isBoth $ diff]
  return (sort . catMaybes $ vc : hsDiffs)

-- | Compare two .[l]hs files. Return any changes found. 
compareHSFiles :: [Extension] -> FilePath -> FilePath -> IO (Maybe VersionChange)
compareHSFiles exts fOldPath fNewPath = do
  let onIOError :: IOException -> IO (Maybe VersionChange)
      onIOError e = do
        putStrLn (printf "IO Exception: %s" (show (e :: IOException)))
        putStrLn (printf "Skipping modules %s, %s" (show fOldPath) (show fNewPath))
        return Nothing

      onParseError :: ParseError -> IO (Maybe VersionChange)
      onParseError (ParseError str) = do
        putStrLn str
        return Nothing
  handle onParseError $
   handle onIOError $  
--  handle (\NonTermination -> error "Non termination") $
    do
     let fromParseResult :: ((ParseResult a) -> a) -> (ParseResult a) -> a
         fromParseResult onErr (ParseOk a) = a
         fromParseResult onErr err@(ParseFailed _ _) = onErr err
         readAndParse fn = fromParseResult (\(ParseFailed src err) -> throw (ParseError (err ++ " : " ++ show src)))  <$> parseFileWithExts exts fn
     fOld@(Module srcLoc1 modName1 optionPragmas1 warningText1 exportSpec1 importDecl1 decl1) <- readAndParse fOldPath
     fNew@(Module srcLoc2 modName2 optionPragmas2 warningText2 exportSpec2 importDecl2 decl2) <- readAndParse fNewPath
   
     let info1 = "One or more entity removed/renamed from module " ++ show modName1
         info2 = "Entity added to module " ++ show modName1
         diff = if (exportSpec1 == Nothing) && (exportSpec2 == Nothing) then Nothing else 
                       fst $ diffGetVC info1 info2
                              (fromMaybe (error "exportSpec1 empty") exportSpec1)
                              (fromMaybe (error "exportSpec2 empty") exportSpec2)
   
     return diff